@mikro-orm/core 7.2.2-dev.1 → 7.2.2-dev.10

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
@@ -889,7 +889,7 @@ export class EntityManager {
889
889
  const name = Utils.className(entityName);
890
890
  /* v8 ignore next */
891
891
  where = Utils.isEntity(where) ? helper(where).getPrimaryKey() : where;
892
- throw failHandler(name, where);
892
+ throw failHandler(name, QueryHelper.processParams(where));
893
893
  }
894
894
  return entity;
895
895
  }
@@ -994,10 +994,14 @@ export class EntityManager {
994
994
  });
995
995
  const uniqueFields = options.onConflictFields ??
996
996
  (Utils.isPlainObject(where) ? Object.keys(where) : meta.primaryKeys);
997
- const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
997
+ const platform = this.getPlatform();
998
+ const returning = getOnConflictReturningFields(meta, data, uniqueFields, options, platform);
999
+ if (options.onConflictWhere && !ret.row) {
1000
+ returning.push(...Object.keys(data));
1001
+ }
998
1002
  if (options.onConflictAction === 'ignore' ||
999
1003
  !helper(entity).hasPrimaryKey() ||
1000
- (returning.length > 0 && !(this.getPlatform().usesReturningStatement() && ret.row))) {
1004
+ (returning.length > 0 && !((platform.usesReturningStatement() || platform.usesOutputStatement()) && ret.row))) {
1001
1005
  const where = {};
1002
1006
  if (Array.isArray(uniqueFields)) {
1003
1007
  for (const prop of uniqueFields) {
@@ -1088,6 +1092,32 @@ export class EntityManager {
1088
1092
  }
1089
1093
  return ret;
1090
1094
  }
1095
+ // merge fields are derived from the first row, rows of a different shape need their own statement
1096
+ if (!options.onConflictMergeFields && options.onConflictAction !== 'ignore') {
1097
+ const pks = this.metadata.get(entityName).primaryKeys;
1098
+ const shapes = new Map();
1099
+ data.forEach((row, i) => {
1100
+ // entity instances carry their unset properties as `undefined` keys, while in plain rows those are merged as `null`
1101
+ const shape = Object.keys(row)
1102
+ .filter(key => !pks.includes(key) && !(Utils.isEntity(row) && row[key] === undefined))
1103
+ .sort()
1104
+ .join();
1105
+ const idx = shapes.get(shape) ?? [];
1106
+ shapes.set(shape, idx);
1107
+ idx.push(i);
1108
+ });
1109
+ if (shapes.size > 1) {
1110
+ const ret = [];
1111
+ for (const idx of shapes.values()) {
1112
+ const res = await this.upsertMany(entityName, idx.map(i => data[i]), options);
1113
+ idx.forEach((i, j) => (ret[i] = res[j]));
1114
+ }
1115
+ // rows resolving to the same entity are returned once, as in the single statement path
1116
+ return [...new Set(ret.filter(entity => entity))];
1117
+ }
1118
+ }
1119
+ // the rows are replaced with their processed copies below, keep the caller's array intact
1120
+ data = [...data];
1091
1121
  const meta = this.metadata.get(entityName);
1092
1122
  const convertCustomTypes = !Utils.isEntity(data[0]);
1093
1123
  const generatedFields = new Set();
@@ -1147,6 +1177,10 @@ export class EntityManager {
1147
1177
  platform: this.getPlatform(),
1148
1178
  });
1149
1179
  row = QueryHelper.processObjectParams(row);
1180
+ // hooks and the result merging need `data[i]` to be the same object as the processed row
1181
+ if (!Utils.isEntity(data[i])) {
1182
+ data[i] = row;
1183
+ }
1150
1184
  validateParams(row, 'insert data');
1151
1185
  allData.push(row);
1152
1186
  allWhere.push(where);
@@ -1177,9 +1211,9 @@ export class EntityManager {
1177
1211
  entitiesByData.clear();
1178
1212
  const loadPK = new Map();
1179
1213
  allData.forEach((row, i) => {
1180
- em.#unitOfWork
1181
- .getChangeSetPersister()
1182
- .mapReturnedValues(Utils.isEntity(data[i]) ? data[i] : null, Utils.isEntity(data[i]) ? {} : data[i], res.rows?.[i], meta, true);
1214
+ em.#unitOfWork.getChangeSetPersister().mapReturnedValues(Utils.isEntity(data[i]) ? data[i] : null, Utils.isEntity(data[i]) ? {} : data[i],
1215
+ // A suppressed conflict omits its row; positional mapping would hydrate a different entity.
1216
+ res.rows?.length === allData.length ? res.rows[i] : undefined, meta, true);
1183
1217
  const entity = Utils.isEntity(data[i])
1184
1218
  ? data[i]
1185
1219
  : em.#entityFactory.create(entityName, row, {
@@ -1197,8 +1231,13 @@ export class EntityManager {
1197
1231
  // (onConflictWhere can suppress writes, leaving some rows out)
1198
1232
  // oxfmt-ignore
1199
1233
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1200
- const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
1201
- const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length === data.length);
1234
+ const platform = this.getPlatform();
1235
+ const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options, platform);
1236
+ if (options.onConflictWhere && res.rows?.length !== allData.length) {
1237
+ returning.push(...meta.comparableProps.filter(p => !p.lazy && !p.embeddable).map(p => p.name));
1238
+ }
1239
+ const reloadFields = returning.length > 0 &&
1240
+ !((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length === data.length);
1202
1241
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
1203
1242
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
1204
1243
  const add = new Set(propIndex !== false && propIndex >= 0
@@ -1229,8 +1268,9 @@ export class EntityManager {
1229
1268
  const row = data2.find(row => {
1230
1269
  const tmp = {};
1231
1270
  add.forEach(k => {
1232
- if (!meta.properties[k]?.primary) {
1233
- tmp[k] = row[k];
1271
+ const prop = meta.properties[k];
1272
+ if (!prop?.primary) {
1273
+ tmp[k] = prop?.customType ? prop.customType.convertToDatabaseValue(row[k], this.getPlatform()) : row[k];
1234
1274
  }
1235
1275
  });
1236
1276
  return this.#comparator.matching(entityName, cond, tmp);
@@ -267,7 +267,7 @@ export declare class UniversalPropertyOptionsBuilder<in out Value, in out Option
267
267
  autoincrement: false;
268
268
  }, IncludeKeys>, IncludeKeys>;
269
269
  /**
270
- * Add the property to the `returning` statement.
270
+ * Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
271
271
  */
272
272
  returning(returning?: boolean): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
273
273
  /**
@@ -88,7 +88,7 @@ export class UniversalPropertyOptionsBuilder {
88
88
  return this.assignOptions({ autoincrement });
89
89
  }
90
90
  /**
91
- * Add the property to the `returning` statement.
91
+ * Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
92
92
  */
93
93
  returning(returning = true) {
94
94
  return this.assignOptions({ returning });
@@ -128,6 +128,8 @@ export declare class MetadataDiscovery {
128
128
  private initRelation;
129
129
  /** Resolves the `through` option of a virtual to-one relation into a read-only formula property. */
130
130
  private initThroughRelation;
131
+ /** `returning` is only honoured for physical columns that can be hydrated back into the entity. */
132
+ private initReturning;
131
133
  private initColumnType;
132
134
  private getMappedType;
133
135
  private getPrefix;
@@ -174,6 +174,7 @@ export class MetadataDiscovery {
174
174
  this.initRelation(p);
175
175
  this.initThroughRelation(m, p);
176
176
  this.initColumnType(p);
177
+ this.initReturning(p);
177
178
  });
178
179
  forEachProp((m, p) => this.initIndexes(m, p));
179
180
  filtered.forEach(meta => this.autoWireBidirectionalProperties(meta));
@@ -2129,6 +2130,16 @@ export class MetadataDiscovery {
2129
2130
  prop.persist = false;
2130
2131
  prop.formula = columns => this.#platform.getThroughRelationFormula(prop, columns);
2131
2132
  }
2133
+ /** `returning` is only honoured for physical columns that can be hydrated back into the entity. */
2134
+ initReturning(prop) {
2135
+ const owning = prop.kind === ReferenceKind.SCALAR ||
2136
+ prop.kind === ReferenceKind.MANY_TO_ONE ||
2137
+ (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner) ||
2138
+ (prop.kind === ReferenceKind.EMBEDDED && prop.object);
2139
+ if (prop.returning && (!owning || prop.persist === false || prop.hydrate === false || prop.formula)) {
2140
+ prop.returning = false;
2141
+ }
2142
+ }
2132
2143
  initColumnType(prop) {
2133
2144
  this.initUnsigned(prop);
2134
2145
  // Get the target properties for FK relations - use targetKey property if specified, otherwise PKs
@@ -175,7 +175,7 @@ export interface PropertyOptions<Owner> {
175
175
  */
176
176
  autoincrement?: boolean;
177
177
  /**
178
- * Add the property to the `returning` statement.
178
+ * Add the property to the `returning` clause of inserts, updates and upserts, reloading the database value into the entity.
179
179
  */
180
180
  returning?: boolean;
181
181
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.2-dev.1",
3
+ "version": "7.2.2-dev.10",
4
4
  "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.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -35,6 +35,8 @@ export declare class QueryHelper {
35
35
  */
36
36
  static liftGroupOperators<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): string | undefined;
37
37
  static inlinePrimaryKeyObjects<T extends object>(where: Dictionary, meta: EntityMetadata<T>, metadata: MetadataStorage, key?: string): boolean;
38
+ /** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
39
+ private static cloneWhere;
38
40
  static processWhere<T extends object>(options: ProcessWhereOptions<T>): FilterQuery<T>;
39
41
  static getActiveFilters<T>(meta: EntityMetadata<T>, options: FilterOptions | undefined, filters: Dictionary<FilterDef>): FilterDef[];
40
42
  /** @internal Sentinel wrapping for arguments accessed while statically resolving an `rls` filter condition. */
@@ -56,15 +56,16 @@ export class QueryHelper {
56
56
  return params.map(item => QueryHelper.processParams(item));
57
57
  }
58
58
  if (Utils.isPlainObject(params)) {
59
- QueryHelper.processObjectParams(params);
59
+ return QueryHelper.processObjectParams(params);
60
60
  }
61
61
  return params;
62
62
  }
63
63
  static processObjectParams(params = {}) {
64
+ const ret = (Array.isArray(params) ? [...params] : { ...params });
64
65
  Utils.getObjectQueryKeys(params).forEach(k => {
65
- params[k] = QueryHelper.processParams(params[k]);
66
+ ret[k] = QueryHelper.processParams(params[k]);
66
67
  });
67
- return params;
68
+ return ret;
68
69
  }
69
70
  /**
70
71
  * converts `{ account: { $or: [ [Object], [Object] ] } }`
@@ -167,10 +168,26 @@ export class QueryHelper {
167
168
  });
168
169
  return false;
169
170
  }
171
+ /** Copies the plain object and array structure of the condition, keeping the leaf values (e.g. entities) by reference. */
172
+ static cloneWhere(where) {
173
+ if (Array.isArray(where)) {
174
+ return where.map(item => QueryHelper.cloneWhere(item));
175
+ }
176
+ if (!Utils.isPlainObject(where)) {
177
+ return where;
178
+ }
179
+ const ret = {};
180
+ Utils.getObjectQueryKeys(where).forEach(k => (ret[k] = QueryHelper.cloneWhere(where[k])));
181
+ return ret;
182
+ }
170
183
  static processWhere(options) {
171
184
  // eslint-disable-next-line prefer-const
172
185
  let { where, entityName, metadata, platform, aliased = true, convertCustomTypes = true, root = true } = options;
173
186
  const meta = metadata.find(entityName);
187
+ // the condition is normalized in place, so work on a copy to keep the one the caller passed in intact
188
+ if (root) {
189
+ where = QueryHelper.cloneWhere(where);
190
+ }
174
191
  // inline PK-only objects in M:N queries, so we don't join the target entity when not needed
175
192
  if (meta && root) {
176
193
  QueryHelper.liftGroupOperators(where, meta, metadata);
package/utils/Utils.js CHANGED
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
153
153
  /** Collection of general-purpose utility methods used throughout the ORM. */
154
154
  export class Utils {
155
155
  static PK_SEPARATOR = '~~~';
156
- static #ORM_VERSION = '7.2.2-dev.1';
156
+ static #ORM_VERSION = '7.2.2-dev.10';
157
157
  /** Default session variable name backing an RLS filter argument (`current_setting('mikro.<filter>.<arg>')`). */
158
158
  static getRlsSettingName(filterName, argName) {
159
159
  return `mikro.${filterName}.${argName}`;
@@ -1,6 +1,7 @@
1
1
  import type { EntityData, EntityKey, EntityMetadata, FilterQuery } from '../typings.js';
2
2
  import type { UpsertOptions } from '../drivers/IDatabaseDriver.js';
3
3
  import { type Raw } from '../utils/RawQueryFragment.js';
4
+ import type { Platform } from '../platforms/Platform.js';
4
5
  /** @internal */
5
6
  export declare function getOnConflictFields<T>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T>): (keyof T)[];
6
7
  /**
@@ -12,7 +13,7 @@ export declare function getOnConflictFields<T>(meta: EntityMetadata<T> | undefin
12
13
  */
13
14
  export declare function getOnCreateGeneratedFields<T extends object>(meta: EntityMetadata<T>, data: T | EntityData<T>): EntityKey<T>[];
14
15
  /** @internal */
15
- export declare function getOnConflictReturningFields<T, P extends string>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T, P>): (keyof T)[] | '*';
16
+ export declare function getOnConflictReturningFields<T, P extends string>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T, P>, platform?: Platform): (keyof T)[] | '*';
16
17
  /** @internal */
17
18
  export declare function getWhereCondition<T extends object>(meta: EntityMetadata<T>, onConflictFields: (keyof T)[] | Raw | undefined, data: EntityData<T>, where: FilterQuery<T>): {
18
19
  where: FilterQuery<T>;
@@ -89,11 +89,15 @@ export function getOnCreateGeneratedFields(meta, data) {
89
89
  .map(prop => prop.name);
90
90
  }
91
91
  /** @internal */
92
- export function getOnConflictReturningFields(meta, data, uniqueFields, options) {
92
+ export function getOnConflictReturningFields(meta, data, uniqueFields, options, platform) {
93
93
  /* v8 ignore next */
94
94
  if (!meta) {
95
95
  return '*';
96
96
  }
97
+ // props with explicit `returning` hint are reloaded even when supplied, as the database might normalize them
98
+ const returning = platform?.usesReturningStatement() || platform?.usesOutputStatement()
99
+ ? meta.props.filter(p => p.returning).map(p => p.name)
100
+ : [];
97
101
  const keys = meta.comparableProps
98
102
  .filter(p => {
99
103
  if (p.lazy || p.embeddable) {
@@ -112,17 +116,17 @@ export function getOnConflictReturningFields(meta, data, uniqueFields, options)
112
116
  keys.push(meta.versionProperty);
113
117
  }
114
118
  if (options.onConflictAction === 'ignore') {
115
- return keys;
119
+ return Utils.unique([...keys, ...returning]);
116
120
  }
117
121
  if (options.onConflictMergeFields) {
118
122
  const onConflictMergeFields = expandFields(meta, options.onConflictMergeFields);
119
- return keys.filter(key => !onConflictMergeFields.includes(key));
123
+ return Utils.unique([...keys.filter(key => !onConflictMergeFields.includes(key)), ...returning]);
120
124
  }
121
125
  if (options.onConflictExcludeFields) {
122
126
  const onConflictExcludeFields = expandFields(meta, options.onConflictExcludeFields);
123
- return [...new Set(keys.concat(...onConflictExcludeFields))];
127
+ return Utils.unique([...keys, ...onConflictExcludeFields, ...returning]);
124
128
  }
125
- return keys.filter(key => !(key in data));
129
+ return Utils.unique([...keys.filter(key => !(key in data)), ...returning]);
126
130
  }
127
131
  function getPropertyValue(obj, key) {
128
132
  if (!key.includes('.')) {