metal-orm 1.0.8 → 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.
- package/README.md +12 -1
- package/dist/decorators/index.cjs +2564 -0
- package/dist/decorators/index.cjs.map +1 -0
- package/dist/decorators/index.d.cts +53 -0
- package/dist/decorators/index.d.ts +53 -0
- package/dist/decorators/index.js +2530 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +701 -0
- package/dist/index.d.ts +701 -0
- package/dist/index.js +4131 -0
- package/dist/index.js.map +1 -0
- package/dist/select-654m4qy8.d.cts +1522 -0
- package/dist/select-654m4qy8.d.ts +1522 -0
- package/package.json +27 -20
- package/src/codegen/typescript.ts +405 -393
- package/src/core/ast/aggregate-functions.ts +30 -0
- package/src/core/ast/builders.ts +43 -0
- package/src/core/ast/expression-builders.ts +310 -0
- package/src/core/ast/expression-nodes.ts +211 -0
- package/src/core/ast/expression-visitor.ts +99 -0
- package/src/core/ast/expression.ts +5 -0
- package/src/{utils → core/ast}/join-node.ts +20 -20
- package/src/{ast → core/ast}/join.ts +18 -18
- package/src/{ast → core/ast}/query.ts +113 -113
- package/src/core/ast/window-functions.ts +140 -0
- package/src/{dialect → core/dialect}/abstract.ts +94 -94
- package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
- package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
- package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
- package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
- package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
- package/src/decorators/bootstrap.ts +126 -0
- package/src/decorators/column.ts +78 -0
- package/src/decorators/entity.ts +36 -0
- package/src/decorators/index.ts +4 -0
- package/src/decorators/relations.ts +107 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +22 -22
- package/src/orm/db-executor.ts +11 -0
- package/src/orm/domain-event-bus.ts +52 -0
- package/src/{runtime → orm}/entity-meta.ts +52 -52
- package/src/orm/entity-metadata.ts +140 -0
- package/src/{runtime → orm}/entity.ts +252 -252
- package/src/{runtime → orm}/execute.ts +36 -36
- package/src/{runtime → orm}/hydration.ts +103 -103
- package/src/orm/identity-map.ts +37 -0
- package/src/{runtime → orm}/lazy-batch.ts +205 -205
- package/src/orm/orm-context.ts +154 -0
- package/src/orm/relation-change-processor.ts +140 -0
- package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
- package/src/{runtime → orm}/relations/has-many.ts +111 -111
- package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
- package/src/orm/runtime-types.ts +39 -0
- package/src/orm/transaction-runner.ts +17 -0
- package/src/orm/unit-of-work.ts +232 -0
- package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
- package/src/{builder → query-builder}/delete-query-state.ts +38 -42
- package/src/{builder → query-builder}/delete.ts +46 -57
- package/src/{builder → query-builder}/hydration-manager.ts +87 -87
- package/src/{builder → query-builder}/hydration-planner.ts +182 -182
- package/src/{builder → query-builder}/insert-query-state.ts +51 -62
- package/src/{builder → query-builder}/insert.ts +48 -59
- package/src/{builder → query-builder}/query-ast-service.ts +208 -226
- package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
- package/src/{builder → query-builder}/relation-conditions.ts +112 -112
- package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
- package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
- package/src/{builder → query-builder}/relation-service.ts +284 -284
- package/src/{builder → query-builder}/relation-types.ts +21 -21
- package/src/{builder → query-builder}/relation-utils.ts +12 -12
- package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
- package/src/{builder → query-builder}/select-query-state.ts +179 -179
- package/src/{builder → query-builder}/select.ts +78 -69
- package/src/{builder → query-builder}/update-query-state.ts +55 -59
- package/src/{builder → query-builder}/update.ts +50 -61
- package/src/schema/column.ts +25 -25
- package/src/schema/relation.ts +116 -116
- package/src/schema/table.ts +34 -34
- package/src/schema/types.ts +76 -76
- package/.github/workflows/publish-metal-orm.yml +0 -38
- package/ROADMAP.md +0 -125
- package/docs/CHANGES.md +0 -104
- package/docs/advanced-features.md +0 -176
- package/docs/api-reference.md +0 -31
- package/docs/dml-operations.md +0 -156
- package/docs/getting-started.md +0 -171
- package/docs/hydration.md +0 -115
- package/docs/index.md +0 -36
- package/docs/multi-dialect-support.md +0 -59
- package/docs/query-builder.md +0 -135
- package/docs/runtime.md +0 -105
- package/docs/schema-definition.md +0 -112
- package/metadata.json +0 -5
- package/playground/api/playground-api.ts +0 -94
- package/playground/index.html +0 -15
- package/playground/src/App.css +0 -1
- package/playground/src/App.tsx +0 -114
- package/playground/src/components/CodeDisplay.tsx +0 -43
- package/playground/src/components/QueryExecutor.tsx +0 -189
- package/playground/src/components/ResultsTable.tsx +0 -67
- package/playground/src/components/ResultsTabs.tsx +0 -105
- package/playground/src/components/ScenarioList.tsx +0 -56
- package/playground/src/components/logo.svg +0 -45
- package/playground/src/data/scenarios.ts +0 -2
- package/playground/src/main.tsx +0 -9
- package/playground/src/services/PlaygroundApiService.ts +0 -60
- package/postcss.config.cjs +0 -5
- package/sql_sql-ansi-cheatsheet-2025.md +0 -264
- package/src/ast/expression.ts +0 -658
- package/src/builder/operations/cte-manager.ts +0 -34
- package/src/builder/operations/filter-manager.ts +0 -68
- package/src/builder/operations/join-manager.ts +0 -36
- package/src/builder/operations/pagination-manager.ts +0 -36
- package/src/playground/features/playground/api/types.ts +0 -16
- package/src/playground/features/playground/clients/MockClient.ts +0 -17
- package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
- package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
- package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
- package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
- package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
- package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
- package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
- package/src/playground/features/playground/data/scenarios/index.ts +0 -29
- package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
- package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
- package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
- package/src/playground/features/playground/data/scenarios/types.ts +0 -70
- package/src/playground/features/playground/data/schema.ts +0 -91
- package/src/playground/features/playground/data/seed.ts +0 -104
- package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
- package/src/runtime/orm-context.ts +0 -539
- package/tests/belongs-to-many.test.ts +0 -57
- package/tests/between.test.ts +0 -43
- package/tests/case-expression.test.ts +0 -58
- package/tests/complex-exists.test.ts +0 -230
- package/tests/cte.test.ts +0 -118
- package/tests/dml.test.ts +0 -206
- package/tests/exists.test.ts +0 -127
- package/tests/like.test.ts +0 -33
- package/tests/orm-runtime.test.ts +0 -254
- package/tests/postgres.test.ts +0 -30
- package/tests/right-join.test.ts +0 -89
- package/tests/subquery-having.test.ts +0 -193
- package/tests/window-function.test.ts +0 -151
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -10
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -14
- /package/src/{constants → core/sql}/sql.ts +0 -0
- /package/src/{runtime → orm}/als.ts +0 -0
- /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
|
+
};
|