metal-orm 1.0.91 → 1.0.93
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 +3 -4
- 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
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
BelongsToManyRelation,
|
|
9
9
|
RelationKinds
|
|
10
10
|
} from '../../../schema/relation.js';
|
|
11
|
-
import type { OpenApiSchema } from '../types.js';
|
|
12
|
-
import {
|
|
11
|
+
import type { OpenApiSchema, OpenApiDialect } from '../types.js';
|
|
12
|
+
import { columnToFilterSchema } from './filter.js';
|
|
13
13
|
import { getColumnMap } from './base.js';
|
|
14
14
|
|
|
15
15
|
export function relationFilterToOpenApiSchema(
|
|
@@ -17,14 +17,26 @@ export function relationFilterToOpenApiSchema(
|
|
|
17
17
|
options?: {
|
|
18
18
|
exclude?: string[];
|
|
19
19
|
include?: string[];
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
|
+
dialect: OpenApiDialect = 'openapi-3.1',
|
|
22
|
+
depth: number = 0
|
|
21
23
|
): OpenApiSchema {
|
|
22
24
|
if (relation.type === RelationKinds.BelongsTo || relation.type === RelationKinds.HasOne) {
|
|
23
|
-
return singleRelationFilterToOpenApiSchema(
|
|
25
|
+
return singleRelationFilterToOpenApiSchema(
|
|
26
|
+
(relation as BelongsToRelation | HasOneRelation).target,
|
|
27
|
+
options,
|
|
28
|
+
dialect,
|
|
29
|
+
depth
|
|
30
|
+
);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
27
|
-
return manyRelationFilterToOpenApiSchema(
|
|
34
|
+
return manyRelationFilterToOpenApiSchema(
|
|
35
|
+
(relation as HasManyRelation | BelongsToManyRelation).target,
|
|
36
|
+
options,
|
|
37
|
+
dialect,
|
|
38
|
+
depth
|
|
39
|
+
);
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
return { type: 'object', properties: {} };
|
|
@@ -32,7 +44,9 @@ export function relationFilterToOpenApiSchema(
|
|
|
32
44
|
|
|
33
45
|
function singleRelationFilterToOpenApiSchema(
|
|
34
46
|
target: TableDef | EntityConstructor,
|
|
35
|
-
options?: { exclude?: string[]; include?: string[] }
|
|
47
|
+
options?: { exclude?: string[]; include?: string[] },
|
|
48
|
+
dialect: OpenApiDialect = 'openapi-3.1',
|
|
49
|
+
depth: number = 0
|
|
36
50
|
): OpenApiSchema {
|
|
37
51
|
const columns = getColumnMap(target);
|
|
38
52
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -47,13 +61,22 @@ function singleRelationFilterToOpenApiSchema(
|
|
|
47
61
|
continue;
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
properties[key] =
|
|
64
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
51
65
|
|
|
52
66
|
if (col.notNull || col.primary) {
|
|
53
67
|
required.push(key);
|
|
54
68
|
}
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
if (depth > 0) {
|
|
72
|
+
const tableDef = target as TableDef;
|
|
73
|
+
if (tableDef.relations) {
|
|
74
|
+
for (const [relationName, relation] of Object.entries(tableDef.relations)) {
|
|
75
|
+
properties[relationName] = relationFilterToOpenApiSchema(relation, undefined, dialect, depth - 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
return {
|
|
58
81
|
type: 'object',
|
|
59
82
|
properties,
|
|
@@ -63,25 +86,31 @@ function singleRelationFilterToOpenApiSchema(
|
|
|
63
86
|
|
|
64
87
|
function manyRelationFilterToOpenApiSchema(
|
|
65
88
|
target: TableDef | EntityConstructor,
|
|
66
|
-
options?: { exclude?: string[]; include?: string[] }
|
|
89
|
+
options?: { exclude?: string[]; include?: string[] },
|
|
90
|
+
dialect: OpenApiDialect = 'openapi-3.1',
|
|
91
|
+
depth: number = 0
|
|
67
92
|
): OpenApiSchema {
|
|
93
|
+
const nestedSchema: OpenApiSchema = depth > 0
|
|
94
|
+
? nestedWhereInputToOpenApiSchema(target, depth, dialect)
|
|
95
|
+
: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: generateNestedProperties(target, options, dialect),
|
|
98
|
+
};
|
|
99
|
+
|
|
68
100
|
return {
|
|
69
101
|
type: 'object',
|
|
70
102
|
properties: {
|
|
71
103
|
some: {
|
|
72
|
-
|
|
104
|
+
...nestedSchema,
|
|
73
105
|
description: 'Filter related records that match all conditions',
|
|
74
|
-
properties: generateNestedProperties(target, options),
|
|
75
106
|
},
|
|
76
107
|
every: {
|
|
77
|
-
|
|
108
|
+
...nestedSchema,
|
|
78
109
|
description: 'Filter related records where all match conditions',
|
|
79
|
-
properties: generateNestedProperties(target, options),
|
|
80
110
|
},
|
|
81
111
|
none: {
|
|
82
|
-
|
|
112
|
+
...nestedSchema,
|
|
83
113
|
description: 'Filter where no related records match',
|
|
84
|
-
properties: generateNestedProperties(target, options),
|
|
85
114
|
},
|
|
86
115
|
isEmpty: {
|
|
87
116
|
type: 'boolean',
|
|
@@ -97,7 +126,8 @@ function manyRelationFilterToOpenApiSchema(
|
|
|
97
126
|
|
|
98
127
|
function generateNestedProperties(
|
|
99
128
|
target: TableDef | EntityConstructor,
|
|
100
|
-
options?: { exclude?: string[]; include?: string[] }
|
|
129
|
+
options?: { exclude?: string[]; include?: string[] },
|
|
130
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
101
131
|
): Record<string, OpenApiSchema> {
|
|
102
132
|
const columns = getColumnMap(target);
|
|
103
133
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -111,7 +141,7 @@ function generateNestedProperties(
|
|
|
111
141
|
continue;
|
|
112
142
|
}
|
|
113
143
|
|
|
114
|
-
properties[key] =
|
|
144
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
115
145
|
}
|
|
116
146
|
|
|
117
147
|
return properties;
|
|
@@ -125,7 +155,8 @@ export function whereInputWithRelationsToOpenApiSchema<T extends TableDef | Enti
|
|
|
125
155
|
relationExclude?: string[];
|
|
126
156
|
relationInclude?: string[];
|
|
127
157
|
maxDepth?: number;
|
|
128
|
-
}
|
|
158
|
+
},
|
|
159
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
129
160
|
): OpenApiSchema {
|
|
130
161
|
const columns = getColumnMap(target);
|
|
131
162
|
const properties: Record<string, OpenApiSchema> = {};
|
|
@@ -140,7 +171,7 @@ export function whereInputWithRelationsToOpenApiSchema<T extends TableDef | Enti
|
|
|
140
171
|
continue;
|
|
141
172
|
}
|
|
142
173
|
|
|
143
|
-
properties[key] =
|
|
174
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
144
175
|
}
|
|
145
176
|
|
|
146
177
|
const tableDef = target as TableDef;
|
|
@@ -157,7 +188,7 @@ export function whereInputWithRelationsToOpenApiSchema<T extends TableDef | Enti
|
|
|
157
188
|
properties[relationName] = relationFilterToOpenApiSchema(relation, {
|
|
158
189
|
exclude: options?.columnExclude,
|
|
159
190
|
include: options?.columnInclude,
|
|
160
|
-
});
|
|
191
|
+
}, dialect);
|
|
161
192
|
}
|
|
162
193
|
}
|
|
163
194
|
|
|
@@ -169,7 +200,8 @@ export function whereInputWithRelationsToOpenApiSchema<T extends TableDef | Enti
|
|
|
169
200
|
|
|
170
201
|
export function nestedWhereInputToOpenApiSchema<T extends TableDef | EntityConstructor>(
|
|
171
202
|
target: T,
|
|
172
|
-
depth: number = 2
|
|
203
|
+
depth: number = 2,
|
|
204
|
+
dialect: OpenApiDialect = 'openapi-3.1'
|
|
173
205
|
): OpenApiSchema {
|
|
174
206
|
if (depth <= 0) {
|
|
175
207
|
return { type: 'object', properties: {} };
|
|
@@ -179,27 +211,13 @@ export function nestedWhereInputToOpenApiSchema<T extends TableDef | EntityConst
|
|
|
179
211
|
const properties: Record<string, OpenApiSchema> = {};
|
|
180
212
|
|
|
181
213
|
for (const [key, col] of Object.entries(columns)) {
|
|
182
|
-
properties[key] =
|
|
214
|
+
properties[key] = columnToFilterSchema(col, dialect);
|
|
183
215
|
}
|
|
184
216
|
|
|
185
217
|
const tableDef = target as TableDef;
|
|
186
218
|
if (tableDef.relations) {
|
|
187
219
|
for (const [relationName, relation] of Object.entries(tableDef.relations)) {
|
|
188
|
-
properties[relationName] = relationFilterToOpenApiSchema(relation);
|
|
189
|
-
|
|
190
|
-
if (depth > 1) {
|
|
191
|
-
if (relation.type === RelationKinds.BelongsTo || relation.type === RelationKinds.HasOne) {
|
|
192
|
-
properties[relationName] = singleRelationFilterToOpenApiSchema((relation as BelongsToRelation | HasOneRelation).target);
|
|
193
|
-
} else if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
|
|
194
|
-
properties[relationName] = {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: {
|
|
197
|
-
some: nestedWhereInputToOpenApiSchema((relation as HasManyRelation | BelongsToManyRelation).target, depth - 1),
|
|
198
|
-
every: nestedWhereInputToOpenApiSchema((relation as HasManyRelation | BelongsToManyRelation).target, depth - 1),
|
|
199
|
-
},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
}
|
|
220
|
+
properties[relationName] = relationFilterToOpenApiSchema(relation, undefined, dialect, depth - 1);
|
|
203
221
|
}
|
|
204
222
|
}
|
|
205
223
|
|
package/src/dto/openapi/types.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface OpenApiSchema {
|
|
|
25
25
|
oneOf?: OpenApiSchema[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface
|
|
28
|
+
export interface OpenApiParameterObject {
|
|
29
29
|
name: string;
|
|
30
30
|
in: 'query' | 'path' | 'header' | 'cookie';
|
|
31
31
|
required?: boolean;
|
|
@@ -33,10 +33,19 @@ export interface OpenApiParameter {
|
|
|
33
33
|
description?: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface OpenApiResponseObject {
|
|
37
|
+
description: string;
|
|
38
|
+
content?: {
|
|
39
|
+
'application/json': {
|
|
40
|
+
schema: OpenApiSchema;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
export interface OpenApiOperation {
|
|
37
46
|
summary?: string;
|
|
38
47
|
description?: string;
|
|
39
|
-
parameters?:
|
|
48
|
+
parameters?: OpenApiParameterObject[];
|
|
40
49
|
requestBody?: {
|
|
41
50
|
description?: string;
|
|
42
51
|
required?: boolean;
|
|
@@ -46,14 +55,7 @@ export interface OpenApiOperation {
|
|
|
46
55
|
};
|
|
47
56
|
};
|
|
48
57
|
};
|
|
49
|
-
responses?: Record<string,
|
|
50
|
-
description: string;
|
|
51
|
-
content?: {
|
|
52
|
-
'application/json': {
|
|
53
|
-
schema: OpenApiSchema;
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
|
-
}>;
|
|
58
|
+
responses?: Record<string, OpenApiResponseObject>;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
export interface OpenApiDocumentInfo {
|
|
@@ -77,8 +79,8 @@ export interface PaginationParams {
|
|
|
77
79
|
|
|
78
80
|
export interface OpenApiComponent {
|
|
79
81
|
schemas?: Record<string, OpenApiSchema>;
|
|
80
|
-
parameters?: Record<string,
|
|
81
|
-
responses?: Record<string,
|
|
82
|
+
parameters?: Record<string, OpenApiParameterObject>;
|
|
83
|
+
responses?: Record<string, OpenApiResponseObject>;
|
|
82
84
|
securitySchemes?: Record<string, unknown>;
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -88,3 +90,12 @@ export interface OpenApiDocument {
|
|
|
88
90
|
paths: Record<string, Record<string, OpenApiOperation>>;
|
|
89
91
|
components?: OpenApiComponent;
|
|
90
92
|
}
|
|
93
|
+
|
|
94
|
+
export type OpenApiDialect = 'openapi-3.0' | 'openapi-3.1';
|
|
95
|
+
|
|
96
|
+
export interface OpenApiDocumentOptions {
|
|
97
|
+
dialect?: OpenApiDialect;
|
|
98
|
+
allowScalarEquals?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type OpenApiParameter = OpenApiParameterObject;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OpenApiSchema, OpenApiOperation, OpenApiDocumentInfo, ApiRouteDefinition, OpenApiDocument } from './types.js';
|
|
1
|
+
import type { OpenApiSchema, OpenApiOperation, OpenApiDocumentInfo, ApiRouteDefinition, OpenApiDocument, OpenApiDialect, OpenApiType } from './types.js';
|
|
2
2
|
|
|
3
3
|
export function schemaToJson(schema: OpenApiSchema): string {
|
|
4
4
|
return JSON.stringify(schema, null, 2);
|
|
@@ -20,10 +20,55 @@ export function mergeSchemas(base: OpenApiSchema, override: Partial<OpenApiSchem
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export function applyNullability(
|
|
24
|
+
schema: OpenApiSchema,
|
|
25
|
+
isNullable: boolean,
|
|
26
|
+
dialect: OpenApiDialect
|
|
27
|
+
): OpenApiSchema {
|
|
28
|
+
if (!isNullable) {
|
|
29
|
+
const { nullable: _, ...clean } = schema;
|
|
30
|
+
return clean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (dialect === 'openapi-3.0') {
|
|
34
|
+
return { ...schema, nullable: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const type = schema.type;
|
|
38
|
+
if (Array.isArray(type)) {
|
|
39
|
+
if (!type.includes('null')) {
|
|
40
|
+
const { nullable: _, ...clean } = schema;
|
|
41
|
+
return { ...clean, type: [...type, 'null'] as OpenApiType[] };
|
|
42
|
+
}
|
|
43
|
+
} else if (type) {
|
|
44
|
+
const { nullable: _, ...clean } = schema;
|
|
45
|
+
return { ...clean, type: [type, 'null'] as OpenApiType[] };
|
|
46
|
+
} else {
|
|
47
|
+
const { nullable: _, ...clean } = schema;
|
|
48
|
+
return { ...clean, type: ['null'] as OpenApiType[] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const { nullable: _, ...clean } = schema;
|
|
52
|
+
return clean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isNullableColumn(col: { notNull?: boolean }): boolean {
|
|
56
|
+
return !col.notNull;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getOpenApiVersionForDialect(dialect: OpenApiDialect): string {
|
|
60
|
+
return dialect === 'openapi-3.0' ? '3.0.3' : '3.1.0';
|
|
61
|
+
}
|
|
62
|
+
|
|
23
63
|
export function generateOpenApiDocument(
|
|
24
64
|
info: OpenApiDocumentInfo,
|
|
25
|
-
routes: ApiRouteDefinition[]
|
|
65
|
+
routes: ApiRouteDefinition[],
|
|
66
|
+
options?: {
|
|
67
|
+
dialect?: OpenApiDialect;
|
|
68
|
+
allowScalarEquals?: boolean;
|
|
69
|
+
}
|
|
26
70
|
): OpenApiDocument {
|
|
71
|
+
const dialect: OpenApiDialect = options?.dialect ?? 'openapi-3.1';
|
|
27
72
|
const paths: Record<string, Record<string, OpenApiOperation>> = {};
|
|
28
73
|
|
|
29
74
|
for (const route of routes) {
|
|
@@ -34,7 +79,7 @@ export function generateOpenApiDocument(
|
|
|
34
79
|
}
|
|
35
80
|
|
|
36
81
|
return {
|
|
37
|
-
openapi:
|
|
82
|
+
openapi: getOpenApiVersionForDialect(dialect),
|
|
38
83
|
info: {
|
|
39
84
|
title: info.title,
|
|
40
85
|
version: info.version,
|
|
@@ -21,11 +21,11 @@ export type EntityOrTableTarget = EntityConstructor | TableDef;
|
|
|
21
21
|
export type EntityOrTableTargetResolver<T extends EntityOrTableTarget = EntityOrTableTarget> =
|
|
22
22
|
T | (() => T);
|
|
23
23
|
|
|
24
|
-
/**
|
|
25
|
-
* Simplified column definition structure used during metadata registration.
|
|
26
|
-
* @template T - Concrete column definition type being extended
|
|
27
|
-
*/
|
|
28
|
-
export type ColumnDefLike<T extends ColumnDef = ColumnDef> = Omit<T, '
|
|
24
|
+
/**
|
|
25
|
+
* Simplified column definition structure used during metadata registration.
|
|
26
|
+
* @template T - Concrete column definition type being extended
|
|
27
|
+
*/
|
|
28
|
+
export type ColumnDefLike<T extends ColumnDef = ColumnDef> = Omit<T, 'table'>;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Transforms simplified column metadata into full ColumnDef objects during table building.
|
|
@@ -250,15 +250,15 @@ export const buildTableDef = <TColumns extends Record<string, ColumnDefLike>>(me
|
|
|
250
250
|
return meta.table;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
// Build columns using a simpler approach that avoids type assertion
|
|
254
|
-
const columns: Record<string, ColumnDef> = {};
|
|
255
|
-
for (const [key, def] of Object.entries(meta.columns)) {
|
|
256
|
-
columns[key] = {
|
|
257
|
-
...def,
|
|
258
|
-
name: key,
|
|
259
|
-
table: meta.tableName
|
|
260
|
-
} as ColumnDef;
|
|
261
|
-
}
|
|
253
|
+
// Build columns using a simpler approach that avoids type assertion
|
|
254
|
+
const columns: Record<string, ColumnDef> = {};
|
|
255
|
+
for (const [key, def] of Object.entries(meta.columns)) {
|
|
256
|
+
columns[key] = {
|
|
257
|
+
...def,
|
|
258
|
+
name: def.name ?? key,
|
|
259
|
+
table: meta.tableName
|
|
260
|
+
} as ColumnDef;
|
|
261
|
+
}
|
|
262
262
|
|
|
263
263
|
const table = defineTable(meta.tableName, columns as MaterializeColumns<TColumns>, {}, meta.hooks);
|
|
264
264
|
meta.table = table;
|