metal-orm 1.0.7 → 1.0.9

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 (153) hide show
  1. package/README.md +133 -121
  2. package/dist/decorators/index.cjs +2564 -0
  3. package/dist/decorators/index.cjs.map +1 -0
  4. package/dist/decorators/index.d.cts +53 -0
  5. package/dist/decorators/index.d.ts +53 -0
  6. package/dist/decorators/index.js +2530 -0
  7. package/dist/decorators/index.js.map +1 -0
  8. package/dist/index.cjs +4227 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +701 -0
  11. package/dist/index.d.ts +701 -0
  12. package/dist/index.js +4131 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/select-654m4qy8.d.cts +1522 -0
  15. package/dist/select-654m4qy8.d.ts +1522 -0
  16. package/package.json +27 -20
  17. package/src/codegen/typescript.ts +405 -393
  18. package/src/core/ast/aggregate-functions.ts +30 -0
  19. package/src/core/ast/builders.ts +43 -0
  20. package/src/core/ast/expression-builders.ts +310 -0
  21. package/src/core/ast/expression-nodes.ts +211 -0
  22. package/src/core/ast/expression-visitor.ts +99 -0
  23. package/src/core/ast/expression.ts +5 -0
  24. package/src/{utils → core/ast}/join-node.ts +20 -20
  25. package/src/{ast → core/ast}/join.ts +18 -18
  26. package/src/{ast → core/ast}/query.ts +113 -113
  27. package/src/core/ast/window-functions.ts +140 -0
  28. package/src/{dialect → core/dialect}/abstract.ts +94 -94
  29. package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
  30. package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
  31. package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
  32. package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
  33. package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
  34. package/src/decorators/bootstrap.ts +126 -0
  35. package/src/decorators/column.ts +78 -0
  36. package/src/decorators/entity.ts +36 -0
  37. package/src/decorators/index.ts +4 -0
  38. package/src/decorators/relations.ts +107 -0
  39. package/src/global.d.ts +1 -0
  40. package/src/index.ts +22 -22
  41. package/src/orm/db-executor.ts +11 -0
  42. package/src/orm/domain-event-bus.ts +52 -0
  43. package/src/{runtime → orm}/entity-meta.ts +52 -52
  44. package/src/orm/entity-metadata.ts +140 -0
  45. package/src/{runtime → orm}/entity.ts +252 -252
  46. package/src/{runtime → orm}/execute.ts +36 -36
  47. package/src/{runtime → orm}/hydration.ts +103 -103
  48. package/src/orm/identity-map.ts +37 -0
  49. package/src/{runtime → orm}/lazy-batch.ts +205 -205
  50. package/src/orm/orm-context.ts +154 -0
  51. package/src/orm/relation-change-processor.ts +140 -0
  52. package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
  53. package/src/{runtime → orm}/relations/has-many.ts +111 -111
  54. package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
  55. package/src/orm/runtime-types.ts +39 -0
  56. package/src/orm/transaction-runner.ts +17 -0
  57. package/src/orm/unit-of-work.ts +232 -0
  58. package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
  59. package/src/{builder → query-builder}/delete-query-state.ts +38 -42
  60. package/src/{builder → query-builder}/delete.ts +46 -57
  61. package/src/{builder → query-builder}/hydration-manager.ts +87 -87
  62. package/src/{builder → query-builder}/hydration-planner.ts +182 -182
  63. package/src/{builder → query-builder}/insert-query-state.ts +51 -62
  64. package/src/{builder → query-builder}/insert.ts +48 -59
  65. package/src/{builder → query-builder}/query-ast-service.ts +208 -226
  66. package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
  67. package/src/{builder → query-builder}/relation-conditions.ts +112 -112
  68. package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
  69. package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
  70. package/src/{builder → query-builder}/relation-service.ts +284 -284
  71. package/src/{builder → query-builder}/relation-types.ts +21 -21
  72. package/src/{builder → query-builder}/relation-utils.ts +12 -12
  73. package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
  74. package/src/{builder → query-builder}/select-query-state.ts +179 -179
  75. package/src/{builder → query-builder}/select.ts +78 -69
  76. package/src/{builder → query-builder}/update-query-state.ts +55 -59
  77. package/src/{builder → query-builder}/update.ts +50 -61
  78. package/src/schema/column.ts +25 -25
  79. package/src/schema/relation.ts +116 -116
  80. package/src/schema/table.ts +34 -34
  81. package/src/schema/types.ts +76 -76
  82. package/.github/workflows/publish-metal-orm.yml +0 -38
  83. package/ROADMAP.md +0 -125
  84. package/docs/CHANGES.md +0 -104
  85. package/docs/advanced-features.md +0 -176
  86. package/docs/api-reference.md +0 -31
  87. package/docs/dml-operations.md +0 -156
  88. package/docs/getting-started.md +0 -171
  89. package/docs/hydration.md +0 -115
  90. package/docs/index.md +0 -36
  91. package/docs/multi-dialect-support.md +0 -59
  92. package/docs/query-builder.md +0 -135
  93. package/docs/runtime.md +0 -105
  94. package/docs/schema-definition.md +0 -112
  95. package/metadata.json +0 -5
  96. package/playground/api/playground-api.ts +0 -94
  97. package/playground/index.html +0 -15
  98. package/playground/src/App.css +0 -1
  99. package/playground/src/App.tsx +0 -114
  100. package/playground/src/components/CodeDisplay.tsx +0 -43
  101. package/playground/src/components/QueryExecutor.tsx +0 -189
  102. package/playground/src/components/ResultsTable.tsx +0 -67
  103. package/playground/src/components/ResultsTabs.tsx +0 -105
  104. package/playground/src/components/ScenarioList.tsx +0 -56
  105. package/playground/src/components/logo.svg +0 -45
  106. package/playground/src/data/scenarios.ts +0 -2
  107. package/playground/src/main.tsx +0 -9
  108. package/playground/src/services/PlaygroundApiService.ts +0 -60
  109. package/postcss.config.cjs +0 -5
  110. package/sql_sql-ansi-cheatsheet-2025.md +0 -264
  111. package/src/ast/expression.ts +0 -658
  112. package/src/builder/operations/cte-manager.ts +0 -34
  113. package/src/builder/operations/filter-manager.ts +0 -68
  114. package/src/builder/operations/join-manager.ts +0 -36
  115. package/src/builder/operations/pagination-manager.ts +0 -36
  116. package/src/playground/features/playground/api/types.ts +0 -16
  117. package/src/playground/features/playground/clients/MockClient.ts +0 -17
  118. package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
  119. package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
  120. package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
  121. package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
  122. package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
  123. package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
  124. package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
  125. package/src/playground/features/playground/data/scenarios/index.ts +0 -29
  126. package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
  127. package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
  128. package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
  129. package/src/playground/features/playground/data/scenarios/types.ts +0 -70
  130. package/src/playground/features/playground/data/schema.ts +0 -91
  131. package/src/playground/features/playground/data/seed.ts +0 -104
  132. package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
  133. package/src/runtime/orm-context.ts +0 -539
  134. package/tests/belongs-to-many.test.ts +0 -57
  135. package/tests/between.test.ts +0 -43
  136. package/tests/case-expression.test.ts +0 -58
  137. package/tests/complex-exists.test.ts +0 -230
  138. package/tests/cte.test.ts +0 -118
  139. package/tests/dml.test.ts +0 -206
  140. package/tests/exists.test.ts +0 -127
  141. package/tests/like.test.ts +0 -33
  142. package/tests/orm-runtime.test.ts +0 -254
  143. package/tests/postgres.test.ts +0 -30
  144. package/tests/right-join.test.ts +0 -89
  145. package/tests/subquery-having.test.ts +0 -193
  146. package/tests/window-function.test.ts +0 -151
  147. package/tsconfig.json +0 -30
  148. package/tsup.config.ts +0 -10
  149. package/vite.config.ts +0 -22
  150. package/vitest.config.ts +0 -14
  151. /package/src/{constants → core/sql}/sql.ts +0 -0
  152. /package/src/{runtime → orm}/als.ts +0 -0
  153. /package/src/{utils → query-builder}/relation-alias.ts +0 -0
@@ -1,111 +1,111 @@
1
- import { HasManyCollection } from '../../schema/types';
2
- import { OrmContext, RelationKey } from '../orm-context';
3
- import { HasManyRelation } from '../../schema/relation';
4
- import { TableDef } from '../../schema/table';
5
- import { EntityMeta, getHydrationRows } from '../entity-meta';
6
-
7
- type Rows = Record<string, any>[];
8
-
9
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
-
11
- export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
12
- private loaded = false;
13
- private items: TChild[] = [];
14
- private readonly added = new Set<TChild>();
15
- private readonly removed = new Set<TChild>();
16
-
17
- constructor(
18
- private readonly ctx: OrmContext,
19
- private readonly meta: EntityMeta<any>,
20
- private readonly root: any,
21
- private readonly relationName: string,
22
- private readonly relation: HasManyRelation,
23
- private readonly rootTable: TableDef,
24
- private readonly loader: () => Promise<Map<string, Rows>>,
25
- private readonly createEntity: (row: Record<string, any>) => TChild,
26
- private readonly localKey: string
27
- ) {
28
- this.hydrateFromCache();
29
- }
30
-
31
- async load(): Promise<TChild[]> {
32
- if (this.loaded) return this.items;
33
- const map = await this.loader();
34
- const key = toKey(this.root[this.localKey]);
35
- const rows = map.get(key) ?? [];
36
- this.items = rows.map(row => this.createEntity(row));
37
- this.loaded = true;
38
- return this.items;
39
- }
40
-
41
- getItems(): TChild[] {
42
- return this.items;
43
- }
44
-
45
- add(data: Partial<TChild>): TChild {
46
- const keyValue = this.root[this.localKey];
47
- const childRow: Record<string, any> = {
48
- ...data,
49
- [this.relation.foreignKey]: keyValue
50
- };
51
- const entity = this.createEntity(childRow);
52
- this.added.add(entity);
53
- this.items.push(entity);
54
- this.ctx.registerRelationChange(
55
- this.root,
56
- this.relationKey,
57
- this.rootTable,
58
- this.relationName,
59
- this.relation,
60
- { kind: 'add', entity }
61
- );
62
- return entity;
63
- }
64
-
65
- attach(entity: TChild): void {
66
- const keyValue = this.root[this.localKey];
67
- (entity as Record<string, any>)[this.relation.foreignKey] = keyValue;
68
- this.ctx.markDirty(entity);
69
- this.items.push(entity);
70
- this.ctx.registerRelationChange(
71
- this.root,
72
- this.relationKey,
73
- this.rootTable,
74
- this.relationName,
75
- this.relation,
76
- { kind: 'attach', entity }
77
- );
78
- }
79
-
80
- remove(entity: TChild): void {
81
- this.items = this.items.filter(item => item !== entity);
82
- this.removed.add(entity);
83
- this.ctx.registerRelationChange(
84
- this.root,
85
- this.relationKey,
86
- this.rootTable,
87
- this.relationName,
88
- this.relation,
89
- { kind: 'remove', entity }
90
- );
91
- }
92
-
93
- clear(): void {
94
- for (const entity of [...this.items]) {
95
- this.remove(entity);
96
- }
97
- }
98
-
99
- private get relationKey(): RelationKey {
100
- return `${this.rootTable.name}.${this.relationName}`;
101
- }
102
-
103
- private hydrateFromCache(): void {
104
- const keyValue = this.root[this.localKey];
105
- if (keyValue === undefined || keyValue === null) return;
106
- const rows = getHydrationRows(this.meta, this.relationName, keyValue);
107
- if (!rows?.length) return;
108
- this.items = rows.map(row => this.createEntity(row));
109
- this.loaded = true;
110
- }
111
- }
1
+ import { HasManyCollection } from '../../schema/types.js';
2
+ import { OrmContext, RelationKey } from '../orm-context.js';
3
+ import { HasManyRelation } from '../../schema/relation.js';
4
+ import { TableDef } from '../../schema/table.js';
5
+ import { EntityMeta, getHydrationRows } from '../entity-meta.js';
6
+
7
+ type Rows = Record<string, any>[];
8
+
9
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
10
+
11
+ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
12
+ private loaded = false;
13
+ private items: TChild[] = [];
14
+ private readonly added = new Set<TChild>();
15
+ private readonly removed = new Set<TChild>();
16
+
17
+ constructor(
18
+ private readonly ctx: OrmContext,
19
+ private readonly meta: EntityMeta<any>,
20
+ private readonly root: any,
21
+ private readonly relationName: string,
22
+ private readonly relation: HasManyRelation,
23
+ private readonly rootTable: TableDef,
24
+ private readonly loader: () => Promise<Map<string, Rows>>,
25
+ private readonly createEntity: (row: Record<string, any>) => TChild,
26
+ private readonly localKey: string
27
+ ) {
28
+ this.hydrateFromCache();
29
+ }
30
+
31
+ async load(): Promise<TChild[]> {
32
+ if (this.loaded) return this.items;
33
+ const map = await this.loader();
34
+ const key = toKey(this.root[this.localKey]);
35
+ const rows = map.get(key) ?? [];
36
+ this.items = rows.map(row => this.createEntity(row));
37
+ this.loaded = true;
38
+ return this.items;
39
+ }
40
+
41
+ getItems(): TChild[] {
42
+ return this.items;
43
+ }
44
+
45
+ add(data: Partial<TChild>): TChild {
46
+ const keyValue = this.root[this.localKey];
47
+ const childRow: Record<string, any> = {
48
+ ...data,
49
+ [this.relation.foreignKey]: keyValue
50
+ };
51
+ const entity = this.createEntity(childRow);
52
+ this.added.add(entity);
53
+ this.items.push(entity);
54
+ this.ctx.registerRelationChange(
55
+ this.root,
56
+ this.relationKey,
57
+ this.rootTable,
58
+ this.relationName,
59
+ this.relation,
60
+ { kind: 'add', entity }
61
+ );
62
+ return entity;
63
+ }
64
+
65
+ attach(entity: TChild): void {
66
+ const keyValue = this.root[this.localKey];
67
+ (entity as Record<string, any>)[this.relation.foreignKey] = keyValue;
68
+ this.ctx.markDirty(entity);
69
+ this.items.push(entity);
70
+ this.ctx.registerRelationChange(
71
+ this.root,
72
+ this.relationKey,
73
+ this.rootTable,
74
+ this.relationName,
75
+ this.relation,
76
+ { kind: 'attach', entity }
77
+ );
78
+ }
79
+
80
+ remove(entity: TChild): void {
81
+ this.items = this.items.filter(item => item !== entity);
82
+ this.removed.add(entity);
83
+ this.ctx.registerRelationChange(
84
+ this.root,
85
+ this.relationKey,
86
+ this.rootTable,
87
+ this.relationName,
88
+ this.relation,
89
+ { kind: 'remove', entity }
90
+ );
91
+ }
92
+
93
+ clear(): void {
94
+ for (const entity of [...this.items]) {
95
+ this.remove(entity);
96
+ }
97
+ }
98
+
99
+ private get relationKey(): RelationKey {
100
+ return `${this.rootTable.name}.${this.relationName}`;
101
+ }
102
+
103
+ private hydrateFromCache(): void {
104
+ const keyValue = this.root[this.localKey];
105
+ if (keyValue === undefined || keyValue === null) return;
106
+ const rows = getHydrationRows(this.meta, this.relationName, keyValue);
107
+ if (!rows?.length) return;
108
+ this.items = rows.map(row => this.createEntity(row));
109
+ this.loaded = true;
110
+ }
111
+ }
@@ -1,149 +1,149 @@
1
- import { ManyToManyCollection } from '../../schema/types';
2
- import { OrmContext, RelationKey } from '../orm-context';
3
- import { BelongsToManyRelation } from '../../schema/relation';
4
- import { TableDef } from '../../schema/table';
5
- import { findPrimaryKey } from '../../builder/hydration-planner';
6
- import { EntityMeta, getHydrationRows } from '../entity-meta';
7
-
8
- type Rows = Record<string, any>[];
9
-
10
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
-
12
- export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
13
- private loaded = false;
14
- private items: TTarget[] = [];
15
-
16
- constructor(
17
- private readonly ctx: OrmContext,
18
- private readonly meta: EntityMeta<any>,
19
- private readonly root: any,
20
- private readonly relationName: string,
21
- private readonly relation: BelongsToManyRelation,
22
- private readonly rootTable: TableDef,
23
- private readonly loader: () => Promise<Map<string, Rows>>,
24
- private readonly createEntity: (row: Record<string, any>) => TTarget,
25
- private readonly localKey: string
26
- ) {
27
- this.hydrateFromCache();
28
- }
29
-
30
- async load(): Promise<TTarget[]> {
31
- if (this.loaded) return this.items;
32
- const map = await this.loader();
33
- const key = toKey(this.root[this.localKey]);
34
- const rows = map.get(key) ?? [];
35
- this.items = rows.map(row => {
36
- const entity = this.createEntity(row);
37
- if ((row as any)._pivot) {
38
- (entity as any)._pivot = row._pivot;
39
- }
40
- return entity;
41
- });
42
- this.loaded = true;
43
- return this.items;
44
- }
45
-
46
- getItems(): TTarget[] {
47
- return this.items;
48
- }
49
-
50
- attach(target: TTarget | number | string): void {
51
- const entity = this.ensureEntity(target);
52
- const id = this.extractId(entity);
53
- if (id == null) return;
54
- if (this.items.some(item => this.extractId(item) === id)) {
55
- return;
56
- }
57
- this.items.push(entity);
58
- this.ctx.registerRelationChange(
59
- this.root,
60
- this.relationKey,
61
- this.rootTable,
62
- this.relationName,
63
- this.relation,
64
- { kind: 'attach', entity }
65
- );
66
- }
67
-
68
- detach(target: TTarget | number | string): void {
69
- const id = typeof target === 'number' || typeof target === 'string'
70
- ? target
71
- : this.extractId(target);
72
-
73
- if (id == null) return;
74
-
75
- const existing = this.items.find(item => this.extractId(item) === id);
76
- if (!existing) return;
77
-
78
- this.items = this.items.filter(item => this.extractId(item) !== id);
79
- this.ctx.registerRelationChange(
80
- this.root,
81
- this.relationKey,
82
- this.rootTable,
83
- this.relationName,
84
- this.relation,
85
- { kind: 'detach', entity: existing }
86
- );
87
- }
88
-
89
- async syncByIds(ids: (number | string)[]): Promise<void> {
90
- await this.load();
91
- const targetKey = this.relation.targetKey || findPrimaryKey(this.relation.target);
92
- const normalized = new Set(ids.map(id => toKey(id)));
93
- const currentIds = new Set(this.items.map(item => toKey(this.extractId(item))));
94
-
95
- for (const id of normalized) {
96
- if (!currentIds.has(id)) {
97
- this.attach(id);
98
- }
99
- }
100
-
101
- for (const item of [...this.items]) {
102
- const itemId = toKey(this.extractId(item));
103
- if (!normalized.has(itemId)) {
104
- this.detach(item);
105
- }
106
- }
107
- }
108
-
109
- private ensureEntity(target: TTarget | number | string): TTarget {
110
- if (typeof target === 'number' || typeof target === 'string') {
111
- const stub: Record<string, any> = {
112
- [this.targetKey]: target
113
- };
114
- return this.createEntity(stub);
115
- }
116
- return target;
117
- }
118
-
119
- private extractId(entity: TTarget | number | string | null | undefined): number | string | null {
120
- if (entity === null || entity === undefined) return null;
121
- if (typeof entity === 'number' || typeof entity === 'string') {
122
- return entity;
123
- }
124
- return (entity as any)[this.targetKey] ?? null;
125
- }
126
-
127
- private get relationKey(): RelationKey {
128
- return `${this.rootTable.name}.${this.relationName}`;
129
- }
130
-
131
- private get targetKey(): string {
132
- return this.relation.targetKey || findPrimaryKey(this.relation.target);
133
- }
134
-
135
- private hydrateFromCache(): void {
136
- const keyValue = this.root[this.localKey];
137
- if (keyValue === undefined || keyValue === null) return;
138
- const rows = getHydrationRows(this.meta, this.relationName, keyValue);
139
- if (!rows?.length) return;
140
- this.items = rows.map(row => {
141
- const entity = this.createEntity(row);
142
- if ((row as any)._pivot) {
143
- (entity as any)._pivot = (row as any)._pivot;
144
- }
145
- return entity;
146
- });
147
- this.loaded = true;
148
- }
149
- }
1
+ import { ManyToManyCollection } from '../../schema/types.js';
2
+ import { OrmContext, RelationKey } from '../orm-context.js';
3
+ import { BelongsToManyRelation } from '../../schema/relation.js';
4
+ import { TableDef } from '../../schema/table.js';
5
+ import { findPrimaryKey } from '../../query-builder/hydration-planner.js';
6
+ import { EntityMeta, getHydrationRows } from '../entity-meta.js';
7
+
8
+ type Rows = Record<string, any>[];
9
+
10
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
11
+
12
+ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
13
+ private loaded = false;
14
+ private items: TTarget[] = [];
15
+
16
+ constructor(
17
+ private readonly ctx: OrmContext,
18
+ private readonly meta: EntityMeta<any>,
19
+ private readonly root: any,
20
+ private readonly relationName: string,
21
+ private readonly relation: BelongsToManyRelation,
22
+ private readonly rootTable: TableDef,
23
+ private readonly loader: () => Promise<Map<string, Rows>>,
24
+ private readonly createEntity: (row: Record<string, any>) => TTarget,
25
+ private readonly localKey: string
26
+ ) {
27
+ this.hydrateFromCache();
28
+ }
29
+
30
+ async load(): Promise<TTarget[]> {
31
+ if (this.loaded) return this.items;
32
+ const map = await this.loader();
33
+ const key = toKey(this.root[this.localKey]);
34
+ const rows = map.get(key) ?? [];
35
+ this.items = rows.map(row => {
36
+ const entity = this.createEntity(row);
37
+ if ((row as any)._pivot) {
38
+ (entity as any)._pivot = row._pivot;
39
+ }
40
+ return entity;
41
+ });
42
+ this.loaded = true;
43
+ return this.items;
44
+ }
45
+
46
+ getItems(): TTarget[] {
47
+ return this.items;
48
+ }
49
+
50
+ attach(target: TTarget | number | string): void {
51
+ const entity = this.ensureEntity(target);
52
+ const id = this.extractId(entity);
53
+ if (id == null) return;
54
+ if (this.items.some(item => this.extractId(item) === id)) {
55
+ return;
56
+ }
57
+ this.items.push(entity);
58
+ this.ctx.registerRelationChange(
59
+ this.root,
60
+ this.relationKey,
61
+ this.rootTable,
62
+ this.relationName,
63
+ this.relation,
64
+ { kind: 'attach', entity }
65
+ );
66
+ }
67
+
68
+ detach(target: TTarget | number | string): void {
69
+ const id = typeof target === 'number' || typeof target === 'string'
70
+ ? target
71
+ : this.extractId(target);
72
+
73
+ if (id == null) return;
74
+
75
+ const existing = this.items.find(item => this.extractId(item) === id);
76
+ if (!existing) return;
77
+
78
+ this.items = this.items.filter(item => this.extractId(item) !== id);
79
+ this.ctx.registerRelationChange(
80
+ this.root,
81
+ this.relationKey,
82
+ this.rootTable,
83
+ this.relationName,
84
+ this.relation,
85
+ { kind: 'detach', entity: existing }
86
+ );
87
+ }
88
+
89
+ async syncByIds(ids: (number | string)[]): Promise<void> {
90
+ await this.load();
91
+ const targetKey = this.relation.targetKey || findPrimaryKey(this.relation.target);
92
+ const normalized = new Set(ids.map(id => toKey(id)));
93
+ const currentIds = new Set(this.items.map(item => toKey(this.extractId(item))));
94
+
95
+ for (const id of normalized) {
96
+ if (!currentIds.has(id)) {
97
+ this.attach(id);
98
+ }
99
+ }
100
+
101
+ for (const item of [...this.items]) {
102
+ const itemId = toKey(this.extractId(item));
103
+ if (!normalized.has(itemId)) {
104
+ this.detach(item);
105
+ }
106
+ }
107
+ }
108
+
109
+ private ensureEntity(target: TTarget | number | string): TTarget {
110
+ if (typeof target === 'number' || typeof target === 'string') {
111
+ const stub: Record<string, any> = {
112
+ [this.targetKey]: target
113
+ };
114
+ return this.createEntity(stub);
115
+ }
116
+ return target;
117
+ }
118
+
119
+ private extractId(entity: TTarget | number | string | null | undefined): number | string | null {
120
+ if (entity === null || entity === undefined) return null;
121
+ if (typeof entity === 'number' || typeof entity === 'string') {
122
+ return entity;
123
+ }
124
+ return (entity as any)[this.targetKey] ?? null;
125
+ }
126
+
127
+ private get relationKey(): RelationKey {
128
+ return `${this.rootTable.name}.${this.relationName}`;
129
+ }
130
+
131
+ private get targetKey(): string {
132
+ return this.relation.targetKey || findPrimaryKey(this.relation.target);
133
+ }
134
+
135
+ private hydrateFromCache(): void {
136
+ const keyValue = this.root[this.localKey];
137
+ if (keyValue === undefined || keyValue === null) return;
138
+ const rows = getHydrationRows(this.meta, this.relationName, keyValue);
139
+ if (!rows?.length) return;
140
+ this.items = rows.map(row => {
141
+ const entity = this.createEntity(row);
142
+ if ((row as any)._pivot) {
143
+ (entity as any)._pivot = (row as any)._pivot;
144
+ }
145
+ return entity;
146
+ });
147
+ this.loaded = true;
148
+ }
149
+ }
@@ -0,0 +1,39 @@
1
+ import { RelationDef } from '../schema/relation.js';
2
+ import { TableDef } from '../schema/table.js';
3
+
4
+ export enum EntityStatus {
5
+ New = 'new',
6
+ Managed = 'managed',
7
+ Dirty = 'dirty',
8
+ Removed = 'removed',
9
+ Detached = 'detached'
10
+ }
11
+
12
+ export interface TrackedEntity {
13
+ table: TableDef;
14
+ entity: any;
15
+ pk: string | number | null;
16
+ status: EntityStatus;
17
+ original: Record<string, any> | null;
18
+ }
19
+
20
+ export type RelationKey = string;
21
+
22
+ export type RelationChange<T> =
23
+ | { kind: 'add'; entity: T }
24
+ | { kind: 'attach'; entity: T }
25
+ | { kind: 'remove'; entity: T }
26
+ | { kind: 'detach'; entity: T };
27
+
28
+ export interface RelationChangeEntry {
29
+ root: any;
30
+ relationKey: RelationKey;
31
+ rootTable: TableDef;
32
+ relationName: string;
33
+ relation: RelationDef;
34
+ change: RelationChange<any>;
35
+ }
36
+
37
+ export interface HasDomainEvents {
38
+ domainEvents?: any[];
39
+ }
@@ -0,0 +1,17 @@
1
+ import type { DbExecutor } from './db-executor.js';
2
+
3
+ export const runInTransaction = async (executor: DbExecutor, action: () => Promise<void>): Promise<void> => {
4
+ if (!executor.beginTransaction) {
5
+ await action();
6
+ return;
7
+ }
8
+
9
+ await executor.beginTransaction();
10
+ try {
11
+ await action();
12
+ await executor.commitTransaction?.();
13
+ } catch (error) {
14
+ await executor.rollbackTransaction?.();
15
+ throw error;
16
+ }
17
+ };