@mikro-orm/core 7.3.0-dev.16 → 7.3.0-dev.18
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 +127 -32
- package/drivers/DatabaseDriver.d.ts +2 -0
- package/drivers/DatabaseDriver.js +16 -3
- package/entity/EntityAssigner.js +15 -3
- package/entity/EntityFactory.d.ts +7 -0
- package/entity/EntityFactory.js +37 -0
- package/entity/EntityLoader.js +24 -9
- package/entity/defineEntity.d.ts +24 -18
- package/entity/defineEntity.js +1 -0
- package/entity/validators.js +4 -1
- package/hydration/ObjectHydrator.js +4 -0
- package/metadata/MetadataDiscovery.d.ts +5 -0
- package/metadata/MetadataDiscovery.js +96 -25
- package/metadata/MetadataStorage.d.ts +1 -0
- package/metadata/MetadataStorage.js +20 -3
- package/metadata/MetadataValidator.js +2 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +4 -0
- package/platforms/Platform.js +8 -0
- package/serialization/EntitySerializer.js +52 -12
- package/types/DoubleType.d.ts +7 -3
- package/types/DoubleType.js +8 -3
- package/typings.d.ts +21 -5
- package/typings.js +5 -2
- package/unit-of-work/ChangeSetComputer.js +2 -1
- package/unit-of-work/ChangeSetPersister.js +24 -5
- package/unit-of-work/CommitOrderCalculator.js +2 -1
- package/unit-of-work/UnitOfWork.js +20 -5
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +46 -6
- package/utils/QueryHelper.js +10 -2
- package/utils/RawQueryFragment.d.ts +5 -1
- package/utils/RawQueryFragment.js +27 -19
- package/utils/Utils.d.ts +2 -0
- package/utils/Utils.js +24 -8
- package/utils/clone.js +5 -7
- package/utils/fs-utils.js +19 -15
- package/utils/upsert-utils.js +2 -1
|
@@ -183,10 +183,11 @@ export class MetadataDiscovery {
|
|
|
183
183
|
}
|
|
184
184
|
discovered.forEach(meta => meta.sync(true));
|
|
185
185
|
this.#metadataProvider.combineCache();
|
|
186
|
+
const referencingProperties = this.indexReferencingProperties(filtered);
|
|
186
187
|
return discovered.map(meta => {
|
|
187
188
|
meta = this.#metadata.get(meta.class);
|
|
188
189
|
meta.sync(true);
|
|
189
|
-
this.findReferencingProperties(meta,
|
|
190
|
+
this.findReferencingProperties(meta, referencingProperties);
|
|
190
191
|
if (meta.inheritanceType === 'tpt') {
|
|
191
192
|
this.computeTPTOwnProps(meta);
|
|
192
193
|
}
|
|
@@ -217,18 +218,20 @@ export class MetadataDiscovery {
|
|
|
217
218
|
.replace(/Array<(.*)>/, '$1') // unwrap array
|
|
218
219
|
.replace(/\[]$/, '') // remove array suffix
|
|
219
220
|
.replace(/\((.*)\)/, '$1'); // unwrap union types
|
|
221
|
+
const discoveredNames = new Set(this.#discovered.map(m => m.className));
|
|
220
222
|
// Names can be ambiguous when a minifier mangles two classes to the same name,
|
|
221
223
|
// so class references also need to be checked by identity.
|
|
224
|
+
const discoveredClasses = new Set(this.#discovered.map(m => m.class));
|
|
222
225
|
const discoveredByIdentity = (target) => {
|
|
223
226
|
const cls = EntitySchema.is(target) ? target.meta.class : target;
|
|
224
|
-
return typeof cls !== 'function' ||
|
|
227
|
+
return typeof cls !== 'function' || discoveredClasses.has(cls);
|
|
225
228
|
};
|
|
226
229
|
const missing = [];
|
|
227
230
|
this.#discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
|
|
228
231
|
const indirect = (prop.kind === ReferenceKind.MANY_TO_MANY ? prop.pivotEntity : prop.through);
|
|
229
232
|
if (indirect) {
|
|
230
233
|
const target = typeof indirect === 'function' && !indirect.prototype ? indirect() : indirect;
|
|
231
|
-
if (!
|
|
234
|
+
if (!discoveredNames.has(Utils.className(target)) || !discoveredByIdentity(target)) {
|
|
232
235
|
missing.push(target);
|
|
233
236
|
}
|
|
234
237
|
}
|
|
@@ -236,7 +239,7 @@ export class MetadataDiscovery {
|
|
|
236
239
|
const target = typeof prop.entity === 'function' && !prop.entity.prototype ? prop.entity() : prop.type;
|
|
237
240
|
if (!unwrap(prop.type)
|
|
238
241
|
.split(/ ?\| ?/)
|
|
239
|
-
.every(type =>
|
|
242
|
+
.every(type => discoveredNames.has(type)) ||
|
|
240
243
|
!Utils.asArray(target).every(discoveredByIdentity)) {
|
|
241
244
|
missing.push(...Utils.asArray(target));
|
|
242
245
|
}
|
|
@@ -302,7 +305,8 @@ export class MetadataDiscovery {
|
|
|
302
305
|
if (validate) {
|
|
303
306
|
this.#validator.validateDiscovered(this.#discovered, this.#config.get('discovery'));
|
|
304
307
|
}
|
|
305
|
-
|
|
308
|
+
const foundNames = new Set(found.map(m => m.name));
|
|
309
|
+
return this.#discovered.filter(meta => foundNames.has(meta.className));
|
|
306
310
|
}
|
|
307
311
|
reset(entityName) {
|
|
308
312
|
const exists = this.#discovered.findIndex(m => m.class === entityName || m.className === Utils.className(entityName));
|
|
@@ -463,7 +467,8 @@ export class MetadataDiscovery {
|
|
|
463
467
|
}
|
|
464
468
|
}
|
|
465
469
|
initManyToOneFieldName(prop, name) {
|
|
466
|
-
|
|
470
|
+
// STI property renaming runs before `initRelation` populates `targetMeta`
|
|
471
|
+
const meta2 = prop.targetMeta ?? this.#metadata.getByClassName(prop.type);
|
|
467
472
|
const ret = [];
|
|
468
473
|
// with `targetKey` on a composite PK target, derive the FK field name from that property
|
|
469
474
|
// instead of the PKs (simple PK targets keep the PK based naming for backwards compatibility)
|
|
@@ -667,20 +672,47 @@ export class MetadataDiscovery {
|
|
|
667
672
|
}
|
|
668
673
|
return [];
|
|
669
674
|
}
|
|
670
|
-
|
|
675
|
+
/**
|
|
676
|
+
* Groups the relations of all entities by target class name in one pass, so `findReferencingProperties()`
|
|
677
|
+
* does not go over the relations of every entity for each entity. Lists keep the order of `metadata` and `relations`.
|
|
678
|
+
*/
|
|
679
|
+
indexReferencingProperties(metadata) {
|
|
680
|
+
const index = { byType: new Map(), withTargetKey: new Map() };
|
|
681
|
+
const add = (map, key, value) => {
|
|
682
|
+
const list = map.get(key);
|
|
683
|
+
if (list) {
|
|
684
|
+
list.push(value);
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
map.set(key, [value]);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
671
690
|
for (const meta2 of metadata) {
|
|
672
691
|
for (const prop2 of meta2.relations) {
|
|
673
|
-
if (prop2.kind !== ReferenceKind.SCALAR
|
|
674
|
-
|
|
692
|
+
if (prop2.kind !== ReferenceKind.SCALAR) {
|
|
693
|
+
add(index.byType, prop2.type, [meta2, prop2]);
|
|
675
694
|
}
|
|
676
|
-
if (prop2.targetKey
|
|
677
|
-
(prop2.type
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
695
|
+
if (prop2.targetKey) {
|
|
696
|
+
const targets = new Set([prop2.type, ...(prop2.polymorphTargets?.map(t => t.className) ?? [])]);
|
|
697
|
+
for (const target of targets) {
|
|
698
|
+
add(index.withTargetKey, target, prop2);
|
|
699
|
+
}
|
|
681
700
|
}
|
|
682
701
|
}
|
|
683
702
|
}
|
|
703
|
+
return index;
|
|
704
|
+
}
|
|
705
|
+
findReferencingProperties(meta, index) {
|
|
706
|
+
for (const [meta2, prop2] of index.byType.get(meta.className) ?? []) {
|
|
707
|
+
meta.referencingProperties.push({ meta: meta2, prop: prop2 });
|
|
708
|
+
}
|
|
709
|
+
for (const prop2 of index.withTargetKey.get(meta.className) ?? []) {
|
|
710
|
+
const targetKey = prop2.targetKey;
|
|
711
|
+
if (!(meta.primaryKeys.length === 1 && meta.primaryKeys[0] === targetKey) &&
|
|
712
|
+
!meta.root.targetKeys?.includes(targetKey)) {
|
|
713
|
+
(meta.root.targetKeys ??= []).push(targetKey);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
684
716
|
}
|
|
685
717
|
initFactoryField(meta, prop) {
|
|
686
718
|
['mappedBy', 'inversedBy', 'pivotEntity'].forEach(type => {
|
|
@@ -785,11 +817,14 @@ export class MetadataDiscovery {
|
|
|
785
817
|
else {
|
|
786
818
|
pivotMeta2.compositePK = true;
|
|
787
819
|
}
|
|
820
|
+
// STI entities share one table, so any m:n within the hierarchy is self-referencing, named after the root
|
|
821
|
+
const stiRoot = meta.root.inheritanceType === 'sti' ? meta.root : undefined;
|
|
822
|
+
const baseMeta = stiRoot ?? meta;
|
|
788
823
|
// handle self-referenced m:n with same default field names
|
|
789
|
-
if (meta.className === targetType &&
|
|
824
|
+
if ((meta.className === targetType || stiRoot === targetMeta.root) &&
|
|
790
825
|
prop.joinColumns.every((joinColumn, idx) => joinColumn === prop.inverseJoinColumns[idx])) {
|
|
791
826
|
// use tableName only when explicitly provided by user, otherwise use className for backwards compatibility
|
|
792
|
-
const baseName = this.isExplicitTableName(
|
|
827
|
+
const baseName = this.isExplicitTableName(baseMeta) ? baseMeta.tableName : baseMeta.className;
|
|
793
828
|
prop.joinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_1', name, meta.compositePK));
|
|
794
829
|
prop.inverseJoinColumns = prop.referencedColumnNames.map(name => this.#namingStrategy.joinKeyColumnName(baseName + '_2', name, meta.compositePK));
|
|
795
830
|
if (prop.inversedBy) {
|
|
@@ -797,13 +832,18 @@ export class MetadataDiscovery {
|
|
|
797
832
|
prop2.inverseJoinColumns = prop.joinColumns;
|
|
798
833
|
prop2.joinColumns = prop.inverseJoinColumns;
|
|
799
834
|
}
|
|
800
|
-
// propagate updated joinColumns to all
|
|
801
|
-
for (const childMeta of this.#discovered.filter(m => m.root ===
|
|
835
|
+
// propagate updated joinColumns to all entities of the hierarchy that inherit this property or its inverse (STI)
|
|
836
|
+
for (const childMeta of this.#discovered.filter(m => m.root === baseMeta)) {
|
|
802
837
|
const childProp = childMeta.properties[prop.name];
|
|
803
838
|
if (childProp) {
|
|
804
839
|
childProp.joinColumns = prop.joinColumns;
|
|
805
840
|
childProp.inverseJoinColumns = prop.inverseJoinColumns;
|
|
806
841
|
}
|
|
842
|
+
const childProp2 = prop.inversedBy ? childMeta.properties[prop.inversedBy] : undefined;
|
|
843
|
+
if (childProp2) {
|
|
844
|
+
childProp2.inverseJoinColumns = prop.joinColumns;
|
|
845
|
+
childProp2.joinColumns = prop.inverseJoinColumns;
|
|
846
|
+
}
|
|
807
847
|
}
|
|
808
848
|
}
|
|
809
849
|
// Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
|
|
@@ -1281,16 +1321,37 @@ export class MetadataDiscovery {
|
|
|
1281
1321
|
// the prefix of the parent cannot be a boolean; it already passed here
|
|
1282
1322
|
const prefix = this.getPrefix(embeddedProp, parentProperty);
|
|
1283
1323
|
const glue = object ? '~' : '_';
|
|
1284
|
-
|
|
1285
|
-
|
|
1324
|
+
const props = Object.values(embeddable.properties);
|
|
1325
|
+
// variants can declare a same-named property of a different kind, the virtual entity lacks it when the variants are shared with another union
|
|
1326
|
+
for (const prop of embeddable.polymorphs?.flatMap(m => Object.values(m.properties)) ?? []) {
|
|
1327
|
+
const same = props.find(p => p.name === prop.name && Utils.isSameKind(p, prop));
|
|
1328
|
+
if (!same && !prop.embedded) {
|
|
1329
|
+
props.push({ ...prop }); // a copy, the variant's metadata is shared with its other owners
|
|
1330
|
+
}
|
|
1331
|
+
else if (same && same.kind !== ReferenceKind.SCALAR && same.type !== prop.type) {
|
|
1332
|
+
// a column shared by relations to different targets cannot have a FK constraint
|
|
1333
|
+
same.createForeignKeyConstraint = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
for (const prop of props) {
|
|
1337
|
+
const sameName = props.filter(p => p.name === prop.name);
|
|
1338
|
+
// same-kind variants of a conflicting property share one owner property, as they share the column
|
|
1339
|
+
const first = sameName.find(p => Utils.isSameKind(p, prop));
|
|
1340
|
+
const conflict = first !== sameName[0];
|
|
1341
|
+
const name = (embeddedProp.embeddedPath?.join(glue) ?? embeddedProp.fieldNames[0] + glue) +
|
|
1342
|
+
prop.name +
|
|
1343
|
+
(conflict ? `_${props.indexOf(first)}` : '');
|
|
1286
1344
|
meta.properties[name] = Utils.copy(prop);
|
|
1287
1345
|
meta.properties[name].name = name;
|
|
1346
|
+
// STI column tracking between the variants does not apply to the owner's columns
|
|
1347
|
+
delete meta.properties[name].stiFieldNames;
|
|
1288
1348
|
meta.properties[name].embedded = [embeddedProp.name, prop.name];
|
|
1289
1349
|
meta.propertyOrder.set(name, (order += 0.01));
|
|
1290
|
-
embeddedProp.embeddedProps[prop.name] = meta.properties[name];
|
|
1350
|
+
embeddedProp.embeddedProps[conflict ? name : prop.name] = meta.properties[name];
|
|
1291
1351
|
meta.properties[name].persist ??= embeddedProp.persist;
|
|
1292
1352
|
const refInArray = array && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.owner;
|
|
1293
|
-
|
|
1353
|
+
// each variant fills only its own column of a conflicting property
|
|
1354
|
+
if (embeddedProp.nullable || refInArray || sameName.some(p => !Utils.isSameKind(p, prop))) {
|
|
1294
1355
|
meta.properties[name].nullable = true;
|
|
1295
1356
|
}
|
|
1296
1357
|
// polymorphic relations derive their column names from the discriminator, so prefix it too
|
|
@@ -1463,6 +1524,13 @@ export class MetadataDiscovery {
|
|
|
1463
1524
|
rootProp.stiFieldNameMap[meta.discriminatorValue] = prop.fieldNames[0];
|
|
1464
1525
|
// subtypes that only differ in type share the column
|
|
1465
1526
|
rootProp.stiFieldNames = Utils.unique([...rootProp.stiFieldNames, ...prop.fieldNames]);
|
|
1527
|
+
// a column shared by relations to different targets cannot have a FK constraint
|
|
1528
|
+
for (const p of Object.values(meta.root.properties)) {
|
|
1529
|
+
const shared = p.type !== newProp.type && compareArrays(p.fieldNames ?? [], newProp.fieldNames);
|
|
1530
|
+
if (shared && p.kind !== ReferenceKind.SCALAR && Utils.isSameKind(p, newProp)) {
|
|
1531
|
+
p.createForeignKeyConstraint = newProp.createForeignKeyConstraint = false;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1466
1534
|
newProp.nullable = true;
|
|
1467
1535
|
newProp.name = name;
|
|
1468
1536
|
newProp.hydrate = false;
|
|
@@ -1507,6 +1575,9 @@ export class MetadataDiscovery {
|
|
|
1507
1575
|
if (isInverseSideRelation && rootProp && rootProp.type !== prop.type) {
|
|
1508
1576
|
return;
|
|
1509
1577
|
}
|
|
1578
|
+
// a sibling matching the root property must not drop the column variants of already renamed siblings
|
|
1579
|
+
newProp.stiFieldNames ??= rootProp?.stiFieldNames;
|
|
1580
|
+
newProp.stiFieldNameMap ??= rootProp?.stiFieldNameMap;
|
|
1510
1581
|
meta.root.addProperty(newProp);
|
|
1511
1582
|
});
|
|
1512
1583
|
meta.tableName = meta.root.tableName;
|
|
@@ -1627,7 +1698,7 @@ export class MetadataDiscovery {
|
|
|
1627
1698
|
if (meta.inheritanceType !== 'tpt') {
|
|
1628
1699
|
return;
|
|
1629
1700
|
}
|
|
1630
|
-
const belongsToTable = (prop) => prop.persist !== false || prop.primary;
|
|
1701
|
+
const belongsToTable = (prop) => prop.persist !== false || prop.primary || !!prop.formula;
|
|
1631
1702
|
// Use meta.properties (object) since meta.props (array) may not be populated yet
|
|
1632
1703
|
const allProps = Object.values(meta.properties);
|
|
1633
1704
|
if (!meta.tptParent) {
|
|
@@ -1873,9 +1944,9 @@ export class MetadataDiscovery {
|
|
|
1873
1944
|
// guard preserves the legacy convention where JSON defaults are passed pre-stringified.
|
|
1874
1945
|
// Array defaults are converted unconditionally (e.g. `default: []` on a JSON-backed embedded array).
|
|
1875
1946
|
if (prop.customType && (Array.isArray(prop.default) || prop.customType.compareAsType() !== 'any')) {
|
|
1876
|
-
val = prop.customType.convertToDatabaseValue(prop.default, this.#platform);
|
|
1947
|
+
val = prop.customType.convertToDatabaseValue(prop.default, this.#platform, { mode: 'discovery' });
|
|
1877
1948
|
}
|
|
1878
|
-
prop.defaultRaw = typeof val === 'string' ?
|
|
1949
|
+
prop.defaultRaw = typeof val === 'string' ? this.#platform.quoteDefaultValue(val) : '' + val;
|
|
1879
1950
|
}
|
|
1880
1951
|
inferTypeFromDefault(prop) {
|
|
1881
1952
|
if ((prop.defaultRaw == null && prop.default == null) || prop.type !== 'any') {
|
|
@@ -9,6 +9,7 @@ export declare class MetadataStorage {
|
|
|
9
9
|
/** Returns the global metadata dictionary, or a specific entry by entity name and path (keyed by the class reference when `target` is provided). */
|
|
10
10
|
static getMetadata(): Dictionary<EntityMetadata>;
|
|
11
11
|
static getMetadata<T = any>(entity: string, path: string, target?: EntityCtor): EntityMetadata<T>;
|
|
12
|
+
private static hashPath;
|
|
12
13
|
/** Checks whether an entity with the given class name exists in the global metadata. */
|
|
13
14
|
static isKnownEntity(name: string): boolean;
|
|
14
15
|
/** Clears all entries from the global metadata registry. */
|
|
@@ -13,6 +13,10 @@ export class MetadataStorage {
|
|
|
13
13
|
static PATH_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.PATH_SYMBOL');
|
|
14
14
|
static META_SYMBOL = Symbol.for('@mikro-orm/core/MetadataStorage.META_SYMBOL');
|
|
15
15
|
static #metadata = getGlobalStorage('metadata');
|
|
16
|
+
/** Lives in the global storage next to the registry, so it stays in sync when several copies of this module share it (e.g. after `vi.resetModules()`). */
|
|
17
|
+
static #classNames = getGlobalStorage('metadata-class-names');
|
|
18
|
+
/** Hashes of entity file paths, decorators call `getMetadata()` with the same path for every decorated member. */
|
|
19
|
+
static #pathHashes = new Map();
|
|
16
20
|
#metadataMap = new Map();
|
|
17
21
|
#idMap;
|
|
18
22
|
#classNameMap;
|
|
@@ -31,7 +35,7 @@ export class MetadataStorage {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
static getMetadata(entity, path, target) {
|
|
34
|
-
const key = entity && path ? entity + '-' +
|
|
38
|
+
const key = entity && path ? entity + '-' + MetadataStorage.hashPath(path) : null;
|
|
35
39
|
// Key the registry by the class reference when available, so two classes minified
|
|
36
40
|
// to the same mangled name don't collide on the `className-path` key.
|
|
37
41
|
if (key && target) {
|
|
@@ -43,23 +47,36 @@ export class MetadataStorage {
|
|
|
43
47
|
}
|
|
44
48
|
// Keep the name-keyed entry in sync, the class-keyed metadata survives `MetadataStorage.clear()`.
|
|
45
49
|
MetadataStorage.#metadata[key] = target[MetadataStorage.META_SYMBOL];
|
|
50
|
+
MetadataStorage.#classNames.names?.add(target[MetadataStorage.META_SYMBOL].className);
|
|
46
51
|
return target[MetadataStorage.META_SYMBOL];
|
|
47
52
|
}
|
|
48
53
|
if (key && !MetadataStorage.#metadata[key]) {
|
|
49
|
-
|
|
54
|
+
const meta = new EntityMetadata({ className: entity, path });
|
|
55
|
+
MetadataStorage.#metadata[key] = meta;
|
|
56
|
+
MetadataStorage.#classNames.names?.add(meta.className);
|
|
50
57
|
}
|
|
51
58
|
if (key) {
|
|
52
59
|
return MetadataStorage.#metadata[key];
|
|
53
60
|
}
|
|
54
61
|
return MetadataStorage.#metadata;
|
|
55
62
|
}
|
|
63
|
+
static hashPath(path) {
|
|
64
|
+
let hash = MetadataStorage.#pathHashes.get(path);
|
|
65
|
+
if (hash === undefined) {
|
|
66
|
+
hash = Utils.hash(path);
|
|
67
|
+
MetadataStorage.#pathHashes.set(path, hash);
|
|
68
|
+
}
|
|
69
|
+
return hash;
|
|
70
|
+
}
|
|
56
71
|
/** Checks whether an entity with the given class name exists in the global metadata. */
|
|
57
72
|
static isKnownEntity(name) {
|
|
58
|
-
|
|
73
|
+
this.#classNames.names ??= new Set(Object.values(this.#metadata).map(meta => meta.className));
|
|
74
|
+
return this.#classNames.names.has(name);
|
|
59
75
|
}
|
|
60
76
|
/** Clears all entries from the global metadata registry. */
|
|
61
77
|
static clear() {
|
|
62
78
|
Object.keys(this.#metadata).forEach(k => delete this.#metadata[k]);
|
|
79
|
+
delete this.#classNames.names;
|
|
63
80
|
}
|
|
64
81
|
/** Returns the map of all registered entity metadata. */
|
|
65
82
|
getAll() {
|
|
@@ -118,12 +118,13 @@ export class MetadataValidator {
|
|
|
118
118
|
return Utils.className(p);
|
|
119
119
|
};
|
|
120
120
|
const pivotProps = new Map();
|
|
121
|
+
const discoveredNames = new Set(discovered.map(m => m.className));
|
|
121
122
|
// check for not discovered entities
|
|
122
123
|
discovered.forEach(meta => Object.values(meta.properties).forEach(prop => {
|
|
123
124
|
if (prop.kind !== ReferenceKind.SCALAR &&
|
|
124
125
|
!unwrap(prop.type)
|
|
125
126
|
.split(/ ?\| ?/)
|
|
126
|
-
.every(type =>
|
|
127
|
+
.every(type => discoveredNames.has(type))) {
|
|
127
128
|
throw MetadataError.fromUnknownEntity(prop.type, `${meta.className}.${prop.name}`);
|
|
128
129
|
}
|
|
129
130
|
if (prop.pivotEntity) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.3.0-dev.
|
|
3
|
+
"version": "7.3.0-dev.18",
|
|
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/platforms/Platform.d.ts
CHANGED
|
@@ -204,6 +204,8 @@ export declare abstract class Platform {
|
|
|
204
204
|
supportsUnionWhere(): boolean;
|
|
205
205
|
/** Returns the SQL type declaration used for array storage. */
|
|
206
206
|
getArrayDeclarationSQL(): string;
|
|
207
|
+
/** Whether array columns support the `&&` (overlap) operator. */
|
|
208
|
+
supportsArrayOverlap(): boolean;
|
|
207
209
|
/** Serializes a string array into its database storage format. */
|
|
208
210
|
marshallArray(values: string[]): string;
|
|
209
211
|
/** Deserializes a database-stored array string back into a string array. */
|
|
@@ -274,6 +276,8 @@ export declare abstract class Platform {
|
|
|
274
276
|
}, quote?: string): string;
|
|
275
277
|
/** Quotes a literal value for safe embedding in SQL. */
|
|
276
278
|
quoteValue(value: any): string;
|
|
279
|
+
/** Quotes a string default value for DDL; dialects that escape with backslashes override this. */
|
|
280
|
+
quoteDefaultValue(value: string): string;
|
|
277
281
|
escape(value: any): string;
|
|
278
282
|
/** Replaces `?` placeholders in SQL with quoted parameter values. */
|
|
279
283
|
formatQuery(sql: string, params: readonly any[]): string;
|
package/platforms/Platform.js
CHANGED
|
@@ -360,6 +360,10 @@ export class Platform {
|
|
|
360
360
|
getArrayDeclarationSQL() {
|
|
361
361
|
return 'text';
|
|
362
362
|
}
|
|
363
|
+
/** Whether array columns support the `&&` (overlap) operator. */
|
|
364
|
+
supportsArrayOverlap() {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
363
367
|
/** Serializes a string array into its database storage format. */
|
|
364
368
|
marshallArray(values) {
|
|
365
369
|
return values.map(v => (/[,",\\]/.test(v) ? JSON.stringify(v) : v)).join(',');
|
|
@@ -573,6 +577,10 @@ export class Platform {
|
|
|
573
577
|
quoteValue(value) {
|
|
574
578
|
return value;
|
|
575
579
|
}
|
|
580
|
+
/** Quotes a string default value for DDL; dialects that escape with backslashes override this. */
|
|
581
|
+
quoteDefaultValue(value) {
|
|
582
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
583
|
+
}
|
|
576
584
|
/* v8 ignore next */
|
|
577
585
|
escape(value) {
|
|
578
586
|
return value;
|
|
@@ -4,20 +4,50 @@ import { ReferenceKind } from '../enums.js';
|
|
|
4
4
|
import { Reference } from '../entity/Reference.js';
|
|
5
5
|
import { SerializationContext } from './SerializationContext.js';
|
|
6
6
|
import { isRaw } from '../utils/RawQueryFragment.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// Keyed by option objects created for a single `serialize()` call, so caller-owned options are never cached.
|
|
8
|
+
const preparedOptions = new WeakMap();
|
|
9
|
+
function createPathLookup(items) {
|
|
10
|
+
const heads = new Set();
|
|
11
|
+
let all = false;
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
const dot = item.indexOf('.');
|
|
14
|
+
heads.add(dot === -1 ? item : item.substring(0, dot));
|
|
15
|
+
all ||= item === '*';
|
|
16
|
+
}
|
|
17
|
+
return { all, heads };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Copies caller options once per `serialize()` call, so the derived child options and path lookups can be cached
|
|
21
|
+
* for every entity of the call without ever caching an options object the caller might mutate later.
|
|
22
|
+
*/
|
|
23
|
+
function prepareOptions(options) {
|
|
24
|
+
if (preparedOptions.has(options)) {
|
|
25
|
+
return options;
|
|
26
|
+
}
|
|
27
|
+
const prepared = {
|
|
28
|
+
...options,
|
|
29
|
+
populate: options.populate ? Utils.asArray(options.populate) : options.populate,
|
|
30
|
+
};
|
|
31
|
+
preparedOptions.set(prepared, { children: new Map() });
|
|
32
|
+
return prepared;
|
|
33
|
+
}
|
|
34
|
+
function pathLookup(options, key) {
|
|
35
|
+
return (preparedOptions.get(options)[key] ??= createPathLookup(options[key]));
|
|
36
|
+
}
|
|
37
|
+
/** Returns true when any entry in `options[key]` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
|
|
38
|
+
function matchesPath(options, key, propName) {
|
|
39
|
+
const lookup = pathLookup(options, key);
|
|
40
|
+
return lookup.all || lookup.heads.has(propName);
|
|
10
41
|
}
|
|
11
42
|
function isVisible(meta, propName, options) {
|
|
12
43
|
const prop = meta.properties[propName];
|
|
13
44
|
if (options.groups && prop?.groups) {
|
|
14
45
|
return prop.groups.some(g => options.groups.includes(g));
|
|
15
46
|
}
|
|
16
|
-
if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options
|
|
47
|
+
if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options, 'fields', propName)) {
|
|
17
48
|
return false;
|
|
18
49
|
}
|
|
19
|
-
if (Array.isArray(options.populate) &&
|
|
20
|
-
options.populate.find(item => item === propName || item.startsWith(propName + '.'))) {
|
|
50
|
+
if (Array.isArray(options.populate) && pathLookup(options, 'populate').heads.has(propName)) {
|
|
21
51
|
return true;
|
|
22
52
|
}
|
|
23
53
|
if (options.exclude?.find(item => item === propName)) {
|
|
@@ -30,7 +60,7 @@ function isVisible(meta, propName, options) {
|
|
|
30
60
|
function isPopulated(propName, options) {
|
|
31
61
|
if (typeof options.populate !== 'boolean' &&
|
|
32
62
|
Array.isArray(options.populate) &&
|
|
33
|
-
matchesPath(options
|
|
63
|
+
matchesPath(options, 'populate', propName)) {
|
|
34
64
|
return true;
|
|
35
65
|
}
|
|
36
66
|
if (typeof options.populate === 'boolean') {
|
|
@@ -45,10 +75,10 @@ export class EntitySerializer {
|
|
|
45
75
|
const wrapped = helper(entity);
|
|
46
76
|
const meta = wrapped.__meta;
|
|
47
77
|
let contextCreated = false;
|
|
78
|
+
options = prepareOptions(options);
|
|
48
79
|
if (!wrapped.__serializationContext.root) {
|
|
49
80
|
const root = new SerializationContext();
|
|
50
81
|
SerializationContext.propagate(root, entity, (meta, prop) => meta.properties[prop]?.kind !== ReferenceKind.SCALAR);
|
|
51
|
-
options.populate = (options.populate ? Utils.asArray(options.populate) : options.populate);
|
|
52
82
|
contextCreated = true;
|
|
53
83
|
}
|
|
54
84
|
const root = wrapped.__serializationContext.root;
|
|
@@ -145,7 +175,8 @@ export class EntitySerializer {
|
|
|
145
175
|
if (entity[prop] instanceof Function) {
|
|
146
176
|
const returnValue = entity[prop]();
|
|
147
177
|
if (!options.ignoreSerializers && serializer) {
|
|
148
|
-
|
|
178
|
+
// copy, so a serializer mutating its options cannot leak into the cached child options
|
|
179
|
+
return serializer(returnValue, { ...this.extractChildOptions(options, prop) });
|
|
149
180
|
}
|
|
150
181
|
return returnValue;
|
|
151
182
|
}
|
|
@@ -186,7 +217,12 @@ export class EntitySerializer {
|
|
|
186
217
|
return prop.customType.toJSON(value, platform);
|
|
187
218
|
}
|
|
188
219
|
static extractChildOptions(options, prop) {
|
|
189
|
-
|
|
220
|
+
const prepared = preparedOptions.get(options);
|
|
221
|
+
const cached = prepared.children.get(prop);
|
|
222
|
+
if (cached) {
|
|
223
|
+
return cached;
|
|
224
|
+
}
|
|
225
|
+
const child = {
|
|
190
226
|
...options,
|
|
191
227
|
populate: Array.isArray(options.populate)
|
|
192
228
|
? Utils.extractChildElements(options.populate, prop, '*')
|
|
@@ -194,6 +230,9 @@ export class EntitySerializer {
|
|
|
194
230
|
exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
|
|
195
231
|
fields: Array.isArray(options.fields) ? this.extractChildFields(options.fields, prop) : options.fields,
|
|
196
232
|
};
|
|
233
|
+
prepared.children.set(prop, child);
|
|
234
|
+
preparedOptions.set(child, { children: new Map() });
|
|
235
|
+
return child;
|
|
197
236
|
}
|
|
198
237
|
/**
|
|
199
238
|
* Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
|
|
@@ -269,8 +308,9 @@ export class EntitySerializer {
|
|
|
269
308
|
* ```
|
|
270
309
|
*/
|
|
271
310
|
export function serialize(entities, options) {
|
|
311
|
+
const prepared = prepareOptions(options ?? {});
|
|
272
312
|
if (Array.isArray(entities)) {
|
|
273
|
-
return entities.map(e => EntitySerializer.serialize(e,
|
|
313
|
+
return entities.map(e => EntitySerializer.serialize(e, prepared));
|
|
274
314
|
}
|
|
275
|
-
return EntitySerializer.serialize(entities,
|
|
315
|
+
return EntitySerializer.serialize(entities, prepared);
|
|
276
316
|
}
|
package/types/DoubleType.d.ts
CHANGED
|
@@ -2,10 +2,14 @@ import { Type } from './Type.js';
|
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
3
|
import type { EntityProperty } from '../typings.js';
|
|
4
4
|
/**
|
|
5
|
-
* Type that maps an SQL DOUBLE to a JS
|
|
5
|
+
* Type that maps an SQL DOUBLE to a JS number (default) or string.
|
|
6
6
|
*/
|
|
7
|
-
export declare class DoubleType extends
|
|
8
|
-
|
|
7
|
+
export declare class DoubleType<Mode extends 'number' | 'string' = 'number'> extends Type<JSTypeByMode<Mode>, string> {
|
|
8
|
+
mode?: Mode | undefined;
|
|
9
|
+
constructor(mode?: Mode | undefined);
|
|
10
|
+
convertToJSValue(value: string): JSTypeByMode<Mode>;
|
|
9
11
|
getColumnType(prop: EntityProperty, platform: Platform): string;
|
|
10
12
|
compareAsType(): string;
|
|
11
13
|
}
|
|
14
|
+
type JSTypeByMode<Mode extends 'number' | 'string'> = Mode extends 'number' ? number : string;
|
|
15
|
+
export {};
|
package/types/DoubleType.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { Type } from './Type.js';
|
|
2
2
|
/**
|
|
3
|
-
* Type that maps an SQL DOUBLE to a JS
|
|
3
|
+
* Type that maps an SQL DOUBLE to a JS number (default) or string.
|
|
4
4
|
*/
|
|
5
5
|
export class DoubleType extends Type {
|
|
6
|
+
mode;
|
|
7
|
+
constructor(mode) {
|
|
8
|
+
super();
|
|
9
|
+
this.mode = mode;
|
|
10
|
+
}
|
|
6
11
|
/* v8 ignore next */
|
|
7
12
|
convertToJSValue(value) {
|
|
8
|
-
if (this.prop?.runtimeType === 'number') {
|
|
13
|
+
if ((this.mode ?? this.prop?.runtimeType) === 'number') {
|
|
9
14
|
return +value;
|
|
10
15
|
}
|
|
11
16
|
return String(value);
|
|
@@ -14,6 +19,6 @@ export class DoubleType extends Type {
|
|
|
14
19
|
return platform.getDoubleDeclarationSQL();
|
|
15
20
|
}
|
|
16
21
|
compareAsType() {
|
|
17
|
-
return this.prop?.runtimeType ?? 'number';
|
|
22
|
+
return this.mode ?? this.prop?.runtimeType ?? 'number';
|
|
18
23
|
}
|
|
19
24
|
}
|
package/typings.d.ts
CHANGED
|
@@ -358,12 +358,15 @@ type ElemMatchCondition<T extends Record<string, any>> = {
|
|
|
358
358
|
$and?: ElemMatchCondition<T>[];
|
|
359
359
|
$not?: ElemMatchCondition<T>;
|
|
360
360
|
};
|
|
361
|
-
type
|
|
361
|
+
type ArrayFilter<T> = T extends readonly (infer E)[] ? [E] extends [Scalar] ? {
|
|
362
|
+
$in?: readonly (readonly ExpandScalar<E>[])[];
|
|
363
|
+
$nin?: readonly (readonly ExpandScalar<E>[])[];
|
|
364
|
+
} : E extends Record<string, any> ? {
|
|
362
365
|
$elemMatch: ElemMatchCondition<E>;
|
|
363
366
|
} : never : never;
|
|
364
367
|
/** Object form of a filter query, mapping entity keys to their filter conditions. */
|
|
365
368
|
export type FilterObject<T> = {
|
|
366
|
-
-readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> |
|
|
369
|
+
-readonly [K in EntityKey<T>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ArrayFilter<FilterObjectProp<T, K>> | null;
|
|
367
370
|
};
|
|
368
371
|
/** Recursively expands a type into its `FilterQuery` form for nested object filtering. */
|
|
369
372
|
export type ExpandQuery<T> = T extends object ? (T extends Scalar ? never : FilterQuery<T>) : FilterValue<T>;
|
|
@@ -383,7 +386,7 @@ export type FilterQuery<T> = ObjectQuery<T> | NonNullable<ExpandScalar<Primary<T
|
|
|
383
386
|
* Used when `using` option is set in `FindOptions` to enforce type-safe index usage.
|
|
384
387
|
*/
|
|
385
388
|
export type IndexFilterQuery<T, Using extends string> = [Using] extends [never] ? FilterQuery<T> : (OperatorMap<T> & {
|
|
386
|
-
-readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> |
|
|
389
|
+
-readonly [K in Extract<EntityKey<T>, IndexColumns<T, Using>>]?: ExpandQuery<ExpandProperty<FilterObjectProp<T, K>>> | ExpandQueryMerged<ExpandProperty<FilterObjectProp<T, K>>> | FilterValue<ExpandProperty<FilterObjectProp<T, K>>> | ArrayFilter<FilterObjectProp<T, K>> | null;
|
|
387
390
|
}) | NonNullable<ExpandScalar<Primary<T>>> | IndexFilterQuery<T, Using>[];
|
|
388
391
|
/** Replaces `where` and `using` on an options type with index-aware variants when `Using` is specified. */
|
|
389
392
|
export type WithUsingOptions<Opts, Entity, Using extends string> = Omit<Opts, 'where' | 'using'> & {
|
|
@@ -564,6 +567,13 @@ export declare namespace LazyRef {
|
|
|
564
567
|
[__lazyRef]?: (arg: T) => T;
|
|
565
568
|
}
|
|
566
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* The brand's only key is optional, so an object type without named properties (like `Record<string, number>`)
|
|
572
|
+
* extends `LazyRef.Brand<infer U>` too, with `U` inferred as `unknown`. `LazyRef<T>` needs `T extends object`,
|
|
573
|
+
* so an `unknown` target means the type is not a `LazyRef` and resolves to `Fallback`. `any` stays `any`.
|
|
574
|
+
* Only checked once `unknown extends U` holds, so a real `LazyRef` skips it.
|
|
575
|
+
*/
|
|
576
|
+
type NotLazyRef<U, Fallback> = IsAny<U> extends true ? U : Fallback;
|
|
567
577
|
/** Alias for `Reference<T> & { id: number }` (see {@apilink Ref}). */
|
|
568
578
|
export type EntityRef<T extends object> = true extends IsUnknown<PrimaryProperty<T>> ? Reference<T> : IsAny<T> extends true ? Reference<T> : {
|
|
569
579
|
[K in PrimaryProperty<T> & keyof T]: T[K];
|
|
@@ -1620,7 +1630,8 @@ export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
|
|
|
1620
1630
|
/** Extracts the element type from an array type. */
|
|
1621
1631
|
export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;
|
|
1622
1632
|
/** Unwraps a property type from its wrapper (Reference, Collection, or array) to the inner entity type. */
|
|
1623
|
-
export type ExpandProperty<T> =
|
|
1633
|
+
export type ExpandProperty<T> = ExpandPropertyOf<T, T>;
|
|
1634
|
+
type ExpandPropertyOf<T, R> = T extends LazyRef.Brand<infer U> ? unknown extends U ? NotLazyRef<U, NonNullable<R>> : NonNullable<U> : T extends ReferenceShape<infer U> ? NonNullable<U> : T extends CollectionShape<infer U> ? NonNullable<U> : T extends (infer U)[] ? NonNullable<U> : NonNullable<T>;
|
|
1624
1635
|
type LoadedLoadable<T, E extends object> = T extends CollectionShape ? LoadedCollection<E> : T extends ScalarReference<infer U> ? LoadedScalarReference<U> : T extends LazyRef.Brand<any> ? E : T extends ReferenceShape ? T & LoadedReference<E> : T extends Scalar ? T : T extends (infer U)[] ? U extends Scalar ? T : E[] : E;
|
|
1625
1636
|
type IsTrue<T> = IsNever<T> extends true ? false : T extends boolean ? (T extends true ? true : false) : false;
|
|
1626
1637
|
type StringLiteral<T> = T extends string ? (string extends T ? never : T) : never;
|
|
@@ -1655,7 +1666,12 @@ export type MergeSelected<T, U, F extends string> = IsLoadedType<T> extends true
|
|
|
1655
1666
|
export type MergeLoaded<T, U, P extends string, F extends string, E extends string, R extends boolean = false> = IsLoadedType<T> extends true ? T & Loaded<U, P, F, E> : Loaded<T, P, F, E>;
|
|
1656
1667
|
/** Extracts the nullability modifiers (`null`, `undefined`, or both) from a type `T`. */
|
|
1657
1668
|
export type AddOptional<T> = undefined | null extends T ? null | undefined : null extends T ? null : undefined extends T ? undefined : never;
|
|
1658
|
-
|
|
1669
|
+
/**
|
|
1670
|
+
* Like {@link ExtractTypeWithLazyRef}, but a record type that matched `LazyRef.Brand` resolves to itself (see
|
|
1671
|
+
* {@link NotLazyRef}). `AutoPath` keeps the unguarded version, as it is on its hot path.
|
|
1672
|
+
*/
|
|
1673
|
+
type ExtractLoadedType<T> = T extends LazyRef.Brand<infer U> ? (unknown extends U ? NotLazyRef<U, ExtractType<T>> : U) : ExtractType<T>;
|
|
1674
|
+
type LoadedProp<T, L extends string = never, F extends string = '*', E extends string = never> = LoadedLoadable<T, Loaded<ExtractLoadedType<T>, L, F, E>>;
|
|
1659
1675
|
/** Extracts the eager-loaded property names declared via `[EagerProps]` as a string union. */
|
|
1660
1676
|
export type AddEager<T> = ExtractEagerProps<T> & string;
|
|
1661
1677
|
/** Combines an explicit populate hint `L` with the entity's eagerly loaded properties. */
|
package/typings.js
CHANGED
|
@@ -304,8 +304,11 @@ export class EntityMetadata {
|
|
|
304
304
|
}, { __gettersDefined: { value: true, enumerable: false } });
|
|
305
305
|
}
|
|
306
306
|
initIndexes(prop) {
|
|
307
|
-
|
|
308
|
-
const
|
|
307
|
+
// `prop.index`/`prop.unique` keep only the name, so an index with any other option must stay in `meta.indexes`
|
|
308
|
+
const isSimple = (index) => index.properties === prop.name &&
|
|
309
|
+
Object.entries(index).every(([key, value]) => key === 'name' || key === 'properties' || value == null);
|
|
310
|
+
const simpleIndex = this.indexes.find(isSimple);
|
|
311
|
+
const simpleUnique = this.uniques.find(isSimple);
|
|
309
312
|
const owner = prop.kind === ReferenceKind.MANY_TO_ONE;
|
|
310
313
|
if (!prop.index && simpleIndex) {
|
|
311
314
|
Utils.defaultValue(simpleIndex, 'name', true);
|