import {Service} from '@e22m4u/js-service'; import {HttpData} from './data-mapping-schema.js'; import {InvalidArgumentError} from '@e22m4u/js-format'; import {DataProjector} from '@e22m4u/js-data-projector'; import {RequestContext, TrieRouter} from '@e22m4u/js-trie-router'; import {DATA_TYPE_LIST, DataParser} from '@e22m4u/js-data-schema'; import {validateDataMappingSchema} from './validate-data-mapping-schema.js'; /** * Константа HttpData определяет какое свойство контекста * запроса будет использовано для формирования данных. * Этот объект связывает значения HttpData со свойствами * контекста запроса. */ const HTTP_DATA_TO_CONTEXT_PROPERTY_MAP = { [HttpData.REQUEST_PARAMS]: 'params', [HttpData.REQUEST_QUERY]: 'query', [HttpData.REQUEST_HEADERS]: 'headers', [HttpData.REQUEST_COOKIES]: 'cookies', [HttpData.REQUEST_BODY]: 'body', }; /** * Trie router data mapper. */ export class TrieRouterDataMapper extends Service { /** * Constructor. * * @param {import('@e22m4u/js-service').ServiceContainer} container */ constructor(container) { super(container); const router = this.getService(TrieRouter); if (!router.hasPreHandler(dataMappingPreHandler)) { router.addPreHandler(dataMappingPreHandler); } if (!router.hasPostHandler(dataMappingPostHandler)) { router.addPostHandler(dataMappingPostHandler); } } /** * Create state by mapping schema. * * @param {import('@e22m4u/js-trie-router').RequestContext} ctx * @param {import('./data-mapping-schema.js').DataMappingSchema} schema * @returns {object} */ createStateByMappingSchema(ctx, schema) { if (!(ctx instanceof RequestContext)) { throw new InvalidArgumentError( 'Parameter "ctx" must be a RequestContext instance, but %v was given.', ctx, ); } validateDataMappingSchema(schema); const res = {}; const dataParser = this.getService(DataParser); const dataProjector = this.getService(DataProjector); // обход каждого свойства схемы // для формирования объекта данных Object.keys(schema).forEach(propName => { // если параметры свойства не определены, // то данное свойство пропускается const propOptions = schema[propName]; if (propOptions === undefined) { return; } // если свойство контекста не определено, // то данное свойство пропускается const ctxProp = HTTP_DATA_TO_CONTEXT_PROPERTY_MAP[propOptions.source]; if (ctxProp === undefined) { return; } let value = ctx[ctxProp]; // если определено вложенное свойство, // то выполняется попытка его извлечения if (propOptions.property && typeof propOptions.property === 'string') { // если свойство контекста содержит объект, // то извлекается значение вложенного свойства if (value && typeof value === 'object' && !Array.isArray(value)) { value = value[propOptions.property]; } // если свойство контекста не содержит // объект, то выбрасывается ошибка else { throw new InvalidArgumentError( 'Property %v does not exist in %v value ' + 'from the property %v of the request context.', propOptions.property, value, ctxProp, ); } } // если определена схема данных, // то выполняется разбор значения if (propOptions.schema !== undefined) { const sourcePath = propOptions.property ? `request.${ctxProp}.${propOptions.property}` : `request.${ctxProp}`; if (DATA_TYPE_LIST.includes(propOptions.schema)) { const dataSchema = {type: propOptions.schema}; value = dataParser.parse(value, dataSchema, {sourcePath}); } else { value = dataParser.parse(value, propOptions.schema, {sourcePath}); } } // если определена схема проекции, // то выполняется создание проекции if (propOptions.projection !== undefined) { value = dataProjector.project(value, propOptions.projection); } // значение присваивается // результирующему объекту res[propName] = value; }); return res; } /** * Filter response by mapping schema. * * @param {*} data * @param {import('./data-mapping-schema.js').DataMappingSchema} schema * @returns {*} */ filterResponseByMappingSchema(data, schema) { validateDataMappingSchema(schema); let res = data; const dataParser = this.getService(DataParser); const dataProjector = this.getService(DataProjector); // обход каждого свойства схемы // для формирования данных ответа Object.keys(schema).forEach(propName => { // если параметры свойства не определены, // то данное свойство пропускается const propOptions = schema[propName]; if (propOptions === undefined) { return; } // если источником не является тело ответа, // то данное свойство пропускается if (propOptions.source !== HttpData.RESPONSE_BODY) { return; } // если определено вложенное свойство, // то выбрасывается ошибка if (propOptions.property !== undefined) { throw new InvalidArgumentError( 'Option "property" is not supported for the %v source, ' + 'but %v was given.', propOptions.property, ); } // если определена схема данных, то выполняется // разбор значения без валидации данных if (propOptions.schema !== undefined) { const sourcePath = 'response.body'; const parsingOptions = {sourcePath, noParsingErrors: true}; if (DATA_TYPE_LIST.includes(propOptions.schema)) { const dataSchema = {type: propOptions.schema}; res = dataParser.parse(res, dataSchema, parsingOptions); } else { res = dataParser.parse(res, propOptions.schema, parsingOptions); } } // если определена схема проекции, // то выполняется создание проекции if (propOptions.projection !== undefined) { res = dataProjector.project(res, propOptions.projection); } }); return res; } } /** * Data mapping pre-handler. * * @type {import('@e22m4u/js-trie-router').PreHandlerHook} */ function dataMappingPreHandler(ctx) { const schema = (ctx.meta || {}).dataMapper; if (schema === undefined) { return; } const mapper = ctx.container.get(TrieRouterDataMapper); const state = mapper.createStateByMappingSchema(ctx, schema); ctx.state = {...ctx.state, ...state}; } /** * Data mapping post handler. * * @type {import('@e22m4u/js-trie-router').PostHandlerHook} */ function dataMappingPostHandler(ctx, data) { const schema = (ctx.meta || {}).dataMapper; if (schema === undefined) { return; } const mapper = ctx.container.get(TrieRouterDataMapper); return mapper.filterResponseByMappingSchema(data, schema); }