metal-orm 1.0.39 → 1.0.41

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.
Files changed (52) hide show
  1. package/dist/index.cjs +1466 -189
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +723 -51
  4. package/dist/index.d.ts +723 -51
  5. package/dist/index.js +1457 -189
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/codegen/typescript.ts +66 -5
  9. package/src/core/ast/aggregate-functions.ts +15 -15
  10. package/src/core/ast/expression-builders.ts +378 -316
  11. package/src/core/ast/expression-nodes.ts +210 -186
  12. package/src/core/ast/expression-visitor.ts +40 -30
  13. package/src/core/ast/query.ts +164 -132
  14. package/src/core/ast/window-functions.ts +86 -86
  15. package/src/core/dialect/abstract.ts +509 -479
  16. package/src/core/dialect/base/groupby-compiler.ts +6 -6
  17. package/src/core/dialect/base/join-compiler.ts +9 -12
  18. package/src/core/dialect/base/orderby-compiler.ts +20 -6
  19. package/src/core/dialect/base/sql-dialect.ts +237 -138
  20. package/src/core/dialect/mssql/index.ts +164 -185
  21. package/src/core/dialect/sqlite/index.ts +39 -34
  22. package/src/core/execution/db-executor.ts +46 -6
  23. package/src/core/execution/executors/mssql-executor.ts +39 -22
  24. package/src/core/execution/executors/mysql-executor.ts +23 -6
  25. package/src/core/execution/executors/sqlite-executor.ts +29 -3
  26. package/src/core/execution/pooling/pool-types.ts +30 -0
  27. package/src/core/execution/pooling/pool.ts +268 -0
  28. package/src/core/functions/standard-strategy.ts +46 -37
  29. package/src/decorators/bootstrap.ts +7 -7
  30. package/src/index.ts +6 -0
  31. package/src/orm/domain-event-bus.ts +49 -0
  32. package/src/orm/entity-metadata.ts +9 -9
  33. package/src/orm/entity.ts +58 -0
  34. package/src/orm/orm-session.ts +465 -270
  35. package/src/orm/orm.ts +61 -11
  36. package/src/orm/pooled-executor-factory.ts +131 -0
  37. package/src/orm/query-logger.ts +6 -12
  38. package/src/orm/relation-change-processor.ts +75 -0
  39. package/src/orm/relations/many-to-many.ts +4 -2
  40. package/src/orm/save-graph.ts +303 -0
  41. package/src/orm/transaction-runner.ts +3 -3
  42. package/src/orm/unit-of-work.ts +128 -0
  43. package/src/query-builder/delete-query-state.ts +67 -38
  44. package/src/query-builder/delete.ts +37 -1
  45. package/src/query-builder/hydration-manager.ts +93 -79
  46. package/src/query-builder/insert-query-state.ts +131 -61
  47. package/src/query-builder/insert.ts +27 -1
  48. package/src/query-builder/query-ast-service.ts +207 -170
  49. package/src/query-builder/select-query-state.ts +169 -162
  50. package/src/query-builder/select.ts +15 -23
  51. package/src/query-builder/update-query-state.ts +114 -77
  52. package/src/query-builder/update.ts +38 -1
package/src/orm/orm.ts CHANGED
@@ -6,29 +6,61 @@ import { InterceptorPipeline } from './interceptor-pipeline.js';
6
6
  import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
7
7
  import { OrmSession } from './orm-session.js';
8
8
 
9
+ /**
10
+ * Options for creating an ORM instance.
11
+ * @template E - The domain event type
12
+ */
9
13
  export interface OrmOptions<E extends DomainEvent = OrmDomainEvent> {
14
+ /** The database dialect */
10
15
  dialect: Dialect;
16
+ /** The database executor factory */
11
17
  executorFactory: DbExecutorFactory;
18
+ /** Optional interceptors pipeline */
12
19
  interceptors?: InterceptorPipeline;
20
+ /** Optional naming strategy */
13
21
  namingStrategy?: NamingStrategy;
14
22
  // model registrations etc.
15
23
  }
16
24
 
25
+ /**
26
+ * Database executor factory interface.
27
+ */
17
28
  export interface DbExecutorFactory {
18
- createExecutor(options?: { tx?: ExternalTransaction }): DbExecutor;
29
+ /**
30
+ * Creates a database executor.
31
+ * @returns The database executor
32
+ */
33
+ createExecutor(): DbExecutor;
34
+
35
+ /**
36
+ * Creates a transactional database executor.
37
+ * @returns The transactional database executor
38
+ */
19
39
  createTransactionalExecutor(): DbExecutor;
20
- }
21
40
 
22
- export interface ExternalTransaction {
23
- // Transaction-specific properties
41
+ /**
42
+ * Disposes any underlying resources (connection pools, background timers, etc).
43
+ */
44
+ dispose(): Promise<void>;
24
45
  }
25
46
 
47
+ /**
48
+ * ORM (Object-Relational Mapping) main class.
49
+ * @template E - The domain event type
50
+ */
26
51
  export class Orm<E extends DomainEvent = OrmDomainEvent> {
52
+ /** The database dialect */
27
53
  readonly dialect: Dialect;
54
+ /** The interceptors pipeline */
28
55
  readonly interceptors: InterceptorPipeline;
56
+ /** The naming strategy */
29
57
  readonly namingStrategy: NamingStrategy;
30
58
  private readonly executorFactory: DbExecutorFactory;
31
59
 
60
+ /**
61
+ * Creates a new ORM instance.
62
+ * @param opts - ORM options
63
+ */
32
64
  constructor(opts: OrmOptions<E>) {
33
65
  this.dialect = opts.dialect;
34
66
  this.interceptors = opts.interceptors ?? new InterceptorPipeline();
@@ -36,23 +68,41 @@ export class Orm<E extends DomainEvent = OrmDomainEvent> {
36
68
  this.executorFactory = opts.executorFactory;
37
69
  }
38
70
 
39
- createSession(options?: { tx?: ExternalTransaction }): OrmSession<E> {
40
- const executor = this.executorFactory.createExecutor(options?.tx);
71
+ /**
72
+ * Creates a new ORM session.
73
+ * @param options - Optional session options
74
+ * @returns The ORM session
75
+ */
76
+ createSession(): OrmSession<E> {
77
+ // No implicit transaction binding; callers should use Orm.transaction() for transactional work.
78
+ const executor = this.executorFactory.createExecutor();
41
79
  return new OrmSession<E>({ orm: this, executor });
42
80
  }
43
81
 
82
+ /**
83
+ * Executes a function within a transaction.
84
+ * @template T - The return type
85
+ * @param fn - The function to execute
86
+ * @returns The result of the function
87
+ * @throws If the transaction fails
88
+ */
44
89
  async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
45
90
  const executor = this.executorFactory.createTransactionalExecutor();
46
91
  const session = new OrmSession<E>({ orm: this, executor });
47
92
  try {
48
- const result = await fn(session);
49
- await session.commit();
50
- return result;
93
+ // A real transaction scope: begin before running user code, commit/rollback after.
94
+ return await session.transaction(() => fn(session));
51
95
  } catch (err) {
52
- await session.rollback();
53
96
  throw err;
54
97
  } finally {
55
- // executor cleanup if needed
98
+ await session.dispose();
56
99
  }
57
100
  }
101
+
102
+ /**
103
+ * Shuts down the ORM and releases underlying resources (pools, timers).
104
+ */
105
+ async dispose(): Promise<void> {
106
+ await this.executorFactory.dispose();
107
+ }
58
108
  }
@@ -0,0 +1,131 @@
1
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
2
+ import { rowsToQueryResult } from '../core/execution/db-executor.js';
3
+ import type { Pool } from '../core/execution/pooling/pool.js';
4
+ import type { DbExecutorFactory } from './orm.js';
5
+
6
+ export interface PooledConnectionAdapter<TConn> {
7
+ query(
8
+ conn: TConn,
9
+ sql: string,
10
+ params?: unknown[]
11
+ ): Promise<Array<Record<string, unknown>>>;
12
+
13
+ beginTransaction(conn: TConn): Promise<void>;
14
+ commitTransaction(conn: TConn): Promise<void>;
15
+ rollbackTransaction(conn: TConn): Promise<void>;
16
+ }
17
+
18
+ type PooledExecutorFactoryOptions<TConn> = {
19
+ pool: Pool<TConn>;
20
+ adapter: PooledConnectionAdapter<TConn>;
21
+ };
22
+
23
+ /**
24
+ * Creates a first-class DbExecutorFactory backed by MetalORM's Pool.
25
+ *
26
+ * Design goals:
27
+ * - No leaks by default: pool leases are always released in `finally`.
28
+ * - Correct transactions: one leased connection per transaction.
29
+ * - Session-friendly: createExecutor() supports transactions without permanently leasing a connection.
30
+ */
31
+ export function createPooledExecutorFactory<TConn>(
32
+ opts: PooledExecutorFactoryOptions<TConn>
33
+ ): DbExecutorFactory {
34
+ const { pool, adapter } = opts;
35
+
36
+ const makeExecutor = (mode: 'session' | 'sticky'): DbExecutor => {
37
+ let lease: Awaited<ReturnType<typeof pool.acquire>> | null = null;
38
+
39
+ const getLease = async () => {
40
+ if (lease) return lease;
41
+ lease = await pool.acquire();
42
+ return lease;
43
+ };
44
+
45
+ const executeWithConn = async (
46
+ conn: TConn,
47
+ sql: string,
48
+ params?: unknown[]
49
+ ): Promise<QueryResult[]> => {
50
+ const rows = await adapter.query(conn, sql, params);
51
+ return [rowsToQueryResult(rows)];
52
+ };
53
+
54
+ return {
55
+ capabilities: { transactions: true },
56
+
57
+ async executeSql(sql, params) {
58
+ // Sticky mode: always reuse a leased connection.
59
+ if (mode === 'sticky') {
60
+ const l = await getLease();
61
+ return executeWithConn(l.resource, sql, params);
62
+ }
63
+
64
+ // Session mode: use the leased connection if we're currently in a transaction;
65
+ // otherwise acquire/release per call.
66
+ if (lease) {
67
+ return executeWithConn(lease.resource, sql, params);
68
+ }
69
+
70
+ const l = await pool.acquire();
71
+ try {
72
+ return await executeWithConn(l.resource, sql, params);
73
+ } finally {
74
+ await l.release();
75
+ }
76
+ },
77
+
78
+ async beginTransaction() {
79
+ const l = await getLease();
80
+ await adapter.beginTransaction(l.resource);
81
+ },
82
+
83
+ async commitTransaction() {
84
+ if (!lease) {
85
+ throw new Error('commitTransaction called without an active transaction');
86
+ }
87
+ const l = lease;
88
+ try {
89
+ await adapter.commitTransaction(l.resource);
90
+ } finally {
91
+ lease = null;
92
+ await l.release();
93
+ }
94
+ },
95
+
96
+ async rollbackTransaction() {
97
+ if (!lease) {
98
+ // Nothing to rollback; keep idempotent semantics.
99
+ return;
100
+ }
101
+ const l = lease;
102
+ try {
103
+ await adapter.rollbackTransaction(l.resource);
104
+ } finally {
105
+ lease = null;
106
+ await l.release();
107
+ }
108
+ },
109
+
110
+ async dispose() {
111
+ if (!lease) return;
112
+ const l = lease;
113
+ lease = null;
114
+ await l.release();
115
+ },
116
+ };
117
+ };
118
+
119
+ return {
120
+ createExecutor() {
121
+ return makeExecutor('session');
122
+ },
123
+ createTransactionalExecutor() {
124
+ return makeExecutor('sticky');
125
+ },
126
+ async dispose() {
127
+ await pool.destroy();
128
+ },
129
+ };
130
+ }
131
+
@@ -31,23 +31,17 @@ export const createQueryLoggingExecutor = (
31
31
  }
32
32
 
33
33
  const wrapped: DbExecutor = {
34
+ capabilities: executor.capabilities,
34
35
  async executeSql(sql, params) {
35
36
  logger({ sql, params });
36
37
  return executor.executeSql(sql, params);
37
38
  }
39
+ ,
40
+ beginTransaction: () => executor.beginTransaction(),
41
+ commitTransaction: () => executor.commitTransaction(),
42
+ rollbackTransaction: () => executor.rollbackTransaction(),
43
+ dispose: () => executor.dispose(),
38
44
  };
39
45
 
40
- if (executor.beginTransaction) {
41
- wrapped.beginTransaction = executor.beginTransaction.bind(executor);
42
- }
43
-
44
- if (executor.commitTransaction) {
45
- wrapped.commitTransaction = executor.commitTransaction.bind(executor);
46
- }
47
-
48
- if (executor.rollbackTransaction) {
49
- wrapped.rollbackTransaction = executor.rollbackTransaction.bind(executor);
50
- }
51
-
52
46
  return wrapped;
53
47
  };
@@ -10,23 +10,42 @@ import type { DbExecutor } from '../core/execution/db-executor.js';
10
10
  import type { RelationChangeEntry } from './runtime-types.js';
11
11
  import { UnitOfWork } from './unit-of-work.js';
12
12
 
13
+ /**
14
+ * Processes relation changes for entity relationships.
15
+ */
13
16
  export class RelationChangeProcessor {
14
17
  private readonly relationChanges: RelationChangeEntry[] = [];
15
18
 
19
+ /**
20
+ * Creates a new RelationChangeProcessor instance.
21
+ * @param unitOfWork - The unit of work instance
22
+ * @param dialect - The database dialect
23
+ * @param executor - The database executor
24
+ */
16
25
  constructor(
17
26
  private readonly unitOfWork: UnitOfWork,
18
27
  private readonly dialect: Dialect,
19
28
  private readonly executor: DbExecutor
20
29
  ) { }
21
30
 
31
+ /**
32
+ * Registers a relation change for processing.
33
+ * @param entry - The relation change entry
34
+ */
22
35
  registerChange(entry: RelationChangeEntry): void {
23
36
  this.relationChanges.push(entry);
24
37
  }
25
38
 
39
+ /**
40
+ * Resets the relation change processor by clearing all pending changes.
41
+ */
26
42
  reset(): void {
27
43
  this.relationChanges.length = 0;
28
44
  }
29
45
 
46
+ /**
47
+ * Processes all pending relation changes.
48
+ */
30
49
  async process(): Promise<void> {
31
50
  if (!this.relationChanges.length) return;
32
51
  const entries = [...this.relationChanges];
@@ -50,6 +69,10 @@ export class RelationChangeProcessor {
50
69
  }
51
70
  }
52
71
 
72
+ /**
73
+ * Handles changes for has-many relations.
74
+ * @param entry - The relation change entry
75
+ */
53
76
  private async handleHasManyChange(entry: RelationChangeEntry): Promise<void> {
54
77
  const relation = entry.relation as HasManyRelation;
55
78
  const target = entry.change.entity;
@@ -73,6 +96,10 @@ export class RelationChangeProcessor {
73
96
  }
74
97
  }
75
98
 
99
+ /**
100
+ * Handles changes for has-one relations.
101
+ * @param entry - The relation change entry
102
+ */
76
103
  private async handleHasOneChange(entry: RelationChangeEntry): Promise<void> {
77
104
  const relation = entry.relation as HasOneRelation;
78
105
  const target = entry.change.entity;
@@ -96,10 +123,18 @@ export class RelationChangeProcessor {
96
123
  }
97
124
  }
98
125
 
126
+ /**
127
+ * Handles changes for belongs-to relations.
128
+ * @param _entry - The relation change entry (reserved for future use)
129
+ */
99
130
  private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
100
131
  // Reserved for future cascade/persist behaviors for belongs-to relations.
101
132
  }
102
133
 
134
+ /**
135
+ * Handles changes for belongs-to-many relations.
136
+ * @param entry - The relation change entry
137
+ */
103
138
  private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
104
139
  const relation = entry.relation as BelongsToManyRelation;
105
140
  const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
@@ -123,12 +158,23 @@ export class RelationChangeProcessor {
123
158
  }
124
159
  }
125
160
 
161
+ /**
162
+ * Assigns a foreign key for has-many relations.
163
+ * @param child - The child entity
164
+ * @param relation - The has-many relation
165
+ * @param rootValue - The root entity's primary key value
166
+ */
126
167
  private assignHasManyForeignKey(child: any, relation: HasManyRelation, rootValue: unknown): void {
127
168
  const current = child[relation.foreignKey];
128
169
  if (current === rootValue) return;
129
170
  child[relation.foreignKey] = rootValue;
130
171
  }
131
172
 
173
+ /**
174
+ * Detaches a child entity from has-many relations.
175
+ * @param child - The child entity
176
+ * @param relation - The has-many relation
177
+ */
132
178
  private detachHasManyChild(child: any, relation: HasManyRelation): void {
133
179
  if (relation.cascade === 'all' || relation.cascade === 'remove') {
134
180
  this.unitOfWork.markRemoved(child);
@@ -138,12 +184,23 @@ export class RelationChangeProcessor {
138
184
  this.unitOfWork.markDirty(child);
139
185
  }
140
186
 
187
+ /**
188
+ * Assigns a foreign key for has-one relations.
189
+ * @param child - The child entity
190
+ * @param relation - The has-one relation
191
+ * @param rootValue - The root entity's primary key value
192
+ */
141
193
  private assignHasOneForeignKey(child: any, relation: HasOneRelation, rootValue: unknown): void {
142
194
  const current = child[relation.foreignKey];
143
195
  if (current === rootValue) return;
144
196
  child[relation.foreignKey] = rootValue;
145
197
  }
146
198
 
199
+ /**
200
+ * Detaches a child entity from has-one relations.
201
+ * @param child - The child entity
202
+ * @param relation - The has-one relation
203
+ */
147
204
  private detachHasOneChild(child: any, relation: HasOneRelation): void {
148
205
  if (relation.cascade === 'all' || relation.cascade === 'remove') {
149
206
  this.unitOfWork.markRemoved(child);
@@ -153,6 +210,12 @@ export class RelationChangeProcessor {
153
210
  this.unitOfWork.markDirty(child);
154
211
  }
155
212
 
213
+ /**
214
+ * Inserts a pivot row for belongs-to-many relations.
215
+ * @param relation - The belongs-to-many relation
216
+ * @param rootId - The root entity's primary key value
217
+ * @param targetId - The target entity's primary key value
218
+ */
156
219
  private async insertPivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
157
220
  const payload = {
158
221
  [relation.pivotForeignKeyToRoot]: rootId,
@@ -163,6 +226,12 @@ export class RelationChangeProcessor {
163
226
  await this.executor.executeSql(compiled.sql, compiled.params);
164
227
  }
165
228
 
229
+ /**
230
+ * Deletes a pivot row for belongs-to-many relations.
231
+ * @param relation - The belongs-to-many relation
232
+ * @param rootId - The root entity's primary key value
233
+ * @param targetId - The target entity's primary key value
234
+ */
166
235
  private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
167
236
  const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
168
237
  const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
@@ -175,6 +244,12 @@ export class RelationChangeProcessor {
175
244
  await this.executor.executeSql(compiled.sql, compiled.params);
176
245
  }
177
246
 
247
+ /**
248
+ * Resolves the primary key value from an entity.
249
+ * @param entity - The entity
250
+ * @param table - The table definition
251
+ * @returns The primary key value or null
252
+ */
178
253
  private resolvePrimaryKeyValue(entity: any, table: TableDef): string | number | null {
179
254
  if (!entity) return null;
180
255
  const key = findPrimaryKey(table);
@@ -63,8 +63,10 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
63
63
  attach(target: TTarget | number | string): void {
64
64
  const entity = this.ensureEntity(target);
65
65
  const id = this.extractId(entity);
66
- if (id == null) return;
67
- if (this.items.some(item => this.extractId(item) === id)) {
66
+ if (id != null && this.items.some(item => this.extractId(item) === id)) {
67
+ return;
68
+ }
69
+ if (id == null && this.items.includes(entity)) {
68
70
  return;
69
71
  }
70
72
  this.items.push(entity);