@mikro-orm/core 7.1.0-dev.4 → 7.1.0-dev.41

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 (61) hide show
  1. package/EntityManager.d.ts +63 -12
  2. package/EntityManager.js +221 -40
  3. package/README.md +2 -1
  4. package/connections/Connection.d.ts +29 -0
  5. package/drivers/IDatabaseDriver.d.ts +45 -7
  6. package/entity/BaseEntity.d.ts +68 -1
  7. package/entity/BaseEntity.js +18 -0
  8. package/entity/Collection.d.ts +6 -3
  9. package/entity/Collection.js +15 -4
  10. package/entity/EntityAssigner.js +8 -0
  11. package/entity/EntityFactory.js +20 -1
  12. package/entity/EntityLoader.d.ts +8 -1
  13. package/entity/EntityLoader.js +89 -28
  14. package/entity/EntityRepository.d.ts +27 -9
  15. package/entity/EntityRepository.js +12 -0
  16. package/entity/Reference.d.ts +42 -1
  17. package/entity/Reference.js +9 -0
  18. package/entity/defineEntity.d.ts +99 -21
  19. package/entity/defineEntity.js +17 -6
  20. package/entity/utils.js +4 -5
  21. package/enums.d.ts +8 -1
  22. package/errors.d.ts +2 -0
  23. package/errors.js +4 -0
  24. package/index.d.ts +2 -2
  25. package/index.js +1 -1
  26. package/metadata/EntitySchema.js +3 -0
  27. package/metadata/MetadataDiscovery.d.ts +12 -0
  28. package/metadata/MetadataDiscovery.js +166 -20
  29. package/metadata/MetadataValidator.d.ts +24 -0
  30. package/metadata/MetadataValidator.js +202 -1
  31. package/metadata/types.d.ts +71 -4
  32. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  33. package/naming-strategy/NamingStrategy.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/platforms/Platform.d.ts +18 -3
  36. package/platforms/Platform.js +58 -6
  37. package/serialization/EntitySerializer.js +2 -1
  38. package/typings.d.ts +202 -22
  39. package/typings.js +51 -14
  40. package/unit-of-work/UnitOfWork.js +15 -4
  41. package/utils/AbstractMigrator.d.ts +20 -5
  42. package/utils/AbstractMigrator.js +263 -28
  43. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  44. package/utils/AbstractSchemaGenerator.js +4 -1
  45. package/utils/Configuration.d.ts +25 -0
  46. package/utils/Configuration.js +1 -0
  47. package/utils/DataloaderUtils.d.ts +10 -1
  48. package/utils/DataloaderUtils.js +78 -0
  49. package/utils/EntityComparator.js +1 -1
  50. package/utils/QueryHelper.d.ts +16 -0
  51. package/utils/QueryHelper.js +15 -0
  52. package/utils/TransactionManager.js +2 -0
  53. package/utils/Utils.js +1 -1
  54. package/utils/fs-utils.d.ts +2 -0
  55. package/utils/fs-utils.js +7 -1
  56. package/utils/index.d.ts +1 -0
  57. package/utils/index.js +1 -0
  58. package/utils/partition-utils.d.ts +17 -0
  59. package/utils/partition-utils.js +79 -0
  60. package/utils/upsert-utils.d.ts +2 -0
  61. package/utils/upsert-utils.js +26 -1
@@ -1,6 +1,7 @@
1
1
  import { Collection } from '../entity/Collection.js';
2
2
  import { helper } from '../entity/wrap.js';
3
3
  import { Reference } from '../entity/Reference.js';
4
+ import { ReferenceKind } from '../enums.js';
4
5
  import { Utils } from './Utils.js';
5
6
  export class DataloaderUtils {
6
7
  static DataLoader;
@@ -216,6 +217,83 @@ export class DataloaderUtils {
216
217
  return ret;
217
218
  };
218
219
  }
220
+ /**
221
+ * Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
222
+ * by entity and relation, issues a single grouped count query per entity+options combination
223
+ * via `em.countBy()`, and maps each input collection to the corresponding count.
224
+ *
225
+ * For 1:M relations, groups by the FK property on the target entity.
226
+ * For M:N relations, groups by the owner FK on the pivot entity.
227
+ */
228
+ static getCountBatchLoadFn(em) {
229
+ return async (collsWithOpts) => {
230
+ const groups = new Map();
231
+ const keys = [];
232
+ for (const [col, opts] of collsWithOpts) {
233
+ const prop = col.property;
234
+ let fkProp;
235
+ let countByClass;
236
+ let targetFilterProp;
237
+ if (prop.kind === ReferenceKind.ONE_TO_MANY) {
238
+ fkProp = prop.mappedBy;
239
+ countByClass = prop.targetMeta.class;
240
+ }
241
+ else {
242
+ // M:N: group by the owner FK on the pivot entity
243
+ const pivotMeta = em.getMetadata().get(prop.pivotEntity);
244
+ const ownerPivotProp = pivotMeta.relations[prop.owner ? 0 : 1];
245
+ const targetPivotProp = pivotMeta.relations[prop.owner ? 1 : 0];
246
+ fkProp = ownerPivotProp.name;
247
+ countByClass = pivotMeta.class;
248
+ targetFilterProp = targetPivotProp.name;
249
+ }
250
+ // Include the owner-side uniqueName in the key so that two distinct owner entity types
251
+ // sharing a relation with the same property name (e.g. `Author.books` and `Publisher.books`)
252
+ // don't collide into a single batch that would use one owner's FK mapping for the other.
253
+ const ownerUniqueName = helper(col.owner).__meta.uniqueName;
254
+ const key = `${ownerUniqueName}.${prop.name}|${JSON.stringify(opts ?? {})}`;
255
+ keys.push(key);
256
+ let group = groups.get(key);
257
+ if (!group) {
258
+ group = { fkProp, countByClass, targetFilterProp, ownerPKs: new Map(), opts: opts ?? {} };
259
+ groups.set(key, group);
260
+ }
261
+ const pk = helper(col.owner).getPrimaryKey();
262
+ group.ownerPKs.set(JSON.stringify(pk), pk);
263
+ }
264
+ const promises = Array.from(groups.entries()).map(async ([key, group]) => {
265
+ const allPKs = Array.from(group.ownerPKs.values());
266
+ const { where, ...countOpts } = group.opts;
267
+ const conditions = [{ [group.fkProp]: { $in: allPKs } }];
268
+ if (where) {
269
+ conditions.push(where);
270
+ }
271
+ // For M:N, apply the target entity's filters through the pivot's target relation
272
+ if (group.targetFilterProp) {
273
+ const targetMeta = em.getMetadata().find(group.countByClass);
274
+ const targetRelMeta = targetMeta.properties[group.targetFilterProp]?.targetMeta;
275
+ if (targetRelMeta) {
276
+ const filterCond = await em.applyFilters(targetRelMeta.class, {}, countOpts.filters, 'read');
277
+ if (filterCond && Object.keys(filterCond).length > 0) {
278
+ conditions.push({ [group.targetFilterProp]: filterCond });
279
+ }
280
+ }
281
+ }
282
+ const fkWhere = conditions.length === 1 ? conditions[0] : { $and: conditions };
283
+ const counts = await em.countBy(group.countByClass, group.fkProp, {
284
+ where: fkWhere,
285
+ ...countOpts,
286
+ });
287
+ return [key, counts];
288
+ });
289
+ const resultsMap = new Map(await Promise.all(promises));
290
+ return collsWithOpts.map(([col], i) => {
291
+ const pk = helper(col.owner).getPrimaryKey();
292
+ const counts = resultsMap.get(keys[i]);
293
+ return counts?.[String(pk)] ?? 0;
294
+ });
295
+ };
296
+ }
219
297
  static async getDataLoader() {
220
298
  if (this.DataLoader) {
221
299
  return this.DataLoader;
@@ -536,7 +536,7 @@ export class EntityComparator {
536
536
  ret += ` && entity${path.map(k => this.wrap(k)).join('')}?.isInitialized()`;
537
537
  }
538
538
  ret += `) {\n`;
539
- if (['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
539
+ if (!prop.array && ['number', 'string', 'boolean'].includes(prop.type.toLowerCase())) {
540
540
  return ret + ` ret${dataKey} = entity${entityKey}${unwrap};\n }\n`;
541
541
  }
542
542
  if (prop.kind === ReferenceKind.EMBEDDED) {
@@ -6,6 +6,22 @@ import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
6
6
  /** @internal */
7
7
  export declare class QueryHelper {
8
8
  static readonly SUPPORTED_OPERATORS: string[];
9
+ /**
10
+ * True when the property has multiple polymorph target types. Covers two structurally-equivalent
11
+ * shapes routed through the same loading path:
12
+ * 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
13
+ * target types, shared pivot with target-side discriminator).
14
+ * 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
15
+ * (many owner types pointing at one target, viewed from the target where "owners" looks
16
+ * like a union of multiple types).
17
+ *
18
+ * Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
19
+ * by discriminator and hydrates each target class separately.
20
+ */
21
+ static isUnionTargetPolymorphic(prop: {
22
+ polymorphic?: boolean;
23
+ polymorphTargets?: readonly unknown[];
24
+ }): boolean;
9
25
  /**
10
26
  * Finds the discriminator value (key) for a given entity class in a discriminator map.
11
27
  * Walks up the prototype chain so TPT subclasses resolve to their root's key.
@@ -7,6 +7,21 @@ import { isRaw, Raw } from './RawQueryFragment.js';
7
7
  /** @internal */
8
8
  export class QueryHelper {
9
9
  static SUPPORTED_OPERATORS = ['>', '<', '<=', '>=', '!', '!='];
10
+ /**
11
+ * True when the property has multiple polymorph target types. Covers two structurally-equivalent
12
+ * shapes routed through the same loading path:
13
+ * 1. Union-target owner side — `Post.attachments: Collection<Image | Video>` (one owner, many
14
+ * target types, shared pivot with target-side discriminator).
15
+ * 2. Merged inverse of Rails-style polymorphic M:N — `Tag.owners: Collection<Post | Video>`
16
+ * (many owner types pointing at one target, viewed from the target where "owners" looks
17
+ * like a union of multiple types).
18
+ *
19
+ * Both cases are loaded via `loadFromUnionTargetPolymorphicPivotTable`, which buckets pivot rows
20
+ * by discriminator and hydrates each target class separately.
21
+ */
22
+ static isUnionTargetPolymorphic(prop) {
23
+ return !!prop.polymorphic && (prop.polymorphTargets?.length ?? 0) > 1;
24
+ }
10
25
  /**
11
26
  * Finds the discriminator value (key) for a given entity class in a discriminator map.
12
27
  * Walks up the prototype chain so TPT subclasses resolve to their root's key.
@@ -136,6 +136,8 @@ export class TransactionManager {
136
136
  cloneEventManager: true,
137
137
  disableTransactions: options.ignoreNestedTransactions,
138
138
  loggerContext: options.loggerContext,
139
+ signal: options.signal,
140
+ inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
139
141
  });
140
142
  }
141
143
  /**
package/utils/Utils.js CHANGED
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
141
141
  /** Collection of general-purpose utility methods used throughout the ORM. */
142
142
  export class Utils {
143
143
  static PK_SEPARATOR = '~~~';
144
- static #ORM_VERSION = '7.1.0-dev.4';
144
+ static #ORM_VERSION = '7.1.0-dev.41';
145
145
  /**
146
146
  * Checks if the argument is instance of `Object`. Returns false for arrays.
147
147
  */
@@ -13,7 +13,9 @@ export interface FsUtils {
13
13
  normalizePath(...parts: string[]): string;
14
14
  relativePath(path: string, relativeTo: string): string;
15
15
  absolutePath(path: string, baseDir?: string): string;
16
+ readFile(path: string): Promise<string>;
16
17
  writeFile(path: string, data: string, options?: Record<string, any>): Promise<void>;
18
+ unlink(path: string): Promise<void>;
17
19
  dynamicImport<T = any>(id: string): Promise<T>;
18
20
  }
19
21
  export declare const fs: FsUtils;
package/utils/fs-utils.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, globSync as nodeGlobSync, mkdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
2
- import { writeFile as nodeWriteFile } from 'node:fs/promises';
2
+ import { readFile as nodeReadFile, unlink as nodeUnlink, writeFile as nodeWriteFile } from 'node:fs/promises';
3
3
  import { isAbsolute, join, normalize, relative } from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { Utils } from './Utils.js';
@@ -181,9 +181,15 @@ export const fs = {
181
181
  }
182
182
  return this.normalizePath(path);
183
183
  },
184
+ async readFile(path) {
185
+ return nodeReadFile(path, 'utf-8');
186
+ },
184
187
  async writeFile(path, data, options) {
185
188
  await nodeWriteFile(path, data, options);
186
189
  },
190
+ async unlink(path) {
191
+ await nodeUnlink(path);
192
+ },
187
193
  async dynamicImport(id) {
188
194
  /* v8 ignore next */
189
195
  const specifier = id.startsWith('file://') ? id : pathToFileURL(id).href;
package/utils/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export * from './EntityComparator.js';
10
10
  export * from './RawQueryFragment.js';
11
11
  export * from './env-vars.js';
12
12
  export * from './upsert-utils.js';
13
+ export * from './partition-utils.js';
package/utils/index.js CHANGED
@@ -10,3 +10,4 @@ export * from './EntityComparator.js';
10
10
  export * from './RawQueryFragment.js';
11
11
  export * from './env-vars.js';
12
12
  export * from './upsert-utils.js';
13
+ export * from './partition-utils.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
3
+ * resolution: unquoted segments fold to lower case, quoted segments preserve case and
4
+ * un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
5
+ * and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
6
+ *
7
+ * `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
8
+ * that would otherwise only surface as runtime PG "relation already exists" errors.
9
+ */
10
+ export declare function normalizePartitionNameForComparison(name: string): string;
11
+ /**
12
+ * Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
13
+ * commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
14
+ * comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
15
+ * as an opaque expression.
16
+ */
17
+ export declare function splitCommaSeparatedIdentifiers(value: string): string[] | null;
@@ -0,0 +1,79 @@
1
+ function normalizeIdentifierSegment(segment) {
2
+ const trimmed = segment.trim();
3
+ if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
4
+ return trimmed.slice(1, -1).replaceAll('""', '"');
5
+ }
6
+ return trimmed.toLowerCase();
7
+ }
8
+ /**
9
+ * Normalize a partition name for collision detection. Mirrors PostgreSQL identifier
10
+ * resolution: unquoted segments fold to lower case, quoted segments preserve case and
11
+ * un-escape embedded `""`. Returns a canonical `schema.name` form so both schema-qualified
12
+ * and bare names compare consistently (`.name` vs `schema.name` stay distinguishable).
13
+ *
14
+ * `Part_1`, `part_1`, and `"part_1"` all normalize to the same value, catching collisions
15
+ * that would otherwise only surface as runtime PG "relation already exists" errors.
16
+ */
17
+ export function normalizePartitionNameForComparison(name) {
18
+ const trimmed = name.trim();
19
+ let depth = 0;
20
+ for (let i = 0; i < trimmed.length; i++) {
21
+ const ch = trimmed[i];
22
+ if (ch === '"') {
23
+ if (trimmed[i + 1] === '"') {
24
+ i++;
25
+ continue;
26
+ }
27
+ depth = depth === 0 ? 1 : 0;
28
+ continue;
29
+ }
30
+ if (ch === '.' && depth === 0) {
31
+ return `${normalizeIdentifierSegment(trimmed.slice(0, i))}.${normalizeIdentifierSegment(trimmed.slice(i + 1))}`;
32
+ }
33
+ }
34
+ return `.${normalizeIdentifierSegment(trimmed)}`;
35
+ }
36
+ /**
37
+ * Split a comma-list of identifiers, respecting double-quoted identifiers (which may contain
38
+ * commas and use `""` to escape embedded quotes). Returns `null` when the input is not a clean
39
+ * comma-list of identifiers (e.g. contains function calls or literals), so callers treat it
40
+ * as an opaque expression.
41
+ */
42
+ export function splitCommaSeparatedIdentifiers(value) {
43
+ const segments = [];
44
+ let buffer = '';
45
+ let inQuote = false;
46
+ for (let i = 0; i < value.length; i++) {
47
+ const ch = value[i];
48
+ if (ch === '"') {
49
+ if (inQuote && value[i + 1] === '"') {
50
+ buffer += '""';
51
+ i++;
52
+ continue;
53
+ }
54
+ inQuote = !inQuote;
55
+ buffer += ch;
56
+ continue;
57
+ }
58
+ if (!inQuote) {
59
+ if (ch === ',') {
60
+ segments.push(buffer);
61
+ buffer = '';
62
+ continue;
63
+ }
64
+ if (!/[\w.\s]/.test(ch)) {
65
+ return null;
66
+ }
67
+ }
68
+ buffer += ch;
69
+ }
70
+ if (inQuote) {
71
+ return null;
72
+ }
73
+ segments.push(buffer);
74
+ const trimmed = segments.map(s => s.trim());
75
+ if (trimmed.some(s => s === '')) {
76
+ return null;
77
+ }
78
+ return trimmed;
79
+ }
@@ -10,3 +10,5 @@ export declare function getWhereCondition<T extends object>(meta: EntityMetadata
10
10
  where: FilterQuery<T>;
11
11
  propIndex: number | false;
12
12
  };
13
+ /** @internal */
14
+ export declare function resetUntouchedCollections<Entity extends object>(meta: EntityMetadata<Entity>, entity: Entity): void;
@@ -1,5 +1,8 @@
1
1
  import { isRaw } from '../utils/RawQueryFragment.js';
2
2
  import { Utils } from './Utils.js';
3
+ import { ReferenceKind } from '../enums.js';
4
+ import { Collection } from '../entity/Collection.js';
5
+ import { helper } from '../entity/wrap.js';
3
6
  function expandEmbeddedProperties(prop, key) {
4
7
  if (prop.object) {
5
8
  return [prop.name];
@@ -52,7 +55,9 @@ export function getOnConflictFields(meta, data, uniqueFields, options) {
52
55
  });
53
56
  }
54
57
  const keys = Object.keys(data).flatMap(f => {
55
- if (!(Array.isArray(uniqueFields) && !uniqueFields.includes(f))) {
58
+ // skip explicitly listed unique fields; for raw onConflictFields we can't introspect the
59
+ // fragment, so merge all data keys (the user is responsible for not corrupting them).
60
+ if (Array.isArray(uniqueFields) && uniqueFields.includes(f)) {
56
61
  return [];
57
62
  }
58
63
  const prop = meta?.properties[f];
@@ -142,3 +147,23 @@ export function getWhereCondition(meta, onConflictFields, data, where) {
142
147
  }
143
148
  return { where, propIndex };
144
149
  }
150
+ /** @internal */
151
+ export function resetUntouchedCollections(meta, entity) {
152
+ // for entities passed in via `em.create()` and then upserted, collection-kind relations
153
+ // were initialized as empty-but-initialized by the hydrator (newEntity=true). once the
154
+ // upsert resolves the entity to a possibly existing row, that state is stale and would
155
+ // cause `em.populate()` to skip those collections. replace them with a fresh
156
+ // uninitialized collection so populate can load them.
157
+ if (helper(entity).__originalEntityData) {
158
+ return;
159
+ }
160
+ for (const prop of meta.relations) {
161
+ if (prop.kind !== ReferenceKind.MANY_TO_MANY && prop.kind !== ReferenceKind.ONE_TO_MANY) {
162
+ continue;
163
+ }
164
+ const collection = entity[prop.name];
165
+ if (Utils.isCollection(collection) && collection.isInitialized() && !collection.isDirty()) {
166
+ Collection.create(entity, prop.name, undefined, false);
167
+ }
168
+ }
169
+ }