@mikro-orm/core 7.1.15-dev.9 → 7.1.16-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.
@@ -19,6 +19,8 @@ export declare class ObjectHydrator extends Hydrator {
19
19
  getEntityHydrator<T extends object>(meta: EntityMetadata<T>, type: 'full' | 'reference', normalizeAccessors?: boolean): EntityHydrator<T>;
20
20
  private createCollectionItemMapper;
21
21
  private wrap;
22
+ /** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
23
+ private quote;
22
24
  private safeKey;
23
25
  }
24
26
  export {};
@@ -87,7 +87,7 @@ export class ObjectHydrator extends Hydrator {
87
87
  ret.push(` if (data${dataKey} === null) {`);
88
88
  if (prop.ref) {
89
89
  ret.push(` entity${entityKey} = new ScalarReference();`);
90
- ret.push(` entity${entityKey}.bind(entity, '${prop.name}');`);
90
+ ret.push(` entity${entityKey}.bind(entity, ${this.quote(prop.name)});`);
91
91
  ret.push(` entity${entityKey}.set(${nullVal});`);
92
92
  }
93
93
  else {
@@ -127,14 +127,14 @@ export class ObjectHydrator extends Hydrator {
127
127
  if (prop.ref) {
128
128
  ret.push(` const value = isScalarReference(entity${entityKey}) ? entity${entityKey}.unwrap() : entity${entityKey};`);
129
129
  ret.push(` entity${entityKey} = oldValue_${idx} ?? new ScalarReference(value);`);
130
- ret.push(` entity${entityKey}.bind(entity, '${prop.name}');`);
130
+ ret.push(` entity${entityKey}.bind(entity, ${this.quote(prop.name)});`);
131
131
  ret.push(` entity${entityKey}.set(value);`);
132
132
  }
133
133
  ret.push(` }`);
134
134
  if (prop.ref) {
135
135
  ret.push(` if (!entity${entityKey}) {`);
136
136
  ret.push(` entity${entityKey} = new ScalarReference();`);
137
- ret.push(` entity${entityKey}.bind(entity, '${prop.name}');`);
137
+ ret.push(` entity${entityKey}.bind(entity, ${this.quote(prop.name)});`);
138
138
  ret.push(` }`);
139
139
  }
140
140
  return ret;
@@ -220,7 +220,7 @@ export class ObjectHydrator extends Hydrator {
220
220
  ret.push(` }`);
221
221
  ret.push(` if (Array.isArray(data${dataKey})) {`);
222
222
  ret.push(` const items = data${dataKey}.map(value => createCollectionItem_${this.safeKey(prop.name)}(value, entity));`);
223
- ret.push(` const coll = Collection.create(entity, '${prop.name}', items, newEntity);`);
223
+ ret.push(` const coll = Collection.create(entity, ${this.quote(prop.name)}, items, newEntity);`);
224
224
  ret.push(` if (newEntity) {`);
225
225
  ret.push(` coll.setDirty();`);
226
226
  ret.push(` } else {`);
@@ -231,11 +231,11 @@ export class ObjectHydrator extends Hydrator {
231
231
  if (!this.platform.usesPivotTable() && prop.owner && prop.kind === ReferenceKind.MANY_TO_MANY) {
232
232
  ret.push(` } else if (!entity${entityKey} && Array.isArray(data${dataKey})) {`);
233
233
  const items = this.platform.usesPivotTable() || !prop.owner ? 'undefined' : '[]';
234
- ret.push(` const coll = Collection.create(entity, '${prop.name}', ${items}, !!data${dataKey} || newEntity);`);
234
+ ret.push(` const coll = Collection.create(entity, ${this.quote(prop.name)}, ${items}, !!data${dataKey} || newEntity);`);
235
235
  ret.push(` coll.setDirty(false);`);
236
236
  }
237
237
  ret.push(` } else if (!entity${entityKey}) {`);
238
- ret.push(` const coll = Collection.create(entity, '${prop.name}', undefined, newEntity);`);
238
+ ret.push(` const coll = Collection.create(entity, ${this.quote(prop.name)}, undefined, newEntity);`);
239
239
  ret.push(` coll.setDirty(false);`);
240
240
  ret.push(` }`);
241
241
  return ret;
@@ -431,10 +431,14 @@ export class ObjectHydrator extends Hydrator {
431
431
  return lines;
432
432
  }
433
433
  wrap(key) {
434
- if (/^\[.*]$/.exec(key)) {
434
+ if (/^\[idx_\d+]$/.exec(key)) {
435
435
  return key;
436
436
  }
437
- return /^\w+$/.exec(key) ? `.${key}` : `['${key}']`;
437
+ return /^\w+$/.exec(key) ? `.${key}` : `[${this.quote(key)}]`;
438
+ }
439
+ /** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
440
+ quote(key) {
441
+ return `'${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
438
442
  }
439
443
  safeKey(key) {
440
444
  return key.replace(/\W/g, '_');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.15-dev.9",
3
+ "version": "7.1.16-dev.0",
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",
package/typings.d.ts CHANGED
@@ -1050,6 +1050,12 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
1050
1050
  constructor(meta?: Partial<EntityMetadata>);
1051
1051
  addProperty(prop: Partial<EntityProperty<Entity>>): void;
1052
1052
  removeProperty(name: string, sync?: boolean): void;
1053
+ /** For TPT entities, the version column exists only on the table of the entity that declares it. */
1054
+ ownsVersionProperty(): boolean;
1055
+ /** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
1056
+ getOwnConcurrencyCheckKeys(): EntityKey<Entity>[];
1057
+ /** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
1058
+ hasOptimisticLock(): boolean;
1053
1059
  getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
1054
1060
  getPrimaryProp(): EntityProperty<Entity>;
1055
1061
  /**
package/typings.js CHANGED
@@ -79,6 +79,25 @@ export class EntityMetadata {
79
79
  this.sync();
80
80
  }
81
81
  }
82
+ /** For TPT entities, the version column exists only on the table of the entity that declares it. */
83
+ ownsVersionProperty() {
84
+ if (!this.versionProperty) {
85
+ return false;
86
+ }
87
+ return this.inheritanceType !== 'tpt' || !this.ownProps || this.ownProps.some(p => p.name === this.versionProperty);
88
+ }
89
+ /** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
90
+ getOwnConcurrencyCheckKeys() {
91
+ const keys = [...this.concurrencyCheckKeys];
92
+ if (this.inheritanceType !== 'tpt' || !this.ownProps) {
93
+ return keys;
94
+ }
95
+ return keys.filter(key => this.ownProps.some(p => p.name === key));
96
+ }
97
+ /** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
98
+ hasOptimisticLock() {
99
+ return this.ownsVersionProperty() || this.getOwnConcurrencyCheckKeys().length > 0;
100
+ }
82
101
  getPrimaryProps(flatten = false) {
83
102
  const pks = this.primaryKeys.map(pk => this.properties[pk]);
84
103
  if (flatten) {
@@ -204,13 +204,14 @@ export class ChangeSetPersister {
204
204
  }
205
205
  checkConcurrencyKeys(meta, changeSet, cond) {
206
206
  const tmp = [];
207
- for (const key of meta.concurrencyCheckKeys) {
207
+ const keys = meta.getOwnConcurrencyCheckKeys();
208
+ for (const key of keys) {
208
209
  cond[key] = changeSet.originalEntity[key];
209
210
  if (changeSet.payload[key]) {
210
211
  tmp.push(key);
211
212
  }
212
213
  }
213
- if (tmp.length === 0 && meta.concurrencyCheckKeys.size > 0) {
214
+ if (tmp.length === 0 && keys.length > 0) {
214
215
  throw OptimisticLockError.lockFailed(changeSet.entity);
215
216
  }
216
217
  }
@@ -303,32 +304,33 @@ export class ChangeSetPersister {
303
304
  options = this.prepareOptions(meta, options, {
304
305
  convertCustomTypes: false,
305
306
  });
306
- if (meta.concurrencyCheckKeys.size === 0 &&
307
- (!meta.versionProperty || changeSet.entity[meta.versionProperty] == null)) {
307
+ if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
308
+ (!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
308
309
  return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
309
310
  }
310
- if (meta.versionProperty) {
311
+ if (meta.ownsVersionProperty()) {
311
312
  cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
312
313
  }
313
314
  this.checkConcurrencyKeys(meta, changeSet, cond);
314
315
  return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
315
316
  }
316
317
  async checkOptimisticLocks(meta, changeSets, options) {
317
- if (meta.concurrencyCheckKeys.size === 0 &&
318
- (!meta.versionProperty || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
318
+ const concurrencyCheckKeys = meta.getOwnConcurrencyCheckKeys();
319
+ if (concurrencyCheckKeys.length === 0 &&
320
+ (!meta.ownsVersionProperty() || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
319
321
  return;
320
322
  }
321
323
  // skip entity references as they don't have version values loaded
322
324
  changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
325
+ const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
323
326
  const $or = changeSets.map(cs => {
324
- const cond = Utils.getPrimaryKeyCond(cs.originalEntity, meta.primaryKeys.concat(...meta.concurrencyCheckKeys));
325
- if (meta.versionProperty) {
327
+ const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
328
+ if (meta.ownsVersionProperty()) {
326
329
  // @ts-ignore
327
330
  cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
328
331
  }
329
332
  return cond;
330
333
  });
331
- const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
332
334
  options = this.prepareOptions(meta, options, {
333
335
  fields: primaryKeys,
334
336
  orderBy: meta.primaryKeys.reduce((o, pk) => {
@@ -336,7 +338,9 @@ export class ChangeSetPersister {
336
338
  return o;
337
339
  }, {}),
338
340
  });
339
- const res = await this.#driver.find(meta.root.class, { $or }, options);
341
+ // TPT tables query their own metadata, as the version column might not live on the root table
342
+ const target = meta.inheritanceType === 'tpt' ? meta : meta.root;
343
+ const res = await this.#driver.find(target.class, { $or }, options);
340
344
  if (res.length !== changeSets.length) {
341
345
  // a FK pointing to a composite PK is an array, so the values need to be compared deeply
342
346
  const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
@@ -347,7 +351,7 @@ export class ChangeSetPersister {
347
351
  }
348
352
  }
349
353
  checkOptimisticLock(meta, changeSet, res) {
350
- if ((meta.versionProperty || meta.concurrencyCheckKeys.size > 0) && res && !res.affectedRows) {
354
+ if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
351
355
  throw OptimisticLockError.lockFailed(changeSet.entity);
352
356
  }
353
357
  }
@@ -356,7 +360,7 @@ export class ChangeSetPersister {
356
360
  * so we use a single query in case of both versioning and default values is used.
357
361
  */
358
362
  async reloadVersionValues(meta, changeSets, options) {
359
- const reloadProps = meta.versionProperty && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
363
+ const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
360
364
  if (changeSets[0].type === ChangeSetType.CREATE) {
361
365
  for (const prop of meta.props) {
362
366
  if (prop.persist === false) {
@@ -331,6 +331,7 @@ export class UnitOfWork {
331
331
  let parentCs = changeSet.tptChangeSets.find(pc => pc.meta === current);
332
332
  if (!parentCs) {
333
333
  parentCs = new ChangeSet(entity, changeSet.type, {}, current);
334
+ parentCs.originalEntity = changeSet.originalEntity;
334
335
  changeSet.tptChangeSets.splice(idx, 0, parentCs);
335
336
  }
336
337
  idx++;
@@ -705,13 +706,14 @@ export class UnitOfWork {
705
706
  payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
706
707
  }
707
708
  }
708
- if (!isCreate && Object.keys(payload).length === 0) {
709
+ // the table declaring the version property or a concurrency check still needs its bump and lock check
710
+ if (!isCreate && Object.keys(payload).length === 0 && !current.hasOptimisticLock()) {
709
711
  current = current.tptParent;
710
712
  continue;
711
713
  }
712
714
  const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
715
+ cs.originalEntity = originalChangeSet.originalEntity;
713
716
  if (current === meta) {
714
- cs.originalEntity = originalChangeSet.originalEntity;
715
717
  leafCs = cs;
716
718
  }
717
719
  else {
@@ -1208,7 +1210,8 @@ export class UnitOfWork {
1208
1210
  const addToGroup = (cs) => {
1209
1211
  // Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
1210
1212
  if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) &&
1211
- !Utils.hasObjectKeys(cs.payload)) {
1213
+ !Utils.hasObjectKeys(cs.payload) &&
1214
+ !cs.meta.hasOptimisticLock()) {
1212
1215
  return;
1213
1216
  }
1214
1217
  const group = groups[cs.type];
@@ -80,6 +80,8 @@ export declare class EntityComparator {
80
80
  private getGenericComparator;
81
81
  private getPropertyComparator;
82
82
  private wrap;
83
+ /** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
84
+ private quote;
83
85
  private safeKey;
84
86
  /**
85
87
  * Sets the toArray helper in the context if not already set.
@@ -758,10 +758,14 @@ export class EntityComparator {
758
758
  return this.getGenericComparator(this.wrap(prop.name), `!equals(last${this.wrap(prop.name)}, current${this.wrap(prop.name)})`);
759
759
  }
760
760
  wrap(key) {
761
- if (/^\[.*]$/.exec(key)) {
761
+ if (/^\[idx_\d+]$/.exec(key)) {
762
762
  return key;
763
763
  }
764
- return /^\w+$/.exec(key) ? `.${key}` : `['${key}']`;
764
+ return /^\w+$/.exec(key) ? `.${key}` : `[${this.quote(key)}]`;
765
+ }
766
+ /** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
767
+ quote(key) {
768
+ return `'${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
765
769
  }
766
770
  safeKey(key) {
767
771
  return key.replace(/\W/g, '_');
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.1.15-dev.9';
156
+ static #ORM_VERSION = '7.1.16-dev.0';
157
157
  /**
158
158
  * Checks if the argument is instance of `Object`. Returns false for arrays.
159
159
  */
@@ -138,7 +138,14 @@ function getPropertyValue(obj, key) {
138
138
  }
139
139
  /** @internal */
140
140
  export function getWhereCondition(meta, onConflictFields, data, where) {
141
- const unique = onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
141
+ // TPT children do not inherit the unique flags and indexes of their parent tables
142
+ const uniqueProps = new Set();
143
+ const uniques = [];
144
+ for (let current = meta; current; current = current.tptParent) {
145
+ current.props.filter(p => p.unique).forEach(p => uniqueProps.add(p.name));
146
+ uniques.push(...current.uniques);
147
+ }
148
+ const unique = onConflictFields ?? [...uniqueProps];
142
149
  const propIndex = !isRaw(unique) &&
143
150
  unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
144
151
  if (onConflictFields || where == null) {
@@ -152,8 +159,8 @@ export function getWhereCondition(meta, onConflictFields, data, where) {
152
159
  }
153
160
  where = { [key]: getPropertyValue(data, unique[propIndex]) };
154
161
  }
155
- else if (meta.uniques.length > 0) {
156
- for (const u of meta.uniques) {
162
+ else {
163
+ for (const u of uniques) {
157
164
  if (Utils.asArray(u.properties).every(p => data[p] != null)) {
158
165
  where = Utils.asArray(u.properties).reduce((o, key) => {
159
166
  o[key] = data[key];