| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- import {InvalidArgumentError} from '@e22m4u/js-format';
- import {validateProjectionSchema} from './validate-projection-schema.js';
- /**
- * Project data.
- *
- * @param {object|Function|string} schemaOrFactory
- * @param {*} data
- * @param {object} [options]
- * @returns {*}
- */
- export function projectData(schemaOrFactory, data, options) {
- // schemaOrFactory
- if (
- !schemaOrFactory ||
- (typeof schemaOrFactory !== 'object' &&
- typeof schemaOrFactory !== 'function' &&
- typeof schemaOrFactory !== 'string') ||
- Array.isArray(schemaOrFactory)
- ) {
- throw new InvalidArgumentError(
- 'Projection schema must be an Object, a Function ' +
- 'or a non-empty String, but %v was given.',
- schemaOrFactory,
- );
- }
- // options
- if (options !== undefined) {
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
- throw new InvalidArgumentError(
- 'Parameter "options" must be an Object, but %v was given.',
- options,
- );
- }
- // options.strict
- if (options.strict !== undefined && typeof options.strict !== 'boolean') {
- throw new InvalidArgumentError(
- 'Option "strict" must be a Boolean, but %v was given.',
- options.strict,
- );
- }
- // options.scope
- if (
- options.scope !== undefined &&
- (!options.scope || typeof options.scope !== 'string')
- ) {
- throw new InvalidArgumentError(
- 'Option "scope" must be a non-empty String, but %v was given.',
- options.scope,
- );
- }
- // options.resolver
- if (
- options.resolver !== undefined &&
- typeof options.resolver !== 'function'
- ) {
- throw new InvalidArgumentError(
- 'Option "resolver" must be a Function, but %v was given.',
- options.resolver,
- );
- }
- }
- const strict = Boolean(options && options.strict);
- const scope = (options && options.scope) || undefined;
- // если вместо схемы передана фабрика,
- // то извлекается фабричное значение
- let schemaOrName = schemaOrFactory;
- if (typeof schemaOrFactory === 'function') {
- schemaOrName = schemaOrFactory();
- // если фабричное значение не является объектом
- // или строкой, то выбрасывается ошибка
- if (
- !schemaOrName ||
- (typeof schemaOrName !== 'object' && typeof schemaOrName !== 'string') ||
- Array.isArray(schemaOrName)
- ) {
- throw new InvalidArgumentError(
- 'Projection schema factory must return an Object ' +
- 'or a non-empty String, but %v was given.',
- schemaOrName,
- );
- }
- }
- // если вместо схемы передана строка,
- // то строка передается в разрешающую функцию
- let schema = schemaOrName;
- if (schemaOrName && typeof schemaOrName === 'string') {
- // если разрешающая функция не определена,
- // то выбрасывается ошибка
- if (!options || !options.resolver) {
- throw new InvalidArgumentError(
- 'Unable to resolve the projection schema %v ' +
- 'without a provided resolver.',
- schemaOrName,
- );
- }
- schema = options.resolver(schemaOrName);
- // если не удалось извлечь схему проекции,
- // то выбрасывается ошибка
- if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
- throw new InvalidArgumentError(
- 'Projection schema resolver must return an Object, but %v was given.',
- schema,
- );
- }
- }
- // валидация полученной схемы проекции
- // без проверки вложенных схем (shallowMode)
- validateProjectionSchema(schema, true);
- // если данные не являются объектом (null, undefined, примитив),
- // то значение возвращается без изменений
- if (data === null || typeof data !== 'object') {
- return data;
- }
- // если данные являются массивом, то проекция
- // применяется к каждому элементу
- if (Array.isArray(data)) {
- return data.map(item => projectData(schema, item, options));
- }
- // если данные являются объектом,
- // то создается проекция согласно схеме
- const result = {};
- // в обычном режиме итерация выполняется по ключам исходного
- // объекта, но в строгом режиме по ключам, описанным в схеме
- // (исключая ключи прототипа Object.keys(x))
- const fields = Object.keys(strict ? schema : data);
- for (const field of fields) {
- // если свойство отсутствует в исходных
- // данных, то свойство игнорируется
- if (!(field in data)) {
- continue;
- }
- // если свойство принадлежит прототипу,
- // то свойство игнорируется
- if (!Object.prototype.hasOwnProperty.call(data, field)) {
- continue;
- }
- const propOptionsOrBoolean = schema[field];
- // проверка доступности свойства для данной
- // области проекции (если определена)
- if (_shouldSelect(propOptionsOrBoolean, strict, scope)) {
- const value = data[field];
- // если определена вложенная схема,
- // то проекция применяется рекурсивно
- if (
- propOptionsOrBoolean &&
- typeof propOptionsOrBoolean === 'object' &&
- propOptionsOrBoolean.schema
- ) {
- result[field] = projectData(
- propOptionsOrBoolean.schema,
- value,
- options,
- );
- }
- // иначе значение присваивается
- // свойству без изменений
- else {
- result[field] = value;
- }
- }
- }
- return result;
- }
- /**
- * Should select (internal).
- *
- * Определяет, следует ли включать свойство в результат.
- * Приоритет: правило для области -> общее правило -> по умолчанию true.
- *
- * @param {object|boolean|undefined} propOptionsOrBoolean
- * @param {boolean|undefined} strict
- * @param {string|undefined} scope
- * @returns {boolean}
- */
- function _shouldSelect(propOptionsOrBoolean, strict, scope) {
- // если настройки свойства являются логическим значением,
- // то значение используется как индикатор видимости
- if (typeof propOptionsOrBoolean === 'boolean') {
- return propOptionsOrBoolean;
- }
- // если настройки свойства являются объектом,
- // то проверяется правило области и общее правило
- if (typeof propOptionsOrBoolean === 'object') {
- const propOptions = propOptionsOrBoolean;
- // если определена область проекции,
- // то выполняется проверка правила области
- if (
- scope &&
- propOptions.scopes &&
- typeof propOptions.scopes === 'object' &&
- propOptions.scopes[scope] != null
- ) {
- const scopeOptionsOrBoolean = propOptions.scopes[scope];
- // если настройки области являются логическим значением,
- // то значение используется как индикатор видимости
- if (typeof scopeOptionsOrBoolean === 'boolean') {
- return scopeOptionsOrBoolean;
- }
- // если настройки области являются объектом,
- // то используется опция select
- if (
- scopeOptionsOrBoolean &&
- typeof scopeOptionsOrBoolean === 'object' &&
- typeof scopeOptionsOrBoolean.select === 'boolean'
- ) {
- return scopeOptionsOrBoolean.select;
- }
- }
- // если область проекции не указана,
- // то проверяется общее правило
- if (typeof propOptionsOrBoolean.select === 'boolean') {
- return propOptionsOrBoolean.select;
- }
- }
- // если для свойства нет правил, то свойство
- // по умолчанию доступно (недоступно в режиме strict)
- return !strict;
- }
|