@e22m4u/js-trie-router-data-mapper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc +9 -0
- package/.commitlintrc +5 -0
- package/.editorconfig +13 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +6 -0
- package/.mocharc.json +4 -0
- package/.prettierrc +7 -0
- package/LICENSE +21 -0
- package/README.md +200 -0
- package/build-cjs.js +16 -0
- package/dist/cjs/index.cjs +245 -0
- package/eslint.config.js +41 -0
- package/examples/query-parsing-example.js +52 -0
- package/examples/response-projection-example.js +47 -0
- package/package.json +71 -0
- package/src/data-mapping-schema.d.ts +41 -0
- package/src/data-mapping-schema.js +16 -0
- package/src/index.d.ts +6 -0
- package/src/index.js +5 -0
- package/src/route-meta.d.ts +8 -0
- package/src/trie-router-data-mapper.d.ts +37 -0
- package/src/trie-router-data-mapper.js +206 -0
- package/src/validate-data-mapping-schema.d.ts +8 -0
- package/src/validate-data-mapping-schema.js +54 -0
- package/src/validate-data-mapping-schema.spec.js +75 -0
- package/tsconfig.json +14 -0
package/.c8rc
ADDED
package/.commitlintrc
ADDED
package/.editorconfig
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# EditorConfig is awesome: https://EditorConfig.org
|
|
2
|
+
|
|
3
|
+
# top-most EditorConfig file
|
|
4
|
+
root = true
|
|
5
|
+
|
|
6
|
+
# Unix-style newlines with a newline ending every file
|
|
7
|
+
[*]
|
|
8
|
+
end_of_line = lf
|
|
9
|
+
insert_final_newline = true
|
|
10
|
+
charset = utf-8
|
|
11
|
+
indent_style = space
|
|
12
|
+
indent_size = 2
|
|
13
|
+
max_line_length = 80
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
npx --no -- commitlint --edit $1
|
package/.mocharc.json
ADDED
package/.prettierrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2025 Mikhail Evstropov <e22m4u@yandex.ru>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
## @e22m4u/js-trie-router-data-mapper
|
|
2
|
+
|
|
3
|
+
Парсинг, валидация и проекция данных для
|
|
4
|
+
[@e22m4u/js-trie-router](https://www.npmjs.com/package/@e22m4u/js-trie-router).
|
|
5
|
+
|
|
6
|
+
## Содержание
|
|
7
|
+
|
|
8
|
+
- [Установка](#установка)
|
|
9
|
+
- [Описание](#описание)
|
|
10
|
+
- [Использование](#использование)
|
|
11
|
+
- [Тесты](#тесты)
|
|
12
|
+
- [Лицензия](#лицензия)
|
|
13
|
+
|
|
14
|
+
## Установка
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @e22m4u/js-trie-router-data-mapper
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Модуль поддерживает ESM и CommonJS стандарты.
|
|
21
|
+
|
|
22
|
+
*ESM*
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import {TrieRouterDataMapper} from '@e22m4u/js-trie-router-data-mapper';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
*CommonJS*
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const {TrieRouterDataMapper} = require('@e22m4u/js-trie-router-data-mapper');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Описание
|
|
35
|
+
|
|
36
|
+
Модуль позволяет определить разметку данных маршрута. На основе разметки
|
|
37
|
+
выполняется автоматический парсинг, валидация и проекция данных HTTP-запроса
|
|
38
|
+
и ответа сервера. Сформированные данные помещаются в контекст запроса
|
|
39
|
+
или определяют структуру возвращаемого ответа.
|
|
40
|
+
|
|
41
|
+
Используется синтаксис указанных ниже модулей (не требуют установки).
|
|
42
|
+
|
|
43
|
+
- Схема данных [@e22m4u/js-data-schema](https://www.npmjs.com/package/@e22m4u/js-data-schema)
|
|
44
|
+
- Схема проекции [@e22m4u/js-data-projector](https://www.npmjs.com/package/@e22m4u/js-data-projector)
|
|
45
|
+
|
|
46
|
+
## Использование
|
|
47
|
+
|
|
48
|
+
Для корректной работы требуется выполнить подключение модуля к маршрутизатору.
|
|
49
|
+
В этот момент будут зарегистрированы два глобальных хука. Первый хук разбирает
|
|
50
|
+
и проверяет входящие данные, а второй фильтрует ответ сервера.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import {TrieRouter} from '@e22m4u/js-trie-router';
|
|
54
|
+
import {TrieRouterDataMapper} from '@e22m4u/js-trie-router-data-mapper';
|
|
55
|
+
|
|
56
|
+
const router = new TrieRouter(); // экземпляр маршрутизатора
|
|
57
|
+
router.useService(TrieRouterDataMapper); // <= подключение модуля
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Разметка данных задается в свойстве `dataMapper` метаданных маршрута. Ключи
|
|
61
|
+
этого объекта определяют имена полей, добавляемых в контекст запроса, а значения
|
|
62
|
+
описывают правила извлечения, валидации и проекции для каждого источника данных.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import {HttpMethod} from '@e22m4u/js-trie-router';
|
|
66
|
+
import {HttpData, DataType} from '@e22m4u/js-trie-router-data-mapper';
|
|
67
|
+
|
|
68
|
+
router.defineRoute({
|
|
69
|
+
method: HttpMethod.POST,
|
|
70
|
+
path: '/createUser',
|
|
71
|
+
meta: {
|
|
72
|
+
dataMapper: {
|
|
73
|
+
userData: { // свойство "userData" будет добавлено в "ctx.state"
|
|
74
|
+
source: HttpData.REQUEST_BODY, // источник данных
|
|
75
|
+
schema: DataType.OBJECT, // тип или схема данных
|
|
76
|
+
// property: ... (извлечь свойство из источника)
|
|
77
|
+
// projection: ... (схема проекции)
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
handler: ({state: {userData}}) => {
|
|
82
|
+
// ...
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Параметры разметки (метаданные маршрута).
|
|
88
|
+
|
|
89
|
+
- `source: HttpData` источник данных;
|
|
90
|
+
- `property?: string` извлечение указанного свойства;
|
|
91
|
+
- `schema?: DataType | DataSchema` тип или схема данных;
|
|
92
|
+
- `projection?: DataProjection` схема проекции;
|
|
93
|
+
|
|
94
|
+
Константы источников данных (параметр `source`).
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
export const HttpData = {
|
|
98
|
+
REQUEST_PARAMS: 'requestParams',
|
|
99
|
+
REQUEST_QUERY: 'requestQuery',
|
|
100
|
+
REQUEST_HEADERS: 'requestHeaders',
|
|
101
|
+
REQUEST_COOKIES: 'requestCookies',
|
|
102
|
+
REQUEST_BODY: 'requestBody',
|
|
103
|
+
RESPONSE_BODY: 'responseBody',
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Константы типов данных (подробнее [@e22m4u/js-data-schema](https://www.npmjs.com/package/@e22m4u/js-data-schema)).
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
export const DataType = {
|
|
111
|
+
ANY: 'any',
|
|
112
|
+
STRING: 'string',
|
|
113
|
+
NUMBER: 'number',
|
|
114
|
+
BOOLEAN: 'boolean',
|
|
115
|
+
ARRAY: 'array',
|
|
116
|
+
OBJECT: 'object',
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Примеры
|
|
121
|
+
|
|
122
|
+
Извлечение и разбор Query-параметра с JSON значением.
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import {HttpMethod} from '@e22m4u/js-trie-router';
|
|
126
|
+
import {HttpData, DataType} from '@e22m4u/js-trie-router-data-mapper';
|
|
127
|
+
|
|
128
|
+
// GET /parseQuery?filter={"foo":"bar"}
|
|
129
|
+
router.defineRoute({
|
|
130
|
+
method: HttpMethod.GET,
|
|
131
|
+
path: '/parseQuery',
|
|
132
|
+
meta: {
|
|
133
|
+
dataMapper: {
|
|
134
|
+
filter: { // свойство "filter" будет добавлено в "ctx.state"
|
|
135
|
+
source: HttpData.REQUEST_QUERY, // источник данных
|
|
136
|
+
property: 'filter', // извлечь свойство из источника
|
|
137
|
+
schema: { // схема для парсинга и валидации
|
|
138
|
+
type: DataType.OBJECT, // разобрать значение как объект
|
|
139
|
+
required: true, // значение является обязательным
|
|
140
|
+
},
|
|
141
|
+
// подробнее о схеме данных (параметр "schema")
|
|
142
|
+
// см. модуль @e22m4u/js-data-schema
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
handler: ({state: {filter}}) => {
|
|
147
|
+
// для запроса GET /parseQuery?filter={"foo":"bar"}
|
|
148
|
+
// значение параметра "filter" будет следующим:
|
|
149
|
+
console.log(typeof filter); // "object"
|
|
150
|
+
console.log(filter); // {foo: 'bar'}
|
|
151
|
+
// если значение разобрать не удалось,
|
|
152
|
+
// то будет выброшена ошибка
|
|
153
|
+
return filter;
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Фильтрация свойств возвращаемого объекта согласно схеме проекции.
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import {HttpMethod} from '@e22m4u/js-trie-router';
|
|
162
|
+
import {HttpData} from '@e22m4u/js-trie-router-data-mapper';
|
|
163
|
+
|
|
164
|
+
router.defineRoute({
|
|
165
|
+
method: HttpMethod.GET,
|
|
166
|
+
path: '/responseProjection',
|
|
167
|
+
meta: {
|
|
168
|
+
dataMapper: {
|
|
169
|
+
response: {
|
|
170
|
+
// свойство "response" не будет добавлено в "ctx.state",
|
|
171
|
+
// так как в данном случае источником выступает возвращаемое
|
|
172
|
+
// значение обработчика маршрута, а не входящие данные
|
|
173
|
+
source: HttpData.RESPONSE_BODY, // источник данных
|
|
174
|
+
projection: {foo: true, bar: false}, // схема проекции
|
|
175
|
+
// подробнее о схеме проекции (параметр "projection")
|
|
176
|
+
// см. модуль @e22m4u/js-data-projector
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
handler: () => {
|
|
181
|
+
return {
|
|
182
|
+
foo: 10, // доступно, явное правило
|
|
183
|
+
bar: 20, // исключено, явное правило
|
|
184
|
+
baz: 30, // исключено, отсутствует в схеме проекции
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
// для запроса GET /responseProjection
|
|
189
|
+
// ответ будет {"foo":10}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Тесты
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npm run test
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Лицензия
|
|
199
|
+
|
|
200
|
+
MIT
|
package/build-cjs.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as esbuild from 'esbuild';
|
|
2
|
+
import packageJson from './package.json' with {type: 'json'};
|
|
3
|
+
|
|
4
|
+
await esbuild.build({
|
|
5
|
+
entryPoints: ['src/index.js'],
|
|
6
|
+
outfile: 'dist/cjs/index.cjs',
|
|
7
|
+
format: 'cjs',
|
|
8
|
+
platform: 'node',
|
|
9
|
+
target: ['node12'],
|
|
10
|
+
bundle: true,
|
|
11
|
+
keepNames: true,
|
|
12
|
+
external: [
|
|
13
|
+
...Object.keys(packageJson.peerDependencies || {}),
|
|
14
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
15
|
+
],
|
|
16
|
+
});
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.js
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
DataSchemaRegistry: () => import_js_data_schema2.DataSchemaRegistry,
|
|
25
|
+
DataType: () => import_js_data_schema2.DataType,
|
|
26
|
+
HTTP_DATA_LIST: () => HTTP_DATA_LIST,
|
|
27
|
+
HttpData: () => HttpData,
|
|
28
|
+
ProjectionSchemaRegistry: () => import_js_data_projector2.ProjectionSchemaRegistry,
|
|
29
|
+
TrieRouterDataMapper: () => TrieRouterDataMapper,
|
|
30
|
+
validateDataMappingSchema: () => validateDataMappingSchema
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/data-mapping-schema.js
|
|
35
|
+
var HttpData = {
|
|
36
|
+
REQUEST_PARAMS: "requestParams",
|
|
37
|
+
REQUEST_QUERY: "requestQuery",
|
|
38
|
+
REQUEST_HEADERS: "requestHeaders",
|
|
39
|
+
REQUEST_COOKIES: "requestCookies",
|
|
40
|
+
REQUEST_BODY: "requestBody",
|
|
41
|
+
RESPONSE_BODY: "responseBody"
|
|
42
|
+
};
|
|
43
|
+
var HTTP_DATA_LIST = Object.values(HttpData);
|
|
44
|
+
|
|
45
|
+
// src/trie-router-data-mapper.js
|
|
46
|
+
var import_js_service = require("@e22m4u/js-service");
|
|
47
|
+
var import_js_format2 = require("@e22m4u/js-format");
|
|
48
|
+
var import_js_data_projector = require("@e22m4u/js-data-projector");
|
|
49
|
+
var import_js_trie_router = require("@e22m4u/js-trie-router");
|
|
50
|
+
var import_js_data_schema = require("@e22m4u/js-data-schema");
|
|
51
|
+
|
|
52
|
+
// src/validate-data-mapping-schema.js
|
|
53
|
+
var import_js_format = require("@e22m4u/js-format");
|
|
54
|
+
function validateDataMappingSchema(schema) {
|
|
55
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
56
|
+
throw new import_js_format.InvalidArgumentError(
|
|
57
|
+
"Mapping schema must be an Object, but %v was given.",
|
|
58
|
+
schema
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
Object.keys(schema).forEach((propName) => {
|
|
62
|
+
const propOptions = schema[propName];
|
|
63
|
+
if (propOptions === void 0) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!propOptions || typeof propOptions !== "object" || Array.isArray(propOptions)) {
|
|
67
|
+
throw new import_js_format.InvalidArgumentError(
|
|
68
|
+
"Property options must be an Object, but %v was given.",
|
|
69
|
+
propOptions
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!propOptions.source || typeof propOptions.source !== "string" || !HTTP_DATA_LIST.includes(propOptions.source)) {
|
|
73
|
+
throw new import_js_format.InvalidArgumentError(
|
|
74
|
+
"Data source %v is not supported.",
|
|
75
|
+
propOptions.source
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (propOptions.property !== void 0 && (!propOptions.property || typeof propOptions.property !== "string")) {
|
|
79
|
+
throw new import_js_format.InvalidArgumentError(
|
|
80
|
+
"Property name must be a non-empty String, but %v was given.",
|
|
81
|
+
propOptions.property
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
__name(validateDataMappingSchema, "validateDataMappingSchema");
|
|
87
|
+
|
|
88
|
+
// src/trie-router-data-mapper.js
|
|
89
|
+
var HTTP_DATA_TO_CONTEXT_PROPERTY_MAP = {
|
|
90
|
+
[HttpData.REQUEST_PARAMS]: "params",
|
|
91
|
+
[HttpData.REQUEST_QUERY]: "query",
|
|
92
|
+
[HttpData.REQUEST_HEADERS]: "headers",
|
|
93
|
+
[HttpData.REQUEST_COOKIES]: "cookies",
|
|
94
|
+
[HttpData.REQUEST_BODY]: "body"
|
|
95
|
+
};
|
|
96
|
+
var _TrieRouterDataMapper = class _TrieRouterDataMapper extends import_js_service.Service {
|
|
97
|
+
/**
|
|
98
|
+
* Constructor.
|
|
99
|
+
*
|
|
100
|
+
* @param {import('@e22m4u/js-service').ServiceContainer} container
|
|
101
|
+
*/
|
|
102
|
+
constructor(container) {
|
|
103
|
+
super(container);
|
|
104
|
+
const router = this.getService(import_js_trie_router.TrieRouter);
|
|
105
|
+
if (!router.hasPreHandler(dataMappingPreHandler)) {
|
|
106
|
+
router.addPreHandler(dataMappingPreHandler);
|
|
107
|
+
}
|
|
108
|
+
if (!router.hasPostHandler(dataMappingPostHandler)) {
|
|
109
|
+
router.addPostHandler(dataMappingPostHandler);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create state by mapping schema.
|
|
114
|
+
*
|
|
115
|
+
* @param {import('@e22m4u/js-trie-router').RequestContext} ctx
|
|
116
|
+
* @param {import('./data-mapping-schema.js').DataMappingSchema} schema
|
|
117
|
+
* @returns {object}
|
|
118
|
+
*/
|
|
119
|
+
createStateByMappingSchema(ctx, schema) {
|
|
120
|
+
if (!(ctx instanceof import_js_trie_router.RequestContext)) {
|
|
121
|
+
throw new import_js_format2.InvalidArgumentError(
|
|
122
|
+
'Parameter "ctx" must be a RequestContext instance, but %v was given.',
|
|
123
|
+
ctx
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
validateDataMappingSchema(schema);
|
|
127
|
+
const res = {};
|
|
128
|
+
const dataParser = this.getService(import_js_data_schema.DataParser);
|
|
129
|
+
const dataProjector = this.getService(import_js_data_projector.DataProjector);
|
|
130
|
+
Object.keys(schema).forEach((propName) => {
|
|
131
|
+
const propOptions = schema[propName];
|
|
132
|
+
if (propOptions === void 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const ctxProp = HTTP_DATA_TO_CONTEXT_PROPERTY_MAP[propOptions.source];
|
|
136
|
+
if (ctxProp === void 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let value = ctx[ctxProp];
|
|
140
|
+
if (propOptions.property && typeof propOptions.property === "string") {
|
|
141
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
142
|
+
value = value[propOptions.property];
|
|
143
|
+
} else {
|
|
144
|
+
throw new import_js_format2.InvalidArgumentError(
|
|
145
|
+
"Property %v does not exist in %v value from the property %v of the request context.",
|
|
146
|
+
propOptions.property,
|
|
147
|
+
value,
|
|
148
|
+
ctxProp
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (propOptions.schema !== void 0) {
|
|
153
|
+
const sourcePath = propOptions.property ? `request.${ctxProp}.${propOptions.property}` : `request.${ctxProp}`;
|
|
154
|
+
if (import_js_data_schema.DATA_TYPE_LIST.includes(propOptions.schema)) {
|
|
155
|
+
const dataSchema = { type: propOptions.schema };
|
|
156
|
+
value = dataParser.parse(value, dataSchema, { sourcePath });
|
|
157
|
+
} else {
|
|
158
|
+
value = dataParser.parse(value, propOptions.schema, { sourcePath });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (propOptions.projection !== void 0) {
|
|
162
|
+
value = dataProjector.project(value, propOptions.projection);
|
|
163
|
+
}
|
|
164
|
+
res[propName] = value;
|
|
165
|
+
});
|
|
166
|
+
return res;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Filter response by mapping schema.
|
|
170
|
+
*
|
|
171
|
+
* @param {*} data
|
|
172
|
+
* @param {import('./data-mapping-schema.js').DataMappingSchema} schema
|
|
173
|
+
* @returns {*}
|
|
174
|
+
*/
|
|
175
|
+
filterResponseByMappingSchema(data, schema) {
|
|
176
|
+
validateDataMappingSchema(schema);
|
|
177
|
+
let res = data;
|
|
178
|
+
const dataParser = this.getService(import_js_data_schema.DataParser);
|
|
179
|
+
const dataProjector = this.getService(import_js_data_projector.DataProjector);
|
|
180
|
+
Object.keys(schema).forEach((propName) => {
|
|
181
|
+
const propOptions = schema[propName];
|
|
182
|
+
if (propOptions === void 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (propOptions.source !== HttpData.RESPONSE_BODY) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (propOptions.property !== void 0) {
|
|
189
|
+
throw new import_js_format2.InvalidArgumentError(
|
|
190
|
+
'Option "property" is not supported for the %v source, but %v was given.',
|
|
191
|
+
propOptions.property
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (propOptions.schema !== void 0) {
|
|
195
|
+
const sourcePath = "response.body";
|
|
196
|
+
const parsingOptions = { sourcePath, noParsingErrors: true };
|
|
197
|
+
if (import_js_data_schema.DATA_TYPE_LIST.includes(propOptions.schema)) {
|
|
198
|
+
const dataSchema = { type: propOptions.schema };
|
|
199
|
+
res = dataParser.parse(res, dataSchema, parsingOptions);
|
|
200
|
+
} else {
|
|
201
|
+
res = dataParser.parse(res, propOptions.schema, parsingOptions);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (propOptions.projection !== void 0) {
|
|
205
|
+
res = dataProjector.project(res, propOptions.projection);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return res;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
__name(_TrieRouterDataMapper, "TrieRouterDataMapper");
|
|
212
|
+
var TrieRouterDataMapper = _TrieRouterDataMapper;
|
|
213
|
+
function dataMappingPreHandler(ctx) {
|
|
214
|
+
const schema = (ctx.meta || {}).dataMapper;
|
|
215
|
+
if (schema === void 0) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const mapper = ctx.container.get(TrieRouterDataMapper);
|
|
219
|
+
const state = mapper.createStateByMappingSchema(ctx, schema);
|
|
220
|
+
ctx.state = { ...ctx.state, ...state };
|
|
221
|
+
}
|
|
222
|
+
__name(dataMappingPreHandler, "dataMappingPreHandler");
|
|
223
|
+
function dataMappingPostHandler(ctx, data) {
|
|
224
|
+
const schema = (ctx.meta || {}).dataMapper;
|
|
225
|
+
if (schema === void 0) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const mapper = ctx.container.get(TrieRouterDataMapper);
|
|
229
|
+
return mapper.filterResponseByMappingSchema(data, schema);
|
|
230
|
+
}
|
|
231
|
+
__name(dataMappingPostHandler, "dataMappingPostHandler");
|
|
232
|
+
|
|
233
|
+
// src/index.js
|
|
234
|
+
var import_js_data_projector2 = require("@e22m4u/js-data-projector");
|
|
235
|
+
var import_js_data_schema2 = require("@e22m4u/js-data-schema");
|
|
236
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
237
|
+
0 && (module.exports = {
|
|
238
|
+
DataSchemaRegistry,
|
|
239
|
+
DataType,
|
|
240
|
+
HTTP_DATA_LIST,
|
|
241
|
+
HttpData,
|
|
242
|
+
ProjectionSchemaRegistry,
|
|
243
|
+
TrieRouterDataMapper,
|
|
244
|
+
validateDataMappingSchema
|
|
245
|
+
});
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import globals from 'globals';
|
|
2
|
+
import eslintJs from '@eslint/js';
|
|
3
|
+
import eslintJsdocPlugin from 'eslint-plugin-jsdoc';
|
|
4
|
+
import eslintMochaPlugin from 'eslint-plugin-mocha';
|
|
5
|
+
import eslintImportPlugin from 'eslint-plugin-import';
|
|
6
|
+
import eslintPrettierConfig from 'eslint-config-prettier';
|
|
7
|
+
import eslintChaiExpectPlugin from 'eslint-plugin-chai-expect';
|
|
8
|
+
|
|
9
|
+
export default [{
|
|
10
|
+
languageOptions: {
|
|
11
|
+
globals: {
|
|
12
|
+
...globals.node,
|
|
13
|
+
...globals.es2021,
|
|
14
|
+
...globals.mocha,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
plugins: {
|
|
18
|
+
'jsdoc': eslintJsdocPlugin,
|
|
19
|
+
'mocha': eslintMochaPlugin,
|
|
20
|
+
'import': eslintImportPlugin,
|
|
21
|
+
'chai-expect': eslintChaiExpectPlugin,
|
|
22
|
+
},
|
|
23
|
+
rules: {
|
|
24
|
+
...eslintJs.configs.recommended.rules,
|
|
25
|
+
...eslintPrettierConfig.rules,
|
|
26
|
+
...eslintImportPlugin.flatConfigs.recommended.rules,
|
|
27
|
+
...eslintMochaPlugin.configs.recommended.rules,
|
|
28
|
+
...eslintChaiExpectPlugin.configs['recommended-flat'].rules,
|
|
29
|
+
...eslintJsdocPlugin.configs['flat/recommended-error'].rules,
|
|
30
|
+
"curly": "error",
|
|
31
|
+
'no-duplicate-imports': 'error',
|
|
32
|
+
'import/export': 0,
|
|
33
|
+
'jsdoc/reject-any-type': 0,
|
|
34
|
+
'jsdoc/reject-function-type': 0,
|
|
35
|
+
'jsdoc/require-param-description': 0,
|
|
36
|
+
'jsdoc/require-returns-description': 0,
|
|
37
|
+
'jsdoc/require-property-description': 0,
|
|
38
|
+
'jsdoc/tag-lines': ['error', 'any', {startLines: 1}],
|
|
39
|
+
},
|
|
40
|
+
files: ['src/**/*.js'],
|
|
41
|
+
}];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import {DataType} from '@e22m4u/js-data-schema';
|
|
3
|
+
import {HttpMethod, TrieRouter} from '@e22m4u/js-trie-router';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
HttpData,
|
|
7
|
+
TrieRouterDataMapper,
|
|
8
|
+
} from '@e22m4u/js-trie-router-data-mapper';
|
|
9
|
+
|
|
10
|
+
const router = new TrieRouter();
|
|
11
|
+
router.useService(TrieRouterDataMapper);
|
|
12
|
+
|
|
13
|
+
// регистрация маршрута для разбора
|
|
14
|
+
// query параметра "filter"
|
|
15
|
+
router.defineRoute({
|
|
16
|
+
method: HttpMethod.GET,
|
|
17
|
+
path: '/parseQuery',
|
|
18
|
+
meta: {
|
|
19
|
+
dataMapper: {
|
|
20
|
+
filter: {
|
|
21
|
+
source: HttpData.REQUEST_QUERY,
|
|
22
|
+
property: 'filter',
|
|
23
|
+
schema: {
|
|
24
|
+
type: DataType.OBJECT,
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
handler: ({state: {filter}}) => {
|
|
31
|
+
return filter;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// создание экземпляра HTTP сервера
|
|
36
|
+
// и подключение обработчика запросов
|
|
37
|
+
const server = new http.Server();
|
|
38
|
+
server.on('request', router.requestListener);
|
|
39
|
+
|
|
40
|
+
// прослушивание входящих запросов
|
|
41
|
+
// на указанный адрес и порт
|
|
42
|
+
const port = 3000;
|
|
43
|
+
const host = '0.0.0.0';
|
|
44
|
+
server.listen(port, host, function () {
|
|
45
|
+
const cyan = '\x1b[36m%s\x1b[0m';
|
|
46
|
+
console.log(cyan, 'Server listening on port:', port);
|
|
47
|
+
console.log(
|
|
48
|
+
cyan,
|
|
49
|
+
'Open in browser:',
|
|
50
|
+
`http://${host}:${port}/parseQuery?filter={"foo":"bar"}`,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import {HttpMethod, TrieRouter} from '@e22m4u/js-trie-router';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
HttpData,
|
|
6
|
+
TrieRouterDataMapper,
|
|
7
|
+
} from '@e22m4u/js-trie-router-data-mapper';
|
|
8
|
+
|
|
9
|
+
const router = new TrieRouter();
|
|
10
|
+
router.useService(TrieRouterDataMapper);
|
|
11
|
+
|
|
12
|
+
// регистрация маршрута для проверки
|
|
13
|
+
// проекции возвращаемого объекта
|
|
14
|
+
router.defineRoute({
|
|
15
|
+
method: HttpMethod.GET,
|
|
16
|
+
path: '/responseProjection',
|
|
17
|
+
meta: {
|
|
18
|
+
dataMapper: {
|
|
19
|
+
response: {
|
|
20
|
+
source: HttpData.RESPONSE_BODY,
|
|
21
|
+
projection: {foo: true, bar: false},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
handler: () => {
|
|
26
|
+
return {foo: 10, bar: 20, baz: 30};
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// создание экземпляра HTTP сервера
|
|
31
|
+
// и подключение обработчика запросов
|
|
32
|
+
const server = new http.Server();
|
|
33
|
+
server.on('request', router.requestListener);
|
|
34
|
+
|
|
35
|
+
// прослушивание входящих запросов
|
|
36
|
+
// на указанный адрес и порт
|
|
37
|
+
const port = 3000;
|
|
38
|
+
const host = '0.0.0.0';
|
|
39
|
+
server.listen(port, host, function () {
|
|
40
|
+
const cyan = '\x1b[36m%s\x1b[0m';
|
|
41
|
+
console.log(cyan, 'Server listening on port:', port);
|
|
42
|
+
console.log(
|
|
43
|
+
cyan,
|
|
44
|
+
'Open in browser:',
|
|
45
|
+
`http://${host}:${port}/responseProjection`,
|
|
46
|
+
);
|
|
47
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@e22m4u/js-trie-router-data-mapper",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Парсинг, валидация и проекция данных для @e22m4u/js-trie-router",
|
|
5
|
+
"author": "Mikhail Evstropov <e22m4u@yandex.ru>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"data",
|
|
9
|
+
"trie",
|
|
10
|
+
"router",
|
|
11
|
+
"parsing",
|
|
12
|
+
"validation"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://gitrepos.ru/e22m4u/js-trie-router-data-mapper",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://gitrepos.ru/e22m4u/js-trie-router-data-mapper.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"types": "./src/index.d.ts",
|
|
21
|
+
"module": "./src/index.js",
|
|
22
|
+
"main": "./dist/cjs/index.cjs",
|
|
23
|
+
"exports": {
|
|
24
|
+
"types": "./src/index.d.ts",
|
|
25
|
+
"import": "./src/index.js",
|
|
26
|
+
"require": "./dist/cjs/index.cjs"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=12"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"lint": "tsc && eslint ./src",
|
|
33
|
+
"lint:fix": "tsc && eslint ./src --fix",
|
|
34
|
+
"format": "prettier --write \"./src/**/*.js\"",
|
|
35
|
+
"test": "npm run lint && c8 --reporter=text-summary mocha --bail",
|
|
36
|
+
"test:coverage": "npm run lint && c8 --reporter=text mocha --bail",
|
|
37
|
+
"build:cjs": "rimraf ./dist/cjs && node build-cjs.js",
|
|
38
|
+
"prepare": "husky"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@e22m4u/js-data-projector": "~0.2.0",
|
|
42
|
+
"@e22m4u/js-data-schema": "~0.0.7",
|
|
43
|
+
"@e22m4u/js-format": "~0.3.2",
|
|
44
|
+
"@e22m4u/js-service": "~0.5.1"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@e22m4u/js-trie-router": "~0.5.12"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@commitlint/cli": "~20.3.1",
|
|
51
|
+
"@commitlint/config-conventional": "~20.3.1",
|
|
52
|
+
"@eslint/js": "~9.39.2",
|
|
53
|
+
"@types/chai": "~5.2.3",
|
|
54
|
+
"@types/mocha": "~10.0.10",
|
|
55
|
+
"c8": "~10.1.3",
|
|
56
|
+
"chai": "~6.2.2",
|
|
57
|
+
"esbuild": "~0.27.2",
|
|
58
|
+
"eslint": "~9.39.2",
|
|
59
|
+
"eslint-config-prettier": "~10.1.8",
|
|
60
|
+
"eslint-plugin-chai-expect": "~3.1.0",
|
|
61
|
+
"eslint-plugin-import": "~2.32.0",
|
|
62
|
+
"eslint-plugin-jsdoc": "~62.0.0",
|
|
63
|
+
"eslint-plugin-mocha": "~11.2.0",
|
|
64
|
+
"globals": "~17.0.0",
|
|
65
|
+
"husky": "~9.1.7",
|
|
66
|
+
"mocha": "~11.7.5",
|
|
67
|
+
"prettier": "~3.7.4",
|
|
68
|
+
"rimraf": "~6.1.2",
|
|
69
|
+
"typescript": "~5.9.3"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {ProjectionSchema} from '@e22m4u/js-data-projector';
|
|
2
|
+
import {DataSchema, DataType} from '@e22m4u/js-data-schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Http data.
|
|
6
|
+
*/
|
|
7
|
+
export declare const HttpData: {
|
|
8
|
+
REQUEST_PARAMS: 'requestParams';
|
|
9
|
+
REQUEST_QUERY: 'requestQuery';
|
|
10
|
+
REQUEST_HEADERS: 'requestHeaders';
|
|
11
|
+
REQUEST_COOKIES: 'requestCookies';
|
|
12
|
+
REQUEST_BODY: 'requestBody';
|
|
13
|
+
RESPONSE_BODY: 'responseBody';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Http data.
|
|
18
|
+
*/
|
|
19
|
+
export type HttpData = (typeof HttpData)[keyof typeof HttpData];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Http data list.
|
|
23
|
+
*/
|
|
24
|
+
export declare const HTTP_DATA_LIST: HttpData[];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Data mapping schema.
|
|
28
|
+
*/
|
|
29
|
+
export type DataMappingSchema = {
|
|
30
|
+
[property: string]: DataMappingPropertyOptions | undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Data mapping property options.
|
|
35
|
+
*/
|
|
36
|
+
export interface DataMappingPropertyOptions {
|
|
37
|
+
source: HttpData;
|
|
38
|
+
property?: string;
|
|
39
|
+
schema?: DataSchema | DataType;
|
|
40
|
+
projection?: ProjectionSchema;
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Http data.
|
|
3
|
+
*/
|
|
4
|
+
export const HttpData = {
|
|
5
|
+
REQUEST_PARAMS: 'requestParams',
|
|
6
|
+
REQUEST_QUERY: 'requestQuery',
|
|
7
|
+
REQUEST_HEADERS: 'requestHeaders',
|
|
8
|
+
REQUEST_COOKIES: 'requestCookies',
|
|
9
|
+
REQUEST_BODY: 'requestBody',
|
|
10
|
+
RESPONSE_BODY: 'responseBody',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Http data list.
|
|
15
|
+
*/
|
|
16
|
+
export const HTTP_DATA_LIST = Object.values(HttpData);
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './route-meta.js';
|
|
2
|
+
export * from './data-mapping-schema.js';
|
|
3
|
+
export * from './trie-router-data-mapper.js';
|
|
4
|
+
export * from './validate-data-mapping-schema.js';
|
|
5
|
+
export {ProjectionSchemaRegistry} from '@e22m4u/js-data-projector';
|
|
6
|
+
export {DataType, DataSchemaRegistry} from '@e22m4u/js-data-schema';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export * from './data-mapping-schema.js';
|
|
2
|
+
export * from './trie-router-data-mapper.js';
|
|
3
|
+
export * from './validate-data-mapping-schema.js';
|
|
4
|
+
export {ProjectionSchemaRegistry} from '@e22m4u/js-data-projector';
|
|
5
|
+
export {DataType, DataSchemaRegistry} from '@e22m4u/js-data-schema';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {RequestContext} from '@e22m4u/js-trie-router';
|
|
2
|
+
import {DataMappingSchema} from './data-mapping-schema.js';
|
|
3
|
+
import {Service, ServiceContainer} from '@e22m4u/js-service';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Trie router data mapper.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TrieRouterDataMapper extends Service {
|
|
9
|
+
/**
|
|
10
|
+
* Constructor.
|
|
11
|
+
*
|
|
12
|
+
* @param container
|
|
13
|
+
*/
|
|
14
|
+
constructor(container?: ServiceContainer);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create state by mapping schema.
|
|
18
|
+
*
|
|
19
|
+
* @param ctx
|
|
20
|
+
* @param schema
|
|
21
|
+
*/
|
|
22
|
+
createStateByMappingSchema(
|
|
23
|
+
ctx: RequestContext,
|
|
24
|
+
schema: DataMappingSchema,
|
|
25
|
+
): object;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Filter response by mapping schema.
|
|
29
|
+
*
|
|
30
|
+
* @param data
|
|
31
|
+
* @param schema
|
|
32
|
+
*/
|
|
33
|
+
filterResponseByMappingSchema(
|
|
34
|
+
data: unknown,
|
|
35
|
+
schema: DataMappingSchema,
|
|
36
|
+
): unknown;
|
|
37
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Константа HttpData определяет какое свойство контекста
|
|
11
|
+
* запроса будет использовано для формирования данных.
|
|
12
|
+
* Этот объект связывает значения HttpData со свойствами
|
|
13
|
+
* контекста запроса.
|
|
14
|
+
*/
|
|
15
|
+
const HTTP_DATA_TO_CONTEXT_PROPERTY_MAP = {
|
|
16
|
+
[HttpData.REQUEST_PARAMS]: 'params',
|
|
17
|
+
[HttpData.REQUEST_QUERY]: 'query',
|
|
18
|
+
[HttpData.REQUEST_HEADERS]: 'headers',
|
|
19
|
+
[HttpData.REQUEST_COOKIES]: 'cookies',
|
|
20
|
+
[HttpData.REQUEST_BODY]: 'body',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Trie router data mapper.
|
|
25
|
+
*/
|
|
26
|
+
export class TrieRouterDataMapper extends Service {
|
|
27
|
+
/**
|
|
28
|
+
* Constructor.
|
|
29
|
+
*
|
|
30
|
+
* @param {import('@e22m4u/js-service').ServiceContainer} container
|
|
31
|
+
*/
|
|
32
|
+
constructor(container) {
|
|
33
|
+
super(container);
|
|
34
|
+
const router = this.getService(TrieRouter);
|
|
35
|
+
if (!router.hasPreHandler(dataMappingPreHandler)) {
|
|
36
|
+
router.addPreHandler(dataMappingPreHandler);
|
|
37
|
+
}
|
|
38
|
+
if (!router.hasPostHandler(dataMappingPostHandler)) {
|
|
39
|
+
router.addPostHandler(dataMappingPostHandler);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create state by mapping schema.
|
|
45
|
+
*
|
|
46
|
+
* @param {import('@e22m4u/js-trie-router').RequestContext} ctx
|
|
47
|
+
* @param {import('./data-mapping-schema.js').DataMappingSchema} schema
|
|
48
|
+
* @returns {object}
|
|
49
|
+
*/
|
|
50
|
+
createStateByMappingSchema(ctx, schema) {
|
|
51
|
+
if (!(ctx instanceof RequestContext)) {
|
|
52
|
+
throw new InvalidArgumentError(
|
|
53
|
+
'Parameter "ctx" must be a RequestContext instance, but %v was given.',
|
|
54
|
+
ctx,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
validateDataMappingSchema(schema);
|
|
58
|
+
const res = {};
|
|
59
|
+
const dataParser = this.getService(DataParser);
|
|
60
|
+
const dataProjector = this.getService(DataProjector);
|
|
61
|
+
// обход каждого свойства схемы
|
|
62
|
+
// для формирования объекта данных
|
|
63
|
+
Object.keys(schema).forEach(propName => {
|
|
64
|
+
// если параметры свойства не определены,
|
|
65
|
+
// то данное свойство пропускается
|
|
66
|
+
const propOptions = schema[propName];
|
|
67
|
+
if (propOptions === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// если свойство контекста не определено,
|
|
71
|
+
// то данное свойство пропускается
|
|
72
|
+
const ctxProp = HTTP_DATA_TO_CONTEXT_PROPERTY_MAP[propOptions.source];
|
|
73
|
+
if (ctxProp === undefined) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let value = ctx[ctxProp];
|
|
77
|
+
// если определено вложенное свойство,
|
|
78
|
+
// то выполняется попытка его извлечения
|
|
79
|
+
if (propOptions.property && typeof propOptions.property === 'string') {
|
|
80
|
+
// если свойство контекста содержит объект,
|
|
81
|
+
// то извлекается значение вложенного свойства
|
|
82
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
83
|
+
value = value[propOptions.property];
|
|
84
|
+
}
|
|
85
|
+
// если свойство контекста не содержит
|
|
86
|
+
// объект, то выбрасывается ошибка
|
|
87
|
+
else {
|
|
88
|
+
throw new InvalidArgumentError(
|
|
89
|
+
'Property %v does not exist in %v value ' +
|
|
90
|
+
'from the property %v of the request context.',
|
|
91
|
+
propOptions.property,
|
|
92
|
+
value,
|
|
93
|
+
ctxProp,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// если определена схема данных,
|
|
98
|
+
// то выполняется разбор значения
|
|
99
|
+
if (propOptions.schema !== undefined) {
|
|
100
|
+
const sourcePath = propOptions.property
|
|
101
|
+
? `request.${ctxProp}.${propOptions.property}`
|
|
102
|
+
: `request.${ctxProp}`;
|
|
103
|
+
if (DATA_TYPE_LIST.includes(propOptions.schema)) {
|
|
104
|
+
const dataSchema = {type: propOptions.schema};
|
|
105
|
+
value = dataParser.parse(value, dataSchema, {sourcePath});
|
|
106
|
+
} else {
|
|
107
|
+
value = dataParser.parse(value, propOptions.schema, {sourcePath});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// если определена схема проекции,
|
|
111
|
+
// то выполняется создание проекции
|
|
112
|
+
if (propOptions.projection !== undefined) {
|
|
113
|
+
value = dataProjector.project(value, propOptions.projection);
|
|
114
|
+
}
|
|
115
|
+
// значение присваивается
|
|
116
|
+
// результирующему объекту
|
|
117
|
+
res[propName] = value;
|
|
118
|
+
});
|
|
119
|
+
return res;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Filter response by mapping schema.
|
|
124
|
+
*
|
|
125
|
+
* @param {*} data
|
|
126
|
+
* @param {import('./data-mapping-schema.js').DataMappingSchema} schema
|
|
127
|
+
* @returns {*}
|
|
128
|
+
*/
|
|
129
|
+
filterResponseByMappingSchema(data, schema) {
|
|
130
|
+
validateDataMappingSchema(schema);
|
|
131
|
+
let res = data;
|
|
132
|
+
const dataParser = this.getService(DataParser);
|
|
133
|
+
const dataProjector = this.getService(DataProjector);
|
|
134
|
+
// обход каждого свойства схемы
|
|
135
|
+
// для формирования данных ответа
|
|
136
|
+
Object.keys(schema).forEach(propName => {
|
|
137
|
+
// если параметры свойства не определены,
|
|
138
|
+
// то данное свойство пропускается
|
|
139
|
+
const propOptions = schema[propName];
|
|
140
|
+
if (propOptions === undefined) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// если источником не является тело ответа,
|
|
144
|
+
// то данное свойство пропускается
|
|
145
|
+
if (propOptions.source !== HttpData.RESPONSE_BODY) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// если определено вложенное свойство,
|
|
149
|
+
// то выбрасывается ошибка
|
|
150
|
+
if (propOptions.property !== undefined) {
|
|
151
|
+
throw new InvalidArgumentError(
|
|
152
|
+
'Option "property" is not supported for the %v source, ' +
|
|
153
|
+
'but %v was given.',
|
|
154
|
+
propOptions.property,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
// если определена схема данных, то выполняется
|
|
158
|
+
// разбор значения без валидации данных
|
|
159
|
+
if (propOptions.schema !== undefined) {
|
|
160
|
+
const sourcePath = 'response.body';
|
|
161
|
+
const parsingOptions = {sourcePath, noParsingErrors: true};
|
|
162
|
+
if (DATA_TYPE_LIST.includes(propOptions.schema)) {
|
|
163
|
+
const dataSchema = {type: propOptions.schema};
|
|
164
|
+
res = dataParser.parse(res, dataSchema, parsingOptions);
|
|
165
|
+
} else {
|
|
166
|
+
res = dataParser.parse(res, propOptions.schema, parsingOptions);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// если определена схема проекции,
|
|
170
|
+
// то выполняется создание проекции
|
|
171
|
+
if (propOptions.projection !== undefined) {
|
|
172
|
+
res = dataProjector.project(res, propOptions.projection);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return res;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Data mapping pre-handler.
|
|
181
|
+
*
|
|
182
|
+
* @type {import('@e22m4u/js-trie-router').PreHandlerHook}
|
|
183
|
+
*/
|
|
184
|
+
function dataMappingPreHandler(ctx) {
|
|
185
|
+
const schema = (ctx.meta || {}).dataMapper;
|
|
186
|
+
if (schema === undefined) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const mapper = ctx.container.get(TrieRouterDataMapper);
|
|
190
|
+
const state = mapper.createStateByMappingSchema(ctx, schema);
|
|
191
|
+
ctx.state = {...ctx.state, ...state};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Data mapping post handler.
|
|
196
|
+
*
|
|
197
|
+
* @type {import('@e22m4u/js-trie-router').PostHandlerHook}
|
|
198
|
+
*/
|
|
199
|
+
function dataMappingPostHandler(ctx, data) {
|
|
200
|
+
const schema = (ctx.meta || {}).dataMapper;
|
|
201
|
+
if (schema === undefined) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const mapper = ctx.container.get(TrieRouterDataMapper);
|
|
205
|
+
return mapper.filterResponseByMappingSchema(data, schema);
|
|
206
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {InvalidArgumentError} from '@e22m4u/js-format';
|
|
2
|
+
import {HTTP_DATA_LIST} from './data-mapping-schema.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validate data mapping schema.
|
|
6
|
+
*
|
|
7
|
+
* @param {import('./data-mapping-schema.js').DataMappingSchema} schema
|
|
8
|
+
*/
|
|
9
|
+
export function validateDataMappingSchema(schema) {
|
|
10
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
11
|
+
throw new InvalidArgumentError(
|
|
12
|
+
'Mapping schema must be an Object, but %v was given.',
|
|
13
|
+
schema,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
// schema[k]
|
|
17
|
+
Object.keys(schema).forEach(propName => {
|
|
18
|
+
const propOptions = schema[propName];
|
|
19
|
+
if (propOptions === undefined) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (
|
|
23
|
+
!propOptions ||
|
|
24
|
+
typeof propOptions !== 'object' ||
|
|
25
|
+
Array.isArray(propOptions)
|
|
26
|
+
) {
|
|
27
|
+
throw new InvalidArgumentError(
|
|
28
|
+
'Property options must be an Object, but %v was given.',
|
|
29
|
+
propOptions,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
// schema[k].source
|
|
33
|
+
if (
|
|
34
|
+
!propOptions.source ||
|
|
35
|
+
typeof propOptions.source !== 'string' ||
|
|
36
|
+
!HTTP_DATA_LIST.includes(propOptions.source)
|
|
37
|
+
) {
|
|
38
|
+
throw new InvalidArgumentError(
|
|
39
|
+
'Data source %v is not supported.',
|
|
40
|
+
propOptions.source,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
// schema[k].property
|
|
44
|
+
if (
|
|
45
|
+
propOptions.property !== undefined &&
|
|
46
|
+
(!propOptions.property || typeof propOptions.property !== 'string')
|
|
47
|
+
) {
|
|
48
|
+
throw new InvalidArgumentError(
|
|
49
|
+
'Property name must be a non-empty String, but %v was given.',
|
|
50
|
+
propOptions.property,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {expect} from 'chai';
|
|
2
|
+
import {format} from '@e22m4u/js-format';
|
|
3
|
+
import {HttpData} from './data-mapping-schema.js';
|
|
4
|
+
import {validateDataMappingSchema} from './validate-data-mapping-schema.js';
|
|
5
|
+
|
|
6
|
+
describe('validateDataMappingSchema', function () {
|
|
7
|
+
it('should require the "schema" parameter to be an Object', function () {
|
|
8
|
+
const throwable = v => () => validateDataMappingSchema(v);
|
|
9
|
+
const error = s =>
|
|
10
|
+
format('Mapping schema must be an Object, but %s was given.', s);
|
|
11
|
+
expect(throwable('str')).to.throw(error('"str"'));
|
|
12
|
+
expect(throwable('')).to.throw(error('""'));
|
|
13
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
14
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
15
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
16
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
17
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
18
|
+
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
19
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
20
|
+
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
21
|
+
throwable({})();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should require property options to be an Object', function () {
|
|
25
|
+
const throwable = v => () => validateDataMappingSchema({prop: v});
|
|
26
|
+
const error = s =>
|
|
27
|
+
format('Property options must be an Object, but %s was given.', s);
|
|
28
|
+
expect(throwable('str')).to.throw(error('"str"'));
|
|
29
|
+
expect(throwable('')).to.throw(error('""'));
|
|
30
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
31
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
32
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
33
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
34
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
35
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
36
|
+
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
37
|
+
throwable({source: HttpData.REQUEST_BODY})();
|
|
38
|
+
throwable(undefined)();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should require the "source" option to be a HttpData value', function () {
|
|
42
|
+
const throwable = v => () => validateDataMappingSchema({prop: {source: v}});
|
|
43
|
+
const error = s => format('Data source %s is not supported.', s);
|
|
44
|
+
expect(throwable('str')).to.throw(error('"str"'));
|
|
45
|
+
expect(throwable('')).to.throw(error('""'));
|
|
46
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
47
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
48
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
49
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
50
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
51
|
+
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
52
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
53
|
+
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
54
|
+
throwable(HttpData.REQUEST_BODY)();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should require the "property" option to be a non-empty String', function () {
|
|
58
|
+
const throwable = v => () =>
|
|
59
|
+
validateDataMappingSchema({
|
|
60
|
+
prop: {source: HttpData.REQUEST_BODY, property: v},
|
|
61
|
+
});
|
|
62
|
+
const error = s =>
|
|
63
|
+
format('Property name must be a non-empty String, but %s was given.', s);
|
|
64
|
+
expect(throwable('')).to.throw(error('""'));
|
|
65
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
66
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
67
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
68
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
69
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
70
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
71
|
+
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
72
|
+
throwable('str')();
|
|
73
|
+
throwable(undefined)();
|
|
74
|
+
});
|
|
75
|
+
});
|
package/tsconfig.json
ADDED