import {InvalidArgumentError} from '@e22m4u/js-format'; import {validateProjectionSchema} from './validate-projection-schema.js'; /** * Project data. * * @param {object|Function|string} schema * @param {object|object[]|*} data * @param {object} [options] * @returns {*} */ export function projectData(schema, data, options) { // options if (options !== undefined) { if (!options || typeof options !== 'object' || Array.isArray(options)) { throw new InvalidArgumentError( 'Projection options must be an Object, but %v was given.', options, ); } // options.strict if (options.strict !== undefined && typeof options.strict !== 'boolean') { throw new InvalidArgumentError( 'Projection 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( 'Projection option "scope" must be a non-empty String, ' + 'but %v was given.', options.scope, ); } // options.nameResolver if ( options.nameResolver !== undefined && typeof options.nameResolver !== 'function' ) { throw new InvalidArgumentError( 'Projection option "nameResolver" must be a Function, ' + 'but %v was given.', options.nameResolver, ); } // options.factoryArgs if ( options.factoryArgs !== undefined && !Array.isArray(options.factoryArgs) ) { throw new InvalidArgumentError( 'Projection option "factoryArgs" must be an Array, ' + 'but %v was given.', options.factoryArgs, ); } } // если схема является фабрикой, // то извлекается фабричное значение if (typeof schema === 'function') { const factoryArgs = (options && options.factoryArgs) || []; schema = schema(...factoryArgs); if ( !schema || (typeof schema !== 'object' && typeof schema !== 'string') || Array.isArray(schema) ) { throw new InvalidArgumentError( 'Schema factory must return an Object ' + 'or a non-empty String, but %v was given.', schema, ); } } // если схема является строкой, // то выполняется разрешение имени if (typeof schema === 'string') { // если разрешающая функция не определена, // то выбрасывается ошибка if (!options || !options.nameResolver) { throw new InvalidArgumentError( 'Projection option "nameResolver" is required to resolve %v name.', schema, ); } schema = options.nameResolver(schema); // если результат разрешающей функции не является // объектом, то выбрасывается ошибка if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { throw new InvalidArgumentError( 'Name resolver must return an Object, but %v was given.', schema, ); } } // после нормализации схемы в объект, // выполняется поверхностная проверка validateProjectionSchema(schema, true); // если данные не являются объектом или массивом, // то значение возвращается без изменений if (data == null || typeof data !== 'object') { return data; } // если данные являются массивом, // то схема применяется к каждому элементу if (Array.isArray(data)) { return data.map(item => projectData(schema, item, options)); } // если данные являются объектом, // то проекция создается согласно схеме const result = {}; const strict = Boolean(options && options.strict); const scope = (options && options.scope) || undefined; // в обычном режиме итерация выполняется по ключам исходного // объекта, а в строгом режиме по ключам, описанным в схеме // (исключая ключи прототипа Object.keys(x)) const propNames = Object.keys(strict ? schema : data); propNames.forEach(propName => { // если свойство отсутствует в исходных // данных, то свойство игнорируется if (!(propName in data)) return; const propOptions = schema[propName]; // проверка доступности свойства для данной // области проекции (если определена) if (_shouldSelect(propOptions, strict, scope)) { const value = data[propName]; // если определена вложенная схема, // то проекция применяется рекурсивно if ( propOptions && typeof propOptions === 'object' && propOptions.schema ) { result[propName] = projectData(propOptions.schema, value, options); } // иначе значение присваивается // свойству без изменений else { result[propName] = value; } } }); return result; } /** * Should select (internal). * * Определяет, следует ли включать свойство в результат. * * Приоритет: * 1. Правило для области. * 2. Общее правило. * 4. Режим проекции. * * @param {object|boolean} propOptions * @param {boolean|undefined} strict * @param {string|undefined} scope * @returns {boolean} */ function _shouldSelect(propOptions, strict, scope) { // если настройки свойства являются логическим значением, // то значение используется как индикатор видимости if (typeof propOptions === 'boolean') { return propOptions; } // если настройки свойства являются объектом, // то проверяется правило области и общее правило if ( propOptions && typeof propOptions === 'object' && !Array.isArray(propOptions) ) { // если определена область проекции, // то выполняется проверка правила области if ( scope && typeof scope === 'string' && propOptions.scopes && typeof propOptions.scopes === 'object' && propOptions.scopes[scope] !== undefined ) { const scopeOptions = propOptions.scopes[scope]; // если настройки активной области проекции // являются логическим значением, то значение // возвращается в качестве результата if (typeof scopeOptions === 'boolean') { return scopeOptions; } // если настройки активной области проекции // являются объектом и содержат опцию select, // то значение опции возвращается как результат if ( scopeOptions && typeof scopeOptions === 'object' && !Array.isArray(scopeOptions) && typeof scopeOptions.select === 'boolean' ) { return scopeOptions.select; } } // если правило видимости для активной области // проекции не определено, то проверяется наличие // общего правила if (typeof propOptions.select === 'boolean') { return propOptions.select; } } // если правила видимости не определены // то результат будет зависеть от режима return !strict; }