metal-orm 1.0.79 → 1.0.80
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 +463 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -2
- package/dist/index.d.ts +118 -2
- package/dist/index.js +457 -4
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/src/core/dialect/postgres/index.ts +9 -5
- package/src/index.ts +6 -4
- package/src/openapi/index.ts +3 -0
- package/src/openapi/schema-extractor.ts +418 -0
- package/src/openapi/schema-types.ts +92 -0
- package/src/openapi/type-mappers.ts +207 -0
- package/src/query-builder/select.ts +25 -11
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { ColumnDef } from '../schema/column-types.js';
|
|
2
|
+
import type { JsonSchemaProperty, JsonSchemaType, JsonSchemaFormat } from './schema-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps SQL column types to OpenAPI JSON Schema types
|
|
6
|
+
*/
|
|
7
|
+
export const mapColumnType = (column: ColumnDef): JsonSchemaProperty => {
|
|
8
|
+
const sqlType = normalizeType(column.type);
|
|
9
|
+
const baseSchema = mapSqlTypeToBaseSchema(sqlType, column);
|
|
10
|
+
|
|
11
|
+
const schema: JsonSchemaProperty = {
|
|
12
|
+
...baseSchema,
|
|
13
|
+
description: column.comment,
|
|
14
|
+
nullable: !column.notNull && !column.primary,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (column.args && sqlType === 'varchar' || sqlType === 'char') {
|
|
18
|
+
schema.maxLength = column.args[0] as number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (column.args && sqlType === 'decimal' || sqlType === 'float') {
|
|
22
|
+
if (column.args.length >= 1) {
|
|
23
|
+
schema.minimum = -(10 ** (column.args[0] as number));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (sqlType === 'enum' && column.args && column.args.length > 0) {
|
|
28
|
+
schema.enum = column.args as (string | number | boolean)[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (column.default !== undefined) {
|
|
32
|
+
schema.default = column.default;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return schema;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeType = (type: string): string => {
|
|
39
|
+
return type.toLowerCase();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const mapSqlTypeToBaseSchema = (
|
|
43
|
+
sqlType: string,
|
|
44
|
+
column: ColumnDef
|
|
45
|
+
): Omit<JsonSchemaProperty, 'nullable' | 'description'> => {
|
|
46
|
+
const type = normalizeType(sqlType);
|
|
47
|
+
|
|
48
|
+
const hasCustomTsType = column.tsType !== undefined;
|
|
49
|
+
|
|
50
|
+
switch (type) {
|
|
51
|
+
case 'int':
|
|
52
|
+
case 'integer':
|
|
53
|
+
case 'bigint':
|
|
54
|
+
return {
|
|
55
|
+
type: hasCustomTsType ? inferTypeFromTsType(column.tsType) : ('integer' as JsonSchemaType),
|
|
56
|
+
format: type === 'bigint' ? 'int64' : 'int32',
|
|
57
|
+
minimum: column.autoIncrement ? 1 : undefined,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
case 'decimal':
|
|
61
|
+
case 'float':
|
|
62
|
+
case 'double':
|
|
63
|
+
return {
|
|
64
|
+
type: hasCustomTsType ? inferTypeFromTsType(column.tsType) : ('number' as JsonSchemaType),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
case 'varchar':
|
|
68
|
+
return {
|
|
69
|
+
type: 'string' as JsonSchemaType,
|
|
70
|
+
minLength: column.notNull ? 1 : undefined,
|
|
71
|
+
maxLength: column.args?.[0] as number | undefined,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
case 'text':
|
|
75
|
+
return {
|
|
76
|
+
type: 'string' as JsonSchemaType,
|
|
77
|
+
minLength: column.notNull ? 1 : undefined,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
case 'char':
|
|
81
|
+
return {
|
|
82
|
+
type: 'string' as JsonSchemaType,
|
|
83
|
+
minLength: column.notNull ? column.args?.[0] as number || 1 : undefined,
|
|
84
|
+
maxLength: column.args?.[0] as number,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
case 'boolean':
|
|
88
|
+
return {
|
|
89
|
+
type: 'boolean' as JsonSchemaType,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'json':
|
|
93
|
+
return {
|
|
94
|
+
anyOf: [
|
|
95
|
+
{ type: 'object' as JsonSchemaType },
|
|
96
|
+
{ type: 'array' as JsonSchemaType },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
case 'blob':
|
|
101
|
+
case 'binary':
|
|
102
|
+
case 'varbinary':
|
|
103
|
+
return {
|
|
104
|
+
type: 'string' as JsonSchemaType,
|
|
105
|
+
format: 'base64' as JsonSchemaFormat,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
case 'date':
|
|
109
|
+
return {
|
|
110
|
+
type: 'string' as JsonSchemaType,
|
|
111
|
+
format: 'date' as JsonSchemaFormat,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
case 'datetime':
|
|
115
|
+
case 'timestamp':
|
|
116
|
+
return {
|
|
117
|
+
type: 'string' as JsonSchemaType,
|
|
118
|
+
format: 'date-time' as JsonSchemaFormat,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
case 'timestamptz':
|
|
122
|
+
return {
|
|
123
|
+
type: 'string' as JsonSchemaType,
|
|
124
|
+
format: 'date-time' as JsonSchemaFormat,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
case 'uuid':
|
|
128
|
+
return {
|
|
129
|
+
type: 'string' as JsonSchemaType,
|
|
130
|
+
format: 'uuid' as JsonSchemaFormat,
|
|
131
|
+
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
case 'enum':
|
|
135
|
+
return {
|
|
136
|
+
type: 'string' as JsonSchemaType,
|
|
137
|
+
enum: (column.args as (string | number | boolean)[]) || [],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
if (column.dialectTypes?.postgres && column.dialectTypes.postgres === 'bytea') {
|
|
142
|
+
return {
|
|
143
|
+
type: 'string' as JsonSchemaType,
|
|
144
|
+
format: 'base64' as JsonSchemaFormat,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
type: 'string' as JsonSchemaType,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const inferTypeFromTsType = (tsType: unknown): JsonSchemaType => {
|
|
155
|
+
if (typeof tsType === 'string') {
|
|
156
|
+
if (tsType === 'number') return 'number' as JsonSchemaType;
|
|
157
|
+
if (tsType === 'string') return 'string' as JsonSchemaType;
|
|
158
|
+
if (tsType === 'boolean') return 'boolean' as JsonSchemaType;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof tsType === 'function') {
|
|
162
|
+
const typeStr = tsType.name?.toLowerCase();
|
|
163
|
+
if (typeStr === 'number') return 'number' as JsonSchemaType;
|
|
164
|
+
if (typeStr === 'string') return 'string' as JsonSchemaType;
|
|
165
|
+
if (typeStr === 'boolean') return 'boolean' as JsonSchemaType;
|
|
166
|
+
if (typeStr === 'array') return 'array' as JsonSchemaType;
|
|
167
|
+
if (typeStr === 'object') return 'object' as JsonSchemaType;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return 'string' as JsonSchemaType;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Maps relation type to array or single object
|
|
175
|
+
*/
|
|
176
|
+
export const mapRelationType = (
|
|
177
|
+
relationType: string
|
|
178
|
+
): { type: 'object' | 'array'; isNullable: boolean } => {
|
|
179
|
+
switch (relationType) {
|
|
180
|
+
case 'HAS_MANY':
|
|
181
|
+
case 'BELONGS_TO_MANY':
|
|
182
|
+
return { type: 'array', isNullable: false };
|
|
183
|
+
case 'HAS_ONE':
|
|
184
|
+
case 'BELONGS_TO':
|
|
185
|
+
return { type: 'object', isNullable: true };
|
|
186
|
+
default:
|
|
187
|
+
return { type: 'object', isNullable: true };
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Gets the OpenAPI format for temporal columns
|
|
193
|
+
*/
|
|
194
|
+
export const getTemporalFormat = (sqlType: string): JsonSchemaFormat | undefined => {
|
|
195
|
+
const type = normalizeType(sqlType);
|
|
196
|
+
|
|
197
|
+
switch (type) {
|
|
198
|
+
case 'date':
|
|
199
|
+
return 'date' as JsonSchemaFormat;
|
|
200
|
+
case 'datetime':
|
|
201
|
+
case 'timestamp':
|
|
202
|
+
case 'timestamptz':
|
|
203
|
+
return 'date-time' as JsonSchemaFormat;
|
|
204
|
+
default:
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
@@ -66,7 +66,8 @@ import { SelectProjectionFacet } from './select/projection-facet.js';
|
|
|
66
66
|
import { SelectPredicateFacet } from './select/predicate-facet.js';
|
|
67
67
|
import { SelectCTEFacet } from './select/cte-facet.js';
|
|
68
68
|
import { SelectSetOpFacet } from './select/setop-facet.js';
|
|
69
|
-
import { SelectRelationFacet } from './select/relation-facet.js';
|
|
69
|
+
import { SelectRelationFacet } from './select/relation-facet.js';
|
|
70
|
+
import { extractSchema, SchemaOptions, OpenApiSchema } from '../openapi/index.js';
|
|
70
71
|
|
|
71
72
|
type ColumnSelectionValue =
|
|
72
73
|
| ColumnDef
|
|
@@ -1108,16 +1109,29 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
1108
1109
|
return this.compile(dialect).sql;
|
|
1109
1110
|
}
|
|
1110
1111
|
|
|
1111
|
-
/**
|
|
1112
|
-
* Gets
|
|
1113
|
-
* @returns Hydration plan or undefined if none exists
|
|
1114
|
-
* @example
|
|
1115
|
-
* const plan = qb.include('posts').getHydrationPlan();
|
|
1116
|
-
* console.log(plan?.relations); // Information about included relations
|
|
1117
|
-
*/
|
|
1118
|
-
getHydrationPlan(): HydrationPlan | undefined {
|
|
1119
|
-
return this.context.hydration.getPlan();
|
|
1120
|
-
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Gets hydration plan for query
|
|
1114
|
+
* @returns Hydration plan or undefined if none exists
|
|
1115
|
+
* @example
|
|
1116
|
+
* const plan = qb.include('posts').getHydrationPlan();
|
|
1117
|
+
* console.log(plan?.relations); // Information about included relations
|
|
1118
|
+
*/
|
|
1119
|
+
getHydrationPlan(): HydrationPlan | undefined {
|
|
1120
|
+
return this.context.hydration.getPlan();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Gets OpenAPI 3.1 JSON Schema for query result
|
|
1125
|
+
* @param options - Schema generation options
|
|
1126
|
+
* @returns OpenAPI 3.1 JSON Schema for query result
|
|
1127
|
+
* @example
|
|
1128
|
+
* const schema = qb.select('id', 'title', 'author').getSchema();
|
|
1129
|
+
* console.log(JSON.stringify(schema, null, 2));
|
|
1130
|
+
*/
|
|
1131
|
+
getSchema(options?: SchemaOptions): OpenApiSchema {
|
|
1132
|
+
const plan = this.context.hydration.getPlan();
|
|
1133
|
+
return extractSchema(this.env.table, plan, this.context.state.ast.columns, options);
|
|
1134
|
+
}
|
|
1121
1135
|
|
|
1122
1136
|
/**
|
|
1123
1137
|
* Gets the Abstract Syntax Tree (AST) representation of the query
|