metal-orm 1.0.80 → 1.0.82

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.
@@ -31,6 +31,8 @@ export interface JsonSchemaProperty {
31
31
  format?: JsonSchemaFormat;
32
32
  description?: string;
33
33
  nullable?: boolean;
34
+ readOnly?: boolean;
35
+ writeOnly?: boolean;
34
36
  minimum?: number;
35
37
  maximum?: number;
36
38
  minLength?: number;
@@ -49,6 +51,22 @@ export interface JsonSchemaProperty {
49
51
  [key: string]: unknown;
50
52
  }
51
53
 
54
+ /**
55
+ * OpenAPI 3.1 parameter definition
56
+ */
57
+ export interface OpenApiParameter {
58
+ name: string;
59
+ in: 'query' | 'path' | 'header' | 'cookie';
60
+ description?: string;
61
+ required?: boolean;
62
+ deprecated?: boolean;
63
+ allowEmptyValue?: boolean;
64
+ style?: string;
65
+ explode?: boolean;
66
+ schema?: JsonSchemaProperty;
67
+ [key: string]: unknown;
68
+ }
69
+
52
70
  /**
53
71
  * Complete OpenAPI 3.1 Schema for an entity or query result
54
72
  */
@@ -60,23 +78,71 @@ export interface OpenApiSchema {
60
78
  }
61
79
 
62
80
  /**
63
- * Schema generation options
81
+ * Column-level schema flags
64
82
  */
65
- export interface SchemaOptions {
66
- /** Use selected columns only (from select/include) vs full entity */
67
- mode?: 'selected' | 'full';
83
+ export interface ColumnSchemaOptions {
68
84
  /** Include description from column comments */
69
85
  includeDescriptions?: boolean;
70
86
  /** Include enum values for enum columns */
71
87
  includeEnums?: boolean;
72
88
  /** Include column examples if available */
73
89
  includeExamples?: boolean;
74
- /** Format output for pretty printing (debugging) */
75
- pretty?: boolean;
90
+ /** Include column defaults */
91
+ includeDefaults?: boolean;
92
+ /** Include nullable flag when applicable */
93
+ includeNullable?: boolean;
94
+ }
95
+
96
+ /**
97
+ * Output schema generation options (query result)
98
+ */
99
+ export interface OutputSchemaOptions extends ColumnSchemaOptions {
100
+ /** Use selected columns only (from select/include) vs full entity */
101
+ mode?: 'selected' | 'full';
76
102
  /** Maximum depth for relation recursion */
77
103
  maxDepth?: number;
78
104
  }
79
105
 
106
+ export type InputRelationMode = 'ids' | 'objects' | 'mixed';
107
+ export type InputSchemaMode = 'create' | 'update';
108
+
109
+ /**
110
+ * Input schema generation options (write payloads)
111
+ */
112
+ export interface InputSchemaOptions extends ColumnSchemaOptions {
113
+ /** Create vs update payload shape */
114
+ mode?: InputSchemaMode;
115
+ /** Include relation payloads */
116
+ includeRelations?: boolean;
117
+ /** How relations are represented (ids, nested objects, or both) */
118
+ relationMode?: InputRelationMode;
119
+ /** Maximum depth for relation recursion */
120
+ maxDepth?: number;
121
+ /** Omit read-only/generated columns from input */
122
+ omitReadOnly?: boolean;
123
+ /** Exclude primary key columns from input */
124
+ excludePrimaryKey?: boolean;
125
+ /** Require primary key columns on update payloads */
126
+ requirePrimaryKey?: boolean;
127
+ }
128
+
129
+ /**
130
+ * Schema generation options
131
+ */
132
+ export interface SchemaOptions extends OutputSchemaOptions {
133
+ /** Input schema options, or false to skip input generation */
134
+ input?: InputSchemaOptions | false;
135
+ }
136
+
137
+ /**
138
+ * Input + output schema bundle
139
+ */
140
+ export interface OpenApiSchemaBundle {
141
+ output: OpenApiSchema;
142
+ input?: OpenApiSchema;
143
+ parameters?: OpenApiParameter[];
144
+ }
145
+
80
146
  /**
81
147
  * Schema extraction context for handling circular references
82
148
  */
@@ -1,34 +1,46 @@
1
1
  import type { ColumnDef } from '../schema/column-types.js';
2
- import type { JsonSchemaProperty, JsonSchemaType, JsonSchemaFormat } from './schema-types.js';
2
+ import type { ColumnSchemaOptions, JsonSchemaProperty, JsonSchemaType, JsonSchemaFormat } from './schema-types.js';
3
3
 
4
4
  /**
5
5
  * Maps SQL column types to OpenAPI JSON Schema types
6
6
  */
7
- export const mapColumnType = (column: ColumnDef): JsonSchemaProperty => {
7
+ export const mapColumnType = (
8
+ column: ColumnDef,
9
+ options: ColumnSchemaOptions = {}
10
+ ): JsonSchemaProperty => {
11
+ const resolved = resolveColumnOptions(options);
8
12
  const sqlType = normalizeType(column.type);
9
13
  const baseSchema = mapSqlTypeToBaseSchema(sqlType, column);
10
14
 
11
15
  const schema: JsonSchemaProperty = {
12
16
  ...baseSchema,
13
- description: column.comment,
14
- nullable: !column.notNull && !column.primary,
15
17
  };
16
18
 
17
- if (column.args && sqlType === 'varchar' || sqlType === 'char') {
19
+ if (resolved.includeDescriptions && column.comment) {
20
+ schema.description = column.comment;
21
+ }
22
+
23
+ if (resolved.includeNullable) {
24
+ schema.nullable = !column.notNull && !column.primary;
25
+ }
26
+
27
+ if ((sqlType === 'varchar' || sqlType === 'char') && column.args) {
18
28
  schema.maxLength = column.args[0] as number | undefined;
19
29
  }
20
30
 
21
- if (column.args && sqlType === 'decimal' || sqlType === 'float') {
31
+ if ((sqlType === 'decimal' || sqlType === 'float') && column.args) {
22
32
  if (column.args.length >= 1) {
23
33
  schema.minimum = -(10 ** (column.args[0] as number));
24
34
  }
25
35
  }
26
36
 
27
- if (sqlType === 'enum' && column.args && column.args.length > 0) {
37
+ if (!resolved.includeEnums) {
38
+ delete schema.enum;
39
+ } else if (sqlType === 'enum' && column.args && column.args.length > 0) {
28
40
  schema.enum = column.args as (string | number | boolean)[];
29
41
  }
30
42
 
31
- if (column.default !== undefined) {
43
+ if (resolved.includeDefaults && column.default !== undefined) {
32
44
  schema.default = column.default;
33
45
  }
34
46
 
@@ -170,6 +182,14 @@ const inferTypeFromTsType = (tsType: unknown): JsonSchemaType => {
170
182
  return 'string' as JsonSchemaType;
171
183
  };
172
184
 
185
+ const resolveColumnOptions = (options: ColumnSchemaOptions): Required<ColumnSchemaOptions> => ({
186
+ includeDescriptions: options.includeDescriptions ?? false,
187
+ includeEnums: options.includeEnums ?? true,
188
+ includeExamples: options.includeExamples ?? false,
189
+ includeDefaults: options.includeDefaults ?? true,
190
+ includeNullable: options.includeNullable ?? true
191
+ });
192
+
173
193
  /**
174
194
  * Maps relation type to array or single object
175
195
  */
@@ -198,12 +198,13 @@ export class UnitOfWork {
198
198
 
199
199
  const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
200
200
  let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
201
- if (this.dialect.supportsReturning()) {
202
- builder = builder.returning(...this.getReturningColumns(tracked.table));
203
- }
204
- const compiled = builder.compile(this.dialect);
205
- const results = await this.executeCompiled(compiled);
206
- this.applyReturningResults(tracked, results);
201
+ if (this.dialect.supportsReturning()) {
202
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
203
+ }
204
+ const compiled = builder.compile(this.dialect);
205
+ const results = await this.executeCompiled(compiled);
206
+ this.applyReturningResults(tracked, results);
207
+ this.applyInsertId(tracked, results);
207
208
 
208
209
  tracked.status = EntityStatus.Managed;
209
210
  tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
@@ -344,18 +345,29 @@ export class UnitOfWork {
344
345
  * @param tracked - The tracked entity
345
346
  * @param results - Query results
346
347
  */
347
- private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
348
- if (!this.dialect.supportsReturning()) return;
349
- const first = results[0];
350
- if (!first || first.values.length === 0) return;
348
+ private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
349
+ if (!this.dialect.supportsReturning()) return;
350
+ const first = results[0];
351
+ if (!first || first.values.length === 0) return;
351
352
 
352
353
  const row = first.values[0];
353
354
  for (let i = 0; i < first.columns.length; i++) {
354
355
  const columnName = this.normalizeColumnName(first.columns[i]);
355
356
  if (!(columnName in tracked.table.columns)) continue;
356
- (tracked.entity as Record<string, unknown>)[columnName] = row[i];
357
- }
358
- }
357
+ (tracked.entity as Record<string, unknown>)[columnName] = row[i];
358
+ }
359
+ }
360
+
361
+ private applyInsertId(tracked: TrackedEntity, results: QueryResult[]): void {
362
+ if (this.dialect.supportsReturning()) return;
363
+ if (tracked.pk != null) return;
364
+ const pkName = findPrimaryKey(tracked.table);
365
+ const pkColumn = tracked.table.columns[pkName];
366
+ if (!pkColumn?.autoIncrement) return;
367
+ const insertId = results.find(result => typeof result.insertId === 'number')?.insertId;
368
+ if (insertId == null) return;
369
+ (tracked.entity as Record<string, unknown>)[pkName] = insertId;
370
+ }
359
371
 
360
372
  /**
361
373
  * Normalizes a column name by removing quotes and table prefixes.
@@ -144,6 +144,7 @@ const collectFromOperand = (node: OperandNode, collector: FilterTableCollector):
144
144
  break;
145
145
  case 'Literal':
146
146
  case 'AliasRef':
147
+ case 'Param':
147
148
  break;
148
149
  default:
149
150
  break;
@@ -67,7 +67,7 @@ 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
69
  import { SelectRelationFacet } from './select/relation-facet.js';
70
- import { extractSchema, SchemaOptions, OpenApiSchema } from '../openapi/index.js';
70
+ import { buildFilterParameters, extractSchema, SchemaOptions, OpenApiSchemaBundle } from '../openapi/index.js';
71
71
 
72
72
  type ColumnSelectionValue =
73
73
  | ColumnDef
@@ -1121,16 +1121,26 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
1121
1121
  }
1122
1122
 
1123
1123
  /**
1124
- * Gets OpenAPI 3.1 JSON Schema for query result
1124
+ * Gets OpenAPI 3.1 JSON Schemas for query output and optional input payloads
1125
1125
  * @param options - Schema generation options
1126
- * @returns OpenAPI 3.1 JSON Schema for query result
1126
+ * @returns OpenAPI 3.1 JSON Schemas for query output and input payloads
1127
1127
  * @example
1128
- * const schema = qb.select('id', 'title', 'author').getSchema();
1129
- * console.log(JSON.stringify(schema, null, 2));
1128
+ * const { output } = qb.select('id', 'title', 'author').getSchema();
1129
+ * console.log(JSON.stringify(output, null, 2));
1130
1130
  */
1131
- getSchema(options?: SchemaOptions): OpenApiSchema {
1131
+ getSchema(options?: SchemaOptions): OpenApiSchemaBundle {
1132
1132
  const plan = this.context.hydration.getPlan();
1133
- return extractSchema(this.env.table, plan, this.context.state.ast.columns, options);
1133
+ const bundle = extractSchema(this.env.table, plan, this.context.state.ast.columns, options);
1134
+ const parameters = buildFilterParameters(
1135
+ this.env.table,
1136
+ this.context.state.ast.where,
1137
+ this.context.state.ast.from,
1138
+ options ?? {}
1139
+ );
1140
+ if (parameters.length) {
1141
+ return { ...bundle, parameters };
1142
+ }
1143
+ return bundle;
1134
1144
  }
1135
1145
 
1136
1146
  /**