@mikro-orm/core 7.0.0-dev.99 → 7.0.0-rc.1
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 +34 -17
- package/EntityManager.js +95 -103
- package/MikroORM.d.ts +5 -5
- package/MikroORM.js +25 -20
- package/cache/FileCacheAdapter.js +11 -3
- package/connections/Connection.d.ts +3 -2
- package/connections/Connection.js +4 -3
- package/drivers/DatabaseDriver.d.ts +11 -11
- package/drivers/DatabaseDriver.js +91 -25
- package/drivers/IDatabaseDriver.d.ts +50 -20
- package/entity/BaseEntity.d.ts +61 -1
- package/entity/Collection.d.ts +8 -1
- package/entity/Collection.js +12 -13
- package/entity/EntityAssigner.js +9 -9
- package/entity/EntityFactory.d.ts +6 -1
- package/entity/EntityFactory.js +40 -22
- package/entity/EntityHelper.d.ts +2 -2
- package/entity/EntityHelper.js +27 -4
- package/entity/EntityLoader.d.ts +5 -4
- package/entity/EntityLoader.js +193 -80
- package/entity/EntityRepository.d.ts +27 -7
- package/entity/EntityRepository.js +8 -2
- package/entity/PolymorphicRef.d.ts +12 -0
- package/entity/PolymorphicRef.js +18 -0
- package/entity/WrappedEntity.d.ts +2 -2
- package/entity/WrappedEntity.js +1 -1
- package/entity/defineEntity.d.ts +89 -50
- package/entity/defineEntity.js +12 -0
- package/entity/index.d.ts +1 -0
- package/entity/index.js +1 -0
- package/entity/utils.d.ts +6 -1
- package/entity/utils.js +33 -0
- package/entity/validators.js +2 -2
- package/enums.d.ts +2 -2
- package/enums.js +1 -0
- package/errors.d.ts +16 -8
- package/errors.js +40 -13
- package/hydration/ObjectHydrator.js +63 -21
- package/index.d.ts +1 -1
- package/logging/colors.d.ts +1 -1
- package/logging/colors.js +7 -6
- package/logging/inspect.js +1 -6
- package/metadata/EntitySchema.d.ts +43 -13
- package/metadata/EntitySchema.js +82 -27
- package/metadata/MetadataDiscovery.d.ts +60 -3
- package/metadata/MetadataDiscovery.js +665 -154
- package/metadata/MetadataProvider.js +3 -1
- package/metadata/MetadataStorage.d.ts +13 -6
- package/metadata/MetadataStorage.js +64 -19
- package/metadata/MetadataValidator.d.ts +32 -2
- package/metadata/MetadataValidator.js +196 -31
- package/metadata/discover-entities.js +5 -5
- package/metadata/types.d.ts +111 -14
- package/naming-strategy/AbstractNamingStrategy.d.ts +11 -3
- package/naming-strategy/AbstractNamingStrategy.js +12 -0
- package/naming-strategy/EntityCaseNamingStrategy.d.ts +3 -3
- package/naming-strategy/EntityCaseNamingStrategy.js +6 -5
- package/naming-strategy/MongoNamingStrategy.d.ts +3 -3
- package/naming-strategy/MongoNamingStrategy.js +6 -6
- package/naming-strategy/NamingStrategy.d.ts +17 -3
- package/naming-strategy/UnderscoreNamingStrategy.d.ts +3 -3
- package/naming-strategy/UnderscoreNamingStrategy.js +6 -6
- package/package.json +2 -2
- package/platforms/Platform.d.ts +4 -2
- package/platforms/Platform.js +5 -2
- package/serialization/EntitySerializer.d.ts +3 -0
- package/serialization/EntitySerializer.js +15 -13
- package/serialization/EntityTransformer.js +6 -6
- package/serialization/SerializationContext.d.ts +6 -6
- package/typings.d.ts +325 -110
- package/typings.js +84 -17
- package/unit-of-work/ChangeSet.d.ts +4 -3
- package/unit-of-work/ChangeSet.js +2 -3
- package/unit-of-work/ChangeSetComputer.d.ts +3 -6
- package/unit-of-work/ChangeSetComputer.js +34 -13
- package/unit-of-work/ChangeSetPersister.d.ts +12 -10
- package/unit-of-work/ChangeSetPersister.js +55 -25
- package/unit-of-work/CommitOrderCalculator.d.ts +12 -10
- package/unit-of-work/CommitOrderCalculator.js +13 -13
- package/unit-of-work/IdentityMap.d.ts +12 -0
- package/unit-of-work/IdentityMap.js +39 -1
- package/unit-of-work/UnitOfWork.d.ts +21 -3
- package/unit-of-work/UnitOfWork.js +203 -56
- package/utils/AbstractSchemaGenerator.js +17 -8
- package/utils/AsyncContext.d.ts +6 -0
- package/utils/AsyncContext.js +42 -0
- package/utils/Configuration.d.ts +52 -11
- package/utils/Configuration.js +12 -8
- package/utils/Cursor.js +21 -8
- package/utils/DataloaderUtils.js +13 -11
- package/utils/EntityComparator.d.ts +14 -7
- package/utils/EntityComparator.js +132 -46
- package/utils/QueryHelper.d.ts +16 -6
- package/utils/QueryHelper.js +53 -18
- package/utils/RawQueryFragment.d.ts +28 -23
- package/utils/RawQueryFragment.js +34 -56
- package/utils/RequestContext.js +2 -2
- package/utils/TransactionContext.js +2 -2
- package/utils/TransactionManager.js +1 -1
- package/utils/Utils.d.ts +7 -26
- package/utils/Utils.js +25 -79
- package/utils/clone.js +7 -21
- package/utils/env-vars.d.ts +4 -0
- package/utils/env-vars.js +13 -3
- package/utils/fs-utils.d.ts +21 -0
- package/utils/fs-utils.js +106 -11
- package/utils/upsert-utils.d.ts +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
import { EntitySchema } from './EntitySchema.js';
|
|
2
3
|
export class MetadataProvider {
|
|
3
4
|
config;
|
|
4
5
|
constructor(config) {
|
|
@@ -13,8 +14,9 @@ export class MetadataProvider {
|
|
|
13
14
|
else if (prop.entity) {
|
|
14
15
|
const tmp = prop.entity();
|
|
15
16
|
prop.type = Array.isArray(tmp) ? tmp.map(t => Utils.className(t)).sort().join(' | ') : Utils.className(tmp);
|
|
17
|
+
prop.target = tmp instanceof EntitySchema ? tmp.meta.class : tmp;
|
|
16
18
|
}
|
|
17
|
-
else if (!prop.type && !(prop.enum && (prop.items?.length ?? 0) > 0)) {
|
|
19
|
+
else if (!prop.type && !((prop.enum || prop.array) && (prop.items?.length ?? 0) > 0)) {
|
|
18
20
|
throw new Error(`Please provide either 'type' or 'entity' attribute in ${meta.className}.${prop.name}.`);
|
|
19
21
|
}
|
|
20
22
|
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Dictionary, EntityMetadata, type EntityName } from '../typings.js';
|
|
2
2
|
import type { EntityManager } from '../EntityManager.js';
|
|
3
3
|
export declare class MetadataStorage {
|
|
4
4
|
static readonly PATH_SYMBOL: unique symbol;
|
|
5
5
|
private static readonly metadata;
|
|
6
6
|
private readonly metadata;
|
|
7
|
+
private readonly idMap;
|
|
8
|
+
private readonly classNameMap;
|
|
9
|
+
private readonly uniqueNameMap;
|
|
7
10
|
constructor(metadata?: Dictionary<EntityMetadata>);
|
|
8
11
|
static getMetadata(): Dictionary<EntityMetadata>;
|
|
9
12
|
static getMetadata<T = any>(entity: string, path: string): EntityMetadata<T>;
|
|
10
13
|
static isKnownEntity(name: string): boolean;
|
|
11
14
|
static clear(): void;
|
|
12
|
-
getAll():
|
|
13
|
-
get<T = any>(entityName: EntityName<T>, init?: boolean
|
|
15
|
+
getAll(): Map<EntityName, EntityMetadata>;
|
|
16
|
+
get<T = any>(entityName: EntityName<T>, init?: boolean): EntityMetadata<T>;
|
|
14
17
|
find<T = any>(entityName: EntityName<T>): EntityMetadata<T> | undefined;
|
|
15
|
-
has(
|
|
16
|
-
set(
|
|
17
|
-
reset(
|
|
18
|
+
has<T>(entityName: EntityName<T>): boolean;
|
|
19
|
+
set<T>(entityName: EntityName<T>, meta: EntityMetadata): EntityMetadata;
|
|
20
|
+
reset<T>(entityName: EntityName<T>): void;
|
|
18
21
|
decorate(em: EntityManager): void;
|
|
19
22
|
[Symbol.iterator](): IterableIterator<EntityMetadata>;
|
|
23
|
+
getById<T>(id: number): EntityMetadata<T>;
|
|
24
|
+
getByClassName<T = any, V extends boolean = true>(className: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
|
|
25
|
+
getByUniqueName<T = any, V extends boolean = true>(uniqueName: string, validate?: V): V extends true ? EntityMetadata<T> : EntityMetadata<T> | undefined;
|
|
26
|
+
private validate;
|
|
20
27
|
}
|
|
@@ -2,6 +2,7 @@ import { EntityMetadata } from '../typings.js';
|
|
|
2
2
|
import { Utils } from '../utils/Utils.js';
|
|
3
3
|
import { MetadataError } from '../errors.js';
|
|
4
4
|
import { EntityHelper } from '../entity/EntityHelper.js';
|
|
5
|
+
import { EntitySchema } from './EntitySchema.js';
|
|
5
6
|
function getGlobalStorage(namespace) {
|
|
6
7
|
const key = `mikro-orm-${namespace}`;
|
|
7
8
|
globalThis[key] = globalThis[key] || {};
|
|
@@ -10,9 +11,19 @@ function getGlobalStorage(namespace) {
|
|
|
10
11
|
export class MetadataStorage {
|
|
11
12
|
static PATH_SYMBOL = Symbol('MetadataStorage.PATH_SYMBOL');
|
|
12
13
|
static metadata = getGlobalStorage('metadata');
|
|
13
|
-
metadata;
|
|
14
|
+
metadata = new Map();
|
|
15
|
+
idMap;
|
|
16
|
+
classNameMap;
|
|
17
|
+
uniqueNameMap;
|
|
14
18
|
constructor(metadata = {}) {
|
|
15
|
-
this.
|
|
19
|
+
this.idMap = {};
|
|
20
|
+
this.uniqueNameMap = {};
|
|
21
|
+
this.classNameMap = Utils.copy(metadata, false);
|
|
22
|
+
for (const meta of Object.values(this.classNameMap)) {
|
|
23
|
+
this.idMap[meta._id] = meta;
|
|
24
|
+
this.uniqueNameMap[meta.uniqueName] = meta;
|
|
25
|
+
this.metadata.set(meta.class, meta);
|
|
26
|
+
}
|
|
16
27
|
}
|
|
17
28
|
static getMetadata(entity, path) {
|
|
18
29
|
const key = entity && path ? entity + '-' + Utils.hash(path) : null;
|
|
@@ -33,40 +44,74 @@ export class MetadataStorage {
|
|
|
33
44
|
getAll() {
|
|
34
45
|
return this.metadata;
|
|
35
46
|
}
|
|
36
|
-
get(entityName, init = false
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
47
|
+
get(entityName, init = false) {
|
|
48
|
+
const exists = this.find(entityName);
|
|
49
|
+
if (exists) {
|
|
50
|
+
return exists;
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
const className = Utils.className(entityName);
|
|
53
|
+
if (!init) {
|
|
54
|
+
throw MetadataError.missingMetadata(className);
|
|
43
55
|
}
|
|
44
|
-
|
|
56
|
+
const meta = new EntityMetadata({ class: entityName, name: className });
|
|
57
|
+
this.set(entityName, meta);
|
|
58
|
+
return meta;
|
|
45
59
|
}
|
|
46
60
|
find(entityName) {
|
|
47
61
|
if (!entityName) {
|
|
48
62
|
return;
|
|
49
63
|
}
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
const meta = this.metadata.get(entityName);
|
|
65
|
+
if (meta) {
|
|
66
|
+
return meta;
|
|
67
|
+
}
|
|
68
|
+
if (entityName instanceof EntitySchema) {
|
|
69
|
+
return this.metadata.get(entityName.meta.class) ?? entityName.meta;
|
|
70
|
+
}
|
|
71
|
+
return this.classNameMap[Utils.className(entityName)];
|
|
52
72
|
}
|
|
53
|
-
has(
|
|
54
|
-
return
|
|
73
|
+
has(entityName) {
|
|
74
|
+
return this.metadata.has(entityName);
|
|
55
75
|
}
|
|
56
|
-
set(
|
|
57
|
-
|
|
76
|
+
set(entityName, meta) {
|
|
77
|
+
this.metadata.set(entityName, meta);
|
|
78
|
+
this.idMap[meta._id] = meta;
|
|
79
|
+
this.uniqueNameMap[meta.uniqueName] = meta;
|
|
80
|
+
this.classNameMap[Utils.className(entityName)] = meta;
|
|
81
|
+
return meta;
|
|
58
82
|
}
|
|
59
|
-
reset(
|
|
60
|
-
|
|
83
|
+
reset(entityName) {
|
|
84
|
+
const meta = this.find(entityName);
|
|
85
|
+
if (meta) {
|
|
86
|
+
this.metadata.delete(meta.class);
|
|
87
|
+
delete this.idMap[meta._id];
|
|
88
|
+
delete this.uniqueNameMap[meta.uniqueName];
|
|
89
|
+
delete this.classNameMap[meta.className];
|
|
90
|
+
}
|
|
61
91
|
}
|
|
62
92
|
decorate(em) {
|
|
63
|
-
|
|
93
|
+
[...this.metadata.values()]
|
|
64
94
|
.filter(meta => meta.prototype)
|
|
65
95
|
.forEach(meta => EntityHelper.decorate(meta, em));
|
|
66
96
|
}
|
|
67
97
|
*[Symbol.iterator]() {
|
|
68
|
-
for (const meta of
|
|
98
|
+
for (const meta of this.metadata.values()) {
|
|
69
99
|
yield meta;
|
|
70
100
|
}
|
|
71
101
|
}
|
|
102
|
+
getById(id) {
|
|
103
|
+
return this.idMap[id];
|
|
104
|
+
}
|
|
105
|
+
getByClassName(className, validate = true) {
|
|
106
|
+
return this.validate(this.classNameMap[className], className, validate);
|
|
107
|
+
}
|
|
108
|
+
getByUniqueName(uniqueName, validate = true) {
|
|
109
|
+
return this.validate(this.uniqueNameMap[uniqueName], uniqueName, validate);
|
|
110
|
+
}
|
|
111
|
+
validate(meta, id, validate) {
|
|
112
|
+
if (!meta && validate) {
|
|
113
|
+
throw MetadataError.missingMetadata(id);
|
|
114
|
+
}
|
|
115
|
+
return meta;
|
|
116
|
+
}
|
|
72
117
|
}
|
|
@@ -1,17 +1,47 @@
|
|
|
1
|
-
import type { EntityMetadata } from '../typings.js';
|
|
1
|
+
import type { EntityMetadata, EntityName } from '../typings.js';
|
|
2
2
|
import { type MetadataDiscoveryOptions } from '../utils/Configuration.js';
|
|
3
3
|
import type { MetadataStorage } from './MetadataStorage.js';
|
|
4
4
|
/**
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
7
|
export declare class MetadataValidator {
|
|
8
|
-
validateEntityDefinition<T>(metadata: MetadataStorage, name:
|
|
8
|
+
validateEntityDefinition<T>(metadata: MetadataStorage, name: EntityName<T>, options: MetadataDiscoveryOptions): void;
|
|
9
9
|
validateDiscovered(discovered: EntityMetadata[], options: MetadataDiscoveryOptions): void;
|
|
10
10
|
private validateReference;
|
|
11
|
+
private validateTargetKey;
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a property has a unique constraint (either via `unique: true` or single-property `@Unique` decorator).
|
|
14
|
+
* Composite unique constraints are not sufficient for targetKey.
|
|
15
|
+
*/
|
|
16
|
+
private isPropertyUnique;
|
|
17
|
+
private validatePolymorphicTargets;
|
|
11
18
|
private validateBidirectional;
|
|
12
19
|
private validateOwningSide;
|
|
13
20
|
private validateInverseSide;
|
|
14
21
|
private validateIndexes;
|
|
15
22
|
private validateDuplicateFieldNames;
|
|
16
23
|
private validateVersionField;
|
|
24
|
+
/**
|
|
25
|
+
* Validates that entity properties do not use dangerous names that could lead to
|
|
26
|
+
* prototype pollution vulnerabilities. This validation ensures that property names
|
|
27
|
+
* cannot be exploited to modify object prototypes when values are assigned during
|
|
28
|
+
* entity hydration or persistence operations.
|
|
29
|
+
*
|
|
30
|
+
* @internal
|
|
31
|
+
*/
|
|
32
|
+
private validatePropertyNames;
|
|
33
|
+
/**
|
|
34
|
+
* Validates view entity configuration.
|
|
35
|
+
* View entities must have an expression.
|
|
36
|
+
*/
|
|
37
|
+
private validateViewEntity;
|
|
38
|
+
/**
|
|
39
|
+
* Validates that STI and TPT are not mixed in the same inheritance hierarchy.
|
|
40
|
+
* An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
|
|
41
|
+
* but not both.
|
|
42
|
+
*
|
|
43
|
+
* Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
|
|
44
|
+
* so we check the raw `inheritance` option from the decorator/schema.
|
|
45
|
+
*/
|
|
46
|
+
private validateInheritanceStrategies;
|
|
17
47
|
}
|
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
import { Utils } from '../utils/Utils.js';
|
|
2
2
|
import { MetadataError } from '../errors.js';
|
|
3
3
|
import { ReferenceKind } from '../enums.js';
|
|
4
|
+
/**
|
|
5
|
+
* List of property names that could lead to prototype pollution vulnerabilities.
|
|
6
|
+
* These names should never be used as entity property names because they could
|
|
7
|
+
* allow malicious code to modify object prototypes when property values are assigned.
|
|
8
|
+
*
|
|
9
|
+
* - `__proto__`: Could modify the prototype chain
|
|
10
|
+
* - `constructor`: Could modify the constructor property
|
|
11
|
+
* - `prototype`: Could modify the prototype object
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
const DANGEROUS_PROPERTY_NAMES = ['__proto__', 'constructor', 'prototype'];
|
|
4
16
|
/**
|
|
5
17
|
* @internal
|
|
6
18
|
*/
|
|
7
19
|
export class MetadataValidator {
|
|
8
20
|
validateEntityDefinition(metadata, name, options) {
|
|
9
21
|
const meta = metadata.get(name);
|
|
10
|
-
|
|
22
|
+
// View entities (expression with view flag) behave like regular tables but are read-only
|
|
23
|
+
// They can have primary keys and are created as actual database views
|
|
24
|
+
if (meta.view) {
|
|
25
|
+
this.validateViewEntity(meta);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Virtual entities (expression without view flag) have restrictions - no PKs, limited relation types
|
|
29
|
+
// Note: meta.virtual is set later in sync(), so we check for expression && !view here
|
|
30
|
+
if (meta.virtual || (meta.expression && !meta.view)) {
|
|
11
31
|
for (const prop of Utils.values(meta.properties)) {
|
|
12
32
|
if (![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED, ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
13
33
|
throw new MetadataError(`Only scalars, embedded properties and to-many relations are allowed inside virtual entity. Found '${prop.kind}' in ${meta.className}.${prop.name}`);
|
|
@@ -26,13 +46,14 @@ export class MetadataValidator {
|
|
|
26
46
|
this.validateDuplicateFieldNames(meta, options);
|
|
27
47
|
this.validateIndexes(meta, meta.indexes ?? [], 'index');
|
|
28
48
|
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
|
|
49
|
+
this.validatePropertyNames(meta);
|
|
29
50
|
for (const prop of Utils.values(meta.properties)) {
|
|
30
51
|
if (prop.kind !== ReferenceKind.SCALAR) {
|
|
31
|
-
this.validateReference(meta, prop,
|
|
32
|
-
this.validateBidirectional(meta, prop
|
|
52
|
+
this.validateReference(meta, prop, options);
|
|
53
|
+
this.validateBidirectional(meta, prop);
|
|
33
54
|
}
|
|
34
|
-
else if (metadata.
|
|
35
|
-
throw MetadataError.propertyTargetsEntityType(meta, prop, metadata.
|
|
55
|
+
else if (metadata.getByClassName(prop.type, false)) {
|
|
56
|
+
throw MetadataError.propertyTargetsEntityType(meta, prop, metadata.getByClassName(prop.type));
|
|
36
57
|
}
|
|
37
58
|
}
|
|
38
59
|
}
|
|
@@ -40,17 +61,15 @@ export class MetadataValidator {
|
|
|
40
61
|
if (discovered.length === 0 && options.warnWhenNoEntities) {
|
|
41
62
|
throw MetadataError.noEntityDiscovered();
|
|
42
63
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
const tableNames = discovered.filter(meta => !meta.abstract && meta === meta.root && (meta.tableName || meta.collection) && meta.schema !== '*');
|
|
64
|
+
// Validate no mixing of STI and TPT in the same hierarchy
|
|
65
|
+
this.validateInheritanceStrategies(discovered);
|
|
66
|
+
const tableNames = discovered.filter(meta => !meta.abstract && !meta.embeddable && meta === meta.root && (meta.tableName || meta.collection) && meta.schema !== '*');
|
|
48
67
|
const duplicateTableNames = Utils.findDuplicates(tableNames.map(meta => {
|
|
49
68
|
const tableName = meta.tableName || meta.collection;
|
|
50
69
|
return (meta.schema ? '.' + meta.schema : '') + tableName;
|
|
51
70
|
}));
|
|
52
|
-
if (duplicateTableNames.length > 0 && options.checkDuplicateTableNames
|
|
53
|
-
throw MetadataError.duplicateEntityDiscovered(duplicateTableNames
|
|
71
|
+
if (duplicateTableNames.length > 0 && options.checkDuplicateTableNames) {
|
|
72
|
+
throw MetadataError.duplicateEntityDiscovered(duplicateTableNames);
|
|
54
73
|
}
|
|
55
74
|
// validate we found at least one entity (not just abstract/base entities)
|
|
56
75
|
if (discovered.filter(meta => meta.name).length === 0 && options.warnWhenNoEntities) {
|
|
@@ -61,7 +80,7 @@ export class MetadataValidator {
|
|
|
61
80
|
.replace(/\[]$/, '') // remove array suffix
|
|
62
81
|
.replace(/\((.*)\)/, '$1'); // unwrap union types
|
|
63
82
|
const name = (p) => {
|
|
64
|
-
if (typeof p === 'function') {
|
|
83
|
+
if (typeof p === 'function' && !p.prototype) {
|
|
65
84
|
return Utils.className(p());
|
|
66
85
|
}
|
|
67
86
|
return Utils.className(p);
|
|
@@ -85,47 +104,134 @@ export class MetadataValidator {
|
|
|
85
104
|
}
|
|
86
105
|
});
|
|
87
106
|
}
|
|
88
|
-
validateReference(meta, prop,
|
|
107
|
+
validateReference(meta, prop, options) {
|
|
89
108
|
// references do have types
|
|
90
109
|
if (!prop.type) {
|
|
91
110
|
throw MetadataError.fromWrongTypeDefinition(meta, prop);
|
|
92
111
|
}
|
|
93
|
-
|
|
112
|
+
// Polymorphic relations have multiple targets, validate PK compatibility
|
|
113
|
+
if (prop.polymorphic && prop.polymorphTargets) {
|
|
114
|
+
this.validatePolymorphicTargets(meta, prop);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const targetMeta = prop.targetMeta;
|
|
94
118
|
// references do have type of known entity
|
|
95
119
|
if (!targetMeta) {
|
|
96
120
|
throw MetadataError.fromWrongTypeDefinition(meta, prop);
|
|
97
121
|
}
|
|
98
|
-
if (targetMeta.abstract && !targetMeta.
|
|
122
|
+
if (targetMeta.abstract && !targetMeta.root?.inheritanceType && !targetMeta.embeddable) {
|
|
99
123
|
throw MetadataError.targetIsAbstract(meta, prop);
|
|
100
124
|
}
|
|
101
125
|
if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.persist === false && targetMeta.compositePK && options.checkNonPersistentCompositeProps) {
|
|
102
126
|
throw MetadataError.nonPersistentCompositeProp(meta, prop);
|
|
103
127
|
}
|
|
128
|
+
this.validateTargetKey(meta, prop, targetMeta);
|
|
129
|
+
}
|
|
130
|
+
validateTargetKey(meta, prop, targetMeta) {
|
|
131
|
+
if (!prop.targetKey) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// targetKey is not supported for ManyToMany relations
|
|
135
|
+
if (prop.kind === ReferenceKind.MANY_TO_MANY) {
|
|
136
|
+
throw MetadataError.targetKeyOnManyToMany(meta, prop);
|
|
137
|
+
}
|
|
138
|
+
// targetKey must point to an existing property
|
|
139
|
+
const targetProp = targetMeta.properties[prop.targetKey];
|
|
140
|
+
if (!targetProp) {
|
|
141
|
+
throw MetadataError.targetKeyNotFound(meta, prop);
|
|
142
|
+
}
|
|
143
|
+
// targetKey must point to a unique property (composite unique is not sufficient)
|
|
144
|
+
if (!this.isPropertyUnique(targetProp, targetMeta)) {
|
|
145
|
+
throw MetadataError.targetKeyNotUnique(meta, prop);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Checks if a property has a unique constraint (either via `unique: true` or single-property `@Unique` decorator).
|
|
150
|
+
* Composite unique constraints are not sufficient for targetKey.
|
|
151
|
+
*/
|
|
152
|
+
isPropertyUnique(prop, meta) {
|
|
153
|
+
if (prop.unique) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Check for single-property unique constraint via @Unique decorator
|
|
157
|
+
return !!meta.uniques?.some(u => {
|
|
158
|
+
const props = Utils.asArray(u.properties);
|
|
159
|
+
return props.length === 1 && props[0] === prop.name && !u.options;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
validatePolymorphicTargets(meta, prop) {
|
|
163
|
+
const targets = prop.polymorphTargets;
|
|
164
|
+
// Validate targetKey exists and is compatible across all targets
|
|
165
|
+
if (prop.targetKey) {
|
|
166
|
+
for (const target of targets) {
|
|
167
|
+
const targetProp = target.properties[prop.targetKey];
|
|
168
|
+
if (!targetProp) {
|
|
169
|
+
throw MetadataError.targetKeyNotFound(meta, prop, target);
|
|
170
|
+
}
|
|
171
|
+
// targetKey must point to a unique property (composite unique is not sufficient)
|
|
172
|
+
if (!this.isPropertyUnique(targetProp, target)) {
|
|
173
|
+
throw MetadataError.targetKeyNotUnique(meta, prop, target);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const firstPKs = targets[0].getPrimaryProps();
|
|
178
|
+
for (let i = 1; i < targets.length; i++) {
|
|
179
|
+
const target = targets[i];
|
|
180
|
+
const targetPKs = target.getPrimaryProps();
|
|
181
|
+
if (targetPKs.length !== firstPKs.length) {
|
|
182
|
+
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, 'different number of primary keys');
|
|
183
|
+
}
|
|
184
|
+
for (let j = 0; j < firstPKs.length; j++) {
|
|
185
|
+
const firstPK = firstPKs[j];
|
|
186
|
+
const targetPK = targetPKs[j];
|
|
187
|
+
if (firstPK.runtimeType !== targetPK.runtimeType) {
|
|
188
|
+
throw MetadataError.incompatiblePolymorphicTargets(meta, prop, targets[0], target, `incompatible primary key types: ${firstPK.name} (${firstPK.runtimeType}) vs ${targetPK.name} (${targetPK.runtimeType})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
104
192
|
}
|
|
105
|
-
validateBidirectional(meta, prop
|
|
193
|
+
validateBidirectional(meta, prop) {
|
|
106
194
|
if (prop.inversedBy) {
|
|
107
|
-
|
|
108
|
-
this.validateOwningSide(meta, prop, inverse, metadata);
|
|
195
|
+
this.validateOwningSide(meta, prop);
|
|
109
196
|
}
|
|
110
197
|
else if (prop.mappedBy) {
|
|
111
|
-
|
|
112
|
-
this.validateInverseSide(meta, prop, inverse, metadata);
|
|
198
|
+
this.validateInverseSide(meta, prop);
|
|
113
199
|
}
|
|
114
|
-
else {
|
|
200
|
+
else if (prop.kind === ReferenceKind.ONE_TO_MANY && !prop.mappedBy) {
|
|
115
201
|
// 1:m property has `mappedBy`
|
|
116
|
-
|
|
117
|
-
throw MetadataError.fromMissingOption(meta, prop, 'mappedBy');
|
|
118
|
-
}
|
|
202
|
+
throw MetadataError.fromMissingOption(meta, prop, 'mappedBy');
|
|
119
203
|
}
|
|
120
204
|
}
|
|
121
|
-
validateOwningSide(meta, prop
|
|
205
|
+
validateOwningSide(meta, prop) {
|
|
206
|
+
// For polymorphic relations, inversedBy may point to multiple entity types
|
|
207
|
+
if (prop.polymorphic && prop.polymorphTargets?.length) {
|
|
208
|
+
// For polymorphic relations, validate inversedBy against each target
|
|
209
|
+
// The inverse property should exist on the target entities and reference back to this property
|
|
210
|
+
for (const targetMeta of prop.polymorphTargets) {
|
|
211
|
+
const inverse = targetMeta.properties[prop.inversedBy];
|
|
212
|
+
// The inverse property is optional - some targets may not have it
|
|
213
|
+
if (!inverse) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
// Validate the inverse property
|
|
217
|
+
if (inverse.targetMeta?.root.class !== meta.root.class) {
|
|
218
|
+
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
|
|
219
|
+
}
|
|
220
|
+
// inverse side is not defined as owner
|
|
221
|
+
if (inverse.inversedBy || inverse.owner) {
|
|
222
|
+
throw MetadataError.fromWrongOwnership(meta, prop, 'inversedBy');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const inverse = prop.targetMeta.properties[prop.inversedBy];
|
|
122
228
|
// has correct `inversedBy` on owning side
|
|
123
229
|
if (!inverse) {
|
|
124
230
|
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy');
|
|
125
231
|
}
|
|
126
|
-
const
|
|
232
|
+
const targetClass = inverse.targetMeta?.root.class;
|
|
127
233
|
// has correct `inversedBy` reference type
|
|
128
|
-
if (inverse.type !== meta.className &&
|
|
234
|
+
if (inverse.type !== meta.className && targetClass !== meta.root.class) {
|
|
129
235
|
throw MetadataError.fromWrongReference(meta, prop, 'inversedBy', inverse);
|
|
130
236
|
}
|
|
131
237
|
// inverse side is not defined as owner
|
|
@@ -133,13 +239,16 @@ export class MetadataValidator {
|
|
|
133
239
|
throw MetadataError.fromWrongOwnership(meta, prop, 'inversedBy');
|
|
134
240
|
}
|
|
135
241
|
}
|
|
136
|
-
validateInverseSide(meta, prop
|
|
242
|
+
validateInverseSide(meta, prop) {
|
|
243
|
+
const owner = prop.targetMeta.properties[prop.mappedBy];
|
|
137
244
|
// has correct `mappedBy` on inverse side
|
|
138
245
|
if (prop.mappedBy && !owner) {
|
|
139
246
|
throw MetadataError.fromWrongReference(meta, prop, 'mappedBy');
|
|
140
247
|
}
|
|
141
248
|
// has correct `mappedBy` reference type
|
|
142
|
-
if
|
|
249
|
+
// For polymorphic relations, check if this entity is one of the polymorphic targets
|
|
250
|
+
const isValidPolymorphicInverse = owner.polymorphic && owner.polymorphTargets?.some(target => target.class === meta.root.class);
|
|
251
|
+
if (!isValidPolymorphicInverse && owner.type !== meta.className && owner.targetMeta?.root.class !== meta.root.class) {
|
|
143
252
|
throw MetadataError.fromWrongReference(meta, prop, 'mappedBy', owner);
|
|
144
253
|
}
|
|
145
254
|
// owning side is not defined as inverse
|
|
@@ -182,7 +291,7 @@ export class MetadataValidator {
|
|
|
182
291
|
return [prop.embedded ? prop.embedded.join('.') : prop.name, prop.fieldNames[0]];
|
|
183
292
|
});
|
|
184
293
|
});
|
|
185
|
-
throw MetadataError.duplicateFieldName(meta.
|
|
294
|
+
throw MetadataError.duplicateFieldName(meta.class, pairs);
|
|
186
295
|
}
|
|
187
296
|
}
|
|
188
297
|
validateVersionField(meta) {
|
|
@@ -199,4 +308,60 @@ export class MetadataValidator {
|
|
|
199
308
|
throw MetadataError.invalidVersionFieldType(meta);
|
|
200
309
|
}
|
|
201
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Validates that entity properties do not use dangerous names that could lead to
|
|
313
|
+
* prototype pollution vulnerabilities. This validation ensures that property names
|
|
314
|
+
* cannot be exploited to modify object prototypes when values are assigned during
|
|
315
|
+
* entity hydration or persistence operations.
|
|
316
|
+
*
|
|
317
|
+
* @internal
|
|
318
|
+
*/
|
|
319
|
+
validatePropertyNames(meta) {
|
|
320
|
+
for (const prop of Utils.values(meta.properties)) {
|
|
321
|
+
if (DANGEROUS_PROPERTY_NAMES.includes(prop.name)) {
|
|
322
|
+
throw MetadataError.dangerousPropertyName(meta, prop);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Validates view entity configuration.
|
|
328
|
+
* View entities must have an expression.
|
|
329
|
+
*/
|
|
330
|
+
validateViewEntity(meta) {
|
|
331
|
+
// View entities must have an expression
|
|
332
|
+
if (!meta.expression) {
|
|
333
|
+
throw MetadataError.viewEntityWithoutExpression(meta);
|
|
334
|
+
}
|
|
335
|
+
// Validate indexes if present
|
|
336
|
+
this.validateIndexes(meta, meta.indexes ?? [], 'index');
|
|
337
|
+
this.validateIndexes(meta, meta.uniques ?? [], 'unique');
|
|
338
|
+
// Validate property names
|
|
339
|
+
this.validatePropertyNames(meta);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Validates that STI and TPT are not mixed in the same inheritance hierarchy.
|
|
343
|
+
* An entity hierarchy can use either STI (discriminatorColumn) or TPT (inheritance: 'tpt'),
|
|
344
|
+
* but not both.
|
|
345
|
+
*
|
|
346
|
+
* Note: This validation runs before `initTablePerTypeInheritance` sets `inheritanceType`,
|
|
347
|
+
* so we check the raw `inheritance` option from the decorator/schema.
|
|
348
|
+
*/
|
|
349
|
+
validateInheritanceStrategies(discovered) {
|
|
350
|
+
const checkedRoots = new Set();
|
|
351
|
+
for (const meta of discovered) {
|
|
352
|
+
if (meta.embeddable) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const root = meta.root;
|
|
356
|
+
if (checkedRoots.has(root)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
checkedRoots.add(root);
|
|
360
|
+
const hasSTI = !!root.discriminatorColumn;
|
|
361
|
+
const hasTPT = root.inheritanceType === 'tpt' || root.inheritance === 'tpt';
|
|
362
|
+
if (hasSTI && hasTPT) {
|
|
363
|
+
throw MetadataError.mixedInheritanceStrategies(root, meta);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
202
367
|
}
|
|
@@ -4,8 +4,8 @@ import { Utils } from '../utils/Utils.js';
|
|
|
4
4
|
import { MetadataStorage } from './MetadataStorage.js';
|
|
5
5
|
import { EntitySchema } from './EntitySchema.js';
|
|
6
6
|
async function getEntityClassOrSchema(filepath, allTargets, baseDir) {
|
|
7
|
-
const path =
|
|
8
|
-
const exports = await
|
|
7
|
+
const path = fs.normalizePath(baseDir, filepath);
|
|
8
|
+
const exports = await fs.dynamicImport(path);
|
|
9
9
|
const targets = Object.values(exports);
|
|
10
10
|
// ignore class implementations that are linked from an EntitySchema
|
|
11
11
|
for (const item of targets) {
|
|
@@ -25,9 +25,9 @@ async function getEntityClassOrSchema(filepath, allTargets, baseDir) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
export async function discoverEntities(paths, options) {
|
|
28
|
-
paths = Utils.asArray(paths).map(path =>
|
|
29
|
-
const baseDir = options?.baseDir ?? process.cwd();
|
|
30
|
-
const files = fs.glob(paths,
|
|
28
|
+
paths = Utils.asArray(paths).map(path => fs.normalizePath(path));
|
|
29
|
+
const baseDir = fs.absolutePath(options?.baseDir ?? process.cwd());
|
|
30
|
+
const files = fs.glob(paths, fs.normalizePath(baseDir));
|
|
31
31
|
const found = new Map();
|
|
32
32
|
for (const filepath of files) {
|
|
33
33
|
const filename = basename(filepath);
|