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