metal-orm 1.1.10 → 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 (192) hide show
  1. package/README.md +758 -769
  2. package/dist/index.cjs +106 -43
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +5 -5
  5. package/dist/index.d.ts +5 -5
  6. package/dist/index.js +106 -43
  7. package/dist/index.js.map +1 -1
  8. package/package.json +100 -96
  9. package/scripts/generate-entities/render.mjs +21 -12
  10. package/scripts/generate-entities/schema.mjs +209 -195
  11. package/scripts/generate-entities/tree-detection.mjs +159 -153
  12. package/scripts/inflection/pt-br.mjs +468 -468
  13. package/scripts/run-eslint.mjs +33 -34
  14. package/scripts/show-sql-seed.sql +102 -0
  15. package/scripts/show-sql.mjs +321 -325
  16. package/src/bulk/bulk-delete-executor.ts +3 -5
  17. package/src/bulk/bulk-insert-executor.ts +7 -7
  18. package/src/bulk/bulk-update-executor.ts +2 -2
  19. package/src/bulk/bulk-upsert-executor.ts +5 -7
  20. package/src/codegen/naming-strategy.ts +54 -54
  21. package/src/codegen/typescript.ts +482 -482
  22. package/src/core/ast/adapters.ts +1 -1
  23. package/src/core/ast/builders.ts +123 -123
  24. package/src/core/ast/expression-builders.ts +745 -745
  25. package/src/core/ast/expression-nodes.ts +304 -304
  26. package/src/core/ast/expression-visitor.ts +212 -212
  27. package/src/core/ast/expression.ts +20 -20
  28. package/src/core/ast/helpers.ts +14 -14
  29. package/src/core/ast/join.ts +18 -18
  30. package/src/core/ast/query.ts +200 -200
  31. package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
  32. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
  33. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
  34. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
  35. package/src/core/ddl/dialects/render-reference.test.ts +70 -70
  36. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
  37. package/src/core/ddl/introspect/catalogs/index.ts +5 -5
  38. package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
  39. package/src/core/ddl/introspect/context.ts +15 -15
  40. package/src/core/ddl/introspect/functions/postgres.ts +35 -35
  41. package/src/core/ddl/introspect/mysql.ts +113 -36
  42. package/src/core/ddl/introspect/registry.ts +40 -40
  43. package/src/core/ddl/introspect/run-select.ts +35 -35
  44. package/src/core/ddl/introspect/utils.ts +56 -56
  45. package/src/core/ddl/naming-strategy.ts +16 -16
  46. package/src/core/ddl/schema-dialect.ts +56 -56
  47. package/src/core/ddl/schema-diff.ts +234 -234
  48. package/src/core/ddl/schema-generator.ts +1 -1
  49. package/src/core/ddl/schema-introspect.ts +25 -25
  50. package/src/core/ddl/sql-writing.ts +171 -171
  51. package/src/core/dialect/abstract.ts +541 -541
  52. package/src/core/dialect/base/cte-compiler.ts +33 -33
  53. package/src/core/dialect/base/function-table-formatter.ts +103 -103
  54. package/src/core/dialect/base/groupby-compiler.ts +21 -21
  55. package/src/core/dialect/base/join-compiler.ts +25 -25
  56. package/src/core/dialect/base/orderby-compiler.ts +35 -35
  57. package/src/core/dialect/base/pagination-strategy.ts +32 -32
  58. package/src/core/dialect/base/returning-strategy.ts +57 -57
  59. package/src/core/dialect/base/sql-dialect.ts +294 -294
  60. package/src/core/dialect/dialect-factory.ts +91 -91
  61. package/src/core/dialect/mssql/functions.ts +120 -120
  62. package/src/core/dialect/mssql/index.ts +317 -317
  63. package/src/core/dialect/mysql/functions.ts +103 -103
  64. package/src/core/dialect/mysql/index.ts +105 -105
  65. package/src/core/dialect/postgres/functions.ts +108 -108
  66. package/src/core/dialect/postgres/index.ts +91 -91
  67. package/src/core/dialect/sqlite/functions.ts +129 -129
  68. package/src/core/dialect/sqlite/index.ts +67 -67
  69. package/src/core/driver/database-driver.ts +19 -19
  70. package/src/core/driver/mssql-driver.ts +23 -23
  71. package/src/core/driver/mysql-driver.ts +23 -23
  72. package/src/core/driver/postgres-driver.ts +23 -23
  73. package/src/core/driver/sqlite-driver.ts +23 -23
  74. package/src/core/execution/db-executor.ts +92 -92
  75. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -94
  76. package/src/core/execution/executors/mssql-executor.ts +184 -184
  77. package/src/core/execution/executors/mysql-executor.ts +88 -88
  78. package/src/core/execution/executors/postgres-executor.ts +26 -26
  79. package/src/core/execution/executors/sqlite-executor.ts +42 -42
  80. package/src/core/execution/pooling/pool-types.ts +30 -30
  81. package/src/core/execution/pooling/pool.ts +275 -275
  82. package/src/core/functions/array.ts +35 -35
  83. package/src/core/functions/control-flow.ts +79 -79
  84. package/src/core/functions/datetime.ts +237 -237
  85. package/src/core/functions/json.ts +70 -70
  86. package/src/core/functions/numeric.ts +297 -297
  87. package/src/core/functions/text.ts +334 -334
  88. package/src/core/functions/types.ts +33 -33
  89. package/src/core/hydration/types.ts +51 -51
  90. package/src/core/sql/sql-operator-config.ts +39 -39
  91. package/src/core/sql/sql.ts +152 -152
  92. package/src/decorators/column-decorator.ts +87 -87
  93. package/src/decorators/decorator-metadata.ts +143 -143
  94. package/src/decorators/entity.ts +93 -93
  95. package/src/decorators/index.ts +39 -39
  96. package/src/decorators/relations.ts +231 -231
  97. package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
  98. package/src/decorators/transformers/transformer-decorators.ts +134 -134
  99. package/src/decorators/transformers/transformer-executor.ts +171 -171
  100. package/src/decorators/transformers/transformer-metadata.ts +73 -73
  101. package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
  102. package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
  103. package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
  104. package/src/decorators/validators/country-validator-registry.ts +64 -64
  105. package/src/decorators/validators/country-validators-decorators.ts +203 -203
  106. package/src/decorators/validators/country-validators.ts +105 -105
  107. package/src/decorators/validators/validator-metadata.ts +59 -59
  108. package/src/dto/dto-types.ts +226 -226
  109. package/src/dto/filter-types.ts +141 -141
  110. package/src/dto/index.ts +95 -95
  111. package/src/dto/openapi/generators/base.ts +29 -29
  112. package/src/dto/openapi/generators/column.ts +37 -37
  113. package/src/dto/openapi/generators/dto.ts +91 -91
  114. package/src/dto/openapi/generators/filter.ts +75 -75
  115. package/src/dto/openapi/generators/nested-dto.ts +618 -618
  116. package/src/dto/openapi/generators/pagination.ts +111 -111
  117. package/src/dto/openapi/generators/relation-filter.ts +228 -228
  118. package/src/dto/openapi/index.ts +12 -12
  119. package/src/dto/openapi/types.ts +101 -101
  120. package/src/dto/openapi/utilities.ts +95 -95
  121. package/src/dto/pagination-utils.ts +150 -150
  122. package/src/dto/transform.ts +197 -197
  123. package/src/index.ts +86 -86
  124. package/src/orm/als.ts +58 -58
  125. package/src/orm/domain-event-bus.ts +119 -119
  126. package/src/orm/entity-context.ts +86 -86
  127. package/src/orm/entity-materializer.ts +159 -159
  128. package/src/orm/entity-meta.ts +97 -97
  129. package/src/orm/entity-registry.ts +39 -39
  130. package/src/orm/entity-relation-cache.ts +39 -39
  131. package/src/orm/execute.ts +194 -194
  132. package/src/orm/execution-context.ts +18 -18
  133. package/src/orm/hydration-context.ts +26 -26
  134. package/src/orm/hydration.ts +112 -112
  135. package/src/orm/identity-map.ts +61 -61
  136. package/src/orm/interceptor-pipeline.ts +16 -16
  137. package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
  138. package/src/orm/lazy-batch/belongs-to.ts +108 -108
  139. package/src/orm/lazy-batch/has-many.ts +69 -69
  140. package/src/orm/lazy-batch/has-one.ts +68 -68
  141. package/src/orm/lazy-batch/shared.ts +125 -125
  142. package/src/orm/orm-session.ts +620 -620
  143. package/src/orm/orm.ts +34 -34
  144. package/src/orm/pooled-executor-factory.ts +121 -121
  145. package/src/orm/query-logger.ts +40 -40
  146. package/src/orm/relation-change-processor.ts +420 -420
  147. package/src/orm/relations/belongs-to.ts +143 -143
  148. package/src/orm/relations/has-many.ts +204 -204
  149. package/src/orm/relations/has-one.ts +174 -174
  150. package/src/orm/relations/many-to-many.ts +288 -288
  151. package/src/orm/relations/morph-many.ts +156 -156
  152. package/src/orm/relations/morph-one.ts +151 -151
  153. package/src/orm/relations/morph-to.ts +162 -162
  154. package/src/orm/runtime-types.ts +92 -92
  155. package/src/orm/save-graph.ts +561 -561
  156. package/src/orm/transaction-runner.ts +24 -24
  157. package/src/orm/unit-of-work.ts +426 -426
  158. package/src/query/index.ts +69 -69
  159. package/src/query/target.ts +46 -46
  160. package/src/query-builder/column-selector.ts +81 -81
  161. package/src/query-builder/delete.ts +141 -141
  162. package/src/query-builder/insert-query-state.ts +149 -149
  163. package/src/query-builder/insert.ts +117 -117
  164. package/src/query-builder/query-ast-service.ts +287 -287
  165. package/src/query-builder/query-resolution.ts +78 -78
  166. package/src/query-builder/raw-column-parser.ts +38 -38
  167. package/src/query-builder/relation-filter-utils.ts +153 -153
  168. package/src/query-builder/relation-join-planner.ts +10 -10
  169. package/src/query-builder/relation-manager.ts +87 -87
  170. package/src/query-builder/relation-projection-helper.ts +102 -102
  171. package/src/query-builder/relation-types.ts +9 -9
  172. package/src/query-builder/relation-utils.ts +15 -15
  173. package/src/query-builder/select/cte-facet.ts +40 -40
  174. package/src/query-builder/select/from-facet.ts +80 -80
  175. package/src/query-builder/select/join-facet.ts +62 -62
  176. package/src/query-builder/select/predicate-facet.ts +104 -104
  177. package/src/query-builder/select/projection-facet.ts +70 -70
  178. package/src/query-builder/select/relation-facet.ts +81 -81
  179. package/src/query-builder/select/setop-facet.ts +36 -36
  180. package/src/query-builder/select-helpers.ts +1 -1
  181. package/src/query-builder/select-query-builder-deps.ts +130 -130
  182. package/src/query-builder/select-query-state.ts +207 -207
  183. package/src/query-builder/update.ts +151 -151
  184. package/src/schema/column-types.ts +322 -322
  185. package/src/schema/table-guards.ts +37 -37
  186. package/src/schema/table.ts +281 -281
  187. package/src/tree/index.ts +13 -13
  188. package/src/tree/nested-set-strategy.ts +545 -545
  189. package/src/tree/tree-decorator.ts +397 -397
  190. package/src/tree/tree-manager.ts +754 -754
  191. package/src/tree/tree-query.ts +427 -427
  192. 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
+