@mikro-orm/core 7.2.2-dev.2 → 7.2.2-dev.20

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.
@@ -580,6 +580,11 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
580
580
  * Checks whether this EntityManager is currently operating inside a database transaction.
581
581
  */
582
582
  isInTransaction(): boolean;
583
+ /**
584
+ * Checks whether transactions are disabled for this EntityManager.
585
+ * @internal
586
+ */
587
+ isTransactionDisabled(): boolean;
583
588
  /**
584
589
  * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
585
590
  */
@@ -633,7 +638,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
633
638
  /**
634
639
  * @internal
635
640
  */
636
- tryCache<T extends object, R>(entityName: EntityName<T>, config: boolean | number | [string, number] | undefined, key: unknown, refresh?: boolean, merge?: boolean): Promise<{
641
+ tryCache<T extends object, R>(entityName: EntityName<T>, config: boolean | number | [string, number] | undefined, key: unknown, refresh?: boolean, merge?: boolean, schema?: string): Promise<{
637
642
  data?: R | null;
638
643
  key: string;
639
644
  } | undefined>;
package/EntityManager.js CHANGED
@@ -133,7 +133,7 @@ export class EntityManager {
133
133
  options.populate = (await em.preparePopulate(entityName, options));
134
134
  const populate = options.populate;
135
135
  const cacheKey = em.cacheKey(entityName, options, 'em.find', where);
136
- const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
136
+ const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true, options.schema);
137
137
  if (cached?.data) {
138
138
  await em.#entityLoader.populate(entityName, cached.data, populate, {
139
139
  ...options,
@@ -834,7 +834,7 @@ export class EntityManager {
834
834
  validateParams(where);
835
835
  options.populate = (await em.preparePopulate(entityName, options));
836
836
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
837
- const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
837
+ const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true, options.schema);
838
838
  if (cached?.data !== undefined) {
839
839
  if (cached.data) {
840
840
  await em.#entityLoader.populate(entityName, [cached.data], options.populate, {
@@ -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,41 @@ 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
+ const seen = new Set();
1114
+ let j = 0;
1115
+ // `res` holds every entity once, so skip the rows resolving to an entity an earlier row already took
1116
+ for (const i of idx) {
1117
+ const row = data[i];
1118
+ const entity = Utils.isEntity(row)
1119
+ ? row
1120
+ : em.#unitOfWork.tryGetById(entityName, row, options.schema, false);
1121
+ if (!seen.has(entity)) {
1122
+ seen.add((ret[i] = res[j++]));
1123
+ }
1124
+ }
1125
+ }
1126
+ // rows resolving to the same entity are returned once, as in the single statement path
1127
+ return [...new Set(ret.filter(entity => entity))];
1128
+ }
1129
+ }
1091
1130
  // the rows are replaced with their processed copies below, keep the caller's array intact
1092
1131
  data = [...data];
1093
1132
  const meta = this.metadata.get(entityName);
@@ -1095,9 +1134,10 @@ export class EntityManager {
1095
1134
  const generatedFields = new Set();
1096
1135
  const allData = [];
1097
1136
  const allWhere = [];
1137
+ const dataIndexes = [];
1138
+ const result = [];
1098
1139
  const entities = new Map();
1099
1140
  const entitiesByData = new Map();
1100
- const entitiesByAllDataIdx = new Map();
1101
1141
  for (let i = 0; i < data.length; i++) {
1102
1142
  let row = data[i];
1103
1143
  let where;
@@ -1105,14 +1145,12 @@ export class EntityManager {
1105
1145
  const entity = row;
1106
1146
  if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
1107
1147
  em.#entityFactory.mergeData(meta, entity, row, { initialized: true });
1108
- entities.set(entity, row);
1109
- entitiesByData.set(row, entity);
1148
+ result[i] = entity;
1110
1149
  continue;
1111
1150
  }
1112
1151
  where = helper(entity).getPrimaryKey();
1113
1152
  getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
1114
1153
  em.#entityFactory.assignDefaultValues(entity, meta);
1115
- entitiesByAllDataIdx.set(allData.length, entity);
1116
1154
  row = em.#comparator.prepareEntity(entity);
1117
1155
  }
1118
1156
  else {
@@ -1122,8 +1160,7 @@ export class EntityManager {
1122
1160
  const exists = em.#unitOfWork.getById(entityName, where, options.schema);
1123
1161
  if (exists) {
1124
1162
  em.assign(exists, row);
1125
- entities.set(exists, row);
1126
- entitiesByData.set(row, exists);
1163
+ result[i] = exists;
1127
1164
  continue;
1128
1165
  }
1129
1166
  }
@@ -1156,22 +1193,25 @@ export class EntityManager {
1156
1193
  validateParams(row, 'insert data');
1157
1194
  allData.push(row);
1158
1195
  allWhere.push(where);
1196
+ dataIndexes.push(i);
1159
1197
  }
1160
- if (entities.size === data.length) {
1161
- return [...entities.keys()];
1198
+ if (allData.length === 0) {
1199
+ return [...new Set(result)];
1162
1200
  }
1163
1201
  // `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
1164
1202
  if (generatedFields.size > 0 && !options.onConflictMergeFields) {
1165
1203
  options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
1166
1204
  }
1167
1205
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
1168
- for (const dto of data) {
1169
- const entity = entitiesByData.get(dto) ?? dto;
1206
+ for (const index of dataIndexes) {
1207
+ const entity = data[index];
1170
1208
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
1171
1209
  }
1172
- for (const [idx, entity] of entitiesByAllDataIdx) {
1173
- allData[idx] = em.#comparator.prepareEntity(entity);
1174
- }
1210
+ dataIndexes.forEach((index, idx) => {
1211
+ if (Utils.isEntity(data[index])) {
1212
+ allData[idx] = em.#comparator.prepareEntity(data[index]);
1213
+ }
1214
+ });
1175
1215
  }
1176
1216
  const res = await em.withSessionContext(options.ctx ?? em.#transactionContext, ctx => em.driver.nativeUpdateMany(entityName, allWhere, allData, {
1177
1217
  upsert: true,
@@ -1179,15 +1219,16 @@ export class EntityManager {
1179
1219
  ...options,
1180
1220
  ctx,
1181
1221
  }));
1182
- entities.clear();
1183
- entitiesByData.clear();
1184
1222
  const loadPK = new Map();
1185
1223
  allData.forEach((row, i) => {
1186
- em.#unitOfWork
1187
- .getChangeSetPersister()
1188
- .mapReturnedValues(Utils.isEntity(data[i]) ? data[i] : null, Utils.isEntity(data[i]) ? {} : data[i], res.rows?.[i], meta, true);
1189
- const entity = Utils.isEntity(data[i])
1190
- ? data[i]
1224
+ // Managed inputs skipped by the SQL batch still occupy positions in `data`.
1225
+ const index = dataIndexes[i];
1226
+ const item = data[index];
1227
+ em.#unitOfWork.getChangeSetPersister().mapReturnedValues(Utils.isEntity(item) ? item : null, Utils.isEntity(item) ? {} : item,
1228
+ // A suppressed conflict omits its row; positional mapping would hydrate a different entity.
1229
+ res.rows?.length === allData.length ? res.rows[i] : undefined, meta, true);
1230
+ const entity = Utils.isEntity(item)
1231
+ ? item
1191
1232
  : em.#entityFactory.create(entityName, row, {
1192
1233
  refresh: true,
1193
1234
  initialized: true,
@@ -1198,13 +1239,19 @@ export class EntityManager {
1198
1239
  }
1199
1240
  entities.set(entity, row);
1200
1241
  entitiesByData.set(row, entity);
1242
+ result[index] = entity;
1201
1243
  });
1202
1244
  // skip the reload only when RETURNING brought back a row for every input
1203
1245
  // (onConflictWhere can suppress writes, leaving some rows out)
1204
1246
  // oxfmt-ignore
1205
1247
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere[0]) ? Object.keys(allWhere[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
1206
- const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
1207
- const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length === data.length);
1248
+ const platform = this.getPlatform();
1249
+ const returning = getOnConflictReturningFields(meta, data[dataIndexes[0]], uniqueFields, options, platform);
1250
+ if (options.onConflictWhere && res.rows?.length !== allData.length) {
1251
+ returning.push(...meta.comparableProps.filter(p => !p.lazy && !p.embeddable).map(p => p.name));
1252
+ }
1253
+ const reloadFields = returning.length > 0 &&
1254
+ !((platform.usesReturningStatement() || platform.usesOutputStatement()) && res.rows?.length === allData.length);
1208
1255
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
1209
1256
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
1210
1257
  const add = new Set(propIndex !== false && propIndex >= 0
@@ -1214,7 +1261,8 @@ export class EntityManager {
1214
1261
  Utils.keys(cond).forEach(key => add.add(key));
1215
1262
  }
1216
1263
  const where = { $or: [] };
1217
- data.forEach((item, idx) => {
1264
+ dataIndexes.forEach((index, idx) => {
1265
+ const item = data[index];
1218
1266
  where.$or[idx] = {};
1219
1267
  const props = Array.isArray(uniqueFields) ? uniqueFields : Object.keys(item);
1220
1268
  props.forEach(prop => {
@@ -1235,8 +1283,9 @@ export class EntityManager {
1235
1283
  const row = data2.find(row => {
1236
1284
  const tmp = {};
1237
1285
  add.forEach(k => {
1238
- if (!meta.properties[k]?.primary) {
1239
- tmp[k] = row[k];
1286
+ const prop = meta.properties[k];
1287
+ if (!prop?.primary) {
1288
+ tmp[k] = prop?.customType ? prop.customType.convertToDatabaseValue(row[k], this.getPlatform()) : row[k];
1240
1289
  }
1241
1290
  });
1242
1291
  return this.#comparator.matching(entityName, cond, tmp);
@@ -1283,7 +1332,7 @@ export class EntityManager {
1283
1332
  await em.eventManager.dispatchEvent(EventType.afterUpsert, { entity, em, meta }, meta);
1284
1333
  }
1285
1334
  }
1286
- return [...entities.keys()];
1335
+ return [...new Set(result)];
1287
1336
  }
1288
1337
  /**
1289
1338
  * Runs your callback wrapped inside a database transaction.
@@ -1940,6 +1989,13 @@ export class EntityManager {
1940
1989
  isInTransaction() {
1941
1990
  return !!this.getContext(false).#transactionContext;
1942
1991
  }
1992
+ /**
1993
+ * Checks whether transactions are disabled for this EntityManager.
1994
+ * @internal
1995
+ */
1996
+ isTransactionDisabled() {
1997
+ return !!this.getContext(false).#disableTransactions;
1998
+ }
1943
1999
  /**
1944
2000
  * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
1945
2001
  */
@@ -2295,7 +2351,7 @@ export class EntityManager {
2295
2351
  /**
2296
2352
  * @internal
2297
2353
  */
2298
- async tryCache(entityName, config, key, refresh, merge) {
2354
+ async tryCache(entityName, config, key, refresh, merge, schema) {
2299
2355
  config ??= this.config.get('resultCache').global;
2300
2356
  if (!config) {
2301
2357
  return undefined;
@@ -2317,6 +2373,7 @@ export class EntityManager {
2317
2373
  merge: true,
2318
2374
  convertCustomTypes: false,
2319
2375
  refresh,
2376
+ schema,
2320
2377
  recomputeSnapshot: true,
2321
2378
  };
2322
2379
  if (Array.isArray(cached) && merge) {
@@ -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.2",
3
+ "version": "7.2.2-dev.20",
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",
@@ -467,7 +467,10 @@ export class UnitOfWork {
467
467
  }
468
468
  const groups = this.getChangeSetGroups();
469
469
  const platform = this.#em.getPlatform();
470
- const runInTransaction = !this.#em.isInTransaction() && platform.supportsTransactions() && this.#em.config.get('implicitTransactions');
470
+ const runInTransaction = !this.#em.isInTransaction() &&
471
+ !this.#em.isTransactionDisabled() &&
472
+ platform.supportsTransactions() &&
473
+ this.#em.config.get('implicitTransactions');
471
474
  if (runInTransaction) {
472
475
  const loggerContext = Utils.merge({ id: this.#em._id }, this.#em.getLoggerContext({ disableContextResolution: true }));
473
476
  await this.#em.getConnection('write').transactional(trx => this.persistToDatabase(groups, trx), {
@@ -111,9 +111,17 @@ export class QueryHelper {
111
111
  const op = this.liftGroupOperators(value, prop.targetMeta, metadata, k);
112
112
  if (op) {
113
113
  delete where[k];
114
- where[op] = value[op].map((v) => {
114
+ const group = value[op].map((v) => {
115
115
  return { [k]: v };
116
116
  });
117
+ if (op in where) {
118
+ // Sibling groups are conjunctive, even when both use `$or`.
119
+ where.$and ??= [];
120
+ where.$and.push({ [op]: group });
121
+ }
122
+ else {
123
+ where[op] = group;
124
+ }
117
125
  }
118
126
  }
119
127
  return undefined;
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.2';
156
+ static #ORM_VERSION = '7.2.2-dev.20';
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('.')) {