metal-orm 1.1.11 → 1.1.16

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 (182) hide show
  1. package/README.md +758 -769
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/package.json +100 -96
  5. package/scripts/generate-entities/schema.mjs +122 -122
  6. package/scripts/generate-entities/tree-detection.mjs +92 -92
  7. package/scripts/inflection/pt-br.mjs +468 -468
  8. package/scripts/run-eslint.mjs +33 -34
  9. package/scripts/show-sql-seed.sql +102 -0
  10. package/scripts/show-sql.mjs +321 -325
  11. package/src/codegen/naming-strategy.ts +54 -54
  12. package/src/codegen/typescript.ts +482 -482
  13. package/src/core/ast/adapters.ts +1 -1
  14. package/src/core/ast/builders.ts +123 -123
  15. package/src/core/ast/expression-builders.ts +745 -745
  16. package/src/core/ast/expression-nodes.ts +304 -304
  17. package/src/core/ast/expression-visitor.ts +212 -212
  18. package/src/core/ast/expression.ts +20 -20
  19. package/src/core/ast/helpers.ts +14 -14
  20. package/src/core/ast/join.ts +18 -18
  21. package/src/core/ast/query.ts +200 -200
  22. package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
  23. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
  24. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
  25. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
  26. package/src/core/ddl/dialects/render-reference.test.ts +70 -70
  27. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
  28. package/src/core/ddl/introspect/catalogs/index.ts +5 -5
  29. package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
  30. package/src/core/ddl/introspect/context.ts +15 -15
  31. package/src/core/ddl/introspect/functions/postgres.ts +35 -35
  32. package/src/core/ddl/introspect/registry.ts +40 -40
  33. package/src/core/ddl/introspect/run-select.ts +35 -35
  34. package/src/core/ddl/introspect/utils.ts +56 -56
  35. package/src/core/ddl/naming-strategy.ts +16 -16
  36. package/src/core/ddl/schema-dialect.ts +56 -56
  37. package/src/core/ddl/schema-diff.ts +234 -234
  38. package/src/core/ddl/schema-generator.ts +1 -1
  39. package/src/core/ddl/schema-introspect.ts +25 -25
  40. package/src/core/ddl/sql-writing.ts +171 -171
  41. package/src/core/dialect/abstract.ts +541 -541
  42. package/src/core/dialect/base/cte-compiler.ts +33 -33
  43. package/src/core/dialect/base/function-table-formatter.ts +103 -103
  44. package/src/core/dialect/base/groupby-compiler.ts +21 -21
  45. package/src/core/dialect/base/join-compiler.ts +25 -25
  46. package/src/core/dialect/base/orderby-compiler.ts +35 -35
  47. package/src/core/dialect/base/pagination-strategy.ts +32 -32
  48. package/src/core/dialect/base/returning-strategy.ts +57 -57
  49. package/src/core/dialect/base/sql-dialect.ts +294 -294
  50. package/src/core/dialect/dialect-factory.ts +91 -91
  51. package/src/core/dialect/mssql/functions.ts +120 -120
  52. package/src/core/dialect/mssql/index.ts +317 -317
  53. package/src/core/dialect/mysql/functions.ts +103 -103
  54. package/src/core/dialect/mysql/index.ts +105 -105
  55. package/src/core/dialect/postgres/functions.ts +108 -108
  56. package/src/core/dialect/postgres/index.ts +91 -91
  57. package/src/core/dialect/sqlite/functions.ts +129 -129
  58. package/src/core/dialect/sqlite/index.ts +67 -67
  59. package/src/core/driver/database-driver.ts +19 -19
  60. package/src/core/driver/mssql-driver.ts +23 -23
  61. package/src/core/driver/mysql-driver.ts +23 -23
  62. package/src/core/driver/postgres-driver.ts +23 -23
  63. package/src/core/driver/sqlite-driver.ts +23 -23
  64. package/src/core/execution/db-executor.ts +92 -92
  65. package/src/core/execution/executors/better-sqlite3-executor.ts +90 -90
  66. package/src/core/execution/executors/mssql-executor.ts +184 -184
  67. package/src/core/execution/executors/mysql-executor.ts +88 -88
  68. package/src/core/execution/executors/postgres-executor.ts +26 -26
  69. package/src/core/execution/executors/sqlite-executor.ts +42 -42
  70. package/src/core/execution/pooling/pool-types.ts +30 -30
  71. package/src/core/execution/pooling/pool.ts +275 -275
  72. package/src/core/functions/array.ts +35 -35
  73. package/src/core/functions/control-flow.ts +79 -79
  74. package/src/core/functions/datetime.ts +237 -237
  75. package/src/core/functions/json.ts +70 -70
  76. package/src/core/functions/numeric.ts +297 -297
  77. package/src/core/functions/text.ts +334 -334
  78. package/src/core/functions/types.ts +33 -33
  79. package/src/core/hydration/types.ts +51 -51
  80. package/src/core/sql/sql-operator-config.ts +39 -39
  81. package/src/core/sql/sql.ts +152 -152
  82. package/src/decorators/column-decorator.ts +87 -87
  83. package/src/decorators/decorator-metadata.ts +143 -143
  84. package/src/decorators/entity.ts +93 -93
  85. package/src/decorators/index.ts +39 -39
  86. package/src/decorators/relations.ts +231 -231
  87. package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
  88. package/src/decorators/transformers/transformer-decorators.ts +134 -134
  89. package/src/decorators/transformers/transformer-executor.ts +171 -171
  90. package/src/decorators/transformers/transformer-metadata.ts +73 -73
  91. package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
  92. package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
  93. package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
  94. package/src/decorators/validators/country-validator-registry.ts +64 -64
  95. package/src/decorators/validators/country-validators-decorators.ts +203 -203
  96. package/src/decorators/validators/country-validators.ts +105 -105
  97. package/src/decorators/validators/validator-metadata.ts +59 -59
  98. package/src/dto/dto-types.ts +226 -226
  99. package/src/dto/filter-types.ts +141 -141
  100. package/src/dto/index.ts +95 -95
  101. package/src/dto/openapi/generators/base.ts +29 -29
  102. package/src/dto/openapi/generators/column.ts +37 -37
  103. package/src/dto/openapi/generators/dto.ts +91 -91
  104. package/src/dto/openapi/generators/filter.ts +75 -75
  105. package/src/dto/openapi/generators/nested-dto.ts +618 -618
  106. package/src/dto/openapi/generators/pagination.ts +111 -111
  107. package/src/dto/openapi/generators/relation-filter.ts +228 -228
  108. package/src/dto/openapi/index.ts +12 -12
  109. package/src/dto/openapi/types.ts +101 -101
  110. package/src/dto/openapi/utilities.ts +95 -95
  111. package/src/dto/pagination-utils.ts +150 -150
  112. package/src/dto/transform.ts +197 -197
  113. package/src/index.ts +86 -86
  114. package/src/orm/als.ts +58 -58
  115. package/src/orm/domain-event-bus.ts +119 -119
  116. package/src/orm/entity-context.ts +86 -86
  117. package/src/orm/entity-materializer.ts +159 -159
  118. package/src/orm/entity-meta.ts +97 -97
  119. package/src/orm/entity-registry.ts +39 -39
  120. package/src/orm/entity-relation-cache.ts +39 -39
  121. package/src/orm/execute.ts +194 -194
  122. package/src/orm/execution-context.ts +18 -18
  123. package/src/orm/hydration-context.ts +26 -26
  124. package/src/orm/hydration.ts +112 -112
  125. package/src/orm/identity-map.ts +61 -61
  126. package/src/orm/interceptor-pipeline.ts +16 -16
  127. package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
  128. package/src/orm/lazy-batch/belongs-to.ts +108 -108
  129. package/src/orm/lazy-batch/has-many.ts +69 -69
  130. package/src/orm/lazy-batch/has-one.ts +68 -68
  131. package/src/orm/lazy-batch/shared.ts +125 -125
  132. package/src/orm/orm-session.ts +620 -620
  133. package/src/orm/orm.ts +34 -34
  134. package/src/orm/pooled-executor-factory.ts +121 -121
  135. package/src/orm/query-logger.ts +40 -40
  136. package/src/orm/relation-change-processor.ts +420 -420
  137. package/src/orm/relations/belongs-to.ts +143 -143
  138. package/src/orm/relations/has-many.ts +204 -204
  139. package/src/orm/relations/has-one.ts +174 -174
  140. package/src/orm/relations/many-to-many.ts +288 -288
  141. package/src/orm/relations/morph-many.ts +156 -156
  142. package/src/orm/relations/morph-one.ts +151 -151
  143. package/src/orm/relations/morph-to.ts +162 -162
  144. package/src/orm/runtime-types.ts +92 -92
  145. package/src/orm/save-graph.ts +561 -561
  146. package/src/orm/transaction-runner.ts +24 -24
  147. package/src/orm/unit-of-work.ts +426 -426
  148. package/src/query/index.ts +69 -69
  149. package/src/query/target.ts +46 -46
  150. package/src/query-builder/column-selector.ts +81 -81
  151. package/src/query-builder/delete.ts +141 -141
  152. package/src/query-builder/insert-query-state.ts +149 -149
  153. package/src/query-builder/insert.ts +117 -117
  154. package/src/query-builder/query-ast-service.ts +287 -287
  155. package/src/query-builder/query-resolution.ts +78 -78
  156. package/src/query-builder/raw-column-parser.ts +38 -38
  157. package/src/query-builder/relation-filter-utils.ts +153 -153
  158. package/src/query-builder/relation-join-planner.ts +10 -10
  159. package/src/query-builder/relation-manager.ts +87 -87
  160. package/src/query-builder/relation-projection-helper.ts +102 -102
  161. package/src/query-builder/relation-types.ts +9 -9
  162. package/src/query-builder/relation-utils.ts +15 -15
  163. package/src/query-builder/select/cte-facet.ts +40 -40
  164. package/src/query-builder/select/from-facet.ts +80 -80
  165. package/src/query-builder/select/join-facet.ts +62 -62
  166. package/src/query-builder/select/predicate-facet.ts +104 -104
  167. package/src/query-builder/select/projection-facet.ts +70 -70
  168. package/src/query-builder/select/relation-facet.ts +81 -81
  169. package/src/query-builder/select/setop-facet.ts +36 -36
  170. package/src/query-builder/select-helpers.ts +1 -1
  171. package/src/query-builder/select-query-builder-deps.ts +130 -130
  172. package/src/query-builder/select-query-state.ts +207 -207
  173. package/src/query-builder/update.ts +151 -151
  174. package/src/schema/column-types.ts +322 -322
  175. package/src/schema/table-guards.ts +37 -37
  176. package/src/schema/table.ts +281 -281
  177. package/src/tree/index.ts +13 -13
  178. package/src/tree/nested-set-strategy.ts +545 -545
  179. package/src/tree/tree-decorator.ts +397 -397
  180. package/src/tree/tree-manager.ts +754 -754
  181. package/src/tree/tree-query.ts +427 -427
  182. package/src/tree/tree-types.ts +299 -299
@@ -1,34 +1,34 @@
1
- import { Dialect } from '../core/dialect/abstract.js';
2
- import { eq } from '../core/ast/expression.js';
3
- import type { DbExecutor } from '../core/execution/db-executor.js';
4
- import { SelectQueryBuilder } from '../query-builder/select.js';
5
- import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
- import type { ColumnDef } from '../schema/column-types.js';
7
- import type { TableDef } from '../schema/table.js';
8
- import { EntityInstance } from '../schema/types.js';
9
- import { RelationDef } from '../schema/relation.js';
10
-
11
- import { selectFromEntity, getTableDefFromEntity } from '../decorators/bootstrap.js';
12
- import type { EntityConstructor } from './entity-metadata.js';
13
- import { Orm } from './orm.js';
14
- import { IdentityMap } from './identity-map.js';
15
- import { UnitOfWork } from './unit-of-work.js';
16
- import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
17
- import { RelationChangeProcessor } from './relation-change-processor.js';
18
- import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
19
- import { ExecutionContext } from './execution-context.js';
20
- import type { HydrationContext } from './hydration-context.js';
21
- import type { EntityContext, PrimaryKey } from './entity-context.js';
22
- import {
23
- DomainEvent,
24
- OrmDomainEvent,
25
- RelationChange,
26
- RelationChangeEntry,
27
- RelationKey,
28
- TrackedEntity
29
- } from './runtime-types.js';
30
- import { executeHydrated } from './execute.js';
31
- import { runInTransaction } from './transaction-runner.js';
1
+ import { Dialect } from '../core/dialect/abstract.js';
2
+ import { eq } from '../core/ast/expression.js';
3
+ import type { DbExecutor } from '../core/execution/db-executor.js';
4
+ import { SelectQueryBuilder } from '../query-builder/select.js';
5
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
+ import type { ColumnDef } from '../schema/column-types.js';
7
+ import type { TableDef } from '../schema/table.js';
8
+ import { EntityInstance } from '../schema/types.js';
9
+ import { RelationDef } from '../schema/relation.js';
10
+
11
+ import { selectFromEntity, getTableDefFromEntity } from '../decorators/bootstrap.js';
12
+ import type { EntityConstructor } from './entity-metadata.js';
13
+ import { Orm } from './orm.js';
14
+ import { IdentityMap } from './identity-map.js';
15
+ import { UnitOfWork } from './unit-of-work.js';
16
+ import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
17
+ import { RelationChangeProcessor } from './relation-change-processor.js';
18
+ import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
19
+ import { ExecutionContext } from './execution-context.js';
20
+ import type { HydrationContext } from './hydration-context.js';
21
+ import type { EntityContext, PrimaryKey } from './entity-context.js';
22
+ import {
23
+ DomainEvent,
24
+ OrmDomainEvent,
25
+ RelationChange,
26
+ RelationChangeEntry,
27
+ RelationKey,
28
+ TrackedEntity
29
+ } from './runtime-types.js';
30
+ import { executeHydrated } from './execute.js';
31
+ import { runInTransaction } from './transaction-runner.js';
32
32
  import { saveGraphInternal, patchGraphInternal, SaveGraphOptions } from './save-graph.js';
33
33
  import type { SaveGraphInputPayload, PatchGraphInputPayload } from './save-graph-types.js';
34
34
  import type { QueryCacheManager } from '../cache/query-cache-manager.js';
@@ -42,505 +42,505 @@ const ROLLBACK_ONLY_TRANSACTION =
42
42
  * Interface for ORM interceptors that allow hooking into the flush lifecycle.
43
43
  */
44
44
  export interface OrmInterceptor {
45
- /**
46
- * Called before the flush operation begins.
47
- * @param ctx - The entity context
48
- */
49
- beforeFlush?(ctx: EntityContext): Promise<void> | void;
50
-
51
- /**
52
- * Called after the flush operation completes.
53
- * @param ctx - The entity context
54
- */
55
- afterFlush?(ctx: EntityContext): Promise<void> | void;
56
- }
57
-
58
- /**
59
- * Options for creating an OrmSession instance.
60
- * @template E - The domain event type
61
- */
62
- export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
63
- /** The ORM instance */
64
- orm: Orm<E>;
65
- /** The database executor */
66
- executor: DbExecutor;
67
- /** Optional query logger for debugging */
68
- queryLogger?: QueryLogger;
69
- /** Optional interceptors for flush lifecycle hooks */
70
- interceptors?: OrmInterceptor[];
71
- /** Optional domain event handlers */
72
- domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
73
- /** Optional cache manager for query caching */
74
- cacheManager?: QueryCacheManager;
75
- /** Optional tenant ID for multi-tenancy */
76
- tenantId?: string | number;
77
- }
78
-
79
- export interface SaveGraphSessionOptions extends SaveGraphOptions {
80
- /** Wrap the save operation in a transaction (default: true). */
81
- transactional?: boolean;
82
- /** Flush after saveGraph when not transactional (default: false). */
83
- flush?: boolean;
84
- }
85
-
86
- /**
87
- * ORM Session that manages entity lifecycle, identity mapping, and database operations.
88
- * @template E - The domain event type
89
- */
90
- export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
91
- /** The ORM instance */
92
- readonly orm: Orm<E>;
93
- /** The database executor */
94
- readonly executor: DbExecutor;
95
- /** The identity map for tracking entity instances */
96
- readonly identityMap: IdentityMap;
97
- /** The unit of work for tracking entity changes */
98
- readonly unitOfWork: UnitOfWork;
99
- /** The domain event bus */
100
- readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
101
- /** The relation change processor */
102
- readonly relationChanges: RelationChangeProcessor;
103
- /** The cache manager for query caching */
104
- readonly cacheManager?: QueryCacheManager;
105
- /** The tenant ID for multi-tenancy support */
106
- readonly tenantId?: string | number;
107
-
45
+ /**
46
+ * Called before the flush operation begins.
47
+ * @param ctx - The entity context
48
+ */
49
+ beforeFlush?(ctx: EntityContext): Promise<void> | void;
50
+
51
+ /**
52
+ * Called after the flush operation completes.
53
+ * @param ctx - The entity context
54
+ */
55
+ afterFlush?(ctx: EntityContext): Promise<void> | void;
56
+ }
57
+
58
+ /**
59
+ * Options for creating an OrmSession instance.
60
+ * @template E - The domain event type
61
+ */
62
+ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
63
+ /** The ORM instance */
64
+ orm: Orm<E>;
65
+ /** The database executor */
66
+ executor: DbExecutor;
67
+ /** Optional query logger for debugging */
68
+ queryLogger?: QueryLogger;
69
+ /** Optional interceptors for flush lifecycle hooks */
70
+ interceptors?: OrmInterceptor[];
71
+ /** Optional domain event handlers */
72
+ domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
73
+ /** Optional cache manager for query caching */
74
+ cacheManager?: QueryCacheManager;
75
+ /** Optional tenant ID for multi-tenancy */
76
+ tenantId?: string | number;
77
+ }
78
+
79
+ export interface SaveGraphSessionOptions extends SaveGraphOptions {
80
+ /** Wrap the save operation in a transaction (default: true). */
81
+ transactional?: boolean;
82
+ /** Flush after saveGraph when not transactional (default: false). */
83
+ flush?: boolean;
84
+ }
85
+
86
+ /**
87
+ * ORM Session that manages entity lifecycle, identity mapping, and database operations.
88
+ * @template E - The domain event type
89
+ */
90
+ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
91
+ /** The ORM instance */
92
+ readonly orm: Orm<E>;
93
+ /** The database executor */
94
+ readonly executor: DbExecutor;
95
+ /** The identity map for tracking entity instances */
96
+ readonly identityMap: IdentityMap;
97
+ /** The unit of work for tracking entity changes */
98
+ readonly unitOfWork: UnitOfWork;
99
+ /** The domain event bus */
100
+ readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
101
+ /** The relation change processor */
102
+ readonly relationChanges: RelationChangeProcessor;
103
+ /** The cache manager for query caching */
104
+ readonly cacheManager?: QueryCacheManager;
105
+ /** The tenant ID for multi-tenancy support */
106
+ readonly tenantId?: string | number;
107
+
108
108
  private readonly interceptors: OrmInterceptor[];
109
109
  private saveGraphDefaults?: SaveGraphSessionOptions;
110
110
  private transactionDepth = 0;
111
111
  private savepointCounter = 0;
112
112
  private rollbackOnly = false;
113
-
114
- /**
115
- * Creates a new OrmSession instance.
116
- * @param opts - Session options
117
- */
118
- constructor(opts: OrmSessionOptions<E>) {
119
- this.orm = opts.orm;
120
- this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
121
- this.interceptors = [...(opts.interceptors ?? [])];
122
-
123
- this.identityMap = new IdentityMap();
124
- this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
125
- this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
126
- this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
127
- this.cacheManager = opts.cacheManager;
128
- this.tenantId = opts.tenantId;
129
- }
130
-
131
- /**
132
- * Releases resources associated with this session (executor/pool leases) and resets tracking.
133
- * Must be safe to call multiple times.
134
- */
135
- async dispose(): Promise<void> {
136
- try {
137
- await this.executor.dispose();
138
- } finally {
139
- // Always reset in-memory tracking.
140
- this.unitOfWork.reset();
141
- this.relationChanges.reset();
142
- }
143
- }
144
-
145
- /**
146
- * Gets the database dialect.
147
- */
148
- get dialect(): Dialect {
149
- return this.orm.dialect;
150
- }
151
-
152
- /**
153
- * Gets the identity buckets map.
154
- */
155
- get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
156
- return this.unitOfWork.identityBuckets;
157
- }
158
-
159
- /**
160
- * Gets all tracked entities.
161
- */
162
- get tracked(): TrackedEntity[] {
163
- return this.unitOfWork.getTracked();
164
- }
165
-
166
- /**
167
- * Gets an entity by table and primary key.
168
- * @param table - The table definition
169
- * @param pk - The primary key value
170
- * @returns The entity or undefined if not found
171
- */
172
- getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
173
- return this.unitOfWork.getEntity(table, pk);
174
- }
175
-
176
- /**
177
- * Sets an entity in the identity map.
178
- * @param table - The table definition
179
- * @param pk - The primary key value
180
- * @param entity - The entity instance
181
- */
182
- setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
183
- this.unitOfWork.setEntity(table, pk, entity);
184
- }
185
-
186
- /**
187
- * Tracks a new entity.
188
- * @param table - The table definition
189
- * @param entity - The entity instance
190
- * @param pk - Optional primary key value
191
- */
192
- trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
193
- this.unitOfWork.trackNew(table, entity, pk);
194
- }
195
-
196
- /**
197
- * Tracks a managed entity.
198
- * @param table - The table definition
199
- * @param pk - The primary key value
200
- * @param entity - The entity instance
201
- */
202
- trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
203
- this.unitOfWork.trackManaged(table, pk, entity);
204
- }
205
-
206
- /**
207
- * Marks an entity as dirty (modified).
208
- * @param entity - The entity to mark as dirty
209
- */
210
- markDirty(entity: object): void {
211
- this.unitOfWork.markDirty(entity);
212
- }
213
-
214
- /**
215
- * Marks an entity as removed.
216
- * @param entity - The entity to mark as removed
217
- */
218
- markRemoved(entity: object): void {
219
- this.unitOfWork.markRemoved(entity);
220
- }
221
-
222
- /**
223
- * Registers a relation change.
224
- * @param root - The root entity
225
- * @param relationKey - The relation key
226
- * @param rootTable - The root table definition
227
- * @param relationName - The relation name
228
- * @param relation - The relation definition
229
- * @param change - The relation change
230
- */
231
- registerRelationChange = (
232
- root: unknown,
233
- relationKey: RelationKey,
234
- rootTable: TableDef,
235
- relationName: string,
236
- relation: RelationDef,
237
- change: RelationChange<unknown>
238
- ): void => {
239
- this.relationChanges.registerChange(
240
- buildRelationChangeEntry(root, relationKey, rootTable, relationName, relation, change)
241
- );
242
- };
243
-
244
- /**
245
- * Gets all tracked entities for a specific table.
246
- * @param table - The table definition
247
- * @returns Array of tracked entities
248
- */
249
- getEntitiesForTable(table: TableDef): TrackedEntity[] {
250
- return this.unitOfWork.getEntitiesForTable(table);
251
- }
252
-
253
- /**
254
- * Registers an interceptor for flush lifecycle hooks.
255
- * @param interceptor - The interceptor to register
256
- */
257
- registerInterceptor(interceptor: OrmInterceptor): void {
258
- this.interceptors.push(interceptor);
259
- }
260
-
261
- /**
262
- * Registers a domain event handler.
263
- * @param type - The event type
264
- * @param handler - The event handler
265
- */
266
- registerDomainEventHandler<TType extends E['type']>(
267
- type: TType,
268
- handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
269
- ): void {
270
- this.domainEvents.on(type, handler);
271
- }
272
-
273
- /**
274
- * Sets default options applied to all saveGraph calls for this session.
275
- * Per-call options override these defaults.
276
- * @param defaults - Default saveGraph options for the session
277
- */
278
- withSaveGraphDefaults(defaults: SaveGraphSessionOptions): this {
279
- this.saveGraphDefaults = { ...defaults };
280
- return this;
281
- }
282
-
283
- /**
284
- * Finds an entity by its primary key.
285
- * @template TCtor - The entity constructor type
286
- * @param entityClass - The entity constructor
287
- * @param id - The primary key value
288
- * @returns The entity instance or null if not found
289
- * @throws If entity metadata is not bootstrapped or table has no primary key
290
- */
291
- async find<TCtor extends EntityConstructor<object>>(
292
- entityClass: TCtor,
293
- id: unknown
294
- ): Promise<InstanceType<TCtor> | null> {
295
- const table = getTableDefFromEntity(entityClass);
296
- if (!table) {
297
- throw new Error('Entity metadata has not been bootstrapped');
298
- }
299
- const primaryKey = findPrimaryKey(table);
300
- const column = table.columns[primaryKey];
301
- if (!column) {
302
- throw new Error('Entity table does not expose a primary key');
303
- }
304
- const columnSelections = Object.values(table.columns).reduce<Record<string, ColumnDef>>((acc, col) => {
305
- acc[col.name] = col;
306
- return acc;
307
- }, {});
308
- const qb = selectFromEntity(entityClass)
309
- .select(columnSelections)
310
- .where(eq(column, id as string | number))
311
- .limit(1);
312
- const rows = await executeHydrated(this, qb);
313
- return (rows[0] ?? null) as InstanceType<TCtor> | null;
314
- }
315
-
316
- /**
317
- * Finds a single entity using a query builder.
318
- * @template TTable - The table type
319
- * @param qb - The query builder
320
- * @returns The first entity instance or null if not found
321
- */
322
- async findOne<TTable extends TableDef>(qb: SelectQueryBuilder<unknown, TTable>): Promise<EntityInstance<TTable> | null> {
323
- const limited = qb.limit(1);
324
- const rows = await executeHydrated(this, limited);
325
- return rows[0] ?? null;
326
- }
327
-
328
- /**
329
- * Finds multiple entities using a query builder.
330
- * @template TTable - The table type
331
- * @param qb - The query builder
332
- * @returns Array of entity instances
333
- */
334
- async findMany<TTable extends TableDef>(qb: SelectQueryBuilder<unknown, TTable>): Promise<EntityInstance<TTable>[]> {
335
- return executeHydrated(this, qb);
336
- }
337
-
338
- /**
339
- * Saves an entity graph (root + nested relations) based on a DTO-like payload.
340
- * @param entityClass - Root entity constructor
341
- * @param payload - DTO payload containing column values and nested relations
342
- * @param options - Graph save options
343
- * @returns The root entity instance
344
- */
345
- async saveGraph<TCtor extends EntityConstructor<object>>(
346
- entityClass: TCtor,
347
- payload: SaveGraphInputPayload<InstanceType<TCtor>>,
348
- options?: SaveGraphSessionOptions
349
- ): Promise<InstanceType<TCtor>>;
350
- async saveGraph<TCtor extends EntityConstructor<object>>(
351
- entityClass: TCtor,
352
- payload: Record<string, unknown>,
353
- options?: SaveGraphSessionOptions
354
- ): Promise<InstanceType<TCtor>> {
355
- const resolved = this.resolveSaveGraphOptions(options);
356
- const { transactional = true, flush = false, ...graphOptions } = resolved;
357
- const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
358
- if (!transactional) {
359
- const result = await execute();
360
- if (flush) {
361
- await this.flush();
362
- }
363
- return result;
364
- }
365
- return this.transaction(() => execute());
366
- }
367
-
368
- /**
369
- * Saves an entity graph and flushes immediately (defaults to transactional: false).
370
- * @param entityClass - Root entity constructor
371
- * @param payload - DTO payload containing column values and nested relations
372
- * @param options - Graph save options
373
- * @returns The root entity instance
374
- */
375
- async saveGraphAndFlush<TCtor extends EntityConstructor<object>>(
376
- entityClass: TCtor,
377
- payload: SaveGraphInputPayload<InstanceType<TCtor>>,
378
- options?: SaveGraphSessionOptions
379
- ): Promise<InstanceType<TCtor>> {
380
- const merged = { ...(options ?? {}), flush: true, transactional: options?.transactional ?? false };
381
- return this.saveGraph(entityClass, payload, merged);
382
- }
383
-
384
- /**
385
- * Updates an existing entity graph (requires a primary key in the payload).
386
- * @param entityClass - Root entity constructor
387
- * @param payload - DTO payload containing column values and nested relations
388
- * @param options - Graph save options
389
- * @returns The root entity instance or null if not found
390
- */
391
- async updateGraph<TCtor extends EntityConstructor<object>>(
392
- entityClass: TCtor,
393
- payload: SaveGraphInputPayload<InstanceType<TCtor>>,
394
- options?: SaveGraphSessionOptions
395
- ): Promise<InstanceType<TCtor> | null> {
396
- const table = getTableDefFromEntity(entityClass);
397
- if (!table) {
398
- throw new Error('Entity metadata has not been bootstrapped');
399
- }
400
- const primaryKey = findPrimaryKey(table);
401
- const pkValue = (payload as Record<string, unknown>)[primaryKey];
402
- if (pkValue === undefined || pkValue === null) {
403
- throw new Error(`updateGraph requires a primary key value for "${primaryKey}"`);
404
- }
405
-
406
- const resolved = this.resolveSaveGraphOptions(options);
407
- const { transactional = true, flush = false, ...graphOptions } = resolved;
408
- const execute = async (): Promise<InstanceType<TCtor> | null> => {
409
- const tracked = this.getEntity(table, pkValue as PrimaryKey) as InstanceType<TCtor> | undefined;
410
- const existing = tracked ?? await this.find(entityClass, pkValue);
411
- if (!existing) return null;
412
- return saveGraphInternal(this, entityClass, payload, graphOptions);
413
- };
414
-
415
- if (!transactional) {
416
- const result = await execute();
417
- if (result && flush) {
418
- await this.flush();
419
- }
420
- return result;
421
- }
422
- return this.transaction(() => execute());
423
- }
424
-
425
- /**
426
- * Patches an existing entity with partial data (requires a primary key in the payload).
427
- * Only the provided fields are updated; other fields remain unchanged.
428
- * @param entityClass - Root entity constructor
429
- * @param payload - Partial DTO payload containing column values and nested relations
430
- * @param options - Graph save options
431
- * @returns The patched entity instance or null if not found
432
- */
433
- async patchGraph<TCtor extends EntityConstructor<object>>(
434
- entityClass: TCtor,
435
- payload: PatchGraphInputPayload<InstanceType<TCtor>>,
436
- options?: SaveGraphSessionOptions
437
- ): Promise<InstanceType<TCtor> | null>;
438
- async patchGraph<TCtor extends EntityConstructor<object>>(
439
- entityClass: TCtor,
440
- payload: Record<string, unknown>,
441
- options?: SaveGraphSessionOptions
442
- ): Promise<InstanceType<TCtor> | null> {
443
- const table = getTableDefFromEntity(entityClass);
444
- if (!table) {
445
- throw new Error('Entity metadata has not been bootstrapped');
446
- }
447
- const primaryKey = findPrimaryKey(table);
448
- const pkValue = payload[primaryKey];
449
- if (pkValue === undefined || pkValue === null) {
450
- throw new Error(`patchGraph requires a primary key value for "${primaryKey}"`);
451
- }
452
-
453
- const resolved = this.resolveSaveGraphOptions(options);
454
- const { transactional = true, flush = false, ...graphOptions } = resolved;
455
- const execute = async (): Promise<InstanceType<TCtor> | null> => {
456
- const tracked = this.getEntity(table, pkValue as PrimaryKey) as InstanceType<TCtor> | undefined;
457
- const existing = tracked ?? await this.find(entityClass, pkValue);
458
- if (!existing) return null;
459
- return patchGraphInternal(this, entityClass, existing, payload, graphOptions);
460
- };
461
-
462
- if (!transactional) {
463
- const result = await execute();
464
- if (result && flush) {
465
- await this.flush();
466
- }
467
- return result;
468
- }
469
- return this.transaction(() => execute());
470
- }
471
-
472
- /**
473
- * Persists an entity (either inserts or updates).
474
- * @param entity - The entity to persist
475
- * @throws If entity metadata is not bootstrapped
476
- */
477
- async persist(entity: object): Promise<void> {
478
- if (this.unitOfWork.findTracked(entity)) {
479
- return;
480
- }
481
- const table = getTableDefFromEntity((entity as { constructor: EntityConstructor }).constructor);
482
- if (!table) {
483
- throw new Error('Entity metadata has not been bootstrapped');
484
- }
485
- const primaryKey = findPrimaryKey(table);
486
- const pkValue = (entity as Record<string, unknown>)[primaryKey];
487
- if (pkValue !== undefined && pkValue !== null) {
488
- this.trackManaged(table, pkValue as PrimaryKey, entity);
489
- } else {
490
- this.trackNew(table, entity);
491
- }
492
- }
493
-
494
- /**
495
- * Marks an entity for removal.
496
- * @param entity - The entity to remove
497
- */
498
- async remove(entity: object): Promise<void> {
499
- this.markRemoved(entity);
500
- }
501
-
502
- /**
503
- * Flushes pending changes to the database without session hooks, relation processing, or domain events.
504
- */
505
- async flush(): Promise<void> {
506
- await this.unitOfWork.flush();
507
- }
508
-
509
- /**
510
- * Flushes pending changes with interceptors and relation processing.
511
- */
512
- private async flushWithHooks(): Promise<void> {
513
- for (const interceptor of this.interceptors) {
514
- await interceptor.beforeFlush?.(this);
515
- }
516
-
517
- await this.unitOfWork.flush();
518
- await this.relationChanges.process();
519
- await this.unitOfWork.flush();
520
-
521
- for (const interceptor of this.interceptors) {
522
- await interceptor.afterFlush?.(this);
523
- }
524
- }
525
-
526
- /**
527
- * Commits the current transaction.
528
- */
529
- async commit(): Promise<void> {
530
- await runInTransaction(this.executor, async () => {
531
- await this.flushWithHooks();
532
- });
533
-
534
- await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
535
- }
536
-
537
- /**
538
- * Executes a function within a transaction.
539
- * @template T - The return type
540
- * @param fn - The function to execute
541
- * @returns The result of the function
542
- * @throws If the transaction fails
543
- */
113
+
114
+ /**
115
+ * Creates a new OrmSession instance.
116
+ * @param opts - Session options
117
+ */
118
+ constructor(opts: OrmSessionOptions<E>) {
119
+ this.orm = opts.orm;
120
+ this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
121
+ this.interceptors = [...(opts.interceptors ?? [])];
122
+
123
+ this.identityMap = new IdentityMap();
124
+ this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
125
+ this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
126
+ this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
127
+ this.cacheManager = opts.cacheManager;
128
+ this.tenantId = opts.tenantId;
129
+ }
130
+
131
+ /**
132
+ * Releases resources associated with this session (executor/pool leases) and resets tracking.
133
+ * Must be safe to call multiple times.
134
+ */
135
+ async dispose(): Promise<void> {
136
+ try {
137
+ await this.executor.dispose();
138
+ } finally {
139
+ // Always reset in-memory tracking.
140
+ this.unitOfWork.reset();
141
+ this.relationChanges.reset();
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Gets the database dialect.
147
+ */
148
+ get dialect(): Dialect {
149
+ return this.orm.dialect;
150
+ }
151
+
152
+ /**
153
+ * Gets the identity buckets map.
154
+ */
155
+ get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
156
+ return this.unitOfWork.identityBuckets;
157
+ }
158
+
159
+ /**
160
+ * Gets all tracked entities.
161
+ */
162
+ get tracked(): TrackedEntity[] {
163
+ return this.unitOfWork.getTracked();
164
+ }
165
+
166
+ /**
167
+ * Gets an entity by table and primary key.
168
+ * @param table - The table definition
169
+ * @param pk - The primary key value
170
+ * @returns The entity or undefined if not found
171
+ */
172
+ getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
173
+ return this.unitOfWork.getEntity(table, pk);
174
+ }
175
+
176
+ /**
177
+ * Sets an entity in the identity map.
178
+ * @param table - The table definition
179
+ * @param pk - The primary key value
180
+ * @param entity - The entity instance
181
+ */
182
+ setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
183
+ this.unitOfWork.setEntity(table, pk, entity);
184
+ }
185
+
186
+ /**
187
+ * Tracks a new entity.
188
+ * @param table - The table definition
189
+ * @param entity - The entity instance
190
+ * @param pk - Optional primary key value
191
+ */
192
+ trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
193
+ this.unitOfWork.trackNew(table, entity, pk);
194
+ }
195
+
196
+ /**
197
+ * Tracks a managed entity.
198
+ * @param table - The table definition
199
+ * @param pk - The primary key value
200
+ * @param entity - The entity instance
201
+ */
202
+ trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
203
+ this.unitOfWork.trackManaged(table, pk, entity);
204
+ }
205
+
206
+ /**
207
+ * Marks an entity as dirty (modified).
208
+ * @param entity - The entity to mark as dirty
209
+ */
210
+ markDirty(entity: object): void {
211
+ this.unitOfWork.markDirty(entity);
212
+ }
213
+
214
+ /**
215
+ * Marks an entity as removed.
216
+ * @param entity - The entity to mark as removed
217
+ */
218
+ markRemoved(entity: object): void {
219
+ this.unitOfWork.markRemoved(entity);
220
+ }
221
+
222
+ /**
223
+ * Registers a relation change.
224
+ * @param root - The root entity
225
+ * @param relationKey - The relation key
226
+ * @param rootTable - The root table definition
227
+ * @param relationName - The relation name
228
+ * @param relation - The relation definition
229
+ * @param change - The relation change
230
+ */
231
+ registerRelationChange = (
232
+ root: unknown,
233
+ relationKey: RelationKey,
234
+ rootTable: TableDef,
235
+ relationName: string,
236
+ relation: RelationDef,
237
+ change: RelationChange<unknown>
238
+ ): void => {
239
+ this.relationChanges.registerChange(
240
+ buildRelationChangeEntry(root, relationKey, rootTable, relationName, relation, change)
241
+ );
242
+ };
243
+
244
+ /**
245
+ * Gets all tracked entities for a specific table.
246
+ * @param table - The table definition
247
+ * @returns Array of tracked entities
248
+ */
249
+ getEntitiesForTable(table: TableDef): TrackedEntity[] {
250
+ return this.unitOfWork.getEntitiesForTable(table);
251
+ }
252
+
253
+ /**
254
+ * Registers an interceptor for flush lifecycle hooks.
255
+ * @param interceptor - The interceptor to register
256
+ */
257
+ registerInterceptor(interceptor: OrmInterceptor): void {
258
+ this.interceptors.push(interceptor);
259
+ }
260
+
261
+ /**
262
+ * Registers a domain event handler.
263
+ * @param type - The event type
264
+ * @param handler - The event handler
265
+ */
266
+ registerDomainEventHandler<TType extends E['type']>(
267
+ type: TType,
268
+ handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
269
+ ): void {
270
+ this.domainEvents.on(type, handler);
271
+ }
272
+
273
+ /**
274
+ * Sets default options applied to all saveGraph calls for this session.
275
+ * Per-call options override these defaults.
276
+ * @param defaults - Default saveGraph options for the session
277
+ */
278
+ withSaveGraphDefaults(defaults: SaveGraphSessionOptions): this {
279
+ this.saveGraphDefaults = { ...defaults };
280
+ return this;
281
+ }
282
+
283
+ /**
284
+ * Finds an entity by its primary key.
285
+ * @template TCtor - The entity constructor type
286
+ * @param entityClass - The entity constructor
287
+ * @param id - The primary key value
288
+ * @returns The entity instance or null if not found
289
+ * @throws If entity metadata is not bootstrapped or table has no primary key
290
+ */
291
+ async find<TCtor extends EntityConstructor<object>>(
292
+ entityClass: TCtor,
293
+ id: unknown
294
+ ): Promise<InstanceType<TCtor> | null> {
295
+ const table = getTableDefFromEntity(entityClass);
296
+ if (!table) {
297
+ throw new Error('Entity metadata has not been bootstrapped');
298
+ }
299
+ const primaryKey = findPrimaryKey(table);
300
+ const column = table.columns[primaryKey];
301
+ if (!column) {
302
+ throw new Error('Entity table does not expose a primary key');
303
+ }
304
+ const columnSelections = Object.values(table.columns).reduce<Record<string, ColumnDef>>((acc, col) => {
305
+ acc[col.name] = col;
306
+ return acc;
307
+ }, {});
308
+ const qb = selectFromEntity(entityClass)
309
+ .select(columnSelections)
310
+ .where(eq(column, id as string | number))
311
+ .limit(1);
312
+ const rows = await executeHydrated(this, qb);
313
+ return (rows[0] ?? null) as InstanceType<TCtor> | null;
314
+ }
315
+
316
+ /**
317
+ * Finds a single entity using a query builder.
318
+ * @template TTable - The table type
319
+ * @param qb - The query builder
320
+ * @returns The first entity instance or null if not found
321
+ */
322
+ async findOne<TTable extends TableDef>(qb: SelectQueryBuilder<unknown, TTable>): Promise<EntityInstance<TTable> | null> {
323
+ const limited = qb.limit(1);
324
+ const rows = await executeHydrated(this, limited);
325
+ return rows[0] ?? null;
326
+ }
327
+
328
+ /**
329
+ * Finds multiple entities using a query builder.
330
+ * @template TTable - The table type
331
+ * @param qb - The query builder
332
+ * @returns Array of entity instances
333
+ */
334
+ async findMany<TTable extends TableDef>(qb: SelectQueryBuilder<unknown, TTable>): Promise<EntityInstance<TTable>[]> {
335
+ return executeHydrated(this, qb);
336
+ }
337
+
338
+ /**
339
+ * Saves an entity graph (root + nested relations) based on a DTO-like payload.
340
+ * @param entityClass - Root entity constructor
341
+ * @param payload - DTO payload containing column values and nested relations
342
+ * @param options - Graph save options
343
+ * @returns The root entity instance
344
+ */
345
+ async saveGraph<TCtor extends EntityConstructor<object>>(
346
+ entityClass: TCtor,
347
+ payload: SaveGraphInputPayload<InstanceType<TCtor>>,
348
+ options?: SaveGraphSessionOptions
349
+ ): Promise<InstanceType<TCtor>>;
350
+ async saveGraph<TCtor extends EntityConstructor<object>>(
351
+ entityClass: TCtor,
352
+ payload: Record<string, unknown>,
353
+ options?: SaveGraphSessionOptions
354
+ ): Promise<InstanceType<TCtor>> {
355
+ const resolved = this.resolveSaveGraphOptions(options);
356
+ const { transactional = true, flush = false, ...graphOptions } = resolved;
357
+ const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
358
+ if (!transactional) {
359
+ const result = await execute();
360
+ if (flush) {
361
+ await this.flush();
362
+ }
363
+ return result;
364
+ }
365
+ return this.transaction(() => execute());
366
+ }
367
+
368
+ /**
369
+ * Saves an entity graph and flushes immediately (defaults to transactional: false).
370
+ * @param entityClass - Root entity constructor
371
+ * @param payload - DTO payload containing column values and nested relations
372
+ * @param options - Graph save options
373
+ * @returns The root entity instance
374
+ */
375
+ async saveGraphAndFlush<TCtor extends EntityConstructor<object>>(
376
+ entityClass: TCtor,
377
+ payload: SaveGraphInputPayload<InstanceType<TCtor>>,
378
+ options?: SaveGraphSessionOptions
379
+ ): Promise<InstanceType<TCtor>> {
380
+ const merged = { ...(options ?? {}), flush: true, transactional: options?.transactional ?? false };
381
+ return this.saveGraph(entityClass, payload, merged);
382
+ }
383
+
384
+ /**
385
+ * Updates an existing entity graph (requires a primary key in the payload).
386
+ * @param entityClass - Root entity constructor
387
+ * @param payload - DTO payload containing column values and nested relations
388
+ * @param options - Graph save options
389
+ * @returns The root entity instance or null if not found
390
+ */
391
+ async updateGraph<TCtor extends EntityConstructor<object>>(
392
+ entityClass: TCtor,
393
+ payload: SaveGraphInputPayload<InstanceType<TCtor>>,
394
+ options?: SaveGraphSessionOptions
395
+ ): Promise<InstanceType<TCtor> | null> {
396
+ const table = getTableDefFromEntity(entityClass);
397
+ if (!table) {
398
+ throw new Error('Entity metadata has not been bootstrapped');
399
+ }
400
+ const primaryKey = findPrimaryKey(table);
401
+ const pkValue = (payload as Record<string, unknown>)[primaryKey];
402
+ if (pkValue === undefined || pkValue === null) {
403
+ throw new Error(`updateGraph requires a primary key value for "${primaryKey}"`);
404
+ }
405
+
406
+ const resolved = this.resolveSaveGraphOptions(options);
407
+ const { transactional = true, flush = false, ...graphOptions } = resolved;
408
+ const execute = async (): Promise<InstanceType<TCtor> | null> => {
409
+ const tracked = this.getEntity(table, pkValue as PrimaryKey) as InstanceType<TCtor> | undefined;
410
+ const existing = tracked ?? await this.find(entityClass, pkValue);
411
+ if (!existing) return null;
412
+ return saveGraphInternal(this, entityClass, payload, graphOptions);
413
+ };
414
+
415
+ if (!transactional) {
416
+ const result = await execute();
417
+ if (result && flush) {
418
+ await this.flush();
419
+ }
420
+ return result;
421
+ }
422
+ return this.transaction(() => execute());
423
+ }
424
+
425
+ /**
426
+ * Patches an existing entity with partial data (requires a primary key in the payload).
427
+ * Only the provided fields are updated; other fields remain unchanged.
428
+ * @param entityClass - Root entity constructor
429
+ * @param payload - Partial DTO payload containing column values and nested relations
430
+ * @param options - Graph save options
431
+ * @returns The patched entity instance or null if not found
432
+ */
433
+ async patchGraph<TCtor extends EntityConstructor<object>>(
434
+ entityClass: TCtor,
435
+ payload: PatchGraphInputPayload<InstanceType<TCtor>>,
436
+ options?: SaveGraphSessionOptions
437
+ ): Promise<InstanceType<TCtor> | null>;
438
+ async patchGraph<TCtor extends EntityConstructor<object>>(
439
+ entityClass: TCtor,
440
+ payload: Record<string, unknown>,
441
+ options?: SaveGraphSessionOptions
442
+ ): Promise<InstanceType<TCtor> | null> {
443
+ const table = getTableDefFromEntity(entityClass);
444
+ if (!table) {
445
+ throw new Error('Entity metadata has not been bootstrapped');
446
+ }
447
+ const primaryKey = findPrimaryKey(table);
448
+ const pkValue = payload[primaryKey];
449
+ if (pkValue === undefined || pkValue === null) {
450
+ throw new Error(`patchGraph requires a primary key value for "${primaryKey}"`);
451
+ }
452
+
453
+ const resolved = this.resolveSaveGraphOptions(options);
454
+ const { transactional = true, flush = false, ...graphOptions } = resolved;
455
+ const execute = async (): Promise<InstanceType<TCtor> | null> => {
456
+ const tracked = this.getEntity(table, pkValue as PrimaryKey) as InstanceType<TCtor> | undefined;
457
+ const existing = tracked ?? await this.find(entityClass, pkValue);
458
+ if (!existing) return null;
459
+ return patchGraphInternal(this, entityClass, existing, payload, graphOptions);
460
+ };
461
+
462
+ if (!transactional) {
463
+ const result = await execute();
464
+ if (result && flush) {
465
+ await this.flush();
466
+ }
467
+ return result;
468
+ }
469
+ return this.transaction(() => execute());
470
+ }
471
+
472
+ /**
473
+ * Persists an entity (either inserts or updates).
474
+ * @param entity - The entity to persist
475
+ * @throws If entity metadata is not bootstrapped
476
+ */
477
+ async persist(entity: object): Promise<void> {
478
+ if (this.unitOfWork.findTracked(entity)) {
479
+ return;
480
+ }
481
+ const table = getTableDefFromEntity((entity as { constructor: EntityConstructor }).constructor);
482
+ if (!table) {
483
+ throw new Error('Entity metadata has not been bootstrapped');
484
+ }
485
+ const primaryKey = findPrimaryKey(table);
486
+ const pkValue = (entity as Record<string, unknown>)[primaryKey];
487
+ if (pkValue !== undefined && pkValue !== null) {
488
+ this.trackManaged(table, pkValue as PrimaryKey, entity);
489
+ } else {
490
+ this.trackNew(table, entity);
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Marks an entity for removal.
496
+ * @param entity - The entity to remove
497
+ */
498
+ async remove(entity: object): Promise<void> {
499
+ this.markRemoved(entity);
500
+ }
501
+
502
+ /**
503
+ * Flushes pending changes to the database without session hooks, relation processing, or domain events.
504
+ */
505
+ async flush(): Promise<void> {
506
+ await this.unitOfWork.flush();
507
+ }
508
+
509
+ /**
510
+ * Flushes pending changes with interceptors and relation processing.
511
+ */
512
+ private async flushWithHooks(): Promise<void> {
513
+ for (const interceptor of this.interceptors) {
514
+ await interceptor.beforeFlush?.(this);
515
+ }
516
+
517
+ await this.unitOfWork.flush();
518
+ await this.relationChanges.process();
519
+ await this.unitOfWork.flush();
520
+
521
+ for (const interceptor of this.interceptors) {
522
+ await interceptor.afterFlush?.(this);
523
+ }
524
+ }
525
+
526
+ /**
527
+ * Commits the current transaction.
528
+ */
529
+ async commit(): Promise<void> {
530
+ await runInTransaction(this.executor, async () => {
531
+ await this.flushWithHooks();
532
+ });
533
+
534
+ await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
535
+ }
536
+
537
+ /**
538
+ * Executes a function within a transaction.
539
+ * @template T - The return type
540
+ * @param fn - The function to execute
541
+ * @returns The result of the function
542
+ * @throws If the transaction fails
543
+ */
544
544
  async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
545
545
  // If the executor can't do transactions, just run and commit once.
546
546
  if (!this.executor.capabilities.transactions) {
@@ -592,10 +592,10 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
592
592
  }
593
593
  }
594
594
  }
595
-
596
- /**
597
- * Rolls back the current transaction.
598
- */
595
+
596
+ /**
597
+ * Rolls back the current transaction.
598
+ */
599
599
  async rollback(): Promise<void> {
600
600
  if (this.executor.capabilities.transactions) {
601
601
  await this.executor.rollbackTransaction();
@@ -606,80 +606,80 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
606
606
  this.unitOfWork.reset();
607
607
  this.relationChanges.reset();
608
608
  }
609
-
610
- /**
611
- * Gets the execution context.
612
- * @returns The execution context
613
- */
614
- getExecutionContext(): ExecutionContext {
615
- return {
616
- dialect: this.orm.dialect,
617
- executor: this.executor,
618
- interceptors: this.orm.interceptors
619
- };
620
- }
621
-
622
- /**
623
- * Gets the hydration context.
624
- * @returns The hydration context
625
- */
626
- getHydrationContext(): HydrationContext<E> {
627
- return {
628
- identityMap: this.identityMap,
629
- unitOfWork: this.unitOfWork,
630
- domainEvents: this.domainEvents,
631
- relationChanges: this.relationChanges,
632
- entityContext: this
633
- };
634
- }
635
-
636
- /**
637
- * Invalidates cache by specific tags.
638
- * @param tags - Tags to invalidate
639
- * @throws Error if no cache manager is configured
640
- * @example
641
- * await session.invalidateCacheTags(['users', 'dashboard']);
642
- */
643
- async invalidateCacheTags(tags: string[]): Promise<void> {
644
- if (!this.cacheManager) {
645
- throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
646
- }
647
- await this.cacheManager.invalidateTags(tags);
648
- }
649
-
650
- /**
651
- * Invalidates cache by key prefix (useful for multi-tenancy).
652
- * @param prefix - Prefix to match cache keys
653
- * @throws Error if no cache manager is configured
654
- * @example
655
- * await session.invalidateCachePrefix('tenant:123:');
656
- */
657
- async invalidateCachePrefix(prefix: string): Promise<void> {
658
- if (!this.cacheManager) {
659
- throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
660
- }
661
- await this.cacheManager.invalidatePrefix(prefix);
662
- }
663
-
664
- /**
665
- * Invalidates a specific cache key.
666
- * @param key - Cache key to invalidate
667
- * @throws Error if no cache manager is configured
668
- * @example
669
- * await session.invalidateCacheKey('active_users');
670
- */
671
- async invalidateCacheKey(key: string): Promise<void> {
672
- if (!this.cacheManager) {
673
- throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
674
- }
675
- await this.cacheManager.invalidateKey(key, this.tenantId);
676
- }
677
-
678
- /**
679
- * Merges session defaults with per-call saveGraph options.
680
- * @param options - Per-call saveGraph options
681
- * @returns Combined options with per-call values taking precedence
682
- */
609
+
610
+ /**
611
+ * Gets the execution context.
612
+ * @returns The execution context
613
+ */
614
+ getExecutionContext(): ExecutionContext {
615
+ return {
616
+ dialect: this.orm.dialect,
617
+ executor: this.executor,
618
+ interceptors: this.orm.interceptors
619
+ };
620
+ }
621
+
622
+ /**
623
+ * Gets the hydration context.
624
+ * @returns The hydration context
625
+ */
626
+ getHydrationContext(): HydrationContext<E> {
627
+ return {
628
+ identityMap: this.identityMap,
629
+ unitOfWork: this.unitOfWork,
630
+ domainEvents: this.domainEvents,
631
+ relationChanges: this.relationChanges,
632
+ entityContext: this
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Invalidates cache by specific tags.
638
+ * @param tags - Tags to invalidate
639
+ * @throws Error if no cache manager is configured
640
+ * @example
641
+ * await session.invalidateCacheTags(['users', 'dashboard']);
642
+ */
643
+ async invalidateCacheTags(tags: string[]): Promise<void> {
644
+ if (!this.cacheManager) {
645
+ throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
646
+ }
647
+ await this.cacheManager.invalidateTags(tags);
648
+ }
649
+
650
+ /**
651
+ * Invalidates cache by key prefix (useful for multi-tenancy).
652
+ * @param prefix - Prefix to match cache keys
653
+ * @throws Error if no cache manager is configured
654
+ * @example
655
+ * await session.invalidateCachePrefix('tenant:123:');
656
+ */
657
+ async invalidateCachePrefix(prefix: string): Promise<void> {
658
+ if (!this.cacheManager) {
659
+ throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
660
+ }
661
+ await this.cacheManager.invalidatePrefix(prefix);
662
+ }
663
+
664
+ /**
665
+ * Invalidates a specific cache key.
666
+ * @param key - Cache key to invalidate
667
+ * @throws Error if no cache manager is configured
668
+ * @example
669
+ * await session.invalidateCacheKey('active_users');
670
+ */
671
+ async invalidateCacheKey(key: string): Promise<void> {
672
+ if (!this.cacheManager) {
673
+ throw new Error('No cache manager configured. Please provide cacheManager when creating the session.');
674
+ }
675
+ await this.cacheManager.invalidateKey(key, this.tenantId);
676
+ }
677
+
678
+ /**
679
+ * Merges session defaults with per-call saveGraph options.
680
+ * @param options - Per-call saveGraph options
681
+ * @returns Combined options with per-call values taking precedence
682
+ */
683
683
  private resolveSaveGraphOptions(options?: SaveGraphSessionOptions): SaveGraphSessionOptions {
684
684
  return { ...(this.saveGraphDefaults ?? {}), ...(options ?? {}) };
685
685
  }
@@ -708,20 +708,20 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
708
708
  }
709
709
  }
710
710
  }
711
-
712
- const buildRelationChangeEntry = (
713
- root: unknown,
714
- relationKey: RelationKey,
715
- rootTable: TableDef,
716
- relationName: string,
717
- relation: RelationDef,
718
- change: RelationChange<unknown>
719
- ): RelationChangeEntry => ({
720
- root,
721
- relationKey,
722
- rootTable,
723
- relationName,
724
- relation,
725
- change
726
- });
727
-
711
+
712
+ const buildRelationChangeEntry = (
713
+ root: unknown,
714
+ relationKey: RelationKey,
715
+ rootTable: TableDef,
716
+ relationName: string,
717
+ relation: RelationDef,
718
+ change: RelationChange<unknown>
719
+ ): RelationChangeEntry => ({
720
+ root,
721
+ relationKey,
722
+ rootTable,
723
+ relationName,
724
+ relation,
725
+ change
726
+ });
727
+