metal-orm 1.0.13 → 1.0.15

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 (115) hide show
  1. package/README.md +75 -82
  2. package/dist/decorators/index.cjs +1600 -27
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +6 -2
  5. package/dist/decorators/index.d.ts +6 -2
  6. package/dist/decorators/index.js +1599 -27
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +4608 -3429
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +511 -159
  11. package/dist/index.d.ts +511 -159
  12. package/dist/index.js +4526 -3415
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-CCp1oz9p.d.cts → select-Bkv8g8u_.d.cts} +193 -67
  15. package/dist/{select-CCp1oz9p.d.ts → select-Bkv8g8u_.d.ts} +193 -67
  16. package/package.json +1 -1
  17. package/src/codegen/typescript.ts +38 -35
  18. package/src/core/ast/adapters.ts +21 -0
  19. package/src/core/ast/aggregate-functions.ts +13 -13
  20. package/src/core/ast/builders.ts +56 -43
  21. package/src/core/ast/expression-builders.ts +34 -34
  22. package/src/core/ast/expression-nodes.ts +18 -16
  23. package/src/core/ast/expression-visitor.ts +122 -69
  24. package/src/core/ast/expression.ts +6 -4
  25. package/src/core/ast/join-metadata.ts +15 -0
  26. package/src/core/ast/join-node.ts +22 -20
  27. package/src/core/ast/join.ts +5 -5
  28. package/src/core/ast/query.ts +52 -88
  29. package/src/core/ast/types.ts +20 -0
  30. package/src/core/ast/window-functions.ts +55 -55
  31. package/src/core/ddl/dialects/base-schema-dialect.ts +20 -6
  32. package/src/core/ddl/dialects/mssql-schema-dialect.ts +32 -8
  33. package/src/core/ddl/dialects/mysql-schema-dialect.ts +21 -10
  34. package/src/core/ddl/dialects/postgres-schema-dialect.ts +52 -7
  35. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +23 -9
  36. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  37. package/src/core/ddl/introspect/catalogs/postgres.ts +143 -0
  38. package/src/core/ddl/introspect/context.ts +9 -0
  39. package/src/core/ddl/introspect/functions/postgres.ts +26 -0
  40. package/src/core/ddl/introspect/mssql.ts +149 -149
  41. package/src/core/ddl/introspect/mysql.ts +99 -99
  42. package/src/core/ddl/introspect/postgres.ts +245 -154
  43. package/src/core/ddl/introspect/registry.ts +26 -0
  44. package/src/core/ddl/introspect/run-select.ts +25 -0
  45. package/src/core/ddl/introspect/sqlite.ts +7 -7
  46. package/src/core/ddl/introspect/types.ts +23 -19
  47. package/src/core/ddl/introspect/utils.ts +1 -1
  48. package/src/core/ddl/naming-strategy.ts +10 -0
  49. package/src/core/ddl/schema-dialect.ts +41 -0
  50. package/src/core/ddl/schema-diff.ts +211 -179
  51. package/src/core/ddl/schema-generator.ts +16 -90
  52. package/src/core/ddl/schema-introspect.ts +25 -32
  53. package/src/core/ddl/schema-plan-executor.ts +17 -0
  54. package/src/core/ddl/schema-types.ts +46 -39
  55. package/src/core/ddl/sql-writing.ts +170 -0
  56. package/src/core/dialect/abstract.ts +144 -126
  57. package/src/core/dialect/base/cte-compiler.ts +33 -0
  58. package/src/core/dialect/base/function-table-formatter.ts +132 -0
  59. package/src/core/dialect/base/groupby-compiler.ts +21 -0
  60. package/src/core/dialect/base/join-compiler.ts +26 -0
  61. package/src/core/dialect/base/orderby-compiler.ts +21 -0
  62. package/src/core/dialect/base/pagination-strategy.ts +32 -0
  63. package/src/core/dialect/base/returning-strategy.ts +56 -0
  64. package/src/core/dialect/base/sql-dialect.ts +181 -204
  65. package/src/core/dialect/dialect-factory.ts +91 -0
  66. package/src/core/dialect/mssql/functions.ts +101 -0
  67. package/src/core/dialect/mssql/index.ts +128 -126
  68. package/src/core/dialect/mysql/functions.ts +101 -0
  69. package/src/core/dialect/mysql/index.ts +20 -18
  70. package/src/core/dialect/postgres/functions.ts +95 -0
  71. package/src/core/dialect/postgres/index.ts +30 -28
  72. package/src/core/dialect/sqlite/functions.ts +115 -0
  73. package/src/core/dialect/sqlite/index.ts +30 -28
  74. package/src/core/driver/database-driver.ts +11 -0
  75. package/src/core/driver/mssql-driver.ts +20 -0
  76. package/src/core/driver/mysql-driver.ts +20 -0
  77. package/src/core/driver/postgres-driver.ts +20 -0
  78. package/src/core/driver/sqlite-driver.ts +20 -0
  79. package/src/core/execution/db-executor.ts +63 -0
  80. package/src/core/execution/executors/mssql-executor.ts +39 -0
  81. package/src/core/execution/executors/mysql-executor.ts +47 -0
  82. package/src/core/execution/executors/postgres-executor.ts +32 -0
  83. package/src/core/execution/executors/sqlite-executor.ts +31 -0
  84. package/src/core/functions/datetime.ts +132 -0
  85. package/src/core/functions/numeric.ts +179 -0
  86. package/src/core/functions/standard-strategy.ts +47 -0
  87. package/src/core/functions/text.ts +147 -0
  88. package/src/core/functions/types.ts +18 -0
  89. package/src/core/hydration/types.ts +57 -0
  90. package/src/decorators/bootstrap.ts +10 -0
  91. package/src/decorators/relations.ts +15 -0
  92. package/src/index.ts +30 -19
  93. package/src/orm/entity-metadata.ts +7 -0
  94. package/src/orm/entity.ts +58 -27
  95. package/src/orm/hydration.ts +25 -17
  96. package/src/orm/lazy-batch.ts +46 -2
  97. package/src/orm/orm-context.ts +60 -60
  98. package/src/orm/query-logger.ts +1 -1
  99. package/src/orm/relation-change-processor.ts +43 -2
  100. package/src/orm/relations/has-one.ts +139 -0
  101. package/src/orm/transaction-runner.ts +1 -1
  102. package/src/orm/unit-of-work.ts +60 -60
  103. package/src/query-builder/delete.ts +22 -5
  104. package/src/query-builder/hydration-manager.ts +2 -1
  105. package/src/query-builder/hydration-planner.ts +8 -7
  106. package/src/query-builder/insert.ts +22 -5
  107. package/src/query-builder/relation-conditions.ts +9 -8
  108. package/src/query-builder/relation-service.ts +3 -2
  109. package/src/query-builder/select.ts +66 -61
  110. package/src/query-builder/update.ts +22 -5
  111. package/src/schema/column.ts +246 -246
  112. package/src/schema/relation.ts +35 -1
  113. package/src/schema/table.ts +28 -28
  114. package/src/schema/types.ts +41 -31
  115. package/src/orm/db-executor.ts +0 -11
package/src/orm/entity.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { Entity, RelationMap, HasManyCollection, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
3
- import { OrmContext } from './orm-context.js';
4
- import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
5
- import { DefaultHasManyCollection } from './relations/has-many.js';
6
- import { DefaultBelongsToReference } from './relations/belongs-to.js';
7
- import { DefaultManyToManyCollection } from './relations/many-to-many.js';
8
- import { HasManyRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
9
- import { loadHasManyRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
2
+ import { Entity, RelationMap, HasManyCollection, HasOneReference, BelongsToReference, ManyToManyCollection } from '../schema/types.js';
3
+ import { OrmContext } from './orm-context.js';
4
+ import { ENTITY_META, EntityMeta, getEntityMeta } from './entity-meta.js';
5
+ import { DefaultHasManyCollection } from './relations/has-many.js';
6
+ import { DefaultHasOneReference } from './relations/has-one.js';
7
+ import { DefaultBelongsToReference } from './relations/belongs-to.js';
8
+ import { DefaultManyToManyCollection } from './relations/many-to-many.js';
9
+ import { HasManyRelation, HasOneRelation, BelongsToRelation, BelongsToManyRelation, RelationKinds } from '../schema/relation.js';
10
+ import { loadHasManyRelation, loadHasOneRelation, loadBelongsToRelation, loadBelongsToManyRelation } from './lazy-batch.js';
10
11
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
11
12
 
12
13
  type Rows = Record<string, any>[];
@@ -135,10 +136,22 @@ const toKey = (value: unknown): string => (value === null || value === undefined
135
136
  ): void => {
136
137
  for (const relationName of Object.keys(meta.table.relations)) {
137
138
  const relation = meta.table.relations[relationName];
138
- const data = row[relationName];
139
- if (!Array.isArray(data)) continue;
140
-
141
- if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
139
+ const data = row[relationName];
140
+ if (relation.type === RelationKinds.HasOne) {
141
+ const localKey = relation.localKey || findPrimaryKey(meta.table);
142
+ const rootValue = entity[localKey];
143
+ if (rootValue === undefined || rootValue === null) continue;
144
+ if (!data || typeof data !== 'object') continue;
145
+ const cache = new Map<string, Record<string, any>>();
146
+ cache.set(toKey(rootValue), data as Record<string, any>);
147
+ meta.relationHydration.set(relationName, cache);
148
+ meta.relationCache.set(relationName, Promise.resolve(cache));
149
+ continue;
150
+ }
151
+
152
+ if (!Array.isArray(data)) continue;
153
+
154
+ if (relation.type === RelationKinds.HasMany || relation.type === RelationKinds.BelongsToMany) {
142
155
  const localKey = relation.localKey || findPrimaryKey(meta.table);
143
156
  const rootValue = entity[localKey];
144
157
  if (rootValue === undefined || rootValue === null) continue;
@@ -165,11 +178,11 @@ const toKey = (value: unknown): string => (value === null || value === undefined
165
178
  }
166
179
  };
167
180
 
168
- const getRelationWrapper = (
169
- meta: EntityMeta<any>,
170
- relationName: string,
171
- owner: any
172
- ): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
181
+ const getRelationWrapper = (
182
+ meta: EntityMeta<any>,
183
+ relationName: string,
184
+ owner: any
185
+ ): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
173
186
  if (meta.relationWrappers.has(relationName)) {
174
187
  return meta.relationWrappers.get(relationName) as HasManyCollection<any>;
175
188
  }
@@ -185,16 +198,34 @@ const getRelationWrapper = (
185
198
  return wrapper;
186
199
  };
187
200
 
188
- const instantiateWrapper = (
189
- meta: EntityMeta<any>,
190
- relationName: string,
191
- relation: HasManyRelation | BelongsToRelation | BelongsToManyRelation,
192
- owner: any
193
- ): HasManyCollection<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
194
- switch (relation.type) {
195
- case RelationKinds.HasMany: {
196
- const hasMany = relation as HasManyRelation;
197
- const localKey = hasMany.localKey || findPrimaryKey(meta.table);
201
+ const instantiateWrapper = (
202
+ meta: EntityMeta<any>,
203
+ relationName: string,
204
+ relation: HasManyRelation | HasOneRelation | BelongsToRelation | BelongsToManyRelation,
205
+ owner: any
206
+ ): HasManyCollection<any> | HasOneReference<any> | BelongsToReference<any> | ManyToManyCollection<any> | undefined => {
207
+ switch (relation.type) {
208
+ case RelationKinds.HasOne: {
209
+ const hasOne = relation as HasOneRelation;
210
+ const localKey = hasOne.localKey || findPrimaryKey(meta.table);
211
+ const loader = () => relationLoaderCache(meta, relationName, () =>
212
+ loadHasOneRelation(meta.ctx, meta.table, relationName, hasOne)
213
+ );
214
+ return new DefaultHasOneReference(
215
+ meta.ctx,
216
+ meta,
217
+ owner,
218
+ relationName,
219
+ hasOne,
220
+ meta.table,
221
+ loader,
222
+ (row: Record<string, any>) => createEntityFromRow(meta.ctx, hasOne.target, row),
223
+ localKey
224
+ );
225
+ }
226
+ case RelationKinds.HasMany: {
227
+ const hasMany = relation as HasManyRelation;
228
+ const localKey = hasMany.localKey || findPrimaryKey(meta.table);
198
229
  const loader = () => relationLoaderCache(meta, relationName, () =>
199
230
  loadHasManyRelation(meta.ctx, meta.table, relationName, hasMany)
200
231
  );
@@ -1,5 +1,6 @@
1
- import { HydrationPlan, HydrationRelationPlan } from '../core/ast/query.js';
2
- import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
1
+ import { HydrationPlan, HydrationRelationPlan } from '../core/hydration/types.js';
2
+ import { RelationKinds } from '../schema/relation.js';
3
+ import { isRelationAlias, makeRelationAlias } from '../query-builder/relation-alias.js';
3
4
 
4
5
  /**
5
6
  * Hydrates query results according to a hydration plan
@@ -47,18 +48,25 @@ export const hydrateRows = (rows: Record<string, any>[], plan?: HydrationPlan):
47
48
  const parent = getOrCreateParent(row);
48
49
  if (!parent) continue;
49
50
 
50
- for (const rel of plan.relations) {
51
- const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
52
- const childPk = row[childPkKey];
53
- if (childPk === null || childPk === undefined) continue;
54
-
55
- const seen = getRelationSeenSet(rootId, rel.name);
56
- if (seen.has(childPk)) continue;
57
- seen.add(childPk);
58
-
59
- const bucket = parent[rel.name] as any[];
60
- bucket.push(buildChild(row, rel));
61
- }
51
+ for (const rel of plan.relations) {
52
+ const childPkKey = makeRelationAlias(rel.aliasPrefix, rel.targetPrimaryKey);
53
+ const childPk = row[childPkKey];
54
+ if (childPk === null || childPk === undefined) continue;
55
+
56
+ const seen = getRelationSeenSet(rootId, rel.name);
57
+ if (seen.has(childPk)) continue;
58
+ seen.add(childPk);
59
+
60
+ if (rel.type === RelationKinds.HasOne) {
61
+ if (!parent[rel.name]) {
62
+ parent[rel.name] = buildChild(row, rel);
63
+ }
64
+ continue;
65
+ }
66
+
67
+ const bucket = parent[rel.name] as any[];
68
+ bucket.push(buildChild(row, rel));
69
+ }
62
70
  }
63
71
 
64
72
  return Array.from(rootMap.values());
@@ -74,9 +82,9 @@ const createBaseRow = (row: Record<string, any>, plan: HydrationPlan): Record<st
74
82
  base[key] = row[key];
75
83
  }
76
84
 
77
- for (const rel of plan.relations) {
78
- base[rel.name] = [];
79
- }
85
+ for (const rel of plan.relations) {
86
+ base[rel.name] = rel.type === RelationKinds.HasOne ? null : [];
87
+ }
80
88
 
81
89
  return base;
82
90
  };
@@ -1,8 +1,9 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { BelongsToManyRelation, HasManyRelation, BelongsToRelation } from '../schema/relation.js';
2
+ import { BelongsToManyRelation, HasManyRelation, HasOneRelation, BelongsToRelation } from '../schema/relation.js';
3
3
  import { SelectQueryBuilder } from '../query-builder/select.js';
4
4
  import { inList, LiteralNode } from '../core/ast/expression.js';
5
- import { OrmContext, QueryResult } from './orm-context.js';
5
+ import { OrmContext } from './orm-context.js';
6
+ import type { QueryResult } from '../core/execution/db-executor.js';
6
7
  import { ColumnDef } from '../schema/column.js';
7
8
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
9
 
@@ -80,6 +81,49 @@ export const loadHasManyRelation = async (
80
81
  return grouped;
81
82
  };
82
83
 
84
+ export const loadHasOneRelation = async (
85
+ ctx: OrmContext,
86
+ rootTable: TableDef,
87
+ _relationName: string,
88
+ relation: HasOneRelation
89
+ ): Promise<Map<string, Record<string, any>>> => {
90
+ const localKey = relation.localKey || findPrimaryKey(rootTable);
91
+ const roots = ctx.getEntitiesForTable(rootTable);
92
+ const keys = new Set<unknown>();
93
+
94
+ for (const tracked of roots) {
95
+ const value = tracked.entity[localKey];
96
+ if (value !== null && value !== undefined) {
97
+ keys.add(value);
98
+ }
99
+ }
100
+
101
+ if (!keys.size) {
102
+ return new Map();
103
+ }
104
+
105
+ const selectMap = selectAllColumns(relation.target);
106
+ const qb = new SelectQueryBuilder(relation.target).select(selectMap);
107
+ const fkColumn = relation.target.columns[relation.foreignKey];
108
+ if (!fkColumn) return new Map();
109
+
110
+ qb.where(inList(fkColumn, Array.from(keys) as (string | number | LiteralNode)[]));
111
+
112
+ const rows = await executeQuery(ctx, qb);
113
+ const lookup = new Map<string, Record<string, any>>();
114
+
115
+ for (const row of rows) {
116
+ const fkValue = row[relation.foreignKey];
117
+ if (fkValue === null || fkValue === undefined) continue;
118
+ const key = toKey(fkValue);
119
+ if (!lookup.has(key)) {
120
+ lookup.set(key, row);
121
+ }
122
+ }
123
+
124
+ return lookup;
125
+ };
126
+
83
127
  export const loadBelongsToRelation = async (
84
128
  ctx: OrmContext,
85
129
  rootTable: TableDef,
@@ -1,21 +1,21 @@
1
1
  import type { Dialect } from '../core/dialect/abstract.js';
2
2
  import type { RelationDef } from '../schema/relation.js';
3
3
  import type { TableDef } from '../schema/table.js';
4
- import type { DbExecutor, QueryResult } from './db-executor.js';
5
- import { DomainEventBus, DomainEventHandler as DomainEventHandlerFn, addDomainEvent } from './domain-event-bus.js';
6
- import { IdentityMap } from './identity-map.js';
7
- import { RelationChangeProcessor } from './relation-change-processor.js';
8
- import { runInTransaction } from './transaction-runner.js';
9
- import { UnitOfWork } from './unit-of-work.js';
10
- import {
11
- EntityStatus,
12
- HasDomainEvents,
13
- RelationChange,
14
- RelationChangeEntry,
15
- RelationKey,
16
- TrackedEntity
17
- } from './runtime-types.js';
18
- import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
4
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
5
+ import { DomainEventBus, DomainEventHandler as DomainEventHandlerFn, addDomainEvent } from './domain-event-bus.js';
6
+ import { IdentityMap } from './identity-map.js';
7
+ import { RelationChangeProcessor } from './relation-change-processor.js';
8
+ import { runInTransaction } from './transaction-runner.js';
9
+ import { UnitOfWork } from './unit-of-work.js';
10
+ import {
11
+ EntityStatus,
12
+ HasDomainEvents,
13
+ RelationChange,
14
+ RelationChangeEntry,
15
+ RelationKey,
16
+ TrackedEntity
17
+ } from './runtime-types.js';
18
+ import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
19
19
 
20
20
  export interface OrmInterceptor {
21
21
  beforeFlush?(ctx: OrmContext): Promise<void> | void;
@@ -24,46 +24,46 @@ export interface OrmInterceptor {
24
24
 
25
25
  export type DomainEventHandler = DomainEventHandlerFn<OrmContext>;
26
26
 
27
- export interface OrmContextOptions {
28
- dialect: Dialect;
29
- executor: DbExecutor;
30
- interceptors?: OrmInterceptor[];
31
- domainEventHandlers?: Record<string, DomainEventHandler[]>;
32
- queryLogger?: QueryLogger;
33
- }
34
-
35
- export class OrmContext {
36
- private readonly identityMap = new IdentityMap();
37
- private readonly executorWithLogging: DbExecutor;
38
- private readonly unitOfWork: UnitOfWork;
39
- private readonly relationChanges: RelationChangeProcessor;
40
- private readonly interceptors: OrmInterceptor[];
41
- private readonly domainEvents: DomainEventBus<OrmContext>;
42
-
43
- constructor(private readonly options: OrmContextOptions) {
44
- this.interceptors = [...(options.interceptors ?? [])];
45
- this.executorWithLogging = createQueryLoggingExecutor(options.executor, options.queryLogger);
46
- this.unitOfWork = new UnitOfWork(
47
- options.dialect,
48
- this.executorWithLogging,
49
- this.identityMap,
50
- () => this
51
- );
52
- this.relationChanges = new RelationChangeProcessor(
53
- this.unitOfWork,
54
- options.dialect,
55
- this.executorWithLogging
56
- );
57
- this.domainEvents = new DomainEventBus<OrmContext>(options.domainEventHandlers);
58
- }
27
+ export interface OrmContextOptions {
28
+ dialect: Dialect;
29
+ executor: DbExecutor;
30
+ interceptors?: OrmInterceptor[];
31
+ domainEventHandlers?: Record<string, DomainEventHandler[]>;
32
+ queryLogger?: QueryLogger;
33
+ }
34
+
35
+ export class OrmContext {
36
+ private readonly identityMap = new IdentityMap();
37
+ private readonly executorWithLogging: DbExecutor;
38
+ private readonly unitOfWork: UnitOfWork;
39
+ private readonly relationChanges: RelationChangeProcessor;
40
+ private readonly interceptors: OrmInterceptor[];
41
+ private readonly domainEvents: DomainEventBus<OrmContext>;
42
+
43
+ constructor(private readonly options: OrmContextOptions) {
44
+ this.interceptors = [...(options.interceptors ?? [])];
45
+ this.executorWithLogging = createQueryLoggingExecutor(options.executor, options.queryLogger);
46
+ this.unitOfWork = new UnitOfWork(
47
+ options.dialect,
48
+ this.executorWithLogging,
49
+ this.identityMap,
50
+ () => this
51
+ );
52
+ this.relationChanges = new RelationChangeProcessor(
53
+ this.unitOfWork,
54
+ options.dialect,
55
+ this.executorWithLogging
56
+ );
57
+ this.domainEvents = new DomainEventBus<OrmContext>(options.domainEventHandlers);
58
+ }
59
59
 
60
60
  get dialect(): Dialect {
61
61
  return this.options.dialect;
62
62
  }
63
63
 
64
- get executor(): DbExecutor {
65
- return this.executorWithLogging;
66
- }
64
+ get executor(): DbExecutor {
65
+ return this.executorWithLogging;
66
+ }
67
67
 
68
68
  get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
69
69
  return this.unitOfWork.identityBuckets;
@@ -147,13 +147,13 @@ export class OrmContext {
147
147
  }
148
148
  }
149
149
 
150
- export { addDomainEvent };
151
- export { EntityStatus };
152
- export type {
153
- QueryResult,
154
- DbExecutor,
155
- RelationKey,
156
- RelationChange,
157
- HasDomainEvents
158
- };
159
- export type { QueryLogEntry, QueryLogger } from './query-logger.js';
150
+ export { addDomainEvent };
151
+ export { EntityStatus };
152
+ export type {
153
+ QueryResult,
154
+ DbExecutor,
155
+ RelationKey,
156
+ RelationChange,
157
+ HasDomainEvents
158
+ };
159
+ export type { QueryLogEntry, QueryLogger } from './query-logger.js';
@@ -1,4 +1,4 @@
1
- import type { DbExecutor } from './db-executor.js';
1
+ import type { DbExecutor } from '../core/execution/db-executor.js';
2
2
 
3
3
  export interface QueryLogEntry {
4
4
  sql: string;
@@ -3,10 +3,10 @@ import type { Dialect } from '../core/dialect/abstract.js';
3
3
  import { DeleteQueryBuilder } from '../query-builder/delete.js';
4
4
  import { InsertQueryBuilder } from '../query-builder/insert.js';
5
5
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
- import type { BelongsToManyRelation, HasManyRelation } from '../schema/relation.js';
6
+ import type { BelongsToManyRelation, HasManyRelation, HasOneRelation } from '../schema/relation.js';
7
7
  import { RelationKinds } from '../schema/relation.js';
8
8
  import type { TableDef } from '../schema/table.js';
9
- import type { DbExecutor } from './db-executor.js';
9
+ import type { DbExecutor } from '../core/execution/db-executor.js';
10
10
  import type { RelationChangeEntry } from './runtime-types.js';
11
11
  import { UnitOfWork } from './unit-of-work.js';
12
12
 
@@ -33,6 +33,9 @@ export class RelationChangeProcessor {
33
33
  case RelationKinds.HasMany:
34
34
  await this.handleHasManyChange(entry);
35
35
  break;
36
+ case RelationKinds.HasOne:
37
+ await this.handleHasOneChange(entry);
38
+ break;
36
39
  case RelationKinds.BelongsToMany:
37
40
  await this.handleBelongsToManyChange(entry);
38
41
  break;
@@ -66,6 +69,29 @@ export class RelationChangeProcessor {
66
69
  }
67
70
  }
68
71
 
72
+ private async handleHasOneChange(entry: RelationChangeEntry): Promise<void> {
73
+ const relation = entry.relation as HasOneRelation;
74
+ const target = entry.change.entity;
75
+ if (!target) return;
76
+
77
+ const tracked = this.unitOfWork.findTracked(target);
78
+ if (!tracked) return;
79
+
80
+ const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
81
+ const rootValue = entry.root[localKey];
82
+ if (rootValue === undefined || rootValue === null) return;
83
+
84
+ if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
85
+ this.assignHasOneForeignKey(tracked.entity, relation, rootValue);
86
+ this.unitOfWork.markDirty(tracked.entity);
87
+ return;
88
+ }
89
+
90
+ if (entry.change.kind === 'remove') {
91
+ this.detachHasOneChild(tracked.entity, relation);
92
+ }
93
+ }
94
+
69
95
  private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
70
96
  // Reserved for future cascade/persist behaviors for belongs-to relations.
71
97
  }
@@ -108,6 +134,21 @@ export class RelationChangeProcessor {
108
134
  this.unitOfWork.markDirty(child);
109
135
  }
110
136
 
137
+ private assignHasOneForeignKey(child: any, relation: HasOneRelation, rootValue: unknown): void {
138
+ const current = child[relation.foreignKey];
139
+ if (current === rootValue) return;
140
+ child[relation.foreignKey] = rootValue;
141
+ }
142
+
143
+ private detachHasOneChild(child: any, relation: HasOneRelation): void {
144
+ if (relation.cascade === 'all' || relation.cascade === 'remove') {
145
+ this.unitOfWork.markRemoved(child);
146
+ return;
147
+ }
148
+ child[relation.foreignKey] = null;
149
+ this.unitOfWork.markDirty(child);
150
+ }
151
+
111
152
  private async insertPivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
112
153
  const payload = {
113
154
  [relation.pivotForeignKeyToRoot]: rootId,
@@ -0,0 +1,139 @@
1
+ import { HasOneReference } from '../../schema/types.js';
2
+ import { OrmContext, RelationKey } from '../orm-context.js';
3
+ import { HasOneRelation } from '../../schema/relation.js';
4
+ import { TableDef } from '../../schema/table.js';
5
+ import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
6
+
7
+ type Row = Record<string, any>;
8
+
9
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
+
11
+ const hideInternal = (obj: any, keys: string[]): void => {
12
+ for (const key of keys) {
13
+ Object.defineProperty(obj, key, {
14
+ value: obj[key],
15
+ writable: false,
16
+ configurable: false,
17
+ enumerable: false
18
+ });
19
+ }
20
+ };
21
+
22
+ export class DefaultHasOneReference<TChild> implements HasOneReference<TChild> {
23
+ private loaded = false;
24
+ private current: TChild | null = null;
25
+
26
+ constructor(
27
+ private readonly ctx: OrmContext,
28
+ private readonly meta: EntityMeta<any>,
29
+ private readonly root: any,
30
+ private readonly relationName: string,
31
+ private readonly relation: HasOneRelation,
32
+ private readonly rootTable: TableDef,
33
+ private readonly loader: () => Promise<Map<string, Row>>,
34
+ private readonly createEntity: (row: Row) => TChild,
35
+ private readonly localKey: string
36
+ ) {
37
+ hideInternal(this, [
38
+ 'ctx',
39
+ 'meta',
40
+ 'root',
41
+ 'relationName',
42
+ 'relation',
43
+ 'rootTable',
44
+ 'loader',
45
+ 'createEntity',
46
+ 'localKey'
47
+ ]);
48
+ this.populateFromHydrationCache();
49
+ }
50
+
51
+ async load(): Promise<TChild | null> {
52
+ if (this.loaded) return this.current;
53
+ const map = await this.loader();
54
+ const keyValue = this.root[this.localKey];
55
+ if (keyValue === undefined || keyValue === null) {
56
+ this.loaded = true;
57
+ return this.current;
58
+ }
59
+ const row = map.get(toKey(keyValue));
60
+ this.current = row ? this.createEntity(row) : null;
61
+ this.loaded = true;
62
+ return this.current;
63
+ }
64
+
65
+ get(): TChild | null {
66
+ return this.current;
67
+ }
68
+
69
+ set(data: Partial<TChild> | TChild | null): TChild | null {
70
+ if (data === null) {
71
+ return this.detachCurrent();
72
+ }
73
+
74
+ const entity = hasEntityMeta(data) ? (data as TChild) : this.createEntity(data as Row);
75
+ if (this.current && this.current !== entity) {
76
+ this.ctx.registerRelationChange(
77
+ this.root,
78
+ this.relationKey,
79
+ this.rootTable,
80
+ this.relationName,
81
+ this.relation,
82
+ { kind: 'remove', entity: this.current }
83
+ );
84
+ }
85
+
86
+ this.assignForeignKey(entity);
87
+ this.current = entity;
88
+ this.loaded = true;
89
+
90
+ this.ctx.registerRelationChange(
91
+ this.root,
92
+ this.relationKey,
93
+ this.rootTable,
94
+ this.relationName,
95
+ this.relation,
96
+ { kind: 'attach', entity }
97
+ );
98
+
99
+ return entity;
100
+ }
101
+
102
+ toJSON(): TChild | null {
103
+ return this.current;
104
+ }
105
+
106
+ private detachCurrent(): TChild | null {
107
+ const previous = this.current;
108
+ if (!previous) return null;
109
+ this.current = null;
110
+ this.loaded = true;
111
+ this.ctx.registerRelationChange(
112
+ this.root,
113
+ this.relationKey,
114
+ this.rootTable,
115
+ this.relationName,
116
+ this.relation,
117
+ { kind: 'remove', entity: previous }
118
+ );
119
+ return null;
120
+ }
121
+
122
+ private assignForeignKey(entity: TChild): void {
123
+ const keyValue = this.root[this.localKey];
124
+ (entity as Row)[this.relation.foreignKey] = keyValue;
125
+ }
126
+
127
+ private get relationKey(): RelationKey {
128
+ return `${this.rootTable.name}.${this.relationName}`;
129
+ }
130
+
131
+ private populateFromHydrationCache(): void {
132
+ const keyValue = this.root[this.localKey];
133
+ if (keyValue === undefined || keyValue === null) return;
134
+ const row = getHydrationRecord(this.meta, this.relationName, keyValue);
135
+ if (!row) return;
136
+ this.current = this.createEntity(row);
137
+ this.loaded = true;
138
+ }
139
+ }
@@ -1,4 +1,4 @@
1
- import type { DbExecutor } from './db-executor.js';
1
+ import type { DbExecutor } from '../core/execution/db-executor.js';
2
2
 
3
3
  export const runInTransaction = async (executor: DbExecutor, action: () => Promise<void>): Promise<void> => {
4
4
  if (!executor.beginTransaction) {