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; }