trie-router-data-mapper.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import {Service} from '@e22m4u/js-service';
  2. import {HttpData} from './data-mapping-schema.js';
  3. import {InvalidArgumentError} from '@e22m4u/js-format';
  4. import {DataProjector} from '@e22m4u/js-data-projector';
  5. import {RequestContext, TrieRouter} from '@e22m4u/js-trie-router';
  6. import {DATA_TYPE_LIST, DataParser} from '@e22m4u/js-data-schema';
  7. import {validateDataMappingSchema} from './validate-data-mapping-schema.js';
  8. /**
  9. * Константа HttpData определяет какое свойство контекста
  10. * запроса будет использовано для формирования данных.
  11. * Этот объект связывает значения HttpData со свойствами
  12. * контекста запроса.
  13. */
  14. const HTTP_DATA_TO_CONTEXT_PROPERTY_MAP = {
  15. [HttpData.REQUEST_PARAMS]: 'params',
  16. [HttpData.REQUEST_QUERY]: 'query',
  17. [HttpData.REQUEST_HEADERS]: 'headers',
  18. [HttpData.REQUEST_COOKIES]: 'cookies',
  19. [HttpData.REQUEST_BODY]: 'body',
  20. };
  21. /**
  22. * Trie router data mapper.
  23. */
  24. export class TrieRouterDataMapper extends Service {
  25. /**
  26. * Constructor.
  27. *
  28. * @param {import('@e22m4u/js-service').ServiceContainer} container
  29. */
  30. constructor(container) {
  31. super(container);
  32. const router = this.getService(TrieRouter);
  33. if (!router.hasPreHandler(dataMappingPreHandler)) {
  34. router.addPreHandler(dataMappingPreHandler);
  35. }
  36. if (!router.hasPostHandler(dataMappingPostHandler)) {
  37. router.addPostHandler(dataMappingPostHandler);
  38. }
  39. }
  40. /**
  41. * Create state by mapping schema.
  42. *
  43. * @param {import('@e22m4u/js-trie-router').RequestContext} ctx
  44. * @param {import('./data-mapping-schema.js').DataMappingSchema} schema
  45. * @returns {object}
  46. */
  47. createStateByMappingSchema(ctx, schema) {
  48. if (!(ctx instanceof RequestContext)) {
  49. throw new InvalidArgumentError(
  50. 'Parameter "ctx" must be a RequestContext instance, but %v was given.',
  51. ctx,
  52. );
  53. }
  54. validateDataMappingSchema(schema);
  55. const res = {};
  56. const dataParser = this.getService(DataParser);
  57. const dataProjector = this.getService(DataProjector);
  58. // обход каждого свойства схемы
  59. // для формирования объекта данных
  60. Object.keys(schema).forEach(propName => {
  61. // если параметры свойства не определены,
  62. // то данное свойство пропускается
  63. const propOptions = schema[propName];
  64. if (propOptions === undefined) {
  65. return;
  66. }
  67. // если свойство контекста не определено,
  68. // то данное свойство пропускается
  69. const ctxProp = HTTP_DATA_TO_CONTEXT_PROPERTY_MAP[propOptions.source];
  70. if (ctxProp === undefined) {
  71. return;
  72. }
  73. let value = ctx[ctxProp];
  74. // если определено вложенное свойство,
  75. // то выполняется попытка его извлечения
  76. if (propOptions.property && typeof propOptions.property === 'string') {
  77. // если свойство контекста содержит объект,
  78. // то извлекается значение вложенного свойства
  79. if (value && typeof value === 'object' && !Array.isArray(value)) {
  80. value = value[propOptions.property];
  81. }
  82. // если свойство контекста не содержит
  83. // объект, то выбрасывается ошибка
  84. else {
  85. throw new InvalidArgumentError(
  86. 'Property %v does not exist in %v value ' +
  87. 'from the property %v of the request context.',
  88. propOptions.property,
  89. value,
  90. ctxProp,
  91. );
  92. }
  93. }
  94. // если определена схема данных,
  95. // то выполняется разбор значения
  96. if (propOptions.schema !== undefined) {
  97. const sourcePath = propOptions.property
  98. ? `request.${ctxProp}.${propOptions.property}`
  99. : `request.${ctxProp}`;
  100. if (DATA_TYPE_LIST.includes(propOptions.schema)) {
  101. const dataSchema = {type: propOptions.schema};
  102. value = dataParser.parse(value, dataSchema, {sourcePath});
  103. } else {
  104. value = dataParser.parse(value, propOptions.schema, {sourcePath});
  105. }
  106. }
  107. // если определена схема проекции,
  108. // то выполняется создание проекции
  109. if (propOptions.projection !== undefined) {
  110. value = dataProjector.project(value, propOptions.projection);
  111. }
  112. // значение присваивается
  113. // результирующему объекту
  114. res[propName] = value;
  115. });
  116. return res;
  117. }
  118. /**
  119. * Filter response by mapping schema.
  120. *
  121. * @param {*} data
  122. * @param {import('./data-mapping-schema.js').DataMappingSchema} schema
  123. * @returns {*}
  124. */
  125. filterResponseByMappingSchema(data, schema) {
  126. validateDataMappingSchema(schema);
  127. let res = data;
  128. const dataParser = this.getService(DataParser);
  129. const dataProjector = this.getService(DataProjector);
  130. // обход каждого свойства схемы
  131. // для формирования данных ответа
  132. Object.keys(schema).forEach(propName => {
  133. // если параметры свойства не определены,
  134. // то данное свойство пропускается
  135. const propOptions = schema[propName];
  136. if (propOptions === undefined) {
  137. return;
  138. }
  139. // если источником не является тело ответа,
  140. // то данное свойство пропускается
  141. if (propOptions.source !== HttpData.RESPONSE_BODY) {
  142. return;
  143. }
  144. // если определено вложенное свойство,
  145. // то выбрасывается ошибка
  146. if (propOptions.property !== undefined) {
  147. throw new InvalidArgumentError(
  148. 'Option "property" is not supported for the %v source, ' +
  149. 'but %v was given.',
  150. propOptions.property,
  151. );
  152. }
  153. // если определена схема данных, то выполняется
  154. // разбор значения без валидации данных
  155. if (propOptions.schema !== undefined) {
  156. const sourcePath = 'response.body';
  157. const parsingOptions = {sourcePath, noParsingErrors: true};
  158. if (DATA_TYPE_LIST.includes(propOptions.schema)) {
  159. const dataSchema = {type: propOptions.schema};
  160. res = dataParser.parse(res, dataSchema, parsingOptions);
  161. } else {
  162. res = dataParser.parse(res, propOptions.schema, parsingOptions);
  163. }
  164. }
  165. // если определена схема проекции,
  166. // то выполняется создание проекции
  167. if (propOptions.projection !== undefined) {
  168. res = dataProjector.project(res, propOptions.projection);
  169. }
  170. });
  171. return res;
  172. }
  173. }
  174. /**
  175. * Data mapping pre-handler.
  176. *
  177. * @type {import('@e22m4u/js-trie-router').PreHandlerHook}
  178. */
  179. function dataMappingPreHandler(ctx) {
  180. const schema = (ctx.meta || {}).dataMapper;
  181. if (schema === undefined) {
  182. return;
  183. }
  184. const mapper = ctx.container.get(TrieRouterDataMapper);
  185. const state = mapper.createStateByMappingSchema(ctx, schema);
  186. ctx.state = {...ctx.state, ...state};
  187. }
  188. /**
  189. * Data mapping post handler.
  190. *
  191. * @type {import('@e22m4u/js-trie-router').PostHandlerHook}
  192. */
  193. function dataMappingPostHandler(ctx, data) {
  194. const schema = (ctx.meta || {}).dataMapper;
  195. if (schema === undefined) {
  196. return;
  197. }
  198. const mapper = ctx.container.get(TrieRouterDataMapper);
  199. return mapper.filterResponseByMappingSchema(data, schema);
  200. }