Skip to content

Commit

Permalink
feat(core): respect ignoreFields on type level in wrap().toObject()
Browse files Browse the repository at this point in the history
Closes #4198
  • Loading branch information
B4nan committed May 14, 2023
1 parent 1b8f2cd commit 193263c
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 71 deletions.
6 changes: 4 additions & 2 deletions 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';
Expand Down Expand Up @@ -30,7 +30,9 @@ export abstract class BaseEntity {
return Reference.create(this) as unknown as Ref<Entity> & LoadedReference<Loaded<Entity, AddEager<Entity>>>;
}

toObject<Entity extends this = this>(ignoreFields: string[] = []): EntityDTO<Entity> {
toObject<Entity extends this = this, Ignored extends EntityKey<Entity> = never>(ignoreFields: Ignored[]): Omit<EntityDTO<Entity>, Ignored>;
toObject<Entity extends this = this>(...args: unknown[]): EntityDTO<Entity>;
toObject<Entity extends this = this, Ignored extends EntityKey<Entity> = never>(ignoreFields?: Ignored[]): Omit<EntityDTO<Entity>, Ignored> {
return helper(this as unknown as Entity).toObject(ignoreFields);
}

Expand Down
64 changes: 32 additions & 32 deletions 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';
Expand All @@ -16,7 +16,7 @@ import type { EntityIdentifier } from './EntityIdentifier';
import { helper } from './wrap';
import type { SerializationContext } from '../serialization/SerializationContext';

export class WrappedEntity<T extends object> {
export class WrappedEntity<Entity extends object> {

__initialized = true;
__touched = false;
Expand All @@ -26,29 +26,29 @@ export class WrappedEntity<T extends object> {
__onLoadFired?: boolean;
__schema?: string;
__em?: EntityManager;
__serializationContext: { root?: SerializationContext<T>; populate?: PopulateOptions<T>[] } = {};
__serializationContext: { root?: SerializationContext<Entity>; populate?: PopulateOptions<Entity>[] } = {};
__loadedProperties = new Set<string>();
__loadedRelations = new Set<string>();
__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<T>;
__pk?: Primary<Entity>;

/** holds the reference wrapper instance (if created), so we can maintain the identity on reference wrappers too */
__reference?: Reference<T>;
__reference?: Reference<Entity>;

/** holds last entity data snapshot, so we can compute changes when persisting managed entities */
__originalEntityData?: EntityData<T>;
__originalEntityData?: EntityData<Entity>;

/** 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<T>,
private readonly pkSerializer?: (e: T) => string,
private readonly pkGetterConverted?: (e: T) => Primary<T>) { }
private readonly pkGetter?: (e: Entity) => Primary<Entity>,
private readonly pkSerializer?: (e: Entity) => string,
private readonly pkGetterConverted?: (e: Entity) => Primary<Entity>) { }

isInitialized(): boolean {
return this.__initialized;
Expand All @@ -63,33 +63,33 @@ export class WrappedEntity<T extends object> {
this.__lazyInitialized = false;
}

toReference(): Ref<T> & LoadedReference<Loaded<T, AddEager<T>>> {
toReference(): Ref<Entity> & LoadedReference<Loaded<Entity, AddEager<Entity>>> {
this.__reference ??= new Reference(this.entity);
return this.__reference as Ref<T> & LoadedReference<Loaded<T, AddEager<T>>>;
return this.__reference as Ref<Entity> & LoadedReference<Loaded<Entity, AddEager<Entity>>>;
}

toObject(ignoreFields: string[] = []): EntityData<T> {
return EntityTransformer.toObject(this.entity, ignoreFields) as EntityData<T>;
toObject<Ignored extends EntityKey<Entity> = never>(ignoreFields?: Ignored[]): Omit<EntityDTO<Entity>, Ignored> {
return EntityTransformer.toObject(this.entity, ignoreFields);
}

toPOJO(): EntityData<T> {
toPOJO(): EntityDTO<Entity> {
return EntityTransformer.toObject(this.entity, [], true);
}

toJSON(...args: any[]): EntityDictionary<T> {
toJSON(...args: any[]): EntityDictionary<Entity> {
// 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<T>, options?: AssignOptions): T {
assign(data: EntityData<Entity>, options?: AssignOptions): Entity {
if ('assign' in this.entity) {
return (this.entity as Dictionary).assign(data, options);
}

return EntityAssigner.assign(this.entity, data, options);
}

async init<P extends Populate<T> = Populate<T>>(populated = true, populate?: P, lockMode?: LockMode, connectionType?: ConnectionType): Promise<T> {
async init<P extends Populate<Entity> = Populate<Entity>>(populated = true, populate?: P, lockMode?: LockMode, connectionType?: ConnectionType): Promise<Entity> {
if (!this.__em) {
throw ValidationError.entityNotManaged(this.entity);
}
Expand Down Expand Up @@ -119,7 +119,7 @@ export class WrappedEntity<T extends object> {
return pk != null;
}

getPrimaryKey(convertCustomTypes = false): Primary<T> | null {
getPrimaryKey(convertCustomTypes = false): Primary<Entity> | null {
const prop = this.__meta.getPrimaryProps()[0];

if (this.__pk != null && this.__meta.compositePK) {
Expand All @@ -138,7 +138,7 @@ export class WrappedEntity<T extends object> {
}

// this method is currently used only in `Driver.syncCollection` and can be probably removed
getPrimaryKeys(convertCustomTypes = false): Primary<T>[] | null {
getPrimaryKeys(convertCustomTypes = false): Primary<Entity>[] | null {
const pk = this.getPrimaryKey(convertCustomTypes);

if (pk == null) {
Expand All @@ -147,17 +147,17 @@ export class WrappedEntity<T extends object> {

if (this.__meta.compositePK) {
return this.__meta.primaryKeys.reduce((ret, pk) => {
const child = this.entity[pk] as AnyEntity<T> | Primary<unknown>;
const child = this.entity[pk] as AnyEntity<Entity> | Primary<unknown>;

if (Utils.isEntity(child, true)) {
const childPk = helper(child).getPrimaryKeys(convertCustomTypes);
ret.push(...childPk as Primary<T>[]);
ret.push(...childPk as Primary<Entity>[]);
} else {
ret.push(child as Primary<T>);
ret.push(child as Primary<Entity>);
}

return ret;
}, [] as Primary<T>[]);
}, [] as Primary<Entity>[]);
}

return [pk];
Expand All @@ -171,25 +171,25 @@ export class WrappedEntity<T extends object> {
this.__schema = schema;
}

setPrimaryKey(id: Primary<T> | null) {
this.entity[this.__meta!.primaryKeys[0]] = id as EntityValue<T>;
setPrimaryKey(id: Primary<Entity> | null) {
this.entity[this.__meta!.primaryKeys[0]] = id as EntityValue<Entity>;
this.__pk = id!;
}

getSerializedPrimaryKey(): string {
return this.pkSerializer!(this.entity);
}

get __meta(): EntityMetadata<T> {
return (this.entity as IWrappedEntityInternal<T>).__meta!;
get __meta(): EntityMetadata<Entity> {
return (this.entity as IWrappedEntityInternal<Entity>).__meta!;
}

get __platform() {
return (this.entity as IWrappedEntityInternal<T>).__platform!;
return (this.entity as IWrappedEntityInternal<Entity>).__platform!;
}

get __primaryKeys(): Primary<T>[] {
return Utils.getPrimaryKeyValues(this.entity, this.__meta!.primaryKeys) as Primary<T>[];
get __primaryKeys(): Primary<Entity>[] {
return Utils.getPrimaryKeyValues(this.entity, this.__meta!.primaryKeys) as Primary<Entity>[];
}

[inspect.custom]() {
Expand Down
54 changes: 27 additions & 27 deletions packages/core/src/serialization/EntityTransformer.ts
@@ -1,13 +1,13 @@
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';
import { ReferenceKind } from '../enums';
import type { Reference } from '../entity/Reference';
import { SerializationContext } from './SerializationContext';

function isVisible<T extends object>(meta: EntityMetadata<T>, propName: EntityKey<T>, ignoreFields: string[] = []): boolean {
function isVisible<Entity extends object>(meta: EntityMetadata<Entity>, propName: EntityKey<Entity>, 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
Expand All @@ -17,7 +17,7 @@ function isVisible<T extends object>(meta: EntityMetadata<T>, propName: EntityKe

export class EntityTransformer {

static toObject<T extends object>(entity: T, ignoreFields: string[] = [], raw = false): EntityData<T> {
static toObject<Entity extends object, Ignored extends EntityKey<Entity> = never>(entity: Entity, ignoreFields: Ignored[] = [], raw = false): Omit<EntityDTO<Entity>, Ignored> {
if (!Array.isArray(ignoreFields)) {
ignoreFields = [];
}
Expand All @@ -26,15 +26,15 @@ export class EntityTransformer {
let contextCreated = false;

if (!wrapped.__serializationContext.root) {
const root = new SerializationContext<T>(wrapped.__serializationContext.populate ?? []);
const root = new SerializationContext<Entity>(wrapped.__serializationContext.populate ?? []);
SerializationContext.propagate(root, entity, isVisible);
contextCreated = true;
}

const root = wrapped.__serializationContext.root!;
const meta = wrapped.__meta;
const ret = {} as EntityData<T>;
const keys = new Set<EntityKey<T>>();
const ret = {} as EntityDTO<Entity>;
const keys = new Set<EntityKey<Entity>>();

if (meta.serializedPrimaryKey && !meta.compositePK) {
keys.add(meta.serializedPrimaryKey);
Expand All @@ -53,15 +53,15 @@ export class EntityTransformer {
}

[...keys]
.filter(prop => raw ? meta.properties[prop] : isVisible<T>(meta, prop, ignoreFields))
.filter(prop => raw ? meta.properties[prop] : isVisible<Entity>(meta, prop, ignoreFields))
.map(prop => {
const cycle = root.visit(meta.className, prop);

if (cycle && visited) {
return [prop, undefined];
}

const val = EntityTransformer.processProperty<T>(prop, entity, raw);
const val = EntityTransformer.processProperty<Entity>(prop, entity, raw);

if (!cycle) {
root.leave(meta.className, prop);
Expand All @@ -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<T>);
.forEach(([prop, value]) => ret[this.propertyName(meta, prop!, wrapped.__platform)] = value as any);

if (!visited) {
root.visited.delete(entity);
Expand All @@ -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<T>)());
.forEach(prop => ret[this.propertyName(meta, prop.name, wrapped.__platform)] = (entity[prop.getterName!] as () => any)());

if (contextCreated) {
root.close();
Expand All @@ -97,19 +97,19 @@ export class EntityTransformer {
return ret;
}

private static propertyName<T>(meta: EntityMetadata<T>, prop: EntityKey<T>, platform?: Platform): EntityKey<T> {
private static propertyName<Entity>(meta: EntityMetadata<Entity>, prop: EntityKey<Entity>, platform?: Platform): EntityKey<Entity> {
if (meta.properties[prop].serializedName) {
return meta.properties[prop].serializedName as EntityKey<T>;
return meta.properties[prop].serializedName as EntityKey<Entity>;
}

if (meta.properties[prop].primary && platform) {
return platform.getSerializedPrimaryKeyField(prop) as EntityKey<T>;
return platform.getSerializedPrimaryKeyField(prop) as EntityKey<Entity>;
}

return prop;
}

private static processProperty<T extends object>(prop: EntityKey<T>, entity: T, raw: boolean): EntityValue<T> | undefined {
private static processProperty<Entity extends object>(prop: EntityKey<Entity>, entity: Entity, raw: boolean): EntityValue<Entity> | undefined {
const wrapped = helper(entity);
const property = wrapped.__meta.properties[prop];
const serializer = property?.serializer;
Expand All @@ -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<T>;
}) as EntityValue<Entity>;
}

const wrapped = entity[prop] && helper(entity[prop]!);
return wrapped ? wrapped.toJSON() as EntityValue<T> : entity[prop];
return wrapped ? wrapped.toJSON() as EntityValue<Entity> : entity[prop];
}

const customType = property?.customType;
Expand All @@ -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<T>;
return wrapped.__platform.normalizePrimaryKey(entity[prop] as unknown as IPrimaryKey) as unknown as EntityValue<Entity>;
}

private static processEntity<T extends object>(prop: keyof T, entity: T, platform: Platform, raw: boolean): EntityValue<T> | undefined {
const child = entity[prop] as unknown as T | Reference<T>;
private static processEntity<Entity extends object>(prop: keyof Entity, entity: Entity, platform: Platform, raw: boolean): EntityValue<Entity> | undefined {
const child = entity[prop] as unknown as Entity | Reference<Entity>;
const wrapped = helper(child);

if (raw && wrapped.isInitialized() && child !== entity) {
return wrapped.toPOJO() as unknown as EntityValue<T>;
return wrapped.toPOJO() as unknown as EntityValue<Entity>;
}

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<T>;
return wrap(child).toJSON(...args) as EntityValue<Entity>;
}

return platform.normalizePrimaryKey(wrapped.getPrimaryKey() as IPrimaryKey) as unknown as EntityValue<T>;
return platform.normalizePrimaryKey(wrapped.getPrimaryKey() as IPrimaryKey) as unknown as EntityValue<Entity>;
}

private static processCollection<T>(prop: keyof T, entity: T, raw: boolean): EntityValue<T> | undefined {
private static processCollection<Entity>(prop: keyof Entity, entity: Entity, raw: boolean): EntityValue<Entity> | undefined {
const col = entity[prop] as Collection<AnyEntity>;

if (raw && col.isInitialized(true)) {
return col.getItems().map(item => wrap(item).toPOJO()) as EntityValue<T>;
return col.getItems().map(item => wrap(item).toPOJO()) as EntityValue<Entity>;
}

if (col.isInitialized(true) && col.shouldPopulate()) {
return col.toArray() as EntityValue<T>;
return col.toArray() as EntityValue<Entity>;
}

if (col.isInitialized()) {
return col.getIdentifiers() as EntityValue<T>;
return col.getIdentifiers() as EntityValue<Entity>;
}

return undefined;
Expand Down
17 changes: 9 additions & 8 deletions packages/core/src/typings.ts
Expand Up @@ -121,19 +121,20 @@ export type FilterQuery<T> =
export type QBFilterQuery<T = any> = FilterQuery<T> | 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<Hint extends string = never>(populate: AutoPath<T, Hint>[] | boolean, options?: EntityLoaderOptions<T, Hint>): Promise<Loaded<T, Hint>>;
init<P extends string = never>(populated?: boolean, populate?: Populate<T, P>, lockMode?: LockMode, connectionType?: ConnectionType): Promise<Loaded<T, P>>;
toReference(): Ref<T> & LoadedReference<Loaded<T, AddEager<T>>>;
toObject(ignoreFields?: string[]): EntityDTO<T>;
toJSON(...args: any[]): EntityDTO<T>;
toPOJO(): EntityDTO<T>;
assign(data: EntityData<T> | Partial<EntityDTO<T>>, options?: AssignOptions | boolean): T;
init<P extends string = never>(populated?: boolean, populate?: Populate<Entity, P>, lockMode?: LockMode, connectionType?: ConnectionType): Promise<Loaded<Entity, P>>;
toReference(): Ref<Entity> & LoadedReference<Loaded<Entity, AddEager<Entity>>>;
toObject<Ignored extends EntityKey<Entity>>(ignoreFields: Ignored[]): Omit<EntityDTO<Entity>, Ignored>;
toObject(...args: unknown[]): EntityDTO<Entity>;
toJSON(...args: any[]): EntityDTO<Entity>;
toPOJO(): EntityDTO<Entity>;
assign(data: EntityData<Entity> | Partial<EntityDTO<Entity>>, options?: AssignOptions | boolean): Entity;
getSchema(): string | undefined;
setSchema(schema?: string): void;
}
Expand Down

0 comments on commit 193263c

Please sign in to comment.