@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 ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "all": true,
3
+ "include": [
4
+ "src/**/*.js"
5
+ ],
6
+ "exclude": [
7
+ "src/**/*.spec.js"
8
+ ]
9
+ }
package/.commitlintrc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "@commitlint/config-conventional"
4
+ ]
5
+ }
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
@@ -0,0 +1,6 @@
1
+ npm run lint:fix
2
+ npm run format
3
+ npm run test
4
+ npm run build:cjs
5
+
6
+ git add -A
package/.mocharc.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extension": ["js"],
3
+ "spec": "src/**/*.spec.js"
4
+ }
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "bracketSpacing": false,
3
+ "singleQuote": true,
4
+ "printWidth": 80,
5
+ "trailingComma": "all",
6
+ "arrowParens": "avoid"
7
+ }
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
+ });
@@ -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
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "target": "es2022",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "noEmit": true,
8
+ "allowJs": true,
9
+ "types": ["node"]
10
+ },
11
+ "include": [
12
+ "./src/**/*.ts",
13
+ "./src/**/*.js"
14
+ ]
15
+ }