@mikro-orm/sql 7.0.4 → 7.0.5-dev.0
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/AbstractSqlConnection.d.ts +58 -94
- package/AbstractSqlConnection.js +238 -235
- package/AbstractSqlDriver.d.ts +155 -410
- package/AbstractSqlDriver.js +1941 -2064
- package/AbstractSqlPlatform.d.ts +73 -83
- package/AbstractSqlPlatform.js +158 -162
- package/PivotCollectionPersister.d.ts +15 -33
- package/PivotCollectionPersister.js +160 -158
- package/README.md +1 -1
- package/SqlEntityManager.d.ts +22 -67
- package/SqlEntityManager.js +38 -54
- package/SqlEntityRepository.d.ts +14 -14
- package/SqlEntityRepository.js +23 -23
- package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +12 -12
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +194 -192
- package/dialects/mysql/BaseMySqlPlatform.d.ts +45 -64
- package/dialects/mysql/BaseMySqlPlatform.js +131 -134
- package/dialects/mysql/MySqlExceptionConverter.d.ts +6 -6
- package/dialects/mysql/MySqlExceptionConverter.js +77 -91
- package/dialects/mysql/MySqlNativeQueryBuilder.d.ts +3 -3
- package/dialects/mysql/MySqlNativeQueryBuilder.js +69 -66
- package/dialects/mysql/MySqlSchemaHelper.d.ts +39 -39
- package/dialects/mysql/MySqlSchemaHelper.js +319 -327
- package/dialects/oracledb/OracleDialect.d.ts +52 -81
- package/dialects/oracledb/OracleDialect.js +149 -155
- package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +12 -12
- package/dialects/oracledb/OracleNativeQueryBuilder.js +236 -232
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -109
- package/dialects/postgresql/BasePostgreSqlPlatform.js +353 -354
- package/dialects/postgresql/FullTextType.d.ts +6 -10
- package/dialects/postgresql/FullTextType.js +51 -51
- package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +5 -5
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +43 -55
- package/dialects/postgresql/PostgreSqlNativeQueryBuilder.d.ts +1 -1
- package/dialects/postgresql/PostgreSqlNativeQueryBuilder.js +4 -4
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +82 -102
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +705 -733
- package/dialects/sqlite/BaseSqliteConnection.d.ts +5 -3
- package/dialects/sqlite/BaseSqliteConnection.js +19 -21
- package/dialects/sqlite/NodeSqliteDialect.d.ts +1 -1
- package/dialects/sqlite/NodeSqliteDialect.js +23 -23
- package/dialects/sqlite/SqliteDriver.d.ts +1 -1
- package/dialects/sqlite/SqliteDriver.js +3 -3
- package/dialects/sqlite/SqliteExceptionConverter.d.ts +6 -6
- package/dialects/sqlite/SqliteExceptionConverter.js +51 -67
- package/dialects/sqlite/SqliteNativeQueryBuilder.d.ts +2 -2
- package/dialects/sqlite/SqliteNativeQueryBuilder.js +7 -7
- package/dialects/sqlite/SqlitePlatform.d.ts +72 -63
- package/dialects/sqlite/SqlitePlatform.js +139 -139
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +60 -70
- package/dialects/sqlite/SqliteSchemaHelper.js +520 -533
- package/package.json +2 -2
- package/plugin/index.d.ts +35 -42
- package/plugin/index.js +36 -43
- package/plugin/transformer.d.ts +94 -117
- package/plugin/transformer.js +881 -890
- package/query/ArrayCriteriaNode.d.ts +4 -4
- package/query/ArrayCriteriaNode.js +18 -18
- package/query/CriteriaNode.d.ts +25 -35
- package/query/CriteriaNode.js +123 -133
- package/query/CriteriaNodeFactory.d.ts +6 -49
- package/query/CriteriaNodeFactory.js +94 -97
- package/query/NativeQueryBuilder.d.ts +118 -118
- package/query/NativeQueryBuilder.js +480 -484
- package/query/ObjectCriteriaNode.d.ts +12 -12
- package/query/ObjectCriteriaNode.js +282 -298
- package/query/QueryBuilder.d.ts +905 -1557
- package/query/QueryBuilder.js +2192 -2322
- package/query/QueryBuilderHelper.d.ts +72 -153
- package/query/QueryBuilderHelper.js +1028 -1079
- package/query/ScalarCriteriaNode.d.ts +3 -3
- package/query/ScalarCriteriaNode.js +46 -53
- package/query/enums.d.ts +14 -14
- package/query/enums.js +14 -14
- package/query/raw.d.ts +6 -16
- package/query/raw.js +10 -10
- package/schema/DatabaseSchema.d.ts +50 -73
- package/schema/DatabaseSchema.js +307 -331
- package/schema/DatabaseTable.d.ts +73 -96
- package/schema/DatabaseTable.js +927 -1012
- package/schema/SchemaComparator.d.ts +66 -70
- package/schema/SchemaComparator.js +740 -766
- package/schema/SchemaHelper.d.ts +95 -109
- package/schema/SchemaHelper.js +659 -675
- package/schema/SqlSchemaGenerator.d.ts +58 -78
- package/schema/SqlSchemaGenerator.js +501 -535
- package/typings.d.ts +266 -380
package/AbstractSqlDriver.js
CHANGED
|
@@ -1,2126 +1,2003 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ALIAS_REPLACEMENT_RE,
|
|
3
|
-
DatabaseDriver,
|
|
4
|
-
EntityManagerType,
|
|
5
|
-
getLoadingStrategy,
|
|
6
|
-
getOnConflictFields,
|
|
7
|
-
getOnConflictReturningFields,
|
|
8
|
-
helper,
|
|
9
|
-
isRaw,
|
|
10
|
-
LoadStrategy,
|
|
11
|
-
parseJsonSafe,
|
|
12
|
-
PolymorphicRef,
|
|
13
|
-
QueryFlag,
|
|
14
|
-
QueryHelper,
|
|
15
|
-
QueryOrder,
|
|
16
|
-
raw,
|
|
17
|
-
RawQueryFragment,
|
|
18
|
-
ReferenceKind,
|
|
19
|
-
Utils,
|
|
20
|
-
} from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, PolymorphicRef, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
21
2
|
import { QueryBuilder } from './query/QueryBuilder.js';
|
|
22
3
|
import { JoinType, QueryType } from './query/enums.js';
|
|
23
4
|
import { SqlEntityManager } from './SqlEntityManager.js';
|
|
24
5
|
import { PivotCollectionPersister } from './PivotCollectionPersister.js';
|
|
25
6
|
/** Base class for SQL database drivers, implementing find/insert/update/delete using QueryBuilder. */
|
|
26
7
|
export class AbstractSqlDriver extends DatabaseDriver {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
throw new Error(
|
|
63
|
-
"indexHint for SQL drivers must be a string (e.g. 'force index(my_index)'). Use an object only with MongoDB.",
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
createEntityManager(useContext) {
|
|
68
|
-
const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
|
|
69
|
-
return new EntityManagerClass(this.config, this, this.metadata, useContext);
|
|
70
|
-
}
|
|
71
|
-
async createQueryBuilderFromOptions(meta, where, options = {}) {
|
|
72
|
-
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
|
|
73
|
-
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
|
|
74
|
-
const joinedProps = this.joinedProps(meta, populate, options);
|
|
75
|
-
const schema = this.getSchemaName(meta, options);
|
|
76
|
-
const qb = this.createQueryBuilder(
|
|
77
|
-
meta.class,
|
|
78
|
-
options.ctx,
|
|
79
|
-
connectionType,
|
|
80
|
-
false,
|
|
81
|
-
options.logging,
|
|
82
|
-
undefined,
|
|
83
|
-
options.em,
|
|
84
|
-
).withSchema(schema);
|
|
85
|
-
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
86
|
-
const orderBy = this.buildOrderBy(qb, meta, populate, options);
|
|
87
|
-
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
88
|
-
Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
|
|
89
|
-
if (Utils.isPrimaryKey(where, meta.compositePK)) {
|
|
90
|
-
where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
|
|
91
|
-
}
|
|
92
|
-
this.validateSqlOptions(options);
|
|
93
|
-
const { first, last, before, after } = options;
|
|
94
|
-
const isCursorPagination = [first, last, before, after].some(v => v != null);
|
|
95
|
-
qb.state.resolvedPopulateWhere = options._populateWhere;
|
|
96
|
-
qb.select(fields)
|
|
97
|
-
// only add populateWhere if we are populate-joining, as this will be used to add `on` conditions
|
|
98
|
-
.populate(
|
|
99
|
-
populate,
|
|
100
|
-
joinedProps.length > 0 ? populateWhere : undefined,
|
|
101
|
-
joinedProps.length > 0 ? options.populateFilter : undefined,
|
|
102
|
-
)
|
|
103
|
-
.where(where)
|
|
104
|
-
.groupBy(options.groupBy)
|
|
105
|
-
.having(options.having)
|
|
106
|
-
.indexHint(options.indexHint)
|
|
107
|
-
.collation(options.collation)
|
|
108
|
-
.comment(options.comments)
|
|
109
|
-
.hintComment(options.hintComments);
|
|
110
|
-
if (isCursorPagination) {
|
|
111
|
-
const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
|
|
112
|
-
qb.andWhere(where).orderBy(newOrderBy);
|
|
113
|
-
} else {
|
|
114
|
-
qb.orderBy(orderBy);
|
|
115
|
-
}
|
|
116
|
-
if (options.limit != null || options.offset != null) {
|
|
117
|
-
qb.limit(options.limit, options.offset);
|
|
118
|
-
}
|
|
119
|
-
if (options.lockMode) {
|
|
120
|
-
qb.setLockMode(options.lockMode, options.lockTableAliases);
|
|
121
|
-
}
|
|
122
|
-
if (options.em) {
|
|
123
|
-
await qb.applyJoinedFilters(options.em, options.filters);
|
|
124
|
-
}
|
|
125
|
-
return qb;
|
|
126
|
-
}
|
|
127
|
-
async find(entityName, where, options = {}) {
|
|
128
|
-
options = { populate: [], orderBy: [], ...options };
|
|
129
|
-
const meta = this.metadata.get(entityName);
|
|
130
|
-
if (meta.virtual) {
|
|
131
|
-
return this.findVirtual(entityName, where, options);
|
|
132
|
-
}
|
|
133
|
-
if (options.unionWhere?.length) {
|
|
134
|
-
where = await this.applyUnionWhere(meta, where, options);
|
|
135
|
-
}
|
|
136
|
-
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
137
|
-
const result = await this.rethrow(qb.execute('all'));
|
|
138
|
-
if (options.last && !options.first) {
|
|
139
|
-
result.reverse();
|
|
140
|
-
}
|
|
141
|
-
return result;
|
|
142
|
-
}
|
|
143
|
-
async findOne(entityName, where, options) {
|
|
144
|
-
const opts = { populate: [], ...options };
|
|
145
|
-
const meta = this.metadata.find(entityName);
|
|
146
|
-
const populate = this.autoJoinOneToOneOwner(meta, opts.populate, opts.fields);
|
|
147
|
-
const joinedProps = this.joinedProps(meta, populate, options);
|
|
148
|
-
const hasToManyJoins = joinedProps.some(hint => this.hasToManyJoins(hint, meta));
|
|
149
|
-
if (joinedProps.length === 0 || !hasToManyJoins) {
|
|
150
|
-
opts.limit = 1;
|
|
151
|
-
}
|
|
152
|
-
if (opts.limit > 0 && !opts.flags?.includes(QueryFlag.DISABLE_PAGINATE)) {
|
|
153
|
-
opts.flags ??= [];
|
|
154
|
-
opts.flags.push(QueryFlag.DISABLE_PAGINATE);
|
|
155
|
-
}
|
|
156
|
-
const res = await this.find(entityName, where, opts);
|
|
157
|
-
return res[0] || null;
|
|
158
|
-
}
|
|
159
|
-
hasToManyJoins(hint, meta) {
|
|
160
|
-
const [propName] = hint.field.split(':', 2);
|
|
161
|
-
const prop = meta.properties[propName];
|
|
162
|
-
if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
if (hint.children && prop.targetMeta) {
|
|
166
|
-
return hint.children.some(hint => this.hasToManyJoins(hint, prop.targetMeta));
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
async findVirtual(entityName, where, options) {
|
|
171
|
-
return this.findFromVirtual(entityName, where, options, QueryType.SELECT);
|
|
172
|
-
}
|
|
173
|
-
async countVirtual(entityName, where, options) {
|
|
174
|
-
return this.findFromVirtual(entityName, where, options, QueryType.COUNT);
|
|
175
|
-
}
|
|
176
|
-
async findFromVirtual(entityName, where, options, type) {
|
|
177
|
-
const meta = this.metadata.get(entityName);
|
|
178
|
-
/* v8 ignore next */
|
|
179
|
-
if (!meta.expression) {
|
|
180
|
-
return type === QueryType.SELECT ? [] : 0;
|
|
181
|
-
}
|
|
182
|
-
if (typeof meta.expression === 'string') {
|
|
183
|
-
return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, type);
|
|
184
|
-
}
|
|
185
|
-
const em = this.createEntityManager();
|
|
186
|
-
em.setTransactionContext(options.ctx);
|
|
187
|
-
const res = meta.expression(em, where, options);
|
|
188
|
-
if (typeof res === 'string') {
|
|
189
|
-
return this.wrapVirtualExpressionInSubquery(meta, res, where, options, type);
|
|
190
|
-
}
|
|
191
|
-
if (res instanceof QueryBuilder) {
|
|
192
|
-
return this.wrapVirtualExpressionInSubquery(meta, res.getFormattedQuery(), where, options, type);
|
|
193
|
-
}
|
|
194
|
-
if (res instanceof RawQueryFragment) {
|
|
195
|
-
const expr = this.platform.formatQuery(res.sql, res.params);
|
|
196
|
-
return this.wrapVirtualExpressionInSubquery(meta, expr, where, options, type);
|
|
197
|
-
}
|
|
198
|
-
/* v8 ignore next */
|
|
199
|
-
return res;
|
|
200
|
-
}
|
|
201
|
-
async *streamFromVirtual(entityName, where, options) {
|
|
202
|
-
const meta = this.metadata.get(entityName);
|
|
203
|
-
/* v8 ignore next */
|
|
204
|
-
if (!meta.expression) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (typeof meta.expression === 'string') {
|
|
208
|
-
yield* this.wrapVirtualExpressionInSubqueryStream(meta, meta.expression, where, options, QueryType.SELECT);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const em = this.createEntityManager();
|
|
212
|
-
em.setTransactionContext(options.ctx);
|
|
213
|
-
const res = meta.expression(em, where, options, true);
|
|
214
|
-
if (typeof res === 'string') {
|
|
215
|
-
yield* this.wrapVirtualExpressionInSubqueryStream(meta, res, where, options, QueryType.SELECT);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (res instanceof QueryBuilder) {
|
|
219
|
-
yield* this.wrapVirtualExpressionInSubqueryStream(
|
|
220
|
-
meta,
|
|
221
|
-
res.getFormattedQuery(),
|
|
222
|
-
where,
|
|
223
|
-
options,
|
|
224
|
-
QueryType.SELECT,
|
|
225
|
-
);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (res instanceof RawQueryFragment) {
|
|
229
|
-
const expr = this.platform.formatQuery(res.sql, res.params);
|
|
230
|
-
yield* this.wrapVirtualExpressionInSubqueryStream(meta, expr, where, options, QueryType.SELECT);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
/* v8 ignore next */
|
|
234
|
-
yield* res;
|
|
235
|
-
}
|
|
236
|
-
async wrapVirtualExpressionInSubquery(meta, expression, where, options, type) {
|
|
237
|
-
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
|
|
238
|
-
qb.setFlag(QueryFlag.DISABLE_PAGINATE);
|
|
239
|
-
const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
|
|
240
|
-
const native = qb.getNativeQuery(false);
|
|
241
|
-
if (type === QueryType.COUNT) {
|
|
242
|
-
native.clear('select').clear('limit').clear('offset').count();
|
|
8
|
+
[EntityManagerType];
|
|
9
|
+
connection;
|
|
10
|
+
replicas = [];
|
|
11
|
+
platform;
|
|
12
|
+
constructor(config, platform, connection, connector) {
|
|
13
|
+
super(config, connector);
|
|
14
|
+
this.connection = new connection(this.config);
|
|
15
|
+
this.replicas = this.createReplicas(conf => new connection(this.config, conf, 'read'));
|
|
16
|
+
this.platform = platform;
|
|
17
|
+
}
|
|
18
|
+
getPlatform() {
|
|
19
|
+
return this.platform;
|
|
20
|
+
}
|
|
21
|
+
/** Evaluates a formula callback, handling both string and Raw return values. */
|
|
22
|
+
evaluateFormula(formula, columns, table) {
|
|
23
|
+
const result = formula(columns, table);
|
|
24
|
+
return isRaw(result) ? this.platform.formatQuery(result.sql, result.params) : result;
|
|
25
|
+
}
|
|
26
|
+
/** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
|
|
27
|
+
getTableProps(meta) {
|
|
28
|
+
return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
|
|
29
|
+
}
|
|
30
|
+
/** Creates a FormulaTable object for use in formula callbacks. */
|
|
31
|
+
createFormulaTable(alias, meta, schema) {
|
|
32
|
+
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
33
|
+
const qualifiedName = effectiveSchema ? `${effectiveSchema}.${meta.tableName}` : meta.tableName;
|
|
34
|
+
return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
|
|
35
|
+
}
|
|
36
|
+
validateSqlOptions(options) {
|
|
37
|
+
if (options.collation != null && typeof options.collation !== 'string') {
|
|
38
|
+
throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
|
|
39
|
+
}
|
|
40
|
+
if (options.indexHint != null && typeof options.indexHint !== 'string') {
|
|
41
|
+
throw new Error("indexHint for SQL drivers must be a string (e.g. 'force index(my_index)'). Use an object only with MongoDB.");
|
|
42
|
+
}
|
|
243
43
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
44
|
+
createEntityManager(useContext) {
|
|
45
|
+
const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
|
|
46
|
+
return new EntityManagerClass(this.config, this, this.metadata, useContext);
|
|
47
|
+
}
|
|
48
|
+
async createQueryBuilderFromOptions(meta, where, options = {}) {
|
|
49
|
+
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
|
|
50
|
+
const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
|
|
51
|
+
const joinedProps = this.joinedProps(meta, populate, options);
|
|
52
|
+
const schema = this.getSchemaName(meta, options);
|
|
53
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
|
|
54
|
+
const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
55
|
+
const orderBy = this.buildOrderBy(qb, meta, populate, options);
|
|
56
|
+
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
57
|
+
Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
|
|
58
|
+
if (Utils.isPrimaryKey(where, meta.compositePK)) {
|
|
59
|
+
where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
|
|
60
|
+
}
|
|
61
|
+
this.validateSqlOptions(options);
|
|
62
|
+
const { first, last, before, after } = options;
|
|
63
|
+
const isCursorPagination = [first, last, before, after].some(v => v != null);
|
|
64
|
+
qb.state.resolvedPopulateWhere = options._populateWhere;
|
|
65
|
+
qb.select(fields)
|
|
66
|
+
// only add populateWhere if we are populate-joining, as this will be used to add `on` conditions
|
|
67
|
+
.populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
|
|
68
|
+
.where(where)
|
|
69
|
+
.groupBy(options.groupBy)
|
|
70
|
+
.having(options.having)
|
|
71
|
+
.indexHint(options.indexHint)
|
|
72
|
+
.collation(options.collation)
|
|
73
|
+
.comment(options.comments)
|
|
74
|
+
.hintComment(options.hintComments);
|
|
75
|
+
if (isCursorPagination) {
|
|
76
|
+
const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
|
|
77
|
+
qb.andWhere(where).orderBy(newOrderBy);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
qb.orderBy(orderBy);
|
|
81
|
+
}
|
|
82
|
+
if (options.limit != null || options.offset != null) {
|
|
83
|
+
qb.limit(options.limit, options.offset);
|
|
84
|
+
}
|
|
85
|
+
if (options.lockMode) {
|
|
86
|
+
qb.setLockMode(options.lockMode, options.lockTableAliases);
|
|
87
|
+
}
|
|
88
|
+
if (options.em) {
|
|
89
|
+
await qb.applyJoinedFilters(options.em, options.filters);
|
|
90
|
+
}
|
|
91
|
+
return qb;
|
|
250
92
|
}
|
|
251
|
-
|
|
252
|
-
|
|
93
|
+
async find(entityName, where, options = {}) {
|
|
94
|
+
options = { populate: [], orderBy: [], ...options };
|
|
95
|
+
const meta = this.metadata.get(entityName);
|
|
96
|
+
if (meta.virtual) {
|
|
97
|
+
return this.findVirtual(entityName, where, options);
|
|
98
|
+
}
|
|
99
|
+
if (options.unionWhere?.length) {
|
|
100
|
+
where = await this.applyUnionWhere(meta, where, options);
|
|
101
|
+
}
|
|
102
|
+
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
103
|
+
const result = await this.rethrow(qb.execute('all'));
|
|
104
|
+
if (options.last && !options.first) {
|
|
105
|
+
result.reverse();
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
async findOne(entityName, where, options) {
|
|
110
|
+
const opts = { populate: [], ...options };
|
|
111
|
+
const meta = this.metadata.find(entityName);
|
|
112
|
+
const populate = this.autoJoinOneToOneOwner(meta, opts.populate, opts.fields);
|
|
113
|
+
const joinedProps = this.joinedProps(meta, populate, options);
|
|
114
|
+
const hasToManyJoins = joinedProps.some(hint => this.hasToManyJoins(hint, meta));
|
|
115
|
+
if (joinedProps.length === 0 || !hasToManyJoins) {
|
|
116
|
+
opts.limit = 1;
|
|
117
|
+
}
|
|
118
|
+
if (opts.limit > 0 && !opts.flags?.includes(QueryFlag.DISABLE_PAGINATE)) {
|
|
119
|
+
opts.flags ??= [];
|
|
120
|
+
opts.flags.push(QueryFlag.DISABLE_PAGINATE);
|
|
121
|
+
}
|
|
122
|
+
const res = await this.find(entityName, where, opts);
|
|
123
|
+
return res[0] || null;
|
|
253
124
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
|
|
265
|
-
for await (const row of res) {
|
|
266
|
-
yield this.mapResult(row, meta);
|
|
125
|
+
hasToManyJoins(hint, meta) {
|
|
126
|
+
const [propName] = hint.field.split(':', 2);
|
|
127
|
+
const prop = meta.properties[propName];
|
|
128
|
+
if (prop && [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (hint.children && prop.targetMeta) {
|
|
132
|
+
return hint.children.some(hint => this.hasToManyJoins(hint, prop.targetMeta));
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
267
135
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
* Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
|
|
271
|
-
* Force balanced strategy to load to-many relations via separate queries.
|
|
272
|
-
*/
|
|
273
|
-
forceBalancedStrategy(options) {
|
|
274
|
-
const clearStrategy = hints => {
|
|
275
|
-
return hints.map(hint => ({
|
|
276
|
-
...hint,
|
|
277
|
-
strategy: undefined,
|
|
278
|
-
children: hint.children ? clearStrategy(hint.children) : undefined,
|
|
279
|
-
}));
|
|
280
|
-
};
|
|
281
|
-
const opts = { ...options, strategy: 'balanced' };
|
|
282
|
-
if (Array.isArray(opts.populate)) {
|
|
283
|
-
opts.populate = clearStrategy(opts.populate);
|
|
136
|
+
async findVirtual(entityName, where, options) {
|
|
137
|
+
return this.findFromVirtual(entityName, where, options, QueryType.SELECT);
|
|
284
138
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
mapResult(result, meta, populate = [], qb, map = {}) {
|
|
288
|
-
// For TPT inheritance, map aliased parent table columns back to their field names
|
|
289
|
-
if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
|
|
290
|
-
this.mapTPTColumns(result, meta, qb);
|
|
139
|
+
async countVirtual(entityName, where, options) {
|
|
140
|
+
return this.findFromVirtual(entityName, where, options, QueryType.COUNT);
|
|
291
141
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
142
|
+
async findFromVirtual(entityName, where, options, type) {
|
|
143
|
+
const meta = this.metadata.get(entityName);
|
|
144
|
+
/* v8 ignore next */
|
|
145
|
+
if (!meta.expression) {
|
|
146
|
+
return type === QueryType.SELECT ? [] : 0;
|
|
147
|
+
}
|
|
148
|
+
if (typeof meta.expression === 'string') {
|
|
149
|
+
return this.wrapVirtualExpressionInSubquery(meta, meta.expression, where, options, type);
|
|
150
|
+
}
|
|
151
|
+
const em = this.createEntityManager();
|
|
152
|
+
em.setTransactionContext(options.ctx);
|
|
153
|
+
const res = meta.expression(em, where, options);
|
|
154
|
+
if (typeof res === 'string') {
|
|
155
|
+
return this.wrapVirtualExpressionInSubquery(meta, res, where, options, type);
|
|
156
|
+
}
|
|
157
|
+
if (res instanceof QueryBuilder) {
|
|
158
|
+
return this.wrapVirtualExpressionInSubquery(meta, res.getFormattedQuery(), where, options, type);
|
|
159
|
+
}
|
|
160
|
+
if (res instanceof RawQueryFragment) {
|
|
161
|
+
const expr = this.platform.formatQuery(res.sql, res.params);
|
|
162
|
+
return this.wrapVirtualExpressionInSubquery(meta, expr, where, options, type);
|
|
163
|
+
}
|
|
164
|
+
/* v8 ignore next */
|
|
165
|
+
return res;
|
|
296
166
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
167
|
+
async *streamFromVirtual(entityName, where, options) {
|
|
168
|
+
const meta = this.metadata.get(entityName);
|
|
169
|
+
/* v8 ignore next */
|
|
170
|
+
if (!meta.expression) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (typeof meta.expression === 'string') {
|
|
174
|
+
yield* this.wrapVirtualExpressionInSubqueryStream(meta, meta.expression, where, options, QueryType.SELECT);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const em = this.createEntityManager();
|
|
178
|
+
em.setTransactionContext(options.ctx);
|
|
179
|
+
const res = meta.expression(em, where, options, true);
|
|
180
|
+
if (typeof res === 'string') {
|
|
181
|
+
yield* this.wrapVirtualExpressionInSubqueryStream(meta, res, where, options, QueryType.SELECT);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (res instanceof QueryBuilder) {
|
|
185
|
+
yield* this.wrapVirtualExpressionInSubqueryStream(meta, res.getFormattedQuery(), where, options, QueryType.SELECT);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (res instanceof RawQueryFragment) {
|
|
189
|
+
const expr = this.platform.formatQuery(res.sql, res.params);
|
|
190
|
+
yield* this.wrapVirtualExpressionInSubqueryStream(meta, expr, where, options, QueryType.SELECT);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
/* v8 ignore next */
|
|
194
|
+
yield* res;
|
|
195
|
+
}
|
|
196
|
+
async wrapVirtualExpressionInSubquery(meta, expression, where, options, type) {
|
|
197
|
+
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
|
|
198
|
+
qb.setFlag(QueryFlag.DISABLE_PAGINATE);
|
|
199
|
+
const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
|
|
200
|
+
const native = qb.getNativeQuery(false);
|
|
201
|
+
if (type === QueryType.COUNT) {
|
|
202
|
+
native.clear('select').clear('limit').clear('offset').count();
|
|
203
|
+
}
|
|
204
|
+
const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
|
|
205
|
+
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
|
|
206
|
+
const query = native.compile();
|
|
207
|
+
const res = await this.execute(query.sql, query.params, 'all', options.ctx);
|
|
208
|
+
if (type === QueryType.COUNT) {
|
|
209
|
+
return res[0].count;
|
|
210
|
+
}
|
|
211
|
+
if (isCursorPagination && !options.first && !!options.last) {
|
|
212
|
+
res.reverse();
|
|
213
|
+
}
|
|
214
|
+
return res.map(row => this.mapResult(row, meta));
|
|
215
|
+
}
|
|
216
|
+
async *wrapVirtualExpressionInSubqueryStream(meta, expression, where, options, type) {
|
|
217
|
+
const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
|
|
218
|
+
qb.unsetFlag(QueryFlag.DISABLE_PAGINATE);
|
|
219
|
+
const native = qb.getNativeQuery(false);
|
|
220
|
+
const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
|
|
221
|
+
native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
|
|
222
|
+
const query = native.compile();
|
|
223
|
+
const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
|
|
224
|
+
const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
|
|
225
|
+
for await (const row of res) {
|
|
226
|
+
yield this.mapResult(row, meta);
|
|
227
|
+
}
|
|
301
228
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
|
|
231
|
+
* Force balanced strategy to load to-many relations via separate queries.
|
|
232
|
+
*/
|
|
233
|
+
forceBalancedStrategy(options) {
|
|
234
|
+
const clearStrategy = (hints) => {
|
|
235
|
+
return hints.map(hint => ({
|
|
236
|
+
...hint,
|
|
237
|
+
strategy: undefined,
|
|
238
|
+
children: hint.children ? clearStrategy(hint.children) : undefined,
|
|
239
|
+
}));
|
|
240
|
+
};
|
|
241
|
+
const opts = { ...options, strategy: 'balanced' };
|
|
242
|
+
if (Array.isArray(opts.populate)) {
|
|
243
|
+
opts.populate = clearStrategy(opts.populate);
|
|
244
|
+
}
|
|
245
|
+
return opts;
|
|
305
246
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
247
|
+
mapResult(result, meta, populate = [], qb, map = {}) {
|
|
248
|
+
// For TPT inheritance, map aliased parent table columns back to their field names
|
|
249
|
+
if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
|
|
250
|
+
this.mapTPTColumns(result, meta, qb);
|
|
251
|
+
}
|
|
252
|
+
// For TPT polymorphic queries (querying a base class), map child table fields
|
|
253
|
+
if (qb && meta.inheritanceType === 'tpt' && meta.allTPTDescendants?.length) {
|
|
254
|
+
const mainAlias = qb.mainAlias?.aliasName ?? 'e0';
|
|
255
|
+
this.mapTPTChildFields(result, meta, mainAlias, qb, result);
|
|
256
|
+
}
|
|
257
|
+
const ret = super.mapResult(result, meta);
|
|
258
|
+
/* v8 ignore next */
|
|
259
|
+
if (!ret) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (qb) {
|
|
263
|
+
// here we map the aliased results (cartesian product) to an object graph
|
|
264
|
+
this.mapJoinedProps(ret, meta, populate, qb, ret, map);
|
|
265
|
+
}
|
|
266
|
+
return ret;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Maps aliased columns from TPT parent tables back to their original field names.
|
|
270
|
+
* TPT parent columns are selected with aliases like `parent_alias__column_name`,
|
|
271
|
+
* and need to be renamed back to `column_name` for the result mapper to work.
|
|
272
|
+
*/
|
|
273
|
+
mapTPTColumns(result, meta, qb) {
|
|
274
|
+
const tptAliases = qb.state.tptAlias;
|
|
275
|
+
// Walk up the TPT hierarchy
|
|
276
|
+
let parentMeta = meta.tptParent;
|
|
277
|
+
while (parentMeta) {
|
|
278
|
+
const parentAlias = tptAliases[parentMeta.className];
|
|
279
|
+
if (parentAlias) {
|
|
280
|
+
// Rename columns from this parent table
|
|
281
|
+
for (const prop of parentMeta.ownProps) {
|
|
282
|
+
for (const fieldName of prop.fieldNames) {
|
|
283
|
+
const aliasedKey = `${parentAlias}__${fieldName}`;
|
|
284
|
+
if (aliasedKey in result) {
|
|
285
|
+
// Copy the value to the unaliased field name and remove the aliased key
|
|
286
|
+
result[fieldName] = result[aliasedKey];
|
|
287
|
+
delete result[aliasedKey];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
parentMeta = parentMeta.tptParent;
|
|
293
|
+
}
|
|
333
294
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
295
|
+
mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
|
|
296
|
+
const joinedProps = this.joinedProps(meta, populate);
|
|
297
|
+
joinedProps.forEach(hint => {
|
|
298
|
+
const [propName, ref] = hint.field.split(':', 2);
|
|
299
|
+
const prop = meta.properties[propName];
|
|
300
|
+
/* v8 ignore next */
|
|
301
|
+
if (!prop) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
|
|
305
|
+
// Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
|
|
306
|
+
if (prop.polymorphic &&
|
|
307
|
+
prop.polymorphTargets?.length &&
|
|
308
|
+
!ref &&
|
|
309
|
+
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
310
|
+
const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
311
|
+
const pathPrefix = !parentJoinPath ? '[populate]' : '';
|
|
312
|
+
let matched = false;
|
|
313
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
314
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
315
|
+
const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
|
|
316
|
+
const meta2 = targetMeta;
|
|
317
|
+
const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
|
|
318
|
+
const hasPK = meta2
|
|
319
|
+
.getPrimaryProps()
|
|
320
|
+
.every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
|
|
321
|
+
if (hasPK && !matched) {
|
|
322
|
+
matched = true;
|
|
323
|
+
const relationPojo = {};
|
|
324
|
+
const tz = this.platform.getTimezone();
|
|
325
|
+
for (const p of targetProps) {
|
|
326
|
+
this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
|
|
327
|
+
}
|
|
328
|
+
// Inject the entity class constructor so that the factory creates the correct type
|
|
329
|
+
Object.defineProperty(relationPojo, 'constructor', {
|
|
330
|
+
value: meta2.class,
|
|
331
|
+
enumerable: false,
|
|
332
|
+
configurable: true,
|
|
333
|
+
});
|
|
334
|
+
result[prop.name] = relationPojo;
|
|
335
|
+
const populateChildren = hint.children || [];
|
|
336
|
+
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
|
|
337
|
+
}
|
|
338
|
+
// Clean up aliased columns for ALL targets (even non-matching ones)
|
|
339
|
+
for (const p of targetProps) {
|
|
340
|
+
for (const name of p.fieldNames) {
|
|
341
|
+
delete root[`${relationAlias}__${name}`];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (!matched) {
|
|
346
|
+
result[prop.name] = null;
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
351
|
+
const meta2 = prop.targetMeta;
|
|
352
|
+
let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
353
|
+
if (!parentJoinPath) {
|
|
354
|
+
path = '[populate]' + path;
|
|
355
|
+
}
|
|
356
|
+
if (pivotRefJoin) {
|
|
357
|
+
path += '[pivot]';
|
|
358
|
+
}
|
|
359
|
+
const relationAlias = qb.getAliasForJoinPath(path, { matchPopulateJoins: true });
|
|
360
|
+
/* v8 ignore next */
|
|
361
|
+
if (!relationAlias) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
// pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
|
|
365
|
+
if (pivotRefJoin) {
|
|
366
|
+
let item;
|
|
367
|
+
if (prop.inverseJoinColumns.length > 1) {
|
|
368
|
+
// composite keys
|
|
369
|
+
item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const alias = `${relationAlias}__${prop.inverseJoinColumns[0]}`;
|
|
373
|
+
item = root[alias];
|
|
374
|
+
}
|
|
375
|
+
prop.joinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
|
|
376
|
+
prop.inverseJoinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
|
|
377
|
+
result[prop.name] ??= [];
|
|
378
|
+
if (item) {
|
|
379
|
+
result[prop.name].push(item);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
|
|
384
|
+
const targetProps = mapToPk
|
|
385
|
+
? meta2.getPrimaryProps()
|
|
386
|
+
: meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
|
|
387
|
+
// If the primary key value for the relation is null, we know we haven't joined to anything
|
|
388
|
+
// and therefore we don't return any record (since all values would be null)
|
|
389
|
+
const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
|
|
390
|
+
return root[`${relationAlias}__${name}`] != null;
|
|
391
|
+
}));
|
|
392
|
+
if (!hasPK) {
|
|
393
|
+
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
|
|
394
|
+
result[prop.name] = [];
|
|
395
|
+
}
|
|
396
|
+
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
397
|
+
result[prop.name] = null;
|
|
398
|
+
}
|
|
399
|
+
for (const prop of targetProps) {
|
|
400
|
+
for (const name of prop.fieldNames) {
|
|
401
|
+
delete root[`${relationAlias}__${name}`];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
let relationPojo = {};
|
|
407
|
+
meta2.props
|
|
408
|
+
.filter(prop => !ref && prop.persist === false && prop.fieldNames)
|
|
409
|
+
.forEach(prop => {
|
|
410
|
+
/* v8 ignore next */
|
|
411
|
+
if (prop.fieldNames.length > 1) {
|
|
412
|
+
// composite keys
|
|
413
|
+
relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
417
|
+
relationPojo[prop.name] = root[alias];
|
|
418
|
+
}
|
|
375
419
|
});
|
|
376
|
-
|
|
420
|
+
const tz = this.platform.getTimezone();
|
|
421
|
+
for (const prop of targetProps) {
|
|
422
|
+
this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
|
|
423
|
+
}
|
|
424
|
+
// Handle TPT polymorphic child fields - map fields from child table aliases
|
|
425
|
+
this.mapTPTChildFields(relationPojo, meta2, relationAlias, qb, root);
|
|
426
|
+
// properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
|
|
427
|
+
// so we need to delete them after everything is mapped from given level
|
|
428
|
+
for (const prop of targetProps) {
|
|
429
|
+
for (const name of prop.fieldNames) {
|
|
430
|
+
delete root[`${relationAlias}__${name}`];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (mapToPk) {
|
|
434
|
+
const tmp = Object.values(relationPojo);
|
|
435
|
+
/* v8 ignore next */
|
|
436
|
+
relationPojo = (meta2.compositePK ? tmp : tmp[0]);
|
|
437
|
+
}
|
|
438
|
+
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
|
|
439
|
+
result[prop.name] ??= [];
|
|
440
|
+
result[prop.name].push(relationPojo);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
result[prop.name] = relationPojo;
|
|
444
|
+
}
|
|
377
445
|
const populateChildren = hint.children || [];
|
|
378
|
-
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (!relationAlias) {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
// pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
|
|
407
|
-
if (pivotRefJoin) {
|
|
408
|
-
let item;
|
|
409
|
-
if (prop.inverseJoinColumns.length > 1) {
|
|
410
|
-
// composite keys
|
|
411
|
-
item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
|
|
412
|
-
} else {
|
|
413
|
-
const alias = `${relationAlias}__${prop.inverseJoinColumns[0]}`;
|
|
414
|
-
item = root[alias];
|
|
415
|
-
}
|
|
416
|
-
prop.joinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
|
|
417
|
-
prop.inverseJoinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
|
|
418
|
-
result[prop.name] ??= [];
|
|
419
|
-
if (item) {
|
|
420
|
-
result[prop.name].push(item);
|
|
421
|
-
}
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
|
|
425
|
-
const targetProps = mapToPk
|
|
426
|
-
? meta2.getPrimaryProps()
|
|
427
|
-
: meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
|
|
428
|
-
// If the primary key value for the relation is null, we know we haven't joined to anything
|
|
429
|
-
// and therefore we don't return any record (since all values would be null)
|
|
430
|
-
const hasPK = meta2.getPrimaryProps().every(pk =>
|
|
431
|
-
pk.fieldNames.every(name => {
|
|
432
|
-
return root[`${relationAlias}__${name}`] != null;
|
|
433
|
-
}),
|
|
434
|
-
);
|
|
435
|
-
if (!hasPK) {
|
|
436
|
-
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
|
|
437
|
-
result[prop.name] = [];
|
|
438
|
-
}
|
|
439
|
-
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
440
|
-
result[prop.name] = null;
|
|
441
|
-
}
|
|
442
|
-
for (const prop of targetProps) {
|
|
443
|
-
for (const name of prop.fieldNames) {
|
|
444
|
-
delete root[`${relationAlias}__${name}`];
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
let relationPojo = {};
|
|
450
|
-
meta2.props
|
|
451
|
-
.filter(prop => !ref && prop.persist === false && prop.fieldNames)
|
|
452
|
-
.forEach(prop => {
|
|
453
|
-
/* v8 ignore next */
|
|
454
|
-
if (prop.fieldNames.length > 1) {
|
|
446
|
+
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Maps a single property from a joined result row into the relation pojo.
|
|
451
|
+
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
|
|
452
|
+
*/
|
|
453
|
+
mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
|
|
454
|
+
if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (prop.polymorphic) {
|
|
458
|
+
const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
459
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
460
|
+
const pkFieldNames = prop.fieldNames.slice(1);
|
|
461
|
+
const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
462
|
+
const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
|
|
463
|
+
if (discriminatorValue != null && pkValue != null) {
|
|
464
|
+
relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
relationPojo[prop.name] = null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else if (prop.fieldNames.length > 1) {
|
|
455
471
|
// composite keys
|
|
456
|
-
|
|
457
|
-
|
|
472
|
+
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
473
|
+
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
474
|
+
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
475
|
+
}
|
|
476
|
+
else if (prop.runtimeType === 'Date') {
|
|
477
|
+
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
478
|
+
const value = root[alias];
|
|
479
|
+
if (tz &&
|
|
480
|
+
tz !== 'local' &&
|
|
481
|
+
typeof value === 'string' &&
|
|
482
|
+
!value.includes('+') &&
|
|
483
|
+
value.lastIndexOf('-') < 11 &&
|
|
484
|
+
!value.endsWith('Z')) {
|
|
485
|
+
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
486
|
+
}
|
|
487
|
+
else if (['string', 'number'].includes(typeof value)) {
|
|
488
|
+
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
relationPojo[prop.name] = value;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
458
495
|
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
459
496
|
relationPojo[prop.name] = root[alias];
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const tmp = Object.values(relationPojo);
|
|
477
|
-
/* v8 ignore next */
|
|
478
|
-
relationPojo = meta2.compositePK ? tmp : tmp[0];
|
|
479
|
-
}
|
|
480
|
-
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
|
|
481
|
-
result[prop.name] ??= [];
|
|
482
|
-
result[prop.name].push(relationPojo);
|
|
483
|
-
} else {
|
|
484
|
-
result[prop.name] = relationPojo;
|
|
485
|
-
}
|
|
486
|
-
const populateChildren = hint.children || [];
|
|
487
|
-
this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Maps a single property from a joined result row into the relation pojo.
|
|
492
|
-
* Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
|
|
493
|
-
*/
|
|
494
|
-
mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
|
|
495
|
-
if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
if (prop.polymorphic) {
|
|
499
|
-
const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
500
|
-
const discriminatorValue = root[discriminatorAlias];
|
|
501
|
-
const pkFieldNames = prop.fieldNames.slice(1);
|
|
502
|
-
const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
503
|
-
const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
|
|
504
|
-
if (discriminatorValue != null && pkValue != null) {
|
|
505
|
-
relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
|
|
506
|
-
} else {
|
|
507
|
-
relationPojo[prop.name] = null;
|
|
508
|
-
}
|
|
509
|
-
} else if (prop.fieldNames.length > 1) {
|
|
510
|
-
// composite keys
|
|
511
|
-
const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
|
|
512
|
-
const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
|
|
513
|
-
relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
|
|
514
|
-
} else if (prop.runtimeType === 'Date') {
|
|
515
|
-
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
516
|
-
const value = root[alias];
|
|
517
|
-
if (
|
|
518
|
-
tz &&
|
|
519
|
-
tz !== 'local' &&
|
|
520
|
-
typeof value === 'string' &&
|
|
521
|
-
!value.includes('+') &&
|
|
522
|
-
value.lastIndexOf('-') < 11 &&
|
|
523
|
-
!value.endsWith('Z')
|
|
524
|
-
) {
|
|
525
|
-
relationPojo[prop.name] = this.platform.parseDate(value + tz);
|
|
526
|
-
} else if (['string', 'number'].includes(typeof value)) {
|
|
527
|
-
relationPojo[prop.name] = this.platform.parseDate(value);
|
|
528
|
-
} else {
|
|
529
|
-
relationPojo[prop.name] = value;
|
|
530
|
-
}
|
|
531
|
-
} else {
|
|
532
|
-
const alias = `${relationAlias}__${prop.fieldNames[0]}`;
|
|
533
|
-
relationPojo[prop.name] = root[alias];
|
|
534
|
-
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
535
|
-
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
536
|
-
if (Array.isArray(item)) {
|
|
537
|
-
relationPojo[prop.name] = item.map(row =>
|
|
538
|
-
row == null ? row : this.comparator.mapResult(prop.targetMeta, row),
|
|
539
|
-
);
|
|
540
|
-
} else {
|
|
541
|
-
relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (options?.deleteFromRoot) {
|
|
546
|
-
for (const name of prop.fieldNames) {
|
|
547
|
-
delete root[`${relationAlias}__${name}`];
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
async count(entityName, where, options = {}) {
|
|
552
|
-
const meta = this.metadata.get(entityName);
|
|
553
|
-
if (meta.virtual) {
|
|
554
|
-
return this.countVirtual(entityName, where, options);
|
|
555
|
-
}
|
|
556
|
-
if (options.unionWhere?.length) {
|
|
557
|
-
where = await this.applyUnionWhere(meta, where, options);
|
|
558
|
-
}
|
|
559
|
-
options = { populate: [], ...options };
|
|
560
|
-
const populate = options.populate;
|
|
561
|
-
const joinedProps = this.joinedProps(meta, populate, options);
|
|
562
|
-
const schema = this.getSchemaName(meta, options);
|
|
563
|
-
const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
|
|
564
|
-
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
565
|
-
if (meta && !Utils.isEmpty(populate)) {
|
|
566
|
-
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
567
|
-
}
|
|
568
|
-
this.validateSqlOptions(options);
|
|
569
|
-
qb.state.resolvedPopulateWhere = options._populateWhere;
|
|
570
|
-
qb.indexHint(options.indexHint)
|
|
571
|
-
.collation(options.collation)
|
|
572
|
-
.comment(options.comments)
|
|
573
|
-
.hintComment(options.hintComments)
|
|
574
|
-
.groupBy(options.groupBy)
|
|
575
|
-
.having(options.having)
|
|
576
|
-
.populate(
|
|
577
|
-
populate,
|
|
578
|
-
joinedProps.length > 0 ? populateWhere : undefined,
|
|
579
|
-
joinedProps.length > 0 ? options.populateFilter : undefined,
|
|
580
|
-
)
|
|
581
|
-
.withSchema(schema)
|
|
582
|
-
.where(where);
|
|
583
|
-
if (options.em) {
|
|
584
|
-
await qb.applyJoinedFilters(options.em, options.filters);
|
|
585
|
-
}
|
|
586
|
-
return this.rethrow(qb.getCount());
|
|
587
|
-
}
|
|
588
|
-
async nativeInsert(entityName, data, options = {}) {
|
|
589
|
-
options.convertCustomTypes ??= true;
|
|
590
|
-
const meta = this.metadata.get(entityName);
|
|
591
|
-
const collections = this.extractManyToMany(meta, data);
|
|
592
|
-
const qb = this.createQueryBuilder(
|
|
593
|
-
entityName,
|
|
594
|
-
options.ctx,
|
|
595
|
-
'write',
|
|
596
|
-
options.convertCustomTypes,
|
|
597
|
-
options.loggerContext,
|
|
598
|
-
).withSchema(this.getSchemaName(meta, options));
|
|
599
|
-
const res = await this.rethrow(qb.insert(data).execute('run', false));
|
|
600
|
-
res.row = res.row || {};
|
|
601
|
-
let pk;
|
|
602
|
-
if (meta.primaryKeys.length > 1) {
|
|
603
|
-
// owner has composite pk
|
|
604
|
-
pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
|
|
605
|
-
} else {
|
|
606
|
-
/* v8 ignore next */
|
|
607
|
-
res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
|
|
608
|
-
if (options.convertCustomTypes && meta?.getPrimaryProp().customType) {
|
|
609
|
-
pk = [meta.getPrimaryProp().customType.convertToDatabaseValue(res.insertId, this.platform)];
|
|
610
|
-
} else {
|
|
611
|
-
pk = [res.insertId];
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
await this.processManyToMany(meta, pk, collections, false, options);
|
|
615
|
-
return res;
|
|
616
|
-
}
|
|
617
|
-
async nativeInsertMany(entityName, data, options = {}, transform) {
|
|
618
|
-
options.processCollections ??= true;
|
|
619
|
-
options.convertCustomTypes ??= true;
|
|
620
|
-
const entityMeta = this.metadata.get(entityName);
|
|
621
|
-
const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
|
|
622
|
-
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
623
|
-
const pks = this.getPrimaryKeyFields(meta);
|
|
624
|
-
const set = new Set();
|
|
625
|
-
data.forEach(row => Utils.keys(row).forEach(k => set.add(k)));
|
|
626
|
-
const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
|
|
627
|
-
// For STI with conflicting fieldNames, include all alternative columns
|
|
628
|
-
let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
|
|
629
|
-
const duplicates = Utils.findDuplicates(fields);
|
|
630
|
-
const params = [];
|
|
631
|
-
if (duplicates.length) {
|
|
632
|
-
fields = Utils.unique(fields);
|
|
633
|
-
}
|
|
634
|
-
const tableName = this.getTableName(meta, options);
|
|
635
|
-
let sql = `insert into ${tableName} `;
|
|
636
|
-
sql +=
|
|
637
|
-
fields.length > 0
|
|
638
|
-
? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')'
|
|
639
|
-
: `(${this.platform.quoteIdentifier(pks[0])})`;
|
|
640
|
-
if (this.platform.usesOutputStatement()) {
|
|
641
|
-
const returningProps = this.getTableProps(meta)
|
|
642
|
-
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
643
|
-
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
644
|
-
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
645
|
-
sql +=
|
|
646
|
-
returningFields.length > 0
|
|
647
|
-
? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`
|
|
648
|
-
: '';
|
|
649
|
-
}
|
|
650
|
-
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
|
|
651
|
-
sql += ' values ';
|
|
652
|
-
} else {
|
|
653
|
-
sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
|
|
497
|
+
if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
|
|
498
|
+
const item = parseJsonSafe(relationPojo[prop.name]);
|
|
499
|
+
if (Array.isArray(item)) {
|
|
500
|
+
relationPojo[prop.name] = item.map(row => row == null ? row : this.comparator.mapResult(prop.targetMeta, row));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
relationPojo[prop.name] =
|
|
504
|
+
item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (options?.deleteFromRoot) {
|
|
509
|
+
for (const name of prop.fieldNames) {
|
|
510
|
+
delete root[`${relationAlias}__${name}`];
|
|
511
|
+
}
|
|
512
|
+
}
|
|
654
513
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
514
|
+
async count(entityName, where, options = {}) {
|
|
515
|
+
const meta = this.metadata.get(entityName);
|
|
516
|
+
if (meta.virtual) {
|
|
517
|
+
return this.countVirtual(entityName, where, options);
|
|
518
|
+
}
|
|
519
|
+
if (options.unionWhere?.length) {
|
|
520
|
+
where = await this.applyUnionWhere(meta, where, options);
|
|
521
|
+
}
|
|
522
|
+
options = { populate: [], ...options };
|
|
523
|
+
const populate = options.populate;
|
|
524
|
+
const joinedProps = this.joinedProps(meta, populate, options);
|
|
525
|
+
const schema = this.getSchemaName(meta, options);
|
|
526
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
|
|
527
|
+
const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
|
|
528
|
+
if (meta && !Utils.isEmpty(populate)) {
|
|
529
|
+
this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
|
|
530
|
+
}
|
|
531
|
+
this.validateSqlOptions(options);
|
|
532
|
+
qb.state.resolvedPopulateWhere = options._populateWhere;
|
|
533
|
+
qb.indexHint(options.indexHint)
|
|
534
|
+
.collation(options.collation)
|
|
535
|
+
.comment(options.comments)
|
|
536
|
+
.hintComment(options.hintComments)
|
|
537
|
+
.groupBy(options.groupBy)
|
|
538
|
+
.having(options.having)
|
|
539
|
+
.populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
|
|
540
|
+
.withSchema(schema)
|
|
541
|
+
.where(where);
|
|
542
|
+
if (options.em) {
|
|
543
|
+
await qb.applyJoinedFilters(options.em, options.filters);
|
|
544
|
+
}
|
|
545
|
+
return this.rethrow(qb.getCount());
|
|
546
|
+
}
|
|
547
|
+
async nativeInsert(entityName, data, options = {}) {
|
|
548
|
+
options.convertCustomTypes ??= true;
|
|
549
|
+
const meta = this.metadata.get(entityName);
|
|
550
|
+
const collections = this.extractManyToMany(meta, data);
|
|
551
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
552
|
+
const res = await this.rethrow(qb.insert(data).execute('run', false));
|
|
553
|
+
res.row = res.row || {};
|
|
554
|
+
let pk;
|
|
555
|
+
if (meta.primaryKeys.length > 1) {
|
|
556
|
+
// owner has composite pk
|
|
557
|
+
pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
/* v8 ignore next */
|
|
561
|
+
res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
|
|
562
|
+
if (options.convertCustomTypes && meta?.getPrimaryProp().customType) {
|
|
563
|
+
pk = [meta.getPrimaryProp().customType.convertToDatabaseValue(res.insertId, this.platform)];
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
pk = [res.insertId];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
await this.processManyToMany(meta, pk, collections, false, options);
|
|
570
|
+
return res;
|
|
571
|
+
}
|
|
572
|
+
async nativeInsertMany(entityName, data, options = {}, transform) {
|
|
573
|
+
options.processCollections ??= true;
|
|
574
|
+
options.convertCustomTypes ??= true;
|
|
575
|
+
const entityMeta = this.metadata.get(entityName);
|
|
576
|
+
const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
|
|
577
|
+
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
578
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
579
|
+
const set = new Set();
|
|
580
|
+
data.forEach(row => Utils.keys(row).forEach(k => set.add(k)));
|
|
581
|
+
const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
|
|
582
|
+
// For STI with conflicting fieldNames, include all alternative columns
|
|
583
|
+
let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
|
|
584
|
+
const duplicates = Utils.findDuplicates(fields);
|
|
585
|
+
const params = [];
|
|
586
|
+
if (duplicates.length) {
|
|
587
|
+
fields = Utils.unique(fields);
|
|
588
|
+
}
|
|
589
|
+
const tableName = this.getTableName(meta, options);
|
|
590
|
+
let sql = `insert into ${tableName} `;
|
|
591
|
+
sql +=
|
|
592
|
+
fields.length > 0
|
|
593
|
+
? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')'
|
|
594
|
+
: `(${this.platform.quoteIdentifier(pks[0])})`;
|
|
595
|
+
if (this.platform.usesOutputStatement()) {
|
|
596
|
+
const returningProps = this.getTableProps(meta)
|
|
597
|
+
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
598
|
+
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
599
|
+
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
600
|
+
sql +=
|
|
601
|
+
returningFields.length > 0
|
|
602
|
+
? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`
|
|
603
|
+
: '';
|
|
604
|
+
}
|
|
605
|
+
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
|
|
606
|
+
sql += ' values ';
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
|
|
610
|
+
}
|
|
611
|
+
const addParams = (prop, row) => {
|
|
612
|
+
const rowValue = row[prop.name];
|
|
613
|
+
if (prop.nullable && rowValue === null) {
|
|
614
|
+
params.push(null);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
let value = rowValue ?? prop.default;
|
|
618
|
+
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
|
|
619
|
+
if (prop.array && value) {
|
|
620
|
+
value = this.platform.cloneEmbeddable(value);
|
|
621
|
+
for (let i = 0; i < value.length; i++) {
|
|
622
|
+
const item = value[i];
|
|
623
|
+
value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
|
|
624
|
+
}
|
|
719
625
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
});
|
|
723
|
-
newFields.forEach((field, idx) => {
|
|
724
|
-
if (!duplicates.includes(field) || !usedDups.includes(field)) {
|
|
725
|
-
params.push(param[idx]);
|
|
726
|
-
keys.push('?');
|
|
727
|
-
usedDups.push(field);
|
|
626
|
+
else {
|
|
627
|
+
value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
|
|
728
628
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
629
|
+
}
|
|
630
|
+
if (typeof value === 'undefined' && this.platform.usesDefaultKeyword()) {
|
|
631
|
+
params.push(raw('default'));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (options.convertCustomTypes && prop.customType) {
|
|
635
|
+
params.push(prop.customType.convertToDatabaseValue(value, this.platform, { key: prop.name, mode: 'query-data' }));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
params.push(value);
|
|
639
|
+
};
|
|
640
|
+
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
|
|
641
|
+
sql += data
|
|
642
|
+
.map(row => {
|
|
643
|
+
const keys = [];
|
|
644
|
+
const usedDups = [];
|
|
645
|
+
props.forEach(prop => {
|
|
646
|
+
// For STI with conflicting fieldNames, use discriminator to determine which field gets value
|
|
647
|
+
if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
|
|
648
|
+
const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
|
|
649
|
+
for (const field of prop.stiFieldNames) {
|
|
650
|
+
params.push(field === activeField ? row[prop.name] : null);
|
|
651
|
+
keys.push('?');
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (prop.fieldNames.length > 1) {
|
|
656
|
+
const newFields = [];
|
|
657
|
+
let rawParam;
|
|
658
|
+
const target = row[prop.name];
|
|
659
|
+
if (prop.polymorphic && target instanceof PolymorphicRef) {
|
|
660
|
+
rawParam = target.toTuple();
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
|
|
664
|
+
}
|
|
665
|
+
// Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
|
|
666
|
+
const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
|
|
667
|
+
const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
|
|
668
|
+
// TODO(v7): instead of making this conditional here, the entity snapshot should respect `ownColumns`,
|
|
669
|
+
// but that means changing the compiled PK getters, which might be seen as breaking
|
|
670
|
+
const columns = allParam.length > 1 ? prop.fieldNames : prop.ownColumns;
|
|
671
|
+
const param = [];
|
|
672
|
+
columns.forEach((field, idx) => {
|
|
673
|
+
if (usedDups.includes(field)) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
newFields.push(field);
|
|
677
|
+
param.push(allParam[idx]);
|
|
678
|
+
});
|
|
679
|
+
newFields.forEach((field, idx) => {
|
|
680
|
+
if (!duplicates.includes(field) || !usedDups.includes(field)) {
|
|
681
|
+
params.push(param[idx]);
|
|
682
|
+
keys.push('?');
|
|
683
|
+
usedDups.push(field);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const field = prop.fieldNames[0];
|
|
689
|
+
if (!duplicates.includes(field) || !usedDups.includes(field)) {
|
|
690
|
+
if (prop.customType &&
|
|
691
|
+
!prop.object &&
|
|
692
|
+
'convertToDatabaseValueSQL' in prop.customType &&
|
|
693
|
+
row[prop.name] != null &&
|
|
694
|
+
!isRaw(row[prop.name])) {
|
|
695
|
+
keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
keys.push('?');
|
|
699
|
+
}
|
|
700
|
+
addParams(prop, row);
|
|
701
|
+
usedDups.push(field);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
return '(' + (keys.join(', ') || 'default') + ')';
|
|
706
|
+
})
|
|
707
|
+
.join(', ');
|
|
708
|
+
}
|
|
709
|
+
if (meta && this.platform.usesReturningStatement()) {
|
|
710
|
+
const returningProps = this.getTableProps(meta)
|
|
711
|
+
.filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
|
|
712
|
+
.filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
|
|
713
|
+
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
|
|
714
|
+
/* v8 ignore next */
|
|
715
|
+
sql +=
|
|
716
|
+
returningFields.length > 0
|
|
717
|
+
? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
|
|
718
|
+
: '';
|
|
719
|
+
}
|
|
720
|
+
if (transform) {
|
|
721
|
+
sql = transform(sql);
|
|
722
|
+
}
|
|
723
|
+
const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
|
|
724
|
+
let pk;
|
|
725
|
+
/* v8 ignore next */
|
|
726
|
+
if (pks.length > 1) {
|
|
727
|
+
// owner has composite pk
|
|
728
|
+
pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
res.row ??= {};
|
|
732
|
+
res.rows ??= [];
|
|
733
|
+
pk = data.map((d, i) => d[pks[0]] ?? res.rows[i]?.[pks[0]]).map(d => [d]);
|
|
734
|
+
res.insertId = res.insertId || res.row[pks[0]];
|
|
735
|
+
}
|
|
736
|
+
for (let i = 0; i < collections.length; i++) {
|
|
737
|
+
await this.processManyToMany(meta, pk[i], collections[i], false, options);
|
|
738
|
+
}
|
|
739
|
+
return res;
|
|
740
|
+
}
|
|
741
|
+
async nativeUpdate(entityName, where, data, options = {}) {
|
|
742
|
+
options.convertCustomTypes ??= true;
|
|
743
|
+
const meta = this.metadata.get(entityName);
|
|
744
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
745
|
+
const collections = this.extractManyToMany(meta, data);
|
|
746
|
+
let res = { affectedRows: 0, insertId: 0, row: {} };
|
|
747
|
+
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
748
|
+
/* v8 ignore next */
|
|
749
|
+
where = { [meta.primaryKeys[0] ?? pks[0]]: where };
|
|
750
|
+
}
|
|
751
|
+
if (!options.upsert && options.unionWhere?.length) {
|
|
752
|
+
where = (await this.applyUnionWhere(meta, where, options, true));
|
|
753
|
+
}
|
|
754
|
+
if (Utils.hasObjectKeys(data)) {
|
|
755
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
756
|
+
if (options.upsert) {
|
|
757
|
+
/* v8 ignore next */
|
|
758
|
+
const uniqueFields = options.onConflictFields ??
|
|
759
|
+
(Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
|
|
760
|
+
const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
|
|
761
|
+
qb.insert(data)
|
|
762
|
+
.onConflict(uniqueFields)
|
|
763
|
+
.returning(returning);
|
|
764
|
+
if (!options.onConflictAction || options.onConflictAction === 'merge') {
|
|
765
|
+
const fields = getOnConflictFields(meta, data, uniqueFields, options);
|
|
766
|
+
qb.merge(fields);
|
|
743
767
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
: '';
|
|
763
|
-
}
|
|
764
|
-
if (transform) {
|
|
765
|
-
sql = transform(sql);
|
|
766
|
-
}
|
|
767
|
-
const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
|
|
768
|
-
let pk;
|
|
769
|
-
/* v8 ignore next */
|
|
770
|
-
if (pks.length > 1) {
|
|
771
|
-
// owner has composite pk
|
|
772
|
-
pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
|
|
773
|
-
} else {
|
|
774
|
-
res.row ??= {};
|
|
775
|
-
res.rows ??= [];
|
|
776
|
-
pk = data.map((d, i) => d[pks[0]] ?? res.rows[i]?.[pks[0]]).map(d => [d]);
|
|
777
|
-
res.insertId = res.insertId || res.row[pks[0]];
|
|
778
|
-
}
|
|
779
|
-
for (let i = 0; i < collections.length; i++) {
|
|
780
|
-
await this.processManyToMany(meta, pk[i], collections[i], false, options);
|
|
781
|
-
}
|
|
782
|
-
return res;
|
|
783
|
-
}
|
|
784
|
-
async nativeUpdate(entityName, where, data, options = {}) {
|
|
785
|
-
options.convertCustomTypes ??= true;
|
|
786
|
-
const meta = this.metadata.get(entityName);
|
|
787
|
-
const pks = this.getPrimaryKeyFields(meta);
|
|
788
|
-
const collections = this.extractManyToMany(meta, data);
|
|
789
|
-
let res = { affectedRows: 0, insertId: 0, row: {} };
|
|
790
|
-
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
791
|
-
/* v8 ignore next */
|
|
792
|
-
where = { [meta.primaryKeys[0] ?? pks[0]]: where };
|
|
793
|
-
}
|
|
794
|
-
if (!options.upsert && options.unionWhere?.length) {
|
|
795
|
-
where = await this.applyUnionWhere(meta, where, options, true);
|
|
796
|
-
}
|
|
797
|
-
if (Utils.hasObjectKeys(data)) {
|
|
798
|
-
const qb = this.createQueryBuilder(
|
|
799
|
-
entityName,
|
|
800
|
-
options.ctx,
|
|
801
|
-
'write',
|
|
802
|
-
options.convertCustomTypes,
|
|
803
|
-
options.loggerContext,
|
|
804
|
-
).withSchema(this.getSchemaName(meta, options));
|
|
805
|
-
if (options.upsert) {
|
|
768
|
+
if (options.onConflictAction === 'ignore') {
|
|
769
|
+
qb.ignore();
|
|
770
|
+
}
|
|
771
|
+
if (options.onConflictWhere) {
|
|
772
|
+
qb.where(options.onConflictWhere);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
else {
|
|
776
|
+
qb.update(data).where(where);
|
|
777
|
+
// reload generated columns and version fields
|
|
778
|
+
const returning = [];
|
|
779
|
+
meta.props
|
|
780
|
+
.filter(prop => (prop.generated && !prop.primary) || prop.version)
|
|
781
|
+
.forEach(prop => returning.push(prop.name));
|
|
782
|
+
qb.returning(returning);
|
|
783
|
+
}
|
|
784
|
+
res = await this.rethrow(qb.execute('run', false));
|
|
785
|
+
}
|
|
806
786
|
/* v8 ignore next */
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (options.
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
787
|
+
const pk = pks.map(pk => Utils.extractPK(data[pk] || where, meta));
|
|
788
|
+
await this.processManyToMany(meta, pk, collections, true, options);
|
|
789
|
+
return res;
|
|
790
|
+
}
|
|
791
|
+
async nativeUpdateMany(entityName, where, data, options = {}, transform) {
|
|
792
|
+
options.processCollections ??= true;
|
|
793
|
+
options.convertCustomTypes ??= true;
|
|
794
|
+
const meta = this.metadata.get(entityName);
|
|
795
|
+
if (options.upsert) {
|
|
796
|
+
const uniqueFields = options.onConflictFields ??
|
|
797
|
+
(Utils.isPlainObject(where[0])
|
|
798
|
+
? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
|
|
799
|
+
: meta.primaryKeys);
|
|
800
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
801
|
+
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
|
|
802
|
+
qb.insert(data)
|
|
803
|
+
.onConflict(uniqueFields)
|
|
804
|
+
.returning(returning);
|
|
805
|
+
if (!options.onConflictAction || options.onConflictAction === 'merge') {
|
|
806
|
+
const fields = getOnConflictFields(meta, data[0], uniqueFields, options);
|
|
807
|
+
qb.merge(fields);
|
|
808
|
+
}
|
|
809
|
+
if (options.onConflictAction === 'ignore') {
|
|
810
|
+
qb.ignore();
|
|
811
|
+
}
|
|
812
|
+
if (options.onConflictWhere) {
|
|
813
|
+
qb.where(options.onConflictWhere);
|
|
814
|
+
}
|
|
815
|
+
return this.rethrow(qb.execute('run', false));
|
|
816
|
+
}
|
|
817
|
+
const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
|
|
818
|
+
const keys = new Set();
|
|
819
|
+
const fields = new Set();
|
|
820
|
+
const returning = new Set();
|
|
821
|
+
for (const row of data) {
|
|
822
|
+
for (const k of Utils.keys(row)) {
|
|
823
|
+
keys.add(k);
|
|
824
|
+
if (isRaw(row[k])) {
|
|
825
|
+
returning.add(k);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
823
829
|
// reload generated columns and version fields
|
|
824
|
-
|
|
825
|
-
meta.
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const pks = Utils.getOrderedPrimaryKeys(cond, meta);
|
|
919
|
-
sql += ` when (${pkCond}) then `;
|
|
920
|
-
if (
|
|
921
|
-
prop.customType &&
|
|
922
|
-
!prop.object &&
|
|
923
|
-
'convertToDatabaseValueSQL' in prop.customType &&
|
|
924
|
-
data[idx][prop.name] != null &&
|
|
925
|
-
!isRaw(data[idx][key])
|
|
926
|
-
) {
|
|
927
|
-
sql += prop.customType.convertToDatabaseValueSQL('?', this.platform);
|
|
928
|
-
} else {
|
|
929
|
-
sql += '?';
|
|
930
|
-
}
|
|
931
|
-
params.push(...pks);
|
|
932
|
-
addParams(prop, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
sql += ` else ${this.platform.quoteIdentifier(fieldName)} end, `;
|
|
936
|
-
return sql;
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
if (meta.versionProperty) {
|
|
940
|
-
const versionProperty = meta.properties[meta.versionProperty];
|
|
941
|
-
const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
|
|
942
|
-
sql += `${quotedFieldName} = `;
|
|
943
|
-
if (versionProperty.runtimeType === 'Date') {
|
|
944
|
-
sql += this.platform.getCurrentTimestampSQL(versionProperty.length);
|
|
945
|
-
} else {
|
|
946
|
-
sql += `${quotedFieldName} + 1`;
|
|
947
|
-
}
|
|
948
|
-
sql += `, `;
|
|
949
|
-
}
|
|
950
|
-
sql = sql.substring(0, sql.length - 2) + ' where ';
|
|
951
|
-
const pkProps = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
|
|
952
|
-
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
|
|
953
|
-
sql +=
|
|
954
|
-
pks.length > 1
|
|
955
|
-
? `(${pks.map(pk => this.platform.quoteIdentifier(pk)).join(', ')})`
|
|
956
|
-
: this.platform.quoteIdentifier(pks[0]);
|
|
957
|
-
const conds = where.map(cond => {
|
|
958
|
-
if (Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
|
|
959
|
-
cond = Object.values(cond)[0];
|
|
960
|
-
}
|
|
961
|
-
if (pks.length > 1) {
|
|
962
|
-
pkProps.forEach(pk => {
|
|
963
|
-
if (Array.isArray(cond[pk])) {
|
|
964
|
-
params.push(...Utils.flatten(cond[pk]));
|
|
965
|
-
} else {
|
|
966
|
-
params.push(cond[pk]);
|
|
967
|
-
}
|
|
830
|
+
meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
|
|
831
|
+
const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
|
|
832
|
+
.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
|
|
833
|
+
.join(' and ');
|
|
834
|
+
const params = [];
|
|
835
|
+
let sql = `update ${this.getTableName(meta, options)} set `;
|
|
836
|
+
const addParams = (prop, value) => {
|
|
837
|
+
if (prop.kind === ReferenceKind.EMBEDDED && prop.object) {
|
|
838
|
+
if (prop.array && value) {
|
|
839
|
+
for (let i = 0; i < value.length; i++) {
|
|
840
|
+
const item = value[i];
|
|
841
|
+
value[i] = this.mapDataToFieldNames(item, false, prop.embeddedProps, options.convertCustomTypes);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
value = this.mapDataToFieldNames(value, false, prop.embeddedProps, options.convertCustomTypes);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
params.push(value ?? null);
|
|
849
|
+
};
|
|
850
|
+
for (const key of keys) {
|
|
851
|
+
const prop = meta.properties[key] ?? meta.root.properties[key];
|
|
852
|
+
if (prop.polymorphic && prop.fieldNames.length > 1) {
|
|
853
|
+
for (let idx = 0; idx < data.length; idx++) {
|
|
854
|
+
const rowValue = data[idx][key];
|
|
855
|
+
if (rowValue instanceof PolymorphicRef) {
|
|
856
|
+
data[idx][key] = rowValue.toTuple();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
|
|
861
|
+
if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
fields.add(fieldName);
|
|
865
|
+
sql += `${this.platform.quoteIdentifier(fieldName)} = case`;
|
|
866
|
+
where.forEach((cond, idx) => {
|
|
867
|
+
if (key in data[idx]) {
|
|
868
|
+
const pks = Utils.getOrderedPrimaryKeys(cond, meta);
|
|
869
|
+
sql += ` when (${pkCond}) then `;
|
|
870
|
+
if (prop.customType &&
|
|
871
|
+
!prop.object &&
|
|
872
|
+
'convertToDatabaseValueSQL' in prop.customType &&
|
|
873
|
+
data[idx][prop.name] != null &&
|
|
874
|
+
!isRaw(data[idx][key])) {
|
|
875
|
+
sql += prop.customType.convertToDatabaseValueSQL('?', this.platform);
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
sql += '?';
|
|
879
|
+
}
|
|
880
|
+
params.push(...pks);
|
|
881
|
+
addParams(prop, prop.fieldNames.length > 1 ? data[idx][key]?.[fieldNameIdx] : data[idx][key]);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
sql += ` else ${this.platform.quoteIdentifier(fieldName)} end, `;
|
|
885
|
+
return sql;
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
if (meta.versionProperty) {
|
|
889
|
+
const versionProperty = meta.properties[meta.versionProperty];
|
|
890
|
+
const quotedFieldName = this.platform.quoteIdentifier(versionProperty.fieldNames[0]);
|
|
891
|
+
sql += `${quotedFieldName} = `;
|
|
892
|
+
if (versionProperty.runtimeType === 'Date') {
|
|
893
|
+
sql += this.platform.getCurrentTimestampSQL(versionProperty.length);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
sql += `${quotedFieldName} + 1`;
|
|
897
|
+
}
|
|
898
|
+
sql += `, `;
|
|
899
|
+
}
|
|
900
|
+
sql = sql.substring(0, sql.length - 2) + ' where ';
|
|
901
|
+
const pkProps = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
|
|
902
|
+
const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
|
|
903
|
+
sql +=
|
|
904
|
+
pks.length > 1
|
|
905
|
+
? `(${pks.map(pk => this.platform.quoteIdentifier(pk)).join(', ')})`
|
|
906
|
+
: this.platform.quoteIdentifier(pks[0]);
|
|
907
|
+
const conds = where.map(cond => {
|
|
908
|
+
if (Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
|
|
909
|
+
cond = Object.values(cond)[0];
|
|
910
|
+
}
|
|
911
|
+
if (pks.length > 1) {
|
|
912
|
+
pkProps.forEach(pk => {
|
|
913
|
+
if (Array.isArray(cond[pk])) {
|
|
914
|
+
params.push(...Utils.flatten(cond[pk]));
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
params.push(cond[pk]);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
return `(${Array.from({ length: pks.length }).fill('?').join(', ')})`;
|
|
921
|
+
}
|
|
922
|
+
params.push(cond);
|
|
923
|
+
return '?';
|
|
968
924
|
});
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
/* v8 ignore next */
|
|
978
|
-
sql +=
|
|
979
|
-
returningFields.length > 0
|
|
980
|
-
? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
|
|
981
|
-
: '';
|
|
982
|
-
}
|
|
983
|
-
if (transform) {
|
|
984
|
-
sql = transform(sql, params);
|
|
985
|
-
}
|
|
986
|
-
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
|
|
987
|
-
for (let i = 0; i < collections.length; i++) {
|
|
988
|
-
await this.processManyToMany(meta, where[i], collections[i], false, options);
|
|
989
|
-
}
|
|
990
|
-
return res;
|
|
991
|
-
}
|
|
992
|
-
async nativeDelete(entityName, where, options = {}) {
|
|
993
|
-
const meta = this.metadata.get(entityName);
|
|
994
|
-
const pks = this.getPrimaryKeyFields(meta);
|
|
995
|
-
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
996
|
-
where = { [pks[0]]: where };
|
|
997
|
-
}
|
|
998
|
-
if (options.unionWhere?.length) {
|
|
999
|
-
where = await this.applyUnionWhere(meta, where, options, true);
|
|
1000
|
-
}
|
|
1001
|
-
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
1002
|
-
.delete(where)
|
|
1003
|
-
.withSchema(this.getSchemaName(meta, options));
|
|
1004
|
-
return this.rethrow(qb.execute('run', false));
|
|
1005
|
-
}
|
|
1006
|
-
/**
|
|
1007
|
-
* Fast comparison for collection snapshots that are represented by PK arrays.
|
|
1008
|
-
* Compares scalars via `===` and fallbacks to Utils.equals()` for more complex types like Buffer.
|
|
1009
|
-
* Always expects the same length of the arrays, since we only compare PKs of the same entity type.
|
|
1010
|
-
*/
|
|
1011
|
-
comparePrimaryKeyArrays(a, b) {
|
|
1012
|
-
for (let i = a.length; i-- !== 0; ) {
|
|
1013
|
-
if (['number', 'string', 'bigint', 'boolean'].includes(typeof a[i])) {
|
|
1014
|
-
if (a[i] !== b[i]) {
|
|
1015
|
-
return false;
|
|
1016
|
-
}
|
|
1017
|
-
} else {
|
|
1018
|
-
if (!Utils.equals(a[i], b[i])) {
|
|
1019
|
-
return false;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
return true;
|
|
1024
|
-
}
|
|
1025
|
-
async syncCollections(collections, options) {
|
|
1026
|
-
const groups = {};
|
|
1027
|
-
for (const coll of collections) {
|
|
1028
|
-
const wrapped = helper(coll.owner);
|
|
1029
|
-
const meta = wrapped.__meta;
|
|
1030
|
-
const pks = wrapped.getPrimaryKeys(true);
|
|
1031
|
-
const snap = coll.getSnapshot();
|
|
1032
|
-
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
1033
|
-
const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)) : [];
|
|
1034
|
-
const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true));
|
|
1035
|
-
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
1036
|
-
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
1037
|
-
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
1038
|
-
const equals = Utils.equals(current, target);
|
|
1039
|
-
// wrong order if we just delete and insert to the end (only owning sides can have fixed order)
|
|
1040
|
-
if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) {
|
|
1041
|
-
deleteDiff.length = insertDiff.length = 0;
|
|
1042
|
-
for (const item of snapshot) {
|
|
1043
|
-
deleteDiff.push(item);
|
|
1044
|
-
}
|
|
1045
|
-
for (const item of current) {
|
|
1046
|
-
insertDiff.push(item);
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
|
|
1050
|
-
const cols = coll.property.referencedColumnNames;
|
|
1051
|
-
const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(
|
|
1052
|
-
this.getSchemaName(meta, options),
|
|
1053
|
-
);
|
|
1054
|
-
if (coll.getSnapshot() === undefined) {
|
|
1055
|
-
if (coll.property.orphanRemoval) {
|
|
1056
|
-
const query = qb
|
|
1057
|
-
.delete({ [coll.property.mappedBy]: pks })
|
|
1058
|
-
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
1059
|
-
await this.rethrow(query.execute());
|
|
1060
|
-
continue;
|
|
1061
|
-
}
|
|
1062
|
-
const query = qb
|
|
1063
|
-
.update({ [coll.property.mappedBy]: null })
|
|
1064
|
-
.where({ [coll.property.mappedBy]: pks })
|
|
1065
|
-
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
1066
|
-
await this.rethrow(query.execute());
|
|
1067
|
-
continue;
|
|
925
|
+
sql += ` in (${conds.join(', ')})`;
|
|
926
|
+
if (this.platform.usesReturningStatement() && returning.size > 0) {
|
|
927
|
+
const returningFields = Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
|
|
928
|
+
/* v8 ignore next */
|
|
929
|
+
sql +=
|
|
930
|
+
returningFields.length > 0
|
|
931
|
+
? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
|
|
932
|
+
: '';
|
|
1068
933
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
let schema = pivotMeta.schema;
|
|
1078
|
-
if (schema === '*') {
|
|
1079
|
-
if (coll.property.owner) {
|
|
1080
|
-
schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
|
|
1081
|
-
} else {
|
|
1082
|
-
const targetMeta = coll.property.targetMeta;
|
|
1083
|
-
const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
|
|
1084
|
-
schema =
|
|
1085
|
-
targetMeta.schema === '*'
|
|
1086
|
-
? (options?.schema ?? targetSchema ?? this.config.get('schema'))
|
|
1087
|
-
: targetMeta.schema;
|
|
1088
|
-
}
|
|
1089
|
-
} else if (schema == null) {
|
|
1090
|
-
schema = this.config.get('schema');
|
|
1091
|
-
}
|
|
1092
|
-
const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
|
|
1093
|
-
const persister = (groups[tableName] ??= new PivotCollectionPersister(
|
|
1094
|
-
pivotMeta,
|
|
1095
|
-
this,
|
|
1096
|
-
options?.ctx,
|
|
1097
|
-
schema,
|
|
1098
|
-
options?.loggerContext,
|
|
1099
|
-
));
|
|
1100
|
-
persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
|
|
1101
|
-
}
|
|
1102
|
-
for (const persister of Utils.values(groups)) {
|
|
1103
|
-
await this.rethrow(persister.execute());
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
1107
|
-
/* v8 ignore next */
|
|
1108
|
-
if (owners.length === 0) {
|
|
1109
|
-
return {};
|
|
1110
|
-
}
|
|
1111
|
-
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1112
|
-
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1113
|
-
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1114
|
-
}
|
|
1115
|
-
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1116
|
-
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1117
|
-
const ownerMeta = pivotProp2.targetMeta;
|
|
1118
|
-
// The pivot query builder doesn't convert custom types, so we need to manually
|
|
1119
|
-
// convert owner PKs to DB format for the query and convert result FKs back to
|
|
1120
|
-
// JS format for consistent key hashing in buildPivotResultMap.
|
|
1121
|
-
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1122
|
-
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1123
|
-
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1124
|
-
if (needsConversion) {
|
|
1125
|
-
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1126
|
-
}
|
|
1127
|
-
const cond = {
|
|
1128
|
-
[pivotProp2.name]: { $in: ownerPks },
|
|
1129
|
-
};
|
|
1130
|
-
if (!Utils.isEmpty(where)) {
|
|
1131
|
-
cond[pivotProp1.name] = { ...where };
|
|
1132
|
-
}
|
|
1133
|
-
where = cond;
|
|
1134
|
-
const populateField = pivotJoin ? `${pivotProp1.name}:ref` : pivotProp1.name;
|
|
1135
|
-
const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
|
|
1136
|
-
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
1137
|
-
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
1138
|
-
const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
1139
|
-
const res = await this.find(pivotMeta.class, where, {
|
|
1140
|
-
ctx,
|
|
1141
|
-
...options,
|
|
1142
|
-
fields,
|
|
1143
|
-
exclude: childExclude,
|
|
1144
|
-
orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
|
|
1145
|
-
populate: [
|
|
1146
|
-
{
|
|
1147
|
-
field: populateField,
|
|
1148
|
-
strategy: LoadStrategy.JOINED,
|
|
1149
|
-
joinType: JoinType.innerJoin,
|
|
1150
|
-
children: populate,
|
|
1151
|
-
dataOnly: pivotProp1.mapToPk && !pivotJoin,
|
|
1152
|
-
},
|
|
1153
|
-
],
|
|
1154
|
-
populateWhere: undefined,
|
|
1155
|
-
// @ts-ignore
|
|
1156
|
-
_populateWhere: 'infer',
|
|
1157
|
-
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
1158
|
-
});
|
|
1159
|
-
// Convert result FK values back to JS format so key hashing
|
|
1160
|
-
// in buildPivotResultMap is consistent with the owner keys.
|
|
1161
|
-
if (needsConversion) {
|
|
1162
|
-
for (const item of res) {
|
|
1163
|
-
const fk = item[pivotProp2.name];
|
|
1164
|
-
if (fk != null) {
|
|
1165
|
-
item[pivotProp2.name] = pkProp.customType.convertToJSValue(fk, this.platform);
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* Load from a polymorphic M:N pivot table.
|
|
1173
|
-
*/
|
|
1174
|
-
async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
1175
|
-
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1176
|
-
// Find the M:1 relation on the pivot pointing to the target entity.
|
|
1177
|
-
// We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
|
|
1178
|
-
const inverseProp = pivotMeta.relations.find(
|
|
1179
|
-
r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta,
|
|
1180
|
-
);
|
|
1181
|
-
if (inverseProp) {
|
|
1182
|
-
return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
|
|
1183
|
-
}
|
|
1184
|
-
return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
|
|
1185
|
-
}
|
|
1186
|
-
/**
|
|
1187
|
-
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
|
|
1188
|
-
*/
|
|
1189
|
-
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
1190
|
-
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1191
|
-
const targetMeta = prop.targetMeta;
|
|
1192
|
-
// Build condition: discriminator = 'post' AND {discriminator} IN (...)
|
|
1193
|
-
const cond = {
|
|
1194
|
-
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1195
|
-
[prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1196
|
-
};
|
|
1197
|
-
if (!Utils.isEmpty(where)) {
|
|
1198
|
-
cond[inverseProp.name] = { ...where };
|
|
1199
|
-
}
|
|
1200
|
-
const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
|
|
1201
|
-
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1202
|
-
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1203
|
-
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1204
|
-
const fields = pivotJoin
|
|
1205
|
-
? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
|
|
1206
|
-
: [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
|
|
1207
|
-
const res = await this.find(pivotMeta.class, cond, {
|
|
1208
|
-
ctx,
|
|
1209
|
-
...options,
|
|
1210
|
-
fields,
|
|
1211
|
-
exclude: childExclude,
|
|
1212
|
-
orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
|
|
1213
|
-
populate: [
|
|
1214
|
-
{
|
|
1215
|
-
field: populateField,
|
|
1216
|
-
strategy: LoadStrategy.JOINED,
|
|
1217
|
-
joinType: JoinType.innerJoin,
|
|
1218
|
-
children: populate,
|
|
1219
|
-
dataOnly: inverseProp.mapToPk && !pivotJoin,
|
|
1220
|
-
},
|
|
1221
|
-
],
|
|
1222
|
-
populateWhere: undefined,
|
|
1223
|
-
// @ts-ignore
|
|
1224
|
-
_populateWhere: 'infer',
|
|
1225
|
-
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
1226
|
-
});
|
|
1227
|
-
return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
|
|
1228
|
-
}
|
|
1229
|
-
/**
|
|
1230
|
-
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
1231
|
-
* Uses single query with join via virtual relation on pivot.
|
|
1232
|
-
*/
|
|
1233
|
-
async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
|
|
1234
|
-
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1235
|
-
const targetMeta = prop.targetMeta;
|
|
1236
|
-
// Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
|
|
1237
|
-
// Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
|
|
1238
|
-
const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
|
|
1239
|
-
// Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
|
|
1240
|
-
const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
1241
|
-
const ownerProp = pivotMeta.properties[ownerRelationName];
|
|
1242
|
-
// Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
|
|
1243
|
-
const cond = {
|
|
1244
|
-
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1245
|
-
[tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1246
|
-
};
|
|
1247
|
-
if (!Utils.isEmpty(where)) {
|
|
1248
|
-
cond[ownerRelationName] = { ...where };
|
|
1249
|
-
}
|
|
1250
|
-
const populateField = ownerRelationName;
|
|
1251
|
-
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1252
|
-
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1253
|
-
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1254
|
-
const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
|
|
1255
|
-
const res = await this.find(pivotMeta.class, cond, {
|
|
1256
|
-
ctx,
|
|
1257
|
-
...options,
|
|
1258
|
-
fields,
|
|
1259
|
-
exclude: childExclude,
|
|
1260
|
-
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1261
|
-
populate: [
|
|
1262
|
-
{
|
|
1263
|
-
field: populateField,
|
|
1264
|
-
strategy: LoadStrategy.JOINED,
|
|
1265
|
-
joinType: JoinType.innerJoin,
|
|
1266
|
-
children: populate,
|
|
1267
|
-
},
|
|
1268
|
-
],
|
|
1269
|
-
populateWhere: undefined,
|
|
1270
|
-
// @ts-ignore
|
|
1271
|
-
_populateWhere: 'infer',
|
|
1272
|
-
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
1273
|
-
});
|
|
1274
|
-
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1275
|
-
}
|
|
1276
|
-
/**
|
|
1277
|
-
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1278
|
-
*/
|
|
1279
|
-
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
1280
|
-
const map = {};
|
|
1281
|
-
for (const owner of owners) {
|
|
1282
|
-
const key = Utils.getPrimaryKeyHash(owner);
|
|
1283
|
-
map[key] = [];
|
|
1284
|
-
}
|
|
1285
|
-
for (const item of results) {
|
|
1286
|
-
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
|
|
1287
|
-
const entity = item[valueProp];
|
|
1288
|
-
if (map[key]) {
|
|
1289
|
-
map[key].push(entity);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
return map;
|
|
1293
|
-
}
|
|
1294
|
-
wrapPopulateFilter(options, propName) {
|
|
1295
|
-
if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
|
|
1296
|
-
return { [propName]: options?.populateFilter };
|
|
1297
|
-
}
|
|
1298
|
-
return undefined;
|
|
1299
|
-
}
|
|
1300
|
-
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1301
|
-
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1302
|
-
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
1303
|
-
}
|
|
1304
|
-
if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
|
|
1305
|
-
return Utils.asArray(parentOrderBy)
|
|
1306
|
-
.filter(o => o[prop.name])
|
|
1307
|
-
.map(o => ({ [pivotProp.name]: o[prop.name] }));
|
|
1308
|
-
}
|
|
1309
|
-
if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
|
|
1310
|
-
return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
1311
|
-
}
|
|
1312
|
-
if (prop.fixedOrder) {
|
|
1313
|
-
return [{ [prop.fixedOrderColumn]: QueryOrder.ASC }];
|
|
1314
|
-
}
|
|
1315
|
-
return [];
|
|
1316
|
-
}
|
|
1317
|
-
async execute(query, params = [], method = 'all', ctx, loggerContext) {
|
|
1318
|
-
return this.rethrow(this.connection.execute(query, params, method, ctx, loggerContext));
|
|
1319
|
-
}
|
|
1320
|
-
async *stream(entityName, where, options) {
|
|
1321
|
-
options = { populate: [], orderBy: [], ...options };
|
|
1322
|
-
const meta = this.metadata.get(entityName);
|
|
1323
|
-
if (meta.virtual) {
|
|
1324
|
-
yield* this.streamFromVirtual(entityName, where, options);
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
1328
|
-
try {
|
|
1329
|
-
const result = qb.stream(options);
|
|
1330
|
-
for await (const item of result) {
|
|
1331
|
-
yield item;
|
|
1332
|
-
}
|
|
1333
|
-
} catch (e) {
|
|
1334
|
-
throw this.convertException(e);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
/**
|
|
1338
|
-
* 1:1 owner side needs to be marked for population so QB auto-joins the owner id
|
|
1339
|
-
*/
|
|
1340
|
-
autoJoinOneToOneOwner(meta, populate, fields = []) {
|
|
1341
|
-
if (!this.config.get('autoJoinOneToOneOwner')) {
|
|
1342
|
-
return populate;
|
|
934
|
+
if (transform) {
|
|
935
|
+
sql = transform(sql, params);
|
|
936
|
+
}
|
|
937
|
+
const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
|
|
938
|
+
for (let i = 0; i < collections.length; i++) {
|
|
939
|
+
await this.processManyToMany(meta, where[i], collections[i], false, options);
|
|
940
|
+
}
|
|
941
|
+
return res;
|
|
1343
942
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
943
|
+
async nativeDelete(entityName, where, options = {}) {
|
|
944
|
+
const meta = this.metadata.get(entityName);
|
|
945
|
+
const pks = this.getPrimaryKeyFields(meta);
|
|
946
|
+
if (Utils.isPrimaryKey(where) && pks.length === 1) {
|
|
947
|
+
where = { [pks[0]]: where };
|
|
948
|
+
}
|
|
949
|
+
if (options.unionWhere?.length) {
|
|
950
|
+
where = await this.applyUnionWhere(meta, where, options, true);
|
|
951
|
+
}
|
|
952
|
+
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
|
|
953
|
+
.delete(where)
|
|
954
|
+
.withSchema(this.getSchemaName(meta, options));
|
|
955
|
+
return this.rethrow(qb.execute('run', false));
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Fast comparison for collection snapshots that are represented by PK arrays.
|
|
959
|
+
* Compares scalars via `===` and fallbacks to Utils.equals()` for more complex types like Buffer.
|
|
960
|
+
* Always expects the same length of the arrays, since we only compare PKs of the same entity type.
|
|
961
|
+
*/
|
|
962
|
+
comparePrimaryKeyArrays(a, b) {
|
|
963
|
+
for (let i = a.length; i-- !== 0;) {
|
|
964
|
+
if (['number', 'string', 'bigint', 'boolean'].includes(typeof a[i])) {
|
|
965
|
+
if (a[i] !== b[i]) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
if (!Utils.equals(a[i], b[i])) {
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
1369
975
|
return true;
|
|
1370
|
-
}
|
|
1371
|
-
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1372
|
-
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1373
|
-
return false;
|
|
1374
|
-
}
|
|
1375
|
-
if (strategy !== LoadStrategy.JOINED) {
|
|
1376
|
-
// force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
|
|
1377
|
-
return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
1378
|
-
}
|
|
1379
|
-
return ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind);
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* @internal
|
|
1384
|
-
*/
|
|
1385
|
-
mergeJoinedResult(rawResults, meta, joinedProps) {
|
|
1386
|
-
if (rawResults.length <= 1) {
|
|
1387
|
-
return rawResults;
|
|
1388
976
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
977
|
+
async syncCollections(collections, options) {
|
|
978
|
+
const groups = {};
|
|
979
|
+
for (const coll of collections) {
|
|
980
|
+
const wrapped = helper(coll.owner);
|
|
981
|
+
const meta = wrapped.__meta;
|
|
982
|
+
const pks = wrapped.getPrimaryKeys(true);
|
|
983
|
+
const snap = coll.getSnapshot();
|
|
984
|
+
const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
|
|
985
|
+
const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)) : [];
|
|
986
|
+
const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true));
|
|
987
|
+
const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
|
|
988
|
+
const insertDiff = current.filter(item => !includes(snapshot, item));
|
|
989
|
+
const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
|
|
990
|
+
const equals = Utils.equals(current, target);
|
|
991
|
+
// wrong order if we just delete and insert to the end (only owning sides can have fixed order)
|
|
992
|
+
if (coll.property.owner && coll.property.fixedOrder && !equals && Array.isArray(deleteDiff)) {
|
|
993
|
+
deleteDiff.length = insertDiff.length = 0;
|
|
994
|
+
for (const item of snapshot) {
|
|
995
|
+
deleteDiff.push(item);
|
|
996
|
+
}
|
|
997
|
+
for (const item of current) {
|
|
998
|
+
insertDiff.push(item);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
|
|
1002
|
+
const cols = coll.property.referencedColumnNames;
|
|
1003
|
+
const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
|
|
1004
|
+
if (coll.getSnapshot() === undefined) {
|
|
1005
|
+
if (coll.property.orphanRemoval) {
|
|
1006
|
+
const query = qb
|
|
1007
|
+
.delete({ [coll.property.mappedBy]: pks })
|
|
1008
|
+
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
1009
|
+
await this.rethrow(query.execute());
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
const query = qb
|
|
1013
|
+
.update({ [coll.property.mappedBy]: null })
|
|
1014
|
+
.where({ [coll.property.mappedBy]: pks })
|
|
1015
|
+
.andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
|
|
1016
|
+
await this.rethrow(query.execute());
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
/* v8 ignore next */
|
|
1020
|
+
const query = qb
|
|
1021
|
+
.update({ [coll.property.mappedBy]: pks })
|
|
1022
|
+
.where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
|
|
1023
|
+
await this.rethrow(query.execute());
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const pivotMeta = this.metadata.find(coll.property.pivotEntity);
|
|
1027
|
+
let schema = pivotMeta.schema;
|
|
1028
|
+
if (schema === '*') {
|
|
1029
|
+
if (coll.property.owner) {
|
|
1030
|
+
schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
const targetMeta = coll.property.targetMeta;
|
|
1034
|
+
const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
|
|
1035
|
+
schema =
|
|
1036
|
+
targetMeta.schema === '*'
|
|
1037
|
+
? (options?.schema ?? targetSchema ?? this.config.get('schema'))
|
|
1038
|
+
: targetMeta.schema;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
else if (schema == null) {
|
|
1042
|
+
schema = this.config.get('schema');
|
|
1043
|
+
}
|
|
1044
|
+
const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
|
|
1045
|
+
const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
|
|
1046
|
+
persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
|
|
1047
|
+
}
|
|
1048
|
+
for (const persister of Utils.values(groups)) {
|
|
1049
|
+
await this.rethrow(persister.execute());
|
|
1418
1050
|
}
|
|
1419
|
-
const prop = meta.properties[propName];
|
|
1420
|
-
const items = collections[propName].flat();
|
|
1421
|
-
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
|
|
1422
|
-
entity[propName] = items;
|
|
1423
|
-
continue;
|
|
1424
|
-
}
|
|
1425
|
-
switch (prop.kind) {
|
|
1426
|
-
case ReferenceKind.ONE_TO_MANY:
|
|
1427
|
-
case ReferenceKind.MANY_TO_MANY:
|
|
1428
|
-
entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? []);
|
|
1429
|
-
break;
|
|
1430
|
-
case ReferenceKind.MANY_TO_ONE:
|
|
1431
|
-
case ReferenceKind.ONE_TO_ONE:
|
|
1432
|
-
entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? [])[0];
|
|
1433
|
-
break;
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
}
|
|
1437
|
-
return res;
|
|
1438
|
-
}
|
|
1439
|
-
shouldHaveColumn(meta, prop, populate, fields, exclude) {
|
|
1440
|
-
if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
|
|
1441
|
-
return false;
|
|
1442
|
-
}
|
|
1443
|
-
if (!fields || fields.includes('*') || prop.primary || meta.root.discriminatorColumn === prop.name) {
|
|
1444
|
-
return true;
|
|
1445
|
-
}
|
|
1446
|
-
return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
|
|
1447
|
-
}
|
|
1448
|
-
getFieldsForJoinedLoad(qb, meta, options) {
|
|
1449
|
-
const fields = [];
|
|
1450
|
-
const populate = options.populate ?? [];
|
|
1451
|
-
const joinedProps = this.joinedProps(meta, populate, options);
|
|
1452
|
-
const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
|
|
1453
|
-
// Ensure TPT joins are applied early so that _tptAlias is available for join resolution
|
|
1454
|
-
// This is needed when populating relations that are inherited from TPT parent entities
|
|
1455
|
-
if (!options.parentJoinPath) {
|
|
1456
|
-
qb.ensureTPTJoins();
|
|
1457
|
-
}
|
|
1458
|
-
// root entity is already handled, skip that
|
|
1459
|
-
if (options.parentJoinPath) {
|
|
1460
|
-
// alias all fields in the primary table
|
|
1461
|
-
meta.props
|
|
1462
|
-
.filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
|
|
1463
|
-
.forEach(prop =>
|
|
1464
|
-
fields.push(
|
|
1465
|
-
...this.mapPropToFieldNames(
|
|
1466
|
-
qb,
|
|
1467
|
-
prop,
|
|
1468
|
-
options.parentTableAlias,
|
|
1469
|
-
meta,
|
|
1470
|
-
options.schema,
|
|
1471
|
-
options.explicitFields,
|
|
1472
|
-
),
|
|
1473
|
-
),
|
|
1474
|
-
);
|
|
1475
1051
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
[
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
...options,
|
|
1514
|
-
populate: hint.children,
|
|
1515
|
-
parentTableAlias: tableAlias,
|
|
1516
|
-
parentJoinPath: targetPath,
|
|
1517
|
-
}),
|
|
1518
|
-
);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
continue;
|
|
1522
|
-
}
|
|
1523
|
-
// ignore ref joins of known FKs unless it's a filter hint
|
|
1524
|
-
if (
|
|
1525
|
-
ref &&
|
|
1526
|
-
!hint.filter &&
|
|
1527
|
-
(prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))
|
|
1528
|
-
) {
|
|
1529
|
-
continue;
|
|
1530
|
-
}
|
|
1531
|
-
const meta2 = prop.targetMeta;
|
|
1532
|
-
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
1533
|
-
const tableAlias = qb.getNextAlias(prop.name);
|
|
1534
|
-
const field = `${options.parentTableAlias}.${prop.name}`;
|
|
1535
|
-
let path = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
1536
|
-
if (!options.parentJoinPath && populateWhereAll && !hint.filter && !path.startsWith('[populate]')) {
|
|
1537
|
-
path = '[populate]' + path;
|
|
1538
|
-
}
|
|
1539
|
-
const mandatoryToOneProperty =
|
|
1540
|
-
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.nullable;
|
|
1541
|
-
const joinType = pivotRefJoin
|
|
1542
|
-
? JoinType.pivotJoin
|
|
1543
|
-
: hint.joinType
|
|
1544
|
-
? hint.joinType
|
|
1545
|
-
: (hint.filter && !prop.nullable) || mandatoryToOneProperty
|
|
1546
|
-
? JoinType.innerJoin
|
|
1547
|
-
: JoinType.leftJoin;
|
|
1548
|
-
const schema =
|
|
1549
|
-
prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1550
|
-
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1551
|
-
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1552
|
-
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1553
|
-
// Use the registry metadata to ensure allTPTDescendants is available
|
|
1554
|
-
const tptMeta = this.metadata.get(meta2.class);
|
|
1555
|
-
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
|
|
1556
|
-
}
|
|
1557
|
-
if (pivotRefJoin) {
|
|
1558
|
-
fields.push(
|
|
1559
|
-
...prop.joinColumns.map(col =>
|
|
1560
|
-
qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`),
|
|
1561
|
-
),
|
|
1562
|
-
...prop.inverseJoinColumns.map(col =>
|
|
1563
|
-
qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`),
|
|
1564
|
-
),
|
|
1565
|
-
);
|
|
1566
|
-
}
|
|
1567
|
-
if (prop.kind === ReferenceKind.ONE_TO_MANY && ref) {
|
|
1568
|
-
fields.push(
|
|
1569
|
-
...this.getFieldsForJoinedLoad(qb, meta2, {
|
|
1570
|
-
...options,
|
|
1571
|
-
explicitFields: prop.referencedColumnNames,
|
|
1572
|
-
exclude: undefined,
|
|
1573
|
-
populate: hint.children,
|
|
1574
|
-
parentTableAlias: tableAlias,
|
|
1575
|
-
parentJoinPath: path,
|
|
1576
|
-
}),
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
const childExplicitFields =
|
|
1580
|
-
options.explicitFields?.filter(f => Utils.isPlainObject(f)).map(o => o[prop.name])[0] || [];
|
|
1581
|
-
options.explicitFields?.forEach(f => {
|
|
1582
|
-
if (typeof f === 'string' && f.startsWith(`${prop.name}.`)) {
|
|
1583
|
-
childExplicitFields.push(f.substring(prop.name.length + 1));
|
|
1584
|
-
}
|
|
1585
|
-
});
|
|
1586
|
-
const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
|
|
1587
|
-
if (!ref && (!prop.mapToPk || hint.dataOnly)) {
|
|
1588
|
-
fields.push(
|
|
1589
|
-
...this.getFieldsForJoinedLoad(qb, meta2, {
|
|
1052
|
+
async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
1053
|
+
/* v8 ignore next */
|
|
1054
|
+
if (owners.length === 0) {
|
|
1055
|
+
return {};
|
|
1056
|
+
}
|
|
1057
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1058
|
+
if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
|
|
1059
|
+
return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
|
|
1060
|
+
}
|
|
1061
|
+
const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
|
|
1062
|
+
const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
|
|
1063
|
+
const ownerMeta = pivotProp2.targetMeta;
|
|
1064
|
+
// The pivot query builder doesn't convert custom types, so we need to manually
|
|
1065
|
+
// convert owner PKs to DB format for the query and convert result FKs back to
|
|
1066
|
+
// JS format for consistent key hashing in buildPivotResultMap.
|
|
1067
|
+
const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
|
|
1068
|
+
const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
|
|
1069
|
+
let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
|
|
1070
|
+
if (needsConversion) {
|
|
1071
|
+
ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
|
|
1072
|
+
}
|
|
1073
|
+
const cond = {
|
|
1074
|
+
[pivotProp2.name]: { $in: ownerPks },
|
|
1075
|
+
};
|
|
1076
|
+
if (!Utils.isEmpty(where)) {
|
|
1077
|
+
cond[pivotProp1.name] = { ...where };
|
|
1078
|
+
}
|
|
1079
|
+
where = cond;
|
|
1080
|
+
const populateField = pivotJoin ? `${pivotProp1.name}:ref` : pivotProp1.name;
|
|
1081
|
+
const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
|
|
1082
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
1083
|
+
const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
|
|
1084
|
+
const fields = pivotJoin
|
|
1085
|
+
? [pivotProp1.name, pivotProp2.name]
|
|
1086
|
+
: [pivotProp1.name, pivotProp2.name, ...childFields];
|
|
1087
|
+
const res = await this.find(pivotMeta.class, where, {
|
|
1088
|
+
ctx,
|
|
1590
1089
|
...options,
|
|
1591
|
-
|
|
1090
|
+
fields,
|
|
1592
1091
|
exclude: childExclude,
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
);
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
const childAliases = {};
|
|
1620
|
-
// LEFT JOIN each descendant table
|
|
1621
|
-
for (const childMeta of descendants) {
|
|
1622
|
-
const childAlias = qb.getNextAlias(childMeta.className);
|
|
1623
|
-
qb.createAlias(childMeta.class, childAlias);
|
|
1624
|
-
childAliases[childMeta.className] = childAlias;
|
|
1625
|
-
qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
|
|
1626
|
-
// Add fields from this child (only ownProps, skip PKs)
|
|
1627
|
-
const schema = childMeta.schema === '*' ? '*' : this.getSchemaName(childMeta);
|
|
1628
|
-
childMeta.ownProps
|
|
1629
|
-
.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))
|
|
1630
|
-
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, childAlias, childMeta, schema)));
|
|
1631
|
-
}
|
|
1632
|
-
// Add computed discriminator (descendants already sorted by depth)
|
|
1633
|
-
if (meta.root.tptDiscriminatorColumn) {
|
|
1634
|
-
fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
/**
|
|
1638
|
-
* Find the alias for a TPT child table in the query builder.
|
|
1639
|
-
* @internal
|
|
1640
|
-
*/
|
|
1641
|
-
findTPTChildAlias(qb, childMeta) {
|
|
1642
|
-
const joins = qb.state.joins;
|
|
1643
|
-
for (const key of Object.keys(joins)) {
|
|
1644
|
-
if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
|
|
1645
|
-
return joins[key].alias;
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
return undefined;
|
|
1649
|
-
}
|
|
1650
|
-
/**
|
|
1651
|
-
* Builds a CASE WHEN expression for TPT discriminator.
|
|
1652
|
-
* Determines concrete entity type based on which child table has a non-null PK.
|
|
1653
|
-
* @internal
|
|
1654
|
-
*/
|
|
1655
|
-
buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
|
|
1656
|
-
const cases = descendants.map(child => {
|
|
1657
|
-
const childAlias = aliasMap[child.className];
|
|
1658
|
-
const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
|
|
1659
|
-
return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
|
|
1660
|
-
});
|
|
1661
|
-
const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
|
|
1662
|
-
const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
|
|
1663
|
-
const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
|
|
1664
|
-
return raw(`${caseExpr} as ${aliased}`);
|
|
1665
|
-
}
|
|
1666
|
-
/**
|
|
1667
|
-
* Maps TPT child-specific fields during hydration.
|
|
1668
|
-
* When a relation points to a TPT base class, the actual entity might be a child class.
|
|
1669
|
-
* This method reads the discriminator to determine the concrete type and maps child-specific fields.
|
|
1670
|
-
* @internal
|
|
1671
|
-
*/
|
|
1672
|
-
mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
|
|
1673
|
-
// Check if this is a TPT base with polymorphic children
|
|
1674
|
-
if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
// Read the discriminator value
|
|
1678
|
-
const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
|
|
1679
|
-
const discriminatorValue = root[discriminatorAlias];
|
|
1680
|
-
if (!discriminatorValue) {
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
// Set the discriminator in the pojo for EntityFactory
|
|
1684
|
-
relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
|
|
1685
|
-
// Find the concrete metadata from discriminator map
|
|
1686
|
-
const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
|
|
1687
|
-
/* v8 ignore next 3 - defensive check for invalid discriminator values */
|
|
1688
|
-
if (!concreteClass) {
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
const concreteMeta = this.metadata.get(concreteClass);
|
|
1692
|
-
if (concreteMeta === meta) {
|
|
1693
|
-
// Already the concrete type, no child fields to map
|
|
1694
|
-
delete root[discriminatorAlias];
|
|
1695
|
-
return;
|
|
1696
|
-
}
|
|
1697
|
-
// Traverse up from concrete type and map fields from each level's table
|
|
1698
|
-
const tz = this.platform.getTimezone();
|
|
1699
|
-
let currentMeta = concreteMeta;
|
|
1700
|
-
while (currentMeta && currentMeta !== meta) {
|
|
1701
|
-
const childAlias = this.findTPTChildAlias(qb, currentMeta);
|
|
1702
|
-
if (childAlias) {
|
|
1703
|
-
// Map fields using same filtering as joined loading, plus skip PKs
|
|
1704
|
-
for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
|
|
1705
|
-
this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, {
|
|
1706
|
-
deleteFromRoot: true,
|
|
1707
|
-
});
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
currentMeta = currentMeta.tptParent;
|
|
1711
|
-
}
|
|
1712
|
-
// Clean up the discriminator alias
|
|
1713
|
-
delete root[discriminatorAlias];
|
|
1714
|
-
}
|
|
1715
|
-
/**
|
|
1716
|
-
* @internal
|
|
1717
|
-
*/
|
|
1718
|
-
mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
|
|
1719
|
-
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1720
|
-
return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
|
|
1721
|
-
const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
|
|
1722
|
-
if (
|
|
1723
|
-
!this.shouldHaveColumn(
|
|
1724
|
-
prop.targetMeta,
|
|
1725
|
-
{ ...childProp, name },
|
|
1726
|
-
[],
|
|
1727
|
-
childFields.length > 0 ? childFields : undefined,
|
|
1728
|
-
)
|
|
1729
|
-
) {
|
|
1730
|
-
return [];
|
|
1731
|
-
}
|
|
1732
|
-
return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
|
|
1733
|
-
});
|
|
1734
|
-
}
|
|
1735
|
-
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
|
|
1736
|
-
if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
|
|
1737
|
-
return prop.fieldNames.map((col, idx) => {
|
|
1738
|
-
if (!prop.customTypes[idx]?.convertToJSValueSQL) {
|
|
1739
|
-
return col;
|
|
1740
|
-
}
|
|
1741
|
-
const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${col}`);
|
|
1742
|
-
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${col}`);
|
|
1743
|
-
return raw(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
if (prop.customType?.convertToJSValueSQL) {
|
|
1747
|
-
const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${prop.fieldNames[0]}`);
|
|
1748
|
-
return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
|
|
1749
|
-
}
|
|
1750
|
-
if (prop.formula) {
|
|
1751
|
-
const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
|
|
1752
|
-
const table = this.createFormulaTable(quotedAlias, meta, schema);
|
|
1753
|
-
const columns = meta.createColumnMappingObject(tableAlias);
|
|
1754
|
-
return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
|
|
1755
|
-
}
|
|
1756
|
-
return prop.fieldNames.map(fieldName => {
|
|
1757
|
-
return raw('?? as ??', [`${tableAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
/** @internal */
|
|
1761
|
-
createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, loggerContext, alias, em) {
|
|
1762
|
-
// do not compute the connectionType if EM is provided as it will be computed from it in the QB later on
|
|
1763
|
-
const connectionType = em
|
|
1764
|
-
? preferredConnectionType
|
|
1765
|
-
: this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
|
|
1766
|
-
const qb = new QueryBuilder(entityName, this.metadata, this, ctx, alias, connectionType, em, loggerContext);
|
|
1767
|
-
if (!convertCustomTypes) {
|
|
1768
|
-
qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
|
|
1769
|
-
}
|
|
1770
|
-
return qb;
|
|
1771
|
-
}
|
|
1772
|
-
resolveConnectionType(args) {
|
|
1773
|
-
if (args.ctx) {
|
|
1774
|
-
return 'write';
|
|
1775
|
-
}
|
|
1776
|
-
if (args.connectionType) {
|
|
1777
|
-
return args.connectionType;
|
|
1778
|
-
}
|
|
1779
|
-
if (this.config.get('preferReadReplicas')) {
|
|
1780
|
-
return 'read';
|
|
1092
|
+
orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
|
|
1093
|
+
populate: [
|
|
1094
|
+
{
|
|
1095
|
+
field: populateField,
|
|
1096
|
+
strategy: LoadStrategy.JOINED,
|
|
1097
|
+
joinType: JoinType.innerJoin,
|
|
1098
|
+
children: populate,
|
|
1099
|
+
dataOnly: pivotProp1.mapToPk && !pivotJoin,
|
|
1100
|
+
},
|
|
1101
|
+
],
|
|
1102
|
+
populateWhere: undefined,
|
|
1103
|
+
// @ts-ignore
|
|
1104
|
+
_populateWhere: 'infer',
|
|
1105
|
+
populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
|
|
1106
|
+
});
|
|
1107
|
+
// Convert result FK values back to JS format so key hashing
|
|
1108
|
+
// in buildPivotResultMap is consistent with the owner keys.
|
|
1109
|
+
if (needsConversion) {
|
|
1110
|
+
for (const item of res) {
|
|
1111
|
+
const fk = item[pivotProp2.name];
|
|
1112
|
+
if (fk != null) {
|
|
1113
|
+
item[pivotProp2.name] = pkProp.customType.convertToJSValue(fk, this.platform);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
|
|
1781
1118
|
}
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1119
|
+
/**
|
|
1120
|
+
* Load from a polymorphic M:N pivot table.
|
|
1121
|
+
*/
|
|
1122
|
+
async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
|
|
1123
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1124
|
+
// Find the M:1 relation on the pivot pointing to the target entity.
|
|
1125
|
+
// We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
|
|
1126
|
+
const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
|
|
1127
|
+
if (inverseProp) {
|
|
1128
|
+
return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
|
|
1129
|
+
}
|
|
1130
|
+
return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
|
|
1791
1131
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (collections[prop.name]) {
|
|
1132
|
+
/**
|
|
1133
|
+
* Load from owner side of polymorphic M:N (e.g., Post -> Tags)
|
|
1134
|
+
*/
|
|
1135
|
+
async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
|
|
1797
1136
|
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
)
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1137
|
+
const targetMeta = prop.targetMeta;
|
|
1138
|
+
// Build condition: discriminator = 'post' AND {discriminator} IN (...)
|
|
1139
|
+
const cond = {
|
|
1140
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1141
|
+
[prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1142
|
+
};
|
|
1143
|
+
if (!Utils.isEmpty(where)) {
|
|
1144
|
+
cond[inverseProp.name] = { ...where };
|
|
1145
|
+
}
|
|
1146
|
+
const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
|
|
1147
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1148
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
|
|
1149
|
+
const childExclude = !Utils.isEmpty(options?.exclude)
|
|
1150
|
+
? options.exclude.map(f => `${inverseProp.name}.${f}`)
|
|
1151
|
+
: [];
|
|
1152
|
+
const fields = pivotJoin
|
|
1153
|
+
? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
|
|
1154
|
+
: [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
|
|
1155
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1156
|
+
ctx,
|
|
1157
|
+
...options,
|
|
1158
|
+
fields,
|
|
1159
|
+
exclude: childExclude,
|
|
1160
|
+
orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
|
|
1161
|
+
populate: [
|
|
1162
|
+
{
|
|
1163
|
+
field: populateField,
|
|
1164
|
+
strategy: LoadStrategy.JOINED,
|
|
1165
|
+
joinType: JoinType.innerJoin,
|
|
1166
|
+
children: populate,
|
|
1167
|
+
dataOnly: inverseProp.mapToPk && !pivotJoin,
|
|
1168
|
+
},
|
|
1169
|
+
],
|
|
1170
|
+
populateWhere: undefined,
|
|
1171
|
+
// @ts-ignore
|
|
1172
|
+
_populateWhere: 'infer',
|
|
1173
|
+
populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
|
|
1174
|
+
});
|
|
1175
|
+
return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
|
|
1808
1176
|
}
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
|
|
1816
|
-
qb.select(raw('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
|
|
1817
|
-
await this.rethrow(qb.execute());
|
|
1818
|
-
}
|
|
1819
|
-
buildPopulateWhere(meta, joinedProps, options) {
|
|
1820
|
-
const where = {};
|
|
1821
|
-
for (const hint of joinedProps) {
|
|
1822
|
-
const [propName] = hint.field.split(':', 2);
|
|
1823
|
-
const prop = meta.properties[propName];
|
|
1824
|
-
if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
|
|
1825
|
-
where[prop.name] = Utils.copy(prop.where);
|
|
1826
|
-
}
|
|
1827
|
-
if (hint.children) {
|
|
1177
|
+
/**
|
|
1178
|
+
* Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
|
|
1179
|
+
* Uses single query with join via virtual relation on pivot.
|
|
1180
|
+
*/
|
|
1181
|
+
async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
|
|
1182
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1828
1183
|
const targetMeta = prop.targetMeta;
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1184
|
+
// Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
|
|
1185
|
+
// Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
|
|
1186
|
+
const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
|
|
1187
|
+
// Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
|
|
1188
|
+
const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
1189
|
+
const ownerProp = pivotMeta.properties[ownerRelationName];
|
|
1190
|
+
// Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
|
|
1191
|
+
const cond = {
|
|
1192
|
+
[prop.discriminatorColumn]: prop.discriminatorValue,
|
|
1193
|
+
[tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
|
|
1194
|
+
};
|
|
1195
|
+
if (!Utils.isEmpty(where)) {
|
|
1196
|
+
cond[ownerRelationName] = { ...where };
|
|
1197
|
+
}
|
|
1198
|
+
const populateField = ownerRelationName;
|
|
1199
|
+
const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
|
|
1200
|
+
const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
|
|
1201
|
+
const childExclude = !Utils.isEmpty(options?.exclude)
|
|
1202
|
+
? options.exclude.map(f => `${ownerRelationName}.${f}`)
|
|
1203
|
+
: [];
|
|
1204
|
+
const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
|
|
1205
|
+
const res = await this.find(pivotMeta.class, cond, {
|
|
1206
|
+
ctx,
|
|
1207
|
+
...options,
|
|
1208
|
+
fields,
|
|
1209
|
+
exclude: childExclude,
|
|
1210
|
+
orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
|
|
1211
|
+
populate: [
|
|
1212
|
+
{
|
|
1213
|
+
field: populateField,
|
|
1214
|
+
strategy: LoadStrategy.JOINED,
|
|
1215
|
+
joinType: JoinType.innerJoin,
|
|
1216
|
+
children: populate,
|
|
1217
|
+
},
|
|
1218
|
+
],
|
|
1219
|
+
populateWhere: undefined,
|
|
1220
|
+
// @ts-ignore
|
|
1221
|
+
_populateWhere: 'infer',
|
|
1222
|
+
populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
|
|
1223
|
+
});
|
|
1224
|
+
return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Build a map from owner PKs to their related entities from pivot table results.
|
|
1228
|
+
*/
|
|
1229
|
+
buildPivotResultMap(owners, results, keyProp, valueProp) {
|
|
1230
|
+
const map = {};
|
|
1231
|
+
for (const owner of owners) {
|
|
1232
|
+
const key = Utils.getPrimaryKeyHash(owner);
|
|
1233
|
+
map[key] = [];
|
|
1234
|
+
}
|
|
1235
|
+
for (const item of results) {
|
|
1236
|
+
const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
|
|
1237
|
+
const entity = item[valueProp];
|
|
1238
|
+
if (map[key]) {
|
|
1239
|
+
map[key].push(entity);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return map;
|
|
1837
1243
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1244
|
+
wrapPopulateFilter(options, propName) {
|
|
1245
|
+
if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
|
|
1246
|
+
return { [propName]: options?.populateFilter };
|
|
1247
|
+
}
|
|
1248
|
+
return undefined;
|
|
1840
1249
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1250
|
+
getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
|
|
1251
|
+
if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
|
|
1252
|
+
return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
1253
|
+
}
|
|
1254
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
|
|
1255
|
+
return Utils.asArray(parentOrderBy)
|
|
1256
|
+
.filter(o => o[prop.name])
|
|
1257
|
+
.map(o => ({ [pivotProp.name]: o[prop.name] }));
|
|
1258
|
+
}
|
|
1259
|
+
if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
|
|
1260
|
+
return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
|
|
1261
|
+
}
|
|
1262
|
+
if (prop.fixedOrder) {
|
|
1263
|
+
return [{ [prop.fixedOrderColumn]: QueryOrder.ASC }];
|
|
1264
|
+
}
|
|
1265
|
+
return [];
|
|
1843
1266
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
}
|
|
1847
|
-
/**
|
|
1848
|
-
* Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
|
|
1849
|
-
* into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
|
|
1850
|
-
* Each branch is planned independently by the database, enabling per-table index usage.
|
|
1851
|
-
*/
|
|
1852
|
-
async applyUnionWhere(meta, where, options, forDml = false) {
|
|
1853
|
-
const unionWhere = options.unionWhere;
|
|
1854
|
-
const strategy = options.unionWhereStrategy ?? 'union-all';
|
|
1855
|
-
const schema = this.getSchemaName(meta, options);
|
|
1856
|
-
const connectionType = this.resolveConnectionType({
|
|
1857
|
-
ctx: options.ctx,
|
|
1858
|
-
connectionType: options.connectionType,
|
|
1859
|
-
});
|
|
1860
|
-
const branchQbs = [];
|
|
1861
|
-
for (const branch of unionWhere) {
|
|
1862
|
-
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging).withSchema(
|
|
1863
|
-
schema,
|
|
1864
|
-
);
|
|
1865
|
-
const pkFields = meta.primaryKeys.map(pk => {
|
|
1866
|
-
const prop = meta.properties[pk];
|
|
1867
|
-
return `${qb.alias}.${prop.fieldNames[0]}`;
|
|
1868
|
-
});
|
|
1869
|
-
qb.select(pkFields).where(branch);
|
|
1870
|
-
if (options.em) {
|
|
1871
|
-
await qb.applyJoinedFilters(options.em, options.filters);
|
|
1872
|
-
}
|
|
1873
|
-
branchQbs.push(qb);
|
|
1267
|
+
async execute(query, params = [], method = 'all', ctx, loggerContext) {
|
|
1268
|
+
return this.rethrow(this.connection.execute(query, params, method, ctx, loggerContext));
|
|
1874
1269
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1270
|
+
async *stream(entityName, where, options) {
|
|
1271
|
+
options = { populate: [], orderBy: [], ...options };
|
|
1272
|
+
const meta = this.metadata.get(entityName);
|
|
1273
|
+
if (meta.virtual) {
|
|
1274
|
+
yield* this.streamFromVirtual(entityName, where, options);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const qb = await this.createQueryBuilderFromOptions(meta, where, options);
|
|
1278
|
+
try {
|
|
1279
|
+
const result = qb.stream(options);
|
|
1280
|
+
for await (const item of result) {
|
|
1281
|
+
yield item;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
catch (e) {
|
|
1285
|
+
throw this.convertException(e);
|
|
1286
|
+
}
|
|
1885
1287
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
// `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
|
|
1893
|
-
// as `options.populateWhere` will be always recomputed to respect filters
|
|
1894
|
-
const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
|
|
1895
|
-
const path = (populateWhereAll ? '[populate]' : '') + meta.className;
|
|
1896
|
-
const optionsOrderBy = Utils.asArray(options.orderBy);
|
|
1897
|
-
const populateOrderBy = this.buildPopulateOrderBy(
|
|
1898
|
-
qb,
|
|
1899
|
-
meta,
|
|
1900
|
-
Utils.asArray(options.populateOrderBy ?? options.orderBy),
|
|
1901
|
-
path,
|
|
1902
|
-
!!options.populateOrderBy,
|
|
1903
|
-
);
|
|
1904
|
-
const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
|
|
1905
|
-
return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
|
|
1906
|
-
}
|
|
1907
|
-
buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
|
|
1908
|
-
const orderBy = [];
|
|
1909
|
-
for (let i = 0; i < populateOrderBy.length; i++) {
|
|
1910
|
-
const orderHint = populateOrderBy[i];
|
|
1911
|
-
for (const field of Utils.getObjectQueryKeys(orderHint)) {
|
|
1912
|
-
const childOrder = orderHint[field];
|
|
1913
|
-
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1914
|
-
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1915
|
-
const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
|
|
1916
|
-
orderBy.push({ [key]: childOrder });
|
|
1917
|
-
continue;
|
|
1918
|
-
}
|
|
1919
|
-
const prop = meta.properties[field];
|
|
1920
|
-
if (!prop) {
|
|
1921
|
-
throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
|
|
1922
|
-
}
|
|
1923
|
-
let path = parentPath;
|
|
1924
|
-
const meta2 = prop.targetMeta;
|
|
1925
|
-
if (
|
|
1926
|
-
prop.kind !== ReferenceKind.SCALAR &&
|
|
1927
|
-
(![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
|
|
1928
|
-
!prop.owner ||
|
|
1929
|
-
Utils.isPlainObject(childOrder))
|
|
1930
|
-
) {
|
|
1931
|
-
path += `.${field}`;
|
|
1932
|
-
}
|
|
1933
|
-
if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
|
|
1934
|
-
path += '[pivot]';
|
|
1288
|
+
/**
|
|
1289
|
+
* 1:1 owner side needs to be marked for population so QB auto-joins the owner id
|
|
1290
|
+
*/
|
|
1291
|
+
autoJoinOneToOneOwner(meta, populate, fields = []) {
|
|
1292
|
+
if (!this.config.get('autoJoinOneToOneOwner')) {
|
|
1293
|
+
return populate;
|
|
1935
1294
|
}
|
|
1936
|
-
const
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1295
|
+
const relationsToPopulate = populate.map(({ field }) => field.split(':')[0]);
|
|
1296
|
+
const toPopulate = meta.relations
|
|
1297
|
+
.filter(prop => prop.kind === ReferenceKind.ONE_TO_ONE &&
|
|
1298
|
+
!prop.owner &&
|
|
1299
|
+
!prop.lazy &&
|
|
1300
|
+
!relationsToPopulate.includes(prop.name))
|
|
1301
|
+
.filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
|
|
1302
|
+
.map(prop => ({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED }));
|
|
1303
|
+
return [...populate, ...toPopulate];
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* @internal
|
|
1307
|
+
*/
|
|
1308
|
+
joinedProps(meta, populate, options) {
|
|
1309
|
+
return populate.filter(hint => {
|
|
1310
|
+
const [propName, ref] = hint.field.split(':', 2);
|
|
1311
|
+
const prop = meta.properties[propName] || {};
|
|
1312
|
+
const strategy = getLoadingStrategy(hint.strategy || prop.strategy || options?.strategy || this.config.get('loadStrategy'), prop.kind);
|
|
1313
|
+
if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
// skip redundant joins for 1:1 owner population hints when using `mapToPk`
|
|
1317
|
+
if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
|
|
1318
|
+
return false;
|
|
1319
|
+
}
|
|
1320
|
+
if (strategy !== LoadStrategy.JOINED) {
|
|
1321
|
+
// force joined strategy for explicit 1:1 owner populate hint as it would require a join anyway
|
|
1322
|
+
return prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner;
|
|
1323
|
+
}
|
|
1324
|
+
return ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind);
|
|
1325
|
+
});
|
|
1965
1326
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1327
|
+
/**
|
|
1328
|
+
* @internal
|
|
1329
|
+
*/
|
|
1330
|
+
mergeJoinedResult(rawResults, meta, joinedProps) {
|
|
1331
|
+
if (rawResults.length <= 1) {
|
|
1332
|
+
return rawResults;
|
|
1333
|
+
}
|
|
1334
|
+
const res = [];
|
|
1335
|
+
const map = {};
|
|
1336
|
+
const collectionsToMerge = {};
|
|
1337
|
+
const hints = joinedProps.map(hint => {
|
|
1338
|
+
const [propName, ref] = hint.field.split(':', 2);
|
|
1339
|
+
return { propName, ref, children: hint.children };
|
|
1340
|
+
});
|
|
1341
|
+
for (const item of rawResults) {
|
|
1342
|
+
const pk = Utils.getCompositeKeyHash(item, meta);
|
|
1343
|
+
if (map[pk]) {
|
|
1344
|
+
for (const { propName } of hints) {
|
|
1345
|
+
if (!item[propName]) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
collectionsToMerge[pk] ??= {};
|
|
1349
|
+
collectionsToMerge[pk][propName] ??= [map[pk][propName]];
|
|
1350
|
+
collectionsToMerge[pk][propName].push(item[propName]);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
map[pk] = item;
|
|
1355
|
+
res.push(item);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
for (const pk in collectionsToMerge) {
|
|
1359
|
+
const entity = map[pk];
|
|
1360
|
+
const collections = collectionsToMerge[pk];
|
|
1361
|
+
for (const { propName, ref, children } of hints) {
|
|
1362
|
+
if (!collections[propName]) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const prop = meta.properties[propName];
|
|
1366
|
+
const items = collections[propName].flat();
|
|
1367
|
+
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind) && ref) {
|
|
1368
|
+
entity[propName] = items;
|
|
1369
|
+
continue;
|
|
1370
|
+
}
|
|
1371
|
+
switch (prop.kind) {
|
|
1372
|
+
case ReferenceKind.ONE_TO_MANY:
|
|
1373
|
+
case ReferenceKind.MANY_TO_MANY:
|
|
1374
|
+
entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? []);
|
|
1375
|
+
break;
|
|
1376
|
+
case ReferenceKind.MANY_TO_ONE:
|
|
1377
|
+
case ReferenceKind.ONE_TO_ONE:
|
|
1378
|
+
entity[propName] = this.mergeJoinedResult(items, prop.targetMeta, children ?? [])[0];
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return res;
|
|
1984
1384
|
}
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1385
|
+
shouldHaveColumn(meta, prop, populate, fields, exclude) {
|
|
1386
|
+
if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
if (!fields || fields.includes('*') || prop.primary || meta.root.discriminatorColumn === prop.name) {
|
|
1390
|
+
return true;
|
|
1391
|
+
}
|
|
1392
|
+
return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
|
|
1393
|
+
}
|
|
1394
|
+
getFieldsForJoinedLoad(qb, meta, options) {
|
|
1395
|
+
const fields = [];
|
|
1396
|
+
const populate = options.populate ?? [];
|
|
1397
|
+
const joinedProps = this.joinedProps(meta, populate, options);
|
|
1398
|
+
const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
|
|
1399
|
+
// Ensure TPT joins are applied early so that _tptAlias is available for join resolution
|
|
1400
|
+
// This is needed when populating relations that are inherited from TPT parent entities
|
|
1401
|
+
if (!options.parentJoinPath) {
|
|
1402
|
+
qb.ensureTPTJoins();
|
|
1403
|
+
}
|
|
1404
|
+
// root entity is already handled, skip that
|
|
1405
|
+
if (options.parentJoinPath) {
|
|
1406
|
+
// alias all fields in the primary table
|
|
1407
|
+
meta.props
|
|
1408
|
+
.filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
|
|
1409
|
+
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, meta, options.schema, options.explicitFields)));
|
|
1410
|
+
}
|
|
1411
|
+
for (const hint of joinedProps) {
|
|
1412
|
+
const [propName, ref] = hint.field.split(':', 2);
|
|
1413
|
+
const prop = meta.properties[propName];
|
|
1414
|
+
// Polymorphic to-one: create a LEFT JOIN per target type
|
|
1415
|
+
// Skip regular :ref hints — polymorphic to-one already has FK + discriminator in the row
|
|
1416
|
+
// But allow filter :ref hints through to create per-target LEFT JOINs with filter checks
|
|
1417
|
+
if (prop.polymorphic &&
|
|
1418
|
+
prop.polymorphTargets?.length &&
|
|
1419
|
+
(!ref || hint.filter) &&
|
|
1420
|
+
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
1421
|
+
const basePath = options.parentJoinPath
|
|
1422
|
+
? `${options.parentJoinPath}.${prop.name}`
|
|
1423
|
+
: `${meta.name}.${prop.name}`;
|
|
1424
|
+
const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
|
|
1425
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
1426
|
+
const tableAlias = qb.getNextAlias(targetMeta.className);
|
|
1427
|
+
const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
|
|
1428
|
+
const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
|
|
1429
|
+
qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
|
|
1430
|
+
if (ref) {
|
|
1431
|
+
// For filter :ref hints, schedule filter check for each target (no field selection)
|
|
1432
|
+
qb.scheduleFilterCheck(targetPath);
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
// Select fields from each target table
|
|
1436
|
+
fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
|
|
1437
|
+
...options,
|
|
1438
|
+
populate: hint.children,
|
|
1439
|
+
parentTableAlias: tableAlias,
|
|
1440
|
+
parentJoinPath: targetPath,
|
|
1441
|
+
}));
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
// ignore ref joins of known FKs unless it's a filter hint
|
|
1447
|
+
if (ref &&
|
|
1448
|
+
!hint.filter &&
|
|
1449
|
+
(prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const meta2 = prop.targetMeta;
|
|
1453
|
+
const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
|
|
1454
|
+
const tableAlias = qb.getNextAlias(prop.name);
|
|
1455
|
+
const field = `${options.parentTableAlias}.${prop.name}`;
|
|
1456
|
+
let path = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
|
|
1457
|
+
if (!options.parentJoinPath && populateWhereAll && !hint.filter && !path.startsWith('[populate]')) {
|
|
1458
|
+
path = '[populate]' + path;
|
|
1459
|
+
}
|
|
1460
|
+
const mandatoryToOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.nullable;
|
|
1461
|
+
const joinType = pivotRefJoin
|
|
1462
|
+
? JoinType.pivotJoin
|
|
1463
|
+
: hint.joinType
|
|
1464
|
+
? hint.joinType
|
|
1465
|
+
: (hint.filter && !prop.nullable) || mandatoryToOneProperty
|
|
1466
|
+
? JoinType.innerJoin
|
|
1467
|
+
: JoinType.leftJoin;
|
|
1468
|
+
const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
|
|
1469
|
+
qb.join(field, tableAlias, {}, joinType, path, schema);
|
|
1470
|
+
// For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
|
|
1471
|
+
if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
|
|
1472
|
+
// Use the registry metadata to ensure allTPTDescendants is available
|
|
1473
|
+
const tptMeta = this.metadata.get(meta2.class);
|
|
1474
|
+
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
|
|
1475
|
+
}
|
|
1476
|
+
if (pivotRefJoin) {
|
|
1477
|
+
fields.push(...prop.joinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)), ...prop.inverseJoinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
|
|
1478
|
+
}
|
|
1479
|
+
if (prop.kind === ReferenceKind.ONE_TO_MANY && ref) {
|
|
1480
|
+
fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
|
|
1481
|
+
...options,
|
|
1482
|
+
explicitFields: prop.referencedColumnNames,
|
|
1483
|
+
exclude: undefined,
|
|
1484
|
+
populate: hint.children,
|
|
1485
|
+
parentTableAlias: tableAlias,
|
|
1486
|
+
parentJoinPath: path,
|
|
1487
|
+
}));
|
|
1488
|
+
}
|
|
1489
|
+
const childExplicitFields = options.explicitFields?.filter(f => Utils.isPlainObject(f)).map(o => o[prop.name])[0] || [];
|
|
1490
|
+
options.explicitFields?.forEach(f => {
|
|
1491
|
+
if (typeof f === 'string' && f.startsWith(`${prop.name}.`)) {
|
|
1492
|
+
childExplicitFields.push(f.substring(prop.name.length + 1));
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
const childExclude = options.exclude
|
|
1496
|
+
? Utils.extractChildElements(options.exclude, prop.name)
|
|
1497
|
+
: options.exclude;
|
|
1498
|
+
if (!ref && (!prop.mapToPk || hint.dataOnly)) {
|
|
1499
|
+
fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
|
|
1500
|
+
...options,
|
|
1501
|
+
explicitFields: childExplicitFields.length === 0 ? undefined : childExplicitFields,
|
|
1502
|
+
exclude: childExclude,
|
|
1503
|
+
populate: hint.children,
|
|
1504
|
+
parentTableAlias: tableAlias,
|
|
1505
|
+
parentJoinPath: path,
|
|
1506
|
+
}));
|
|
1507
|
+
}
|
|
1508
|
+
else if (hint.filter ||
|
|
1509
|
+
(prop.mapToPk && !hint.dataOnly) ||
|
|
1510
|
+
(ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
|
|
1511
|
+
fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return fields;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
|
|
1518
|
+
* @internal
|
|
1519
|
+
*/
|
|
1520
|
+
addTPTPolymorphicJoinsForRelation(qb, meta, baseAlias, fields) {
|
|
1521
|
+
// allTPTDescendants is pre-computed during discovery, sorted by depth (deepest first)
|
|
1522
|
+
const descendants = meta.allTPTDescendants;
|
|
1523
|
+
const childAliases = {};
|
|
1524
|
+
// LEFT JOIN each descendant table
|
|
1525
|
+
for (const childMeta of descendants) {
|
|
1526
|
+
const childAlias = qb.getNextAlias(childMeta.className);
|
|
1527
|
+
qb.createAlias(childMeta.class, childAlias);
|
|
1528
|
+
childAliases[childMeta.className] = childAlias;
|
|
1529
|
+
qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
|
|
1530
|
+
// Add fields from this child (only ownProps, skip PKs)
|
|
1531
|
+
const schema = childMeta.schema === '*' ? '*' : this.getSchemaName(childMeta);
|
|
1532
|
+
childMeta
|
|
1533
|
+
.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))
|
|
1534
|
+
.forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, childAlias, childMeta, schema)));
|
|
1535
|
+
}
|
|
1536
|
+
// Add computed discriminator (descendants already sorted by depth)
|
|
1537
|
+
if (meta.root.tptDiscriminatorColumn) {
|
|
1538
|
+
fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
|
|
1539
|
+
}
|
|
1993
1540
|
}
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
|
|
1541
|
+
/**
|
|
1542
|
+
* Find the alias for a TPT child table in the query builder.
|
|
1543
|
+
* @internal
|
|
1544
|
+
*/
|
|
1545
|
+
findTPTChildAlias(qb, childMeta) {
|
|
1546
|
+
const joins = qb.state.joins;
|
|
1547
|
+
for (const key of Object.keys(joins)) {
|
|
1548
|
+
if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
|
|
1549
|
+
return joins[key].alias;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return undefined;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Builds a CASE WHEN expression for TPT discriminator.
|
|
1556
|
+
* Determines concrete entity type based on which child table has a non-null PK.
|
|
1557
|
+
* @internal
|
|
1558
|
+
*/
|
|
1559
|
+
buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
|
|
1560
|
+
const cases = descendants.map(child => {
|
|
1561
|
+
const childAlias = aliasMap[child.className];
|
|
1562
|
+
const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
|
|
1563
|
+
return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
|
|
1564
|
+
});
|
|
1565
|
+
const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
|
|
1566
|
+
const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
|
|
1567
|
+
const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
|
|
1568
|
+
return raw(`${caseExpr} as ${aliased}`);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Maps TPT child-specific fields during hydration.
|
|
1572
|
+
* When a relation points to a TPT base class, the actual entity might be a child class.
|
|
1573
|
+
* This method reads the discriminator to determine the concrete type and maps child-specific fields.
|
|
1574
|
+
* @internal
|
|
1575
|
+
*/
|
|
1576
|
+
mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
|
|
1577
|
+
// Check if this is a TPT base with polymorphic children
|
|
1578
|
+
if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
// Read the discriminator value
|
|
1582
|
+
const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
|
|
1583
|
+
const discriminatorValue = root[discriminatorAlias];
|
|
1584
|
+
if (!discriminatorValue) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
// Set the discriminator in the pojo for EntityFactory
|
|
1588
|
+
relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
|
|
1589
|
+
// Find the concrete metadata from discriminator map
|
|
1590
|
+
const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
|
|
1591
|
+
/* v8 ignore next 3 - defensive check for invalid discriminator values */
|
|
1592
|
+
if (!concreteClass) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const concreteMeta = this.metadata.get(concreteClass);
|
|
1596
|
+
if (concreteMeta === meta) {
|
|
1597
|
+
// Already the concrete type, no child fields to map
|
|
1598
|
+
delete root[discriminatorAlias];
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
// Traverse up from concrete type and map fields from each level's table
|
|
1602
|
+
const tz = this.platform.getTimezone();
|
|
1603
|
+
let currentMeta = concreteMeta;
|
|
1604
|
+
while (currentMeta && currentMeta !== meta) {
|
|
1605
|
+
const childAlias = this.findTPTChildAlias(qb, currentMeta);
|
|
1606
|
+
if (childAlias) {
|
|
1607
|
+
// Map fields using same filtering as joined loading, plus skip PKs
|
|
1608
|
+
for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
|
|
1609
|
+
this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, {
|
|
1610
|
+
deleteFromRoot: true,
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
currentMeta = currentMeta.tptParent;
|
|
1615
|
+
}
|
|
1616
|
+
// Clean up the discriminator alias
|
|
1617
|
+
delete root[discriminatorAlias];
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* @internal
|
|
1621
|
+
*/
|
|
1622
|
+
mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
|
|
1623
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1624
|
+
return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
|
|
1625
|
+
const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
|
|
1626
|
+
if (!this.shouldHaveColumn(prop.targetMeta, { ...childProp, name }, [], childFields.length > 0 ? childFields : undefined)) {
|
|
1627
|
+
return [];
|
|
1628
|
+
}
|
|
1629
|
+
return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
|
|
1633
|
+
if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
|
|
1634
|
+
return prop.fieldNames.map((col, idx) => {
|
|
1635
|
+
if (!prop.customTypes[idx]?.convertToJSValueSQL) {
|
|
1636
|
+
return col;
|
|
1637
|
+
}
|
|
1638
|
+
const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${col}`);
|
|
1639
|
+
const aliased = this.platform.quoteIdentifier(`${tableAlias}__${col}`);
|
|
1640
|
+
return raw(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
if (prop.customType?.convertToJSValueSQL) {
|
|
1644
|
+
const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${prop.fieldNames[0]}`);
|
|
1645
|
+
return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
|
|
1646
|
+
}
|
|
1647
|
+
if (prop.formula) {
|
|
1648
|
+
const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
|
|
1649
|
+
const table = this.createFormulaTable(quotedAlias, meta, schema);
|
|
1650
|
+
const columns = meta.createColumnMappingObject(tableAlias);
|
|
1651
|
+
return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
|
|
1652
|
+
}
|
|
1653
|
+
return prop.fieldNames.map(fieldName => {
|
|
1654
|
+
return raw('?? as ??', [`${tableAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
|
|
1655
|
+
});
|
|
2007
1656
|
}
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
1657
|
+
/** @internal */
|
|
1658
|
+
createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, loggerContext, alias, em) {
|
|
1659
|
+
// do not compute the connectionType if EM is provided as it will be computed from it in the QB later on
|
|
1660
|
+
const connectionType = em
|
|
1661
|
+
? preferredConnectionType
|
|
1662
|
+
: this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
|
|
1663
|
+
const qb = new QueryBuilder(entityName, this.metadata, this, ctx, alias, connectionType, em, loggerContext);
|
|
1664
|
+
if (!convertCustomTypes) {
|
|
1665
|
+
qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
|
|
1666
|
+
}
|
|
1667
|
+
return qb;
|
|
2021
1668
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
1669
|
+
resolveConnectionType(args) {
|
|
1670
|
+
if (args.ctx) {
|
|
1671
|
+
return 'write';
|
|
1672
|
+
}
|
|
1673
|
+
if (args.connectionType) {
|
|
1674
|
+
return args.connectionType;
|
|
1675
|
+
}
|
|
1676
|
+
if (this.config.get('preferReadReplicas')) {
|
|
1677
|
+
return 'read';
|
|
1678
|
+
}
|
|
1679
|
+
return 'write';
|
|
1680
|
+
}
|
|
1681
|
+
extractManyToMany(meta, data) {
|
|
1682
|
+
const ret = {};
|
|
1683
|
+
for (const prop of meta.relations) {
|
|
1684
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
|
|
1685
|
+
ret[prop.name] = data[prop.name].map((item) => Utils.asArray(item));
|
|
1686
|
+
delete data[prop.name];
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return ret;
|
|
1690
|
+
}
|
|
1691
|
+
async processManyToMany(meta, pks, collections, clear, options) {
|
|
1692
|
+
for (const prop of meta.relations) {
|
|
1693
|
+
if (collections[prop.name]) {
|
|
1694
|
+
const pivotMeta = this.metadata.get(prop.pivotEntity);
|
|
1695
|
+
const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
|
|
1696
|
+
persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
|
|
1697
|
+
await this.rethrow(persister.execute());
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
2027
1700
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
|
|
1701
|
+
async lockPessimistic(entity, options) {
|
|
1702
|
+
const meta = helper(entity).__meta;
|
|
1703
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
|
|
1704
|
+
const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
|
|
1705
|
+
qb.select(raw('1'))
|
|
1706
|
+
.where(cond)
|
|
1707
|
+
.setLockMode(options.lockMode, options.lockTableAliases);
|
|
1708
|
+
await this.rethrow(qb.execute());
|
|
1709
|
+
}
|
|
1710
|
+
buildPopulateWhere(meta, joinedProps, options) {
|
|
1711
|
+
const where = {};
|
|
1712
|
+
for (const hint of joinedProps) {
|
|
1713
|
+
const [propName] = hint.field.split(':', 2);
|
|
1714
|
+
const prop = meta.properties[propName];
|
|
1715
|
+
if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
|
|
1716
|
+
where[prop.name] = Utils.copy(prop.where);
|
|
1717
|
+
}
|
|
1718
|
+
if (hint.children) {
|
|
1719
|
+
const targetMeta = prop.targetMeta;
|
|
1720
|
+
if (targetMeta) {
|
|
1721
|
+
const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
|
|
1722
|
+
if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
|
|
1723
|
+
where[prop.name] ??= {};
|
|
1724
|
+
Object.assign(where[prop.name], inner);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (Utils.isEmpty(options.populateWhere) && !RawQueryFragment.hasObjectFragments(options.populateWhere)) {
|
|
1730
|
+
return where;
|
|
1731
|
+
}
|
|
1732
|
+
if (Utils.isEmpty(where) && !RawQueryFragment.hasObjectFragments(where)) {
|
|
1733
|
+
return options.populateWhere;
|
|
1734
|
+
}
|
|
1735
|
+
/* v8 ignore next */
|
|
1736
|
+
return { $and: [options.populateWhere, where] };
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
|
|
1740
|
+
* into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
|
|
1741
|
+
* Each branch is planned independently by the database, enabling per-table index usage.
|
|
1742
|
+
*/
|
|
1743
|
+
async applyUnionWhere(meta, where, options, forDml = false) {
|
|
1744
|
+
const unionWhere = options.unionWhere;
|
|
1745
|
+
const strategy = options.unionWhereStrategy ?? 'union-all';
|
|
1746
|
+
const schema = this.getSchemaName(meta, options);
|
|
1747
|
+
const connectionType = this.resolveConnectionType({
|
|
1748
|
+
ctx: options.ctx,
|
|
1749
|
+
connectionType: options.connectionType,
|
|
1750
|
+
});
|
|
1751
|
+
const branchQbs = [];
|
|
1752
|
+
for (const branch of unionWhere) {
|
|
1753
|
+
const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging).withSchema(schema);
|
|
1754
|
+
const pkFields = meta.primaryKeys.map(pk => {
|
|
1755
|
+
const prop = meta.properties[pk];
|
|
1756
|
+
return `${qb.alias}.${prop.fieldNames[0]}`;
|
|
1757
|
+
});
|
|
1758
|
+
qb.select(pkFields).where(branch);
|
|
1759
|
+
if (options.em) {
|
|
1760
|
+
await qb.applyJoinedFilters(options.em, options.filters);
|
|
1761
|
+
}
|
|
1762
|
+
branchQbs.push(qb);
|
|
1763
|
+
}
|
|
1764
|
+
const [first, ...rest] = branchQbs;
|
|
1765
|
+
const unionQb = strategy === 'union' ? first.union(...rest) : first.unionAll(...rest);
|
|
1766
|
+
const pkHash = Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
1767
|
+
// MySQL does not allow referencing the target table in a subquery
|
|
1768
|
+
// for UPDATE/DELETE, so we wrap the union in a derived table.
|
|
1769
|
+
if (forDml) {
|
|
1770
|
+
const { sql, params } = unionQb.toQuery();
|
|
1771
|
+
return {
|
|
1772
|
+
$and: [where, { [pkHash]: { $in: raw(`select * from (${sql}) as __u`, params) } }],
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
return {
|
|
1776
|
+
$and: [where, { [pkHash]: { $in: unionQb.toRaw() } }],
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
buildOrderBy(qb, meta, populate, options) {
|
|
1780
|
+
const joinedProps = this.joinedProps(meta, populate, options);
|
|
1781
|
+
// `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
|
|
1782
|
+
// as `options.populateWhere` will be always recomputed to respect filters
|
|
1783
|
+
const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
|
|
1784
|
+
const path = (populateWhereAll ? '[populate]' : '') + meta.className;
|
|
1785
|
+
const optionsOrderBy = Utils.asArray(options.orderBy);
|
|
1786
|
+
const populateOrderBy = this.buildPopulateOrderBy(qb, meta, Utils.asArray(options.populateOrderBy ?? options.orderBy), path, !!options.populateOrderBy);
|
|
1787
|
+
const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
|
|
1788
|
+
return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
|
|
1789
|
+
}
|
|
1790
|
+
buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
|
|
1791
|
+
const orderBy = [];
|
|
1792
|
+
for (let i = 0; i < populateOrderBy.length; i++) {
|
|
1793
|
+
const orderHint = populateOrderBy[i];
|
|
1794
|
+
for (const field of Utils.getObjectQueryKeys(orderHint)) {
|
|
1795
|
+
const childOrder = orderHint[field];
|
|
1796
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1797
|
+
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1798
|
+
const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
|
|
1799
|
+
orderBy.push({ [key]: childOrder });
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
const prop = meta.properties[field];
|
|
1803
|
+
if (!prop) {
|
|
1804
|
+
throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
|
|
1805
|
+
}
|
|
1806
|
+
let path = parentPath;
|
|
1807
|
+
const meta2 = prop.targetMeta;
|
|
1808
|
+
if (prop.kind !== ReferenceKind.SCALAR &&
|
|
1809
|
+
(![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
|
|
1810
|
+
!prop.owner ||
|
|
1811
|
+
Utils.isPlainObject(childOrder))) {
|
|
1812
|
+
path += `.${field}`;
|
|
1813
|
+
}
|
|
1814
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
|
|
1815
|
+
path += '[pivot]';
|
|
1816
|
+
}
|
|
1817
|
+
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
|
|
1818
|
+
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
|
|
1819
|
+
if (!join) {
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
if (join &&
|
|
1823
|
+
![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) &&
|
|
1824
|
+
typeof childOrder === 'object') {
|
|
1825
|
+
const children = this.buildPopulateOrderBy(qb, meta2, Utils.asArray(childOrder), path, explicit, propAlias);
|
|
1826
|
+
orderBy.push(...children);
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && join) {
|
|
1830
|
+
if (prop.fixedOrderColumn) {
|
|
1831
|
+
orderBy.push({ [`${join.alias}.${prop.fixedOrderColumn}`]: childOrder });
|
|
1832
|
+
}
|
|
1833
|
+
else {
|
|
1834
|
+
for (const col of prop.inverseJoinColumns) {
|
|
1835
|
+
orderBy.push({ [`${join.ownerAlias}.${col}`]: childOrder });
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
const order = typeof childOrder === 'object' ? childOrder[field] : childOrder;
|
|
1841
|
+
if (order) {
|
|
1842
|
+
orderBy.push({ [`${propAlias}.${field}`]: order });
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return orderBy;
|
|
1847
|
+
}
|
|
1848
|
+
buildJoinedPropsOrderBy(qb, meta, populate, options, parentPath) {
|
|
1849
|
+
const orderBy = [];
|
|
1850
|
+
const joinedProps = this.joinedProps(meta, populate, options);
|
|
1851
|
+
for (const hint of joinedProps) {
|
|
1852
|
+
const [propName, ref] = hint.field.split(':', 2);
|
|
1853
|
+
const prop = meta.properties[propName];
|
|
1854
|
+
let path = `${parentPath}.${propName}`;
|
|
1855
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && ref) {
|
|
1856
|
+
path += '[pivot]';
|
|
1857
|
+
}
|
|
1858
|
+
if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
|
|
1859
|
+
this.buildToManyOrderBy(qb, prop, path, ref, orderBy);
|
|
1860
|
+
}
|
|
1861
|
+
if (hint.children) {
|
|
1862
|
+
orderBy.push(...this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path));
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return orderBy;
|
|
2041
1866
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
1867
|
+
buildToManyOrderBy(qb, prop, path, ref, orderBy) {
|
|
1868
|
+
const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
|
|
1869
|
+
const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
|
|
1870
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
|
|
1871
|
+
const alias = ref ? propAlias : join.ownerAlias;
|
|
1872
|
+
orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
|
|
1873
|
+
}
|
|
1874
|
+
const effectiveOrderBy = QueryHelper.mergeOrderBy(prop.orderBy, prop.targetMeta?.orderBy);
|
|
1875
|
+
for (const item of effectiveOrderBy) {
|
|
1876
|
+
for (const field of Utils.getObjectQueryKeys(item)) {
|
|
1877
|
+
const order = item[field];
|
|
1878
|
+
if (RawQueryFragment.isKnownFragmentSymbol(field)) {
|
|
1879
|
+
const { sql, params } = RawQueryFragment.getKnownFragment(field);
|
|
1880
|
+
const sql2 = propAlias ? sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : sql;
|
|
1881
|
+
const key = raw(sql2, params);
|
|
1882
|
+
orderBy.push({ [key]: order });
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
orderBy.push({ [`${propAlias}.${field}`]: order });
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
2044
1888
|
}
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
const parts = field.split('.');
|
|
2062
|
-
const rootPropName = parts.shift(); // first one is the `prop`
|
|
2063
|
-
const prop = QueryHelper.findProperty(rootPropName, {
|
|
2064
|
-
metadata: this.metadata,
|
|
2065
|
-
platform: this.platform,
|
|
2066
|
-
entityName: meta.class,
|
|
2067
|
-
where: {},
|
|
2068
|
-
aliasMap: qb.getAliasMap(),
|
|
2069
|
-
});
|
|
2070
|
-
this.processField(meta, prop, parts.join('.'), ret);
|
|
2071
|
-
}
|
|
2072
|
-
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
|
|
2073
|
-
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
|
|
2074
|
-
}
|
|
2075
|
-
if (
|
|
2076
|
-
meta.root.inheritanceType === 'sti' &&
|
|
2077
|
-
!options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)
|
|
2078
|
-
) {
|
|
2079
|
-
ret.push(meta.root.discriminatorColumn);
|
|
2080
|
-
}
|
|
2081
|
-
} else if (!Utils.isEmpty(options.exclude) || lazyProps.some(p => !p.formula && (p.kind !== '1:1' || p.owner))) {
|
|
2082
|
-
const props = meta.props.filter(prop =>
|
|
2083
|
-
this.platform.shouldHaveColumn(prop, populate, options.exclude, false, false),
|
|
2084
|
-
);
|
|
2085
|
-
ret.push(...props.filter(p => !lazyProps.includes(p)).map(p => p.name));
|
|
2086
|
-
addFormulas = true;
|
|
2087
|
-
} else if (hasLazyFormulas || requiresSQLConversion) {
|
|
2088
|
-
ret.push('*');
|
|
2089
|
-
addFormulas = true;
|
|
2090
|
-
} else {
|
|
2091
|
-
ret.push('*');
|
|
1889
|
+
normalizeFields(fields, prefix = '') {
|
|
1890
|
+
const ret = [];
|
|
1891
|
+
for (const field of fields) {
|
|
1892
|
+
if (typeof field === 'string') {
|
|
1893
|
+
ret.push(prefix + field);
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (Utils.isPlainObject(field)) {
|
|
1897
|
+
for (const key of Object.keys(field)) {
|
|
1898
|
+
ret.push(...this.normalizeFields(field[key], key + '.'));
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return ret;
|
|
2092
1903
|
}
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
const columns = meta.createColumnMappingObject(alias);
|
|
2097
|
-
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
2098
|
-
for (const prop of meta.props) {
|
|
2099
|
-
if (lazyProps.includes(prop)) {
|
|
2100
|
-
continue;
|
|
1904
|
+
processField(meta, prop, field, ret) {
|
|
1905
|
+
if (!prop || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) {
|
|
1906
|
+
return;
|
|
2101
1907
|
}
|
|
2102
|
-
if (prop.
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
1908
|
+
if (prop.kind === ReferenceKind.EMBEDDED) {
|
|
1909
|
+
if (prop.object) {
|
|
1910
|
+
ret.push(prop.name);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const parts = field.split('.');
|
|
1914
|
+
const top = parts.shift();
|
|
1915
|
+
for (const key of Object.keys(prop.embeddedProps)) {
|
|
1916
|
+
if (!top || key === top) {
|
|
1917
|
+
this.processField(meta, prop.embeddedProps[key], parts.join('.'), ret);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return;
|
|
2106
1921
|
}
|
|
2107
|
-
if (
|
|
2108
|
-
|
|
1922
|
+
if (prop.persist === false && !prop.embedded && !prop.formula) {
|
|
1923
|
+
return;
|
|
2109
1924
|
}
|
|
2110
|
-
|
|
1925
|
+
ret.push(prop.name);
|
|
2111
1926
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
1927
|
+
buildFields(meta, populate, joinedProps, qb, alias, options, schema) {
|
|
1928
|
+
const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
|
|
1929
|
+
const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
|
|
1930
|
+
const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
|
|
1931
|
+
const hasExplicitFields = !!options.fields;
|
|
1932
|
+
const ret = [];
|
|
1933
|
+
let addFormulas = false;
|
|
1934
|
+
// handle root entity properties first, this is used for both strategies in the same way
|
|
1935
|
+
if (options.fields) {
|
|
1936
|
+
for (const field of this.normalizeFields(options.fields)) {
|
|
1937
|
+
if (field === '*') {
|
|
1938
|
+
ret.push('*');
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
const parts = field.split('.');
|
|
1942
|
+
const rootPropName = parts.shift(); // first one is the `prop`
|
|
1943
|
+
const prop = QueryHelper.findProperty(rootPropName, {
|
|
1944
|
+
metadata: this.metadata,
|
|
1945
|
+
platform: this.platform,
|
|
1946
|
+
entityName: meta.class,
|
|
1947
|
+
where: {},
|
|
1948
|
+
aliasMap: qb.getAliasMap(),
|
|
1949
|
+
});
|
|
1950
|
+
this.processField(meta, prop, parts.join('.'), ret);
|
|
1951
|
+
}
|
|
1952
|
+
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
|
|
1953
|
+
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
|
|
1954
|
+
}
|
|
1955
|
+
if (meta.root.inheritanceType === 'sti' &&
|
|
1956
|
+
!options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
|
|
1957
|
+
ret.push(meta.root.discriminatorColumn);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
else if (!Utils.isEmpty(options.exclude) || lazyProps.some(p => !p.formula && (p.kind !== '1:1' || p.owner))) {
|
|
1961
|
+
const props = meta.props.filter(prop => this.platform.shouldHaveColumn(prop, populate, options.exclude, false, false));
|
|
1962
|
+
ret.push(...props.filter(p => !lazyProps.includes(p)).map(p => p.name));
|
|
1963
|
+
addFormulas = true;
|
|
1964
|
+
}
|
|
1965
|
+
else if (hasLazyFormulas || requiresSQLConversion) {
|
|
1966
|
+
ret.push('*');
|
|
1967
|
+
addFormulas = true;
|
|
1968
|
+
}
|
|
1969
|
+
else {
|
|
1970
|
+
ret.push('*');
|
|
1971
|
+
}
|
|
1972
|
+
if (ret.length > 0 && !hasExplicitFields && addFormulas) {
|
|
1973
|
+
// Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
|
|
1974
|
+
const quotedAlias = this.platform.quoteIdentifier(alias);
|
|
1975
|
+
const columns = meta.createColumnMappingObject(alias);
|
|
1976
|
+
const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
|
|
1977
|
+
for (const prop of meta.props) {
|
|
1978
|
+
if (lazyProps.includes(prop)) {
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
if (prop.formula) {
|
|
1982
|
+
const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
|
|
1983
|
+
const table = this.createFormulaTable(quotedAlias.toString(), meta, effectiveSchema);
|
|
1984
|
+
ret.push(raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`));
|
|
1985
|
+
}
|
|
1986
|
+
if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
|
|
1987
|
+
ret.push(prop.name);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
// add joined relations after the root entity fields
|
|
1992
|
+
if (joinedProps.length > 0) {
|
|
1993
|
+
ret.push(...this.getFieldsForJoinedLoad(qb, meta, {
|
|
1994
|
+
explicitFields: options.fields,
|
|
1995
|
+
exclude: options.exclude,
|
|
1996
|
+
populate,
|
|
1997
|
+
parentTableAlias: alias,
|
|
1998
|
+
...options,
|
|
1999
|
+
}));
|
|
2000
|
+
}
|
|
2001
|
+
return Utils.unique(ret);
|
|
2123
2002
|
}
|
|
2124
|
-
return Utils.unique(ret);
|
|
2125
|
-
}
|
|
2126
2003
|
}
|