@mikro-orm/core 7.1.0-dev.0 → 7.1.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.
- package/EntityManager.d.ts +63 -11
- package/EntityManager.js +163 -18
- package/MikroORM.d.ts +3 -3
- package/drivers/DatabaseDriver.d.ts +1 -0
- package/drivers/DatabaseDriver.js +3 -0
- package/drivers/IDatabaseDriver.d.ts +30 -1
- package/entity/BaseEntity.d.ts +2 -2
- package/entity/Collection.d.ts +6 -3
- package/entity/Collection.js +15 -4
- package/entity/EntityFactory.js +20 -1
- package/entity/EntityLoader.d.ts +3 -1
- package/entity/EntityLoader.js +39 -16
- package/entity/EntityRepository.d.ts +27 -9
- package/entity/EntityRepository.js +12 -0
- package/entity/WrappedEntity.d.ts +2 -2
- package/entity/defineEntity.d.ts +40 -9
- package/entity/defineEntity.js +0 -6
- package/entity/utils.js +4 -5
- package/errors.d.ts +2 -0
- package/errors.js +4 -0
- package/index.d.ts +2 -2
- package/index.js +1 -1
- package/metadata/MetadataDiscovery.d.ts +11 -0
- package/metadata/MetadataDiscovery.js +127 -9
- package/metadata/MetadataValidator.js +9 -0
- package/metadata/types.d.ts +10 -2
- package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
- package/naming-strategy/NamingStrategy.d.ts +1 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +5 -0
- package/platforms/Platform.js +7 -0
- package/serialization/EntitySerializer.d.ts +18 -6
- package/serialization/EntitySerializer.js +31 -5
- package/typings.d.ts +116 -6
- package/typings.js +19 -2
- package/utils/AbstractMigrator.d.ts +19 -1
- package/utils/AbstractMigrator.js +219 -0
- package/utils/Configuration.d.ts +10 -3
- package/utils/Configuration.js +1 -0
- package/utils/DataloaderUtils.d.ts +10 -1
- package/utils/DataloaderUtils.js +78 -0
- package/utils/QueryHelper.d.ts +16 -0
- package/utils/QueryHelper.js +15 -0
- package/utils/Utils.d.ts +1 -1
- package/utils/Utils.js +1 -1
- package/utils/fs-utils.d.ts +2 -0
- package/utils/fs-utils.js +7 -1
|
@@ -159,6 +159,7 @@ export class MetadataDiscovery {
|
|
|
159
159
|
forEachProp((m, p) => this.initGeneratedColumn(m, p));
|
|
160
160
|
filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
|
|
161
161
|
filtered.forEach(meta => this.initCheckConstraints(meta));
|
|
162
|
+
filtered.forEach(meta => this.initTriggers(meta));
|
|
162
163
|
forEachProp((_m, p) => {
|
|
163
164
|
this.initDefaultValue(p);
|
|
164
165
|
this.inferTypeFromDefault(p);
|
|
@@ -479,17 +480,28 @@ export class MetadataDiscovery {
|
|
|
479
480
|
prop.polymorphic = prop2.polymorphic;
|
|
480
481
|
prop.discriminator = prop2.discriminator;
|
|
481
482
|
prop.discriminatorColumn = prop2.discriminatorColumn;
|
|
482
|
-
|
|
483
|
+
// For a union-target pivot each inverse side sits on one specific target class, so its
|
|
484
|
+
// discriminator value is that class's tableName. For Rails-style, prop2 has a single fixed value.
|
|
485
|
+
prop.discriminatorValue = QueryHelper.isUnionTargetPolymorphic(prop2) ? meta.tableName : prop2.discriminatorValue;
|
|
483
486
|
}
|
|
484
487
|
prop.referencedColumnNames ??= Utils.flatten(meta.primaryKeys.map(primaryKey => meta.properties[primaryKey].fieldNames));
|
|
485
|
-
//
|
|
486
|
-
|
|
488
|
+
// Union-target polymorphic M:N: owner side is fixed (real FK), target side uses discriminator-derived names.
|
|
489
|
+
const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
|
|
490
|
+
if (prop.polymorphic && prop.discriminator && !isUnionTargetMN) {
|
|
491
|
+
// Rails-style: owner side is polymorphic, uses discriminator base name (e.g. taggable_id instead of post_id)
|
|
487
492
|
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, referencedColumnName, prop.referencedColumnNames.length > 1));
|
|
488
493
|
}
|
|
489
494
|
else {
|
|
490
495
|
prop.joinColumns ??= prop.referencedColumnNames.map(referencedColumnName => this.#namingStrategy.joinKeyColumnName(meta.root.className, referencedColumnName, meta.compositePK));
|
|
491
496
|
}
|
|
492
|
-
|
|
497
|
+
if (isUnionTargetMN) {
|
|
498
|
+
// Target side uses discriminator base name (e.g. attachable_id — shared across Image/Video)
|
|
499
|
+
const targetPkCols = Utils.flatten(meta2.primaryKeys.map(pk => meta2.properties[pk].fieldNames));
|
|
500
|
+
prop.inverseJoinColumns ??= targetPkCols.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, targetPkCols.length > 1));
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
prop.inverseJoinColumns ??= this.initManyToOneFieldName(prop, meta2.root.className);
|
|
504
|
+
}
|
|
493
505
|
}
|
|
494
506
|
isExplicitTableName(meta) {
|
|
495
507
|
return meta.tableName !== this.#namingStrategy.classToTableName(meta.className);
|
|
@@ -583,6 +595,24 @@ export class MetadataDiscovery {
|
|
|
583
595
|
if (prop.inversedBy) {
|
|
584
596
|
prop.targetMeta.properties[prop.inversedBy].pivotEntity = pivotMeta.class;
|
|
585
597
|
}
|
|
598
|
+
// Propagate pivotEntity to ALL inverse collections using mappedBy pointing at this
|
|
599
|
+
// owner prop. Covers three cases:
|
|
600
|
+
// - regular inverse (Tag.posts mappedBy Post.tags) — handled by inversedBy above
|
|
601
|
+
// - union-target inverse (Image.posts mappedBy Post.attachments) — on each polymorph target
|
|
602
|
+
// - merged inverse (Tag.owners mappedBy [Post,Video].tags) — union collection on the target
|
|
603
|
+
const inverseCandidates = QueryHelper.isUnionTargetPolymorphic(prop)
|
|
604
|
+
? prop.polymorphTargets
|
|
605
|
+
: [prop.targetMeta];
|
|
606
|
+
for (const targetMeta of inverseCandidates) {
|
|
607
|
+
for (const inverseProp of Object.values(targetMeta.properties)) {
|
|
608
|
+
if (inverseProp.kind === ReferenceKind.MANY_TO_MANY &&
|
|
609
|
+
inverseProp.mappedBy === prop.name &&
|
|
610
|
+
!inverseProp.pivotEntity) {
|
|
611
|
+
inverseProp.pivotEntity = pivotMeta.class;
|
|
612
|
+
inverseProp.pivotTable = pivotMeta.tableName;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
586
616
|
return pivotMeta;
|
|
587
617
|
});
|
|
588
618
|
}
|
|
@@ -721,8 +751,12 @@ export class MetadataDiscovery {
|
|
|
721
751
|
}
|
|
722
752
|
}
|
|
723
753
|
}
|
|
724
|
-
//
|
|
725
|
-
if (prop.
|
|
754
|
+
// Union-target polymorphic M:N: discriminator + target FK share the pivot across multiple target types
|
|
755
|
+
if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
|
|
756
|
+
this.defineUnionTargetPolymorphicPivotProperties(pivotMeta2, meta, prop);
|
|
757
|
+
}
|
|
758
|
+
else if (prop.polymorphic && prop.discriminatorColumn) {
|
|
759
|
+
// Rails-style polymorphic M:N: multiple owners share the pivot, single target type
|
|
726
760
|
this.definePolymorphicPivotProperties(pivotMeta2, meta, prop, targetMeta);
|
|
727
761
|
}
|
|
728
762
|
else {
|
|
@@ -809,6 +843,33 @@ export class MetadataDiscovery {
|
|
|
809
843
|
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
810
844
|
pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue] = meta.class;
|
|
811
845
|
}
|
|
846
|
+
/**
|
|
847
|
+
* Mirror of definePolymorphicPivotProperties for union-target M:N
|
|
848
|
+
* (e.g. Post.attachments -> Image | Video via shared pivot with a target-side discriminator).
|
|
849
|
+
*
|
|
850
|
+
* Pivot shape:
|
|
851
|
+
* (owner_fk..., discriminator_column, target_fk...)
|
|
852
|
+
* - owner side is a normal M:1 to the single owner entity
|
|
853
|
+
* - target side is a discriminator column + per-target-type virtual M:1 relations
|
|
854
|
+
*/
|
|
855
|
+
defineUnionTargetPolymorphicPivotProperties(pivotMeta, meta, prop) {
|
|
856
|
+
const discriminatorColumn = prop.discriminatorColumn;
|
|
857
|
+
const targets = prop.polymorphTargets;
|
|
858
|
+
pivotMeta.properties[meta.name + '_owner'] = this.definePivotProperty(prop, meta.name + '_owner', meta.class, prop.discriminator, true, false);
|
|
859
|
+
const discriminatorProp = this.createPivotScalarProperty(discriminatorColumn, [this.#platform.getVarcharTypeDeclarationSQL(prop)], [discriminatorColumn], { type: 'string', primary: true, nullable: false });
|
|
860
|
+
this.initFieldName(discriminatorProp);
|
|
861
|
+
pivotMeta.properties[discriminatorColumn] = discriminatorProp;
|
|
862
|
+
const firstTargetColumnTypes = this.getPrimaryKeyColumnTypes(targets[0]);
|
|
863
|
+
pivotMeta.properties[prop.discriminator] = this.createPivotScalarProperty(prop.discriminator, firstTargetColumnTypes, [...prop.inverseJoinColumns], { type: targets[0].className, primary: true, nullable: false });
|
|
864
|
+
pivotMeta.polymorphicDiscriminatorMap ??= {};
|
|
865
|
+
for (const targetMeta of targets) {
|
|
866
|
+
const relationName = `${prop.discriminator}_${targetMeta.tableName}`;
|
|
867
|
+
const relation = this.definePolymorphicOwnerRelation(prop, relationName, targetMeta);
|
|
868
|
+
relation.joinColumns = relation.fieldNames = relation.ownColumns = [...prop.inverseJoinColumns];
|
|
869
|
+
pivotMeta.properties[relationName] = relation;
|
|
870
|
+
pivotMeta.polymorphicDiscriminatorMap[targetMeta.tableName] = targetMeta.class;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
812
873
|
/**
|
|
813
874
|
* Create a virtual M:1 relation from pivot to a polymorphic owner entity.
|
|
814
875
|
* This enables single-query join loading for inverse-side polymorphic M:N.
|
|
@@ -961,6 +1022,7 @@ export class MetadataDiscovery {
|
|
|
961
1022
|
meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
|
|
962
1023
|
meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
|
|
963
1024
|
meta.checks = Utils.unique([...base.checks, ...meta.checks]);
|
|
1025
|
+
meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
|
|
964
1026
|
const pks = Object.values(meta.properties)
|
|
965
1027
|
.filter(p => p.primary)
|
|
966
1028
|
.map(p => p.name);
|
|
@@ -1045,11 +1107,15 @@ export class MetadataDiscovery {
|
|
|
1045
1107
|
prop.discriminatorColumn ??= this.#namingStrategy.discriminatorColumnName(prop.discriminator);
|
|
1046
1108
|
prop.createForeignKeyConstraint = false;
|
|
1047
1109
|
const isToOne = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind);
|
|
1048
|
-
|
|
1110
|
+
const isUnionTargetMN = prop.kind === ReferenceKind.MANY_TO_MANY && Array.isArray(prop.target);
|
|
1111
|
+
if (isToOne || isUnionTargetMN) {
|
|
1049
1112
|
const types = prop.type.split(/ ?\| ?/);
|
|
1050
1113
|
prop.polymorphTargets = discovered.filter(m => types.includes(m.className) && !m.embeddable);
|
|
1051
1114
|
prop.targetMeta = prop.polymorphTargets[0];
|
|
1052
1115
|
prop.referencedPKs = prop.targetMeta?.primaryKeys;
|
|
1116
|
+
if (isUnionTargetMN && prop.polymorphTargets.length < 2) {
|
|
1117
|
+
throw new MetadataError(`${meta.className}.${prop.name} union-target polymorphic M:N requires at least two target entity types; use a regular M:N relation for a single target.`);
|
|
1118
|
+
}
|
|
1053
1119
|
}
|
|
1054
1120
|
if (prop.discriminatorMap) {
|
|
1055
1121
|
const normalizedMap = {};
|
|
@@ -1065,7 +1131,7 @@ export class MetadataDiscovery {
|
|
|
1065
1131
|
}
|
|
1066
1132
|
prop.discriminatorMap = normalizedMap;
|
|
1067
1133
|
}
|
|
1068
|
-
else if (isToOne) {
|
|
1134
|
+
else if (isToOne || isUnionTargetMN) {
|
|
1069
1135
|
prop.discriminatorMap = {};
|
|
1070
1136
|
const tableNameToTarget = new Map();
|
|
1071
1137
|
for (const target of prop.polymorphTargets) {
|
|
@@ -1520,6 +1586,27 @@ export class MetadataDiscovery {
|
|
|
1520
1586
|
}
|
|
1521
1587
|
}
|
|
1522
1588
|
}
|
|
1589
|
+
initTriggers(meta) {
|
|
1590
|
+
if (meta.triggers.length === 0) {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const columns = meta.createSchemaColumnMappingObject();
|
|
1594
|
+
const table = this.createSchemaTable(meta);
|
|
1595
|
+
for (const trigger of meta.triggers) {
|
|
1596
|
+
if (trigger.body && trigger.expression) {
|
|
1597
|
+
throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} defines both 'body' and 'expression'. Use one or the other.`);
|
|
1598
|
+
}
|
|
1599
|
+
if (!trigger.body && !trigger.expression) {
|
|
1600
|
+
throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} must define either 'body' or 'expression'.`);
|
|
1601
|
+
}
|
|
1602
|
+
trigger.name ??= this.#namingStrategy.indexName(meta.tableName, trigger.events, 'trigger');
|
|
1603
|
+
trigger.forEach ??= 'row';
|
|
1604
|
+
if (trigger.body instanceof Function) {
|
|
1605
|
+
trigger.body = trigger.body(columns, table);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
meta.hasTriggers = true;
|
|
1609
|
+
}
|
|
1523
1610
|
initGeneratedColumn(meta, prop) {
|
|
1524
1611
|
if (!prop.generated && prop.columnTypes) {
|
|
1525
1612
|
const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
|
|
@@ -1646,7 +1733,14 @@ export class MetadataDiscovery {
|
|
|
1646
1733
|
return (!Type.getType(t[type]).ensureComparable(meta, prop) && (prop.type === t[type] || brand === type));
|
|
1647
1734
|
});
|
|
1648
1735
|
if (type) {
|
|
1649
|
-
prop.
|
|
1736
|
+
if (prop.array) {
|
|
1737
|
+
// built-in type + array: true — force-create instance for ArrayType wrapping
|
|
1738
|
+
prop.customType = new prop.type();
|
|
1739
|
+
prop.type = prop.customType.constructor.name;
|
|
1740
|
+
}
|
|
1741
|
+
else {
|
|
1742
|
+
prop.type = type === 'datetime' ? 'Date' : type;
|
|
1743
|
+
}
|
|
1650
1744
|
}
|
|
1651
1745
|
else {
|
|
1652
1746
|
prop.customType = new prop.type();
|
|
@@ -1656,6 +1750,30 @@ export class MetadataDiscovery {
|
|
|
1656
1750
|
if (simple) {
|
|
1657
1751
|
return;
|
|
1658
1752
|
}
|
|
1753
|
+
if (prop.array &&
|
|
1754
|
+
prop.customType &&
|
|
1755
|
+
!(prop.customType instanceof t.array) &&
|
|
1756
|
+
!(prop.customType instanceof t.enumArray) &&
|
|
1757
|
+
prop.kind === ReferenceKind.SCALAR) {
|
|
1758
|
+
const innerType = prop.customType;
|
|
1759
|
+
innerType.platform = this.#platform;
|
|
1760
|
+
innerType.meta = meta;
|
|
1761
|
+
innerType.prop = prop;
|
|
1762
|
+
if (!prop.columnTypes) {
|
|
1763
|
+
const arrayDecl = this.#platform.getArrayDeclarationSQL();
|
|
1764
|
+
if (arrayDecl.endsWith('[]')) {
|
|
1765
|
+
// native array support (e.g. postgres) — use inner type's column type with [] suffix
|
|
1766
|
+
prop.columnTypes = [innerType.getColumnType(prop, this.#platform) + '[]'];
|
|
1767
|
+
}
|
|
1768
|
+
else {
|
|
1769
|
+
// non-native (e.g. mysql, sqlite) — store as text/clob
|
|
1770
|
+
prop.columnTypes = [arrayDecl];
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
const platform = this.#platform;
|
|
1774
|
+
prop.customType = new t.array(v => innerType.convertToJSValue(v, platform), v => innerType.convertToDatabaseValue(v, platform));
|
|
1775
|
+
prop.type = prop.customType.constructor.name;
|
|
1776
|
+
}
|
|
1659
1777
|
if (!prop.customType && ['json', 'jsonb'].includes(prop.type?.toLowerCase())) {
|
|
1660
1778
|
prop.customType = new t.json();
|
|
1661
1779
|
}
|
|
@@ -171,6 +171,15 @@ export class MetadataValidator {
|
|
|
171
171
|
}
|
|
172
172
|
validatePolymorphicTargets(meta, prop) {
|
|
173
173
|
const targets = prop.polymorphTargets;
|
|
174
|
+
// Union-target M:N stores one scalar target FK per pivot row, so composite-PK targets
|
|
175
|
+
// can't round-trip through this schema.
|
|
176
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY && targets.length > 1) {
|
|
177
|
+
for (const target of targets) {
|
|
178
|
+
if (target.compositePK) {
|
|
179
|
+
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `${target.className} has a composite primary key; union-target polymorphic M:N does not support composite-PK targets.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
174
183
|
// Validate targetKey exists and is compatible across all targets
|
|
175
184
|
if (prop.targetKey) {
|
|
176
185
|
for (const target of targets) {
|
package/metadata/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw } from '../typings.js';
|
|
1
|
+
import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, TriggerDef } from '../typings.js';
|
|
2
2
|
import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
|
|
3
3
|
import type { Type, types } from '../types/index.js';
|
|
4
4
|
import type { EntityManager } from '../EntityManager.js';
|
|
@@ -57,6 +57,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
|
|
|
57
57
|
};
|
|
58
58
|
/** Used to make ORM aware of externally defined triggers. This is needed for MS SQL Server multi inserts, ignored in other dialects. */
|
|
59
59
|
hasTriggers?: boolean;
|
|
60
|
+
/** Database triggers to create for this entity's table. (SQL drivers only) */
|
|
61
|
+
triggers?: TriggerDef<E>[];
|
|
60
62
|
/** SQL query that maps to a {@doclink virtual-entities | virtual entity}, or for view entities, the view definition. */
|
|
61
63
|
expression?: string | ((em: any, where: ObjectQuery<E>, options: FindOptions<E, any, any, any>, stream?: boolean) => object);
|
|
62
64
|
/** Set {@doclink repositories#custom-repository | custom repository class}. */
|
|
@@ -202,6 +204,12 @@ export interface PropertyOptions<Owner> {
|
|
|
202
204
|
* @see https://mikro-orm.io/docs/defining-entities#lazy-scalar-properties
|
|
203
205
|
*/
|
|
204
206
|
lazy?: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Marks this property as an array. When used with a custom type, the type is automatically
|
|
209
|
+
* wrapped in an `ArrayType` that delegates element conversion to the inner type.
|
|
210
|
+
* The column type is inferred as `innerType[]` (e.g. `int[]`, `time[]`).
|
|
211
|
+
*/
|
|
212
|
+
array?: boolean;
|
|
205
213
|
/**
|
|
206
214
|
* Set true to define entity's unique primary key identifier.
|
|
207
215
|
* Alias for `@PrimaryKey()` decorator
|
|
@@ -334,7 +342,7 @@ export interface PropertyOptions<Owner> {
|
|
|
334
342
|
}
|
|
335
343
|
export interface ReferenceOptions<Owner, Target> extends PropertyOptions<Owner> {
|
|
336
344
|
/** Set target entity type. For polymorphic relations, pass an array of entity types. */
|
|
337
|
-
entity?: () => EntityName<Target> | EntityName
|
|
345
|
+
entity?: () => EntityName<Target> | EntityName[];
|
|
338
346
|
/** Set what actions on owning entity should be cascaded to the relationship. Defaults to [Cascade.PERSIST, Cascade.MERGE] (see {@doclink cascading}). */
|
|
339
347
|
cascade?: Cascade[];
|
|
340
348
|
/** Always load the relationship. Discouraged for use with to-many relations for performance reasons. */
|
|
@@ -4,7 +4,7 @@ import { type ReferenceKind } from '../enums.js';
|
|
|
4
4
|
export declare abstract class AbstractNamingStrategy implements NamingStrategy {
|
|
5
5
|
getClassName(file: string, separator?: string): string;
|
|
6
6
|
classToMigrationName(timestamp: string, customMigrationName?: string): string;
|
|
7
|
-
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
|
|
7
|
+
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
|
|
8
8
|
/**
|
|
9
9
|
* @inheritDoc
|
|
10
10
|
*/
|
|
@@ -75,7 +75,7 @@ export interface NamingStrategy {
|
|
|
75
75
|
/**
|
|
76
76
|
* Returns key/constraint name for the given type. Some drivers might not support all the types (e.g. mysql and sqlite enforce the PK name).
|
|
77
77
|
*/
|
|
78
|
-
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
|
|
78
|
+
indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
|
|
79
79
|
/**
|
|
80
80
|
* Returns alias name for given entity. The alias needs to be unique across the query, which is by default
|
|
81
81
|
* ensured via appended index parameter. It is optional to use it as long as you ensure it will be unique.
|
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.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",
|
package/platforms/Platform.d.ts
CHANGED
|
@@ -211,6 +211,11 @@ export declare abstract class Platform {
|
|
|
211
211
|
getFullTextWhereClause(prop: EntityProperty): string;
|
|
212
212
|
supportsCreatingFullTextIndex(): boolean;
|
|
213
213
|
getFullTextIndexExpression(indexName: string, schemaName: string | undefined, tableName: string, columns: SimpleColumnMeta[]): string;
|
|
214
|
+
/**
|
|
215
|
+
* Generates the SQL index hint clause for the given index names.
|
|
216
|
+
* Returns `undefined` when the platform does not support index hints (e.g. PostgreSQL, SQLite).
|
|
217
|
+
*/
|
|
218
|
+
formatIndexHint(indexNames: string[]): string | undefined;
|
|
214
219
|
/** Whether the driver automatically parses JSON columns into JS objects. */
|
|
215
220
|
convertsJsonAutomatically(): boolean;
|
|
216
221
|
/** Converts a JS value to its JSON database representation (typically JSON.stringify). */
|
package/platforms/Platform.js
CHANGED
|
@@ -415,6 +415,13 @@ export class Platform {
|
|
|
415
415
|
getFullTextIndexExpression(indexName, schemaName, tableName, columns) {
|
|
416
416
|
throw new Error('Full text searching is not supported by this driver.');
|
|
417
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Generates the SQL index hint clause for the given index names.
|
|
420
|
+
* Returns `undefined` when the platform does not support index hints (e.g. PostgreSQL, SQLite).
|
|
421
|
+
*/
|
|
422
|
+
formatIndexHint(indexNames) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
418
425
|
/** Whether the driver automatically parses JSON columns into JS objects. */
|
|
419
426
|
convertsJsonAutomatically() {
|
|
420
427
|
return true;
|
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
import type { ArrayElement, AutoPath, CleanTypeConfig, ExtractFieldsHint, FromEntityType, SerializeDTO, TypeConfig, UnboxArray } from '../typings.js';
|
|
1
|
+
import type { ArrayElement, AutoPath, CleanTypeConfig, ExtractFieldsHint, FromEntityType, ResolveSerializeFields, SerializeDTO, SerializeFieldsKeepPK, TypeConfig, UnboxArray } from '../typings.js';
|
|
2
2
|
import { type PopulatePath } from '../enums.js';
|
|
3
|
-
/** Converts entity instances to plain DTOs via `serialize()`, with fine-grained control over populate, exclude, and serialization groups. */
|
|
3
|
+
/** Converts entity instances to plain DTOs via `serialize()`, with fine-grained control over populate, exclude, fields, and serialization groups. */
|
|
4
4
|
export declare class EntitySerializer {
|
|
5
|
-
/** Serializes an entity to a plain DTO, with fine-grained control over population, exclusion, groups, and custom types. */
|
|
6
|
-
static serialize<T extends object, P extends string = never, E extends string = never>(entity: T, options?: SerializeOptions<T, P, E>): SerializeDTO<T, P, E
|
|
5
|
+
/** Serializes an entity to a plain DTO, with fine-grained control over population, exclusion, fields, groups, and custom types. */
|
|
6
|
+
static serialize<T extends object, P extends string = never, E extends string = never, F extends string = never>(entity: T, options?: SerializeOptions<T, P, E, F>): SerializeDTO<T, P, E, never, ResolveSerializeFields<F>, SerializeFieldsKeepPK<F>>;
|
|
7
7
|
private static propertyName;
|
|
8
8
|
private static processProperty;
|
|
9
9
|
private static processCustomType;
|
|
10
10
|
private static extractChildOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
|
|
13
|
+
* wildcard removes the whitelist on the sub-tree (everything is included), so consumers don't have to repeat
|
|
14
|
+
* every field of the child. Otherwise dot-paths are stripped of the parent prefix and passed down.
|
|
15
|
+
*/
|
|
16
|
+
private static extractChildFields;
|
|
11
17
|
private static processEntity;
|
|
12
18
|
private static processCollection;
|
|
13
19
|
}
|
|
14
|
-
export interface SerializeOptions<T, P extends string = never, E extends string = never> {
|
|
20
|
+
export interface SerializeOptions<T, P extends string = never, E extends string = never, F extends string = never> {
|
|
15
21
|
/** Specify which relation should be serialized as populated and which as a FK. */
|
|
16
22
|
populate?: readonly AutoPath<T, P, `${PopulatePath.ALL}`>[];
|
|
17
23
|
/** Specify which properties should be omitted. */
|
|
18
24
|
exclude?: readonly AutoPath<T, E>[];
|
|
25
|
+
/**
|
|
26
|
+
* Whitelist of properties to serialize, supports dot-paths (e.g. `['name', 'books.title']`). When set, only the
|
|
27
|
+
* listed properties end up in the output, including primary keys. A bare property name keeps its entire sub-tree;
|
|
28
|
+
* a dot-path additionally narrows the nested object to the listed sub-keys. `exclude` takes precedence on conflict.
|
|
29
|
+
*/
|
|
30
|
+
fields?: readonly AutoPath<T, F, `${PopulatePath.ALL}`>[];
|
|
19
31
|
/** Enforce unpopulated references to be returned as objects, e.g. `{ author: { id: 1 } }` instead of `{ author: 1 }`. */
|
|
20
32
|
forceObject?: boolean;
|
|
21
33
|
/** Ignore custom property serializers. */
|
|
@@ -41,4 +53,4 @@ export interface SerializeOptions<T, P extends string = never, E extends string
|
|
|
41
53
|
* const dto2 = wrap(user).serialize({ exclude: ['id', 'email'], forceObject: true });
|
|
42
54
|
* ```
|
|
43
55
|
*/
|
|
44
|
-
export declare function serialize<Entity extends object, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Populate extends string = never, Exclude extends string = never, Config extends TypeConfig = never>(entity: Entity, options?: Config & SerializeOptions<UnboxArray<Entity>, Populate, Exclude>): Naked extends object[] ? SerializeDTO<ArrayElement<Naked>, Populate, Exclude, CleanTypeConfig<Config>>[] : SerializeDTO<Naked, Populate, Exclude, CleanTypeConfig<Config>, ExtractFieldsHint<Entity>>;
|
|
56
|
+
export declare function serialize<Entity extends object, Naked extends FromEntityType<Entity> = FromEntityType<Entity>, Populate extends string = never, Exclude extends string = never, Fields extends string = never, Config extends TypeConfig = never>(entity: Entity, options?: Config & SerializeOptions<UnboxArray<Entity>, Populate, Exclude, Fields>): Naked extends object[] ? SerializeDTO<ArrayElement<Naked>, Populate, Exclude, CleanTypeConfig<Config>, ResolveSerializeFields<Fields, ExtractFieldsHint<Entity>>, SerializeFieldsKeepPK<Fields>>[] : SerializeDTO<Naked, Populate, Exclude, CleanTypeConfig<Config>, ResolveSerializeFields<Fields, ExtractFieldsHint<Entity>>, SerializeFieldsKeepPK<Fields>>;
|
|
@@ -4,13 +4,19 @@ 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
|
+
/** Returns true when any entry in `items` is `propName`, the wildcard `*`, or a dot-path under `propName`. */
|
|
8
|
+
function matchesPath(items, propName) {
|
|
9
|
+
return items.some(item => item === propName || item === '*' || item.startsWith(propName + '.'));
|
|
10
|
+
}
|
|
7
11
|
function isVisible(meta, propName, options) {
|
|
8
12
|
const prop = meta.properties[propName];
|
|
9
13
|
if (options.groups && prop?.groups) {
|
|
10
14
|
return prop.groups.some(g => options.groups.includes(g));
|
|
11
15
|
}
|
|
12
|
-
if (Array.isArray(options.
|
|
13
|
-
|
|
16
|
+
if (Array.isArray(options.fields) && options.fields.length > 0 && !matchesPath(options.fields, propName)) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(options.populate) && matchesPath(options.populate, propName)) {
|
|
14
20
|
return true;
|
|
15
21
|
}
|
|
16
22
|
if (options.exclude?.find(item => item === propName)) {
|
|
@@ -22,7 +28,8 @@ function isVisible(meta, propName, options) {
|
|
|
22
28
|
}
|
|
23
29
|
function isPopulated(propName, options) {
|
|
24
30
|
if (typeof options.populate !== 'boolean' &&
|
|
25
|
-
options.populate
|
|
31
|
+
Array.isArray(options.populate) &&
|
|
32
|
+
matchesPath(options.populate, propName)) {
|
|
26
33
|
return true;
|
|
27
34
|
}
|
|
28
35
|
if (typeof options.populate === 'boolean') {
|
|
@@ -30,9 +37,9 @@ function isPopulated(propName, options) {
|
|
|
30
37
|
}
|
|
31
38
|
return false;
|
|
32
39
|
}
|
|
33
|
-
/** Converts entity instances to plain DTOs via `serialize()`, with fine-grained control over populate, exclude, and serialization groups. */
|
|
40
|
+
/** Converts entity instances to plain DTOs via `serialize()`, with fine-grained control over populate, exclude, fields, and serialization groups. */
|
|
34
41
|
export class EntitySerializer {
|
|
35
|
-
/** Serializes an entity to a plain DTO, with fine-grained control over population, exclusion, groups, and custom types. */
|
|
42
|
+
/** Serializes an entity to a plain DTO, with fine-grained control over population, exclusion, fields, groups, and custom types. */
|
|
36
43
|
static serialize(entity, options = {}) {
|
|
37
44
|
const wrapped = helper(entity);
|
|
38
45
|
const meta = wrapped.__meta;
|
|
@@ -184,8 +191,27 @@ export class EntitySerializer {
|
|
|
184
191
|
? Utils.extractChildElements(options.populate, prop, '*')
|
|
185
192
|
: options.populate,
|
|
186
193
|
exclude: Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop) : options.exclude,
|
|
194
|
+
fields: Array.isArray(options.fields) ? this.extractChildFields(options.fields, prop) : options.fields,
|
|
187
195
|
};
|
|
188
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Extracts the nested `fields` whitelist for a child property. A bare parent name (`fields: ['books']`) or a
|
|
199
|
+
* wildcard removes the whitelist on the sub-tree (everything is included), so consumers don't have to repeat
|
|
200
|
+
* every field of the child. Otherwise dot-paths are stripped of the parent prefix and passed down.
|
|
201
|
+
*/
|
|
202
|
+
static extractChildFields(fields, prop) {
|
|
203
|
+
const out = [];
|
|
204
|
+
const dotPrefix = prop + '.';
|
|
205
|
+
for (const field of fields) {
|
|
206
|
+
if (field === prop || field === '*') {
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if (field.startsWith(dotPrefix)) {
|
|
210
|
+
out.push(field.substring(dotPrefix.length));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
189
215
|
static processEntity(prop, entity, platform, options) {
|
|
190
216
|
const child = Reference.unwrapReference(entity[prop.name]);
|
|
191
217
|
const wrapped = helper(child);
|