@mikro-orm/core 7.0.0-dev.229 → 7.0.0-dev.230

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/EntityManager.js CHANGED
@@ -117,7 +117,13 @@ export class EntityManager {
117
117
  await em.tryFlush(entityName, options);
118
118
  where = await em.processWhere(entityName, where, options, 'read');
119
119
  validateParams(where);
120
- options.orderBy = options.orderBy || {};
120
+ const meta = this.metadata.get(entityName);
121
+ if (meta.orderBy) {
122
+ options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
123
+ }
124
+ else {
125
+ options.orderBy ??= {};
126
+ }
121
127
  options.populate = await em.preparePopulate(entityName, options);
122
128
  const populate = options.populate;
123
129
  const cacheKey = em.cacheKey(entityName, options, 'em.find', where);
@@ -131,7 +137,6 @@ export class EntityManager {
131
137
  });
132
138
  return cached.data;
133
139
  }
134
- const meta = this.metadata.get(entityName);
135
140
  options = { ...options };
136
141
  // save the original hint value so we know it was infer/all
137
142
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
@@ -62,7 +62,6 @@ export declare class Collection<T extends object, O extends object = object> {
62
62
  init<TT extends T, P extends string = never>(options?: InitCollectionOptions<TT, P>): Promise<LoadedCollection<Loaded<TT, P>>>;
63
63
  private getEntityManager;
64
64
  private createCondition;
65
- private createOrderBy;
66
65
  private createManyToManyCondition;
67
66
  private createLoadCountCondition;
68
67
  private checkInitialized;
@@ -4,7 +4,6 @@ import { DataloaderType, ReferenceKind } from '../enums.js';
4
4
  import { Reference } from './Reference.js';
5
5
  import { helper, wrap } from './wrap.js';
6
6
  import { QueryHelper } from '../utils/QueryHelper.js';
7
- import { Raw } from '../utils/RawQueryFragment.js';
8
7
  import { inspect } from '../logging/inspect.js';
9
8
  export class Collection {
10
9
  owner;
@@ -93,9 +92,10 @@ export class Collection {
93
92
  async matching(options) {
94
93
  const em = this.getEntityManager();
95
94
  const { where, ctx, ...opts } = options;
96
- opts.orderBy = this.createOrderBy(opts.orderBy);
97
95
  let items;
98
96
  if (this.property.kind === ReferenceKind.MANY_TO_MANY && em.getPlatform().usesPivotTable()) {
97
+ // M:N via pivot table bypasses em.find(), so merge all 3 levels here
98
+ opts.orderBy = QueryHelper.mergeOrderBy(opts.orderBy, this.property.orderBy, this.property.targetMeta?.orderBy);
99
99
  options.populate = await em.preparePopulate(this.property.targetMeta.class, options);
100
100
  const cond = await em.applyFilters(this.property.targetMeta.class, where, options.filters ?? {}, 'read');
101
101
  const map = await em.getDriver().loadFromPivotTable(this.property, [helper(this.owner).__primaryKeys], cond, opts.orderBy, ctx, options);
@@ -103,6 +103,8 @@ export class Collection {
103
103
  await em.populate(items, options.populate, options);
104
104
  }
105
105
  else {
106
+ // em.find() merges entity-level orderBy, so only merge runtime + relation here
107
+ opts.orderBy = QueryHelper.mergeOrderBy(opts.orderBy, this.property.orderBy);
106
108
  items = await em.find(this.property.targetMeta.class, this.createCondition(where), opts);
107
109
  }
108
110
  if (options.store) {
@@ -236,7 +238,7 @@ export class Collection {
236
238
  options = { ...options, filters: QueryHelper.mergePropertyFilters(this.property.filters, options.filters) };
237
239
  if (options.dataloader ?? [DataloaderType.ALL, DataloaderType.COLLECTION].includes(em.config.getDataloaderType())) {
238
240
  const order = [...this.items]; // copy order of references
239
- const orderBy = this.createOrderBy(options.orderBy);
241
+ const orderBy = QueryHelper.mergeOrderBy(options.orderBy, this.property.orderBy, this.property.targetMeta?.orderBy);
240
242
  const customOrder = orderBy.length > 0;
241
243
  const pivotTable = this.property.kind === ReferenceKind.MANY_TO_MANY && em.getPlatform().usesPivotTable();
242
244
  const loader = await em.getDataLoader(pivotTable ? 'm:n' : '1:m');
@@ -300,12 +302,6 @@ export class Collection {
300
302
  }
301
303
  return cond;
302
304
  }
303
- createOrderBy(orderBy = []) {
304
- if (Utils.isEmpty(orderBy) && !Raw.hasObjectFragments(orderBy) && this.property.orderBy) {
305
- orderBy = this.property.orderBy;
306
- }
307
- return Utils.asArray(orderBy);
308
- }
309
305
  createManyToManyCondition(cond) {
310
306
  const dict = cond;
311
307
  if (this.property.owner || this.property.pivotTable) {
@@ -141,7 +141,8 @@ export class EntityLoader {
141
141
  .flatMap(orderBy => orderBy[prop.name]);
142
142
  const where = await this.extractChildCondition(options, prop);
143
143
  if (prop.kind === ReferenceKind.MANY_TO_MANY && this.driver.getPlatform().usesPivotTable()) {
144
- const res = await this.findChildrenFromPivotTable(filtered, prop, options, innerOrderBy, populate, !!ref);
144
+ const pivotOrderBy = QueryHelper.mergeOrderBy(innerOrderBy, prop.orderBy, prop.targetMeta?.orderBy);
145
+ const res = await this.findChildrenFromPivotTable(filtered, prop, options, pivotOrderBy, populate, !!ref);
145
146
  return Utils.flatten(res);
146
147
  }
147
148
  if (prop.polymorphic && prop.polymorphTargets) {
@@ -152,7 +153,8 @@ export class EntityLoader {
152
153
  where,
153
154
  orderBy: innerOrderBy,
154
155
  }, !!(ref || prop.mapToPk));
155
- this.initializeCollections(filtered, prop, field, items, innerOrderBy.length > 0, partial);
156
+ const customOrder = innerOrderBy.length > 0 || !!prop.orderBy || !!prop.targetMeta?.orderBy;
157
+ this.initializeCollections(filtered, prop, field, items, customOrder, partial);
156
158
  return items;
157
159
  }
158
160
  async populateScalar(meta, filtered, options) {
@@ -327,10 +329,7 @@ export class EntityLoader {
327
329
  if (!Utils.isEmpty(prop.where) || Raw.hasObjectFragments(prop.where)) {
328
330
  where = { $and: [where, prop.where] };
329
331
  }
330
- const orderBy = [...Utils.asArray(options.orderBy), ...Utils.asArray(prop.orderBy)].filter((order, idx, array) => {
331
- // skip consecutive ordering with the same key to get around mongo issues
332
- return idx === 0 || !Utils.equals(Utils.getObjectQueryKeys(array[idx - 1]), Utils.getObjectQueryKeys(order));
333
- });
332
+ const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
334
333
  const items = await this.em.find(meta.class, where, {
335
334
  filters, convertCustomTypes, lockMode, populateWhere, logging,
336
335
  orderBy,
@@ -4,7 +4,7 @@ import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, Fi
4
4
  import type { Raw } from '../utils/RawQueryFragment.js';
5
5
  import type { ScalarReference } from './Reference.js';
6
6
  import type { SerializeOptions } from '../serialization/EntitySerializer.js';
7
- import type { Cascade, DeferMode, EmbeddedPrefixMode, LoadStrategy, QueryOrderMap } from '../enums.js';
7
+ import type { Cascade, DeferMode, EmbeddedPrefixMode, LoadStrategy, QueryOrderKeysFlat, QueryOrderMap } from '../enums.js';
8
8
  import type { EventSubscriber } from '../events/EventSubscriber.js';
9
9
  import type { IType, Type } from '../types/Type.js';
10
10
  import { types } from '../types/index.js';
@@ -419,7 +419,9 @@ declare const propertyBuilders: {
419
419
  interval: () => UniversalPropertyOptionsBuilder<string, EmptyOptions, IncludeKeysForProperty>;
420
420
  unknown: () => UniversalPropertyOptionsBuilder<{}, EmptyOptions, IncludeKeysForProperty>;
421
421
  };
422
- export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'repository'> {
422
+ /** Own keys + base entity keys (when TBase is not `never`). Guards against `keyof never = string | number | symbol`. */
423
+ type AllKeys<TProperties, TBase> = keyof TProperties | (IsNever<TBase> extends true ? never : keyof TBase);
424
+ export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'repository' | 'orderBy'> {
423
425
  name: TName;
424
426
  tableName?: TTableName;
425
427
  extends?: {
@@ -430,10 +432,15 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
430
432
  hooks?: DefineEntityHooks;
431
433
  repository?: () => TRepository;
432
434
  inheritance?: 'tpt';
433
- discriminatorColumn?: keyof TProperties;
434
- versionProperty?: keyof TProperties;
435
- concurrencyCheckKeys?: Set<keyof TProperties>;
436
- serializedPrimaryKey?: keyof TProperties;
435
+ orderBy?: {
436
+ [K in Extract<AllKeys<TProperties, TBase>, string>]?: QueryOrderKeysFlat;
437
+ } | {
438
+ [K in Extract<AllKeys<TProperties, TBase>, string>]?: QueryOrderKeysFlat;
439
+ }[];
440
+ discriminatorColumn?: string;
441
+ versionProperty?: AllKeys<TProperties, TBase>;
442
+ concurrencyCheckKeys?: Set<AllKeys<TProperties, TBase>>;
443
+ serializedPrimaryKey?: AllKeys<TProperties, TBase>;
437
444
  indexes?: {
438
445
  properties?: keyof TProperties | (keyof TProperties)[];
439
446
  name?: string;
@@ -11,6 +11,17 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
11
11
  schema?: string;
12
12
  /** Override default collection/table name. Alias for `tableName`. */
13
13
  collection?: string;
14
+ /**
15
+ * Set default ordering for this entity. This ordering is applied when:
16
+ * - Querying the entity directly via `em.find()`, `em.findAll()`, etc.
17
+ * - Populating the entity as a relation
18
+ *
19
+ * All orderings are combined together. Precedence (highest to lowest):
20
+ * 1. Runtime `FindOptions.orderBy`
21
+ * 2. Relation-level `@OneToMany({ orderBy })` / `@ManyToMany({ orderBy })`
22
+ * 3. Entity-level `@Entity({ orderBy })`
23
+ */
24
+ orderBy?: QueryOrderMap<E> | QueryOrderMap<E>[];
14
25
  /** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
15
26
  discriminatorColumn?: (T extends EntityClass<infer P> ? keyof P : string) | AnyString;
16
27
  /** For {@doclink inheritance-mapping#single-table-inheritance | Single Table Inheritance}. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
3
  "type": "module",
4
- "version": "7.0.0-dev.229",
4
+ "version": "7.0.0-dev.230",
5
5
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
6
6
  "exports": {
7
7
  "./package.json": "./package.json",
package/typings.d.ts CHANGED
@@ -681,6 +681,11 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
681
681
  /** For TPT: properties defined only in THIS entity (not inherited from parent). */
682
682
  ownProps?: EntityProperty<Entity>[];
683
683
  hasTriggers?: boolean;
684
+ /**
685
+ * Default ordering for this entity. Applied when querying this entity directly
686
+ * or when it's populated as a relation. Combined with other orderings based on precedence.
687
+ */
688
+ orderBy?: QueryOrderMap<Entity> | QueryOrderMap<Entity>[];
684
689
  /** @internal can be used for computed numeric cache keys */
685
690
  readonly _id: number;
686
691
  }
@@ -1,4 +1,5 @@
1
1
  import type { Dictionary, EntityMetadata, EntityName, EntityProperty, FilterDef, FilterQuery } from '../typings.js';
2
+ import { type QueryOrderMap } from '../enums.js';
2
3
  import type { Platform } from '../platforms/Platform.js';
3
4
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
4
5
  import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
@@ -26,6 +27,11 @@ export declare class QueryHelper {
26
27
  private static processJsonCondition;
27
28
  private static getValueType;
28
29
  static findProperty<T>(fieldName: string, options: ProcessWhereOptions<T>): EntityProperty<T> | undefined;
30
+ /**
31
+ * Merges multiple orderBy sources with key-level deduplication (first-seen key wins).
32
+ * RawQueryFragment symbol keys are never deduped (each is unique).
33
+ */
34
+ static mergeOrderBy<T>(...sources: (QueryOrderMap<T> | QueryOrderMap<T>[] | undefined)[]): QueryOrderMap<T>[];
29
35
  }
30
36
  interface ProcessWhereOptions<T> {
31
37
  where: FilterQuery<T>;
@@ -300,4 +300,29 @@ export class QueryHelper {
300
300
  const meta = entityName ? options.metadata.find(entityName) : undefined;
301
301
  return meta?.properties[propName];
302
302
  }
303
+ /**
304
+ * Merges multiple orderBy sources with key-level deduplication (first-seen key wins).
305
+ * RawQueryFragment symbol keys are never deduped (each is unique).
306
+ */
307
+ static mergeOrderBy(...sources) {
308
+ const result = [];
309
+ const seenKeys = new Set();
310
+ for (const source of sources) {
311
+ if (source == null) {
312
+ continue;
313
+ }
314
+ for (const item of Utils.asArray(source)) {
315
+ for (const key of Utils.getObjectQueryKeys(item)) {
316
+ if (typeof key === 'symbol') {
317
+ result.push({ [key]: item[key] });
318
+ }
319
+ else if (!seenKeys.has(key)) {
320
+ seenKeys.add(key);
321
+ result.push({ [key]: item[key] });
322
+ }
323
+ }
324
+ }
325
+ }
326
+ return result;
327
+ }
303
328
  }
package/utils/Utils.js CHANGED
@@ -123,7 +123,7 @@ export function parseJsonSafe(value) {
123
123
  }
124
124
  export class Utils {
125
125
  static PK_SEPARATOR = '~~~';
126
- static #ORM_VERSION = '7.0.0-dev.229';
126
+ static #ORM_VERSION = '7.0.0-dev.230';
127
127
  /**
128
128
  * Checks if the argument is instance of `Object`. Returns false for arrays.
129
129
  */