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.
@@ -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
- import type { OpenApiSchema, SchemaExtractionContext, SchemaOptions, JsonSchemaProperty, JsonSchemaType } from './schema-types.js';
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 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
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
- ): OpenApiSchema => {
23
- const mode = options.mode ?? 'full';
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
- const context: SchemaExtractionContext = {
26
- visitedTables: new Set(),
27
- schemaCache: new Map(),
28
- depth: 0,
29
- maxDepth: options.maxDepth ?? 5,
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: SchemaOptions
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
- description: 'Computed CASE expression',
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
- description: 'Subquery result',
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
- description: 'CAST expression result',
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 = (functionName: string): JsonSchemaProperty => {
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
- description: `${upperName} aggregate function result`,
152
- nullable: false,
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
- description: `${upperName} aggregate function result`,
161
- nullable: true,
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
- description: `${upperName} aggregate function result`,
169
- nullable: true,
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
- description: `Unknown function: ${functionName}`,
176
- nullable: true,
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 = (functionName: string): JsonSchemaProperty => {
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
- description: `${upperName} window function result`,
197
- nullable: false,
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
- description: `${upperName} window function result`,
207
- nullable: true,
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
- description: `Unknown window function: ${functionName}`,
214
- nullable: true,
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: SchemaOptions
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
- const property = mapColumnType(column);
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: SchemaOptions
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
- const property = mapColumnType(column);
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: SchemaOptions
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);