@mikro-orm/core 7.2.0-dev.1 → 7.2.0-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.
Files changed (43) hide show
  1. package/EntityManager.d.ts +1 -1
  2. package/EntityManager.js +31 -25
  3. package/MikroORM.d.ts +4 -0
  4. package/MikroORM.js +6 -0
  5. package/cache/CacheAdapter.d.ts +6 -4
  6. package/cache/FileCacheAdapter.d.ts +1 -1
  7. package/cache/FileCacheAdapter.js +7 -2
  8. package/connections/Connection.d.ts +7 -0
  9. package/connections/Connection.js +9 -0
  10. package/drivers/DatabaseDriver.d.ts +7 -0
  11. package/drivers/DatabaseDriver.js +72 -9
  12. package/hydration/ObjectHydrator.js +3 -0
  13. package/metadata/EntitySchema.js +5 -2
  14. package/metadata/MetadataDiscovery.js +34 -11
  15. package/metadata/MetadataProvider.js +1 -1
  16. package/metadata/MetadataStorage.d.ts +4 -3
  17. package/metadata/MetadataStorage.js +15 -1
  18. package/metadata/Routine.js +4 -1
  19. package/naming-strategy/AbstractNamingStrategy.js +2 -1
  20. package/naming-strategy/NamingStrategy.d.ts +2 -1
  21. package/package.json +1 -1
  22. package/platforms/Platform.d.ts +2 -0
  23. package/platforms/Platform.js +4 -0
  24. package/typings.d.ts +2 -0
  25. package/unit-of-work/ChangeSet.js +10 -7
  26. package/unit-of-work/ChangeSetPersister.js +15 -7
  27. package/utils/AbstractMigrator.d.ts +1 -1
  28. package/utils/AbstractMigrator.js +3 -2
  29. package/utils/Configuration.d.ts +6 -0
  30. package/utils/Configuration.js +3 -1
  31. package/utils/Cursor.js +1 -6
  32. package/utils/EntityComparator.js +4 -1
  33. package/utils/QueryHelper.d.ts +5 -0
  34. package/utils/QueryHelper.js +25 -0
  35. package/utils/RawQueryFragment.d.ts +6 -0
  36. package/utils/RawQueryFragment.js +15 -6
  37. package/utils/RequestContext.d.ts +2 -2
  38. package/utils/RequestContext.js +11 -2
  39. package/utils/TransactionManager.d.ts +6 -0
  40. package/utils/TransactionManager.js +35 -2
  41. package/utils/Utils.js +6 -3
  42. package/utils/clone.js +6 -0
  43. package/utils/env-vars.js +1 -0
@@ -92,7 +92,7 @@ export class MetadataProvider {
92
92
  if (!this.useCache()) {
93
93
  return undefined;
94
94
  }
95
- const cache = meta.path && this.config.getMetadataCacheAdapter().get(this.getCacheKey(meta));
95
+ const cache = meta.path && this.config.getMetadataCacheAdapter().get(this.getCacheKey(meta), meta.path);
96
96
  if (cache) {
97
97
  this.loadFromCache(meta, cache);
98
98
  meta.root = root;
@@ -1,13 +1,14 @@
1
- import { type Dictionary, EntityMetadata, type EntityName } from '../typings.js';
1
+ import { type Dictionary, type EntityCtor, EntityMetadata, type EntityName } from '../typings.js';
2
2
  import type { EntityManager } from '../EntityManager.js';
3
3
  /** Registry that stores and provides access to entity metadata by class, name, or id. */
4
4
  export declare class MetadataStorage {
5
5
  #private;
6
6
  static readonly PATH_SYMBOL: unique symbol;
7
+ static readonly META_SYMBOL: unique symbol;
7
8
  constructor(metadata?: Dictionary<EntityMetadata>);
8
- /** Returns the global metadata dictionary, or a specific entry by entity name and path. */
9
+ /** Returns the global metadata dictionary, or a specific entry by entity name and path (keyed by the class reference when `target` is provided). */
9
10
  static getMetadata(): Dictionary<EntityMetadata>;
10
- static getMetadata<T = any>(entity: string, path: string): EntityMetadata<T>;
11
+ static getMetadata<T = any>(entity: string, path: string, target?: EntityCtor): EntityMetadata<T>;
11
12
  /** Checks whether an entity with the given class name exists in the global metadata. */
12
13
  static isKnownEntity(name: string): boolean;
13
14
  /** Clears all entries from the global metadata registry. */
@@ -11,6 +11,7 @@ function getGlobalStorage(namespace) {
11
11
  /** Registry that stores and provides access to entity metadata by class, name, or id. */
12
12
  export class MetadataStorage {
13
13
  static PATH_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.PATH_SYMBOL');
14
+ static META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
14
15
  static #metadata = getGlobalStorage('metadata');
15
16
  #metadataMap = new Map();
16
17
  #idMap;
@@ -26,8 +27,21 @@ export class MetadataStorage {
26
27
  this.#metadataMap.set(meta.class, meta);
27
28
  }
28
29
  }
29
- static getMetadata(entity, path) {
30
+ static getMetadata(entity, path, target) {
30
31
  const key = entity && path ? entity + '-' + Utils.hash(path) : null;
32
+ // Key the registry by the class reference when available, so two classes minified
33
+ // to the same mangled name don't collide on the `className-path` key.
34
+ if (key && target) {
35
+ if (!Object.hasOwn(target, MetadataStorage.META_SYMBOL)) {
36
+ Object.defineProperty(target, MetadataStorage.META_SYMBOL, {
37
+ value: new EntityMetadata({ className: entity, path }),
38
+ writable: true,
39
+ });
40
+ }
41
+ // Keep the name-keyed entry in sync, the class-keyed metadata survives `MetadataStorage.clear()`.
42
+ MetadataStorage.#metadata[key] = target[MetadataStorage.META_SYMBOL];
43
+ return target[MetadataStorage.META_SYMBOL];
44
+ }
31
45
  if (key && !MetadataStorage.#metadata[key]) {
32
46
  MetadataStorage.#metadata[key] = new EntityMetadata({ className: entity, path });
33
47
  }
@@ -135,6 +135,9 @@ export class Routine {
135
135
  return new Routine(config);
136
136
  }
137
137
  static is(item) {
138
- return item instanceof Routine;
138
+ if (item instanceof Routine) {
139
+ return true;
140
+ }
141
+ return item != null && typeof item === 'object' && item.constructor?.name === 'Routine' && 'type' in item;
139
142
  }
140
143
  }
@@ -10,7 +10,8 @@ export class AbstractNamingStrategy {
10
10
  classToMigrationName(timestamp, customMigrationName) {
11
11
  let migrationName = `Migration${timestamp}`;
12
12
  if (customMigrationName) {
13
- migrationName += `_${customMigrationName}`;
13
+ // the name becomes part of a class identifier
14
+ migrationName += `_${customMigrationName.replace(/[^$\p{ID_Continue}]+/gu, '_')}`;
14
15
  }
15
16
  return migrationName;
16
17
  }
@@ -9,7 +9,8 @@ export interface NamingStrategy {
9
9
  */
10
10
  classToTableName(entityName: string, tableName?: string): string;
11
11
  /**
12
- * Return a migration name. This name should allow ordering.
12
+ * Return a migration name. This name should allow ordering, and has to be a valid class identifier,
13
+ * as it is used as the class name in the generated migration file.
13
14
  */
14
15
  classToMigrationName(timestamp: string, customMigrationName?: string): string;
15
16
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.2.0-dev.1",
3
+ "version": "7.2.0-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",
@@ -227,6 +227,8 @@ export declare abstract class Platform {
227
227
  formatIndexHint(indexNames: string[]): string | undefined;
228
228
  /** Whether the driver automatically parses JSON columns into JS objects. */
229
229
  convertsJsonAutomatically(): boolean;
230
+ /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
231
+ preservesDatesInsideJson(): boolean;
230
232
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
231
233
  convertJsonToDatabaseValue(value: unknown, context?: TransformContext): unknown;
232
234
  /** Converts a database JSON value to its JS representation. */
@@ -465,6 +465,10 @@ export class Platform {
465
465
  convertsJsonAutomatically() {
466
466
  return true;
467
467
  }
468
+ /** Whether date values inside JSON documents keep their native type (e.g. BSON dates), instead of being serialized to ISO strings. */
469
+ preservesDatesInsideJson() {
470
+ return false;
471
+ }
468
472
  /** Converts a JS value to its JSON database representation (typically JSON.stringify). */
469
473
  convertJsonToDatabaseValue(value, context) {
470
474
  return JSON.stringify(value);
package/typings.d.ts CHANGED
@@ -1470,6 +1470,8 @@ export interface IMigrationGenerator {
1470
1470
  }
1471
1471
  /** Interface that all migration classes must implement. */
1472
1472
  export interface Migration {
1473
+ /** Stable migration name, used instead of the class name (which minifiers can mangle). */
1474
+ name?: string;
1473
1475
  up(): Promise<void> | void;
1474
1476
  down(): Promise<void> | void;
1475
1477
  isTransactional(): boolean;
@@ -29,15 +29,18 @@ export class ChangeSet {
29
29
  else {
30
30
  this.primaryKey = this.originalEntity[this.meta.primaryKeys[0]];
31
31
  }
32
- if (!this.meta.compositePK &&
33
- this.meta.getPrimaryProp().targetMeta?.compositePK &&
34
- typeof this.primaryKey === 'object' &&
35
- this.primaryKey !== null) {
36
- this.primaryKey = this.meta.getPrimaryProp().targetMeta.primaryKeys.map(childPK => {
37
- return this.primaryKey[childPK];
38
- });
32
+ const primaryProp = this.meta.getPrimaryProp();
33
+ const relationPK = !this.meta.compositePK && !!primaryProp.targetMeta?.compositePK;
34
+ // arrays are already the ordered tuple of the target's primary keys
35
+ if (relationPK && Utils.isPlainObject(this.primaryKey)) {
36
+ const pk = this.primaryKey;
37
+ this.primaryKey = primaryProp.targetMeta.primaryKeys.map(childPK => pk[childPK]);
39
38
  }
40
39
  if (object && this.primaryKey != null) {
40
+ // the whole tuple belongs to the single relation PK, it must not be spread over the (single) PK prop
41
+ if (relationPK) {
42
+ return { [primaryProp.name]: this.primaryKey };
43
+ }
41
44
  return Utils.primaryKeyToObject(this.meta, this.primaryKey);
42
45
  }
43
46
  return this.primaryKey ?? null;
@@ -3,7 +3,7 @@ import { PolymorphicRef } from '../entity/PolymorphicRef.js';
3
3
  import { helper } from '../entity/wrap.js';
4
4
  import { ChangeSetType } from './ChangeSet.js';
5
5
  import { isRaw } from '../utils/RawQueryFragment.js';
6
- import { Utils } from '../utils/Utils.js';
6
+ import { equals, Utils } from '../utils/Utils.js';
7
7
  import { OptimisticLockError, ValidationError } from '../errors.js';
8
8
  import { ReferenceKind } from '../enums.js';
9
9
  /** @internal Executes change sets against the database, handling inserts, updates, and deletes. */
@@ -230,11 +230,15 @@ export class ChangeSetPersister {
230
230
  }
231
231
  const res = await this.#driver.nativeUpdateMany(meta.class, cond, payload, options);
232
232
  const map = new Map();
233
- res.rows?.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, true, this.#platform, true), item));
233
+ // returning rows are not mapped yet, so they are keyed by field names - we need to build the hash
234
+ // from those to be able to match them with `getSerializedPrimaryKey()` of the entity
235
+ const pkFields = meta.getPrimaryProps().flatMap(prop => prop.fieldNames);
236
+ res.rows?.forEach(item => map.set(Utils.getPrimaryKeyHash(pkFields.map(field => item[field])), item));
234
237
  for (const changeSet of changeSets) {
235
238
  if (res.rows) {
236
239
  const row = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
237
- this.mapReturnedValues(changeSet.entity, changeSet.payload, row, meta);
240
+ // STI batches can mix child types, so map through the change set's own metadata
241
+ this.mapReturnedValues(changeSet.entity, changeSet.payload, row, changeSet.meta);
238
242
  }
239
243
  changeSet.persisted = true;
240
244
  }
@@ -334,7 +338,8 @@ export class ChangeSetPersister {
334
338
  });
335
339
  const res = await this.#driver.find(meta.root.class, { $or }, options);
336
340
  if (res.length !== changeSets.length) {
337
- const compare = (a, b, keys) => keys.every(k => a[k] === b[k]);
341
+ // a FK pointing to a composite PK is an array, so the values need to be compared deeply
342
+ const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
338
343
  const entity = changeSets.find(cs => {
339
344
  return !res.some(row => compare(Utils.getPrimaryKeyCond(cs.entity, primaryKeys), row, primaryKeys));
340
345
  }).entity;
@@ -375,7 +380,8 @@ export class ChangeSetPersister {
375
380
  changeSets.forEach(cs => {
376
381
  Utils.keys(cs.payload).forEach(k => {
377
382
  if (isRaw(cs.payload[k]) && isRaw(cs.entity[k])) {
378
- returning.add(meta.properties[k]);
383
+ // STI batches can mix child types, so the property might not exist on `meta`
384
+ returning.add(cs.meta.properties[k]);
379
385
  }
380
386
  });
381
387
  });
@@ -400,12 +406,14 @@ export class ChangeSetPersister {
400
406
  options = this.prepareOptions(meta, options, {
401
407
  fields: Utils.unique(reloadProps.map(prop => prop.name)),
402
408
  });
403
- const data = await this.#driver.find(meta.class, { [pk]: { $in: pks } }, options);
409
+ // a mixed STI batch shares one table but the child discriminator would filter out the siblings
410
+ const target = changeSets.some(cs => cs.meta !== meta) && meta.root.discriminatorColumn ? meta.root : meta;
411
+ const data = await this.#driver.find(target.class, { [pk]: { $in: pks } }, options);
404
412
  const map = new Map();
405
413
  data.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta, false, this.#platform, true), item));
406
414
  for (const changeSet of changeSets) {
407
415
  const data = map.get(helper(changeSet.entity).getSerializedPrimaryKey());
408
- this.#hydrator.hydrate(changeSet.entity, meta, data, this.#factory, 'full', false, true);
416
+ this.#hydrator.hydrate(changeSet.entity, changeSet.meta, data, this.#factory, 'full', false, true);
409
417
  Object.assign(changeSet.payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
410
418
  }
411
419
  }
@@ -103,7 +103,7 @@ export declare abstract class AbstractMigrator<D extends IDatabaseDriver> implem
103
103
  name: string;
104
104
  path: string;
105
105
  }): RunnableMigration;
106
- protected initialize(MigrationClass: Constructor<Migration>, name: string): RunnableMigration;
106
+ protected initialize(MigrationClass: Constructor<Migration>, name?: string): RunnableMigration;
107
107
  /**
108
108
  * Checks if `src` folder exists, it so, tries to adjust the migrations and seeders paths automatically to use it.
109
109
  * If there is a `dist` or `build` folder, it will be used for the JS variant (`path` option), while the `src` folder will be
@@ -358,7 +358,8 @@ export class AbstractMigrator {
358
358
  initialize(MigrationClass, name) {
359
359
  const instance = new MigrationClass(this.driver, this.config);
360
360
  return {
361
- name: this.storage.getMigrationName(name),
361
+ // the constructor name is the last resort, minifiers can mangle it (e.g. when bundling)
362
+ name: this.storage.getMigrationName(name ?? instance.name ?? MigrationClass.name),
362
363
  up: afterRun => this.runner.run(instance, 'up', afterRun),
363
364
  down: afterRun => this.runner.run(instance, 'down', afterRun),
364
365
  };
@@ -409,7 +410,7 @@ export class AbstractMigrator {
409
410
  if (this.options.migrationsList) {
410
411
  return this.options.migrationsList.map(migration => {
411
412
  if (typeof migration === 'function') {
412
- return this.initialize(migration, migration.name);
413
+ return this.initialize(migration);
413
414
  }
414
415
  return this.initialize(migration.class, migration.name);
415
416
  });
@@ -233,6 +233,12 @@ export type MigrationsOptions = {
233
233
  * @default true
234
234
  */
235
235
  snapshot?: boolean;
236
+ /**
237
+ * Update the snapshot from the database schema when running migrations up or down.
238
+ * Disable to keep the snapshot managed solely by `migration:create`.
239
+ * @default true
240
+ */
241
+ snapshotOnMigrate?: boolean;
236
242
  /** Custom name for the snapshot file. */
237
243
  snapshotName?: string;
238
244
  /**
@@ -84,8 +84,10 @@ const DEFAULTS = {
84
84
  dropTables: true,
85
85
  safe: false,
86
86
  snapshot: true,
87
+ snapshotOnMigrate: true,
87
88
  emit: 'ts',
88
- fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name : ''}`,
89
+ // mirrors `NamingStrategy.classToMigrationName`, so the file name matches the class it declares
90
+ fileName: (timestamp, name) => `Migration${timestamp}${name ? '_' + name.replace(/[^$\p{ID_Continue}]+/gu, '_') : ''}`,
89
91
  },
90
92
  schemaGenerator: {
91
93
  createForeignKeyConstraints: true,
package/utils/Cursor.js CHANGED
@@ -150,12 +150,7 @@ export class Cursor {
150
150
  return Buffer.from(JSON.stringify(value)).toString('base64url');
151
151
  }
152
152
  static decode(value) {
153
- return JSON.parse(Buffer.from(value, 'base64url').toString('utf8')).map((value) => {
154
- if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}/.exec(value)) {
155
- return new Date(value);
156
- }
157
- return value;
158
- });
153
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
159
154
  }
160
155
  static getDefinition(meta, orderBy) {
161
156
  return Utils.asArray(orderBy).flatMap(order => {
@@ -164,7 +164,9 @@ export class EntityComparator {
164
164
  const lines = [];
165
165
  const context = new Map();
166
166
  context.set('isEntityOrRef', (val) => Utils.isEntity(val, true));
167
- context.set('getCompositeKeyValue', (val) => Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform)));
167
+ context.set('getCompositeKeyValue', (val) =>
168
+ // deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
169
+ Utils.flatten(Utils.getCompositeKeyValue(val, meta, 'convertToDatabaseValue', this.#platform), true));
168
170
  context.set('getPrimaryKeyHash', (val) => Utils.getPrimaryKeyHash(Utils.asArray(val)));
169
171
  if (meta.primaryKeys.length > 1) {
170
172
  lines.push(` const pks = entity.__helper.__pk ? getCompositeKeyValue(entity.__helper.__pk) : [`);
@@ -447,6 +449,7 @@ export class EntityComparator {
447
449
  const ret = [];
448
450
  const padding = ' '.repeat(level * 2);
449
451
  const idx = this.#tmpIndex++;
452
+ ret.push(`${padding}if (entity${entityKey} === null) ret${dataKey} = null;`);
450
453
  ret.push(`${padding}if (Array.isArray(entity${entityKey})) {`);
451
454
  ret.push(`${padding} ret${dataKey} = [];`);
452
455
  ret.push(`${padding} entity${entityKey}.forEach((_, idx_${idx}) => {`);
@@ -40,6 +40,11 @@ export declare class QueryHelper {
40
40
  static mergePropertyFilters(propFilters: FilterOptions | undefined, options: FilterOptions | undefined): FilterOptions | undefined;
41
41
  static isFilterActive<T>(meta: EntityMetadata<T>, filterName: string, filter: FilterDef, options: Dictionary<boolean | Dictionary>): boolean;
42
42
  static processCustomType<T extends object>(prop: EntityProperty<T>, cond: FilterQuery<T>, platform: Platform, key?: string, fromQuery?: boolean): FilterQuery<T>;
43
+ /**
44
+ * Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
45
+ * resolve, so the custom types have to be applied positionally instead.
46
+ */
47
+ private static processCompositeCustomTypes;
43
48
  private static isSupportedOperator;
44
49
  private static processJsonCondition;
45
50
  static findProperty<T>(fieldName: string, options: ProcessWhereOptions<T>): EntityProperty<T> | undefined;
@@ -227,6 +227,13 @@ export class QueryHelper {
227
227
  if (prop?.customType && convertCustomTypes && !isRaw(value)) {
228
228
  value = QueryHelper.processCustomType(prop, value, platform, undefined, true);
229
229
  }
230
+ else if (!prop &&
231
+ meta?.compositePK &&
232
+ convertCustomTypes &&
233
+ Array.isArray(value) &&
234
+ key === Utils.getPrimaryKeyHash(meta.primaryKeys)) {
235
+ value = QueryHelper.processCompositeCustomTypes(value, meta, platform);
236
+ }
230
237
  // oxfmt-ignore
231
238
  const isJsonProperty = prop?.customType instanceof JsonType && !isRaw(value) && (Utils.isPlainObject(value) ? !['$eq', '$elemMatch'].includes(Object.keys(value)[0]) : !Array.isArray(value));
232
239
  if (isJsonProperty && prop?.kind !== ReferenceKind.EMBEDDED) {
@@ -321,6 +328,24 @@ export class QueryHelper {
321
328
  }
322
329
  return prop.customType.convertToDatabaseValue(cond, platform, { fromQuery, key, mode: 'query' });
323
330
  }
331
+ /**
332
+ * Composite PK conditions are keyed by a hash of all the PK names, which `findProperty` cannot
333
+ * resolve, so the custom types have to be applied positionally instead.
334
+ */
335
+ static processCompositeCustomTypes(value, meta, platform) {
336
+ const props = meta.primaryKeys.map(pk => meta.properties[pk]);
337
+ if (!props.some(prop => prop.customType)) {
338
+ return value;
339
+ }
340
+ // the tuple can be longer than the PK when the user passes a malformed condition
341
+ const convert = (tuple) => tuple.map((val, idx) => {
342
+ if (!props[idx]?.customType) {
343
+ return val;
344
+ }
345
+ return QueryHelper.processCustomType(props[idx], val, platform, undefined, true);
346
+ });
347
+ return value.every(val => Array.isArray(val)) ? value.map(val => convert(val)) : convert(value);
348
+ }
324
349
  static isSupportedOperator(key) {
325
350
  return !!QueryHelper.SUPPORTED_OPERATORS.find(op => key === op);
326
351
  }
@@ -61,6 +61,12 @@ export declare const ALIAS_REPLACEMENT_RE = "\\[::alias::\\]";
61
61
  * await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() });
62
62
  * ```
63
63
  *
64
+ * Named parameters are supported via an object of parameters, use `:name` for values and `:name:` for identifiers:
65
+ *
66
+ * ```ts
67
+ * raw('select :col: from geo where city = :city or region = :city', { col: 'city', city: 'Brno' });
68
+ * ```
69
+ *
64
70
  * You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature:
65
71
  *
66
72
  * ```ts
@@ -146,6 +146,12 @@ export const ALIAS_REPLACEMENT_RE = '\\[::alias::\\]';
146
146
  * await em.find(User, { [raw(alias => `lower(${alias}.name)`)]: name.toLowerCase() });
147
147
  * ```
148
148
  *
149
+ * Named parameters are supported via an object of parameters, use `:name` for values and `:name:` for identifiers:
150
+ *
151
+ * ```ts
152
+ * raw('select :col: from geo where city = :city or region = :city', { col: 'city', city: 'Brno' });
153
+ * ```
154
+ *
149
155
  * You can also use the `sql` tagged template function, which works the same, but supports only the simple string signature:
150
156
  *
151
157
  * ```ts
@@ -191,13 +197,16 @@ export function raw(sql, params) {
191
197
  return Utils.getPrimaryKeyHash(sql);
192
198
  }
193
199
  if (typeof params === 'object' && !Array.isArray(params)) {
194
- const pairs = Object.entries(params);
200
+ const dict = params;
195
201
  const objectParams = [];
196
- for (const [key, value] of pairs) {
197
- sql = sql.replace(`:${key}:`, '??');
198
- sql = sql.replace(`:${key}`, '?');
199
- objectParams.push(value);
200
- }
202
+ // single left-to-right scan keeps values in SQL-placeholder order while `::` casts and unknown tokens stay untouched
203
+ sql = sql.replace(/(?<!:):([$\w]+)(:(?!:))?/g, (match, key, identifier) => {
204
+ if (!Object.hasOwn(dict, key)) {
205
+ return match;
206
+ }
207
+ objectParams.push(dict[key]);
208
+ return identifier ? '??' : '?';
209
+ });
201
210
  return new RawQueryFragment(sql, objectParams);
202
211
  }
203
212
  return new RawQueryFragment(sql, params);
@@ -17,13 +17,13 @@ export declare class RequestContext {
17
17
  * If the handler is async, the return value needs to be awaited.
18
18
  * Uses `AsyncLocalStorage.run()`, suitable for regular express style middlewares with a `next` callback.
19
19
  */
20
- static create<T>(em: EntityManager | EntityManager[], next: (...args: any[]) => T, options?: CreateContextOptions): T;
20
+ static create<T>(em: EntityManager | EntityManager[], next: (...args: any[]) => T, options?: ((name: string) => CreateContextOptions) | CreateContextOptions): T;
21
21
  /**
22
22
  * Creates new RequestContext instance and runs the code inside its domain.
23
23
  * If the handler is async, the return value needs to be awaited.
24
24
  * Uses `AsyncLocalStorage.enterWith()`, suitable for elysia style middlewares without a `next` callback.
25
25
  */
26
- static enter(em: EntityManager | EntityManager[], options?: CreateContextOptions): void;
26
+ static enter(em: EntityManager | EntityManager[], options?: ((name: string) => CreateContextOptions) | CreateContextOptions): void;
27
27
  /**
28
28
  * Returns current RequestContext (if available).
29
29
  */
@@ -50,10 +50,19 @@ export class RequestContext {
50
50
  static createContext(em, options = {}) {
51
51
  const forks = new Map();
52
52
  if (Array.isArray(em)) {
53
- em.forEach(em => forks.set(em.name, em.fork({ useContext: true, ...options })));
53
+ if (typeof options === 'function') {
54
+ for (const emInstance of em) {
55
+ forks.set(emInstance.name, emInstance.fork({ useContext: true, ...options(emInstance.name) }));
56
+ }
57
+ }
58
+ else {
59
+ for (const emInstance of em) {
60
+ forks.set(emInstance.name, emInstance.fork({ useContext: true, ...options }));
61
+ }
62
+ }
54
63
  }
55
64
  else {
56
- forks.set(em.name, em.fork({ useContext: true, ...options }));
65
+ forks.set(em.name, em.fork({ useContext: true, ...(typeof options === 'function' ? options(em.name) : options) }));
57
66
  }
58
67
  return new RequestContext(forks);
59
68
  }
@@ -50,6 +50,12 @@ export declare class TransactionManager {
50
50
  * Merges entities from fork to parent EntityManager.
51
51
  */
52
52
  private mergeEntitiesToParent;
53
+ /**
54
+ * Returns the property names a given property is tracked and snapshotted under, paired with their values.
55
+ * Inlined embeddables are hydrated as a single object, so only their leaves tell whether it is complete.
56
+ */
57
+ private getTrackedValues;
58
+ private restore;
53
59
  /**
54
60
  * Registers a deletion handler to unset entity identities after flush.
55
61
  */
@@ -169,19 +169,52 @@ export class TransactionManager {
169
169
  if (!wrapped.__initialized && parentWrapped.__initialized) {
170
170
  continue;
171
171
  }
172
- parentWrapped.__data = wrapped.__data;
173
- parentWrapped.__originalEntityData = wrapped.__originalEntityData;
172
+ const parentData = parentWrapped.__data;
173
+ const parentSnapshot = parentWrapped.__originalEntityData;
174
+ parentWrapped.__data = { ...wrapped.__data };
175
+ const originalEntityData = { ...wrapped.__originalEntityData };
174
176
  for (const prop of meta.hydrateProps) {
177
+ const tracked = this.getTrackedValues(prop, entity[prop.name]);
178
+ // the fork entity can be partially loaded, and propagating a property it does not know about
179
+ // would clobber the parent state, so we restore both its value and its snapshot entries
180
+ if (!tracked.every(([key, value]) => value !== undefined || wrapped.__loadedProperties.has(key))) {
181
+ this.restore(parentWrapped.__data, parentData, prop.name);
182
+ tracked.forEach(([key]) => this.restore(originalEntityData, parentSnapshot, key));
183
+ continue;
184
+ }
175
185
  if (prop.kind === ReferenceKind.SCALAR) {
176
186
  parentEntity[prop.name] = entity[prop.name];
177
187
  }
178
188
  }
189
+ if (wrapped.__originalEntityData) {
190
+ parentWrapped.__originalEntityData = originalEntityData;
191
+ }
179
192
  }
180
193
  else {
181
194
  parentUoW.merge(entity, new Set([entity]));
182
195
  }
183
196
  }
184
197
  }
198
+ /**
199
+ * Returns the property names a given property is tracked and snapshotted under, paired with their values.
200
+ * Inlined embeddables are hydrated as a single object, so only their leaves tell whether it is complete.
201
+ */
202
+ getTrackedValues(prop, value) {
203
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
204
+ return Object.values(prop.embeddedProps).flatMap(child => {
205
+ return this.getTrackedValues(child, value?.[child.embedded[1]]);
206
+ });
207
+ }
208
+ return [[prop.name, value]];
209
+ }
210
+ restore(target, source, key) {
211
+ if (source && key in source) {
212
+ target[key] = source[key];
213
+ }
214
+ else {
215
+ delete target[key];
216
+ }
217
+ }
185
218
  /**
186
219
  * Registers a deletion handler to unset entity identities after flush.
187
220
  */
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.0-dev.1';
156
+ static #ORM_VERSION = '7.2.0-dev.10';
157
157
  /**
158
158
  * Checks if the argument is instance of `Object`. Returns false for arrays.
159
159
  */
@@ -402,7 +402,8 @@ export class Utils {
402
402
  static getCompositeKeyHash(data, meta, convertCustomTypes = false, platform, flat = false) {
403
403
  let pks = this.getCompositeKeyValue(data, meta, convertCustomTypes, platform);
404
404
  if (flat) {
405
- pks = Utils.flatten(pks);
405
+ // deep flatten, nested composite PKs produce nested arrays that would be comma-joined by the hash
406
+ pks = Utils.flatten(pks, true);
406
407
  }
407
408
  return Utils.getPrimaryKeyHash(pks);
408
409
  }
@@ -462,7 +463,9 @@ export class Utils {
462
463
  }
463
464
  static getPrimaryKeyCond(entity, primaryKeys) {
464
465
  const cond = primaryKeys.reduce((o, pk) => {
465
- o[pk] = Utils.extractPK(entity[pk]);
466
+ const value = entity[pk];
467
+ // FKs pointing to a composite PK are arrays, which `extractPK` rejects
468
+ o[pk] = Utils.isPrimaryKey(value, true) ? value : Utils.extractPK(value);
466
469
  return o;
467
470
  }, {});
468
471
  if (Object.values(cond).some(v => v === null)) {
package/utils/clone.js CHANGED
@@ -112,6 +112,12 @@ export function clone(parent, respectCustomCloneMethod = true) {
112
112
  });
113
113
  }
114
114
  for (const i in parent) {
115
+ // an own `__proto__` key (as produced by `JSON.parse`) has no own counterpart on
116
+ // `child` to shadow the inherited accessor, so assigning it would replace the
117
+ // clone's prototype instead of copying the value
118
+ if (i === '__proto__') {
119
+ continue;
120
+ }
115
121
  let attrs;
116
122
  if (proto) {
117
123
  attrs = getPropertyDescriptor(proto, i);
package/utils/env-vars.js CHANGED
@@ -79,6 +79,7 @@ export function loadEnvironmentVars() {
79
79
  read2('silent', bool);
80
80
  read2('emit');
81
81
  read2('snapshot', bool);
82
+ read2('snapshotOnMigrate', bool);
82
83
  read2('snapshotName');
83
84
  cleanup(ret, 'migrations');
84
85
  ret.schemaGenerator = {};