metal-orm 1.0.39 → 1.0.41
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/dist/index.cjs +1466 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +723 -51
- package/dist/index.d.ts +723 -51
- package/dist/index.js +1457 -189
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen/typescript.ts +66 -5
- package/src/core/ast/aggregate-functions.ts +15 -15
- package/src/core/ast/expression-builders.ts +378 -316
- package/src/core/ast/expression-nodes.ts +210 -186
- package/src/core/ast/expression-visitor.ts +40 -30
- package/src/core/ast/query.ts +164 -132
- package/src/core/ast/window-functions.ts +86 -86
- package/src/core/dialect/abstract.ts +509 -479
- package/src/core/dialect/base/groupby-compiler.ts +6 -6
- package/src/core/dialect/base/join-compiler.ts +9 -12
- package/src/core/dialect/base/orderby-compiler.ts +20 -6
- package/src/core/dialect/base/sql-dialect.ts +237 -138
- package/src/core/dialect/mssql/index.ts +164 -185
- package/src/core/dialect/sqlite/index.ts +39 -34
- package/src/core/execution/db-executor.ts +46 -6
- package/src/core/execution/executors/mssql-executor.ts +39 -22
- package/src/core/execution/executors/mysql-executor.ts +23 -6
- package/src/core/execution/executors/sqlite-executor.ts +29 -3
- package/src/core/execution/pooling/pool-types.ts +30 -0
- package/src/core/execution/pooling/pool.ts +268 -0
- package/src/core/functions/standard-strategy.ts +46 -37
- package/src/decorators/bootstrap.ts +7 -7
- package/src/index.ts +6 -0
- package/src/orm/domain-event-bus.ts +49 -0
- package/src/orm/entity-metadata.ts +9 -9
- package/src/orm/entity.ts +58 -0
- package/src/orm/orm-session.ts +465 -270
- package/src/orm/orm.ts +61 -11
- package/src/orm/pooled-executor-factory.ts +131 -0
- package/src/orm/query-logger.ts +6 -12
- package/src/orm/relation-change-processor.ts +75 -0
- package/src/orm/relations/many-to-many.ts +4 -2
- package/src/orm/save-graph.ts +303 -0
- package/src/orm/transaction-runner.ts +3 -3
- package/src/orm/unit-of-work.ts +128 -0
- package/src/query-builder/delete-query-state.ts +67 -38
- package/src/query-builder/delete.ts +37 -1
- package/src/query-builder/hydration-manager.ts +93 -79
- package/src/query-builder/insert-query-state.ts +131 -61
- package/src/query-builder/insert.ts +27 -1
- package/src/query-builder/query-ast-service.ts +207 -170
- package/src/query-builder/select-query-state.ts +169 -162
- package/src/query-builder/select.ts +15 -23
- package/src/query-builder/update-query-state.ts +114 -77
- package/src/query-builder/update.ts +38 -1
package/src/orm/orm.ts
CHANGED
|
@@ -6,29 +6,61 @@ import { InterceptorPipeline } from './interceptor-pipeline.js';
|
|
|
6
6
|
import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
|
|
7
7
|
import { OrmSession } from './orm-session.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Options for creating an ORM instance.
|
|
11
|
+
* @template E - The domain event type
|
|
12
|
+
*/
|
|
9
13
|
export interface OrmOptions<E extends DomainEvent = OrmDomainEvent> {
|
|
14
|
+
/** The database dialect */
|
|
10
15
|
dialect: Dialect;
|
|
16
|
+
/** The database executor factory */
|
|
11
17
|
executorFactory: DbExecutorFactory;
|
|
18
|
+
/** Optional interceptors pipeline */
|
|
12
19
|
interceptors?: InterceptorPipeline;
|
|
20
|
+
/** Optional naming strategy */
|
|
13
21
|
namingStrategy?: NamingStrategy;
|
|
14
22
|
// model registrations etc.
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Database executor factory interface.
|
|
27
|
+
*/
|
|
17
28
|
export interface DbExecutorFactory {
|
|
18
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Creates a database executor.
|
|
31
|
+
* @returns The database executor
|
|
32
|
+
*/
|
|
33
|
+
createExecutor(): DbExecutor;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a transactional database executor.
|
|
37
|
+
* @returns The transactional database executor
|
|
38
|
+
*/
|
|
19
39
|
createTransactionalExecutor(): DbExecutor;
|
|
20
|
-
}
|
|
21
40
|
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Disposes any underlying resources (connection pools, background timers, etc).
|
|
43
|
+
*/
|
|
44
|
+
dispose(): Promise<void>;
|
|
24
45
|
}
|
|
25
46
|
|
|
47
|
+
/**
|
|
48
|
+
* ORM (Object-Relational Mapping) main class.
|
|
49
|
+
* @template E - The domain event type
|
|
50
|
+
*/
|
|
26
51
|
export class Orm<E extends DomainEvent = OrmDomainEvent> {
|
|
52
|
+
/** The database dialect */
|
|
27
53
|
readonly dialect: Dialect;
|
|
54
|
+
/** The interceptors pipeline */
|
|
28
55
|
readonly interceptors: InterceptorPipeline;
|
|
56
|
+
/** The naming strategy */
|
|
29
57
|
readonly namingStrategy: NamingStrategy;
|
|
30
58
|
private readonly executorFactory: DbExecutorFactory;
|
|
31
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Creates a new ORM instance.
|
|
62
|
+
* @param opts - ORM options
|
|
63
|
+
*/
|
|
32
64
|
constructor(opts: OrmOptions<E>) {
|
|
33
65
|
this.dialect = opts.dialect;
|
|
34
66
|
this.interceptors = opts.interceptors ?? new InterceptorPipeline();
|
|
@@ -36,23 +68,41 @@ export class Orm<E extends DomainEvent = OrmDomainEvent> {
|
|
|
36
68
|
this.executorFactory = opts.executorFactory;
|
|
37
69
|
}
|
|
38
70
|
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Creates a new ORM session.
|
|
73
|
+
* @param options - Optional session options
|
|
74
|
+
* @returns The ORM session
|
|
75
|
+
*/
|
|
76
|
+
createSession(): OrmSession<E> {
|
|
77
|
+
// No implicit transaction binding; callers should use Orm.transaction() for transactional work.
|
|
78
|
+
const executor = this.executorFactory.createExecutor();
|
|
41
79
|
return new OrmSession<E>({ orm: this, executor });
|
|
42
80
|
}
|
|
43
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Executes a function within a transaction.
|
|
84
|
+
* @template T - The return type
|
|
85
|
+
* @param fn - The function to execute
|
|
86
|
+
* @returns The result of the function
|
|
87
|
+
* @throws If the transaction fails
|
|
88
|
+
*/
|
|
44
89
|
async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
|
|
45
90
|
const executor = this.executorFactory.createTransactionalExecutor();
|
|
46
91
|
const session = new OrmSession<E>({ orm: this, executor });
|
|
47
92
|
try {
|
|
48
|
-
|
|
49
|
-
await session.
|
|
50
|
-
return result;
|
|
93
|
+
// A real transaction scope: begin before running user code, commit/rollback after.
|
|
94
|
+
return await session.transaction(() => fn(session));
|
|
51
95
|
} catch (err) {
|
|
52
|
-
await session.rollback();
|
|
53
96
|
throw err;
|
|
54
97
|
} finally {
|
|
55
|
-
|
|
98
|
+
await session.dispose();
|
|
56
99
|
}
|
|
57
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Shuts down the ORM and releases underlying resources (pools, timers).
|
|
104
|
+
*/
|
|
105
|
+
async dispose(): Promise<void> {
|
|
106
|
+
await this.executorFactory.dispose();
|
|
107
|
+
}
|
|
58
108
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
|
|
2
|
+
import { rowsToQueryResult } from '../core/execution/db-executor.js';
|
|
3
|
+
import type { Pool } from '../core/execution/pooling/pool.js';
|
|
4
|
+
import type { DbExecutorFactory } from './orm.js';
|
|
5
|
+
|
|
6
|
+
export interface PooledConnectionAdapter<TConn> {
|
|
7
|
+
query(
|
|
8
|
+
conn: TConn,
|
|
9
|
+
sql: string,
|
|
10
|
+
params?: unknown[]
|
|
11
|
+
): Promise<Array<Record<string, unknown>>>;
|
|
12
|
+
|
|
13
|
+
beginTransaction(conn: TConn): Promise<void>;
|
|
14
|
+
commitTransaction(conn: TConn): Promise<void>;
|
|
15
|
+
rollbackTransaction(conn: TConn): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type PooledExecutorFactoryOptions<TConn> = {
|
|
19
|
+
pool: Pool<TConn>;
|
|
20
|
+
adapter: PooledConnectionAdapter<TConn>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a first-class DbExecutorFactory backed by MetalORM's Pool.
|
|
25
|
+
*
|
|
26
|
+
* Design goals:
|
|
27
|
+
* - No leaks by default: pool leases are always released in `finally`.
|
|
28
|
+
* - Correct transactions: one leased connection per transaction.
|
|
29
|
+
* - Session-friendly: createExecutor() supports transactions without permanently leasing a connection.
|
|
30
|
+
*/
|
|
31
|
+
export function createPooledExecutorFactory<TConn>(
|
|
32
|
+
opts: PooledExecutorFactoryOptions<TConn>
|
|
33
|
+
): DbExecutorFactory {
|
|
34
|
+
const { pool, adapter } = opts;
|
|
35
|
+
|
|
36
|
+
const makeExecutor = (mode: 'session' | 'sticky'): DbExecutor => {
|
|
37
|
+
let lease: Awaited<ReturnType<typeof pool.acquire>> | null = null;
|
|
38
|
+
|
|
39
|
+
const getLease = async () => {
|
|
40
|
+
if (lease) return lease;
|
|
41
|
+
lease = await pool.acquire();
|
|
42
|
+
return lease;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const executeWithConn = async (
|
|
46
|
+
conn: TConn,
|
|
47
|
+
sql: string,
|
|
48
|
+
params?: unknown[]
|
|
49
|
+
): Promise<QueryResult[]> => {
|
|
50
|
+
const rows = await adapter.query(conn, sql, params);
|
|
51
|
+
return [rowsToQueryResult(rows)];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
capabilities: { transactions: true },
|
|
56
|
+
|
|
57
|
+
async executeSql(sql, params) {
|
|
58
|
+
// Sticky mode: always reuse a leased connection.
|
|
59
|
+
if (mode === 'sticky') {
|
|
60
|
+
const l = await getLease();
|
|
61
|
+
return executeWithConn(l.resource, sql, params);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Session mode: use the leased connection if we're currently in a transaction;
|
|
65
|
+
// otherwise acquire/release per call.
|
|
66
|
+
if (lease) {
|
|
67
|
+
return executeWithConn(lease.resource, sql, params);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const l = await pool.acquire();
|
|
71
|
+
try {
|
|
72
|
+
return await executeWithConn(l.resource, sql, params);
|
|
73
|
+
} finally {
|
|
74
|
+
await l.release();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async beginTransaction() {
|
|
79
|
+
const l = await getLease();
|
|
80
|
+
await adapter.beginTransaction(l.resource);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async commitTransaction() {
|
|
84
|
+
if (!lease) {
|
|
85
|
+
throw new Error('commitTransaction called without an active transaction');
|
|
86
|
+
}
|
|
87
|
+
const l = lease;
|
|
88
|
+
try {
|
|
89
|
+
await adapter.commitTransaction(l.resource);
|
|
90
|
+
} finally {
|
|
91
|
+
lease = null;
|
|
92
|
+
await l.release();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async rollbackTransaction() {
|
|
97
|
+
if (!lease) {
|
|
98
|
+
// Nothing to rollback; keep idempotent semantics.
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const l = lease;
|
|
102
|
+
try {
|
|
103
|
+
await adapter.rollbackTransaction(l.resource);
|
|
104
|
+
} finally {
|
|
105
|
+
lease = null;
|
|
106
|
+
await l.release();
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async dispose() {
|
|
111
|
+
if (!lease) return;
|
|
112
|
+
const l = lease;
|
|
113
|
+
lease = null;
|
|
114
|
+
await l.release();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
createExecutor() {
|
|
121
|
+
return makeExecutor('session');
|
|
122
|
+
},
|
|
123
|
+
createTransactionalExecutor() {
|
|
124
|
+
return makeExecutor('sticky');
|
|
125
|
+
},
|
|
126
|
+
async dispose() {
|
|
127
|
+
await pool.destroy();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
package/src/orm/query-logger.ts
CHANGED
|
@@ -31,23 +31,17 @@ export const createQueryLoggingExecutor = (
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const wrapped: DbExecutor = {
|
|
34
|
+
capabilities: executor.capabilities,
|
|
34
35
|
async executeSql(sql, params) {
|
|
35
36
|
logger({ sql, params });
|
|
36
37
|
return executor.executeSql(sql, params);
|
|
37
38
|
}
|
|
39
|
+
,
|
|
40
|
+
beginTransaction: () => executor.beginTransaction(),
|
|
41
|
+
commitTransaction: () => executor.commitTransaction(),
|
|
42
|
+
rollbackTransaction: () => executor.rollbackTransaction(),
|
|
43
|
+
dispose: () => executor.dispose(),
|
|
38
44
|
};
|
|
39
45
|
|
|
40
|
-
if (executor.beginTransaction) {
|
|
41
|
-
wrapped.beginTransaction = executor.beginTransaction.bind(executor);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (executor.commitTransaction) {
|
|
45
|
-
wrapped.commitTransaction = executor.commitTransaction.bind(executor);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (executor.rollbackTransaction) {
|
|
49
|
-
wrapped.rollbackTransaction = executor.rollbackTransaction.bind(executor);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
46
|
return wrapped;
|
|
53
47
|
};
|
|
@@ -10,23 +10,42 @@ 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
|
|
|
13
|
+
/**
|
|
14
|
+
* Processes relation changes for entity relationships.
|
|
15
|
+
*/
|
|
13
16
|
export class RelationChangeProcessor {
|
|
14
17
|
private readonly relationChanges: RelationChangeEntry[] = [];
|
|
15
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new RelationChangeProcessor instance.
|
|
21
|
+
* @param unitOfWork - The unit of work instance
|
|
22
|
+
* @param dialect - The database dialect
|
|
23
|
+
* @param executor - The database executor
|
|
24
|
+
*/
|
|
16
25
|
constructor(
|
|
17
26
|
private readonly unitOfWork: UnitOfWork,
|
|
18
27
|
private readonly dialect: Dialect,
|
|
19
28
|
private readonly executor: DbExecutor
|
|
20
29
|
) { }
|
|
21
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Registers a relation change for processing.
|
|
33
|
+
* @param entry - The relation change entry
|
|
34
|
+
*/
|
|
22
35
|
registerChange(entry: RelationChangeEntry): void {
|
|
23
36
|
this.relationChanges.push(entry);
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Resets the relation change processor by clearing all pending changes.
|
|
41
|
+
*/
|
|
26
42
|
reset(): void {
|
|
27
43
|
this.relationChanges.length = 0;
|
|
28
44
|
}
|
|
29
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Processes all pending relation changes.
|
|
48
|
+
*/
|
|
30
49
|
async process(): Promise<void> {
|
|
31
50
|
if (!this.relationChanges.length) return;
|
|
32
51
|
const entries = [...this.relationChanges];
|
|
@@ -50,6 +69,10 @@ export class RelationChangeProcessor {
|
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Handles changes for has-many relations.
|
|
74
|
+
* @param entry - The relation change entry
|
|
75
|
+
*/
|
|
53
76
|
private async handleHasManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
54
77
|
const relation = entry.relation as HasManyRelation;
|
|
55
78
|
const target = entry.change.entity;
|
|
@@ -73,6 +96,10 @@ export class RelationChangeProcessor {
|
|
|
73
96
|
}
|
|
74
97
|
}
|
|
75
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Handles changes for has-one relations.
|
|
101
|
+
* @param entry - The relation change entry
|
|
102
|
+
*/
|
|
76
103
|
private async handleHasOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
77
104
|
const relation = entry.relation as HasOneRelation;
|
|
78
105
|
const target = entry.change.entity;
|
|
@@ -96,10 +123,18 @@ export class RelationChangeProcessor {
|
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Handles changes for belongs-to relations.
|
|
128
|
+
* @param _entry - The relation change entry (reserved for future use)
|
|
129
|
+
*/
|
|
99
130
|
private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
|
|
100
131
|
// Reserved for future cascade/persist behaviors for belongs-to relations.
|
|
101
132
|
}
|
|
102
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Handles changes for belongs-to-many relations.
|
|
136
|
+
* @param entry - The relation change entry
|
|
137
|
+
*/
|
|
103
138
|
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
104
139
|
const relation = entry.relation as BelongsToManyRelation;
|
|
105
140
|
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
@@ -123,12 +158,23 @@ export class RelationChangeProcessor {
|
|
|
123
158
|
}
|
|
124
159
|
}
|
|
125
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Assigns a foreign key for has-many relations.
|
|
163
|
+
* @param child - The child entity
|
|
164
|
+
* @param relation - The has-many relation
|
|
165
|
+
* @param rootValue - The root entity's primary key value
|
|
166
|
+
*/
|
|
126
167
|
private assignHasManyForeignKey(child: any, relation: HasManyRelation, rootValue: unknown): void {
|
|
127
168
|
const current = child[relation.foreignKey];
|
|
128
169
|
if (current === rootValue) return;
|
|
129
170
|
child[relation.foreignKey] = rootValue;
|
|
130
171
|
}
|
|
131
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Detaches a child entity from has-many relations.
|
|
175
|
+
* @param child - The child entity
|
|
176
|
+
* @param relation - The has-many relation
|
|
177
|
+
*/
|
|
132
178
|
private detachHasManyChild(child: any, relation: HasManyRelation): void {
|
|
133
179
|
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
134
180
|
this.unitOfWork.markRemoved(child);
|
|
@@ -138,12 +184,23 @@ export class RelationChangeProcessor {
|
|
|
138
184
|
this.unitOfWork.markDirty(child);
|
|
139
185
|
}
|
|
140
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Assigns a foreign key for has-one relations.
|
|
189
|
+
* @param child - The child entity
|
|
190
|
+
* @param relation - The has-one relation
|
|
191
|
+
* @param rootValue - The root entity's primary key value
|
|
192
|
+
*/
|
|
141
193
|
private assignHasOneForeignKey(child: any, relation: HasOneRelation, rootValue: unknown): void {
|
|
142
194
|
const current = child[relation.foreignKey];
|
|
143
195
|
if (current === rootValue) return;
|
|
144
196
|
child[relation.foreignKey] = rootValue;
|
|
145
197
|
}
|
|
146
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Detaches a child entity from has-one relations.
|
|
201
|
+
* @param child - The child entity
|
|
202
|
+
* @param relation - The has-one relation
|
|
203
|
+
*/
|
|
147
204
|
private detachHasOneChild(child: any, relation: HasOneRelation): void {
|
|
148
205
|
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
149
206
|
this.unitOfWork.markRemoved(child);
|
|
@@ -153,6 +210,12 @@ export class RelationChangeProcessor {
|
|
|
153
210
|
this.unitOfWork.markDirty(child);
|
|
154
211
|
}
|
|
155
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Inserts a pivot row for belongs-to-many relations.
|
|
215
|
+
* @param relation - The belongs-to-many relation
|
|
216
|
+
* @param rootId - The root entity's primary key value
|
|
217
|
+
* @param targetId - The target entity's primary key value
|
|
218
|
+
*/
|
|
156
219
|
private async insertPivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
157
220
|
const payload = {
|
|
158
221
|
[relation.pivotForeignKeyToRoot]: rootId,
|
|
@@ -163,6 +226,12 @@ export class RelationChangeProcessor {
|
|
|
163
226
|
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
164
227
|
}
|
|
165
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Deletes a pivot row for belongs-to-many relations.
|
|
231
|
+
* @param relation - The belongs-to-many relation
|
|
232
|
+
* @param rootId - The root entity's primary key value
|
|
233
|
+
* @param targetId - The target entity's primary key value
|
|
234
|
+
*/
|
|
166
235
|
private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
167
236
|
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
168
237
|
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
@@ -175,6 +244,12 @@ export class RelationChangeProcessor {
|
|
|
175
244
|
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
176
245
|
}
|
|
177
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Resolves the primary key value from an entity.
|
|
249
|
+
* @param entity - The entity
|
|
250
|
+
* @param table - The table definition
|
|
251
|
+
* @returns The primary key value or null
|
|
252
|
+
*/
|
|
178
253
|
private resolvePrimaryKeyValue(entity: any, table: TableDef): string | number | null {
|
|
179
254
|
if (!entity) return null;
|
|
180
255
|
const key = findPrimaryKey(table);
|
|
@@ -63,8 +63,10 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
|
|
|
63
63
|
attach(target: TTarget | number | string): void {
|
|
64
64
|
const entity = this.ensureEntity(target);
|
|
65
65
|
const id = this.extractId(entity);
|
|
66
|
-
if (id
|
|
67
|
-
|
|
66
|
+
if (id != null && this.items.some(item => this.extractId(item) === id)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (id == null && this.items.includes(entity)) {
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
this.items.push(entity);
|