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