metal-orm 1.0.64 → 1.0.66

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.
@@ -1,4 +1,5 @@
1
1
  import { ColumnNode, eq } from '../core/ast/expression.js';
2
+ import type { ValueOperandInput } from '../core/ast/expression.js';
2
3
  import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
3
4
  import { InsertQueryBuilder } from '../query-builder/insert.js';
4
5
  import { UpdateQueryBuilder } from '../query-builder/update.js';
@@ -9,12 +10,13 @@ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
9
10
  import { IdentityMap } from './identity-map.js';
10
11
  import { EntityStatus } from './runtime-types.js';
11
12
  import type { TrackedEntity } from './runtime-types.js';
13
+ import type { PrimaryKey } from './entity-context.js';
12
14
 
13
15
  /**
14
16
  * Unit of Work pattern implementation for tracking entity changes.
15
17
  */
16
18
  export class UnitOfWork {
17
- private readonly trackedEntities = new Map<unknown, TrackedEntity>();
19
+ private readonly trackedEntities = new Map<object, TrackedEntity>();
18
20
 
19
21
  /**
20
22
  * Creates a new UnitOfWork instance.
@@ -51,7 +53,7 @@ export class UnitOfWork {
51
53
  * @param pk - The primary key value
52
54
  * @returns The entity or undefined if not found
53
55
  */
54
- getEntity(table: TableDef, pk: string | number): unknown | undefined {
56
+ getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
55
57
  return this.identityMap.getEntity(table, pk);
56
58
  }
57
59
 
@@ -69,7 +71,7 @@ export class UnitOfWork {
69
71
  * @param entity - The entity to find
70
72
  * @returns The tracked entity or undefined if not found
71
73
  */
72
- findTracked(entity: unknown): TrackedEntity | undefined {
74
+ findTracked(entity: object): TrackedEntity | undefined {
73
75
  return this.trackedEntities.get(entity);
74
76
  }
75
77
 
@@ -79,7 +81,7 @@ export class UnitOfWork {
79
81
  * @param pk - The primary key value
80
82
  * @param entity - The entity instance
81
83
  */
82
- setEntity(table: TableDef, pk: string | number, entity: unknown): void {
84
+ setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
83
85
  if (pk === null || pk === undefined) return;
84
86
  let tracked = this.trackedEntities.get(entity);
85
87
  if (!tracked) {
@@ -104,7 +106,7 @@ export class UnitOfWork {
104
106
  * @param entity - The entity instance
105
107
  * @param pk - Optional primary key value
106
108
  */
107
- trackNew(table: TableDef, entity: unknown, pk?: string | number): void {
109
+ trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
108
110
  const tracked: TrackedEntity = {
109
111
  table,
110
112
  entity,
@@ -124,7 +126,7 @@ export class UnitOfWork {
124
126
  * @param pk - The primary key value
125
127
  * @param entity - The entity instance
126
128
  */
127
- trackManaged(table: TableDef, pk: string | number, entity: unknown): void {
129
+ trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
128
130
  const tracked: TrackedEntity = {
129
131
  table,
130
132
  entity,
@@ -140,7 +142,7 @@ export class UnitOfWork {
140
142
  * Marks an entity as dirty (modified).
141
143
  * @param entity - The entity to mark as dirty
142
144
  */
143
- markDirty(entity: unknown): void {
145
+ markDirty(entity: object): void {
144
146
  const tracked = this.trackedEntities.get(entity);
145
147
  if (!tracked) return;
146
148
  if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
@@ -151,7 +153,7 @@ export class UnitOfWork {
151
153
  * Marks an entity as removed.
152
154
  * @param entity - The entity to mark as removed
153
155
  */
154
- markRemoved(entity: unknown): void {
156
+ markRemoved(entity: object): void {
155
157
  const tracked = this.trackedEntities.get(entity);
156
158
  if (!tracked) return;
157
159
  tracked.status = EntityStatus.Removed;
@@ -195,7 +197,7 @@ export class UnitOfWork {
195
197
  await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
196
198
 
197
199
  const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
198
- let builder = new InsertQueryBuilder(tracked.table).values(payload);
200
+ let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
199
201
  if (this.dialect.supportsReturning()) {
200
202
  builder = builder.returning(...this.getReturningColumns(tracked.table));
201
203
  }
@@ -291,7 +293,7 @@ export class UnitOfWork {
291
293
  const snapshot = tracked.original ?? {};
292
294
  const changes: Record<string, unknown> = {};
293
295
  for (const column of Object.keys(tracked.table.columns)) {
294
- const current = tracked.entity[column];
296
+ const current = (tracked.entity as Record<string, unknown>)[column];
295
297
  if (snapshot[column] !== current) {
296
298
  changes[column] = current;
297
299
  }
@@ -351,7 +353,7 @@ export class UnitOfWork {
351
353
  for (let i = 0; i < first.columns.length; i++) {
352
354
  const columnName = this.normalizeColumnName(first.columns[i]);
353
355
  if (!(columnName in tracked.table.columns)) continue;
354
- tracked.entity[columnName] = row[i];
356
+ (tracked.entity as Record<string, unknown>)[columnName] = row[i];
355
357
  }
356
358
  }
357
359
 
@@ -394,10 +396,11 @@ export class UnitOfWork {
394
396
  * @param tracked - The tracked entity
395
397
  * @returns Primary key value or null
396
398
  */
397
- private getPrimaryKeyValue(tracked: TrackedEntity): string | number | null {
399
+ private getPrimaryKeyValue(tracked: TrackedEntity): PrimaryKey | null {
398
400
  const key = findPrimaryKey(tracked.table);
399
- const val = tracked.entity[key];
401
+ const val = (tracked.entity as Record<string, unknown>)[key];
400
402
  if (val === undefined || val === null) return null;
401
- return val;
403
+ if (typeof val !== 'string' && typeof val !== 'number') return null;
404
+ return val as PrimaryKey;
402
405
  }
403
406
  }
@@ -1,161 +1,162 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { InsertQueryNode, SelectQueryNode } from '../core/ast/query.js';
1
+ import { TableDef } from '../schema/table.js';
2
+ import { InsertQueryNode, SelectQueryNode } from '../core/ast/query.js';
3
3
  import {
4
4
  ColumnNode,
5
5
  OperandNode,
6
6
  isValueOperandInput,
7
7
  valueToOperand
8
8
  } from '../core/ast/expression.js';
9
- import {
10
- buildColumnNodes,
11
- createTableNode
12
- } from '../core/ast/builders.js';
13
-
14
- type InsertRows = Record<string, unknown>[];
15
-
16
- /**
17
- * Maintains immutable state for building INSERT queries
18
- */
19
- export class InsertQueryState {
20
- public readonly table: TableDef;
21
- public readonly ast: InsertQueryNode;
22
-
23
- /**
24
- * Creates a new InsertQueryState instance
25
- * @param table - The table definition for the INSERT query
26
- * @param ast - Optional initial AST node, defaults to a basic INSERT query
27
- */
28
- constructor(table: TableDef, ast?: InsertQueryNode) {
29
- this.table = table;
30
- this.ast = ast ?? {
31
- type: 'InsertQuery',
32
- into: createTableNode(table),
33
- columns: [],
34
- source: {
35
- type: 'InsertValues',
36
- rows: []
37
- }
38
- };
39
- }
40
-
41
- private clone(nextAst: InsertQueryNode): InsertQueryState {
42
- return new InsertQueryState(this.table, nextAst);
43
- }
44
-
45
- private ensureColumnsFromRow(rows: InsertRows): ColumnNode[] {
46
- if (this.ast.columns.length) return this.ast.columns;
47
- return buildColumnNodes(this.table, Object.keys(rows[0]));
48
- }
49
-
50
- private appendValues(rows: OperandNode[][]): OperandNode[][] {
51
- if (this.ast.source.type === 'InsertValues') {
52
- return [...this.ast.source.rows, ...rows];
53
- }
54
- return rows;
55
- }
56
-
57
- private getTableColumns(): ColumnNode[] {
58
- const names = Object.keys(this.table.columns);
59
- if (!names.length) return [];
60
- return buildColumnNodes(this.table, names);
61
- }
62
-
63
- /**
64
- * Adds VALUES clause to the INSERT query
65
- * @param rows - Array of row objects to insert
66
- * @returns A new InsertQueryState with the VALUES clause added
67
- * @throws Error if mixing VALUES with SELECT source
68
- * @throws Error if invalid values are provided
69
- */
70
- withValues(rows: Record<string, unknown>[]): InsertQueryState {
71
- if (!rows.length) return this;
72
-
73
- if (this.ast.source.type === 'InsertSelect') {
74
- throw new Error('Cannot mix INSERT ... VALUES with INSERT ... SELECT source.');
75
- }
76
-
77
- const definedColumns = this.ensureColumnsFromRow(rows);
78
-
79
- const newRows: OperandNode[][] = rows.map((row, rowIndex) =>
80
- definedColumns.map(column => {
81
- const rawValue = row[column.name];
82
-
83
- if (!isValueOperandInput(rawValue)) {
84
- throw new Error(
85
- `Invalid insert value for column "${column.name}" in row ${rowIndex}: only primitives, null, or OperandNodes are allowed`
86
- );
87
- }
88
-
89
- return valueToOperand(rawValue);
90
- })
91
- );
92
-
93
- return this.clone({
94
- ...this.ast,
95
- columns: definedColumns,
96
- source: {
97
- type: 'InsertValues',
98
- rows: this.appendValues(newRows)
99
- }
100
- });
101
- }
102
-
103
- /**
104
- * Sets the columns for the INSERT query
105
- * @param columns - Column nodes to insert into
106
- * @returns A new InsertQueryState with the specified columns
107
- */
108
- withColumns(columns: ColumnNode[]): InsertQueryState {
109
- if (!columns.length) return this;
110
- return this.clone({
111
- ...this.ast,
112
- columns: [...columns]
113
- });
114
- }
115
-
116
- /**
117
- * Adds SELECT source to the INSERT query
118
- * @param query - The SELECT query to use as source
119
- * @param columns - Target columns for the INSERT
120
- * @returns A new InsertQueryState with the SELECT source
121
- * @throws Error if mixing SELECT with VALUES source
122
- * @throws Error if no destination columns specified
123
- */
124
- withSelect(query: SelectQueryNode, columns: ColumnNode[]): InsertQueryState {
125
- const targetColumns =
126
- columns.length
127
- ? columns
128
- : this.ast.columns.length
129
- ? this.ast.columns
130
- : this.getTableColumns();
131
-
132
- if (!targetColumns.length) {
133
- throw new Error('INSERT ... SELECT requires specifying destination columns.');
134
- }
135
-
136
- if (this.ast.source.type === 'InsertValues' && this.ast.source.rows.length) {
137
- throw new Error('Cannot mix INSERT ... SELECT with INSERT ... VALUES source.');
138
- }
139
-
140
- return this.clone({
141
- ...this.ast,
142
- columns: [...targetColumns],
143
- source: {
144
- type: 'InsertSelect',
145
- query
146
- }
147
- });
148
- }
149
-
150
- /**
151
- * Adds a RETURNING clause to the INSERT query
152
- * @param columns - Columns to return after insertion
153
- * @returns A new InsertQueryState with the RETURNING clause added
154
- */
155
- withReturning(columns: ColumnNode[]): InsertQueryState {
156
- return this.clone({
157
- ...this.ast,
158
- returning: [...columns]
159
- });
160
- }
161
- }
9
+ import type { ValueOperandInput } from '../core/ast/expression.js';
10
+ import {
11
+ buildColumnNodes,
12
+ createTableNode
13
+ } from '../core/ast/builders.js';
14
+
15
+ type InsertRows = Record<string, ValueOperandInput>[];
16
+
17
+ /**
18
+ * Maintains immutable state for building INSERT queries
19
+ */
20
+ export class InsertQueryState {
21
+ public readonly table: TableDef;
22
+ public readonly ast: InsertQueryNode;
23
+
24
+ /**
25
+ * Creates a new InsertQueryState instance
26
+ * @param table - The table definition for the INSERT query
27
+ * @param ast - Optional initial AST node, defaults to a basic INSERT query
28
+ */
29
+ constructor(table: TableDef, ast?: InsertQueryNode) {
30
+ this.table = table;
31
+ this.ast = ast ?? {
32
+ type: 'InsertQuery',
33
+ into: createTableNode(table),
34
+ columns: [],
35
+ source: {
36
+ type: 'InsertValues',
37
+ rows: []
38
+ }
39
+ };
40
+ }
41
+
42
+ private clone(nextAst: InsertQueryNode): InsertQueryState {
43
+ return new InsertQueryState(this.table, nextAst);
44
+ }
45
+
46
+ private ensureColumnsFromRow(rows: InsertRows): ColumnNode[] {
47
+ if (this.ast.columns.length) return this.ast.columns;
48
+ return buildColumnNodes(this.table, Object.keys(rows[0]));
49
+ }
50
+
51
+ private appendValues(rows: OperandNode[][]): OperandNode[][] {
52
+ if (this.ast.source.type === 'InsertValues') {
53
+ return [...this.ast.source.rows, ...rows];
54
+ }
55
+ return rows;
56
+ }
57
+
58
+ private getTableColumns(): ColumnNode[] {
59
+ const names = Object.keys(this.table.columns);
60
+ if (!names.length) return [];
61
+ return buildColumnNodes(this.table, names);
62
+ }
63
+
64
+ /**
65
+ * Adds VALUES clause to the INSERT query
66
+ * @param rows - Array of row objects to insert
67
+ * @returns A new InsertQueryState with the VALUES clause added
68
+ * @throws Error if mixing VALUES with SELECT source
69
+ * @throws Error if invalid values are provided
70
+ */
71
+ withValues(rows: InsertRows): InsertQueryState {
72
+ if (!rows.length) return this;
73
+
74
+ if (this.ast.source.type === 'InsertSelect') {
75
+ throw new Error('Cannot mix INSERT ... VALUES with INSERT ... SELECT source.');
76
+ }
77
+
78
+ const definedColumns = this.ensureColumnsFromRow(rows);
79
+
80
+ const newRows: OperandNode[][] = rows.map((row, rowIndex) =>
81
+ definedColumns.map(column => {
82
+ const rawValue = row[column.name];
83
+
84
+ if (!isValueOperandInput(rawValue)) {
85
+ throw new Error(
86
+ `Invalid insert value for column "${column.name}" in row ${rowIndex}: only primitives, null, or OperandNodes are allowed`
87
+ );
88
+ }
89
+
90
+ return valueToOperand(rawValue);
91
+ })
92
+ );
93
+
94
+ return this.clone({
95
+ ...this.ast,
96
+ columns: definedColumns,
97
+ source: {
98
+ type: 'InsertValues',
99
+ rows: this.appendValues(newRows)
100
+ }
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Sets the columns for the INSERT query
106
+ * @param columns - Column nodes to insert into
107
+ * @returns A new InsertQueryState with the specified columns
108
+ */
109
+ withColumns(columns: ColumnNode[]): InsertQueryState {
110
+ if (!columns.length) return this;
111
+ return this.clone({
112
+ ...this.ast,
113
+ columns: [...columns]
114
+ });
115
+ }
116
+
117
+ /**
118
+ * Adds SELECT source to the INSERT query
119
+ * @param query - The SELECT query to use as source
120
+ * @param columns - Target columns for the INSERT
121
+ * @returns A new InsertQueryState with the SELECT source
122
+ * @throws Error if mixing SELECT with VALUES source
123
+ * @throws Error if no destination columns specified
124
+ */
125
+ withSelect(query: SelectQueryNode, columns: ColumnNode[]): InsertQueryState {
126
+ const targetColumns =
127
+ columns.length
128
+ ? columns
129
+ : this.ast.columns.length
130
+ ? this.ast.columns
131
+ : this.getTableColumns();
132
+
133
+ if (!targetColumns.length) {
134
+ throw new Error('INSERT ... SELECT requires specifying destination columns.');
135
+ }
136
+
137
+ if (this.ast.source.type === 'InsertValues' && this.ast.source.rows.length) {
138
+ throw new Error('Cannot mix INSERT ... SELECT with INSERT ... VALUES source.');
139
+ }
140
+
141
+ return this.clone({
142
+ ...this.ast,
143
+ columns: [...targetColumns],
144
+ source: {
145
+ type: 'InsertSelect',
146
+ query
147
+ }
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Adds a RETURNING clause to the INSERT query
153
+ * @param columns - Columns to return after insertion
154
+ * @returns A new InsertQueryState with the RETURNING clause added
155
+ */
156
+ withReturning(columns: ColumnNode[]): InsertQueryState {
157
+ return this.clone({
158
+ ...this.ast,
159
+ returning: [...columns]
160
+ });
161
+ }
162
+ }
@@ -1,7 +1,8 @@
1
1
  import type { SelectQueryBuilder } from './select.js';
2
2
  import { TableDef } from '../schema/table.js';
3
3
  import { ColumnDef } from '../schema/column-types.js';
4
- import { ColumnNode } from '../core/ast/expression.js';
4
+ import { ColumnNode } from '../core/ast/expression.js';
5
+ import type { ValueOperandInput } from '../core/ast/expression.js';
5
6
  import { CompiledQuery, InsertCompiler, Dialect } from '../core/dialect/abstract.js';
6
7
  import { DialectKey, resolveDialectInput } from '../core/dialect/dialect-factory.js';
7
8
  import { InsertQueryNode, SelectQueryNode } from '../core/ast/query.js';
@@ -36,7 +37,9 @@ export class InsertQueryBuilder<T> {
36
37
  * @param rowOrRows - Single row object or array of row objects to insert
37
38
  * @returns A new InsertQueryBuilder with the VALUES clause added
38
39
  */
39
- values(rowOrRows: Record<string, unknown> | Record<string, unknown>[]): InsertQueryBuilder<T> {
40
+ values(
41
+ rowOrRows: Record<string, ValueOperandInput> | Record<string, ValueOperandInput>[]
42
+ ): InsertQueryBuilder<T> {
40
43
  const rows = Array.isArray(rowOrRows) ? rowOrRows : [rowOrRows];
41
44
  if (!rows.length) return this;
42
45
  return this.clone(this.state.withValues(rows));