metal-orm 1.0.91 → 1.0.92
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/dist/index.cjs +214 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -24
- package/dist/index.d.ts +49 -24
- package/dist/index.js +206 -118
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-entities/render.mjs +16 -3
- package/src/decorators/column-decorator.ts +3 -1
- package/src/dto/openapi/generators/column.ts +12 -9
- package/src/dto/openapi/generators/dto.ts +10 -10
- package/src/dto/openapi/generators/filter.ts +47 -46
- package/src/dto/openapi/generators/nested-dto.ts +107 -21
- package/src/dto/openapi/generators/relation-filter.ts +54 -36
- package/src/dto/openapi/types.ts +23 -12
- package/src/dto/openapi/utilities.ts +48 -3
- package/src/orm/entity-metadata.ts +14 -14
package/package.json
CHANGED
|
@@ -4,6 +4,14 @@ import { buildSchemaMetadata } from './schema.mjs';
|
|
|
4
4
|
|
|
5
5
|
const escapeJsString = value => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
6
6
|
|
|
7
|
+
const sanitizePropertyName = columnName => {
|
|
8
|
+
if (!columnName) return '';
|
|
9
|
+
return columnName
|
|
10
|
+
.replace(/\s+/g, '_')
|
|
11
|
+
.replace(/[^a-zA-Z0-9_$]/g, '_')
|
|
12
|
+
.replace(/^[0-9]/, '_$&');
|
|
13
|
+
};
|
|
14
|
+
|
|
7
15
|
const formatJsDoc = comment => {
|
|
8
16
|
if (!comment) return null;
|
|
9
17
|
const normalized = comment.replace(/\r\n?/g, '\n').trim();
|
|
@@ -159,7 +167,7 @@ const buildColumnDoc = column => {
|
|
|
159
167
|
return entries.join('\n');
|
|
160
168
|
};
|
|
161
169
|
|
|
162
|
-
const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema) => {
|
|
170
|
+
const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema, propertyName) => {
|
|
163
171
|
const base = parseColumnType(column.type);
|
|
164
172
|
let expr = base.factory;
|
|
165
173
|
|
|
@@ -198,6 +206,10 @@ const renderColumnExpression = (column, tablePk, tableSchema, defaultSchema) =>
|
|
|
198
206
|
const tsType = base.ts || 'any';
|
|
199
207
|
const optional = !column.notNull;
|
|
200
208
|
|
|
209
|
+
if (column.name !== propertyName) {
|
|
210
|
+
expr = `{ ...${expr}, name: '${escapeJsString(column.name)}' }`;
|
|
211
|
+
}
|
|
212
|
+
|
|
201
213
|
return {
|
|
202
214
|
decorator,
|
|
203
215
|
expr,
|
|
@@ -236,10 +248,11 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
|
|
|
236
248
|
lines.push(`export class ${className} {`);
|
|
237
249
|
|
|
238
250
|
for (const col of table.columns) {
|
|
239
|
-
const
|
|
251
|
+
const propertyName = sanitizePropertyName(col.name);
|
|
252
|
+
const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema, propertyName);
|
|
240
253
|
appendJsDoc(lines, rendered.comment, ' ');
|
|
241
254
|
lines.push(` @${rendered.decorator}(${rendered.expr})`);
|
|
242
|
-
lines.push(` ${
|
|
255
|
+
lines.push(` ${propertyName}${rendered.optional ? '?:' : '!:'} ${rendered.tsType};`);
|
|
243
256
|
lines.push('');
|
|
244
257
|
}
|
|
245
258
|
|
|
@@ -12,6 +12,7 @@ export interface ColumnOptions {
|
|
|
12
12
|
notNull?: boolean;
|
|
13
13
|
primary?: boolean;
|
|
14
14
|
tsType?: ColumnDef['tsType'];
|
|
15
|
+
name?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -35,7 +36,8 @@ const normalizeColumnInput = (input: ColumnInput): ColumnDefLike => {
|
|
|
35
36
|
generated: asDefinition.generated,
|
|
36
37
|
check: asDefinition.check,
|
|
37
38
|
references: asDefinition.references,
|
|
38
|
-
comment: asDefinition.comment
|
|
39
|
+
comment: asDefinition.comment,
|
|
40
|
+
name: asOptions.name ?? asDefinition.name
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
if (!column.type) {
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import type { ColumnDef } from '../../../schema/column-types.js';
|
|
2
|
-
import type { OpenApiSchema } from '../types.js';
|
|
2
|
+
import type { OpenApiSchema, OpenApiDialect } from '../types.js';
|
|
3
3
|
import { columnTypeToOpenApiType, columnTypeToOpenApiFormat } from '../type-mappings.js';
|
|
4
|
+
import { applyNullability, isNullableColumn } from '../utilities.js';
|
|
4
5
|
|
|
5
|
-
export function columnToOpenApiSchema(
|
|
6
|
+
export function columnToOpenApiSchema(
|
|
7
|
+
col: ColumnDef,
|
|
8
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
9
|
+
): OpenApiSchema {
|
|
6
10
|
const schema: OpenApiSchema = {
|
|
7
11
|
type: columnTypeToOpenApiType(col),
|
|
8
12
|
format: columnTypeToOpenApiFormat(col),
|
|
9
13
|
};
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
15
|
+
const nullable = isNullableColumn(col);
|
|
16
|
+
const result = applyNullability(schema, nullable, dialect);
|
|
14
17
|
|
|
15
18
|
if (col.comment) {
|
|
16
|
-
|
|
19
|
+
result.description = col.comment;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
if (col.type.toUpperCase() === 'ENUM' && col.args && Array.isArray(col.args) && col.args.every(v => typeof v === 'string')) {
|
|
20
|
-
|
|
23
|
+
result.enum = col.args as string[];
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
const args = col.args;
|
|
@@ -25,10 +28,10 @@ export function columnToOpenApiSchema(col: ColumnDef): OpenApiSchema {
|
|
|
25
28
|
if (col.type.toUpperCase() === 'VARCHAR' || col.type.toUpperCase() === 'CHAR') {
|
|
26
29
|
const length = args[0] as number;
|
|
27
30
|
if (length) {
|
|
28
|
-
|
|
31
|
+
result.example = 'a'.repeat(length);
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
return
|
|
36
|
+
return result;
|
|
34
37
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { TableDef } from '../../../schema/table.js';
|
|
2
2
|
import type { EntityConstructor } from '../../../orm/entity-metadata.js';
|
|
3
|
-
import type { OpenApiSchema } from '../types.js';
|
|
3
|
+
import type { OpenApiSchema, OpenApiDialect } from '../types.js';
|
|
4
4
|
import { columnToOpenApiSchema } from './column.js';
|
|
5
5
|
import { getColumnMap } from './base.js';
|
|
6
6
|
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
8
|
export function dtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExclude extends keyof any>(
|
|
9
9
|
target: T,
|
|
10
|
-
exclude?: TExclude[]
|
|
10
|
+
exclude?: TExclude[],
|
|
11
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
11
12
|
): OpenApiSchema {
|
|
12
13
|
const columns = getColumnMap(target);
|
|
13
14
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -18,7 +19,7 @@ export function dtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExcl
|
|
|
18
19
|
continue;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
properties[key] = columnToOpenApiSchema(col);
|
|
22
|
+
properties[key] = columnToOpenApiSchema(col, dialect);
|
|
22
23
|
|
|
23
24
|
if (col.notNull || col.primary) {
|
|
24
25
|
required.push(key);
|
|
@@ -35,7 +36,8 @@ export function dtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExcl
|
|
|
35
36
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
37
|
export function createDtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExclude extends keyof any>(
|
|
37
38
|
target: T,
|
|
38
|
-
exclude?: TExclude[]
|
|
39
|
+
exclude?: TExclude[],
|
|
40
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
39
41
|
): OpenApiSchema {
|
|
40
42
|
const columns = getColumnMap(target);
|
|
41
43
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -50,7 +52,7 @@ export function createDtoToOpenApiSchema<T extends TableDef | EntityConstructor,
|
|
|
50
52
|
continue;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
properties[key] = columnToOpenApiSchema(col);
|
|
55
|
+
properties[key] = columnToOpenApiSchema(col, dialect);
|
|
54
56
|
|
|
55
57
|
if (col.notNull && !col.default) {
|
|
56
58
|
required.push(key);
|
|
@@ -67,7 +69,8 @@ export function createDtoToOpenApiSchema<T extends TableDef | EntityConstructor,
|
|
|
67
69
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
70
|
export function updateDtoToOpenApiSchema<T extends TableDef | EntityConstructor, TExclude extends keyof any>(
|
|
69
71
|
target: T,
|
|
70
|
-
exclude?: TExclude[]
|
|
72
|
+
exclude?: TExclude[],
|
|
73
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
71
74
|
): OpenApiSchema {
|
|
72
75
|
const columns = getColumnMap(target);
|
|
73
76
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -81,10 +84,7 @@ export function updateDtoToOpenApiSchema<T extends TableDef | EntityConstructor,
|
|
|
81
84
|
continue;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
properties[key] =
|
|
85
|
-
...columnToOpenApiSchema(col),
|
|
86
|
-
...(!col.notNull ? { nullable: true } : {}),
|
|
87
|
-
};
|
|
87
|
+
properties[key] = columnToOpenApiSchema(col, dialect);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
return {
|
|
@@ -1,54 +1,45 @@
|
|
|
1
1
|
import type { TableDef } from '../../../schema/table.js';
|
|
2
2
|
import type { ColumnDef } from '../../../schema/column-types.js';
|
|
3
3
|
import type { EntityConstructor } from '../../../orm/entity-metadata.js';
|
|
4
|
-
import type { OpenApiSchema } from '../types.js';
|
|
5
|
-
import { columnTypeToOpenApiType } from '../type-mappings.js';
|
|
4
|
+
import type { OpenApiSchema, OpenApiDialect } from '../types.js';
|
|
5
|
+
import { columnTypeToOpenApiType, columnTypeToOpenApiFormat } from '../type-mappings.js';
|
|
6
6
|
import { getColumnMap } from './base.js';
|
|
7
|
+
import { applyNullability, isNullableColumn } from '../utilities.js';
|
|
7
8
|
|
|
8
9
|
function filterFieldToOpenApiSchema(col: ColumnDef): OpenApiSchema {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let filterProperties: Record<string, OpenApiSchema> = {};
|
|
10
|
+
const openApiType = columnTypeToOpenApiType(col);
|
|
11
|
+
const openApiFormat = columnTypeToOpenApiFormat(col);
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
filterProperties = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
filterProperties = {
|
|
43
|
-
|
|
44
|
-
not: { type: 'string' },
|
|
45
|
-
in: { type: 'array', items: { type: 'string' } },
|
|
46
|
-
notIn: { type: 'array', items: { type: 'string' } },
|
|
47
|
-
contains: { type: 'string' },
|
|
48
|
-
startsWith: { type: 'string' },
|
|
49
|
-
endsWith: { type: 'string' },
|
|
50
|
-
mode: { type: 'string', enum: ['default', 'insensitive'] },
|
|
51
|
-
};
|
|
13
|
+
const filterProperties: Record<string, OpenApiSchema> = {};
|
|
14
|
+
|
|
15
|
+
filterProperties.equals = { type: openApiType, format: openApiFormat };
|
|
16
|
+
filterProperties.not = { type: openApiType, format: openApiFormat };
|
|
17
|
+
filterProperties.in = { type: 'array', items: { type: openApiType, format: openApiFormat } };
|
|
18
|
+
filterProperties.notIn = { type: 'array', items: { type: openApiType, format: openApiFormat } };
|
|
19
|
+
|
|
20
|
+
const isNumeric = openApiType === 'integer' || openApiType === 'number';
|
|
21
|
+
const isDateOrTime = openApiType === 'string' && (openApiFormat === 'date' || openApiFormat === 'date-time');
|
|
22
|
+
const isString = openApiType === 'string' && !isDateOrTime;
|
|
23
|
+
|
|
24
|
+
if (isNumeric) {
|
|
25
|
+
filterProperties.lt = { type: openApiType, format: openApiFormat };
|
|
26
|
+
filterProperties.lte = { type: openApiType, format: openApiFormat };
|
|
27
|
+
filterProperties.gt = { type: openApiType, format: openApiFormat };
|
|
28
|
+
filterProperties.gte = { type: openApiType, format: openApiFormat };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isDateOrTime) {
|
|
32
|
+
filterProperties.lt = { type: openApiType, format: openApiFormat };
|
|
33
|
+
filterProperties.lte = { type: openApiType, format: openApiFormat };
|
|
34
|
+
filterProperties.gt = { type: openApiType, format: openApiFormat };
|
|
35
|
+
filterProperties.gte = { type: openApiType, format: openApiFormat };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isString) {
|
|
39
|
+
filterProperties.contains = { type: 'string' };
|
|
40
|
+
filterProperties.startsWith = { type: 'string' };
|
|
41
|
+
filterProperties.endsWith = { type: 'string' };
|
|
42
|
+
filterProperties.mode = { type: 'string', enum: ['default', 'insensitive'] };
|
|
52
43
|
}
|
|
53
44
|
|
|
54
45
|
return {
|
|
@@ -57,14 +48,24 @@ function filterFieldToOpenApiSchema(col: ColumnDef): OpenApiSchema {
|
|
|
57
48
|
};
|
|
58
49
|
}
|
|
59
50
|
|
|
51
|
+
export function columnToFilterSchema(
|
|
52
|
+
col: ColumnDef,
|
|
53
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
54
|
+
): OpenApiSchema {
|
|
55
|
+
const filterSchema = filterFieldToOpenApiSchema(col);
|
|
56
|
+
const nullable = isNullableColumn(col);
|
|
57
|
+
return applyNullability(filterSchema, nullable, dialect);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
60
|
export function whereInputToOpenApiSchema<T extends TableDef | EntityConstructor>(
|
|
61
|
-
target: T
|
|
61
|
+
target: T,
|
|
62
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
62
63
|
): OpenApiSchema {
|
|
63
64
|
const columns = getColumnMap(target);
|
|
64
65
|
const properties: Record<string, OpenApiSchema> = {};
|
|
65
66
|
|
|
66
67
|
for (const [key, col] of Object.entries(columns)) {
|
|
67
|
-
properties[key] =
|
|
68
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
return {
|
|
@@ -7,8 +7,9 @@ import type {
|
|
|
7
7
|
HasOneRelation,
|
|
8
8
|
BelongsToManyRelation
|
|
9
9
|
} from '../../../schema/relation.js';
|
|
10
|
-
import type { OpenApiSchema, OpenApiComponent } from '../types.js';
|
|
10
|
+
import type { OpenApiSchema, OpenApiComponent, OpenApiDialect, OpenApiParameterObject, OpenApiResponseObject } from '../types.js';
|
|
11
11
|
import { columnToOpenApiSchema } from './column.js';
|
|
12
|
+
import { columnToFilterSchema } from './filter.js';
|
|
12
13
|
import { getColumnMap } from './base.js';
|
|
13
14
|
import { RelationKinds } from '../../../schema/relation.js';
|
|
14
15
|
|
|
@@ -125,10 +126,7 @@ export function updateDtoWithRelationsToOpenApiSchema<T extends TableDef | Entit
|
|
|
125
126
|
continue;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
properties[key] =
|
|
129
|
-
...columnToOpenApiSchema(col),
|
|
130
|
-
nullable: true,
|
|
131
|
-
};
|
|
129
|
+
properties[key] = columnToOpenApiSchema(col);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
132
|
const tableDef = target as TableDef;
|
|
@@ -159,10 +157,7 @@ function updateDtoToOpenApiSchemaForComponent(
|
|
|
159
157
|
continue;
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
properties[key] =
|
|
163
|
-
...columnToOpenApiSchema(col),
|
|
164
|
-
nullable: true,
|
|
165
|
-
};
|
|
160
|
+
properties[key] = columnToOpenApiSchema(col);
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
return {
|
|
@@ -270,7 +265,8 @@ function whereInputWithRelationsToOpenApiSchema(
|
|
|
270
265
|
relationInclude?: string[];
|
|
271
266
|
maxDepth?: number;
|
|
272
267
|
prefix?: string;
|
|
273
|
-
}
|
|
268
|
+
},
|
|
269
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
274
270
|
): OpenApiSchema {
|
|
275
271
|
const columns = getColumnMap(target);
|
|
276
272
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -285,7 +281,7 @@ function whereInputWithRelationsToOpenApiSchema(
|
|
|
285
281
|
continue;
|
|
286
282
|
}
|
|
287
283
|
|
|
288
|
-
properties[key] =
|
|
284
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
289
285
|
}
|
|
290
286
|
|
|
291
287
|
const tableDef = target as TableDef;
|
|
@@ -300,9 +296,9 @@ function whereInputWithRelationsToOpenApiSchema(
|
|
|
300
296
|
}
|
|
301
297
|
|
|
302
298
|
properties[relationName] = relationFilterToOpenApiSchema(relation, {
|
|
303
|
-
exclude: options
|
|
304
|
-
include: options
|
|
305
|
-
});
|
|
299
|
+
exclude: options.columnExclude,
|
|
300
|
+
include: options.columnInclude,
|
|
301
|
+
}, dialect);
|
|
306
302
|
}
|
|
307
303
|
}
|
|
308
304
|
|
|
@@ -317,10 +313,11 @@ function relationFilterToOpenApiSchema(
|
|
|
317
313
|
options?: {
|
|
318
314
|
exclude?: string[];
|
|
319
315
|
include?: string[];
|
|
320
|
-
}
|
|
316
|
+
},
|
|
317
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
321
318
|
): OpenApiSchema {
|
|
322
319
|
if (relation.type === RelationKinds.BelongsTo || relation.type === RelationKinds.HasOne) {
|
|
323
|
-
return singleRelationFilterToOpenApiSchema((relation as BelongsToRelation | HasOneRelation).target, options);
|
|
320
|
+
return singleRelationFilterToOpenApiSchema((relation as BelongsToRelation | HasOneRelation).target, options, dialect);
|
|
324
321
|
}
|
|
325
322
|
|
|
326
323
|
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
@@ -332,7 +329,8 @@ function relationFilterToOpenApiSchema(
|
|
|
332
329
|
|
|
333
330
|
function singleRelationFilterToOpenApiSchema(
|
|
334
331
|
target: TableDef | EntityConstructor,
|
|
335
|
-
options?: { exclude?: string[]; include?: string[] }
|
|
332
|
+
options?: { exclude?: string[]; include?: string[] },
|
|
333
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
336
334
|
): OpenApiSchema {
|
|
337
335
|
const columns = getColumnMap(target);
|
|
338
336
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -346,7 +344,7 @@ function singleRelationFilterToOpenApiSchema(
|
|
|
346
344
|
continue;
|
|
347
345
|
}
|
|
348
346
|
|
|
349
|
-
properties[key] =
|
|
347
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
350
348
|
}
|
|
351
349
|
|
|
352
350
|
return {
|
|
@@ -395,7 +393,7 @@ function generateNestedProperties(
|
|
|
395
393
|
const properties: Record<string, OpenApiSchema> = {};
|
|
396
394
|
|
|
397
395
|
for (const [key, col] of Object.entries(columns)) {
|
|
398
|
-
properties[key] =
|
|
396
|
+
properties[key] = columnToFilterSchema(col);
|
|
399
397
|
}
|
|
400
398
|
|
|
401
399
|
return properties;
|
|
@@ -403,8 +401,8 @@ function generateNestedProperties(
|
|
|
403
401
|
|
|
404
402
|
export function createApiComponentsSection(
|
|
405
403
|
schemas: Record<string, OpenApiSchema>,
|
|
406
|
-
parameters?: Record<string,
|
|
407
|
-
responses?: Record<string,
|
|
404
|
+
parameters?: Record<string, OpenApiParameterObject>,
|
|
405
|
+
responses?: Record<string, OpenApiResponseObject>
|
|
408
406
|
): OpenApiComponent {
|
|
409
407
|
const component: OpenApiComponent = {};
|
|
410
408
|
|
|
@@ -439,6 +437,94 @@ export function responseToRef(responseName: string): ComponentReference {
|
|
|
439
437
|
return createRef(`responses/${responseName}`);
|
|
440
438
|
}
|
|
441
439
|
|
|
440
|
+
export function canonicalizeSchema(schema: OpenApiSchema): OpenApiSchema {
|
|
441
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
442
|
+
return schema;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (Array.isArray(schema)) {
|
|
446
|
+
return schema.map(canonicalizeSchema) as unknown as OpenApiSchema;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const canonical: OpenApiSchema = {};
|
|
450
|
+
|
|
451
|
+
const keys = Object.keys(schema).sort();
|
|
452
|
+
|
|
453
|
+
for (const key of keys) {
|
|
454
|
+
if (key === 'description' || key === 'example') {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const value = schema[key as keyof OpenApiSchema];
|
|
459
|
+
|
|
460
|
+
if (typeof value === 'object' && value !== null) {
|
|
461
|
+
(canonical as Record<string, unknown>)[key] = canonicalizeSchema(value as OpenApiSchema);
|
|
462
|
+
} else {
|
|
463
|
+
(canonical as Record<string, unknown>)[key] = value;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return canonical;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function computeSchemaHash(schema: OpenApiSchema): string {
|
|
471
|
+
const canonical = canonicalizeSchema(schema);
|
|
472
|
+
const json = JSON.stringify(canonical);
|
|
473
|
+
let hash = 0;
|
|
474
|
+
|
|
475
|
+
for (let i = 0; i < json.length; i++) {
|
|
476
|
+
const char = json.charCodeAt(i);
|
|
477
|
+
hash = ((hash << 5) - hash) + char;
|
|
478
|
+
hash = hash & hash;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const hex = Math.abs(hash).toString(16);
|
|
482
|
+
return hex.padStart(8, '0').slice(0, 6);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
interface DeterministicNamingState {
|
|
486
|
+
contentHashToName: Map<string, string>;
|
|
487
|
+
nameToContentHash: Map<string, string>;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function createDeterministicNamingState(): DeterministicNamingState {
|
|
491
|
+
return {
|
|
492
|
+
contentHashToName: new Map(),
|
|
493
|
+
nameToContentHash: new Map(),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function getDeterministicComponentName(
|
|
498
|
+
baseName: string,
|
|
499
|
+
schema: OpenApiSchema,
|
|
500
|
+
state: DeterministicNamingState
|
|
501
|
+
): string {
|
|
502
|
+
const hash = computeSchemaHash(schema);
|
|
503
|
+
const normalizedBase = baseName.replace(/[^A-Za-z0-9_]/g, '');
|
|
504
|
+
|
|
505
|
+
const existingName = state.contentHashToName.get(hash);
|
|
506
|
+
if (existingName) {
|
|
507
|
+
return existingName;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!state.nameToContentHash.has(normalizedBase)) {
|
|
511
|
+
state.contentHashToName.set(hash, normalizedBase);
|
|
512
|
+
state.nameToContentHash.set(normalizedBase, hash);
|
|
513
|
+
return normalizedBase;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const existingHash = state.nameToContentHash.get(normalizedBase)!;
|
|
517
|
+
if (existingHash === hash) {
|
|
518
|
+
return normalizedBase;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const uniqueName = `${normalizedBase}_${hash}`;
|
|
522
|
+
state.contentHashToName.set(hash, uniqueName);
|
|
523
|
+
state.nameToContentHash.set(uniqueName, hash);
|
|
524
|
+
|
|
525
|
+
return uniqueName;
|
|
526
|
+
}
|
|
527
|
+
|
|
442
528
|
export function replaceWithRefs(
|
|
443
529
|
schema: OpenApiSchema,
|
|
444
530
|
schemaMap: Record<string, OpenApiSchema>,
|