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.
@@ -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 the hydration plan for the query
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