metal-orm 1.0.80 → 1.0.81
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 +402 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -23
- package/dist/index.d.ts +79 -23
- package/dist/index.js +401 -71
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/src/core/execution/db-executor.ts +5 -4
- package/src/core/execution/executors/mysql-executor.ts +9 -7
- package/src/openapi/index.ts +1 -0
- package/src/openapi/query-parameters.ts +206 -0
- package/src/openapi/schema-extractor.ts +290 -122
- package/src/openapi/schema-types.ts +72 -6
- package/src/openapi/type-mappers.ts +28 -8
- package/src/orm/unit-of-work.ts +25 -13
- package/src/query-builder/select.ts +17 -7
|
@@ -2,40 +2,101 @@ import type { TableDef } from '../schema/table.js';
|
|
|
2
2
|
import type { RelationDef } from '../schema/relation.js';
|
|
3
3
|
import type { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
|
|
4
4
|
import type { ProjectionNode } from '../query-builder/select-query-state.js';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
OpenApiSchema,
|
|
9
|
+
OpenApiSchemaBundle,
|
|
10
|
+
SchemaExtractionContext,
|
|
11
|
+
SchemaOptions,
|
|
12
|
+
OutputSchemaOptions,
|
|
13
|
+
InputSchemaOptions,
|
|
14
|
+
JsonSchemaProperty,
|
|
15
|
+
JsonSchemaType
|
|
16
|
+
} from './schema-types.js';
|
|
7
17
|
import { mapColumnType, mapRelationType } from './type-mappers.js';
|
|
8
18
|
|
|
19
|
+
const DEFAULT_MAX_DEPTH = 5;
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
|
-
* Extracts OpenAPI 3.1
|
|
11
|
-
* @param table - Table definition
|
|
12
|
-
* @param plan - Hydration plan from query builder
|
|
13
|
-
* @param projectionNodes - Projection AST nodes (for computed fields)
|
|
14
|
-
* @param options - Schema generation options
|
|
15
|
-
* @returns OpenAPI 3.1 JSON Schema
|
|
22
|
+
* Extracts OpenAPI 3.1 schemas for output and optional input payloads.
|
|
16
23
|
*/
|
|
17
24
|
export const extractSchema = (
|
|
18
25
|
table: TableDef,
|
|
19
26
|
plan: HydrationPlan | undefined,
|
|
20
27
|
projectionNodes: ProjectionNode[] | undefined,
|
|
21
28
|
options: SchemaOptions = {}
|
|
22
|
-
):
|
|
23
|
-
const
|
|
29
|
+
): OpenApiSchemaBundle => {
|
|
30
|
+
const outputOptions = resolveOutputOptions(options);
|
|
31
|
+
const outputContext = createContext(outputOptions.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
32
|
+
const output = extractOutputSchema(table, plan, projectionNodes, outputContext, outputOptions);
|
|
33
|
+
|
|
34
|
+
const inputOptions = resolveInputOptions(options);
|
|
35
|
+
if (!inputOptions) {
|
|
36
|
+
return { output };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputContext = createContext(inputOptions.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
40
|
+
const input = extractInputSchema(table, inputContext, inputOptions);
|
|
41
|
+
|
|
42
|
+
return { output, input };
|
|
43
|
+
};
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
const resolveOutputOptions = (options: SchemaOptions): OutputSchemaOptions => ({
|
|
46
|
+
mode: options.mode ?? 'full',
|
|
47
|
+
includeDescriptions: options.includeDescriptions,
|
|
48
|
+
includeEnums: options.includeEnums,
|
|
49
|
+
includeExamples: options.includeExamples,
|
|
50
|
+
includeDefaults: options.includeDefaults,
|
|
51
|
+
includeNullable: options.includeNullable,
|
|
52
|
+
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const resolveInputOptions = (options: SchemaOptions): InputSchemaOptions | undefined => {
|
|
56
|
+
if (options.input === false) return undefined;
|
|
57
|
+
const input = options.input ?? {};
|
|
58
|
+
const mode = input.mode ?? 'create';
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
mode,
|
|
62
|
+
includeRelations: input.includeRelations ?? true,
|
|
63
|
+
relationMode: input.relationMode ?? 'mixed',
|
|
64
|
+
includeDescriptions: input.includeDescriptions ?? options.includeDescriptions,
|
|
65
|
+
includeEnums: input.includeEnums ?? options.includeEnums,
|
|
66
|
+
includeExamples: input.includeExamples ?? options.includeExamples,
|
|
67
|
+
includeDefaults: input.includeDefaults ?? options.includeDefaults,
|
|
68
|
+
includeNullable: input.includeNullable ?? options.includeNullable,
|
|
69
|
+
maxDepth: input.maxDepth ?? options.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
70
|
+
omitReadOnly: input.omitReadOnly ?? true,
|
|
71
|
+
excludePrimaryKey: input.excludePrimaryKey ?? false,
|
|
72
|
+
requirePrimaryKey: input.requirePrimaryKey ?? (mode === 'update')
|
|
30
73
|
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const createContext = (maxDepth: number): SchemaExtractionContext => ({
|
|
77
|
+
visitedTables: new Set(),
|
|
78
|
+
schemaCache: new Map(),
|
|
79
|
+
depth: 0,
|
|
80
|
+
maxDepth
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Output schema extraction (query results)
|
|
85
|
+
*/
|
|
86
|
+
const extractOutputSchema = (
|
|
87
|
+
table: TableDef,
|
|
88
|
+
plan: HydrationPlan | undefined,
|
|
89
|
+
projectionNodes: ProjectionNode[] | undefined,
|
|
90
|
+
context: SchemaExtractionContext,
|
|
91
|
+
options: OutputSchemaOptions
|
|
92
|
+
): OpenApiSchema => {
|
|
93
|
+
const mode = options.mode ?? 'full';
|
|
31
94
|
|
|
32
|
-
// Detect if query contains computed fields (non-Column nodes)
|
|
33
95
|
const hasComputedFields = projectionNodes && projectionNodes.some(
|
|
34
96
|
node => node.type !== 'Column'
|
|
35
97
|
);
|
|
36
98
|
|
|
37
99
|
if (hasComputedFields) {
|
|
38
|
-
// Use projection-based extraction for computed fields + relations
|
|
39
100
|
return extractFromProjectionNodes(table, projectionNodes!, context, options);
|
|
40
101
|
}
|
|
41
102
|
|
|
@@ -46,22 +107,144 @@ export const extractSchema = (
|
|
|
46
107
|
return extractFullTableSchema(table, context, options);
|
|
47
108
|
};
|
|
48
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Input schema extraction (write payloads)
|
|
112
|
+
*/
|
|
113
|
+
const extractInputSchema = (
|
|
114
|
+
table: TableDef,
|
|
115
|
+
context: SchemaExtractionContext,
|
|
116
|
+
options: InputSchemaOptions
|
|
117
|
+
): OpenApiSchema => {
|
|
118
|
+
const cacheKey = `${table.name}:${options.mode ?? 'create'}`;
|
|
119
|
+
|
|
120
|
+
if (context.schemaCache.has(cacheKey)) {
|
|
121
|
+
return context.schemaCache.get(cacheKey)!;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (context.visitedTables.has(cacheKey) && context.depth > 0) {
|
|
125
|
+
return buildCircularReferenceSchema(table.name, 'input');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
context.visitedTables.add(cacheKey);
|
|
129
|
+
|
|
130
|
+
const properties: Record<string, JsonSchemaProperty> = {};
|
|
131
|
+
const required: string[] = [];
|
|
132
|
+
const primaryKey = findPrimaryKey(table);
|
|
133
|
+
|
|
134
|
+
for (const [columnName, column] of Object.entries(table.columns)) {
|
|
135
|
+
const isPrimary = columnName === primaryKey || column.primary;
|
|
136
|
+
if (options.excludePrimaryKey && isPrimary) continue;
|
|
137
|
+
if (options.omitReadOnly && isReadOnlyColumn(column)) continue;
|
|
138
|
+
|
|
139
|
+
properties[columnName] = mapColumnType(column, options);
|
|
140
|
+
|
|
141
|
+
if (options.mode === 'create' && isRequiredForCreate(column)) {
|
|
142
|
+
required.push(columnName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.mode === 'update' && options.requirePrimaryKey && isPrimary) {
|
|
146
|
+
required.push(columnName);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (options.includeRelations && context.depth < context.maxDepth) {
|
|
151
|
+
for (const [relationName, relation] of Object.entries(table.relations)) {
|
|
152
|
+
properties[relationName] = extractInputRelationSchema(
|
|
153
|
+
relation,
|
|
154
|
+
{ ...context, depth: context.depth + 1 },
|
|
155
|
+
options
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const schema: OpenApiSchema = {
|
|
161
|
+
type: 'object',
|
|
162
|
+
properties,
|
|
163
|
+
required
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
context.schemaCache.set(cacheKey, schema);
|
|
167
|
+
return schema;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const isReadOnlyColumn = (column: { autoIncrement?: boolean; generated?: string }): boolean =>
|
|
171
|
+
Boolean(column.autoIncrement || column.generated === 'always');
|
|
172
|
+
|
|
173
|
+
const isRequiredForCreate = (column: { notNull?: boolean; primary?: boolean; default?: unknown; autoIncrement?: boolean; generated?: string }): boolean => {
|
|
174
|
+
if (isReadOnlyColumn(column)) return false;
|
|
175
|
+
if (column.default !== undefined) return false;
|
|
176
|
+
return Boolean(column.notNull || column.primary);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const buildPrimaryKeySchema = (
|
|
180
|
+
table: TableDef,
|
|
181
|
+
options: InputSchemaOptions
|
|
182
|
+
): JsonSchemaProperty => {
|
|
183
|
+
const primaryKey = findPrimaryKey(table);
|
|
184
|
+
const column = table.columns[primaryKey];
|
|
185
|
+
if (!column) {
|
|
186
|
+
return {
|
|
187
|
+
anyOf: [
|
|
188
|
+
{ type: 'string' as JsonSchemaType },
|
|
189
|
+
{ type: 'number' as JsonSchemaType },
|
|
190
|
+
{ type: 'integer' as JsonSchemaType }
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return mapColumnType(column, options);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const extractInputRelationSchema = (
|
|
199
|
+
relation: RelationDef,
|
|
200
|
+
context: SchemaExtractionContext,
|
|
201
|
+
options: InputSchemaOptions
|
|
202
|
+
): JsonSchemaProperty => {
|
|
203
|
+
const { type: relationType, isNullable } = mapRelationType(relation.type);
|
|
204
|
+
const relationMode = options.relationMode ?? 'mixed';
|
|
205
|
+
const allowIds = relationMode !== 'objects';
|
|
206
|
+
const allowObjects = relationMode !== 'ids';
|
|
207
|
+
|
|
208
|
+
const variants: JsonSchemaProperty[] = [];
|
|
209
|
+
|
|
210
|
+
if (allowIds) {
|
|
211
|
+
variants.push(buildPrimaryKeySchema(relation.target, options));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (allowObjects) {
|
|
215
|
+
const targetSchema = extractInputSchema(relation.target, context, options);
|
|
216
|
+
variants.push(targetSchema as JsonSchemaProperty);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const itemSchema: JsonSchemaProperty =
|
|
220
|
+
variants.length === 1 ? variants[0] : { anyOf: variants };
|
|
221
|
+
|
|
222
|
+
if (relationType === 'array') {
|
|
223
|
+
return {
|
|
224
|
+
type: 'array',
|
|
225
|
+
items: itemSchema,
|
|
226
|
+
nullable: isNullable
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...itemSchema,
|
|
232
|
+
nullable: isNullable
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
49
236
|
/**
|
|
50
237
|
* Extracts schema from projection nodes (handles computed fields)
|
|
51
|
-
* @param table - Table definition
|
|
52
|
-
* @param projectionNodes - Projection AST nodes
|
|
53
|
-
* @param context - Schema extraction context
|
|
54
|
-
* @param options - Schema generation options
|
|
55
|
-
* @returns OpenAPI 3.1 JSON Schema
|
|
56
238
|
*/
|
|
57
239
|
const extractFromProjectionNodes = (
|
|
58
240
|
table: TableDef,
|
|
59
241
|
projectionNodes: ProjectionNode[],
|
|
60
242
|
context: SchemaExtractionContext,
|
|
61
|
-
options:
|
|
243
|
+
options: OutputSchemaOptions
|
|
62
244
|
): OpenApiSchema => {
|
|
63
245
|
const properties: Record<string, JsonSchemaProperty> = {};
|
|
64
246
|
const required: string[] = [];
|
|
247
|
+
const includeDescriptions = Boolean(options.includeDescriptions);
|
|
65
248
|
|
|
66
249
|
for (const node of projectionNodes) {
|
|
67
250
|
if (!node || typeof node !== 'object') continue;
|
|
@@ -76,11 +259,7 @@ const extractFromProjectionNodes = (
|
|
|
76
259
|
const column = table.columns[columnNode.name];
|
|
77
260
|
if (!column) continue;
|
|
78
261
|
|
|
79
|
-
const property = mapColumnType(column);
|
|
80
|
-
if (!property.description && options.includeDescriptions && column.comment) {
|
|
81
|
-
property.description = column.comment;
|
|
82
|
-
}
|
|
83
|
-
|
|
262
|
+
const property = mapColumnType(column, options);
|
|
84
263
|
properties[propertyName] = property;
|
|
85
264
|
|
|
86
265
|
if (column.notNull || column.primary) {
|
|
@@ -89,38 +268,44 @@ const extractFromProjectionNodes = (
|
|
|
89
268
|
} else if (projection.type === 'Function' || projection.type === 'WindowFunction') {
|
|
90
269
|
const fnNode = node as { fn?: string; name?: string };
|
|
91
270
|
const functionName = fnNode.fn?.toUpperCase() ?? fnNode.name?.toUpperCase() ?? '';
|
|
92
|
-
const propertySchema = projection.type === 'Function'
|
|
93
|
-
? mapFunctionNodeToSchema(functionName)
|
|
94
|
-
: mapWindowFunctionToSchema(functionName);
|
|
95
|
-
|
|
271
|
+
const propertySchema = projection.type === 'Function'
|
|
272
|
+
? mapFunctionNodeToSchema(functionName, includeDescriptions)
|
|
273
|
+
: mapWindowFunctionToSchema(functionName, includeDescriptions);
|
|
274
|
+
|
|
96
275
|
properties[propertyName] = propertySchema;
|
|
97
276
|
|
|
98
277
|
const isCountFunction = functionName === 'COUNT';
|
|
99
278
|
const isWindowRankFunction = functionName === 'ROW_NUMBER' || functionName === 'RANK';
|
|
100
|
-
|
|
279
|
+
|
|
101
280
|
if (isCountFunction || isWindowRankFunction) {
|
|
102
281
|
required.push(propertyName);
|
|
103
282
|
}
|
|
104
283
|
} else if (projection.type === 'CaseExpression') {
|
|
105
284
|
const propertySchema: JsonSchemaProperty = {
|
|
106
285
|
type: 'string' as JsonSchemaType,
|
|
107
|
-
|
|
108
|
-
nullable: true,
|
|
286
|
+
nullable: true
|
|
109
287
|
};
|
|
288
|
+
if (includeDescriptions) {
|
|
289
|
+
propertySchema.description = 'Computed CASE expression';
|
|
290
|
+
}
|
|
110
291
|
properties[propertyName] = propertySchema;
|
|
111
292
|
} else if (projection.type === 'ScalarSubquery') {
|
|
112
293
|
const propertySchema: JsonSchemaProperty = {
|
|
113
294
|
type: 'object' as JsonSchemaType,
|
|
114
|
-
|
|
115
|
-
nullable: true,
|
|
295
|
+
nullable: true
|
|
116
296
|
};
|
|
297
|
+
if (includeDescriptions) {
|
|
298
|
+
propertySchema.description = 'Subquery result';
|
|
299
|
+
}
|
|
117
300
|
properties[propertyName] = propertySchema;
|
|
118
301
|
} else if (projection.type === 'CastExpression') {
|
|
119
302
|
const propertySchema: JsonSchemaProperty = {
|
|
120
303
|
type: 'string' as JsonSchemaType,
|
|
121
|
-
|
|
122
|
-
nullable: true,
|
|
304
|
+
nullable: true
|
|
123
305
|
};
|
|
306
|
+
if (includeDescriptions) {
|
|
307
|
+
propertySchema.description = 'CAST expression result';
|
|
308
|
+
}
|
|
124
309
|
properties[propertyName] = propertySchema;
|
|
125
310
|
}
|
|
126
311
|
}
|
|
@@ -128,16 +313,17 @@ const extractFromProjectionNodes = (
|
|
|
128
313
|
return {
|
|
129
314
|
type: 'object',
|
|
130
315
|
properties,
|
|
131
|
-
required
|
|
316
|
+
required
|
|
132
317
|
};
|
|
133
318
|
};
|
|
134
319
|
|
|
135
320
|
/**
|
|
136
321
|
* Maps SQL aggregate functions to OpenAPI types
|
|
137
|
-
* @param functionName - SQL function name
|
|
138
|
-
* @returns OpenAPI JSON Schema property
|
|
139
322
|
*/
|
|
140
|
-
const mapFunctionNodeToSchema = (
|
|
323
|
+
const mapFunctionNodeToSchema = (
|
|
324
|
+
functionName: string,
|
|
325
|
+
includeDescriptions: boolean
|
|
326
|
+
): JsonSchemaProperty => {
|
|
141
327
|
const upperName = functionName.toUpperCase();
|
|
142
328
|
|
|
143
329
|
switch (upperName) {
|
|
@@ -146,44 +332,41 @@ const mapFunctionNodeToSchema = (functionName: string): JsonSchemaProperty => {
|
|
|
146
332
|
case 'AVG':
|
|
147
333
|
case 'MIN':
|
|
148
334
|
case 'MAX':
|
|
149
|
-
return {
|
|
335
|
+
return withOptionalDescription({
|
|
150
336
|
type: 'number' as JsonSchemaType,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
};
|
|
337
|
+
nullable: false
|
|
338
|
+
}, includeDescriptions, `${upperName} aggregate function result`);
|
|
154
339
|
|
|
155
340
|
case 'GROUP_CONCAT':
|
|
156
341
|
case 'STRING_AGG':
|
|
157
342
|
case 'ARRAY_AGG':
|
|
158
|
-
return {
|
|
343
|
+
return withOptionalDescription({
|
|
159
344
|
type: 'string' as JsonSchemaType,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
345
|
+
nullable: true
|
|
346
|
+
}, includeDescriptions, `${upperName} aggregate function result`);
|
|
163
347
|
|
|
164
348
|
case 'JSON_ARRAYAGG':
|
|
165
349
|
case 'JSON_OBJECTAGG':
|
|
166
|
-
return {
|
|
350
|
+
return withOptionalDescription({
|
|
167
351
|
type: 'object' as JsonSchemaType,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
};
|
|
352
|
+
nullable: true
|
|
353
|
+
}, includeDescriptions, `${upperName} aggregate function result`);
|
|
171
354
|
|
|
172
355
|
default:
|
|
173
|
-
return {
|
|
356
|
+
return withOptionalDescription({
|
|
174
357
|
type: 'string' as JsonSchemaType,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
};
|
|
358
|
+
nullable: true
|
|
359
|
+
}, includeDescriptions, `Unknown function: ${functionName}`);
|
|
178
360
|
}
|
|
179
361
|
};
|
|
180
362
|
|
|
181
363
|
/**
|
|
182
364
|
* Maps SQL window functions to OpenAPI types
|
|
183
|
-
* @param functionName - SQL function name
|
|
184
|
-
* @returns OpenAPI JSON Schema property
|
|
185
365
|
*/
|
|
186
|
-
const mapWindowFunctionToSchema = (
|
|
366
|
+
const mapWindowFunctionToSchema = (
|
|
367
|
+
functionName: string,
|
|
368
|
+
includeDescriptions: boolean
|
|
369
|
+
): JsonSchemaProperty => {
|
|
187
370
|
const upperName = functionName.toUpperCase();
|
|
188
371
|
|
|
189
372
|
switch (upperName) {
|
|
@@ -191,44 +374,47 @@ const mapWindowFunctionToSchema = (functionName: string): JsonSchemaProperty =>
|
|
|
191
374
|
case 'RANK':
|
|
192
375
|
case 'DENSE_RANK':
|
|
193
376
|
case 'NTILE':
|
|
194
|
-
return {
|
|
377
|
+
return withOptionalDescription({
|
|
195
378
|
type: 'integer' as JsonSchemaType,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
};
|
|
379
|
+
nullable: false
|
|
380
|
+
}, includeDescriptions, `${upperName} window function result`);
|
|
199
381
|
|
|
200
382
|
case 'LAG':
|
|
201
383
|
case 'LEAD':
|
|
202
384
|
case 'FIRST_VALUE':
|
|
203
385
|
case 'LAST_VALUE':
|
|
204
|
-
return {
|
|
386
|
+
return withOptionalDescription({
|
|
205
387
|
type: 'string' as JsonSchemaType,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
};
|
|
388
|
+
nullable: true
|
|
389
|
+
}, includeDescriptions, `${upperName} window function result`);
|
|
209
390
|
|
|
210
391
|
default:
|
|
211
|
-
return {
|
|
392
|
+
return withOptionalDescription({
|
|
212
393
|
type: 'string' as JsonSchemaType,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
394
|
+
nullable: true
|
|
395
|
+
}, includeDescriptions, `Unknown window function: ${functionName}`);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const withOptionalDescription = (
|
|
400
|
+
schema: JsonSchemaProperty,
|
|
401
|
+
includeDescriptions: boolean,
|
|
402
|
+
description: string
|
|
403
|
+
): JsonSchemaProperty => {
|
|
404
|
+
if (includeDescriptions) {
|
|
405
|
+
return { ...schema, description };
|
|
216
406
|
}
|
|
407
|
+
return schema;
|
|
217
408
|
};
|
|
218
409
|
|
|
219
410
|
/**
|
|
220
411
|
* Extracts schema with only selected columns and relations
|
|
221
|
-
* @param table - Table definition
|
|
222
|
-
* @param plan - Hydration plan
|
|
223
|
-
* @param context - Schema extraction context
|
|
224
|
-
* @param options - Schema generation options
|
|
225
|
-
* @returns OpenAPI 3.1 JSON Schema
|
|
226
412
|
*/
|
|
227
413
|
const extractSelectedSchema = (
|
|
228
414
|
table: TableDef,
|
|
229
415
|
plan: HydrationPlan,
|
|
230
416
|
context: SchemaExtractionContext,
|
|
231
|
-
options:
|
|
417
|
+
options: OutputSchemaOptions
|
|
232
418
|
): OpenApiSchema => {
|
|
233
419
|
const properties: Record<string, JsonSchemaProperty> = {};
|
|
234
420
|
const required: string[] = [];
|
|
@@ -237,12 +423,7 @@ const extractSelectedSchema = (
|
|
|
237
423
|
const column = table.columns[columnName];
|
|
238
424
|
if (!column) return;
|
|
239
425
|
|
|
240
|
-
|
|
241
|
-
if (!property.description && options.includeDescriptions && column.comment) {
|
|
242
|
-
property.description = column.comment;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
properties[columnName] = property;
|
|
426
|
+
properties[columnName] = mapColumnType(column, options);
|
|
246
427
|
|
|
247
428
|
if (column.notNull || column.primary) {
|
|
248
429
|
required.push(columnName);
|
|
@@ -272,21 +453,17 @@ const extractSelectedSchema = (
|
|
|
272
453
|
return {
|
|
273
454
|
type: 'object',
|
|
274
455
|
properties,
|
|
275
|
-
required
|
|
456
|
+
required
|
|
276
457
|
};
|
|
277
458
|
};
|
|
278
459
|
|
|
279
460
|
/**
|
|
280
461
|
* Extracts full table schema (all columns, all relations)
|
|
281
|
-
* @param table - Table definition
|
|
282
|
-
* @param context - Schema extraction context
|
|
283
|
-
* @param options - Schema generation options
|
|
284
|
-
* @returns OpenAPI 3.1 JSON Schema
|
|
285
462
|
*/
|
|
286
463
|
const extractFullTableSchema = (
|
|
287
464
|
table: TableDef,
|
|
288
465
|
context: SchemaExtractionContext,
|
|
289
|
-
options:
|
|
466
|
+
options: OutputSchemaOptions
|
|
290
467
|
): OpenApiSchema => {
|
|
291
468
|
const cacheKey = table.name;
|
|
292
469
|
|
|
@@ -295,16 +472,7 @@ const extractFullTableSchema = (
|
|
|
295
472
|
}
|
|
296
473
|
|
|
297
474
|
if (context.visitedTables.has(cacheKey) && context.depth > 0) {
|
|
298
|
-
return
|
|
299
|
-
type: 'object',
|
|
300
|
-
properties: {
|
|
301
|
-
_ref: {
|
|
302
|
-
type: 'string' as JsonSchemaType,
|
|
303
|
-
description: `Circular reference to ${table.name}`,
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
required: [],
|
|
307
|
-
};
|
|
475
|
+
return buildCircularReferenceSchema(table.name, 'output');
|
|
308
476
|
}
|
|
309
477
|
|
|
310
478
|
context.visitedTables.add(cacheKey);
|
|
@@ -313,12 +481,7 @@ const extractFullTableSchema = (
|
|
|
313
481
|
const required: string[] = [];
|
|
314
482
|
|
|
315
483
|
Object.entries(table.columns).forEach(([columnName, column]) => {
|
|
316
|
-
|
|
317
|
-
if (!property.description && options.includeDescriptions && column.comment) {
|
|
318
|
-
property.description = column.comment;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
properties[columnName] = property;
|
|
484
|
+
properties[columnName] = mapColumnType(column, options);
|
|
322
485
|
|
|
323
486
|
if (column.notNull || column.primary) {
|
|
324
487
|
required.push(columnName);
|
|
@@ -349,7 +512,7 @@ const extractFullTableSchema = (
|
|
|
349
512
|
const schema: OpenApiSchema = {
|
|
350
513
|
type: 'object',
|
|
351
514
|
properties,
|
|
352
|
-
required
|
|
515
|
+
required
|
|
353
516
|
};
|
|
354
517
|
|
|
355
518
|
context.schemaCache.set(cacheKey, schema);
|
|
@@ -358,19 +521,13 @@ const extractFullTableSchema = (
|
|
|
358
521
|
|
|
359
522
|
/**
|
|
360
523
|
* Extracts schema for a single relation
|
|
361
|
-
* @param relation - Relation definition
|
|
362
|
-
* @param relationPlan - Hydration plan for relation
|
|
363
|
-
* @param selectedColumns - Selected columns from relation
|
|
364
|
-
* @param context - Schema extraction context
|
|
365
|
-
* @param options - Schema generation options
|
|
366
|
-
* @returns OpenAPI JSON Schema property for relation
|
|
367
524
|
*/
|
|
368
525
|
const extractRelationSchema = (
|
|
369
526
|
relation: RelationDef,
|
|
370
527
|
relationPlan: HydrationRelationPlan | undefined,
|
|
371
528
|
selectedColumns: string[],
|
|
372
529
|
context: SchemaExtractionContext,
|
|
373
|
-
options:
|
|
530
|
+
options: OutputSchemaOptions
|
|
374
531
|
): JsonSchemaProperty => {
|
|
375
532
|
const targetTable = relation.target;
|
|
376
533
|
const { type: relationType, isNullable } = mapRelationType(relation.type);
|
|
@@ -382,7 +539,7 @@ const extractRelationSchema = (
|
|
|
382
539
|
rootTable: targetTable.name,
|
|
383
540
|
rootPrimaryKey: relationPlan.targetPrimaryKey,
|
|
384
541
|
rootColumns: selectedColumns,
|
|
385
|
-
relations: []
|
|
542
|
+
relations: []
|
|
386
543
|
};
|
|
387
544
|
|
|
388
545
|
targetSchema = extractSelectedSchema(targetTable, plan, context, options);
|
|
@@ -394,7 +551,7 @@ const extractRelationSchema = (
|
|
|
394
551
|
return {
|
|
395
552
|
type: 'array',
|
|
396
553
|
items: targetSchema as JsonSchemaProperty,
|
|
397
|
-
nullable: isNullable
|
|
554
|
+
nullable: isNullable
|
|
398
555
|
};
|
|
399
556
|
}
|
|
400
557
|
|
|
@@ -403,15 +560,26 @@ const extractRelationSchema = (
|
|
|
403
560
|
properties: targetSchema.properties,
|
|
404
561
|
required: targetSchema.required,
|
|
405
562
|
nullable: isNullable,
|
|
406
|
-
description: targetSchema.description
|
|
563
|
+
description: targetSchema.description
|
|
407
564
|
};
|
|
408
565
|
};
|
|
409
566
|
|
|
567
|
+
const buildCircularReferenceSchema = (
|
|
568
|
+
tableName: string,
|
|
569
|
+
kind: 'input' | 'output'
|
|
570
|
+
): OpenApiSchema => ({
|
|
571
|
+
type: 'object',
|
|
572
|
+
properties: {
|
|
573
|
+
_ref: {
|
|
574
|
+
type: 'string' as JsonSchemaType,
|
|
575
|
+
description: `Circular ${kind} reference to ${tableName}`
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
required: []
|
|
579
|
+
});
|
|
580
|
+
|
|
410
581
|
/**
|
|
411
582
|
* Converts a schema to a JSON string with optional pretty printing
|
|
412
|
-
* @param schema - OpenAPI schema
|
|
413
|
-
* @param pretty - Whether to pretty print
|
|
414
|
-
* @returns JSON string
|
|
415
583
|
*/
|
|
416
584
|
export const schemaToJson = (schema: OpenApiSchema, pretty = false): string => {
|
|
417
585
|
return JSON.stringify(schema, null, pretty ? 2 : 0);
|