@mikro-orm/core 7.2.0-dev.13 → 7.2.0-dev.14
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 +24 -5
- package/drivers/IDatabaseDriver.d.ts +1 -0
- package/entity/EntityFactory.js +6 -0
- package/entity/EntityLoader.js +11 -6
- package/entity/EntityRepository.js +5 -1
- package/entity/defineEntity.d.ts +31 -13
- package/entity/defineEntity.js +1 -1
- package/errors.d.ts +1 -0
- package/errors.js +3 -0
- package/events/EventManager.js +6 -3
- package/hydration/ObjectHydrator.d.ts +2 -0
- package/hydration/ObjectHydrator.js +12 -8
- package/metadata/MetadataDiscovery.js +39 -11
- package/metadata/MetadataStorage.js +17 -1
- package/package.json +1 -1
- package/platforms/Platform.d.ts +3 -2
- package/platforms/Platform.js +3 -1
- package/types/Type.js +4 -4
- package/typings.d.ts +8 -2
- package/typings.js +23 -1
- package/unit-of-work/ChangeSetPersister.js +17 -13
- package/unit-of-work/UnitOfWork.js +10 -4
- package/utils/DataloaderUtils.js +2 -1
- package/utils/EntityComparator.d.ts +2 -0
- package/utils/EntityComparator.js +7 -3
- package/utils/QueryHelper.js +12 -4
- package/utils/Utils.d.ts +12 -2
- package/utils/Utils.js +20 -2
- package/utils/upsert-utils.d.ts +9 -1
- package/utils/upsert-utils.js +26 -3
package/EntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
|
|
1
|
+
import { getOnConflictReturningFields, getOnCreateGeneratedFields, getWhereCondition, resetUntouchedCollections, } from './utils/upsert-utils.js';
|
|
2
2
|
import { Utils } from './utils/Utils.js';
|
|
3
3
|
import { Cursor } from './utils/Cursor.js';
|
|
4
4
|
import { QueryHelper } from './utils/QueryHelper.js';
|
|
@@ -260,7 +260,7 @@ export class EntityManager {
|
|
|
260
260
|
addFilter(options) {
|
|
261
261
|
options = { ...options };
|
|
262
262
|
if (options.entity) {
|
|
263
|
-
options.entity = Utils.asArray(options.entity).map(n => Utils.
|
|
263
|
+
options.entity = Utils.asArray(options.entity).map(n => Utils.classOrName(n));
|
|
264
264
|
}
|
|
265
265
|
options.default ??= true;
|
|
266
266
|
this.getContext(false).#filters[options.name] = options;
|
|
@@ -793,6 +793,7 @@ export class EntityManager {
|
|
|
793
793
|
}
|
|
794
794
|
const meta = this.metadata.get(entityName);
|
|
795
795
|
const convertCustomTypes = !Utils.isEntity(data);
|
|
796
|
+
let generatedFields = [];
|
|
796
797
|
if (Utils.isEntity(data)) {
|
|
797
798
|
entity = data;
|
|
798
799
|
if (helper(entity).__managed && helper(entity).__em === em && !this.config.get('upsertManaged')) {
|
|
@@ -800,6 +801,7 @@ export class EntityManager {
|
|
|
800
801
|
return entity;
|
|
801
802
|
}
|
|
802
803
|
where = helper(entity).getPrimaryKey();
|
|
804
|
+
generatedFields = getOnCreateGeneratedFields(meta, entity);
|
|
803
805
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
804
806
|
data = em.#comparator.prepareEntity(entity);
|
|
805
807
|
}
|
|
@@ -812,6 +814,7 @@ export class EntityManager {
|
|
|
812
814
|
return em.assign(exists, data);
|
|
813
815
|
}
|
|
814
816
|
}
|
|
817
|
+
generatedFields = getOnCreateGeneratedFields(meta, data);
|
|
815
818
|
em.#entityFactory.assignDefaultValues(data, meta, true);
|
|
816
819
|
for (const key of Object.keys(data)) {
|
|
817
820
|
const prop = meta.properties[key];
|
|
@@ -820,6 +823,10 @@ export class EntityManager {
|
|
|
820
823
|
}
|
|
821
824
|
}
|
|
822
825
|
}
|
|
826
|
+
// `onCreate` generated values are for the insert clause only, they must not overwrite an existing row
|
|
827
|
+
if (generatedFields.length > 0 && !options.onConflictMergeFields) {
|
|
828
|
+
options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
|
|
829
|
+
}
|
|
823
830
|
where = getWhereCondition(meta, options.onConflictFields, data, where).where;
|
|
824
831
|
data = QueryHelper.processObjectParams(data);
|
|
825
832
|
validateParams(data, 'insert data');
|
|
@@ -939,6 +946,7 @@ export class EntityManager {
|
|
|
939
946
|
}
|
|
940
947
|
const meta = this.metadata.get(entityName);
|
|
941
948
|
const convertCustomTypes = !Utils.isEntity(data[0]);
|
|
949
|
+
const generatedFields = new Set();
|
|
942
950
|
const allData = [];
|
|
943
951
|
const allWhere = [];
|
|
944
952
|
const entities = new Map();
|
|
@@ -956,6 +964,7 @@ export class EntityManager {
|
|
|
956
964
|
continue;
|
|
957
965
|
}
|
|
958
966
|
where = helper(entity).getPrimaryKey();
|
|
967
|
+
getOnCreateGeneratedFields(meta, entity).forEach(field => generatedFields.add(field));
|
|
959
968
|
em.#entityFactory.assignDefaultValues(entity, meta);
|
|
960
969
|
entitiesByAllDataIdx.set(allData.length, entity);
|
|
961
970
|
row = em.#comparator.prepareEntity(entity);
|
|
@@ -972,6 +981,7 @@ export class EntityManager {
|
|
|
972
981
|
continue;
|
|
973
982
|
}
|
|
974
983
|
}
|
|
984
|
+
getOnCreateGeneratedFields(meta, row).forEach(field => generatedFields.add(field));
|
|
975
985
|
em.#entityFactory.assignDefaultValues(row, meta, true);
|
|
976
986
|
for (const key of Object.keys(row)) {
|
|
977
987
|
const prop = meta.properties[key];
|
|
@@ -1000,6 +1010,10 @@ export class EntityManager {
|
|
|
1000
1010
|
if (entities.size === data.length) {
|
|
1001
1011
|
return [...entities.keys()];
|
|
1002
1012
|
}
|
|
1013
|
+
// `onCreate` generated values are for the insert clause only, they must not overwrite existing rows
|
|
1014
|
+
if (generatedFields.size > 0 && !options.onConflictMergeFields) {
|
|
1015
|
+
options.onConflictExcludeFields = [...(options.onConflictExcludeFields ?? []), ...generatedFields];
|
|
1016
|
+
}
|
|
1003
1017
|
if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
|
|
1004
1018
|
for (const dto of data) {
|
|
1005
1019
|
const entity = entitiesByData.get(dto) ?? dto;
|
|
@@ -1649,8 +1663,9 @@ export class EntityManager {
|
|
|
1649
1663
|
}
|
|
1650
1664
|
// For TPT inheritance, check the entity's own properties, not just the root's
|
|
1651
1665
|
// For STI, meta.properties includes all properties anyway
|
|
1652
|
-
|
|
1653
|
-
|
|
1666
|
+
// use an own-property check so inherited `Object.prototype` members (e.g. `__proto__`, `constructor`) are not treated as populatable
|
|
1667
|
+
const ret = Object.hasOwn(meta.properties, p);
|
|
1668
|
+
if (ret && parts.length > 0) {
|
|
1654
1669
|
return this.canPopulate(meta.properties[p].targetMeta.class, parts.join('.'));
|
|
1655
1670
|
}
|
|
1656
1671
|
return ret;
|
|
@@ -2109,7 +2124,11 @@ export class EntityManager {
|
|
|
2109
2124
|
]) {
|
|
2110
2125
|
delete opts[k];
|
|
2111
2126
|
}
|
|
2112
|
-
|
|
2127
|
+
// the table name (plus discriminator value for STI) is stable across builds and processes,
|
|
2128
|
+
// unlike class names, which minifiers can mangle to the same short name for two entities
|
|
2129
|
+
const meta = this.metadata.find(entityName);
|
|
2130
|
+
const key = meta?.tableName ? [meta.schema, meta.tableName, meta.discriminatorValue] : Utils.className(entityName);
|
|
2131
|
+
return [key, method, opts, where];
|
|
2113
2132
|
}
|
|
2114
2133
|
/**
|
|
2115
2134
|
* @internal
|
|
@@ -345,6 +345,7 @@ export interface CountByOptions<T extends object> {
|
|
|
345
345
|
filters?: FilterOptions;
|
|
346
346
|
having?: FilterQuery<T>;
|
|
347
347
|
schema?: string;
|
|
348
|
+
connectionType?: ConnectionType;
|
|
348
349
|
flushMode?: FlushMode | `${FlushMode}`;
|
|
349
350
|
loggerContext?: LogContext;
|
|
350
351
|
logging?: LoggingOptions;
|
package/entity/EntityFactory.js
CHANGED
|
@@ -77,6 +77,12 @@ export class EntityFactory {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
data = { ...data };
|
|
80
|
+
if (options.newEntity && meta2.root.inheritanceType === 'sti' && meta2.discriminatorValue != null) {
|
|
81
|
+
const prop = meta2.properties[meta2.root.discriminatorColumn];
|
|
82
|
+
if (prop && prop.userDefined !== false) {
|
|
83
|
+
data[prop.name] ??= meta2.discriminatorValue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
const entity = exists ?? this.createEntity(data, meta2, options);
|
|
81
87
|
wrapped = helper(entity);
|
|
82
88
|
wrapped.__processing = true;
|
package/entity/EntityLoader.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryHelper } from '../utils/QueryHelper.js';
|
|
2
|
-
import { Utils } from '../utils/Utils.js';
|
|
2
|
+
import { DANGEROUS_PROPERTY_NAMES, Utils } from '../utils/Utils.js';
|
|
3
3
|
import { ValidationError } from '../errors.js';
|
|
4
4
|
import { LoadStrategy, PopulatePath, ReferenceKind, } from '../enums.js';
|
|
5
5
|
import { Reference } from './Reference.js';
|
|
@@ -124,10 +124,10 @@ export class EntityLoader {
|
|
|
124
124
|
mergeNestedPopulate(populate) {
|
|
125
125
|
const tmp = populate.reduce((ret, item) => {
|
|
126
126
|
/* v8 ignore next */
|
|
127
|
-
if (item.field === PopulatePath.ALL) {
|
|
127
|
+
if (item.field === PopulatePath.ALL || DANGEROUS_PROPERTY_NAMES.includes(item.field)) {
|
|
128
128
|
return ret;
|
|
129
129
|
}
|
|
130
|
-
if (!ret
|
|
130
|
+
if (!Object.hasOwn(ret, item.field)) {
|
|
131
131
|
ret[item.field] = item;
|
|
132
132
|
return ret;
|
|
133
133
|
}
|
|
@@ -229,9 +229,10 @@ export class EntityLoader {
|
|
|
229
229
|
}
|
|
230
230
|
toPopulate.push(entity);
|
|
231
231
|
}
|
|
232
|
-
else if (refValue == null && !helper(entity).__loadedProperties.has(prop.name)) {
|
|
232
|
+
else if (refValue == null && !prop.object && !helper(entity).__loadedProperties.has(prop.name)) {
|
|
233
233
|
// FK columns weren't loaded (partial loading) — need to re-fetch them.
|
|
234
234
|
// If the property IS in __loadedProperties, the FK was loaded and is genuinely null.
|
|
235
|
+
// Object-embedded virtual props are skipped — they are populated via the embeddable instance.
|
|
235
236
|
needsFkLoad.push(entity);
|
|
236
237
|
}
|
|
237
238
|
}
|
|
@@ -445,7 +446,10 @@ export class EntityLoader {
|
|
|
445
446
|
}
|
|
446
447
|
}
|
|
447
448
|
}
|
|
448
|
-
|
|
449
|
+
// a missing target row means an orphaned reference, unless the query was narrowed by a populate condition
|
|
450
|
+
if ([ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind) &&
|
|
451
|
+
items.length !== children.length &&
|
|
452
|
+
Utils.isEmpty(options.where)) {
|
|
449
453
|
const nullVal = this.#em.config.get('forceUndefined') ? undefined : null;
|
|
450
454
|
const itemsMap = new Set();
|
|
451
455
|
const childrenMap = new Set();
|
|
@@ -667,7 +671,8 @@ export class EntityLoader {
|
|
|
667
671
|
}
|
|
668
672
|
return cond;
|
|
669
673
|
});
|
|
670
|
-
|
|
674
|
+
// partial extraction from `$or` is unsound — the parent may have matched via a dropped branch
|
|
675
|
+
if (child.length > 0 && (op === '$and' || child.length === where[op].length)) {
|
|
671
676
|
subCond[op] = child;
|
|
672
677
|
}
|
|
673
678
|
}
|
|
@@ -224,7 +224,11 @@ export class EntityRepository {
|
|
|
224
224
|
}
|
|
225
225
|
const entityName = entities[0].constructor.name;
|
|
226
226
|
const repoType = Utils.className(this.entityName);
|
|
227
|
-
|
|
227
|
+
// compare class identity where possible, as minifiers can mangle two classes to the same name
|
|
228
|
+
const entityMeta = entities[0].__meta;
|
|
229
|
+
const repoMeta = this.getEntityManager().getMetadata?.().find(this.entityName);
|
|
230
|
+
const mismatch = entityMeta && repoMeta ? entityMeta.class !== repoMeta.class : entityName && repoType !== entityName;
|
|
231
|
+
if (mismatch) {
|
|
228
232
|
throw ValidationError.fromWrongRepositoryType(entityName, repoType, method);
|
|
229
233
|
}
|
|
230
234
|
}
|
package/entity/defineEntity.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityManager } from '../EntityManager.js';
|
|
2
2
|
import type { ColumnType, PropertyOptions, ReferenceOptions, EnumOptions, EmbeddedOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, ManyToManyOptions, IndexColumnOptions } from '../metadata/types.js';
|
|
3
|
-
import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, LazyRef, IndexCallback, TriggerCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints, ExtractDefineEntityProperties } from '../typings.js';
|
|
3
|
+
import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, LazyRef, IndexCallback, TriggerCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints, OptionalProps, ExtractDefineEntityProperties } from '../typings.js';
|
|
4
4
|
import type { Raw } from '../utils/RawQueryFragment.js';
|
|
5
5
|
import type { ScalarReference } from './Reference.js';
|
|
6
6
|
import type { SerializeOptions } from '../serialization/EntitySerializer.js';
|
|
@@ -22,7 +22,7 @@ type HasKind<Options, K extends string> = Options extends {
|
|
|
22
22
|
kind: infer X extends string;
|
|
23
23
|
} ? X extends K ? true : false : false;
|
|
24
24
|
/** Lightweight chain result type for property builders - reduces type instantiation cost by avoiding full class resolution. */
|
|
25
|
-
export interface PropertyChain<Value, Options> {
|
|
25
|
+
export interface PropertyChain<in out Value, in out Options> {
|
|
26
26
|
'~type'?: {
|
|
27
27
|
value: Value;
|
|
28
28
|
};
|
|
@@ -112,7 +112,7 @@ export interface PropertyChain<Value, Options> {
|
|
|
112
112
|
getter(getter?: boolean): PropertyChain<Value, Options>;
|
|
113
113
|
getterName(getterName: string): PropertyChain<Value, Options>;
|
|
114
114
|
serializedPrimaryKey(serializedPrimaryKey?: boolean): PropertyChain<Value, Options>;
|
|
115
|
-
serializer(serializer: (value: Value, options?: SerializeOptions<any>) => any): PropertyChain<Value, Options>;
|
|
115
|
+
serializer(serializer: (value: SerializerValue<Value, Options>, options?: SerializeOptions<any>) => any): PropertyChain<Value, Options>;
|
|
116
116
|
serializedName(serializedName: string): PropertyChain<Value, Options>;
|
|
117
117
|
groups(...groups: string[]): PropertyChain<Value, Options>;
|
|
118
118
|
customOrder(...customOrder: string[] | number[] | boolean[]): PropertyChain<Value, Options>;
|
|
@@ -180,7 +180,7 @@ export interface PropertyChain<Value, Options> {
|
|
|
180
180
|
foreignKeyName(foreignKeyName: string): HasKind<Options, 'm:1' | '1:m' | '1:1' | 'm:n'> extends true ? PropertyChain<Value, Options> : never;
|
|
181
181
|
}
|
|
182
182
|
/** @internal */
|
|
183
|
-
export declare class UniversalPropertyOptionsBuilder<Value, Options, IncludeKeys extends BuilderKeys> implements Record<Exclude<UniversalPropertyKeys, ExcludeKeys>, any> {
|
|
183
|
+
export declare class UniversalPropertyOptionsBuilder<in out Value, in out Options, in out IncludeKeys extends BuilderKeys> implements Record<Exclude<UniversalPropertyKeys, ExcludeKeys>, any> {
|
|
184
184
|
'~options': Options;
|
|
185
185
|
'~type'?: {
|
|
186
186
|
value: Value;
|
|
@@ -627,7 +627,7 @@ type PartialWhere<TProperties, TBase> = string | FilterQuery<{
|
|
|
627
627
|
[K in AllKeys<TProperties, TBase> & string]?: unknown;
|
|
628
628
|
}>;
|
|
629
629
|
/** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
|
|
630
|
-
export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false, TDiscriminatorColumn extends string | undefined = undefined, TDiscriminatorValue extends string | number | undefined = undefined, TBaseDiscriminatorColumn extends string | undefined = undefined> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminator' | 'discriminatorColumn' | 'discriminatorValue' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
|
|
630
|
+
export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false, TDiscriminatorColumn extends string | undefined = undefined, TDiscriminatorValue extends string | number | undefined = undefined, TBaseDiscriminatorColumn extends string | undefined = undefined, TEmbeddable extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminator' | 'discriminatorColumn' | 'discriminatorValue' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
|
|
631
631
|
name: TName;
|
|
632
632
|
tableName?: TTableName;
|
|
633
633
|
extends?: {
|
|
@@ -649,6 +649,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
649
649
|
strict?: boolean;
|
|
650
650
|
}>;
|
|
651
651
|
forceObject?: TForceObject;
|
|
652
|
+
embeddable?: TEmbeddable;
|
|
652
653
|
inheritance?: 'tpt';
|
|
653
654
|
orderBy?: {
|
|
654
655
|
[K in Extract<AllKeys<TProperties, TBase>, string>]?: QueryOrderKeysFlat;
|
|
@@ -699,7 +700,7 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
|
|
|
699
700
|
}[];
|
|
700
701
|
}
|
|
701
702
|
/** Defines an entity schema using property builders, with full type inference from the property definitions. */
|
|
702
|
-
export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false, const TDiscriminatorColumn extends string | undefined = undefined, const TDiscriminatorValue extends string | number | undefined = undefined, const TBaseDiscriminatorColumn extends string | undefined = undefined>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject, TDiscriminatorColumn, TDiscriminatorValue, TBaseDiscriminatorColumn>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue>, TBase, TProperties, EntityCtor<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue>>, TDiscriminatorColumn>;
|
|
703
|
+
export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false, const TDiscriminatorColumn extends string | undefined = undefined, const TDiscriminatorValue extends string | number | undefined = undefined, const TBaseDiscriminatorColumn extends string | undefined = undefined, const TEmbeddable extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject, TDiscriminatorColumn, TDiscriminatorValue, TBaseDiscriminatorColumn, TEmbeddable>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable>, TBase, TProperties, EntityCtor<InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject, TBaseDiscriminatorColumn, TDiscriminatorValue, TEmbeddable>>, TDiscriminatorColumn>;
|
|
703
704
|
export declare namespace defineEntity {
|
|
704
705
|
export { propertyBuilders as properties };
|
|
705
706
|
}
|
|
@@ -734,22 +735,24 @@ type InferTypeByString<T extends string> = T extends keyof typeof types ? InferJ
|
|
|
734
735
|
type InferJSType<T> = T extends typeof Type<infer TValue, any> ? NonNullable<TValue> : never;
|
|
735
736
|
type InferColumnType<T extends string> = T extends 'int' | 'int4' | 'integer' | 'bigint' | 'int8' | 'int2' | 'tinyint' | 'smallint' | 'mediumint' ? number : T extends 'double' | 'double precision' | 'real' | 'float8' | 'decimal' | 'numeric' | 'float' | 'float4' ? number : T extends 'datetime' | 'time' | 'time with time zone' | 'timestamp' | 'timestamp with time zone' | 'timetz' | 'timestamptz' | 'date' | 'interval' ? Date : T extends 'ObjectId' | 'objectId' | 'character varying' | 'varchar' | 'char' | 'character' | 'uuid' | 'text' | 'tinytext' | 'mediumtext' | 'longtext' | 'enum' ? string : T extends 'boolean' | 'bool' | 'bit' ? boolean : T extends 'blob' | 'tinyblob' | 'mediumblob' | 'longblob' | 'bytea' ? Buffer : T extends 'point' | 'line' | 'lseg' | 'box' | 'circle' | 'path' | 'polygon' | 'geometry' ? number[] : T extends 'tsvector' | 'tsquery' ? string[] : T extends 'json' | 'jsonb' ? any : any;
|
|
736
737
|
type BaseEntityMethodKeys = 'toObject' | 'toPOJO' | 'serialize' | 'assign' | 'populate' | 'init' | 'toReference';
|
|
738
|
+
interface BaseEntityMethods<in out Entity extends object> extends Pick<IWrappedEntity<Entity>, BaseEntityMethodKeys> {
|
|
739
|
+
}
|
|
737
740
|
/** Infers the entity type from a `defineEntity()` properties map, resolving builders, base classes, and primary keys. */
|
|
738
|
-
export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined> = (IsNever<Base> extends true ? {} : Base extends {
|
|
741
|
+
export type InferEntityFromProperties<Properties extends Record<string, any>, PK extends (keyof Properties)[] | undefined = undefined, Base = never, Repository = never, ForceObject extends boolean = false, BaseDiscriminatorColumn extends string | undefined = undefined, DiscriminatorValue extends string | number | undefined = undefined, Embeddable extends boolean = false> = (IsNever<Base> extends true ? {} : Base extends {
|
|
739
742
|
toObject(...args: any[]): any;
|
|
740
|
-
} ?
|
|
743
|
+
} ? BaseEntityMethods<{
|
|
741
744
|
-readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
|
|
742
745
|
} & {
|
|
743
746
|
[PrimaryKeyProp]?: InferCombinedPrimaryKey<Properties, PK, Base>;
|
|
744
747
|
} & (IsNever<Repository> extends true ? {} : {
|
|
745
748
|
[EntityRepositoryType]?: Repository extends Constructor<infer R> ? R : Repository;
|
|
746
|
-
}) & NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue
|
|
749
|
+
}) & NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue, Embeddable>> : {}) & {
|
|
747
750
|
-readonly [K in keyof Properties]: InferBuilderValue<MaybeReturnType<Properties[K]>>;
|
|
748
751
|
} & {
|
|
749
752
|
[PrimaryKeyProp]?: InferCombinedPrimaryKey<Properties, PK, Base>;
|
|
750
753
|
} & (IsNever<Repository> extends true ? {} : {
|
|
751
754
|
[EntityRepositoryType]?: Repository extends Constructor<infer R> ? R : Repository;
|
|
752
|
-
}) & (IsNever<Base> extends true ? {} : NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue>) & (ForceObject extends true ? {
|
|
755
|
+
}) & (IsNever<Base> extends true ? {} : NarrowDiscriminator<Omit<Base, typeof PrimaryKeyProp>, BaseDiscriminatorColumn, DiscriminatorValue, Embeddable>) & (ForceObject extends true ? {
|
|
753
756
|
[Config]?: DefineConfig<{
|
|
754
757
|
forceObject: true;
|
|
755
758
|
}>;
|
|
@@ -757,13 +760,26 @@ export type InferEntityFromProperties<Properties extends Record<string, any>, PK
|
|
|
757
760
|
[IndexHints]?: [Omit<ExtractBaseProperties<Base>, keyof Properties> & Properties];
|
|
758
761
|
};
|
|
759
762
|
type ExtractBaseProperties<Base> = [ExtractDefineEntityProperties<Base>] extends [infer P extends Record<string, any>] ? [P] extends [never] ? {} : P : {};
|
|
760
|
-
type
|
|
763
|
+
type ExtractOptionalProps<Base> = Base extends {
|
|
764
|
+
[OptionalProps]?: infer K;
|
|
765
|
+
} ? (K extends string ? K : never) : never;
|
|
766
|
+
type NarrowDiscriminator<Base, DiscColumn extends string | undefined, DiscValue, Embeddable extends boolean = false> = DiscColumn extends string ? DiscColumn extends keyof Base ? DiscValue extends string | number ? Embeddable extends true ? Omit<Base, DiscColumn> & {
|
|
767
|
+
[K in DiscColumn]: DiscValue;
|
|
768
|
+
} : Omit<Base, DiscColumn | typeof OptionalProps> & {
|
|
761
769
|
[K in DiscColumn]: DiscValue;
|
|
770
|
+
} & {
|
|
771
|
+
[OptionalProps]?: DiscColumn | ExtractOptionalProps<Base>;
|
|
762
772
|
} : Base : Base : Base;
|
|
763
773
|
type InferCombinedPrimaryKey<Properties extends Record<string, any>, PK, Base> = PK extends undefined ? CombinePrimaryKeys<InferPrimaryKey<Properties>, ExtractBasePrimaryKey<Base>> : PK;
|
|
764
|
-
type ExtractBasePrimaryKey<Base> = Base extends {
|
|
774
|
+
type ExtractBasePrimaryKey<Base> = typeof PrimaryKeyProp extends keyof Base ? Base extends {
|
|
765
775
|
[PrimaryKeyProp]?: infer BasePK;
|
|
766
|
-
} ? BasePK : never
|
|
776
|
+
} ? BasePK : never : [keyof Base] extends [never] ? never : Base extends {
|
|
777
|
+
_id?: any;
|
|
778
|
+
} ? '_id' : Base extends {
|
|
779
|
+
id?: any;
|
|
780
|
+
} ? 'id' : Base extends {
|
|
781
|
+
uuid?: any;
|
|
782
|
+
} ? 'uuid' : never;
|
|
767
783
|
type CombinePrimaryKeys<ChildPK, BasePK> = [ChildPK] extends [never] ? BasePK : [BasePK] extends [never] ? IsUnion<ChildPK> extends true ? ChildPK[] : ChildPK : ChildPK | BasePK;
|
|
768
784
|
/** Extracts the primary key property names from a properties map by finding builders with `primary: true`. */
|
|
769
785
|
export type InferPrimaryKey<Properties extends Record<string, any>> = {
|
|
@@ -790,6 +806,8 @@ type InferBuilderValue<Builder> = Builder extends {
|
|
|
790
806
|
type MaybeArray<Value, Options> = Options extends {
|
|
791
807
|
array: true;
|
|
792
808
|
} ? Value[] : Value;
|
|
809
|
+
/** The runtime value passed to a custom serializer — the property value as stored on the entity (e.g. `Collection`/`Ref` for relations). Skips `MaybeMapToPk`, as its `Primary<Value>` branch is too costly for the type checker. */
|
|
810
|
+
type SerializerValue<Value, Options> = MaybeNullable<MaybeRelationRef<MaybeArray<Value, Options>, Options>, Options>;
|
|
793
811
|
type MaybeMapToPk<Value, Options> = Options extends {
|
|
794
812
|
mapToPk: true;
|
|
795
813
|
} ? Primary<Value> : Value;
|
package/entity/defineEntity.js
CHANGED
|
@@ -496,7 +496,7 @@ export class StringPropertyOptionsBuilder extends UniversalPropertyOptionsBuilde
|
|
|
496
496
|
export class OneToManyOptionsBuilderOnlyMappedBy extends UniversalPropertyOptionsBuilder {
|
|
497
497
|
/** Point to the owning side property name. */
|
|
498
498
|
mappedBy(mappedBy) {
|
|
499
|
-
return
|
|
499
|
+
return this.assignOptions({ mappedBy });
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
502
|
function createPropertyBuilders(options) {
|
package/errors.d.ts
CHANGED
|
@@ -60,6 +60,7 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
|
|
|
60
60
|
static duplicateFieldName(entityName: EntityName, names: [string, string][]): MetadataError;
|
|
61
61
|
static multipleDecorators(entityName: string, propertyName: string): MetadataError;
|
|
62
62
|
static missingMetadata(entity: string): MetadataError;
|
|
63
|
+
static ambiguousEntityName(className: string): MetadataError;
|
|
63
64
|
static invalidPrimaryKey(meta: EntityMetadata, prop: EntityProperty, requiredName: string): MetadataError;
|
|
64
65
|
static invalidManyToManyWithPivotEntity(meta1: EntityMetadata, prop1: EntityProperty, meta2: EntityMetadata, prop2: EntityProperty): MetadataError;
|
|
65
66
|
static targetIsAbstract(meta: EntityMetadata, prop: EntityProperty): MetadataError;
|
package/errors.js
CHANGED
|
@@ -195,6 +195,9 @@ export class MetadataError extends ValidationError {
|
|
|
195
195
|
static missingMetadata(entity) {
|
|
196
196
|
return new MetadataError(`Metadata for entity ${entity} not found`);
|
|
197
197
|
}
|
|
198
|
+
static ambiguousEntityName(className) {
|
|
199
|
+
return new MetadataError(`Entity name '${className}' is ambiguous, multiple discovered entity classes share it (possibly due to a minifier mangling class names). Use a class reference instead of a string name.`);
|
|
200
|
+
}
|
|
198
201
|
static invalidPrimaryKey(meta, prop, requiredName) {
|
|
199
202
|
return this.fromMessage(meta, prop, `has wrong field name, '${requiredName}' is required in current driver`);
|
|
200
203
|
}
|
package/events/EventManager.js
CHANGED
|
@@ -43,7 +43,10 @@ export class EventManager {
|
|
|
43
43
|
}));
|
|
44
44
|
for (const listener of this.#listeners[event] ?? new Set()) {
|
|
45
45
|
const entities = this.#entities.get(listener);
|
|
46
|
-
if (entities.size === 0 ||
|
|
46
|
+
if (entities.size === 0 ||
|
|
47
|
+
!entity ||
|
|
48
|
+
entities.has(entity.constructor) ||
|
|
49
|
+
entities.has(entity.constructor.name)) {
|
|
47
50
|
listeners.push(listener[event].bind(listener));
|
|
48
51
|
}
|
|
49
52
|
}
|
|
@@ -68,7 +71,7 @@ export class EventManager {
|
|
|
68
71
|
}
|
|
69
72
|
for (const listener of this.#listeners[event] ?? new Set()) {
|
|
70
73
|
const entities = this.#entities.get(listener);
|
|
71
|
-
if (entities.size === 0 || entities.has(meta.className)) {
|
|
74
|
+
if (entities.size === 0 || entities.has(meta.class) || entities.has(meta.className)) {
|
|
72
75
|
this.#cache.set(cacheKey, true);
|
|
73
76
|
return true;
|
|
74
77
|
}
|
|
@@ -84,6 +87,6 @@ export class EventManager {
|
|
|
84
87
|
if (!listener.getSubscribedEntities) {
|
|
85
88
|
return new Set();
|
|
86
89
|
}
|
|
87
|
-
return new Set(listener.getSubscribedEntities().map(name => Utils.
|
|
90
|
+
return new Set(listener.getSubscribedEntities().map(name => Utils.classOrName(name)));
|
|
88
91
|
}
|
|
89
92
|
}
|
|
@@ -19,6 +19,8 @@ export declare class ObjectHydrator extends Hydrator {
|
|
|
19
19
|
getEntityHydrator<T extends object>(meta: EntityMetadata<T>, type: 'full' | 'reference', normalizeAccessors?: boolean): EntityHydrator<T>;
|
|
20
20
|
private createCollectionItemMapper;
|
|
21
21
|
private wrap;
|
|
22
|
+
/** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
|
|
23
|
+
private quote;
|
|
22
24
|
private safeKey;
|
|
23
25
|
}
|
|
24
26
|
export {};
|
|
@@ -87,7 +87,7 @@ export class ObjectHydrator extends Hydrator {
|
|
|
87
87
|
ret.push(` if (data${dataKey} === null) {`);
|
|
88
88
|
if (prop.ref) {
|
|
89
89
|
ret.push(` entity${entityKey} = new ScalarReference();`);
|
|
90
|
-
ret.push(` entity${entityKey}.bind(entity,
|
|
90
|
+
ret.push(` entity${entityKey}.bind(entity, ${this.quote(prop.name)});`);
|
|
91
91
|
ret.push(` entity${entityKey}.set(${nullVal});`);
|
|
92
92
|
}
|
|
93
93
|
else {
|
|
@@ -127,14 +127,14 @@ export class ObjectHydrator extends Hydrator {
|
|
|
127
127
|
if (prop.ref) {
|
|
128
128
|
ret.push(` const value = isScalarReference(entity${entityKey}) ? entity${entityKey}.unwrap() : entity${entityKey};`);
|
|
129
129
|
ret.push(` entity${entityKey} = oldValue_${idx} ?? new ScalarReference(value);`);
|
|
130
|
-
ret.push(` entity${entityKey}.bind(entity,
|
|
130
|
+
ret.push(` entity${entityKey}.bind(entity, ${this.quote(prop.name)});`);
|
|
131
131
|
ret.push(` entity${entityKey}.set(value);`);
|
|
132
132
|
}
|
|
133
133
|
ret.push(` }`);
|
|
134
134
|
if (prop.ref) {
|
|
135
135
|
ret.push(` if (!entity${entityKey}) {`);
|
|
136
136
|
ret.push(` entity${entityKey} = new ScalarReference();`);
|
|
137
|
-
ret.push(` entity${entityKey}.bind(entity,
|
|
137
|
+
ret.push(` entity${entityKey}.bind(entity, ${this.quote(prop.name)});`);
|
|
138
138
|
ret.push(` }`);
|
|
139
139
|
}
|
|
140
140
|
return ret;
|
|
@@ -220,7 +220,7 @@ export class ObjectHydrator extends Hydrator {
|
|
|
220
220
|
ret.push(` }`);
|
|
221
221
|
ret.push(` if (Array.isArray(data${dataKey})) {`);
|
|
222
222
|
ret.push(` const items = data${dataKey}.map(value => createCollectionItem_${this.safeKey(prop.name)}(value, entity));`);
|
|
223
|
-
ret.push(` const coll = Collection.create(entity,
|
|
223
|
+
ret.push(` const coll = Collection.create(entity, ${this.quote(prop.name)}, items, newEntity);`);
|
|
224
224
|
ret.push(` if (newEntity) {`);
|
|
225
225
|
ret.push(` coll.setDirty();`);
|
|
226
226
|
ret.push(` } else {`);
|
|
@@ -231,11 +231,11 @@ export class ObjectHydrator extends Hydrator {
|
|
|
231
231
|
if (!this.platform.usesPivotTable() && prop.owner && prop.kind === ReferenceKind.MANY_TO_MANY) {
|
|
232
232
|
ret.push(` } else if (!entity${entityKey} && Array.isArray(data${dataKey})) {`);
|
|
233
233
|
const items = this.platform.usesPivotTable() || !prop.owner ? 'undefined' : '[]';
|
|
234
|
-
ret.push(` const coll = Collection.create(entity,
|
|
234
|
+
ret.push(` const coll = Collection.create(entity, ${this.quote(prop.name)}, ${items}, !!data${dataKey} || newEntity);`);
|
|
235
235
|
ret.push(` coll.setDirty(false);`);
|
|
236
236
|
}
|
|
237
237
|
ret.push(` } else if (!entity${entityKey}) {`);
|
|
238
|
-
ret.push(` const coll = Collection.create(entity,
|
|
238
|
+
ret.push(` const coll = Collection.create(entity, ${this.quote(prop.name)}, undefined, newEntity);`);
|
|
239
239
|
ret.push(` coll.setDirty(false);`);
|
|
240
240
|
ret.push(` }`);
|
|
241
241
|
return ret;
|
|
@@ -431,10 +431,14 @@ export class ObjectHydrator extends Hydrator {
|
|
|
431
431
|
return lines;
|
|
432
432
|
}
|
|
433
433
|
wrap(key) {
|
|
434
|
-
if (/^\[
|
|
434
|
+
if (/^\[idx_\d+]$/.exec(key)) {
|
|
435
435
|
return key;
|
|
436
436
|
}
|
|
437
|
-
return /^\w+$/.exec(key) ? `.${key}` : `[
|
|
437
|
+
return /^\w+$/.exec(key) ? `.${key}` : `[${this.quote(key)}]`;
|
|
438
|
+
}
|
|
439
|
+
/** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
|
|
440
|
+
quote(key) {
|
|
441
|
+
return `'${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
438
442
|
}
|
|
439
443
|
safeKey(key) {
|
|
440
444
|
return key.replace(/\W/g, '_');
|
|
@@ -441,7 +441,18 @@ export class MetadataDiscovery {
|
|
|
441
441
|
if (prop.kind === ReferenceKind.SCALAR || prop.kind === ReferenceKind.EMBEDDED) {
|
|
442
442
|
prop.fieldNames = [this.#namingStrategy.propertyToColumnName(prop.name, object)];
|
|
443
443
|
}
|
|
444
|
-
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
|
|
444
|
+
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && prop.polymorphic) {
|
|
445
|
+
if (prop.targetMeta) {
|
|
446
|
+
// same layout as `initManyToOneFields` builds later: `[discriminatorColumn, ...fkIdColumns]`
|
|
447
|
+
const pkFields = prop.targetMeta.getPrimaryProps().flatMap(pk => {
|
|
448
|
+
this.initFieldName(pk);
|
|
449
|
+
return pk.fieldNames;
|
|
450
|
+
});
|
|
451
|
+
const idColumns = pkFields.map(fieldName => this.#namingStrategy.joinKeyColumnName(prop.discriminator, fieldName, pkFields.length > 1));
|
|
452
|
+
prop.fieldNames = [prop.discriminatorColumn, ...idColumns];
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
else if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
|
|
445
456
|
prop.fieldNames = this.initManyToOneFieldName(prop, prop.name);
|
|
446
457
|
}
|
|
447
458
|
else if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner) {
|
|
@@ -451,10 +462,13 @@ export class MetadataDiscovery {
|
|
|
451
462
|
initManyToOneFieldName(prop, name) {
|
|
452
463
|
const meta2 = prop.targetMeta;
|
|
453
464
|
const ret = [];
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
465
|
+
// with `targetKey` on a composite PK target, derive the FK field name from that property
|
|
466
|
+
// instead of the PKs (simple PK targets keep the PK based naming for backwards compatibility)
|
|
467
|
+
const referencedKeys = prop.targetKey && meta2.compositePK ? [prop.targetKey] : meta2.primaryKeys;
|
|
468
|
+
for (const referencedKey of referencedKeys) {
|
|
469
|
+
this.initFieldName(meta2.properties[referencedKey]);
|
|
470
|
+
for (const fieldName of meta2.properties[referencedKey].fieldNames) {
|
|
471
|
+
ret.push(this.#namingStrategy.joinKeyColumnName(name, fieldName, !prop.targetKey && meta2.compositePK));
|
|
458
472
|
}
|
|
459
473
|
}
|
|
460
474
|
return ret;
|
|
@@ -1055,11 +1069,12 @@ export class MetadataDiscovery {
|
|
|
1055
1069
|
}
|
|
1056
1070
|
// TPT children have their own tables that don't contain the parent's columns,
|
|
1057
1071
|
// so propagating parent indexes/uniques/checks/triggers would target missing columns.
|
|
1072
|
+
// deep equality, as the subclass items might be copies of the base class ones (e.g. with TC39 decorators)
|
|
1058
1073
|
if (meta.inheritanceType !== 'tpt' || !meta.tptParent) {
|
|
1059
|
-
meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
|
|
1060
|
-
meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
|
|
1061
|
-
meta.checks = Utils.unique([...base.checks, ...meta.checks]);
|
|
1062
|
-
meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
|
|
1074
|
+
meta.indexes = Utils.unique([...base.indexes, ...meta.indexes], Utils.equals);
|
|
1075
|
+
meta.uniques = Utils.unique([...base.uniques, ...meta.uniques], Utils.equals);
|
|
1076
|
+
meta.checks = Utils.unique([...base.checks, ...meta.checks], Utils.equals);
|
|
1077
|
+
meta.triggers = Utils.unique([...base.triggers, ...meta.triggers], Utils.equals);
|
|
1063
1078
|
}
|
|
1064
1079
|
const pks = Object.values(meta.properties)
|
|
1065
1080
|
.filter(p => p.primary)
|
|
@@ -1247,8 +1262,17 @@ export class MetadataDiscovery {
|
|
|
1247
1262
|
if (embeddedProp.nullable || refInArray) {
|
|
1248
1263
|
meta.properties[name].nullable = true;
|
|
1249
1264
|
}
|
|
1265
|
+
// polymorphic relations derive their column names from the discriminator, so prefix it too
|
|
1266
|
+
if (meta.properties[name].polymorphic && !object) {
|
|
1267
|
+
meta.properties[name].discriminator = prefix + meta.properties[name].discriminator;
|
|
1268
|
+
meta.properties[name].discriminatorColumn = prefix + meta.properties[name].discriminatorColumn;
|
|
1269
|
+
}
|
|
1250
1270
|
if (meta.properties[name].fieldNames) {
|
|
1251
|
-
|
|
1271
|
+
const { fieldNames, polymorphic } = meta.properties[name];
|
|
1272
|
+
// polymorphic `fieldNames` hold `[discriminatorColumn, ...fkIdColumns]`, so prefix all of them
|
|
1273
|
+
for (let i = 0; i < (polymorphic ? fieldNames.length : 1); i++) {
|
|
1274
|
+
fieldNames[i] = prefix + fieldNames[i];
|
|
1275
|
+
}
|
|
1252
1276
|
}
|
|
1253
1277
|
else {
|
|
1254
1278
|
const name2 = meta.properties[name].name;
|
|
@@ -1677,6 +1701,9 @@ export class MetadataDiscovery {
|
|
|
1677
1701
|
if (prop.persist === false || prop.nativeEnumName || !prop.items?.every(item => typeof item === 'string')) {
|
|
1678
1702
|
continue;
|
|
1679
1703
|
}
|
|
1704
|
+
if (prop.customType instanceof t.json || ['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1680
1707
|
this.initFieldName(prop);
|
|
1681
1708
|
let expression = null;
|
|
1682
1709
|
if (prop.enum) {
|
|
@@ -2080,6 +2107,7 @@ export class MetadataDiscovery {
|
|
|
2080
2107
|
prop.columnTypes.push(...columnTypes);
|
|
2081
2108
|
if (!targetMeta.compositePK || prop.targetKey) {
|
|
2082
2109
|
prop.customType = referencedProp.customType;
|
|
2110
|
+
prop.collation ??= referencedProp.collation;
|
|
2083
2111
|
}
|
|
2084
2112
|
}
|
|
2085
2113
|
}
|
|
@@ -2135,7 +2163,7 @@ export class MetadataDiscovery {
|
|
|
2135
2163
|
shouldForceConstructorUsage(meta) {
|
|
2136
2164
|
const forceConstructor = this.#config.get('forceEntityConstructor');
|
|
2137
2165
|
if (Array.isArray(forceConstructor)) {
|
|
2138
|
-
return forceConstructor.some(cls => Utils.
|
|
2166
|
+
return forceConstructor.some(cls => Utils.matchesEntity(cls, meta));
|
|
2139
2167
|
}
|
|
2140
2168
|
return forceConstructor;
|
|
2141
2169
|
}
|
|
@@ -17,6 +17,7 @@ export class MetadataStorage {
|
|
|
17
17
|
#idMap;
|
|
18
18
|
#classNameMap;
|
|
19
19
|
#uniqueNameMap;
|
|
20
|
+
#ambiguousNames = new Set();
|
|
20
21
|
constructor(metadata = {}) {
|
|
21
22
|
this.#idMap = {};
|
|
22
23
|
this.#uniqueNameMap = {};
|
|
@@ -64,6 +65,10 @@ export class MetadataStorage {
|
|
|
64
65
|
}
|
|
65
66
|
/** Returns metadata for the given entity, optionally initializing it if not found. */
|
|
66
67
|
get(entityName, init = false) {
|
|
68
|
+
// string lookups cannot be resolved when several classes were minified to the same name
|
|
69
|
+
if (typeof entityName === 'string' && this.#ambiguousNames.has(entityName)) {
|
|
70
|
+
throw MetadataError.ambiguousEntityName(entityName);
|
|
71
|
+
}
|
|
67
72
|
const exists = this.find(entityName);
|
|
68
73
|
if (exists) {
|
|
69
74
|
return exists;
|
|
@@ -99,7 +104,13 @@ export class MetadataStorage {
|
|
|
99
104
|
this.#metadataMap.set(entityName, meta);
|
|
100
105
|
this.#idMap[meta._id] = meta;
|
|
101
106
|
this.#uniqueNameMap[meta.uniqueName] = meta;
|
|
102
|
-
|
|
107
|
+
const className = Utils.className(entityName);
|
|
108
|
+
const existing = this.#classNameMap[className];
|
|
109
|
+
// track name collisions caused by minifiers mangling two classes to the same name
|
|
110
|
+
if (existing && existing !== meta && existing.class !== meta.class) {
|
|
111
|
+
this.#ambiguousNames.add(className);
|
|
112
|
+
}
|
|
113
|
+
this.#classNameMap[className] = meta;
|
|
103
114
|
return meta;
|
|
104
115
|
}
|
|
105
116
|
/** Removes metadata for the given entity from all internal maps. */
|
|
@@ -110,6 +121,11 @@ export class MetadataStorage {
|
|
|
110
121
|
delete this.#idMap[meta._id];
|
|
111
122
|
delete this.#uniqueNameMap[meta.uniqueName];
|
|
112
123
|
delete this.#classNameMap[meta.className];
|
|
124
|
+
// the name may still be ambiguous among the remaining metas
|
|
125
|
+
const remaining = new Set([...this.#metadataMap.values()].filter(m => m.className === meta.className).map(m => m.class));
|
|
126
|
+
if (remaining.size <= 1) {
|
|
127
|
+
this.#ambiguousNames.delete(meta.className);
|
|
128
|
+
}
|
|
113
129
|
}
|
|
114
130
|
}
|
|
115
131
|
/** Decorates all entity prototypes with helper methods (e.g. init, toJSON). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/core",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.14",
|
|
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,8 +211,9 @@ export declare abstract class Platform {
|
|
|
211
211
|
getBlobDeclarationSQL(): string;
|
|
212
212
|
getJsonDeclarationSQL(): string;
|
|
213
213
|
getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | Raw;
|
|
214
|
-
|
|
215
|
-
|
|
214
|
+
/** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
|
|
215
|
+
getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean | string, value?: unknown): string | Raw;
|
|
216
|
+
processJsonCondition<T extends object>(o: FilterQuery<T>, value: EntityValue<T>, path: EntityKey<T>[], alias: boolean | string): FilterQuery<T>;
|
|
216
217
|
protected getJsonValueType(value: unknown): string;
|
|
217
218
|
getJsonIndexDefinition(index: {
|
|
218
219
|
columnNames: string[];
|
package/platforms/Platform.js
CHANGED
|
@@ -413,6 +413,7 @@ export class Platform {
|
|
|
413
413
|
getSearchJsonPropertySQL(path, type, aliased) {
|
|
414
414
|
return path;
|
|
415
415
|
}
|
|
416
|
+
/** When `aliased` is a string, it holds an explicit alias the key was prefixed with. */
|
|
416
417
|
getSearchJsonPropertyKey(path, type, aliased, value) {
|
|
417
418
|
return path.join('.');
|
|
418
419
|
}
|
|
@@ -424,7 +425,8 @@ export class Platform {
|
|
|
424
425
|
return o;
|
|
425
426
|
}
|
|
426
427
|
if (path.length === 1) {
|
|
427
|
-
|
|
428
|
+
const key = typeof alias === 'string' ? `${alias}.${path[0]}` : path[0];
|
|
429
|
+
o[key] = value;
|
|
428
430
|
return o;
|
|
429
431
|
}
|
|
430
432
|
const type = this.getJsonValueType(value);
|
package/types/Type.js
CHANGED
|
@@ -55,11 +55,11 @@ export class Type {
|
|
|
55
55
|
return prop.columnTypes?.[0] ?? platform.getTextTypeDeclarationSQL(prop);
|
|
56
56
|
}
|
|
57
57
|
static getType(cls) {
|
|
58
|
-
|
|
59
|
-
if (!Type.types.has(
|
|
60
|
-
Type.types.set(
|
|
58
|
+
// keyed by class reference, as minifiers can mangle two classes to the same name
|
|
59
|
+
if (!Type.types.has(cls)) {
|
|
60
|
+
Type.types.set(cls, new cls());
|
|
61
61
|
}
|
|
62
|
-
return Type.types.get(
|
|
62
|
+
return Type.types.get(cls);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Checks whether the argument is instance of `Type`.
|
package/typings.d.ts
CHANGED
|
@@ -478,12 +478,12 @@ type NonArrayObject = object & {
|
|
|
478
478
|
export type EntityDataProp<T, C extends boolean> = T extends Date ? string | Date : T extends Scalar ? T : T extends ScalarReference<infer U> ? EntityDataProp<U, C> : T extends {
|
|
479
479
|
__runtime?: infer Runtime;
|
|
480
480
|
__raw?: infer Raw;
|
|
481
|
-
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
|
|
481
|
+
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? EntityDataNested<U, C> : T extends ReferenceShape<infer U> ? EntityDataNested<U, C> : T extends CollectionShape<infer U> ? U | U[] | EntityDataNested<U & object, C> | EntityDataNested<U & object, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | EntityDataNested<U, C> | EntityDataNested<U, C>[] : U[] | EntityDataNested<U, C>[] : EntityDataNested<T, C>;
|
|
482
482
|
/** Like `EntityDataProp` but used in `RequiredEntityData` context with required/optional key distinction. */
|
|
483
483
|
export type RequiredEntityDataProp<T, O, C extends boolean> = T extends Date ? string | Date : Exclude<T, null> extends RequiredNullable.Brand ? T | null : T extends Scalar ? T : T extends ScalarReference<infer U> ? RequiredEntityDataProp<U, O, C> : T extends {
|
|
484
484
|
__runtime?: infer Runtime;
|
|
485
485
|
__raw?: infer Raw;
|
|
486
|
-
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? U extends NonArrayObject ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
|
|
486
|
+
} ? C extends true ? Raw : Runtime : T extends LazyRef.Brand<infer U> ? RequiredEntityDataNested<U, O, C> : T extends ReferenceShape<infer U> ? RequiredEntityDataNested<U, O, C> : T extends CollectionShape<infer U> ? U | U[] | RequiredEntityDataNested<U & object, O, C> | RequiredEntityDataNested<U & object, O, C>[] : T extends readonly (infer U)[] ? [U] extends [NonArrayObject] ? U | U[] | RequiredEntityDataNested<U, O, C> | RequiredEntityDataNested<U, O, C>[] : U[] | RequiredEntityDataNested<U, O, C>[] : RequiredEntityDataNested<T, O, C>;
|
|
487
487
|
/** Nested entity data shape for embedded or related entities within `EntityData`. */
|
|
488
488
|
export type EntityDataNested<T, C extends boolean = false> = T extends undefined ? never : T extends any[] ? Readonly<T> : EntityData<T, C> | ExpandEntityProp<T, C>;
|
|
489
489
|
type UnwrapScalarRef<T> = T extends ScalarReference<infer U> ? U : T;
|
|
@@ -1050,6 +1050,12 @@ export declare class EntityMetadata<Entity = any, Class extends EntityCtor<Entit
|
|
|
1050
1050
|
constructor(meta?: Partial<EntityMetadata>);
|
|
1051
1051
|
addProperty(prop: Partial<EntityProperty<Entity>>): void;
|
|
1052
1052
|
removeProperty(name: string, sync?: boolean): void;
|
|
1053
|
+
/** For TPT entities, the version column exists only on the table of the entity that declares it. */
|
|
1054
|
+
ownsVersionProperty(): boolean;
|
|
1055
|
+
/** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
|
|
1056
|
+
getOwnConcurrencyCheckKeys(): EntityKey<Entity>[];
|
|
1057
|
+
/** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
|
|
1058
|
+
hasOptimisticLock(): boolean;
|
|
1053
1059
|
getPrimaryProps(flatten?: boolean): EntityProperty<Entity>[];
|
|
1054
1060
|
getPrimaryProp(): EntityProperty<Entity>;
|
|
1055
1061
|
/**
|
package/typings.js
CHANGED
|
@@ -79,6 +79,25 @@ export class EntityMetadata {
|
|
|
79
79
|
this.sync();
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
/** For TPT entities, the version column exists only on the table of the entity that declares it. */
|
|
83
|
+
ownsVersionProperty() {
|
|
84
|
+
if (!this.versionProperty) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return this.inheritanceType !== 'tpt' || !this.ownProps || this.ownProps.some(p => p.name === this.versionProperty);
|
|
88
|
+
}
|
|
89
|
+
/** For TPT entities, concurrency check columns exist only on the table of the entity that declares them. */
|
|
90
|
+
getOwnConcurrencyCheckKeys() {
|
|
91
|
+
const keys = [...this.concurrencyCheckKeys];
|
|
92
|
+
if (this.inheritanceType !== 'tpt' || !this.ownProps) {
|
|
93
|
+
return keys;
|
|
94
|
+
}
|
|
95
|
+
return keys.filter(key => this.ownProps.some(p => p.name === key));
|
|
96
|
+
}
|
|
97
|
+
/** Whether updates of this table are guarded by a version property or concurrency check columns it owns. */
|
|
98
|
+
hasOptimisticLock() {
|
|
99
|
+
return this.ownsVersionProperty() || this.getOwnConcurrencyCheckKeys().length > 0;
|
|
100
|
+
}
|
|
82
101
|
getPrimaryProps(flatten = false) {
|
|
83
102
|
const pks = this.primaryKeys.map(pk => this.properties[pk]);
|
|
84
103
|
if (flatten) {
|
|
@@ -216,7 +235,10 @@ export class EntityMetadata {
|
|
|
216
235
|
if (config) {
|
|
217
236
|
const platform = config.getPlatform();
|
|
218
237
|
for (const prop of this.props) {
|
|
219
|
-
if (prop.enum &&
|
|
238
|
+
if (prop.enum &&
|
|
239
|
+
!prop.nativeEnumName &&
|
|
240
|
+
prop.items?.every(item => typeof item === 'string') &&
|
|
241
|
+
!['json', 'jsonb'].includes(prop.columnTypes?.[0])) {
|
|
220
242
|
const name = platform.getIndexName(this.tableName, prop.fieldNames, 'check');
|
|
221
243
|
const exists = this.checks.findIndex(check => check.name === name);
|
|
222
244
|
if (exists !== -1) {
|
|
@@ -204,13 +204,14 @@ export class ChangeSetPersister {
|
|
|
204
204
|
}
|
|
205
205
|
checkConcurrencyKeys(meta, changeSet, cond) {
|
|
206
206
|
const tmp = [];
|
|
207
|
-
|
|
207
|
+
const keys = meta.getOwnConcurrencyCheckKeys();
|
|
208
|
+
for (const key of keys) {
|
|
208
209
|
cond[key] = changeSet.originalEntity[key];
|
|
209
210
|
if (changeSet.payload[key]) {
|
|
210
211
|
tmp.push(key);
|
|
211
212
|
}
|
|
212
213
|
}
|
|
213
|
-
if (tmp.length === 0 &&
|
|
214
|
+
if (tmp.length === 0 && keys.length > 0) {
|
|
214
215
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
215
216
|
}
|
|
216
217
|
}
|
|
@@ -303,32 +304,33 @@ export class ChangeSetPersister {
|
|
|
303
304
|
options = this.prepareOptions(meta, options, {
|
|
304
305
|
convertCustomTypes: false,
|
|
305
306
|
});
|
|
306
|
-
if (meta.
|
|
307
|
-
(!meta.
|
|
307
|
+
if (meta.getOwnConcurrencyCheckKeys().length === 0 &&
|
|
308
|
+
(!meta.ownsVersionProperty() || changeSet.entity[meta.versionProperty] == null)) {
|
|
308
309
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
309
310
|
}
|
|
310
|
-
if (meta.
|
|
311
|
+
if (meta.ownsVersionProperty()) {
|
|
311
312
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(changeSet.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
312
313
|
}
|
|
313
314
|
this.checkConcurrencyKeys(meta, changeSet, cond);
|
|
314
315
|
return this.#driver.nativeUpdate(changeSet.meta.class, cond, changeSet.payload, options);
|
|
315
316
|
}
|
|
316
317
|
async checkOptimisticLocks(meta, changeSets, options) {
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
const concurrencyCheckKeys = meta.getOwnConcurrencyCheckKeys();
|
|
319
|
+
if (concurrencyCheckKeys.length === 0 &&
|
|
320
|
+
(!meta.ownsVersionProperty() || changeSets.every(cs => cs.entity[meta.versionProperty] == null))) {
|
|
319
321
|
return;
|
|
320
322
|
}
|
|
321
323
|
// skip entity references as they don't have version values loaded
|
|
322
324
|
changeSets = changeSets.filter(cs => helper(cs.entity).__initialized);
|
|
325
|
+
const primaryKeys = meta.primaryKeys.concat(...concurrencyCheckKeys);
|
|
323
326
|
const $or = changeSets.map(cs => {
|
|
324
|
-
const cond = Utils.getPrimaryKeyCond(cs.originalEntity,
|
|
325
|
-
if (meta.
|
|
327
|
+
const cond = Utils.getPrimaryKeyCond(cs.originalEntity, primaryKeys);
|
|
328
|
+
if (meta.ownsVersionProperty()) {
|
|
326
329
|
// @ts-ignore
|
|
327
330
|
cond[meta.versionProperty] = this.#platform.convertVersionValue(cs.entity[meta.versionProperty], meta.properties[meta.versionProperty]);
|
|
328
331
|
}
|
|
329
332
|
return cond;
|
|
330
333
|
});
|
|
331
|
-
const primaryKeys = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
|
|
332
334
|
options = this.prepareOptions(meta, options, {
|
|
333
335
|
fields: primaryKeys,
|
|
334
336
|
orderBy: meta.primaryKeys.reduce((o, pk) => {
|
|
@@ -336,7 +338,9 @@ export class ChangeSetPersister {
|
|
|
336
338
|
return o;
|
|
337
339
|
}, {}),
|
|
338
340
|
});
|
|
339
|
-
|
|
341
|
+
// TPT tables query their own metadata, as the version column might not live on the root table
|
|
342
|
+
const target = meta.inheritanceType === 'tpt' ? meta : meta.root;
|
|
343
|
+
const res = await this.#driver.find(target.class, { $or }, options);
|
|
340
344
|
if (res.length !== changeSets.length) {
|
|
341
345
|
// a FK pointing to a composite PK is an array, so the values need to be compared deeply
|
|
342
346
|
const compare = (a, b, keys) => keys.every(k => equals(a[k], b[k]));
|
|
@@ -347,7 +351,7 @@ export class ChangeSetPersister {
|
|
|
347
351
|
}
|
|
348
352
|
}
|
|
349
353
|
checkOptimisticLock(meta, changeSet, res) {
|
|
350
|
-
if ((meta.
|
|
354
|
+
if ((meta.ownsVersionProperty() || meta.getOwnConcurrencyCheckKeys().length > 0) && res && !res.affectedRows) {
|
|
351
355
|
throw OptimisticLockError.lockFailed(changeSet.entity);
|
|
352
356
|
}
|
|
353
357
|
}
|
|
@@ -356,7 +360,7 @@ export class ChangeSetPersister {
|
|
|
356
360
|
* so we use a single query in case of both versioning and default values is used.
|
|
357
361
|
*/
|
|
358
362
|
async reloadVersionValues(meta, changeSets, options) {
|
|
359
|
-
const reloadProps = meta.
|
|
363
|
+
const reloadProps = meta.ownsVersionProperty() && !this.#usesReturningStatement ? [meta.properties[meta.versionProperty]] : [];
|
|
360
364
|
if (changeSets[0].type === ChangeSetType.CREATE) {
|
|
361
365
|
for (const prop of meta.props) {
|
|
362
366
|
if (prop.persist === false) {
|
|
@@ -124,7 +124,6 @@ export class UnitOfWork {
|
|
|
124
124
|
if (options?.newEntity) {
|
|
125
125
|
return entity;
|
|
126
126
|
}
|
|
127
|
-
const forceUndefined = this.#em.config.get('forceUndefined');
|
|
128
127
|
const wrapped = helper(entity);
|
|
129
128
|
if (options?.loaded && wrapped.__initialized && !wrapped.__onLoadFired) {
|
|
130
129
|
this.#loadedEntities.add(entity);
|
|
@@ -332,6 +331,7 @@ export class UnitOfWork {
|
|
|
332
331
|
let parentCs = changeSet.tptChangeSets.find(pc => pc.meta === current);
|
|
333
332
|
if (!parentCs) {
|
|
334
333
|
parentCs = new ChangeSet(entity, changeSet.type, {}, current);
|
|
334
|
+
parentCs.originalEntity = changeSet.originalEntity;
|
|
335
335
|
changeSet.tptChangeSets.splice(idx, 0, parentCs);
|
|
336
336
|
}
|
|
337
337
|
idx++;
|
|
@@ -527,6 +527,10 @@ export class UnitOfWork {
|
|
|
527
527
|
if (prop.formula) {
|
|
528
528
|
delete referrer[prop.name];
|
|
529
529
|
}
|
|
530
|
+
else if (prop.primary) {
|
|
531
|
+
// the referrer's primary key contains the removed entity, so its identity cannot survive the removal
|
|
532
|
+
this.unsetIdentity(referrer);
|
|
533
|
+
}
|
|
530
534
|
else {
|
|
531
535
|
delete helper(referrer).__data[prop.name];
|
|
532
536
|
}
|
|
@@ -702,13 +706,14 @@ export class UnitOfWork {
|
|
|
702
706
|
payload[pk] = identifiers[i] ?? originalChangeSet.payload[pk];
|
|
703
707
|
}
|
|
704
708
|
}
|
|
705
|
-
|
|
709
|
+
// the table declaring the version property or a concurrency check still needs its bump and lock check
|
|
710
|
+
if (!isCreate && Object.keys(payload).length === 0 && !current.hasOptimisticLock()) {
|
|
706
711
|
current = current.tptParent;
|
|
707
712
|
continue;
|
|
708
713
|
}
|
|
709
714
|
const cs = new ChangeSet(entity, originalChangeSet.type, payload, current);
|
|
715
|
+
cs.originalEntity = originalChangeSet.originalEntity;
|
|
710
716
|
if (current === meta) {
|
|
711
|
-
cs.originalEntity = originalChangeSet.originalEntity;
|
|
712
717
|
leafCs = cs;
|
|
713
718
|
}
|
|
714
719
|
else {
|
|
@@ -1205,7 +1210,8 @@ export class UnitOfWork {
|
|
|
1205
1210
|
const addToGroup = (cs) => {
|
|
1206
1211
|
// Skip stub TPT changesets with empty payload (e.g. leaf with no own-property changes on UPDATE)
|
|
1207
1212
|
if ((cs.type === ChangeSetType.UPDATE || cs.type === ChangeSetType.UPDATE_EARLY) &&
|
|
1208
|
-
!Utils.hasObjectKeys(cs.payload)
|
|
1213
|
+
!Utils.hasObjectKeys(cs.payload) &&
|
|
1214
|
+
!cs.meta.hasOptimisticLock()) {
|
|
1209
1215
|
return;
|
|
1210
1216
|
}
|
|
1211
1217
|
const group = groups[cs.type];
|
package/utils/DataloaderUtils.js
CHANGED
|
@@ -197,7 +197,8 @@ export class DataloaderUtils {
|
|
|
197
197
|
const prop = group[0][0].property;
|
|
198
198
|
const options = {};
|
|
199
199
|
const wrap = (cond) => ({ [prop.name]: cond });
|
|
200
|
-
|
|
200
|
+
// `findChildrenFromPivotTable` expects the `orderBy` relative to the target entity, so no wrapping here
|
|
201
|
+
const orderBy = Utils.asArray(group[0][1]?.orderBy);
|
|
201
202
|
const populate = wrap(group[0][1]?.populate);
|
|
202
203
|
const owners = group.map(c => c[0].owner);
|
|
203
204
|
const $or = [];
|
|
@@ -80,6 +80,8 @@ export declare class EntityComparator {
|
|
|
80
80
|
private getGenericComparator;
|
|
81
81
|
private getPropertyComparator;
|
|
82
82
|
private wrap;
|
|
83
|
+
/** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
|
|
84
|
+
private quote;
|
|
83
85
|
private safeKey;
|
|
84
86
|
/**
|
|
85
87
|
* Sets the toArray helper in the context if not already set.
|
|
@@ -587,7 +587,7 @@ export class EntityComparator {
|
|
|
587
587
|
}
|
|
588
588
|
}
|
|
589
589
|
else if (prop.polymorphic) {
|
|
590
|
-
const discriminatorMapKey = `discriminatorMapReverse_${prop.name}`;
|
|
590
|
+
const discriminatorMapKey = `discriminatorMapReverse_${this.safeKey(prop.name)}`;
|
|
591
591
|
const reverseMap = new Map();
|
|
592
592
|
for (const [key, value] of Object.entries(prop.discriminatorMap)) {
|
|
593
593
|
reverseMap.set(value, key);
|
|
@@ -758,10 +758,14 @@ export class EntityComparator {
|
|
|
758
758
|
return this.getGenericComparator(this.wrap(prop.name), `!equals(last${this.wrap(prop.name)}, current${this.wrap(prop.name)})`);
|
|
759
759
|
}
|
|
760
760
|
wrap(key) {
|
|
761
|
-
if (/^\[
|
|
761
|
+
if (/^\[idx_\d+]$/.exec(key)) {
|
|
762
762
|
return key;
|
|
763
763
|
}
|
|
764
|
-
return /^\w+$/.exec(key) ? `.${key}` : `[
|
|
764
|
+
return /^\w+$/.exec(key) ? `.${key}` : `[${this.quote(key)}]`;
|
|
765
|
+
}
|
|
766
|
+
/** Renders a key as a single-quoted JS string literal, safe to embed in generated code. */
|
|
767
|
+
quote(key) {
|
|
768
|
+
return `'${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
765
769
|
}
|
|
766
770
|
safeKey(key) {
|
|
767
771
|
return key.replace(/\W/g, '_');
|
package/utils/QueryHelper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Reference } from '../entity/Reference.js';
|
|
2
|
-
import { Utils } from './Utils.js';
|
|
2
|
+
import { DANGEROUS_PROPERTY_NAMES, Utils } from './Utils.js';
|
|
3
3
|
import { ARRAY_OPERATORS, GroupOperator, JSON_KEY_OPERATORS, ReferenceKind } from '../enums.js';
|
|
4
4
|
import { JsonType } from '../types/JsonType.js';
|
|
5
5
|
import { helper } from '../entity/wrap.js';
|
|
@@ -237,7 +237,11 @@ export class QueryHelper {
|
|
|
237
237
|
// oxfmt-ignore
|
|
238
238
|
const isJsonProperty = prop?.customType instanceof JsonType && !isRaw(value) && (Utils.isPlainObject(value) ? !['$eq', '$elemMatch'].includes(Object.keys(value)[0]) : !Array.isArray(value));
|
|
239
239
|
if (isJsonProperty && prop?.kind !== ReferenceKind.EMBEDDED) {
|
|
240
|
-
|
|
240
|
+
// an explicit alias prefix (e.g. `a.meta`) has to survive, otherwise the condition falls back to the root alias
|
|
241
|
+
const explicitAlias = key.includes('.')
|
|
242
|
+
? key.split('.').slice(0, -1).join('.')
|
|
243
|
+
: undefined;
|
|
244
|
+
return this.processJsonCondition(o, value, [prop.fieldNames[0]], platform, aliased && explicitAlias != null ? explicitAlias : aliased);
|
|
241
245
|
}
|
|
242
246
|
// oxfmt-ignore
|
|
243
247
|
if (Array.isArray(value) && !Utils.isOperator(key) && !QueryHelper.isSupportedOperator(key) && !(customExpression && Raw.getKnownFragment(key).params.length > 0) && options.type !== 'orderBy') {
|
|
@@ -269,7 +273,11 @@ export class QueryHelper {
|
|
|
269
273
|
options.forEach(filter => (opts[filter] = true));
|
|
270
274
|
}
|
|
271
275
|
else if (Utils.isPlainObject(options)) {
|
|
272
|
-
Object.keys(options).forEach(filter =>
|
|
276
|
+
Object.keys(options).forEach(filter => {
|
|
277
|
+
if (!DANGEROUS_PROPERTY_NAMES.includes(filter)) {
|
|
278
|
+
opts[filter] = options[filter];
|
|
279
|
+
}
|
|
280
|
+
});
|
|
273
281
|
}
|
|
274
282
|
return Object.keys(filters)
|
|
275
283
|
.filter(f => QueryHelper.isFilterActive(meta, f, filters[f], opts))
|
|
@@ -297,7 +305,7 @@ export class QueryHelper {
|
|
|
297
305
|
return Utils.mergeConfig({}, propFilters, options);
|
|
298
306
|
}
|
|
299
307
|
static isFilterActive(meta, filterName, filter, options) {
|
|
300
|
-
if (filter.entity && !filter.entity.
|
|
308
|
+
if (filter.entity && !Utils.asArray(filter.entity).some(e => Utils.matchesEntity(e, meta))) {
|
|
301
309
|
return false;
|
|
302
310
|
}
|
|
303
311
|
if (options[filterName] === false) {
|
package/utils/Utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompiledFunctions, Dictionary, EntityData, EntityDictionary, EntityKey, EntityMetadata, EntityName, EntityProperty, Primary } from '../typings.js';
|
|
1
|
+
import type { CompiledFunctions, Dictionary, EntityCtor, EntityData, EntityDictionary, EntityKey, EntityMetadata, EntityName, EntityProperty, Primary } from '../typings.js';
|
|
2
2
|
import type { Platform } from '../platforms/Platform.js';
|
|
3
3
|
import { ScalarReference } from '../entity/Reference.js';
|
|
4
4
|
import { Collection } from '../entity/Collection.js';
|
|
@@ -58,7 +58,7 @@ export declare class Utils {
|
|
|
58
58
|
/**
|
|
59
59
|
* Gets array without duplicates.
|
|
60
60
|
*/
|
|
61
|
-
static unique<T = string>(items: T[]): T[];
|
|
61
|
+
static unique<T = string>(items: T[], equals?: (a: T, b: T) => boolean): T[];
|
|
62
62
|
/**
|
|
63
63
|
* Merges all sources into the target recursively.
|
|
64
64
|
*/
|
|
@@ -129,6 +129,16 @@ export declare class Utils {
|
|
|
129
129
|
* Gets string name of given class.
|
|
130
130
|
*/
|
|
131
131
|
static className<T>(classOrName: string | EntityName<T>): string;
|
|
132
|
+
/**
|
|
133
|
+
* Normalizes an entity reference for identity-safe matching: keeps class references
|
|
134
|
+
* (minifiers can mangle two classes to the same name) and falls back to the class name otherwise.
|
|
135
|
+
*/
|
|
136
|
+
static classOrName<T>(classOrName: string | EntityName<T>): EntityCtor<T> | string;
|
|
137
|
+
/**
|
|
138
|
+
* Checks whether the given entity reference points at the given metadata,
|
|
139
|
+
* comparing classes by identity and strings by class name.
|
|
140
|
+
*/
|
|
141
|
+
static matchesEntity<T>(classOrName: string | EntityName<T>, meta: EntityMetadata<any>): boolean;
|
|
132
142
|
static extractChildElements(items: readonly string[], prefix: string, allSymbol?: string): string[];
|
|
133
143
|
/**
|
|
134
144
|
* Tries to detect TypeScript support.
|
package/utils/Utils.js
CHANGED
|
@@ -153,7 +153,7 @@ export function parseJsonSafe(value) {
|
|
|
153
153
|
/** Collection of general-purpose utility methods used throughout the ORM. */
|
|
154
154
|
export class Utils {
|
|
155
155
|
static PK_SEPARATOR = '~~~';
|
|
156
|
-
static #ORM_VERSION = '7.2.0-dev.
|
|
156
|
+
static #ORM_VERSION = '7.2.0-dev.14';
|
|
157
157
|
/**
|
|
158
158
|
* Checks if the argument is instance of `Object`. Returns false for arrays.
|
|
159
159
|
*/
|
|
@@ -216,10 +216,13 @@ export class Utils {
|
|
|
216
216
|
/**
|
|
217
217
|
* Gets array without duplicates.
|
|
218
218
|
*/
|
|
219
|
-
static unique(items) {
|
|
219
|
+
static unique(items, equals) {
|
|
220
220
|
if (items.length < 2) {
|
|
221
221
|
return items;
|
|
222
222
|
}
|
|
223
|
+
if (equals) {
|
|
224
|
+
return items.filter((a, idx) => items.findIndex(b => equals(a, b)) === idx);
|
|
225
|
+
}
|
|
223
226
|
return [...new Set(items)];
|
|
224
227
|
}
|
|
225
228
|
/**
|
|
@@ -562,6 +565,21 @@ export class Utils {
|
|
|
562
565
|
}
|
|
563
566
|
return classOrName.name;
|
|
564
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Normalizes an entity reference for identity-safe matching: keeps class references
|
|
570
|
+
* (minifiers can mangle two classes to the same name) and falls back to the class name otherwise.
|
|
571
|
+
*/
|
|
572
|
+
static classOrName(classOrName) {
|
|
573
|
+
return typeof classOrName === 'function' ? classOrName : Utils.className(classOrName);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Checks whether the given entity reference points at the given metadata,
|
|
577
|
+
* comparing classes by identity and strings by class name.
|
|
578
|
+
*/
|
|
579
|
+
static matchesEntity(classOrName, meta) {
|
|
580
|
+
const ref = Utils.classOrName(classOrName);
|
|
581
|
+
return typeof ref === 'function' ? ref === meta.class : ref === meta.className;
|
|
582
|
+
}
|
|
565
583
|
static extractChildElements(items, prefix, allSymbol) {
|
|
566
584
|
return items
|
|
567
585
|
.filter(field => field === allSymbol || field.startsWith(`${prefix}.`))
|
package/utils/upsert-utils.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import type { EntityData, EntityMetadata, FilterQuery } from '../typings.js';
|
|
1
|
+
import type { EntityData, EntityKey, EntityMetadata, FilterQuery } from '../typings.js';
|
|
2
2
|
import type { UpsertOptions } from '../drivers/IDatabaseDriver.js';
|
|
3
3
|
import { type Raw } from '../utils/RawQueryFragment.js';
|
|
4
4
|
/** @internal */
|
|
5
5
|
export declare function getOnConflictFields<T>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T>): (keyof T)[];
|
|
6
|
+
/**
|
|
7
|
+
* Detects properties that will get their value generated by an `onCreate` hook during the upsert,
|
|
8
|
+
* i.e. those with an `onCreate` hook and no value provided. Such values are meant for the insert
|
|
9
|
+
* clause only and must not overwrite an existing row via the `on conflict do update set` clause.
|
|
10
|
+
* The property filter mirrors `EntityFactory.assignDefaultValues`.
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function getOnCreateGeneratedFields<T extends object>(meta: EntityMetadata<T>, data: T | EntityData<T>): EntityKey<T>[];
|
|
6
14
|
/** @internal */
|
|
7
15
|
export declare function getOnConflictReturningFields<T, P extends string>(meta: EntityMetadata<T> | undefined, data: EntityData<T>, uniqueFields: (keyof T)[] | Raw, options: UpsertOptions<T, P>): (keyof T)[] | '*';
|
|
8
16
|
/** @internal */
|
package/utils/upsert-utils.js
CHANGED
|
@@ -72,6 +72,22 @@ export function getOnConflictFields(meta, data, uniqueFields, options) {
|
|
|
72
72
|
}
|
|
73
73
|
return keys;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Detects properties that will get their value generated by an `onCreate` hook during the upsert,
|
|
77
|
+
* i.e. those with an `onCreate` hook and no value provided. Such values are meant for the insert
|
|
78
|
+
* clause only and must not overwrite an existing row via the `on conflict do update set` clause.
|
|
79
|
+
* The property filter mirrors `EntityFactory.assignDefaultValues`.
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
export function getOnCreateGeneratedFields(meta, data) {
|
|
83
|
+
return meta.props
|
|
84
|
+
.filter(prop => prop.onCreate &&
|
|
85
|
+
!prop.embedded &&
|
|
86
|
+
![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) &&
|
|
87
|
+
!(prop.getter && !prop.setter) &&
|
|
88
|
+
data[prop.name] == null)
|
|
89
|
+
.map(prop => prop.name);
|
|
90
|
+
}
|
|
75
91
|
/** @internal */
|
|
76
92
|
export function getOnConflictReturningFields(meta, data, uniqueFields, options) {
|
|
77
93
|
/* v8 ignore next */
|
|
@@ -122,7 +138,14 @@ function getPropertyValue(obj, key) {
|
|
|
122
138
|
}
|
|
123
139
|
/** @internal */
|
|
124
140
|
export function getWhereCondition(meta, onConflictFields, data, where) {
|
|
125
|
-
|
|
141
|
+
// TPT children do not inherit the unique flags and indexes of their parent tables
|
|
142
|
+
const uniqueProps = new Set();
|
|
143
|
+
const uniques = [];
|
|
144
|
+
for (let current = meta; current; current = current.tptParent) {
|
|
145
|
+
current.props.filter(p => p.unique).forEach(p => uniqueProps.add(p.name));
|
|
146
|
+
uniques.push(...current.uniques);
|
|
147
|
+
}
|
|
148
|
+
const unique = onConflictFields ?? [...uniqueProps];
|
|
126
149
|
const propIndex = !isRaw(unique) &&
|
|
127
150
|
unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
|
|
128
151
|
if (onConflictFields || where == null) {
|
|
@@ -136,8 +159,8 @@ export function getWhereCondition(meta, onConflictFields, data, where) {
|
|
|
136
159
|
}
|
|
137
160
|
where = { [key]: getPropertyValue(data, unique[propIndex]) };
|
|
138
161
|
}
|
|
139
|
-
else
|
|
140
|
-
for (const u of
|
|
162
|
+
else {
|
|
163
|
+
for (const u of uniques) {
|
|
141
164
|
if (Utils.asArray(u.properties).every(p => data[p] != null)) {
|
|
142
165
|
where = Utils.asArray(u.properties).reduce((o, key) => {
|
|
143
166
|
o[key] = data[key];
|