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