@mikro-orm/core 7.0.0-dev.225 → 7.0.0-dev.227
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 +8 -2
- package/cache/FileCacheAdapter.js +9 -1
- package/drivers/DatabaseDriver.js +38 -0
- package/entity/EntityHelper.js +13 -1
- package/entity/EntityLoader.d.ts +1 -0
- package/entity/EntityLoader.js +123 -25
- package/entity/PolymorphicRef.d.ts +12 -0
- package/entity/PolymorphicRef.js +18 -0
- package/entity/defineEntity.d.ts +8 -4
- package/entity/defineEntity.js +8 -0
- package/entity/index.d.ts +1 -0
- package/entity/index.js +1 -0
- package/errors.d.ts +3 -2
- package/errors.js +9 -4
- package/hydration/ObjectHydrator.js +40 -8
- package/metadata/EntitySchema.d.ts +1 -1
- package/metadata/MetadataDiscovery.d.ts +22 -0
- package/metadata/MetadataDiscovery.js +222 -11
- package/metadata/MetadataValidator.d.ts +6 -0
- package/metadata/MetadataValidator.js +76 -3
- package/metadata/types.d.ts +21 -5
- package/naming-strategy/AbstractNamingStrategy.d.ts +4 -0
- package/naming-strategy/AbstractNamingStrategy.js +6 -0
- package/naming-strategy/NamingStrategy.d.ts +4 -0
- package/package.json +1 -1
- package/typings.d.ts +20 -6
- package/typings.js +5 -1
- package/unit-of-work/ChangeSetComputer.js +14 -4
- package/unit-of-work/ChangeSetPersister.js +7 -0
- package/unit-of-work/UnitOfWork.js +13 -2
- package/utils/EntityComparator.d.ts +5 -0
- package/utils/EntityComparator.js +66 -12
- package/utils/QueryHelper.d.ts +4 -0
- package/utils/QueryHelper.js +10 -1
- package/utils/Utils.d.ts +1 -1
- package/utils/Utils.js +1 -1
package/EntityManager.js
CHANGED
|
@@ -325,8 +325,10 @@ export class EntityManager {
|
|
|
325
325
|
return ret;
|
|
326
326
|
}
|
|
327
327
|
async getJoinedFilters(meta, options) {
|
|
328
|
+
// If user provided populateFilter, merge it with computed filters
|
|
329
|
+
const userFilter = options.populateFilter;
|
|
328
330
|
if (!this.config.get('filtersOnRelations') || !options.populate) {
|
|
329
|
-
return
|
|
331
|
+
return userFilter;
|
|
330
332
|
}
|
|
331
333
|
const ret = {};
|
|
332
334
|
for (const hint of options.populate) {
|
|
@@ -360,7 +362,11 @@ export class EntityManager {
|
|
|
360
362
|
}
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
|
-
|
|
365
|
+
// Merge user-provided populateFilter with computed filters
|
|
366
|
+
if (userFilter) {
|
|
367
|
+
Utils.merge(ret, userFilter);
|
|
368
|
+
}
|
|
369
|
+
return Utils.hasObjectKeys(ret) ? ret : undefined;
|
|
364
370
|
}
|
|
365
371
|
/**
|
|
366
372
|
* When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
|
|
@@ -53,7 +53,15 @@ export class FileCacheAdapter {
|
|
|
53
53
|
clear() {
|
|
54
54
|
const path = this.path('*');
|
|
55
55
|
const files = fs.glob(path);
|
|
56
|
-
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
/* v8 ignore next */
|
|
58
|
+
try {
|
|
59
|
+
unlinkSync(file);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ignore if file is already gone
|
|
63
|
+
}
|
|
64
|
+
}
|
|
57
65
|
this.cache = {};
|
|
58
66
|
}
|
|
59
67
|
combine() {
|
|
@@ -8,6 +8,7 @@ import { EntityManager } from '../EntityManager.js';
|
|
|
8
8
|
import { CursorError, ValidationError } from '../errors.js';
|
|
9
9
|
import { DriverException } from '../exceptions.js';
|
|
10
10
|
import { helper } from '../entity/wrap.js';
|
|
11
|
+
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
11
12
|
import { JsonType } from '../types/JsonType.js';
|
|
12
13
|
import { MikroORM } from '../MikroORM.js';
|
|
13
14
|
export class DatabaseDriver {
|
|
@@ -260,6 +261,43 @@ export class DatabaseDriver {
|
|
|
260
261
|
}
|
|
261
262
|
return;
|
|
262
263
|
}
|
|
264
|
+
// Handle polymorphic relations - convert tuple or PolymorphicRef to separate columns
|
|
265
|
+
// Tuple format: ['discriminator', id] or ['discriminator', id1, id2] for composite keys
|
|
266
|
+
// Must be checked BEFORE joinColumns array handling since polymorphic uses fieldNames (includes discriminator)
|
|
267
|
+
if (prop.polymorphic && prop.fieldNames && prop.fieldNames.length >= 2) {
|
|
268
|
+
let discriminator;
|
|
269
|
+
let ids;
|
|
270
|
+
if (Array.isArray(data[k]) && typeof data[k][0] === 'string' && prop.discriminatorMap?.[data[k][0]]) {
|
|
271
|
+
// Tuple format: ['discriminator', ...ids]
|
|
272
|
+
const [disc, ...rest] = data[k];
|
|
273
|
+
discriminator = disc;
|
|
274
|
+
ids = rest;
|
|
275
|
+
}
|
|
276
|
+
else if (data[k] instanceof PolymorphicRef) {
|
|
277
|
+
// PolymorphicRef wrapper (internal use)
|
|
278
|
+
discriminator = data[k].discriminator;
|
|
279
|
+
const polyId = data[k].id;
|
|
280
|
+
// Handle object-style composite key IDs like { tenantId: 1, orgId: 100 }
|
|
281
|
+
if (polyId && typeof polyId === 'object' && !Array.isArray(polyId)) {
|
|
282
|
+
const targetEntity = prop.discriminatorMap?.[discriminator];
|
|
283
|
+
const targetMeta = this.metadata.get(targetEntity);
|
|
284
|
+
ids = targetMeta.primaryKeys.map(pk => polyId[pk]);
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
ids = Utils.asArray(polyId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (discriminator) {
|
|
291
|
+
const discriminatorColumn = prop.fieldNames[0];
|
|
292
|
+
const idColumns = prop.fieldNames.slice(1);
|
|
293
|
+
delete data[k];
|
|
294
|
+
data[discriminatorColumn] = discriminator;
|
|
295
|
+
idColumns.forEach((col, idx) => {
|
|
296
|
+
data[col] = ids[idx];
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
263
301
|
if (prop.joinColumns && Array.isArray(data[k])) {
|
|
264
302
|
const copy = Utils.flatten(data[k]);
|
|
265
303
|
delete data[k];
|
package/entity/EntityHelper.js
CHANGED
|
@@ -176,7 +176,19 @@ export class EntityHelper {
|
|
|
176
176
|
});
|
|
177
177
|
}
|
|
178
178
|
static propagate(meta, entity, owner, prop, value, old) {
|
|
179
|
-
|
|
179
|
+
// For polymorphic relations, get bidirectional relations from the actual entity's metadata
|
|
180
|
+
let bidirectionalRelations;
|
|
181
|
+
if (prop.polymorphic && prop.polymorphTargets?.length) {
|
|
182
|
+
// For polymorphic relations, we need to get the bidirectional relations from the actual value's metadata
|
|
183
|
+
if (!value) {
|
|
184
|
+
return; // No value means no propagation needed
|
|
185
|
+
}
|
|
186
|
+
bidirectionalRelations = helper(value).__meta.bidirectionalRelations;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
bidirectionalRelations = prop.targetMeta.bidirectionalRelations;
|
|
190
|
+
}
|
|
191
|
+
for (const prop2 of bidirectionalRelations) {
|
|
180
192
|
if ((prop2.inversedBy || prop2.mappedBy) !== prop.name) {
|
|
181
193
|
continue;
|
|
182
194
|
}
|
package/entity/EntityLoader.d.ts
CHANGED
package/entity/EntityLoader.js
CHANGED
|
@@ -144,6 +144,9 @@ export class EntityLoader {
|
|
|
144
144
|
const res = await this.findChildrenFromPivotTable(filtered, prop, options, innerOrderBy, populate, !!ref);
|
|
145
145
|
return Utils.flatten(res);
|
|
146
146
|
}
|
|
147
|
+
if (prop.polymorphic && prop.polymorphTargets) {
|
|
148
|
+
return this.populatePolymorphic(entities, prop, options, !!ref);
|
|
149
|
+
}
|
|
147
150
|
const { items, partial } = await this.findChildren(options.filtered ?? entities, prop, populate, {
|
|
148
151
|
...options,
|
|
149
152
|
where,
|
|
@@ -163,6 +166,64 @@ export class EntityLoader {
|
|
|
163
166
|
populate: [],
|
|
164
167
|
});
|
|
165
168
|
}
|
|
169
|
+
async populatePolymorphic(entities, prop, options, ref) {
|
|
170
|
+
const ownerMeta = this.metadata.get(entities[0].constructor);
|
|
171
|
+
// Separate entities: those with loaded refs vs those needing FK load
|
|
172
|
+
const toPopulate = [];
|
|
173
|
+
const needsFkLoad = [];
|
|
174
|
+
for (const entity of entities) {
|
|
175
|
+
const refValue = entity[prop.name];
|
|
176
|
+
if (refValue && helper(refValue).hasPrimaryKey()) {
|
|
177
|
+
if ((ref && !options.refresh) || // :ref hint - already have reference
|
|
178
|
+
(!ref && helper(refValue).__initialized && !options.refresh) // already loaded
|
|
179
|
+
) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
toPopulate.push(entity);
|
|
183
|
+
}
|
|
184
|
+
else if (refValue == null && !helper(entity).__loadedProperties.has(prop.name)) {
|
|
185
|
+
// FK columns weren't loaded (partial loading) — need to re-fetch them.
|
|
186
|
+
// If the property IS in __loadedProperties, the FK was loaded and is genuinely null.
|
|
187
|
+
needsFkLoad.push(entity);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Load FK columns using populateScalar pattern
|
|
191
|
+
if (needsFkLoad.length > 0) {
|
|
192
|
+
await this.populateScalar(ownerMeta, needsFkLoad, {
|
|
193
|
+
...options,
|
|
194
|
+
fields: [...ownerMeta.primaryKeys, prop.name],
|
|
195
|
+
});
|
|
196
|
+
// After loading FKs, add to toPopulate if not using :ref hint
|
|
197
|
+
if (!ref) {
|
|
198
|
+
for (const entity of needsFkLoad) {
|
|
199
|
+
const refValue = entity[prop.name];
|
|
200
|
+
if (refValue && helper(refValue).hasPrimaryKey()) {
|
|
201
|
+
toPopulate.push(entity);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (toPopulate.length === 0) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
// Group references by target class for batch loading
|
|
210
|
+
const groups = new Map();
|
|
211
|
+
for (const entity of toPopulate) {
|
|
212
|
+
const refValue = Reference.unwrapReference(entity[prop.name]);
|
|
213
|
+
const discriminator = QueryHelper.findDiscriminatorValue(prop.discriminatorMap, helper(refValue).__meta.class);
|
|
214
|
+
const group = groups.get(discriminator) ?? [];
|
|
215
|
+
group.push(refValue);
|
|
216
|
+
groups.set(discriminator, group);
|
|
217
|
+
}
|
|
218
|
+
// Load each group concurrently - identity map handles merging with existing references
|
|
219
|
+
const allItems = [];
|
|
220
|
+
await Promise.all([...groups].map(async ([discriminator, children]) => {
|
|
221
|
+
const targetMeta = this.metadata.find(prop.discriminatorMap[discriminator]);
|
|
222
|
+
await this.populateScalar(targetMeta, children, options);
|
|
223
|
+
allItems.push(...children);
|
|
224
|
+
}));
|
|
225
|
+
return allItems;
|
|
226
|
+
}
|
|
166
227
|
initializeCollections(filtered, prop, field, children, customOrder, partial) {
|
|
167
228
|
if (prop.kind === ReferenceKind.ONE_TO_MANY) {
|
|
168
229
|
this.initializeOneToMany(filtered, children, prop, field, partial);
|
|
@@ -215,8 +276,17 @@ export class EntityLoader {
|
|
|
215
276
|
let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
|
|
216
277
|
let schema = options.schema;
|
|
217
278
|
const partial = !Utils.isEmpty(prop.where) || !Utils.isEmpty(options.where);
|
|
279
|
+
let polymorphicOwnerProp;
|
|
218
280
|
if (prop.kind === ReferenceKind.ONE_TO_MANY || (prop.kind === ReferenceKind.MANY_TO_MANY && !prop.owner)) {
|
|
219
|
-
|
|
281
|
+
const ownerProp = meta.properties[prop.mappedBy];
|
|
282
|
+
if (ownerProp.polymorphic && ownerProp.fieldNames.length >= 2) {
|
|
283
|
+
const idColumns = ownerProp.fieldNames.slice(1);
|
|
284
|
+
fk = idColumns.length === 1 ? idColumns[0] : idColumns;
|
|
285
|
+
polymorphicOwnerProp = ownerProp;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
fk = ownerProp.name;
|
|
289
|
+
}
|
|
220
290
|
}
|
|
221
291
|
if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
|
|
222
292
|
children.length = 0;
|
|
@@ -229,9 +299,24 @@ export class EntityLoader {
|
|
|
229
299
|
if (!schema && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
|
|
230
300
|
schema = children.find(e => e.__helper.__schema)?.__helper.__schema;
|
|
231
301
|
}
|
|
232
|
-
// When targetKey is set, get the targetKey value instead of PK
|
|
233
302
|
const ids = Utils.unique(children.map(e => prop.targetKey ? e[prop.targetKey] : e.__helper.getPrimaryKey()));
|
|
234
|
-
let where
|
|
303
|
+
let where;
|
|
304
|
+
if (polymorphicOwnerProp && Array.isArray(fk)) {
|
|
305
|
+
const conditions = ids.map(id => {
|
|
306
|
+
const pkValues = Object.values(id);
|
|
307
|
+
return Object.fromEntries(fk.map((col, idx) => [col, pkValues[idx]]));
|
|
308
|
+
});
|
|
309
|
+
where = (conditions.length === 1 ? conditions[0] : { $or: conditions });
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
where = this.mergePrimaryCondition(ids, fk, options, meta, this.metadata, this.driver.getPlatform());
|
|
313
|
+
}
|
|
314
|
+
if (polymorphicOwnerProp) {
|
|
315
|
+
const parentMeta = this.metadata.find(entities[0].constructor);
|
|
316
|
+
const discriminatorValue = QueryHelper.findDiscriminatorValue(polymorphicOwnerProp.discriminatorMap, parentMeta.class) ?? parentMeta.tableName;
|
|
317
|
+
const discriminatorColumn = polymorphicOwnerProp.fieldNames[0];
|
|
318
|
+
where = { $and: [where, { [discriminatorColumn]: discriminatorValue }] };
|
|
319
|
+
}
|
|
235
320
|
const fields = this.buildFields(options.fields, prop, ref);
|
|
236
321
|
/* eslint-disable prefer-const */
|
|
237
322
|
let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere = 'infer', connectionType, logging, } = options;
|
|
@@ -362,30 +447,42 @@ export class EntityLoader {
|
|
|
362
447
|
for (const entity of entities) {
|
|
363
448
|
visited.add(entity);
|
|
364
449
|
}
|
|
365
|
-
// skip lazy scalar properties
|
|
366
450
|
if (!prop.targetMeta) {
|
|
367
451
|
return;
|
|
368
452
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
453
|
+
const populateChildren = async (targetMeta, items) => {
|
|
454
|
+
await this.populate(targetMeta.class, items, populate.children ?? populate.all, {
|
|
455
|
+
where: await this.extractChildCondition(options, prop, false),
|
|
456
|
+
orderBy: innerOrderBy,
|
|
457
|
+
fields,
|
|
458
|
+
exclude,
|
|
459
|
+
validate: false,
|
|
460
|
+
lookup: false,
|
|
461
|
+
filters,
|
|
462
|
+
ignoreLazyScalarProperties,
|
|
463
|
+
populateWhere,
|
|
464
|
+
connectionType,
|
|
465
|
+
logging,
|
|
466
|
+
schema,
|
|
467
|
+
// @ts-ignore not a public option, will be propagated to the populate call
|
|
468
|
+
refresh: refresh && !filtered.every(item => options.visited.has(item)),
|
|
469
|
+
// @ts-ignore not a public option, will be propagated to the populate call
|
|
470
|
+
visited: options.visited,
|
|
471
|
+
// @ts-ignore not a public option
|
|
472
|
+
filtered,
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
if (prop.polymorphic && prop.polymorphTargets) {
|
|
476
|
+
await Promise.all(prop.polymorphTargets.map(async (targetMeta) => {
|
|
477
|
+
const targetChildren = unique.filter(child => helper(child).__meta.className === targetMeta.className);
|
|
478
|
+
if (targetChildren.length > 0) {
|
|
479
|
+
await populateChildren(targetMeta, targetChildren);
|
|
480
|
+
}
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
await populateChildren(prop.targetMeta, unique);
|
|
485
|
+
}
|
|
389
486
|
}
|
|
390
487
|
/** @internal */
|
|
391
488
|
async findChildrenFromPivotTable(filtered, prop, options, orderBy, populate, pivotJoin) {
|
|
@@ -394,7 +491,8 @@ export class EntityLoader {
|
|
|
394
491
|
let where = await this.extractChildCondition(options, prop, true);
|
|
395
492
|
const fields = this.buildFields(options.fields, prop);
|
|
396
493
|
const exclude = Array.isArray(options.exclude) ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
|
|
397
|
-
const
|
|
494
|
+
const populateFilter = options.populateFilter?.[prop.name];
|
|
495
|
+
const options2 = { ...options, fields, exclude, populateFilter };
|
|
398
496
|
['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
|
|
399
497
|
options2.populate = (populate?.children ?? []);
|
|
400
498
|
if (prop.customType) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrapper class for polymorphic relation reference data.
|
|
3
|
+
* Holds the discriminator value (type identifier) and the primary key value(s).
|
|
4
|
+
* Used internally to track polymorphic FK values before hydration.
|
|
5
|
+
*/
|
|
6
|
+
export declare class PolymorphicRef {
|
|
7
|
+
readonly discriminator: string;
|
|
8
|
+
id: unknown;
|
|
9
|
+
constructor(discriminator: string, id: unknown);
|
|
10
|
+
/** Returns `[discriminator, ...idValues]` tuple suitable for column-level expansion. */
|
|
11
|
+
toTuple(): unknown[];
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper class for polymorphic relation reference data.
|
|
4
|
+
* Holds the discriminator value (type identifier) and the primary key value(s).
|
|
5
|
+
* Used internally to track polymorphic FK values before hydration.
|
|
6
|
+
*/
|
|
7
|
+
export class PolymorphicRef {
|
|
8
|
+
discriminator;
|
|
9
|
+
id;
|
|
10
|
+
constructor(discriminator, id) {
|
|
11
|
+
this.discriminator = discriminator;
|
|
12
|
+
this.id = id;
|
|
13
|
+
}
|
|
14
|
+
/** Returns `[discriminator, ...idValues]` tuple suitable for column-level expansion. */
|
|
15
|
+
toTuple() {
|
|
16
|
+
return [this.discriminator, ...Utils.asArray(this.id)];
|
|
17
|
+
}
|
|
18
|
+
}
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -312,6 +312,10 @@ export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys
|
|
|
312
312
|
owner<T extends boolean = true>(owner?: T): Pick<UniversalPropertyOptionsBuilder<Value, Omit<Options, 'owner'> & {
|
|
313
313
|
owner: T;
|
|
314
314
|
}, IncludeKeys>, IncludeKeys>;
|
|
315
|
+
/** For polymorphic relations. Specifies the property name that stores the entity type discriminator. Defaults to the property name. */
|
|
316
|
+
discriminator(discriminator: string): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
317
|
+
/** For polymorphic relations. Custom mapping of discriminator values to entity class names. */
|
|
318
|
+
discriminatorMap(discriminatorMap: Dictionary<string>): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
315
319
|
/** Point to the inverse side property name. */
|
|
316
320
|
inversedBy(inversedBy: keyof Value | ((e: Value) => any)): Pick<UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys>, IncludeKeys>;
|
|
317
321
|
/** Point to the owning side property name. */
|
|
@@ -390,11 +394,11 @@ declare const propertyBuilders: {
|
|
|
390
394
|
manyToMany: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
|
|
391
395
|
kind: "m:n";
|
|
392
396
|
}, IncludeKeysForManyToManyOptions>;
|
|
393
|
-
manyToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
|
|
397
|
+
manyToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
|
|
394
398
|
kind: "m:1";
|
|
395
399
|
}, IncludeKeysForManyToOneOptions>;
|
|
396
400
|
oneToMany: <Target extends EntityTarget>(target: Target) => OneToManyOptionsBuilderOnlyMappedBy<InferEntity<Target>>;
|
|
397
|
-
oneToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
|
|
401
|
+
oneToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
|
|
398
402
|
kind: "1:1";
|
|
399
403
|
}, IncludeKeysForOneToOneOptions>;
|
|
400
404
|
date: () => UniversalPropertyOptionsBuilder<string, EmptyOptions, IncludeKeysForProperty>;
|
|
@@ -468,11 +472,11 @@ export declare namespace defineEntity {
|
|
|
468
472
|
manyToMany: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
|
|
469
473
|
kind: "m:n";
|
|
470
474
|
}, IncludeKeysForManyToManyOptions>;
|
|
471
|
-
manyToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
|
|
475
|
+
manyToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
|
|
472
476
|
kind: "m:1";
|
|
473
477
|
}, IncludeKeysForManyToOneOptions>;
|
|
474
478
|
oneToMany: <Target extends EntityTarget>(target: Target) => OneToManyOptionsBuilderOnlyMappedBy<InferEntity<Target>>;
|
|
475
|
-
oneToOne: <Target extends EntityTarget>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target>, EmptyOptions & {
|
|
479
|
+
oneToOne: <Target extends EntityTarget | EntityTarget[]>(target: Target) => UniversalPropertyOptionsBuilder<InferEntity<Target extends (infer T)[] ? T : Target>, EmptyOptions & {
|
|
476
480
|
kind: "1:1";
|
|
477
481
|
}, IncludeKeysForOneToOneOptions>;
|
|
478
482
|
date: () => UniversalPropertyOptionsBuilder<string, EmptyOptions, IncludeKeysForProperty>;
|
package/entity/defineEntity.js
CHANGED
|
@@ -353,6 +353,14 @@ export class UniversalPropertyOptionsBuilder {
|
|
|
353
353
|
owner(owner = true) {
|
|
354
354
|
return this.assignOptions({ owner });
|
|
355
355
|
}
|
|
356
|
+
/** For polymorphic relations. Specifies the property name that stores the entity type discriminator. Defaults to the property name. */
|
|
357
|
+
discriminator(discriminator) {
|
|
358
|
+
return this.assignOptions({ discriminator });
|
|
359
|
+
}
|
|
360
|
+
/** For polymorphic relations. Custom mapping of discriminator values to entity class names. */
|
|
361
|
+
discriminatorMap(discriminatorMap) {
|
|
362
|
+
return this.assignOptions({ discriminatorMap });
|
|
363
|
+
}
|
|
356
364
|
/** Point to the inverse side property name. */
|
|
357
365
|
inversedBy(inversedBy) {
|
|
358
366
|
return this.assignOptions({ inversedBy });
|
package/entity/index.d.ts
CHANGED
package/entity/index.js
CHANGED
package/errors.d.ts
CHANGED
|
@@ -63,8 +63,9 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
63
63
|
static propertyTargetsEntityType(meta: EntityMetadata, prop: EntityProperty, target: EntityMetadata): MetadataError<Partial<any>>;
|
|
64
64
|
static fromMissingOption(meta: EntityMetadata, prop: EntityProperty, option: string): MetadataError<Partial<any>>;
|
|
65
65
|
static targetKeyOnManyToMany(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
66
|
-
static targetKeyNotUnique(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
67
|
-
static targetKeyNotFound(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
66
|
+
static targetKeyNotUnique(meta: EntityMetadata, prop: EntityProperty, target?: EntityMetadata): MetadataError<Partial<any>>;
|
|
67
|
+
static targetKeyNotFound(meta: EntityMetadata, prop: EntityProperty, target?: EntityMetadata): MetadataError<Partial<any>>;
|
|
68
|
+
static incompatiblePolymorphicTargets(meta: EntityMetadata, prop: EntityProperty, target1: EntityMetadata, target2: EntityMetadata, reason: string): MetadataError<Partial<any>>;
|
|
68
69
|
static dangerousPropertyName(meta: EntityMetadata, prop: EntityProperty): MetadataError<Partial<any>>;
|
|
69
70
|
static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
|
|
70
71
|
private static fromMessage;
|
package/errors.js
CHANGED
|
@@ -216,11 +216,16 @@ export class MetadataError extends ValidationError {
|
|
|
216
216
|
static targetKeyOnManyToMany(meta, prop) {
|
|
217
217
|
return this.fromMessage(meta, prop, `uses 'targetKey' option which is not supported for ManyToMany relations`);
|
|
218
218
|
}
|
|
219
|
-
static targetKeyNotUnique(meta, prop) {
|
|
220
|
-
|
|
219
|
+
static targetKeyNotUnique(meta, prop, target) {
|
|
220
|
+
const targetName = target?.className ?? prop.type;
|
|
221
|
+
return this.fromMessage(meta, prop, `has 'targetKey' set to '${prop.targetKey}', but ${targetName}.${prop.targetKey} is not marked as unique`);
|
|
221
222
|
}
|
|
222
|
-
static targetKeyNotFound(meta, prop) {
|
|
223
|
-
|
|
223
|
+
static targetKeyNotFound(meta, prop, target) {
|
|
224
|
+
const targetName = target?.className ?? prop.type;
|
|
225
|
+
return this.fromMessage(meta, prop, `has 'targetKey' set to '${prop.targetKey}', but ${targetName}.${prop.targetKey} does not exist`);
|
|
226
|
+
}
|
|
227
|
+
static incompatiblePolymorphicTargets(meta, prop, target1, target2, reason) {
|
|
228
|
+
return this.fromMessage(meta, prop, `has incompatible polymorphic targets ${target1.className} and ${target2.className}: ${reason}`);
|
|
224
229
|
}
|
|
225
230
|
static dangerousPropertyName(meta, prop) {
|
|
226
231
|
return this.fromMessage(meta, prop, `uses a dangerous property name '${prop.name}' which could lead to prototype pollution. Please use a different property name.`);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Hydrator } from './Hydrator.js';
|
|
2
2
|
import { Collection } from '../entity/Collection.js';
|
|
3
3
|
import { Reference, ScalarReference } from '../entity/Reference.js';
|
|
4
|
+
import { PolymorphicRef } from '../entity/PolymorphicRef.js';
|
|
4
5
|
import { parseJsonSafe, Utils } from '../utils/Utils.js';
|
|
5
6
|
import { ReferenceKind } from '../enums.js';
|
|
6
7
|
import { Raw } from '../utils/RawQueryFragment.js';
|
|
8
|
+
import { ValidationError } from '../errors.js';
|
|
7
9
|
export class ObjectHydrator extends Hydrator {
|
|
8
10
|
hydrators = {
|
|
9
11
|
'full~true': new Map(),
|
|
@@ -49,6 +51,8 @@ export class ObjectHydrator extends Hydrator {
|
|
|
49
51
|
context.set('isPrimaryKey', Utils.isPrimaryKey);
|
|
50
52
|
context.set('Collection', Collection);
|
|
51
53
|
context.set('Reference', Reference);
|
|
54
|
+
context.set('PolymorphicRef', PolymorphicRef);
|
|
55
|
+
context.set('ValidationError', ValidationError);
|
|
52
56
|
const registerCustomType = (prop, convertorKey, method, context) => {
|
|
53
57
|
context.set(`${method}_${convertorKey}`, (val) => {
|
|
54
58
|
/* v8 ignore next */
|
|
@@ -134,23 +138,51 @@ export class ObjectHydrator extends Hydrator {
|
|
|
134
138
|
const nullVal = this.config.get('forceUndefined') ? 'undefined' : 'null';
|
|
135
139
|
ret.push(` if (data${dataKey} === null) {\n entity${entityKey} = ${nullVal};`);
|
|
136
140
|
ret.push(` } else if (typeof data${dataKey} !== 'undefined') {`);
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
141
|
+
// For polymorphic: instanceof check; for regular: isPrimaryKey() check
|
|
142
|
+
const pkCheck = prop.polymorphic ? `data${dataKey} instanceof PolymorphicRef` : `isPrimaryKey(data${dataKey}, true)`;
|
|
143
|
+
ret.push(` if (${pkCheck}) {`);
|
|
140
144
|
// When targetKey is set, pass the key option to createReference so it uses the alternate key
|
|
141
145
|
const keyOption = prop.targetKey ? `, key: '${prop.targetKey}'` : '';
|
|
142
|
-
if (prop.
|
|
143
|
-
|
|
146
|
+
if (prop.polymorphic) {
|
|
147
|
+
// For polymorphic: target class from discriminator map, PK from data.id
|
|
148
|
+
const discriminatorMapKey = this.safeKey(`discriminatorMap_${prop.name}_${this.tmpIndex++}`);
|
|
149
|
+
context.set(discriminatorMapKey, prop.discriminatorMap);
|
|
150
|
+
ret.push(` const targetClass = ${discriminatorMapKey}[data${dataKey}.discriminator];`);
|
|
151
|
+
ret.push(` if (!targetClass) throw new ValidationError(\`Unknown discriminator value '\${data${dataKey}.discriminator}' for polymorphic relation '${prop.name}'. Valid values: \${Object.keys(${discriminatorMapKey}).join(', ')}\`);`);
|
|
152
|
+
if (prop.ref) {
|
|
153
|
+
ret.push(` entity${entityKey} = Reference.create(factory.createReference(targetClass, data${dataKey}.id, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} }));`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
ret.push(` entity${entityKey} = factory.createReference(targetClass, data${dataKey}.id, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} });`);
|
|
157
|
+
}
|
|
144
158
|
}
|
|
145
159
|
else {
|
|
146
|
-
|
|
160
|
+
// For regular: fixed target class, PK is the data itself
|
|
161
|
+
const targetKey = this.safeKey(`${prop.targetMeta.tableName}_${this.tmpIndex++}`);
|
|
162
|
+
context.set(targetKey, prop.targetMeta.class);
|
|
163
|
+
if (prop.ref) {
|
|
164
|
+
ret.push(` entity${entityKey} = Reference.create(factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} }));`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
ret.push(` entity${entityKey} = factory.createReference(${targetKey}, data${dataKey}, { merge: true, convertCustomTypes, normalizeAccessors, schema${keyOption} });`);
|
|
168
|
+
}
|
|
147
169
|
}
|
|
148
170
|
ret.push(` } else if (data${dataKey} && typeof data${dataKey} === 'object') {`);
|
|
171
|
+
// For full entity hydration, polymorphic needs to determine target class from entity itself
|
|
172
|
+
let hydrateTargetExpr;
|
|
173
|
+
if (prop.polymorphic) {
|
|
174
|
+
hydrateTargetExpr = `data${dataKey}.constructor`;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const targetKey = this.safeKey(`${prop.targetMeta.tableName}_${this.tmpIndex++}`);
|
|
178
|
+
context.set(targetKey, prop.targetMeta.class);
|
|
179
|
+
hydrateTargetExpr = targetKey;
|
|
180
|
+
}
|
|
149
181
|
if (prop.ref) {
|
|
150
|
-
ret.push(` entity${entityKey} = Reference.create(factory.${method}(${
|
|
182
|
+
ret.push(` entity${entityKey} = Reference.create(factory.${method}(${hydrateTargetExpr}, data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, normalizeAccessors, schema }));`);
|
|
151
183
|
}
|
|
152
184
|
else {
|
|
153
|
-
ret.push(` entity${entityKey} = factory.${method}(${
|
|
185
|
+
ret.push(` entity${entityKey} = factory.${method}(${hydrateTargetExpr}, data${dataKey}, { initialized: true, merge: true, newEntity, convertCustomTypes, normalizeAccessors, schema });`);
|
|
154
186
|
}
|
|
155
187
|
ret.push(` }`);
|
|
156
188
|
ret.push(` }`);
|
|
@@ -7,7 +7,7 @@ type TypeType = string | NumberConstructor | StringConstructor | BooleanConstruc
|
|
|
7
7
|
type TypeDef<Target> = {
|
|
8
8
|
type: TypeType;
|
|
9
9
|
} | {
|
|
10
|
-
entity: () => EntityName<Target
|
|
10
|
+
entity: () => EntityName<Target> | EntityName[];
|
|
11
11
|
};
|
|
12
12
|
type EmbeddedTypeDef<Target> = {
|
|
13
13
|
type: TypeType;
|
|
@@ -42,11 +42,33 @@ export declare class MetadataDiscovery {
|
|
|
42
42
|
private initFactoryField;
|
|
43
43
|
private ensureCorrectFKOrderInPivotEntity;
|
|
44
44
|
private definePivotTableEntity;
|
|
45
|
+
/**
|
|
46
|
+
* Create a scalar property for a pivot table column.
|
|
47
|
+
*/
|
|
48
|
+
private createPivotScalarProperty;
|
|
49
|
+
/**
|
|
50
|
+
* Get column types for an entity's primary keys, initializing them if needed.
|
|
51
|
+
*/
|
|
52
|
+
private getPrimaryKeyColumnTypes;
|
|
53
|
+
/**
|
|
54
|
+
* Add missing FK columns for a polymorphic entity to an existing pivot table.
|
|
55
|
+
*/
|
|
56
|
+
private addPolymorphicPivotColumns;
|
|
57
|
+
/**
|
|
58
|
+
* Define properties for a polymorphic pivot table.
|
|
59
|
+
*/
|
|
60
|
+
private definePolymorphicPivotProperties;
|
|
61
|
+
/**
|
|
62
|
+
* Create a virtual M:1 relation from pivot to a polymorphic owner entity.
|
|
63
|
+
* This enables single-query join loading for inverse-side polymorphic M:N.
|
|
64
|
+
*/
|
|
65
|
+
private definePolymorphicOwnerRelation;
|
|
45
66
|
private defineFixedOrderProperty;
|
|
46
67
|
private definePivotProperty;
|
|
47
68
|
private autoWireBidirectionalProperties;
|
|
48
69
|
private defineBaseEntityProperties;
|
|
49
70
|
private initPolyEmbeddables;
|
|
71
|
+
private initPolymorphicRelation;
|
|
50
72
|
private initEmbeddables;
|
|
51
73
|
private initSingleTableInheritance;
|
|
52
74
|
private createDiscriminatorProperty;
|