@mikro-orm/knex 6.0.0-rc.0 → 6.0.0-rc.2

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.
@@ -10,8 +10,22 @@ export declare abstract class AbstractSqlConnection extends Connection {
10
10
  /** @inheritDoc */
11
11
  connect(): void | Promise<void>;
12
12
  getKnex(): Knex;
13
+ /**
14
+ * @inheritDoc
15
+ */
13
16
  close(force?: boolean): Promise<void>;
17
+ /**
18
+ * @inheritDoc
19
+ */
14
20
  isConnected(): Promise<boolean>;
21
+ /**
22
+ * @inheritDoc
23
+ */
24
+ checkConnection(): Promise<{
25
+ ok: boolean;
26
+ reason?: string;
27
+ error?: Error;
28
+ }>;
15
29
  transactional<T>(cb: (trx: Transaction<Knex.Transaction>) => Promise<T>, options?: {
16
30
  isolationLevel?: IsolationLevel;
17
31
  readOnly?: boolean;
@@ -26,10 +26,16 @@ class AbstractSqlConnection extends core_1.Connection {
26
26
  }
27
27
  return this.client;
28
28
  }
29
+ /**
30
+ * @inheritDoc
31
+ */
29
32
  async close(force) {
30
33
  await super.close(force);
31
34
  await this.getKnex().destroy();
32
35
  }
36
+ /**
37
+ * @inheritDoc
38
+ */
33
39
  async isConnected() {
34
40
  try {
35
41
  await this.getKnex().raw('select 1');
@@ -39,6 +45,18 @@ class AbstractSqlConnection extends core_1.Connection {
39
45
  return false;
40
46
  }
41
47
  }
48
+ /**
49
+ * @inheritDoc
50
+ */
51
+ async checkConnection() {
52
+ try {
53
+ await this.getKnex().raw('select 1');
54
+ return { ok: true };
55
+ }
56
+ catch (error) {
57
+ return { ok: false, reason: error.message, error };
58
+ }
59
+ }
42
60
  async transactional(cb, options = {}) {
43
61
  const trx = await this.begin(options);
44
62
  try {
@@ -1,5 +1,5 @@
1
1
  import type { Knex } from 'knex';
2
- import { type AnyEntity, type Collection, type Configuration, type ConnectionType, type Constructor, type CountOptions, DatabaseDriver, type DeleteOptions, type Dictionary, type DriverMethodOptions, type EntityData, type EntityDictionary, type EntityField, EntityManagerType, type EntityMetadata, type EntityName, type EntityProperty, type FilterQuery, type FindOneOptions, type FindOptions, type IDatabaseDriver, type LockOptions, type LoggingOptions, type NativeInsertUpdateManyOptions, type NativeInsertUpdateOptions, type OrderDefinition, type PopulateOptions, type Primary, type QueryOrderMap, type QueryResult, type Transaction, type UpsertManyOptions, type UpsertOptions } from '@mikro-orm/core';
2
+ import { type AnyEntity, type Collection, type Configuration, type ConnectionType, type Constructor, type CountOptions, DatabaseDriver, type DeleteOptions, type Dictionary, type DriverMethodOptions, type EntityData, type EntityDictionary, type EntityField, EntityManagerType, type EntityMetadata, type EntityName, type EntityProperty, type FilterQuery, type FindOneOptions, type FindOptions, type IDatabaseDriver, type LockOptions, type LoggingOptions, type NativeInsertUpdateManyOptions, type NativeInsertUpdateOptions, type Options, type OrderDefinition, type PopulateOptions, type Primary, type QueryOrderMap, type QueryResult, type Transaction, type UpsertManyOptions, type UpsertOptions } from '@mikro-orm/core';
3
3
  import type { AbstractSqlConnection } from './AbstractSqlConnection';
4
4
  import type { AbstractSqlPlatform } from './AbstractSqlPlatform';
5
5
  import { QueryBuilder, QueryType } from './query';
@@ -15,6 +15,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
15
15
  createEntityManager<D extends IDatabaseDriver = IDatabaseDriver>(useContext?: boolean): D[typeof EntityManagerType];
16
16
  find<T extends object, P extends string = never, F extends string = '*'>(entityName: string, where: FilterQuery<T>, options?: FindOptions<T, P, F>): Promise<EntityData<T>[]>;
17
17
  findOne<T extends object, P extends string = never, F extends string = '*'>(entityName: string, where: FilterQuery<T>, options?: FindOneOptions<T, P, F>): Promise<EntityData<T> | null>;
18
+ protected hasToManyJoins<T extends object>(hint: PopulateOptions<T>, meta: EntityMetadata<T>): boolean;
18
19
  findVirtual<T extends object>(entityName: string, where: FilterQuery<T>, options: FindOptions<T, any, any>): Promise<EntityData<T>[]>;
19
20
  countVirtual<T extends object>(entityName: string, where: FilterQuery<T>, options: CountOptions<T, any>): Promise<number>;
20
21
  protected findFromVirtual<T extends object>(entityName: string, where: FilterQuery<T>, options: FindOptions<T, any> | CountOptions<T, any>, type: QueryType): Promise<EntityData<T>[] | number>;
@@ -38,12 +39,17 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
38
39
  /**
39
40
  * @internal
40
41
  */
41
- joinedProps<T>(meta: EntityMetadata, populate: PopulateOptions<T>[]): PopulateOptions<T>[];
42
+ joinedProps<T>(meta: EntityMetadata, populate: PopulateOptions<T>[], options?: {
43
+ strategy?: Options['loadStrategy'];
44
+ }): PopulateOptions<T>[];
42
45
  /**
43
46
  * @internal
44
47
  */
45
48
  mergeJoinedResult<T extends object>(rawResults: EntityData<T>[], meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[]): EntityData<T>[];
46
- protected getFieldsForJoinedLoad<T extends object>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, explicitFields?: Field<T>[], populate?: PopulateOptions<T>[], parentTableAlias?: string, parentJoinPath?: string): Field<T>[];
49
+ protected getFieldsForJoinedLoad<T extends object>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, explicitFields?: Field<T>[], populate?: PopulateOptions<T>[], options?: {
50
+ strategy?: Options['loadStrategy'];
51
+ populateWhere?: FindOptions<any>['populateWhere'];
52
+ }, parentTableAlias?: string, parentJoinPath?: string): Field<T>[];
47
53
  /**
48
54
  * @internal
49
55
  */
@@ -57,8 +63,12 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
57
63
  protected extractManyToMany<T>(entityName: string, data: EntityDictionary<T>): EntityData<T>;
58
64
  protected processManyToMany<T extends object>(meta: EntityMetadata<T> | undefined, pks: Primary<T>[], collections: EntityData<T>, clear: boolean, options?: DriverMethodOptions): Promise<void>;
59
65
  lockPessimistic<T extends object>(entity: T, options: LockOptions): Promise<void>;
60
- protected buildJoinedPropsOrderBy<T extends object>(entityName: string, qb: QueryBuilder<T>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], parentPath?: string): QueryOrderMap<T>[];
66
+ protected buildOrderBy<T extends object>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'strategy' | 'orderBy' | 'populateOrderBy'>): QueryOrderMap<T>[];
67
+ protected buildPopulateOrderBy<T extends object>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, populateOrderBy: QueryOrderMap<T>[], parentPath: string, parentAlias?: string): QueryOrderMap<T>[];
68
+ protected buildJoinedPropsOrderBy<T extends object>(qb: QueryBuilder<T>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], options?: Pick<FindOptions<any>, 'strategy' | 'orderBy' | 'populateOrderBy'>, parentPath?: string): QueryOrderMap<T>[];
61
69
  protected normalizeFields<T extends object>(fields: Field<T>[], prefix?: string): string[];
62
70
  protected processField<T extends object>(meta: EntityMetadata<T>, prop: EntityProperty<T> | undefined, field: string, ret: Field<T>[], populate: PopulateOptions<T>[], joinedProps: PopulateOptions<T>[], qb: QueryBuilder<T>): void;
63
- protected buildFields<T extends object>(meta: EntityMetadata<T>, populate: PopulateOptions<T>[], joinedProps: PopulateOptions<T>[], qb: QueryBuilder<T>, alias: string, fields?: Field<T>[]): Field<T>[];
71
+ protected buildFields<T extends object>(meta: EntityMetadata<T>, populate: PopulateOptions<T>[], joinedProps: PopulateOptions<T>[], qb: QueryBuilder<T>, alias: string, fields?: Field<T>[], options?: {
72
+ strategy?: Options['loadStrategy'];
73
+ }): Field<T>[];
64
74
  }
@@ -29,17 +29,19 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
29
29
  return this.findVirtual(entityName, where, options);
30
30
  }
31
31
  const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
32
- const joinedProps = this.joinedProps(meta, populate);
32
+ const joinedProps = this.joinedProps(meta, populate, options);
33
33
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
34
- const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options.fields);
35
- const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(entityName, qb, meta, joinedProps);
36
- const orderBy = [...core_1.Utils.asArray(options.orderBy), ...joinedPropsOrderBy];
34
+ const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options.fields, options);
35
+ const orderBy = this.buildOrderBy(qb, meta, populate, options);
36
+ core_1.Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
37
37
  if (core_1.Utils.isPrimaryKey(where, meta.compositePK)) {
38
38
  where = { [core_1.Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
39
39
  }
40
40
  const { first, last, before, after } = options;
41
41
  const isCursorPagination = [first, last, before, after].some(v => v != null);
42
+ qb.__populateWhere = options._populateWhere;
42
43
  qb.select(fields)
44
+ // only add populateWhere if we are populate-joining, as this will be used to add `on` conditions
43
45
  .populate(populate, joinedProps.length > 0 ? options.populateWhere : undefined)
44
46
  .where(where)
45
47
  .groupBy(options.groupBy)
@@ -55,13 +57,12 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
55
57
  else {
56
58
  qb.orderBy(orderBy);
57
59
  }
58
- if (options.limit !== undefined) {
60
+ if (options.limit != null || options.offset != null) {
59
61
  qb.limit(options.limit, options.offset);
60
62
  }
61
63
  if (options.lockMode) {
62
64
  qb.setLockMode(options.lockMode, options.lockTableAliases);
63
65
  }
64
- core_1.Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
65
66
  const result = await this.rethrow(qb.execute('all'));
66
67
  if (isCursorPagination && !first && !!last) {
67
68
  result.reverse();
@@ -72,8 +73,9 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
72
73
  const opts = { populate: [], ...(options || {}) };
73
74
  const meta = this.metadata.find(entityName);
74
75
  const populate = this.autoJoinOneToOneOwner(meta, opts.populate, opts.fields);
75
- const joinedProps = this.joinedProps(meta, populate);
76
- if (joinedProps.length === 0) {
76
+ const joinedProps = this.joinedProps(meta, populate, options);
77
+ const hasToManyJoins = joinedProps.some(hint => this.hasToManyJoins(hint, meta));
78
+ if (joinedProps.length === 0 || !hasToManyJoins) {
77
79
  opts.limit = 1;
78
80
  }
79
81
  if (opts.limit > 0 && !opts.flags?.includes(core_1.QueryFlag.DISABLE_PAGINATE)) {
@@ -83,6 +85,17 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
83
85
  const res = await this.find(entityName, where, opts);
84
86
  return res[0] || null;
85
87
  }
88
+ hasToManyJoins(hint, meta) {
89
+ const [propName] = hint.field.split(':', 2);
90
+ const prop = meta.properties[propName];
91
+ if (prop && [core_1.ReferenceKind.ONE_TO_MANY, core_1.ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
92
+ return true;
93
+ }
94
+ if (hint.children && prop.targetMeta) {
95
+ return hint.children.some(hint => this.hasToManyJoins(hint, prop.targetMeta));
96
+ }
97
+ return false;
98
+ }
86
99
  async findVirtual(entityName, where, options) {
87
100
  return this.findFromVirtual(entityName, where, options, query_1.QueryType.SELECT);
88
101
  }
@@ -155,34 +168,61 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
155
168
  }
156
169
  mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
157
170
  const joinedProps = this.joinedProps(meta, populate);
158
- joinedProps.forEach(p => {
159
- const relation = meta.properties[p.field];
171
+ joinedProps.forEach(hint => {
172
+ const [propName, ref] = hint.field.split(':', 2);
173
+ const prop = meta.properties[propName];
160
174
  /* istanbul ignore next */
161
- if (!relation) {
175
+ if (!prop) {
176
+ return;
177
+ }
178
+ const pivotRefJoin = prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref;
179
+ const meta2 = this.metadata.find(prop.type);
180
+ let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
181
+ if (!parentJoinPath) {
182
+ path = '[populate]' + path;
183
+ }
184
+ if (pivotRefJoin) {
185
+ path += '[pivot]';
186
+ }
187
+ const relationAlias = qb.getAliasForJoinPath(path, { matchPopulateJoins: true });
188
+ // pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
189
+ if (pivotRefJoin) {
190
+ let item;
191
+ if (prop.inverseJoinColumns.length > 1) { // composite keys
192
+ item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
193
+ }
194
+ else {
195
+ const alias = `${relationAlias}__${prop.inverseJoinColumns[0]}`;
196
+ item = root[alias];
197
+ }
198
+ prop.joinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
199
+ prop.inverseJoinColumns.forEach(name => delete root[`${relationAlias}__${name}`]);
200
+ result[prop.name] ??= [];
201
+ if (item) {
202
+ result[prop.name].push(item);
203
+ }
162
204
  return;
163
205
  }
164
- const meta2 = this.metadata.find(relation.type);
165
- const path = parentJoinPath ? `${parentJoinPath}.${relation.name}` : `${meta.name}.${relation.name}`;
166
- const relationAlias = qb.getAliasForJoinPath(path);
167
- const relationPojo = {};
168
206
  // If the primary key value for the relation is null, we know we haven't joined to anything
169
207
  // and therefore we don't return any record (since all values would be null)
170
208
  const hasPK = meta2.primaryKeys.every(pk => meta2.properties[pk].fieldNames.every(name => {
171
209
  return root[`${relationAlias}__${name}`] != null;
172
210
  }));
173
211
  if (!hasPK) {
174
- if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(relation.kind)) {
175
- result[relation.name] ??= [];
212
+ if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
213
+ result[prop.name] ??= [];
176
214
  }
177
- if ([core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(relation.kind)) {
178
- result[relation.name] = null;
215
+ if ([core_1.ReferenceKind.MANY_TO_ONE, core_1.ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
216
+ result[prop.name] ??= null;
179
217
  }
180
218
  return;
181
219
  }
182
- const tz = this.platform.getTimezone();
220
+ const relationPojo = {};
183
221
  meta2.props
184
222
  .filter(prop => prop.persist === false && prop.fieldNames)
223
+ .filter(prop => !prop.lazy || populate.some(p => p.field === prop.name || p.all))
185
224
  .forEach(prop => {
225
+ /* istanbul ignore if */
186
226
  if (prop.fieldNames.length > 1) { // composite keys
187
227
  relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
188
228
  }
@@ -192,11 +232,12 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
192
232
  }
193
233
  });
194
234
  meta2.props
195
- .filter(prop => this.platform.shouldHaveColumn(prop, p.children || []))
235
+ .filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []))
196
236
  .forEach(prop => {
197
237
  if (prop.fieldNames.length > 1) { // composite keys
198
- relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
238
+ const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
199
239
  prop.fieldNames.map(name => delete root[`${relationAlias}__${name}`]);
240
+ relationPojo[prop.name] = core_1.Utils.mapFlatCompositePrimaryKey(fk, prop);
200
241
  }
201
242
  else if (prop.runtimeType === 'Date') {
202
243
  const alias = `${relationAlias}__${prop.fieldNames[0]}`;
@@ -209,14 +250,14 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
209
250
  delete root[alias];
210
251
  }
211
252
  });
212
- if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(relation.kind)) {
213
- result[relation.name] ??= [];
214
- result[relation.name].push(relationPojo);
253
+ if ([core_1.ReferenceKind.MANY_TO_MANY, core_1.ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
254
+ result[prop.name] ??= [];
255
+ result[prop.name].push(relationPojo);
215
256
  }
216
257
  else {
217
- result[relation.name] = relationPojo;
258
+ result[prop.name] = relationPojo;
218
259
  }
219
- const populateChildren = p.children || [];
260
+ const populateChildren = hint.children || [];
220
261
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
221
262
  });
222
263
  }
@@ -388,10 +429,10 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
388
429
  }
389
430
  else {
390
431
  qb.update(data).where(where);
391
- // reload generated columns
432
+ // reload generated columns and version fields
392
433
  const returning = [];
393
434
  meta?.props
394
- .filter(prop => prop.generated && !prop.primary)
435
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
395
436
  .forEach(prop => returning.push(prop.name));
396
437
  qb.returning(returning);
397
438
  }
@@ -407,7 +448,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
407
448
  options.convertCustomTypes ??= true;
408
449
  const meta = this.metadata.get(entityName);
409
450
  if (options.upsert) {
410
- const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]) : meta.primaryKeys);
451
+ const uniqueFields = options.onConflictFields ?? (core_1.Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => core_1.Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
411
452
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
412
453
  const returning = (0, core_1.getOnConflictReturningFields)(meta, data[0], uniqueFields, options);
413
454
  qb.insert(data)
@@ -420,7 +461,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
420
461
  if (options.onConflictAction === 'ignore') {
421
462
  qb.ignore();
422
463
  }
423
- return qb.execute('run', false);
464
+ return this.rethrow(qb.execute('run', false));
424
465
  }
425
466
  const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
426
467
  const keys = new Set();
@@ -433,9 +474,9 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
433
474
  }
434
475
  });
435
476
  });
436
- // reload generated columns
477
+ // reload generated columns and version fields
437
478
  meta?.props
438
- .filter(prop => prop.generated && !prop.primary)
479
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
439
480
  .forEach(prop => returning.add(prop.name));
440
481
  const pkCond = core_1.Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames)).map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ');
441
482
  const params = [];
@@ -627,7 +668,7 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
627
668
  const targetAlias = qb.getNextAlias(prop.targetMeta.tableName);
628
669
  const targetSchema = this.getSchemaName(prop.targetMeta, options) ?? this.platform.getDefaultSchemaName();
629
670
  qb.innerJoin(pivotProp1.name, targetAlias, {}, targetSchema);
630
- const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options.fields);
671
+ const targetFields = this.buildFields(prop.targetMeta, (options.populate ?? []), [], qb, targetAlias, options.fields, options);
631
672
  for (const field of targetFields) {
632
673
  const f = field.toString();
633
674
  fields.unshift(f.includes('.') ? field : `${targetAlias}.${f}`);
@@ -638,7 +679,8 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
638
679
  qb.leftJoin(`${targetAlias}.${hint.field}`, alias);
639
680
  // eslint-disable-next-line dot-notation
640
681
  Object.values(qb['_joins']).forEach(join => {
641
- if (join.alias === alias && join.prop.name === hint.field) {
682
+ const [propName] = hint.field.split(':', 2);
683
+ if (join.alias === alias && join.prop.name === propName) {
642
684
  fields.push(...qb.helper.mapJoinColumns(qb.type, join));
643
685
  }
644
686
  });
@@ -702,16 +744,21 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
702
744
  const toPopulate = meta.relations
703
745
  .filter(prop => prop.kind === core_1.ReferenceKind.ONE_TO_ONE && !prop.owner && !relationsToPopulate.includes(prop.name))
704
746
  .filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
705
- .map(prop => ({ field: prop.name, strategy: prop.strategy }));
747
+ .map(prop => ({ field: `${prop.name}:ref`, strategy: prop.strategy }));
706
748
  return [...populate, ...toPopulate];
707
749
  }
708
750
  /**
709
751
  * @internal
710
752
  */
711
- joinedProps(meta, populate) {
712
- return populate.filter(p => {
713
- const prop = meta.properties[p.field] || {};
714
- return (p.strategy || prop.strategy || this.config.get('loadStrategy')) === core_1.LoadStrategy.JOINED && ![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind);
753
+ joinedProps(meta, populate, options) {
754
+ return populate.filter(hint => {
755
+ const [propName, ref] = hint.field.split(':', 2);
756
+ const prop = meta.properties[propName] || {};
757
+ if (ref) {
758
+ // keep only pivot ref joins here, as that only triggers actual join
759
+ return prop.kind === core_1.ReferenceKind.MANY_TO_MANY;
760
+ }
761
+ return (prop.strategy || options?.strategy || hint.strategy || this.config.get('loadStrategy')) === core_1.LoadStrategy.JOINED && ![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind);
715
762
  });
716
763
  }
717
764
  /**
@@ -724,18 +771,23 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
724
771
  const pk = core_1.Utils.getCompositeKeyHash(item, meta);
725
772
  if (map[pk]) {
726
773
  for (const hint of joinedProps) {
727
- const prop = meta.properties[hint.field];
728
- if (!item[hint.field]) {
774
+ const [propName, ref] = hint.field.split(':', 2);
775
+ const prop = meta.properties[propName];
776
+ if (!item[propName]) {
777
+ continue;
778
+ }
779
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref) {
780
+ map[pk][propName] = [...map[pk][propName], ...item[propName]];
729
781
  continue;
730
782
  }
731
783
  switch (prop.kind) {
732
784
  case core_1.ReferenceKind.ONE_TO_MANY:
733
785
  case core_1.ReferenceKind.MANY_TO_MANY:
734
- map[pk][hint.field] = this.mergeJoinedResult([...map[pk][hint.field], ...item[hint.field]], prop.targetMeta, hint.children ?? []);
786
+ map[pk][propName] = this.mergeJoinedResult([...map[pk][propName], ...item[propName]], prop.targetMeta, hint.children ?? []);
735
787
  break;
736
788
  case core_1.ReferenceKind.MANY_TO_ONE:
737
789
  case core_1.ReferenceKind.ONE_TO_ONE:
738
- map[pk][hint.field] = this.mergeJoinedResult([map[pk][hint.field], item[hint.field]], prop.targetMeta, hint.children ?? [])[0];
790
+ map[pk][propName] = this.mergeJoinedResult([map[pk][propName], item[propName]], prop.targetMeta, hint.children ?? [])[0];
739
791
  break;
740
792
  }
741
793
  }
@@ -747,9 +799,9 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
747
799
  }
748
800
  return res;
749
801
  }
750
- getFieldsForJoinedLoad(qb, meta, explicitFields, populate = [], parentTableAlias, parentJoinPath) {
802
+ getFieldsForJoinedLoad(qb, meta, explicitFields, populate = [], options, parentTableAlias, parentJoinPath) {
751
803
  const fields = [];
752
- const joinedProps = this.joinedProps(meta, populate);
804
+ const joinedProps = this.joinedProps(meta, populate, options);
753
805
  if (explicitFields?.includes('*')) {
754
806
  fields.push('*');
755
807
  }
@@ -762,25 +814,39 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
762
814
  }
763
815
  return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
764
816
  };
765
- // alias all fields in the primary table
766
- meta.props
767
- .filter(prop => shouldHaveColumn(prop, populate, explicitFields))
768
- .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, parentTableAlias)));
769
- joinedProps.forEach(relation => {
770
- const [propName] = relation.field.split(':', 2);
817
+ const populateWhereAll = options?._populateWhere === 'all' || core_1.Utils.isEmpty(options?._populateWhere);
818
+ // root entity is already handled, skip that
819
+ if (parentJoinPath) {
820
+ // alias all fields in the primary table
821
+ meta.props
822
+ .filter(prop => shouldHaveColumn(prop, populate, explicitFields))
823
+ .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, parentTableAlias)));
824
+ }
825
+ joinedProps.forEach(hint => {
826
+ const [propName, ref] = hint.field.split(':', 2);
771
827
  const prop = meta.properties[propName];
772
828
  const meta2 = this.metadata.find(prop.type);
829
+ const pivotRefJoin = prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref;
773
830
  const tableAlias = qb.getNextAlias(prop.name);
774
831
  const field = parentTableAlias ? `${parentTableAlias}.${prop.name}` : prop.name;
775
- const path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
776
- qb.join(field, tableAlias, {}, query_1.JoinType.leftJoin, path);
832
+ let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
833
+ if (!parentJoinPath && populateWhereAll && !path.startsWith('[populate]')) {
834
+ path = '[populate]' + path;
835
+ }
836
+ const joinType = pivotRefJoin ? query_1.JoinType.pivotJoin : query_1.JoinType.leftJoin;
837
+ qb.join(field, tableAlias, {}, joinType, path);
838
+ if (pivotRefJoin) {
839
+ 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}`)));
840
+ }
777
841
  const childExplicitFields = explicitFields?.filter(f => core_1.Utils.isPlainObject(f)).map(o => o[prop.name])[0] || [];
778
842
  explicitFields?.forEach(f => {
779
843
  if (typeof f === 'string' && f.startsWith(`${prop.name}.`)) {
780
844
  childExplicitFields.push(f.substring(prop.name.length + 1));
781
845
  }
782
846
  });
783
- fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, relation.children, tableAlias, path));
847
+ if (!ref) {
848
+ fields.push(...this.getFieldsForJoinedLoad(qb, meta2, childExplicitFields.length === 0 ? undefined : childExplicitFields, hint.children, options, tableAlias, path));
849
+ }
784
850
  });
785
851
  return fields;
786
852
  }
@@ -790,7 +856,17 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
790
856
  mapPropToFieldNames(qb, prop, tableAlias) {
791
857
  const knex = this.connection.getKnex();
792
858
  const aliased = knex.ref(tableAlias ? `${tableAlias}__${prop.fieldNames[0]}` : prop.fieldNames[0]).toString();
793
- if (prop.customType?.convertToJSValueSQL && tableAlias) {
859
+ if (tableAlias && prop.customTypes?.some(type => type.convertToJSValueSQL)) {
860
+ return prop.fieldNames.map((col, idx) => {
861
+ if (!prop.customTypes[idx].convertToJSValueSQL) {
862
+ return col;
863
+ }
864
+ const prefixed = knex.ref(col).withSchema(tableAlias).toString();
865
+ const aliased = knex.ref(`${tableAlias}__${col}`).toString();
866
+ return (0, core_1.raw)(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
867
+ });
868
+ }
869
+ if (tableAlias && prop.customType?.convertToJSValueSQL) {
794
870
  const prefixed = knex.ref(prop.fieldNames[0]).withSchema(tableAlias).toString();
795
871
  return [(0, core_1.raw)(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
796
872
  }
@@ -857,25 +933,83 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
857
933
  qb.select((0, core_1.raw)('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
858
934
  await this.rethrow(qb.execute());
859
935
  }
860
- buildJoinedPropsOrderBy(entityName, qb, meta, populate, parentPath) {
936
+ buildOrderBy(qb, meta, populate, options) {
937
+ const joinedProps = this.joinedProps(meta, populate, options);
938
+ // `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
939
+ // as `options.populateWhere` will be always recomputed to respect filters
940
+ const populateWhereAll = options._populateWhere !== 'infer' && !core_1.Utils.isEmpty(options._populateWhere);
941
+ const path = (populateWhereAll ? '[populate]' : '') + meta.className;
942
+ const populateOrderBy = this.buildPopulateOrderBy(qb, meta, core_1.Utils.asArray(options.populateOrderBy ?? options.orderBy), path);
943
+ const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
944
+ return [...core_1.Utils.asArray(options.orderBy), ...populateOrderBy, ...joinedPropsOrderBy];
945
+ }
946
+ buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, parentAlias = qb.alias) {
861
947
  const orderBy = [];
862
- const joinedProps = this.joinedProps(meta, populate);
863
- joinedProps.forEach(relation => {
864
- const [propName] = relation.field.split(':', 2);
948
+ for (let i = 0; i < populateOrderBy.length; i++) {
949
+ const orderHint = populateOrderBy[i];
950
+ for (const propName of core_1.Utils.keys(orderHint)) {
951
+ const prop = meta.properties[propName];
952
+ const meta2 = this.metadata.find(prop.type);
953
+ const childOrder = orderHint[prop.name];
954
+ let path = `${parentPath}.${propName}`;
955
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
956
+ path += '[pivot]';
957
+ }
958
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
959
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true }) ?? parentAlias;
960
+ if (!join && parentAlias === qb.alias) {
961
+ continue;
962
+ }
963
+ if (![core_1.ReferenceKind.SCALAR, core_1.ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
964
+ const children = this.buildPopulateOrderBy(qb, meta2, core_1.Utils.asArray(childOrder), path, propAlias);
965
+ orderBy.push(...children);
966
+ continue;
967
+ }
968
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && join) {
969
+ if (prop.fixedOrderColumn) {
970
+ orderBy.push({ [`${join.alias}.${prop.fixedOrderColumn}`]: childOrder });
971
+ }
972
+ else {
973
+ for (const col of prop.inverseJoinColumns) {
974
+ orderBy.push({ [`${join.ownerAlias}.${col}`]: childOrder });
975
+ }
976
+ }
977
+ continue;
978
+ }
979
+ const order = typeof childOrder === 'object' ? childOrder[propName] : childOrder;
980
+ orderBy.push({ [`${propAlias}.${propName}`]: order });
981
+ }
982
+ }
983
+ return orderBy;
984
+ }
985
+ buildJoinedPropsOrderBy(qb, meta, populate, options, parentPath) {
986
+ const orderBy = [];
987
+ const joinedProps = this.joinedProps(meta, populate, options);
988
+ for (const hint of joinedProps) {
989
+ const [propName, ref] = hint.field.split(':', 2);
865
990
  const prop = meta.properties[propName];
866
991
  const propOrderBy = prop.orderBy;
867
- const path = `${parentPath ? parentPath : entityName}.${relation.field}`;
868
- const propAlias = qb.getAliasForJoinPath(path);
992
+ let path = `${parentPath}.${propName}`;
993
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && ref) {
994
+ path += '[pivot]';
995
+ }
996
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
997
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
998
+ const meta2 = this.metadata.find(prop.type);
999
+ if (prop.kind === core_1.ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1000
+ const alias = ref ? propAlias : join.ownerAlias;
1001
+ orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: core_1.QueryOrder.ASC });
1002
+ }
869
1003
  if (propOrderBy) {
870
1004
  core_1.Utils.keys(propOrderBy).forEach(field => {
871
1005
  orderBy.push({ [`${propAlias}.${field}`]: propOrderBy[field] });
872
1006
  });
873
1007
  }
874
- if (relation.children) {
875
- const meta2 = this.metadata.find(prop.type);
876
- orderBy.push(...this.buildJoinedPropsOrderBy(prop.name, qb, meta2, relation.children, path));
1008
+ if (hint.children) {
1009
+ const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta2, hint.children, options, path);
1010
+ orderBy.push(...buildJoinedPropsOrderBy);
877
1011
  }
878
- });
1012
+ }
879
1013
  return orderBy;
880
1014
  }
881
1015
  normalizeFields(fields, prefix = '') {
@@ -913,17 +1047,15 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
913
1047
  }
914
1048
  ret.push(prop.name);
915
1049
  }
916
- buildFields(meta, populate, joinedProps, qb, alias, fields) {
1050
+ buildFields(meta, populate, joinedProps, qb, alias, fields, options) {
917
1051
  const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => p.field === prop.name || p.all));
918
1052
  const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
919
1053
  const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
920
1054
  const hasExplicitFields = !!fields;
921
1055
  const ret = [];
922
1056
  let addFormulas = false;
923
- if (joinedProps.length > 0) {
924
- ret.push(...this.getFieldsForJoinedLoad(qb, meta, fields, populate));
925
- }
926
- else if (fields) {
1057
+ // handle root entity properties first, this is used for both strategies in the same way
1058
+ if (fields) {
927
1059
  for (const field of this.normalizeFields(fields)) {
928
1060
  if (field === '*') {
929
1061
  ret.push('*');
@@ -940,7 +1072,9 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
940
1072
  });
941
1073
  this.processField(meta, prop, parts.join('.'), ret, populate, joinedProps, qb);
942
1074
  }
943
- ret.unshift(...meta.primaryKeys.filter(pk => !fields.includes(pk)));
1075
+ if (!fields.includes('*') && !fields.includes(`${qb.alias}.*`)) {
1076
+ ret.unshift(...meta.primaryKeys.filter(pk => !fields.includes(pk)));
1077
+ }
944
1078
  }
945
1079
  else if (lazyProps.filter(p => !p.formula).length > 0) {
946
1080
  const props = meta.props.filter(prop => this.platform.shouldHaveColumn(prop, populate, false));
@@ -951,6 +1085,9 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
951
1085
  ret.push('*');
952
1086
  addFormulas = true;
953
1087
  }
1088
+ else {
1089
+ ret.push('*');
1090
+ }
954
1091
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
955
1092
  meta.props
956
1093
  .filter(prop => prop.formula && !lazyProps.includes(prop))
@@ -963,7 +1100,11 @@ class AbstractSqlDriver extends core_1.DatabaseDriver {
963
1100
  .filter(prop => prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)
964
1101
  .forEach(prop => ret.push(prop.name));
965
1102
  }
966
- return ret.length > 0 ? core_1.Utils.unique(ret) : ['*'];
1103
+ // add joined relations after the root entity fields
1104
+ if (joinedProps.length > 0) {
1105
+ ret.push(...this.getFieldsForJoinedLoad(qb, meta, fields, populate, options, alias));
1106
+ }
1107
+ return core_1.Utils.unique(ret);
967
1108
  }
968
1109
  }
969
1110
  exports.AbstractSqlDriver = AbstractSqlDriver;
@@ -24,7 +24,7 @@ class SqlEntityRepository extends core_1.EntityRepository {
24
24
  * Returns configured knex instance.
25
25
  */
26
26
  getKnex(type) {
27
- return this.getEntityManager().getKnex();
27
+ return this.getEntityManager().getKnex(type);
28
28
  }
29
29
  /**
30
30
  * @inheritDoc