@mikro-orm/core 7.1.0-dev.37 → 7.1.0-dev.38
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 +42 -10
- package/entity/EntityLoader.js +43 -10
- package/metadata/MetadataDiscovery.js +4 -0
- package/package.json +1 -1
- package/utils/Utils.js +1 -1
- package/utils/upsert-utils.d.ts +2 -0
- package/utils/upsert-utils.js +26 -1
package/EntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
|
|
1
|
+
import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
|
|
2
2
|
import { Utils } from './utils/Utils.js';
|
|
3
3
|
import { Cursor } from './utils/Cursor.js';
|
|
4
4
|
import { QueryHelper } from './utils/QueryHelper.js';
|
|
@@ -322,13 +322,13 @@ export class EntityManager {
|
|
|
322
322
|
}
|
|
323
323
|
const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
|
|
324
324
|
const children = [];
|
|
325
|
-
const lookUpChildren = (ret,
|
|
326
|
-
const children = types.filter(meta2 => meta2.extends ===
|
|
327
|
-
children.forEach(m => lookUpChildren(ret, m
|
|
325
|
+
const lookUpChildren = (ret, parent) => {
|
|
326
|
+
const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent);
|
|
327
|
+
children.forEach(m => lookUpChildren(ret, m));
|
|
328
328
|
ret.push(...children.filter(c => c.discriminatorValue));
|
|
329
329
|
return children;
|
|
330
330
|
};
|
|
331
|
-
lookUpChildren(children, meta
|
|
331
|
+
lookUpChildren(children, meta);
|
|
332
332
|
/* v8 ignore next */
|
|
333
333
|
where[meta.root.discriminatorColumn] =
|
|
334
334
|
children.length > 0
|
|
@@ -363,6 +363,13 @@ export class EntityManager {
|
|
|
363
363
|
if (!joined && !hint.filter) {
|
|
364
364
|
continue;
|
|
365
365
|
}
|
|
366
|
+
// Polymorphic to-one relations are already filtered via per-target LEFT JOINs created
|
|
367
|
+
// for the `:ref filter` hint; emitting a populate-filter entry here would auto-join only
|
|
368
|
+
// the first target meta and either drop rows pointing to other targets or reference
|
|
369
|
+
// parent-table-only columns on the child sub-table alias for TPT children.
|
|
370
|
+
if (prop.polymorphic && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
366
373
|
const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
|
|
367
374
|
const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
|
|
368
375
|
...options,
|
|
@@ -448,7 +455,23 @@ export class EntityManager {
|
|
|
448
455
|
const prop = meta?.properties[field];
|
|
449
456
|
if (prop && !ref) {
|
|
450
457
|
hint.children ??= [];
|
|
451
|
-
|
|
458
|
+
const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta];
|
|
459
|
+
for (const targetMeta of targets) {
|
|
460
|
+
const before = hint.children.length;
|
|
461
|
+
await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
|
|
462
|
+
// For polymorphic relations, `hint.children` is applied to every polymorph target during
|
|
463
|
+
// population, so drop any auto-added hint whose field does not exist on every target —
|
|
464
|
+
// otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722).
|
|
465
|
+
if (targets.length > 1 && hint.children.length > before) {
|
|
466
|
+
const added = hint.children.splice(before);
|
|
467
|
+
for (const h of added) {
|
|
468
|
+
const f = h.field.split(':')[0];
|
|
469
|
+
if (targets.every(t => f in t.properties)) {
|
|
470
|
+
hint.children.push(h);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
452
475
|
}
|
|
453
476
|
}
|
|
454
477
|
}
|
|
@@ -849,6 +872,7 @@ export class EntityManager {
|
|
|
849
872
|
em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
|
|
850
873
|
}
|
|
851
874
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
875
|
+
resetUntouchedCollections(meta, entity);
|
|
852
876
|
const snapshot = this.#comparator.prepareEntity(entity);
|
|
853
877
|
em.#unitOfWork.register(entity, snapshot, { refresh: true });
|
|
854
878
|
if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
|
|
@@ -1080,6 +1104,7 @@ export class EntityManager {
|
|
|
1080
1104
|
}
|
|
1081
1105
|
for (const [entity] of entities) {
|
|
1082
1106
|
// recompute the data as there might be some values missing (e.g. those with db column defaults)
|
|
1107
|
+
resetUntouchedCollections(meta, entity);
|
|
1083
1108
|
const snapshot = this.#comparator.prepareEntity(entity);
|
|
1084
1109
|
em.#unitOfWork.register(entity, snapshot, { refresh: true });
|
|
1085
1110
|
}
|
|
@@ -1872,9 +1897,13 @@ export class EntityManager {
|
|
|
1872
1897
|
return [];
|
|
1873
1898
|
}
|
|
1874
1899
|
const meta = this.metadata.find(entityName);
|
|
1875
|
-
// infer populate hints from `fields` when present; merge with explicit `populate` if both are set
|
|
1900
|
+
// infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
|
|
1901
|
+
// `populateArray` is only kept when the entries are user-provided strings — when this method runs a
|
|
1902
|
+
// second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
|
|
1876
1903
|
const fields = options.fields ? this.buildFields(options.fields) : undefined;
|
|
1877
|
-
const populateArray = Array.isArray(options.populate)
|
|
1904
|
+
const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
|
|
1905
|
+
? options.populate
|
|
1906
|
+
: undefined;
|
|
1878
1907
|
const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
|
|
1879
1908
|
const shouldInfer = fields !== undefined && options.populate === undefined;
|
|
1880
1909
|
const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
|
|
@@ -1918,8 +1947,11 @@ export class EntityManager {
|
|
|
1918
1947
|
}
|
|
1919
1948
|
else {
|
|
1920
1949
|
// with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
|
|
1921
|
-
// require a JOIN to access sub-fields, whereas bare names are already handled by the driver
|
|
1922
|
-
|
|
1950
|
+
// require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
|
|
1951
|
+
// skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
|
|
1952
|
+
// both a `:ref` and a non-`:ref` entry for the same relation
|
|
1953
|
+
const existing = new Set(populateArray.map(p => p.split(':')[0]));
|
|
1954
|
+
const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
|
|
1923
1955
|
options.populate = [...populateArray, ...extras];
|
|
1924
1956
|
}
|
|
1925
1957
|
}
|
package/entity/EntityLoader.js
CHANGED
|
@@ -319,7 +319,7 @@ export class EntityLoader {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
async findChildren(entities, prop, populate, options, ref) {
|
|
322
|
-
const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
|
|
322
|
+
const children = Utils.unique(this.getChildReferences(entities, prop, options, ref, populate));
|
|
323
323
|
const meta = prop.targetMeta;
|
|
324
324
|
// When targetKey is set, use it for FK lookup instead of the PK
|
|
325
325
|
let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
@@ -337,10 +337,10 @@ export class EntityLoader {
|
|
|
337
337
|
fk = ownerProp.name;
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
|
-
if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
|
|
340
|
+
if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && (!ref || (!prop.mapToPk && !populate.filter))) {
|
|
341
341
|
children.length = 0;
|
|
342
342
|
fk = meta.properties[prop.mappedBy].name;
|
|
343
|
-
children.push(...this.filterByReferences(entities, prop
|
|
343
|
+
children.push(...this.filterByReferences(entities, prop, options, ref));
|
|
344
344
|
}
|
|
345
345
|
if (children.length === 0) {
|
|
346
346
|
return { items: [], partial };
|
|
@@ -707,7 +707,7 @@ export class EntityLoader {
|
|
|
707
707
|
}
|
|
708
708
|
return ret;
|
|
709
709
|
}
|
|
710
|
-
getChildReferences(entities, prop, options, ref) {
|
|
710
|
+
getChildReferences(entities, prop, options, ref, populate) {
|
|
711
711
|
const filtered = this.filterCollections(entities, prop.name, options, ref);
|
|
712
712
|
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
713
713
|
return filtered.map(e => e[prop.name].owner);
|
|
@@ -723,7 +723,7 @@ export class EntityLoader {
|
|
|
723
723
|
return filtered;
|
|
724
724
|
}
|
|
725
725
|
// MANY_TO_ONE or ONE_TO_ONE
|
|
726
|
-
return this.filterReferences(entities, prop.name, options, ref);
|
|
726
|
+
return this.filterReferences(entities, prop.name, options, ref, populate);
|
|
727
727
|
}
|
|
728
728
|
filterCollections(entities, field, options, ref) {
|
|
729
729
|
if (options.refresh) {
|
|
@@ -749,7 +749,7 @@ export class EntityLoader {
|
|
|
749
749
|
}
|
|
750
750
|
return this.isPropertyLoaded(entity[f], r.join('.'));
|
|
751
751
|
}
|
|
752
|
-
filterReferences(entities, field, options, ref) {
|
|
752
|
+
filterReferences(entities, field, options, ref, populate) {
|
|
753
753
|
if (ref) {
|
|
754
754
|
return [];
|
|
755
755
|
}
|
|
@@ -758,13 +758,19 @@ export class EntityLoader {
|
|
|
758
758
|
return children.map(e => Reference.unwrapReference(e[field]));
|
|
759
759
|
}
|
|
760
760
|
if (options.fields) {
|
|
761
|
+
// `:ref` populate children are satisfied by FK/PK and will be loaded lazily — they don't force a parent reload
|
|
762
|
+
const refChildren = new Set((populate?.children ?? [])
|
|
763
|
+
.map(c => c.field.split(':'))
|
|
764
|
+
.filter(parts => parts[1] === 'ref')
|
|
765
|
+
.map(parts => parts[0]));
|
|
761
766
|
return children
|
|
762
767
|
.map(e => Reference.unwrapReference(e[field]))
|
|
763
768
|
.filter(target => {
|
|
764
769
|
const wrapped = helper(target);
|
|
765
770
|
const childFields = options.fields
|
|
766
771
|
.filter(f => f.startsWith(`${field}.`))
|
|
767
|
-
.map(f => f.substring(field.length + 1))
|
|
772
|
+
.map(f => f.substring(field.length + 1))
|
|
773
|
+
.filter(cf => !refChildren.has(cf.split('.')[0]));
|
|
768
774
|
return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
|
|
769
775
|
});
|
|
770
776
|
}
|
|
@@ -772,12 +778,39 @@ export class EntityLoader {
|
|
|
772
778
|
.filter(e => !e[field].__helper.__initialized)
|
|
773
779
|
.map(e => Reference.unwrapReference(e[field]));
|
|
774
780
|
}
|
|
775
|
-
filterByReferences(entities,
|
|
781
|
+
filterByReferences(entities, prop, options, ref) {
|
|
776
782
|
/* v8 ignore next */
|
|
777
|
-
if (refresh) {
|
|
783
|
+
if (options.refresh) {
|
|
778
784
|
return entities;
|
|
779
785
|
}
|
|
780
|
-
|
|
786
|
+
const field = prop.name;
|
|
787
|
+
return entities.filter(e => {
|
|
788
|
+
const value = e[field];
|
|
789
|
+
if (value === null) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
if (value === undefined || !Utils.isEntity(value, true)) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
const target = Reference.unwrapReference(value);
|
|
796
|
+
const wrapped = helper(target);
|
|
797
|
+
if (!wrapped.__initialized) {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
if (ref) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
if (options.fields) {
|
|
804
|
+
const childFields = options.fields
|
|
805
|
+
.filter(f => f.startsWith(`${prop.name}.`))
|
|
806
|
+
.map(f => f.substring(prop.name.length + 1));
|
|
807
|
+
return childFields.length > 0 && !childFields.every(f => this.isPropertyLoaded(target, f));
|
|
808
|
+
}
|
|
809
|
+
return prop.targetMeta.comparableProps.some(targetProp => {
|
|
810
|
+
const inlineEmbedded = targetProp.kind === ReferenceKind.EMBEDDED && !targetProp.object;
|
|
811
|
+
return !inlineEmbedded && !targetProp.lazy && !wrapped.__loadedProperties.has(targetProp.name);
|
|
812
|
+
});
|
|
813
|
+
});
|
|
781
814
|
}
|
|
782
815
|
lookupAllRelationships(entityName) {
|
|
783
816
|
const ret = [];
|
|
@@ -605,6 +605,10 @@ export class MetadataDiscovery {
|
|
|
605
605
|
prop.pivotEntity = pivotMeta.class;
|
|
606
606
|
if (prop.inversedBy) {
|
|
607
607
|
prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
|
|
608
|
+
const targetRoot = prop.targetMeta.root;
|
|
609
|
+
if (targetRoot !== prop.targetMeta && targetRoot.properties[prop.inversedBy]) {
|
|
610
|
+
targetRoot.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
|
|
611
|
+
}
|
|
608
612
|
}
|
|
609
613
|
// Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
|
|
610
614
|
// owner prop. Covers three cases:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.1.0-dev.
|
|
3
|
+
"version": "7.1.0-dev.38",
|
|
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/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.
|
|
144
|
+
static #ORM_VERSION = '7.1.0-dev.38';
|
|
145
145
|
/**
|
|
146
146
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
147
147
|
*/
|
package/utils/upsert-utils.d.ts
CHANGED
|
@@ -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;
|
package/utils/upsert-utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|