@e22m4u/js-repository-json-schema 0.0.1
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 +120 -0
- package/build-cjs.js +16 -0
- package/eslint.config.js +41 -0
- package/package.json +67 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/json-schema-generator.d.ts +40 -0
- package/src/json-schema-generator.js +377 -0
- package/src/json-schema-generator.spec.js +77 -0
- package/tsconfig.json +15 -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) 2025-2026 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,120 @@
|
|
|
1
|
+
## @e22m4u/js-repository-json-schema
|
|
2
|
+
|
|
3
|
+
Модуль генерации *JSON Schema (Draft 2020-12)* для
|
|
4
|
+
[@e22m4u/js-repository](http://www.npmjs.com/package/@e22m4u/js-repository)
|
|
5
|
+
|
|
6
|
+
## Содержание
|
|
7
|
+
|
|
8
|
+
- [Установка](#установка)
|
|
9
|
+
- [Использование](#использование)
|
|
10
|
+
- [Параметры](#параметры)
|
|
11
|
+
- [Тесты](#тесты)
|
|
12
|
+
- [Лицензия](#лицензия)
|
|
13
|
+
|
|
14
|
+
## Установка
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @e22m4u/js-repository-json-schema
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Модуль поддерживает ESM и CommonJS стандарты.
|
|
21
|
+
|
|
22
|
+
*ESM*
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import {JsonSchemaGenerator} from '@e22m4u/js-repository-json-schema';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
*CommonJS*
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const {JsonSchemaGenerator} = require('@e22m4u/js-repository-json-schema');
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Использование
|
|
35
|
+
|
|
36
|
+
Определение простой модели и генерация для нее JSON-схемы.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import {DataType, DatabaseSchema} from '@e22m4u/js-repository';
|
|
40
|
+
import {JsonSchemaGenerator} from '@e22m4u/js-repository-json-schema';
|
|
41
|
+
|
|
42
|
+
// создание схемы БД, определение источника данных и модели
|
|
43
|
+
const dbs = new DatabaseSchema();
|
|
44
|
+
|
|
45
|
+
dbs.defineModel({
|
|
46
|
+
name: 'user',
|
|
47
|
+
properties: {
|
|
48
|
+
firstName: {
|
|
49
|
+
type: DataType.STRING,
|
|
50
|
+
required: true, // поле будет добавлено в массив "required"
|
|
51
|
+
},
|
|
52
|
+
age: {
|
|
53
|
+
type: DataType.NUMBER,
|
|
54
|
+
default: 18, // будет добавлено поле "default"
|
|
55
|
+
},
|
|
56
|
+
isActive: DataType.BOOLEAN,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// получение генератора из сервис-контейнера
|
|
61
|
+
const generator = dbs.getService(JsonSchemaGenerator);
|
|
62
|
+
|
|
63
|
+
// генерация JSON-схемы по названию модели
|
|
64
|
+
const schema = generator.genSchema('user');
|
|
65
|
+
|
|
66
|
+
console.log(schema);
|
|
67
|
+
// {
|
|
68
|
+
// "type": "object",
|
|
69
|
+
// "properties": {
|
|
70
|
+
// "firstName": {
|
|
71
|
+
// "type": "string",
|
|
72
|
+
// "example": ""
|
|
73
|
+
// },
|
|
74
|
+
// "age": {
|
|
75
|
+
// "type": "number",
|
|
76
|
+
// "default": 18
|
|
77
|
+
// },
|
|
78
|
+
// "isActive": {
|
|
79
|
+
// "type": "boolean",
|
|
80
|
+
// "example": false
|
|
81
|
+
// }
|
|
82
|
+
// },
|
|
83
|
+
// "required": [
|
|
84
|
+
// "firstName"
|
|
85
|
+
// ]
|
|
86
|
+
// }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
*i. Если для модели определен источник данных, то генератор автоматически
|
|
90
|
+
добавит первичный ключ (*id*), если он не был описан вручную.*
|
|
91
|
+
|
|
92
|
+
## Параметры
|
|
93
|
+
|
|
94
|
+
Метод `genSchema` принимает второй необязательный аргумент с настройками схемы.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const schema = generator.genSchema('user', {
|
|
98
|
+
// исключить определенные свойства
|
|
99
|
+
// (например, пароли или внутренние ключи)
|
|
100
|
+
excludeProperties: ['password', 'internalToken'],
|
|
101
|
+
|
|
102
|
+
// пользовательская фабрика для генерации $ref ссылок
|
|
103
|
+
// по умолчанию modelName => ({ $ref: `#/components/schemas/${modelName}` })
|
|
104
|
+
refFactory: (modelName) => ({$ref: `#/components/schemas/${modelName}Input`}),
|
|
105
|
+
|
|
106
|
+
// тип первичных и внешних ключей по умолчанию
|
|
107
|
+
// "number" или "string" (по умолчанию "number")
|
|
108
|
+
defaultPrimaryKeyType: 'string',
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Тесты
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run test
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Лицензия
|
|
119
|
+
|
|
120
|
+
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: ['node18'],
|
|
10
|
+
bundle: true,
|
|
11
|
+
keepNames: true,
|
|
12
|
+
external: [
|
|
13
|
+
...Object.keys(packageJson.peerDependencies || {}),
|
|
14
|
+
...Object.keys(packageJson.dependencies || {}),
|
|
15
|
+
],
|
|
16
|
+
});
|
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
|
+
}];
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@e22m4u/js-repository-json-schema",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Модуль генерации JSON Schema для @e22m4u/js-repository",
|
|
5
|
+
"author": "Mikhail Evstropov <e22m4u@yandex.ru>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"repository",
|
|
9
|
+
"json-schema"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://gitverse.ru/e22m4u/js-repository-json-schema",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://gitverse.ru/e22m4u/js-repository-json-schema.git"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"types": "./src/index.d.ts",
|
|
18
|
+
"module": "./src/index.js",
|
|
19
|
+
"main": "./dist/cjs/index.cjs",
|
|
20
|
+
"exports": {
|
|
21
|
+
"types": "./src/index.d.ts",
|
|
22
|
+
"import": "./src/index.js",
|
|
23
|
+
"require": "./dist/cjs/index.cjs"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"lint": "tsc && eslint ./src",
|
|
30
|
+
"lint:fix": "tsc && eslint ./src --fix",
|
|
31
|
+
"format": "prettier --write \"./src/**/*.js\"",
|
|
32
|
+
"test": "npm run lint && c8 --reporter=text-summary mocha --bail",
|
|
33
|
+
"test:coverage": "npm run lint && c8 --reporter=text mocha --bail",
|
|
34
|
+
"build:cjs": "rimraf ./dist/cjs && node build-cjs.js",
|
|
35
|
+
"prepare": "husky"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@e22m4u/js-format": "~0.4.0",
|
|
39
|
+
"@e22m4u/js-service": "~0.6.2"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@e22m4u/js-repository": "~0.8.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@commitlint/cli": "~21.0.2",
|
|
46
|
+
"@commitlint/config-conventional": "~21.0.2",
|
|
47
|
+
"@eslint/js": "~9.39.2",
|
|
48
|
+
"@types/chai": "~5.2.3",
|
|
49
|
+
"@types/mocha": "~10.0.10",
|
|
50
|
+
"@types/node": "~25.9.2",
|
|
51
|
+
"c8": "~11.0.0",
|
|
52
|
+
"chai": "~6.2.2",
|
|
53
|
+
"esbuild": "~0.28.0",
|
|
54
|
+
"eslint": "~9.39.2",
|
|
55
|
+
"eslint-config-prettier": "~10.1.8",
|
|
56
|
+
"eslint-plugin-chai-expect": "~4.1.0",
|
|
57
|
+
"eslint-plugin-import": "~2.32.0",
|
|
58
|
+
"eslint-plugin-jsdoc": "~63.0.2",
|
|
59
|
+
"eslint-plugin-mocha": "~11.3.0",
|
|
60
|
+
"globals": "~17.6.0",
|
|
61
|
+
"husky": "~9.1.7",
|
|
62
|
+
"mocha": "~11.7.6",
|
|
63
|
+
"prettier": "~3.8.3",
|
|
64
|
+
"rimraf": "~6.1.3",
|
|
65
|
+
"typescript": "~6.0.3"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './json-schema-generator.js';
|
package/src/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './json-schema-generator.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {Service} from '@e22m4u/js-service';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Структура, которую возвращает генератор.
|
|
5
|
+
*/
|
|
6
|
+
interface JsonSchema {
|
|
7
|
+
type?: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
|
8
|
+
properties?: Record<string, JsonSchema>;
|
|
9
|
+
required?: string[];
|
|
10
|
+
items?: JsonSchema;
|
|
11
|
+
example?: unknown;
|
|
12
|
+
default?: unknown;
|
|
13
|
+
$ref?: string;
|
|
14
|
+
allOf?: JsonSchema[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Опции, которые принимает метод.
|
|
19
|
+
*/
|
|
20
|
+
interface JsonSchemaGeneratorOptions {
|
|
21
|
+
excludeProperties?: string[];
|
|
22
|
+
refFactory?: (modelName: string) => {$ref: string};
|
|
23
|
+
defaultPrimaryKeyType?: 'number' | 'string';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Json schema generator.
|
|
28
|
+
*/
|
|
29
|
+
export class JsonSchemaGenerator extends Service {
|
|
30
|
+
/**
|
|
31
|
+
* Сгенерировать JSON Schema для указанной модели.
|
|
32
|
+
*
|
|
33
|
+
* @param modelName Название модели
|
|
34
|
+
* @param options Опции генерации
|
|
35
|
+
*/
|
|
36
|
+
genSchema(
|
|
37
|
+
modelName: string,
|
|
38
|
+
options?: JsonSchemaGeneratorOptions,
|
|
39
|
+
): JsonSchema;
|
|
40
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import {Service} from '@e22m4u/js-service';
|
|
2
|
+
import {InvalidArgumentError} from '@e22m4u/js-format';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
DataType,
|
|
6
|
+
singularize,
|
|
7
|
+
RelationType,
|
|
8
|
+
DefinitionRegistry,
|
|
9
|
+
ModelDefinitionUtils,
|
|
10
|
+
DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
|
|
11
|
+
} from '@e22m4u/js-repository';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Сервис генерации JSON Schema (OpenAPI/Swagger совместимой)
|
|
15
|
+
* на основе определений моделей репозитория.
|
|
16
|
+
*/
|
|
17
|
+
export class JsonSchemaGenerator extends Service {
|
|
18
|
+
/**
|
|
19
|
+
* Сгенерировать JSON Schema для указанной модели.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} modelName Название модели
|
|
22
|
+
* @param {object} [options] Опции генерации
|
|
23
|
+
* @param {string[]} [options.excludeProperties] Массив свойств для исключения из схемы
|
|
24
|
+
* @param {Function} [options.refFactory] Функция для создания $ref строк
|
|
25
|
+
* @param {string} [options.defaultPrimaryKeyType] Тип по умолчанию для Primary Key и Foreign Key ('number' или 'string')
|
|
26
|
+
* @returns {object} JSON Schema
|
|
27
|
+
*/
|
|
28
|
+
genSchema(modelName, options = {}) {
|
|
29
|
+
// modelName
|
|
30
|
+
if (!modelName || typeof modelName !== 'string') {
|
|
31
|
+
throw new InvalidArgumentError(
|
|
32
|
+
'Parameter "modelName" must be a non-empty String, but %v was given.',
|
|
33
|
+
modelName,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
// options
|
|
37
|
+
if (
|
|
38
|
+
options === null ||
|
|
39
|
+
typeof options !== 'object' ||
|
|
40
|
+
Array.isArray(options)
|
|
41
|
+
) {
|
|
42
|
+
throw new InvalidArgumentError(
|
|
43
|
+
'Parameter "options" must be an Object, but %v was given.',
|
|
44
|
+
options,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
// проверка опций и инициализация
|
|
48
|
+
// значений по умолчанию
|
|
49
|
+
this._validateOptions(options);
|
|
50
|
+
const opts = this._normalizeOptions(options);
|
|
51
|
+
// получение определения модели из реестра
|
|
52
|
+
const registry = this.getService(DefinitionRegistry);
|
|
53
|
+
const modelDef = registry.getModel(modelName);
|
|
54
|
+
// базовый каркас схемы
|
|
55
|
+
const schema = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {},
|
|
58
|
+
};
|
|
59
|
+
const requiredFields = [];
|
|
60
|
+
const propertiesDef = modelDef.properties || {};
|
|
61
|
+
// обработка неявного первичного ключа (primary key)
|
|
62
|
+
this._injectImplicitPrimaryKeyIfNeeded(
|
|
63
|
+
modelDef,
|
|
64
|
+
propertiesDef,
|
|
65
|
+
schema,
|
|
66
|
+
opts,
|
|
67
|
+
);
|
|
68
|
+
// обработка явно заданных свойств модели
|
|
69
|
+
for (const [propName, propDef] of Object.entries(propertiesDef)) {
|
|
70
|
+
if (opts.excludeProperties.includes(propName)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
schema.properties[propName] = this._mapPropertyToSchema(propDef, opts);
|
|
74
|
+
// если свойство явно помечено как обязательное (и не имеет default)
|
|
75
|
+
if (propDef && typeof propDef === 'object' && propDef.required) {
|
|
76
|
+
requiredFields.push(propName);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// обработка неявных внешних ключей от связей
|
|
80
|
+
// (foreign keys & discriminators)
|
|
81
|
+
this._injectImplicitForeignKeys(modelDef, propertiesDef, schema, opts);
|
|
82
|
+
// добавление массива required, если есть обязательные поля
|
|
83
|
+
if (requiredFields.length > 0) {
|
|
84
|
+
schema.required = requiredFields;
|
|
85
|
+
}
|
|
86
|
+
// обработка наследования (иерархия моделей)
|
|
87
|
+
// если у модели есть базовая модель, мы не разворачиваем её свойства,
|
|
88
|
+
// а используем allOf с ссылкой ($ref) на родительскую схему
|
|
89
|
+
if (modelDef.base) {
|
|
90
|
+
return {
|
|
91
|
+
allOf: [opts.refFactory(modelDef.base), schema],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return schema;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Проверка опций генератора.
|
|
99
|
+
*
|
|
100
|
+
* @param {object} options
|
|
101
|
+
* @private
|
|
102
|
+
*/
|
|
103
|
+
_validateOptions(options) {
|
|
104
|
+
// excludeProperties
|
|
105
|
+
if (options.excludeProperties !== undefined) {
|
|
106
|
+
if (!Array.isArray(options.excludeProperties)) {
|
|
107
|
+
throw new InvalidArgumentError(
|
|
108
|
+
'Option "excludeProperties" must be an Array, but %v was given.',
|
|
109
|
+
options.excludeProperties,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
// excludeProperties[n]
|
|
113
|
+
options.excludeProperties.forEach((propertyName, index) => {
|
|
114
|
+
if (!propertyName || typeof propertyName !== 'string') {
|
|
115
|
+
throw new InvalidArgumentError(
|
|
116
|
+
'Element %d of the option "excludeProperties" ' +
|
|
117
|
+
'must be a non-empty String, but %v was given.',
|
|
118
|
+
index,
|
|
119
|
+
propertyName,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// refFactory
|
|
125
|
+
if (
|
|
126
|
+
options.refFactory !== undefined &&
|
|
127
|
+
typeof options.refFactory !== 'function'
|
|
128
|
+
) {
|
|
129
|
+
throw new InvalidArgumentError(
|
|
130
|
+
'Option "refFactory" must be a Function, but %v was given.',
|
|
131
|
+
options.refFactory,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
// defaultPrimaryKeyType
|
|
135
|
+
if (
|
|
136
|
+
options.defaultPrimaryKeyType !== undefined &&
|
|
137
|
+
!['string', 'number'].includes(options.defaultPrimaryKeyType)
|
|
138
|
+
) {
|
|
139
|
+
throw new InvalidArgumentError(
|
|
140
|
+
'Option "defaultPrimaryKeyType" allows "number" ' +
|
|
141
|
+
'or "string" value, but %v was given.',
|
|
142
|
+
options.defaultPrimaryKeyType,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Нормализация опций генератора.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} options
|
|
151
|
+
* @returns {object}
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
_normalizeOptions(options) {
|
|
155
|
+
return {
|
|
156
|
+
excludeProperties: options.excludeProperties || [],
|
|
157
|
+
refFactory:
|
|
158
|
+
options.refFactory ||
|
|
159
|
+
(modelName => ({$ref: `#/components/schemas/${modelName}`})),
|
|
160
|
+
defaultPrimaryKeyType: options.defaultPrimaryKeyType || 'number',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Преобразование определения свойства
|
|
166
|
+
* репозитория в JSON Schema объект.
|
|
167
|
+
*
|
|
168
|
+
* @param {string|object} propDef Определение свойства
|
|
169
|
+
* @param {object} opts Настройки генератора
|
|
170
|
+
* @returns {object}
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
_mapPropertyToSchema(propDef, opts) {
|
|
174
|
+
// если передана краткая форма (просто строка DataType)
|
|
175
|
+
if (typeof propDef === 'string') {
|
|
176
|
+
return this._createSchemaByType(propDef);
|
|
177
|
+
}
|
|
178
|
+
if (!propDef || typeof propDef !== 'object') {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
// если это вложенный объект, ссылающийся на другую модель
|
|
182
|
+
if (propDef.type === DataType.OBJECT && propDef.model) {
|
|
183
|
+
return opts.refFactory(propDef.model);
|
|
184
|
+
}
|
|
185
|
+
// если это массив элементов, ссылающихся на другую модель
|
|
186
|
+
if (propDef.type === DataType.ARRAY && propDef.itemModel) {
|
|
187
|
+
return {
|
|
188
|
+
type: 'array',
|
|
189
|
+
items: opts.refFactory(propDef.itemModel),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// получение базовой схемы с учетом типа
|
|
193
|
+
const schema = this._createSchemaByType(propDef.type);
|
|
194
|
+
// если это массив примитивов (itemType)
|
|
195
|
+
if (propDef.type === DataType.ARRAY && propDef.itemType) {
|
|
196
|
+
schema.items = this._createSchemaByType(propDef.itemType);
|
|
197
|
+
}
|
|
198
|
+
// установка реального default, если он задан,
|
|
199
|
+
// то example удаляется, чтобы не дублировать логику
|
|
200
|
+
if (propDef.default !== undefined) {
|
|
201
|
+
schema.default =
|
|
202
|
+
typeof propDef.default === 'function'
|
|
203
|
+
? propDef.default()
|
|
204
|
+
: propDef.default;
|
|
205
|
+
delete schema.example;
|
|
206
|
+
}
|
|
207
|
+
return schema;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Создание примитивной схемы в зависимости от типа данных.
|
|
212
|
+
* Добавляет 'example' с пустым значением для Swagger.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} dataType Тип данных
|
|
215
|
+
* @returns {object}
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
_createSchemaByType(dataType) {
|
|
219
|
+
switch (dataType) {
|
|
220
|
+
case DataType.STRING:
|
|
221
|
+
return {type: 'string', example: ''};
|
|
222
|
+
case DataType.NUMBER:
|
|
223
|
+
return {type: 'number', example: 0};
|
|
224
|
+
case DataType.BOOLEAN:
|
|
225
|
+
return {type: 'boolean', example: false};
|
|
226
|
+
case DataType.ARRAY:
|
|
227
|
+
return {type: 'array', example: []};
|
|
228
|
+
case DataType.OBJECT:
|
|
229
|
+
return {type: 'object', example: {}};
|
|
230
|
+
case DataType.ANY:
|
|
231
|
+
return {example: ''}; // any type
|
|
232
|
+
default:
|
|
233
|
+
// фолбэк для нераспознанных типов
|
|
234
|
+
// (например, если передали 'string' напрямую)
|
|
235
|
+
return {type: dataType, example: dataType === 'number' ? 0 : ''};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Добавление неявного первичного ключа, если он не был задан
|
|
241
|
+
* в свойствах текущей модели и если модель не наследуется
|
|
242
|
+
* от другой.
|
|
243
|
+
*
|
|
244
|
+
* @param {object} modelDef
|
|
245
|
+
* @param {object} propertiesDef
|
|
246
|
+
* @param {object} schema
|
|
247
|
+
* @param {object} opts
|
|
248
|
+
* @private
|
|
249
|
+
*/
|
|
250
|
+
_injectImplicitPrimaryKeyIfNeeded(modelDef, propertiesDef, schema, opts) {
|
|
251
|
+
// если источник данных не указан,
|
|
252
|
+
// неявный первичный ключ генерировать не нужно
|
|
253
|
+
if (!modelDef.datasource) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// если модель наследуется, то предполагается,
|
|
257
|
+
// что первичный ключ определен у родителя
|
|
258
|
+
if (modelDef.base) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// поиск явно заданного первичного ключа в свойствах
|
|
262
|
+
const hasExplicitPk = Object.values(propertiesDef).some(
|
|
263
|
+
prop => prop && typeof prop === 'object' && prop.primaryKey,
|
|
264
|
+
);
|
|
265
|
+
// если явного первичного ключа нет и поле "id" (стандартное) не описано
|
|
266
|
+
if (!hasExplicitPk && !propertiesDef[DEFAULT_PRIMARY_KEY_PROPERTY_NAME]) {
|
|
267
|
+
if (!opts.excludeProperties.includes(DEFAULT_PRIMARY_KEY_PROPERTY_NAME)) {
|
|
268
|
+
schema.properties[DEFAULT_PRIMARY_KEY_PROPERTY_NAME] =
|
|
269
|
+
this._createSchemaByType(opts.defaultPrimaryKeyType);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Инъекция свойств для хранения внешних ключей и дискриминаторов,
|
|
276
|
+
* которые возникают из-за связей (relations), но не описаны явно
|
|
277
|
+
* в properties.
|
|
278
|
+
*
|
|
279
|
+
* @param {object} modelDef
|
|
280
|
+
* @param {object} propertiesDef
|
|
281
|
+
* @param {object} schema
|
|
282
|
+
* @param {object} opts
|
|
283
|
+
* @private
|
|
284
|
+
*/
|
|
285
|
+
_injectImplicitForeignKeys(modelDef, propertiesDef, schema, opts) {
|
|
286
|
+
const relations = modelDef.relations || {};
|
|
287
|
+
for (const [relName, relDef] of Object.entries(relations)) {
|
|
288
|
+
// определение типа ключа для текущей связи
|
|
289
|
+
const foreignKeyDataType = this._resolveForeignKeyDataType(relDef, opts);
|
|
290
|
+
// обработка связи belongsTo
|
|
291
|
+
// (хранит foreign key и, опционально, discriminator)
|
|
292
|
+
if (relDef.type === RelationType.BELONGS_TO) {
|
|
293
|
+
const foreignKey = relDef.foreignKey || `${relName}Id`;
|
|
294
|
+
// внешний ключ
|
|
295
|
+
if (
|
|
296
|
+
!propertiesDef[foreignKey] &&
|
|
297
|
+
!opts.excludeProperties.includes(foreignKey)
|
|
298
|
+
) {
|
|
299
|
+
schema.properties[foreignKey] = this._createSchemaByType(
|
|
300
|
+
foreignKeyDataType,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
// дискриминатор (для полиморфных связей)
|
|
304
|
+
if (relDef.polymorphic) {
|
|
305
|
+
const discriminator = relDef.discriminator || `${relName}Type`;
|
|
306
|
+
if (
|
|
307
|
+
!propertiesDef[discriminator] &&
|
|
308
|
+
!opts.excludeProperties.includes(discriminator)
|
|
309
|
+
) {
|
|
310
|
+
schema.properties[discriminator] = this._createSchemaByType(
|
|
311
|
+
DataType.STRING,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// обработка связи referencesMany
|
|
317
|
+
// (хранит массив foreign keys)
|
|
318
|
+
else if (relDef.type === RelationType.REFERENCES_MANY) {
|
|
319
|
+
const singularRelName = singularize(relName);
|
|
320
|
+
const foreignKey = relDef.foreignKey || `${singularRelName}Ids`;
|
|
321
|
+
if (
|
|
322
|
+
!propertiesDef[foreignKey] &&
|
|
323
|
+
!opts.excludeProperties.includes(foreignKey)
|
|
324
|
+
) {
|
|
325
|
+
schema.properties[foreignKey] = {
|
|
326
|
+
type: 'array',
|
|
327
|
+
items: this._createSchemaByType(foreignKeyDataType),
|
|
328
|
+
example: [],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// связи HAS_ONE и HAS_MANY не хранят внешний ключ
|
|
333
|
+
// в текущей таблице/модели, поэтому игнорируются
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Пытается определить тип первичного ключа целевой модели.
|
|
339
|
+
* Если связь полиморфная или тип ключа ANY, то возвращает
|
|
340
|
+
* тип по умолчанию (opts.defaultPrimaryKeyType).
|
|
341
|
+
*
|
|
342
|
+
* @param {object} relDef Определение связи
|
|
343
|
+
* @param {object} opts Настройки генератора
|
|
344
|
+
* @returns {string} Тип данных (DataType)
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
_resolveForeignKeyDataType(relDef, opts) {
|
|
348
|
+
// если целевая модель не указана (например, полиморфная связь)
|
|
349
|
+
if (!relDef.model) {
|
|
350
|
+
return opts.defaultPrimaryKeyType;
|
|
351
|
+
}
|
|
352
|
+
const registry = this.getService(DefinitionRegistry);
|
|
353
|
+
// если целевая модель еще не зарегистрирована
|
|
354
|
+
if (!registry.hasModel(relDef.model)) {
|
|
355
|
+
throw new InvalidArgumentError(
|
|
356
|
+
'Model %v must be registered before generating a JSON Schema.',
|
|
357
|
+
relDef.model,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const utils = this.getService(ModelDefinitionUtils);
|
|
361
|
+
try {
|
|
362
|
+
// попытка получить имя первичного ключа и его тип
|
|
363
|
+
const pkName = utils.getPrimaryKeyAsPropertyName(relDef.model);
|
|
364
|
+
const pkType = utils.getDataTypeByPropertyName(relDef.model, pkName);
|
|
365
|
+
// если тип определен и это не ANY,
|
|
366
|
+
// то используется данный тип
|
|
367
|
+
if (pkType && pkType !== DataType.ANY) {
|
|
368
|
+
return pkType;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// ошибки игнорируются (например, циклическое наследование)
|
|
372
|
+
// и просто провал к типу по умолчанию
|
|
373
|
+
}
|
|
374
|
+
// во всех остальных случаях используется значение по умолчанию
|
|
375
|
+
return opts.defaultPrimaryKeyType;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {expect} from 'chai';
|
|
2
|
+
import {format} from '@e22m4u/js-format';
|
|
3
|
+
import {JsonSchemaGenerator} from './json-schema-generator.js';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DataType,
|
|
7
|
+
DatabaseSchema,
|
|
8
|
+
DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
|
|
9
|
+
} from '@e22m4u/js-repository';
|
|
10
|
+
|
|
11
|
+
describe('JsonSchemaGenerator', function () {
|
|
12
|
+
describe('genSchema', function () {
|
|
13
|
+
it('should require the parameter "modelName" to be a non-empty String', function () {
|
|
14
|
+
const throwable = v => () => {
|
|
15
|
+
const dbs = new DatabaseSchema();
|
|
16
|
+
dbs.defineModel({name: 'model'});
|
|
17
|
+
const S = dbs.getService(JsonSchemaGenerator);
|
|
18
|
+
S.genSchema(v);
|
|
19
|
+
};
|
|
20
|
+
const error = s =>
|
|
21
|
+
format(
|
|
22
|
+
'Parameter "modelName" must be a non-empty String, but %s was given.',
|
|
23
|
+
s,
|
|
24
|
+
);
|
|
25
|
+
expect(throwable('')).to.throw(error('""'));
|
|
26
|
+
expect(throwable(10)).to.throw(error('10'));
|
|
27
|
+
expect(throwable(0)).to.throw(error('0'));
|
|
28
|
+
expect(throwable(true)).to.throw(error('true'));
|
|
29
|
+
expect(throwable(false)).to.throw(error('false'));
|
|
30
|
+
expect(throwable([])).to.throw(error('Array'));
|
|
31
|
+
expect(throwable({})).to.throw(error('Object'));
|
|
32
|
+
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
33
|
+
expect(throwable(null)).to.throw(error('null'));
|
|
34
|
+
throwable('model')();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should inject a primary key at the beginning of the properties object', function () {
|
|
38
|
+
const dbs = new DatabaseSchema();
|
|
39
|
+
dbs.defineDatasource({name: 'memory', adapter: 'memory'});
|
|
40
|
+
dbs.defineModel({
|
|
41
|
+
name: 'modelWithDbAndProps',
|
|
42
|
+
datasource: 'memory',
|
|
43
|
+
properties: {
|
|
44
|
+
firstName: DataType.STRING,
|
|
45
|
+
age: DataType.NUMBER,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const S = dbs.getService(JsonSchemaGenerator);
|
|
49
|
+
const schema = S.genSchema('modelWithDbAndProps');
|
|
50
|
+
const propertyKeys = Object.keys(schema.properties);
|
|
51
|
+
expect(propertyKeys[0]).to.be.eq(DEFAULT_PRIMARY_KEY_PROPERTY_NAME);
|
|
52
|
+
expect(propertyKeys[1]).to.be.eq('firstName');
|
|
53
|
+
expect(propertyKeys[2]).to.be.eq('age');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should inject a primary key if a datasource is defined', function () {
|
|
57
|
+
const dbs = new DatabaseSchema();
|
|
58
|
+
dbs.defineDatasource({name: 'memory', adapter: 'memory'});
|
|
59
|
+
dbs.defineModel({name: 'modelWithDb', datasource: 'memory'});
|
|
60
|
+
const S = dbs.getService(JsonSchemaGenerator);
|
|
61
|
+
const schema = S.genSchema('modelWithDb');
|
|
62
|
+
expect(schema.properties).to.have.property(
|
|
63
|
+
DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should not inject a primary key if a datasource is not defined', function () {
|
|
68
|
+
const dbs = new DatabaseSchema();
|
|
69
|
+
dbs.defineModel({name: 'modelWithoutDb'});
|
|
70
|
+
const S = dbs.getService(JsonSchemaGenerator);
|
|
71
|
+
const schema = S.genSchema('modelWithoutDb');
|
|
72
|
+
expect(schema.properties).to.not.have.property(
|
|
73
|
+
DEFAULT_PRIMARY_KEY_PROPERTY_NAME,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
package/tsconfig.json
ADDED