diff --git a/packages/core/src/entity/BaseEntity.ts b/packages/core/src/entity/BaseEntity.ts index b5e7e90db2bb..b0447a9f45dc 100644 --- a/packages/core/src/entity/BaseEntity.ts +++ b/packages/core/src/entity/BaseEntity.ts @@ -1,5 +1,5 @@ import { Reference } from './Reference'; -import type { AutoPath, Ref, EntityData, EntityDTO, Loaded, AddEager, LoadedReference } from '../typings'; +import type { AutoPath, Ref, EntityData, EntityDTO, Loaded, AddEager, LoadedReference, EntityKey } from '../typings'; import type { AssignOptions } from './EntityAssigner'; import { EntityAssigner } from './EntityAssigner'; import type { EntityLoaderOptions } from './EntityLoader'; @@ -30,7 +30,9 @@ export abstract class BaseEntity { return Reference.create(this) as unknown as Ref & LoadedReference>>; } - toObject(ignoreFields: string[] = []): EntityDTO { + toObject = never>(ignoreFields: Ignored[]): Omit, Ignored>; + toObject(...args: unknown[]): EntityDTO; + toObject = never>(ignoreFields?: Ignored[]): Omit, Ignored> { return helper(this as unknown as Entity).toObject(ignoreFields); } diff --git a/packages/core/src/entity/WrappedEntity.ts b/packages/core/src/entity/WrappedEntity.ts index 78488027955f..f105ab8a7f18 100644 --- a/packages/core/src/entity/WrappedEntity.ts +++ b/packages/core/src/entity/WrappedEntity.ts @@ -1,8 +1,8 @@ import { inspect } from 'util'; import type { EntityManager } from '../EntityManager'; import type { - AnyEntity, ConnectionType, Dictionary, EntityData, EntityDictionary, EntityMetadata, IHydrator, EntityValue, - IWrappedEntityInternal, Populate, PopulateOptions, Primary, AutoPath, Loaded, Ref, AddEager, Loaded, LoadedReference, + AnyEntity, ConnectionType, Dictionary, EntityData, EntityDictionary, EntityMetadata, IHydrator, EntityValue, EntityKey, + IWrappedEntityInternal, Populate, PopulateOptions, Primary, AutoPath, Loaded, Ref, AddEager, Loaded, LoadedReference, EntityDTO, } from '../typings'; import { Reference } from './Reference'; import { EntityTransformer } from '../serialization/EntityTransformer'; @@ -16,7 +16,7 @@ import type { EntityIdentifier } from './EntityIdentifier'; import { helper } from './wrap'; import type { SerializationContext } from '../serialization/SerializationContext'; -export class WrappedEntity { +export class WrappedEntity { __initialized = true; __touched = false; @@ -26,29 +26,29 @@ export class WrappedEntity { __onLoadFired?: boolean; __schema?: string; __em?: EntityManager; - __serializationContext: { root?: SerializationContext; populate?: PopulateOptions[] } = {}; + __serializationContext: { root?: SerializationContext; populate?: PopulateOptions[] } = {}; __loadedProperties = new Set(); __loadedRelations = new Set(); __data: Dictionary = {}; __processing = false; /** stores last known primary key, as its current state might be broken due to propagation/orphan removal, but we need to know the PK to be able t remove the entity */ - __pk?: Primary; + __pk?: Primary; /** holds the reference wrapper instance (if created), so we can maintain the identity on reference wrappers too */ - __reference?: Reference; + __reference?: Reference; /** holds last entity data snapshot, so we can compute changes when persisting managed entities */ - __originalEntityData?: EntityData; + __originalEntityData?: EntityData; /** holds wrapped primary key, so we can compute change set without eager commit */ __identifier?: EntityIdentifier; - constructor(private readonly entity: T, + constructor(private readonly entity: Entity, private readonly hydrator: IHydrator, - private readonly pkGetter?: (e: T) => Primary, - private readonly pkSerializer?: (e: T) => string, - private readonly pkGetterConverted?: (e: T) => Primary) { } + private readonly pkGetter?: (e: Entity) => Primary, + private readonly pkSerializer?: (e: Entity) => string, + private readonly pkGetterConverted?: (e: Entity) => Primary) { } isInitialized(): boolean { return this.__initialized; @@ -63,25 +63,25 @@ export class WrappedEntity { this.__lazyInitialized = false; } - toReference(): Ref & LoadedReference>> { + toReference(): Ref & LoadedReference>> { this.__reference ??= new Reference(this.entity); - return this.__reference as Ref & LoadedReference>>; + return this.__reference as Ref & LoadedReference>>; } - toObject(ignoreFields: string[] = []): EntityData { - return EntityTransformer.toObject(this.entity, ignoreFields) as EntityData; + toObject = never>(ignoreFields?: Ignored[]): Omit, Ignored> { + return EntityTransformer.toObject(this.entity, ignoreFields); } - toPOJO(): EntityData { + toPOJO(): EntityDTO { return EntityTransformer.toObject(this.entity, [], true); } - toJSON(...args: any[]): EntityDictionary { + toJSON(...args: any[]): EntityDictionary { // toJSON methods is added to the prototype during discovery to support automatic serialization via JSON.stringify() return (this.entity as Dictionary).toJSON(...args); } - assign(data: EntityData, options?: AssignOptions): T { + assign(data: EntityData, options?: AssignOptions): Entity { if ('assign' in this.entity) { return (this.entity as Dictionary).assign(data, options); } @@ -89,7 +89,7 @@ export class WrappedEntity { return EntityAssigner.assign(this.entity, data, options); } - async init

= Populate>(populated = true, populate?: P, lockMode?: LockMode, connectionType?: ConnectionType): Promise { + async init

= Populate>(populated = true, populate?: P, lockMode?: LockMode, connectionType?: ConnectionType): Promise { if (!this.__em) { throw ValidationError.entityNotManaged(this.entity); } @@ -119,7 +119,7 @@ export class WrappedEntity { return pk != null; } - getPrimaryKey(convertCustomTypes = false): Primary | null { + getPrimaryKey(convertCustomTypes = false): Primary | null { const prop = this.__meta.getPrimaryProps()[0]; if (this.__pk != null && this.__meta.compositePK) { @@ -138,7 +138,7 @@ export class WrappedEntity { } // this method is currently used only in `Driver.syncCollection` and can be probably removed - getPrimaryKeys(convertCustomTypes = false): Primary[] | null { + getPrimaryKeys(convertCustomTypes = false): Primary[] | null { const pk = this.getPrimaryKey(convertCustomTypes); if (pk == null) { @@ -147,17 +147,17 @@ export class WrappedEntity { if (this.__meta.compositePK) { return this.__meta.primaryKeys.reduce((ret, pk) => { - const child = this.entity[pk] as AnyEntity | Primary; + const child = this.entity[pk] as AnyEntity | Primary; if (Utils.isEntity(child, true)) { const childPk = helper(child).getPrimaryKeys(convertCustomTypes); - ret.push(...childPk as Primary[]); + ret.push(...childPk as Primary[]); } else { - ret.push(child as Primary); + ret.push(child as Primary); } return ret; - }, [] as Primary[]); + }, [] as Primary[]); } return [pk]; @@ -171,8 +171,8 @@ export class WrappedEntity { this.__schema = schema; } - setPrimaryKey(id: Primary | null) { - this.entity[this.__meta!.primaryKeys[0]] = id as EntityValue; + setPrimaryKey(id: Primary | null) { + this.entity[this.__meta!.primaryKeys[0]] = id as EntityValue; this.__pk = id!; } @@ -180,16 +180,16 @@ export class WrappedEntity { return this.pkSerializer!(this.entity); } - get __meta(): EntityMetadata { - return (this.entity as IWrappedEntityInternal).__meta!; + get __meta(): EntityMetadata { + return (this.entity as IWrappedEntityInternal).__meta!; } get __platform() { - return (this.entity as IWrappedEntityInternal).__platform!; + return (this.entity as IWrappedEntityInternal).__platform!; } - get __primaryKeys(): Primary[] { - return Utils.getPrimaryKeyValues(this.entity, this.__meta!.primaryKeys) as Primary[]; + get __primaryKeys(): Primary[] { + return Utils.getPrimaryKeyValues(this.entity, this.__meta!.primaryKeys) as Primary[]; } [inspect.custom]() { diff --git a/packages/core/src/serialization/EntityTransformer.ts b/packages/core/src/serialization/EntityTransformer.ts index 3d2d6db0f1ea..e7b08d8ae839 100644 --- a/packages/core/src/serialization/EntityTransformer.ts +++ b/packages/core/src/serialization/EntityTransformer.ts @@ -1,5 +1,5 @@ import type { Collection } from '../entity/Collection'; -import type { AnyEntity, EntityData, EntityKey, EntityMetadata, EntityValue, IPrimaryKey } from '../typings'; +import type { AnyEntity, EntityDTO, EntityKey, EntityMetadata, EntityValue, IPrimaryKey } from '../typings'; import { helper, wrap } from '../entity/wrap'; import type { Platform } from '../platforms'; import { Utils } from '../utils/Utils'; @@ -7,7 +7,7 @@ import { ReferenceKind } from '../enums'; import type { Reference } from '../entity/Reference'; import { SerializationContext } from './SerializationContext'; -function isVisible(meta: EntityMetadata, propName: EntityKey, ignoreFields: string[] = []): boolean { +function isVisible(meta: EntityMetadata, propName: EntityKey, ignoreFields: string[] = []): boolean { const prop = meta.properties[propName]; const visible = prop && !prop.hidden; const prefixed = prop && !prop.primary && propName.startsWith('_'); // ignore prefixed properties, if it's not a PK @@ -17,7 +17,7 @@ function isVisible(meta: EntityMetadata, propName: EntityKe export class EntityTransformer { - static toObject(entity: T, ignoreFields: string[] = [], raw = false): EntityData { + static toObject = never>(entity: Entity, ignoreFields: Ignored[] = [], raw = false): Omit, Ignored> { if (!Array.isArray(ignoreFields)) { ignoreFields = []; } @@ -26,15 +26,15 @@ export class EntityTransformer { let contextCreated = false; if (!wrapped.__serializationContext.root) { - const root = new SerializationContext(wrapped.__serializationContext.populate ?? []); + const root = new SerializationContext(wrapped.__serializationContext.populate ?? []); SerializationContext.propagate(root, entity, isVisible); contextCreated = true; } const root = wrapped.__serializationContext.root!; const meta = wrapped.__meta; - const ret = {} as EntityData; - const keys = new Set>(); + const ret = {} as EntityDTO; + const keys = new Set>(); if (meta.serializedPrimaryKey && !meta.compositePK) { keys.add(meta.serializedPrimaryKey); @@ -53,7 +53,7 @@ export class EntityTransformer { } [...keys] - .filter(prop => raw ? meta.properties[prop] : isVisible(meta, prop, ignoreFields)) + .filter(prop => raw ? meta.properties[prop] : isVisible(meta, prop, ignoreFields)) .map(prop => { const cycle = root.visit(meta.className, prop); @@ -61,7 +61,7 @@ export class EntityTransformer { return [prop, undefined]; } - const val = EntityTransformer.processProperty(prop, entity, raw); + const val = EntityTransformer.processProperty(prop, entity, raw); if (!cycle) { root.leave(meta.className, prop); @@ -70,7 +70,7 @@ export class EntityTransformer { return [prop, val] as const; }) .filter(([, value]) => typeof value !== 'undefined') - .forEach(([prop, value]) => ret[this.propertyName(meta, prop!, wrapped.__platform)] = value as EntityValue); + .forEach(([prop, value]) => ret[this.propertyName(meta, prop!, wrapped.__platform)] = value as any); if (!visited) { root.visited.delete(entity); @@ -83,12 +83,12 @@ export class EntityTransformer { // decorated getters meta.props .filter(prop => prop.getter && !prop.hidden && typeof entity[prop.name] !== 'undefined') - .forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = entity[prop.name]); + .forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = entity[prop.name] as any); // decorated get methods meta.props .filter(prop => prop.getterName && !prop.hidden && entity[prop.getterName] as unknown instanceof Function) - .forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = (entity[prop.getterName!] as () => EntityValue)()); + .forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = (entity[prop.getterName!] as () => any)()); if (contextCreated) { root.close(); @@ -97,19 +97,19 @@ export class EntityTransformer { return ret; } - private static propertyName(meta: EntityMetadata, prop: EntityKey, platform?: Platform): EntityKey { + private static propertyName(meta: EntityMetadata, prop: EntityKey, platform?: Platform): EntityKey { if (meta.properties[prop].serializedName) { - return meta.properties[prop].serializedName as EntityKey; + return meta.properties[prop].serializedName as EntityKey; } if (meta.properties[prop].primary && platform) { - return platform.getSerializedPrimaryKeyField(prop) as EntityKey; + return platform.getSerializedPrimaryKeyField(prop) as EntityKey; } return prop; } - private static processProperty(prop: EntityKey, entity: T, raw: boolean): EntityValue | undefined { + private static processProperty(prop: EntityKey, entity: Entity, raw: boolean): EntityValue | undefined { const wrapped = helper(entity); const property = wrapped.__meta.properties[prop]; const serializer = property?.serializer; @@ -131,11 +131,11 @@ export class EntityTransformer { return (entity[prop] as object[]).map(item => { const wrapped = item && helper(item); return wrapped ? wrapped.toJSON() : item; - }) as EntityValue; + }) as EntityValue; } const wrapped = entity[prop] && helper(entity[prop]!); - return wrapped ? wrapped.toJSON() as EntityValue : entity[prop]; + return wrapped ? wrapped.toJSON() as EntityValue : entity[prop]; } const customType = property?.customType; @@ -144,38 +144,38 @@ export class EntityTransformer { return customType.toJSON(entity[prop], wrapped.__platform); } - return wrapped.__platform.normalizePrimaryKey(entity[prop] as unknown as IPrimaryKey) as unknown as EntityValue; + return wrapped.__platform.normalizePrimaryKey(entity[prop] as unknown as IPrimaryKey) as unknown as EntityValue; } - private static processEntity(prop: keyof T, entity: T, platform: Platform, raw: boolean): EntityValue | undefined { - const child = entity[prop] as unknown as T | Reference; + private static processEntity(prop: keyof Entity, entity: Entity, platform: Platform, raw: boolean): EntityValue | undefined { + const child = entity[prop] as unknown as Entity | Reference; const wrapped = helper(child); if (raw && wrapped.isInitialized() && child !== entity) { - return wrapped.toPOJO() as unknown as EntityValue; + return wrapped.toPOJO() as unknown as EntityValue; } if (wrapped.isInitialized() && (wrapped.__populated || !wrapped.__managed) && child !== entity && !wrapped.__lazyInitialized) { const args = [...wrapped.__meta.toJsonParams.map(() => undefined)]; - return wrap(child).toJSON(...args) as EntityValue; + return wrap(child).toJSON(...args) as EntityValue; } - return platform.normalizePrimaryKey(wrapped.getPrimaryKey() as IPrimaryKey) as unknown as EntityValue; + return platform.normalizePrimaryKey(wrapped.getPrimaryKey() as IPrimaryKey) as unknown as EntityValue; } - private static processCollection(prop: keyof T, entity: T, raw: boolean): EntityValue | undefined { + private static processCollection(prop: keyof Entity, entity: Entity, raw: boolean): EntityValue | undefined { const col = entity[prop] as Collection; if (raw && col.isInitialized(true)) { - return col.getItems().map(item => wrap(item).toPOJO()) as EntityValue; + return col.getItems().map(item => wrap(item).toPOJO()) as EntityValue; } if (col.isInitialized(true) && col.shouldPopulate()) { - return col.toArray() as EntityValue; + return col.toArray() as EntityValue; } if (col.isInitialized()) { - return col.getIdentifiers() as EntityValue; + return col.getIdentifiers() as EntityValue; } return undefined; diff --git a/packages/core/src/typings.ts b/packages/core/src/typings.ts index 7ac9bfb1dd45..22ea42f424b2 100644 --- a/packages/core/src/typings.ts +++ b/packages/core/src/typings.ts @@ -121,19 +121,20 @@ export type FilterQuery = export type QBFilterQuery = FilterQuery | Dictionary; export interface IWrappedEntity< - T extends object, - P extends string = string, + Entity extends object, + Hint extends string = string, > { isInitialized(): boolean; isTouched(): boolean; populated(populated?: boolean): void; populate(populate: AutoPath[] | boolean, options?: EntityLoaderOptions): Promise>; - init

(populated?: boolean, populate?: Populate, lockMode?: LockMode, connectionType?: ConnectionType): Promise>; - toReference(): Ref & LoadedReference>>; - toObject(ignoreFields?: string[]): EntityDTO; - toJSON(...args: any[]): EntityDTO; - toPOJO(): EntityDTO; - assign(data: EntityData | Partial>, options?: AssignOptions | boolean): T; + init

(populated?: boolean, populate?: Populate, lockMode?: LockMode, connectionType?: ConnectionType): Promise>; + toReference(): Ref & LoadedReference>>; + toObject>(ignoreFields: Ignored[]): Omit, Ignored>; + toObject(...args: unknown[]): EntityDTO; + toJSON(...args: any[]): EntityDTO; + toPOJO(): EntityDTO; + assign(data: EntityData | Partial>, options?: AssignOptions | boolean): Entity; getSchema(): string | undefined; setSchema(schema?: string): void; } diff --git a/tests/EntityHelper.mysql.test.ts b/tests/EntityHelper.mysql.test.ts index 409a07515f73..0e26e33d0d3d 100644 --- a/tests/EntityHelper.mysql.test.ts +++ b/tests/EntityHelper.mysql.test.ts @@ -14,7 +14,10 @@ describe('EntityHelperMySql', () => { test(`toObject allows to hide PK (GH issue 644)`, async () => { const bar = FooBar2.create('fb'); await orm.em.persistAndFlush(bar); - expect(wrap(bar).toObject(['id'])).not.toMatchObject({ id: bar.id, name: 'fb' }); + const dto = wrap(bar).toObject(['id']); + expect(dto).not.toMatchObject({ id: bar.id, name: 'fb' }); + // @ts-expect-error + expect(dto.id).toBeUndefined(); }); test(`toObject handles recursion in 1:1`, async () => { diff --git a/tests/types.test.ts b/tests/types.test.ts index bcd3d7977e12..543fbcf6e5bb 100644 --- a/tests/types.test.ts +++ b/tests/types.test.ts @@ -500,7 +500,9 @@ describe('check typings', () => { test('Loaded type with EntityDTO (with ORM base entities)', async () => { const b1 = createEntity>(); const o11 = wrap(b1).toObject(); - const o12 = b1.toObject(); + const o12 = b1.toObject(['id']); + // @ts-expect-error + const id10 = o12.id; // @ts-expect-error o11.publisher is now just number, as it's not populated const id11 = o11.publisher?.id; // @ts-expect-error o12.publisher is now just number, as it's not populated