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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"engines": {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"gen:entities": "node scripts/generate-entities.mjs",
|
|
30
30
|
"test": "vitest",
|
|
31
31
|
"test:ui": "vitest --ui",
|
|
32
|
+
"test:mysql": "vitest --run tests/e2e/mysql-memory.test.ts tests/e2e/decorators-mysql-memory.test.ts tests/e2e/save-graph-mysql-memory.test.ts",
|
|
33
|
+
"test:sqlite": "vitest --run tests/e2e/sqlite-memory.test.ts tests/e2e/decorators-sqlite-memory.test.ts tests/e2e/save-graph-sqlite-memory.test.ts",
|
|
34
|
+
"test:pglite": "vitest --run tests/e2e/pglite-memory.test.ts",
|
|
35
|
+
"test:e2e": "vitest tests/e2e",
|
|
32
36
|
"show-sql": "node scripts/show-sql.mjs",
|
|
33
37
|
"lint": "node scripts/run-eslint.mjs",
|
|
34
38
|
"lint:fix": "node scripts/run-eslint.mjs --fix"
|
|
@@ -54,11 +58,13 @@
|
|
|
54
58
|
}
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
|
-
"@
|
|
61
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
58
62
|
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
59
63
|
"@typescript-eslint/parser": "^8.20.0",
|
|
64
|
+
"@vitest/ui": "^4.0.14",
|
|
60
65
|
"eslint": "^8.57.0",
|
|
61
66
|
"eslint-plugin-deprecation": "^3.0.0",
|
|
67
|
+
"mysql-memory-server": "^1.13.0",
|
|
62
68
|
"mysql2": "^3.15.3",
|
|
63
69
|
"pg": "^8.16.3",
|
|
64
70
|
"sqlite3": "^5.1.7",
|
|
@@ -33,11 +33,15 @@ export class PostgresDialect extends SqlDialectBase {
|
|
|
33
33
|
* @param id - Identifier to quote
|
|
34
34
|
* @returns Quoted identifier
|
|
35
35
|
*/
|
|
36
|
-
quoteIdentifier(id: string): string {
|
|
37
|
-
return `"${id}"`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
quoteIdentifier(id: string): string {
|
|
37
|
+
return `"${id}"`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected formatPlaceholder(index: number): string {
|
|
41
|
+
return `$${index}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
41
45
|
* Compiles JSON path expression using PostgreSQL syntax
|
|
42
46
|
* @param node - JSON path node
|
|
43
47
|
* @returns PostgreSQL JSON path expression
|
package/src/index.ts
CHANGED
|
@@ -41,16 +41,18 @@ export * from './orm/relations/has-many.js';
|
|
|
41
41
|
export * from './orm/relations/belongs-to.js';
|
|
42
42
|
export * from './orm/relations/many-to-many.js';
|
|
43
43
|
export * from './orm/execute.js';
|
|
44
|
-
export type { EntityContext } from './orm/entity-context.js';
|
|
45
|
-
export type { PrimaryKey as EntityPrimaryKey } from './orm/entity-context.js';
|
|
44
|
+
export type { EntityContext } from './orm/entity-context.js';
|
|
45
|
+
export type { PrimaryKey as EntityPrimaryKey } from './orm/entity-context.js';
|
|
46
46
|
export * from './orm/execution-context.js';
|
|
47
47
|
export * from './orm/hydration-context.js';
|
|
48
48
|
export * from './orm/domain-event-bus.js';
|
|
49
49
|
export * from './orm/runtime-types.js';
|
|
50
50
|
export * from './orm/query-logger.js';
|
|
51
|
+
export * from './orm/interceptor-pipeline.js';
|
|
51
52
|
export * from './orm/jsonify.js';
|
|
52
|
-
export * from './orm/save-graph-types.js';
|
|
53
|
-
export * from './decorators/index.js';
|
|
53
|
+
export * from './orm/save-graph-types.js';
|
|
54
|
+
export * from './decorators/index.js';
|
|
55
|
+
export * from './openapi/index.js';
|
|
54
56
|
|
|
55
57
|
// NEW: execution abstraction + helpers
|
|
56
58
|
export * from './core/execution/db-executor.js';
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import type { TableDef } from '../schema/table.js';
|
|
2
|
+
import type { RelationDef } from '../schema/relation.js';
|
|
3
|
+
import type { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
|
|
4
|
+
import type { ProjectionNode } from '../query-builder/select-query-state.js';
|
|
5
|
+
|
|
6
|
+
import type { OpenApiSchema, SchemaExtractionContext, SchemaOptions, JsonSchemaProperty, JsonSchemaType } from './schema-types.js';
|
|
7
|
+
import { mapColumnType, mapRelationType } from './type-mappers.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extracts OpenAPI 3.1 schema from a query builder's hydration plan
|
|
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
|
|
16
|
+
*/
|
|
17
|
+
export const extractSchema = (
|
|
18
|
+
table: TableDef,
|
|
19
|
+
plan: HydrationPlan | undefined,
|
|
20
|
+
projectionNodes: ProjectionNode[] | undefined,
|
|
21
|
+
options: SchemaOptions = {}
|
|
22
|
+
): OpenApiSchema => {
|
|
23
|
+
const mode = options.mode ?? 'full';
|
|
24
|
+
|
|
25
|
+
const context: SchemaExtractionContext = {
|
|
26
|
+
visitedTables: new Set(),
|
|
27
|
+
schemaCache: new Map(),
|
|
28
|
+
depth: 0,
|
|
29
|
+
maxDepth: options.maxDepth ?? 5,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Detect if query contains computed fields (non-Column nodes)
|
|
33
|
+
const hasComputedFields = projectionNodes && projectionNodes.some(
|
|
34
|
+
node => node.type !== 'Column'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (hasComputedFields) {
|
|
38
|
+
// Use projection-based extraction for computed fields + relations
|
|
39
|
+
return extractFromProjectionNodes(table, projectionNodes!, context, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (mode === 'selected' && plan) {
|
|
43
|
+
return extractSelectedSchema(table, plan, context, options);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return extractFullTableSchema(table, context, options);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 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
|
+
*/
|
|
57
|
+
const extractFromProjectionNodes = (
|
|
58
|
+
table: TableDef,
|
|
59
|
+
projectionNodes: ProjectionNode[],
|
|
60
|
+
context: SchemaExtractionContext,
|
|
61
|
+
options: SchemaOptions
|
|
62
|
+
): OpenApiSchema => {
|
|
63
|
+
const properties: Record<string, JsonSchemaProperty> = {};
|
|
64
|
+
const required: string[] = [];
|
|
65
|
+
|
|
66
|
+
for (const node of projectionNodes) {
|
|
67
|
+
if (!node || typeof node !== 'object') continue;
|
|
68
|
+
|
|
69
|
+
const projection = node as { type: string; alias?: string; fn?: string; value?: unknown };
|
|
70
|
+
const propertyName = projection.alias ?? '';
|
|
71
|
+
|
|
72
|
+
if (!propertyName) continue;
|
|
73
|
+
|
|
74
|
+
if (projection.type === 'Column') {
|
|
75
|
+
const columnNode = node as { table: string; name: string };
|
|
76
|
+
const column = table.columns[columnNode.name];
|
|
77
|
+
if (!column) continue;
|
|
78
|
+
|
|
79
|
+
const property = mapColumnType(column);
|
|
80
|
+
if (!property.description && options.includeDescriptions && column.comment) {
|
|
81
|
+
property.description = column.comment;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
properties[propertyName] = property;
|
|
85
|
+
|
|
86
|
+
if (column.notNull || column.primary) {
|
|
87
|
+
required.push(propertyName);
|
|
88
|
+
}
|
|
89
|
+
} else if (projection.type === 'Function' || projection.type === 'WindowFunction') {
|
|
90
|
+
const fnNode = node as { fn?: string; name?: string };
|
|
91
|
+
const functionName = fnNode.fn?.toUpperCase() ?? fnNode.name?.toUpperCase() ?? '';
|
|
92
|
+
const propertySchema = projection.type === 'Function'
|
|
93
|
+
? mapFunctionNodeToSchema(functionName)
|
|
94
|
+
: mapWindowFunctionToSchema(functionName);
|
|
95
|
+
|
|
96
|
+
properties[propertyName] = propertySchema;
|
|
97
|
+
|
|
98
|
+
const isCountFunction = functionName === 'COUNT';
|
|
99
|
+
const isWindowRankFunction = functionName === 'ROW_NUMBER' || functionName === 'RANK';
|
|
100
|
+
|
|
101
|
+
if (isCountFunction || isWindowRankFunction) {
|
|
102
|
+
required.push(propertyName);
|
|
103
|
+
}
|
|
104
|
+
} else if (projection.type === 'CaseExpression') {
|
|
105
|
+
const propertySchema: JsonSchemaProperty = {
|
|
106
|
+
type: 'string' as JsonSchemaType,
|
|
107
|
+
description: 'Computed CASE expression',
|
|
108
|
+
nullable: true,
|
|
109
|
+
};
|
|
110
|
+
properties[propertyName] = propertySchema;
|
|
111
|
+
} else if (projection.type === 'ScalarSubquery') {
|
|
112
|
+
const propertySchema: JsonSchemaProperty = {
|
|
113
|
+
type: 'object' as JsonSchemaType,
|
|
114
|
+
description: 'Subquery result',
|
|
115
|
+
nullable: true,
|
|
116
|
+
};
|
|
117
|
+
properties[propertyName] = propertySchema;
|
|
118
|
+
} else if (projection.type === 'CastExpression') {
|
|
119
|
+
const propertySchema: JsonSchemaProperty = {
|
|
120
|
+
type: 'string' as JsonSchemaType,
|
|
121
|
+
description: 'CAST expression result',
|
|
122
|
+
nullable: true,
|
|
123
|
+
};
|
|
124
|
+
properties[propertyName] = propertySchema;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties,
|
|
131
|
+
required,
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Maps SQL aggregate functions to OpenAPI types
|
|
137
|
+
* @param functionName - SQL function name
|
|
138
|
+
* @returns OpenAPI JSON Schema property
|
|
139
|
+
*/
|
|
140
|
+
const mapFunctionNodeToSchema = (functionName: string): JsonSchemaProperty => {
|
|
141
|
+
const upperName = functionName.toUpperCase();
|
|
142
|
+
|
|
143
|
+
switch (upperName) {
|
|
144
|
+
case 'COUNT':
|
|
145
|
+
case 'SUM':
|
|
146
|
+
case 'AVG':
|
|
147
|
+
case 'MIN':
|
|
148
|
+
case 'MAX':
|
|
149
|
+
return {
|
|
150
|
+
type: 'number' as JsonSchemaType,
|
|
151
|
+
description: `${upperName} aggregate function result`,
|
|
152
|
+
nullable: false,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
case 'GROUP_CONCAT':
|
|
156
|
+
case 'STRING_AGG':
|
|
157
|
+
case 'ARRAY_AGG':
|
|
158
|
+
return {
|
|
159
|
+
type: 'string' as JsonSchemaType,
|
|
160
|
+
description: `${upperName} aggregate function result`,
|
|
161
|
+
nullable: true,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
case 'JSON_ARRAYAGG':
|
|
165
|
+
case 'JSON_OBJECTAGG':
|
|
166
|
+
return {
|
|
167
|
+
type: 'object' as JsonSchemaType,
|
|
168
|
+
description: `${upperName} aggregate function result`,
|
|
169
|
+
nullable: true,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
default:
|
|
173
|
+
return {
|
|
174
|
+
type: 'string' as JsonSchemaType,
|
|
175
|
+
description: `Unknown function: ${functionName}`,
|
|
176
|
+
nullable: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Maps SQL window functions to OpenAPI types
|
|
183
|
+
* @param functionName - SQL function name
|
|
184
|
+
* @returns OpenAPI JSON Schema property
|
|
185
|
+
*/
|
|
186
|
+
const mapWindowFunctionToSchema = (functionName: string): JsonSchemaProperty => {
|
|
187
|
+
const upperName = functionName.toUpperCase();
|
|
188
|
+
|
|
189
|
+
switch (upperName) {
|
|
190
|
+
case 'ROW_NUMBER':
|
|
191
|
+
case 'RANK':
|
|
192
|
+
case 'DENSE_RANK':
|
|
193
|
+
case 'NTILE':
|
|
194
|
+
return {
|
|
195
|
+
type: 'integer' as JsonSchemaType,
|
|
196
|
+
description: `${upperName} window function result`,
|
|
197
|
+
nullable: false,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
case 'LAG':
|
|
201
|
+
case 'LEAD':
|
|
202
|
+
case 'FIRST_VALUE':
|
|
203
|
+
case 'LAST_VALUE':
|
|
204
|
+
return {
|
|
205
|
+
type: 'string' as JsonSchemaType,
|
|
206
|
+
description: `${upperName} window function result`,
|
|
207
|
+
nullable: true,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
default:
|
|
211
|
+
return {
|
|
212
|
+
type: 'string' as JsonSchemaType,
|
|
213
|
+
description: `Unknown window function: ${functionName}`,
|
|
214
|
+
nullable: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 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
|
+
*/
|
|
227
|
+
const extractSelectedSchema = (
|
|
228
|
+
table: TableDef,
|
|
229
|
+
plan: HydrationPlan,
|
|
230
|
+
context: SchemaExtractionContext,
|
|
231
|
+
options: SchemaOptions
|
|
232
|
+
): OpenApiSchema => {
|
|
233
|
+
const properties: Record<string, JsonSchemaProperty> = {};
|
|
234
|
+
const required: string[] = [];
|
|
235
|
+
|
|
236
|
+
plan.rootColumns.forEach(columnName => {
|
|
237
|
+
const column = table.columns[columnName];
|
|
238
|
+
if (!column) return;
|
|
239
|
+
|
|
240
|
+
const property = mapColumnType(column);
|
|
241
|
+
if (!property.description && options.includeDescriptions && column.comment) {
|
|
242
|
+
property.description = column.comment;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
properties[columnName] = property;
|
|
246
|
+
|
|
247
|
+
if (column.notNull || column.primary) {
|
|
248
|
+
required.push(columnName);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
plan.relations.forEach(relationPlan => {
|
|
253
|
+
const relation = table.relations[relationPlan.name];
|
|
254
|
+
if (!relation) return;
|
|
255
|
+
|
|
256
|
+
const relationSchema = extractRelationSchema(
|
|
257
|
+
relation,
|
|
258
|
+
relationPlan,
|
|
259
|
+
relationPlan.columns,
|
|
260
|
+
context,
|
|
261
|
+
options
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
properties[relationPlan.name] = relationSchema;
|
|
265
|
+
|
|
266
|
+
const { isNullable } = mapRelationType(relation.type);
|
|
267
|
+
if (!isNullable && relationPlan.name) {
|
|
268
|
+
required.push(relationPlan.name);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
type: 'object',
|
|
274
|
+
properties,
|
|
275
|
+
required,
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 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
|
+
*/
|
|
286
|
+
const extractFullTableSchema = (
|
|
287
|
+
table: TableDef,
|
|
288
|
+
context: SchemaExtractionContext,
|
|
289
|
+
options: SchemaOptions
|
|
290
|
+
): OpenApiSchema => {
|
|
291
|
+
const cacheKey = table.name;
|
|
292
|
+
|
|
293
|
+
if (context.schemaCache.has(cacheKey)) {
|
|
294
|
+
return context.schemaCache.get(cacheKey)!;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
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
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
context.visitedTables.add(cacheKey);
|
|
311
|
+
|
|
312
|
+
const properties: Record<string, JsonSchemaProperty> = {};
|
|
313
|
+
const required: string[] = [];
|
|
314
|
+
|
|
315
|
+
Object.entries(table.columns).forEach(([columnName, column]) => {
|
|
316
|
+
const property = mapColumnType(column);
|
|
317
|
+
if (!property.description && options.includeDescriptions && column.comment) {
|
|
318
|
+
property.description = column.comment;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
properties[columnName] = property;
|
|
322
|
+
|
|
323
|
+
if (column.notNull || column.primary) {
|
|
324
|
+
required.push(columnName);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
Object.entries(table.relations).forEach(([relationName, relation]) => {
|
|
329
|
+
if (context.depth >= context.maxDepth) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const relationSchema = extractRelationSchema(
|
|
334
|
+
relation,
|
|
335
|
+
undefined,
|
|
336
|
+
[],
|
|
337
|
+
{ ...context, depth: context.depth + 1 },
|
|
338
|
+
options
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
properties[relationName] = relationSchema;
|
|
342
|
+
|
|
343
|
+
const { isNullable } = mapRelationType(relation.type);
|
|
344
|
+
if (!isNullable) {
|
|
345
|
+
required.push(relationName);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const schema: OpenApiSchema = {
|
|
350
|
+
type: 'object',
|
|
351
|
+
properties,
|
|
352
|
+
required,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
context.schemaCache.set(cacheKey, schema);
|
|
356
|
+
return schema;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 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
|
+
*/
|
|
368
|
+
const extractRelationSchema = (
|
|
369
|
+
relation: RelationDef,
|
|
370
|
+
relationPlan: HydrationRelationPlan | undefined,
|
|
371
|
+
selectedColumns: string[],
|
|
372
|
+
context: SchemaExtractionContext,
|
|
373
|
+
options: SchemaOptions
|
|
374
|
+
): JsonSchemaProperty => {
|
|
375
|
+
const targetTable = relation.target;
|
|
376
|
+
const { type: relationType, isNullable } = mapRelationType(relation.type);
|
|
377
|
+
|
|
378
|
+
let targetSchema: OpenApiSchema;
|
|
379
|
+
|
|
380
|
+
if (relationPlan && selectedColumns.length > 0) {
|
|
381
|
+
const plan: HydrationPlan = {
|
|
382
|
+
rootTable: targetTable.name,
|
|
383
|
+
rootPrimaryKey: relationPlan.targetPrimaryKey,
|
|
384
|
+
rootColumns: selectedColumns,
|
|
385
|
+
relations: [],
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
targetSchema = extractSelectedSchema(targetTable, plan, context, options);
|
|
389
|
+
} else {
|
|
390
|
+
targetSchema = extractFullTableSchema(targetTable, context, options);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (relationType === 'array') {
|
|
394
|
+
return {
|
|
395
|
+
type: 'array',
|
|
396
|
+
items: targetSchema as JsonSchemaProperty,
|
|
397
|
+
nullable: isNullable,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
type: 'object' as JsonSchemaType,
|
|
403
|
+
properties: targetSchema.properties,
|
|
404
|
+
required: targetSchema.required,
|
|
405
|
+
nullable: isNullable,
|
|
406
|
+
description: targetSchema.description,
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 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
|
+
*/
|
|
416
|
+
export const schemaToJson = (schema: OpenApiSchema, pretty = false): string => {
|
|
417
|
+
return JSON.stringify(schema, null, pretty ? 2 : 0);
|
|
418
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.1 JSON Schema type representation
|
|
3
|
+
*/
|
|
4
|
+
export type JsonSchemaType =
|
|
5
|
+
| 'string'
|
|
6
|
+
| 'number'
|
|
7
|
+
| 'integer'
|
|
8
|
+
| 'boolean'
|
|
9
|
+
| 'object'
|
|
10
|
+
| 'array'
|
|
11
|
+
| 'null';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Common OpenAPI 3.1 JSON Schema formats
|
|
15
|
+
*/
|
|
16
|
+
export type JsonSchemaFormat =
|
|
17
|
+
| 'date-time'
|
|
18
|
+
| 'date'
|
|
19
|
+
| 'time'
|
|
20
|
+
| 'email'
|
|
21
|
+
| 'uuid'
|
|
22
|
+
| 'uri'
|
|
23
|
+
| 'binary'
|
|
24
|
+
| 'base64';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* OpenAPI 3.1 JSON Schema property definition
|
|
28
|
+
*/
|
|
29
|
+
export interface JsonSchemaProperty {
|
|
30
|
+
type?: JsonSchemaType | JsonSchemaType[];
|
|
31
|
+
format?: JsonSchemaFormat;
|
|
32
|
+
description?: string;
|
|
33
|
+
nullable?: boolean;
|
|
34
|
+
minimum?: number;
|
|
35
|
+
maximum?: number;
|
|
36
|
+
minLength?: number;
|
|
37
|
+
maxLength?: number;
|
|
38
|
+
pattern?: string;
|
|
39
|
+
enum?: (string | number | boolean)[];
|
|
40
|
+
default?: unknown;
|
|
41
|
+
example?: unknown;
|
|
42
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
43
|
+
required?: string[];
|
|
44
|
+
items?: JsonSchemaProperty;
|
|
45
|
+
$ref?: string;
|
|
46
|
+
anyOf?: JsonSchemaProperty[];
|
|
47
|
+
allOf?: JsonSchemaProperty[];
|
|
48
|
+
oneOf?: JsonSchemaProperty[];
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Complete OpenAPI 3.1 Schema for an entity or query result
|
|
54
|
+
*/
|
|
55
|
+
export interface OpenApiSchema {
|
|
56
|
+
type: 'object';
|
|
57
|
+
properties: Record<string, JsonSchemaProperty>;
|
|
58
|
+
required: string[];
|
|
59
|
+
description?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Schema generation options
|
|
64
|
+
*/
|
|
65
|
+
export interface SchemaOptions {
|
|
66
|
+
/** Use selected columns only (from select/include) vs full entity */
|
|
67
|
+
mode?: 'selected' | 'full';
|
|
68
|
+
/** Include description from column comments */
|
|
69
|
+
includeDescriptions?: boolean;
|
|
70
|
+
/** Include enum values for enum columns */
|
|
71
|
+
includeEnums?: boolean;
|
|
72
|
+
/** Include column examples if available */
|
|
73
|
+
includeExamples?: boolean;
|
|
74
|
+
/** Format output for pretty printing (debugging) */
|
|
75
|
+
pretty?: boolean;
|
|
76
|
+
/** Maximum depth for relation recursion */
|
|
77
|
+
maxDepth?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Schema extraction context for handling circular references
|
|
82
|
+
*/
|
|
83
|
+
export interface SchemaExtractionContext {
|
|
84
|
+
/** Set of already visited tables to detect cycles */
|
|
85
|
+
visitedTables: Set<string>;
|
|
86
|
+
/** Map of table names to their generated schemas */
|
|
87
|
+
schemaCache: Map<string, OpenApiSchema>;
|
|
88
|
+
/** Current extraction depth */
|
|
89
|
+
depth: number;
|
|
90
|
+
/** Maximum depth to recurse */
|
|
91
|
+
maxDepth: number;
|
|
92
|
+
}
|