|
|
@@ -0,0 +1,205 @@
|
|
|
+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.projectInput(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';
|
|
|
+ if (DATA_TYPE_LIST.includes(propOptions.schema)) {
|
|
|
+ const dataSchema = {type: propOptions.schema};
|
|
|
+ res = dataParser.parse(res, dataSchema, {sourcePath});
|
|
|
+ } else {
|
|
|
+ res = dataParser.parse(res, propOptions.schema, {sourcePath});
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // если определена схема проекции,
|
|
|
+ // то выполняется создание проекции
|
|
|
+ if (propOptions.projection !== undefined) {
|
|
|
+ res = dataProjector.projectOutput(res, propOptions.projection);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Data mapping pre-handler.
|
|
|
+ *
|
|
|
+ * @type {import('@e22m4u/js-trie-router').PreHandlerHook}
|
|
|
+ */
|
|
|
+function dataMappingPreHandler(ctx) {
|
|
|
+ const schema = (ctx.meta || {}).dataMap;
|
|
|
+ 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 || {}).dataMap;
|
|
|
+ if (schema === undefined) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const mapper = ctx.container.get(TrieRouterDataMapper);
|
|
|
+ return mapper.filterResponseByMappingSchema(data, schema);
|
|
|
+}
|