Skip to content

Commit

Permalink
feat(core): return single entity from em.populate() when called on …
Browse files Browse the repository at this point in the history
…single entity
  • Loading branch information
B4nan committed Dec 11, 2023
1 parent 264fc71 commit 4c4ec23
Show file tree
Hide file tree
Showing 10 changed files with 56 additions and 33 deletions.
11 changes: 11 additions & 0 deletions docs/docs/upgrading-v5-to-v6.md
Expand Up @@ -457,6 +457,17 @@ const users = await em.find(User, {}, {

`populate: false` is still allowed and serves as a way to disable eager loaded properties.

## `em.populate()` returns just the entity when called on a single entity

`em.populate()` now returns what you feed in—when you call it with a single entity, you get single entity back, when you call it with an array of entities, you get an array back.

This has been the case initially, but it was problematic to type the method strictly, so it was changed to always an array in v5. This is now resolved in v6, so we can have the previous behavior back, but type-safe.

```diff
-const [author] = await em.populate(author, ['books']);
+const author = await em.populate(author, ['books']);
```

## Duplicate field names are now validated

When you use the same `fieldName` for two properties in one entity, error will be thrown:
Expand Down
16 changes: 9 additions & 7 deletions packages/core/src/EntityManager.ts
Expand Up @@ -42,6 +42,7 @@ import type {
import type {
AnyEntity,
AnyString,
ArrayElement,
AutoPath,
ConnectionType,
Dictionary,
Expand All @@ -66,6 +67,7 @@ import type {
Primary,
Ref,
RequiredEntityData,
UnboxArray,
} from './typings';
import {
EventType,
Expand Down Expand Up @@ -1671,20 +1673,20 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
Naked extends FromEntityType<Entity> = FromEntityType<Entity>,
Hint extends string = never,
Fields extends string = '*',
>(entities: Entity | Entity[], populate: AutoPath<Entity, Hint, '*'>[] | false, options: EntityLoaderOptions<Entity, Fields> = {}): Promise<MergeLoaded<Entity, Naked, Hint, Fields>[]> {
entities = Utils.asArray(entities);
>(entities: Entity, populate: AutoPath<UnboxArray<Entity>, Hint, '*'>[] | false, options: EntityLoaderOptions<UnboxArray<Entity>, Fields> = {}): Promise<Entity extends object[] ? MergeLoaded<ArrayElement<Entity>, Naked, Hint, Fields>[] : MergeLoaded<Entity, Naked, Hint, Fields>> {
const arr = Utils.asArray(entities);

if (entities.length === 0) {
return entities as MergeLoaded<Entity, Naked, Hint, Fields>[];
if (arr.length === 0) {
return entities as any;
}

const em = this.getContext();
options.schema ??= em._schema;
const entityName = (entities[0] as Dictionary).constructor.name;
const entityName = (arr[0] as Dictionary).constructor.name;
const preparedPopulate = em.preparePopulate<Entity, Hint>(entityName, { populate: populate as any });
await em.entityLoader.populate(entityName, entities, preparedPopulate, options);
await em.entityLoader.populate(entityName, arr, preparedPopulate, options as EntityLoaderOptions<Entity>);

return entities as MergeLoaded<Entity, Naked, Hint, Fields>[];
return entities as any;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/entity/EntityLoader.ts
Expand Up @@ -52,7 +52,8 @@ export class EntityLoader {
}

/**
* Loads specified relations in batch. This will execute one query for each relation, that will populate it on all of the specified entities.
* Loads specified relations in batch.
* This will execute one query for each relation, that will populate it on all the specified entities.
*/
async populate<Entity extends object, Fields extends string = '*'>(entityName: string, entities: Entity[], populate: PopulateOptions<Entity>[] | boolean, options: EntityLoaderOptions<Entity, Fields>): Promise<void> {
if (entities.length === 0 || Utils.isEmpty(populate)) {
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/entity/EntityRepository.ts
Expand Up @@ -16,6 +16,7 @@ import type {
FromEntityType,
IsSubset,
MergeLoaded,
ArrayElement,
} from '../typings';
import type {
CountOptions,
Expand Down Expand Up @@ -226,12 +227,14 @@ export class EntityRepository<Entity extends object> {
* Loads specified relations in batch. This will execute one query for each relation, that will populate it on all the specified entities.
*/
async populate<
Ent extends Entity | Entity[],
Hint extends string = never,
Naked extends FromEntityType<Entity> = FromEntityType<Entity>,
Fields extends string = '*',
>(entities: Entity | Entity[], populate: AutoPath<Entity, Hint, '*'>[] | false, options?: EntityLoaderOptions<Entity, Fields>): Promise<MergeLoaded<Entity, Naked, Hint, Fields>[]> {
>(entities: Ent, populate: AutoPath<Entity, Hint, '*'>[] | false, options?: EntityLoaderOptions<Entity, Fields>): Promise<Ent extends object[] ? MergeLoaded<ArrayElement<Ent>, Naked, Hint, Fields>[] : MergeLoaded<Ent, Naked, Hint, Fields>> {
this.validateRepositoryType(entities, 'populate');
return this.getEntityManager().populate(entities as Entity, populate, options);
// @ts-ignore hard to type
return this.getEntityManager().populate(entities, populate, options);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/entity/WrappedEntity.ts
Expand Up @@ -151,6 +151,7 @@ export class WrappedEntity<Entity extends object> {
throw ValidationError.entityNotManaged(this.entity);
}

// @ts-ignore hard to type
await this.__em.populate(this.entity, populate, options);

return this.entity as Loaded<Entity, Hint>;
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/metadata/EntitySchema.ts
Expand Up @@ -7,7 +7,7 @@ import {
type Dictionary,
type EntityName,
type EntityProperty,
type ExcludeFunctions,
type CleanKeys,
type ExpandProperty,
type IsNever,
} from '../typings';
Expand Down Expand Up @@ -37,7 +37,7 @@ export type EntitySchemaMetadata<Entity, Base = never> =
& Omit<Partial<EntityMetadata<Entity>>, 'name' | 'properties' | 'extends'>
& ({ name: string } | { class: Constructor<Entity>; name?: string })
& { extends?: string | EntitySchema<Base> }
& { properties?: { [Key in keyof OmitBaseProps<Entity, Base> as ExcludeFunctions<OmitBaseProps<Entity, Base>, Key>]-?: EntitySchemaProperty<ExpandProperty<NonNullable<Entity[Key]>>, Entity> } };
& { properties?: { [Key in keyof OmitBaseProps<Entity, Base> as CleanKeys<OmitBaseProps<Entity, Base>, Key>]-?: EntitySchemaProperty<ExpandProperty<NonNullable<Entity[Key]>>, Entity> } };

export class EntitySchema<Entity = any, Base = never> {

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/serialization/EntitySerializer.ts
Expand Up @@ -258,6 +258,7 @@ export interface SerializeOptions<T, P extends string = never, E extends string
/** Skip properties with `null` value. */
skipNull?: boolean;
}

/**
* Converts entity instance to POJO, converting the `Collection`s to arrays and unwrapping the `Reference` wrapper, while respecting the serialization options.
* This method accepts either a single entity or an array of entities, and returns the corresponding POJO or an array of POJO.
Expand Down
20 changes: 11 additions & 9 deletions packages/core/src/typings.ts
Expand Up @@ -31,12 +31,13 @@ import type { FindOneOptions, FindOptions } from './drivers';

export type Constructor<T = unknown> = new (...args: any[]) => T;
export type Dictionary<T = any> = { [k: string]: T };
export type EntityKey<T = unknown> = string & keyof { [K in keyof T as ExcludeFunctions<T, K>]?: unknown };
export type EntityKey<T = unknown> = string & keyof { [K in keyof T as CleanKeys<T, K>]?: unknown };
export type EntityValue<T> = T[EntityKey<T>];
export type FilterKey<T> = keyof FilterQuery<T>;
export type AsyncFunction<R = any, T = Dictionary> = (args: T) => Promise<T>;
export type Compute<T> = { [K in keyof T]: T[K] } & {};
export type ExcludeFunctions<T, K extends keyof T> = T[K] extends Function ? never : (K extends symbol | '__selectedType' | '__loadedType' ? never : K);
type InternalKeys = 'EntityRepositoryType' | 'PrimaryKeyProp' | 'OptionalProps' | 'EagerProps' | 'HiddenProps' | '__selectedType' | '__loadedType';
export type CleanKeys<T, K extends keyof T> = T[K] extends Function ? never : (K extends symbol | InternalKeys ? never : K);
export type Cast<T, R> = T extends R ? T : R;
export type IsUnknown<T> = T extends unknown ? unknown extends T ? true : never : never;
export type IsAny<T> = 0 extends (1 & T) ? true : false;
Expand Down Expand Up @@ -204,7 +205,7 @@ export type GetRepository<Entity extends { [k: PropertyKey]: any }, Fallback> =

export type EntityDataPropValue<T> = T | Primary<T>;
type ExpandEntityProp<T> = T extends Record<string, any>
? { [K in keyof T as ExcludeFunctions<T, K>]?: EntityDataProp<ExpandProperty<T[K]>> | EntityDataPropValue<ExpandProperty<T[K]>> | null } | EntityDataPropValue<ExpandProperty<T>>
? { [K in keyof T as CleanKeys<T, K>]?: EntityDataProp<ExpandProperty<T[K]>> | EntityDataPropValue<ExpandProperty<T[K]>> | null } | EntityDataPropValue<ExpandProperty<T>>
: T;
type ExpandRequiredEntityProp<T, I> = T extends Record<string, any>
? ExpandRequiredEntityPropObject<T, I> | EntityDataPropValue<ExpandProperty<T>>
Expand Down Expand Up @@ -266,8 +267,8 @@ type IsOptional<T, K extends keyof T, I> = T[K] extends Collection<any, any>
: K extends ProbablyOptionalProps<T>
? true
: false;
type RequiredKeys<T, K extends keyof T, I> = IsOptional<T, K, I> extends false ? ExcludeFunctions<T, K> : never;
type OptionalKeys<T, K extends keyof T, I> = IsOptional<T, K, I> extends false ? never : ExcludeFunctions<T, K>;
type RequiredKeys<T, K extends keyof T, I> = IsOptional<T, K, I> extends false ? CleanKeys<T, K> : never;
type OptionalKeys<T, K extends keyof T, I> = IsOptional<T, K, I> extends false ? never : CleanKeys<T, K>;
export type EntityData<T> = { [K in EntityKey<T>]?: EntityDataItem<T[K]> };
export type RequiredEntityData<T, I = never> = {
[K in keyof T as RequiredKeys<T, K, I>]: T[K] | RequiredEntityDataProp<T[K], T> | Primary<T[K]>
Expand Down Expand Up @@ -816,7 +817,7 @@ export type PopulateOptions<T> = {
type Loadable<T extends object> = Collection<T, any> | Reference<T> | Ref<T> | readonly T[]; // we need to support raw arrays in embeddables too to allow population
type ExtractType<T> = T extends Loadable<infer U> ? U : T;

type ExtractStringKeys<T> = { [K in keyof T]: ExcludeFunctions<T, K> }[keyof T] & {};
type ExtractStringKeys<T> = { [K in keyof T]: CleanKeys<T, K> }[keyof T] & {};
type StringKeys<T, E extends string = never> = T extends Collection<any, any>
? ExtractStringKeys<ExtractType<T>> | E
: T extends Reference<any>
Expand Down Expand Up @@ -859,6 +860,7 @@ export type AutoPath<O, P extends string | boolean, E extends string = never, D
: never
: never;

export type UnboxArray<T> = T extends any[] ? ArrayElement<T> : T;
export type ArrayElement<ArrayType extends unknown[]> = ArrayType extends (infer ElementType)[] ? ElementType : never;

export type ExpandProperty<T> = T extends Reference<infer U>
Expand Down Expand Up @@ -914,7 +916,7 @@ export type IsSubset<T, U> = keyof U extends keyof T
? {}
: Dictionary extends U
? {}
: { [K in keyof U as K extends keyof T ? never : ExcludeFunctions<U, K>]: never; };
: { [K in keyof U as K extends keyof T ? never : CleanKeys<U, K>]: never; };

// eslint-disable-next-line @typescript-eslint/naming-convention
declare const __selectedType: unique symbol;
Expand All @@ -940,11 +942,11 @@ type MergeFields<F1 extends string, F2 extends string, P1, P2> =
export type MergeLoaded<T, U, P extends string, F extends string> =
T extends Loaded<U, infer PP, infer FF>
? string extends FF
? T
? Loaded<T, P, F>
: string extends P
? Loaded<U, never, F | (FF & string)>
: Loaded<U, P | (PP & string), MergeFields<F, (FF & string), P, PP>>
: T;
: Loaded<T, P, F>;

type AddOptional<T> = undefined | null extends T ? null | undefined : null extends T ? null : undefined extends T ? undefined : never;
type LoadedProp<T, L extends string = never, F extends string = '*'> = LoadedLoadable<T, Loaded<ExtractType<T>, L, F>>;
Expand Down
Expand Up @@ -392,7 +392,7 @@ describe('partial loading (mysql)', () => {
expect(r1.books[0].author.name).toBeUndefined();
expect(r1.books[0].author.email).toBe(god.email);

const [r2] = await orm.em.populate(r1, ['*'], { refresh: true });
const r2 = await orm.em.populate(r1, ['*'], { refresh: true });
expect(r2.name).toBe('t1');
expect(r2.books[0].title).toBe('Bible 1');
expect(r2.books[0].price).toBe('123.00');
Expand Down
24 changes: 13 additions & 11 deletions tests/features/single-table-inheritance/GH997.test.ts
Expand Up @@ -98,18 +98,20 @@ describe('GH issue 997', () => {
.orderBy({ type: QueryOrder.ASC })
.getResult();

const parents = await orm.em.populate(results as Child1[], ['qaInfo.parent', 'rel']);
const parent = await orm.em.populate(results[0] as Child1, ['qaInfo.parent', 'rel']);

expect(parent).toBeInstanceOf(Child1);
expect(parent.type).toBe('Child1');
expect(parent.qaInfo.length).toBe(0);
expect((parent as Child1).rel.length).toBe(3);
expect((parent as Child1).rel[0]).toBeInstanceOf(Child1Specific);
expect((parent as Child1).rel[0].child1).toBeInstanceOf(Child1);
expect((parent as Child1).rel[1]).toBeInstanceOf(Child1Specific);
expect((parent as Child1).rel[1].child1).toBeInstanceOf(Child1);
expect((parent as Child1).rel[2]).toBeInstanceOf(Child1Specific);
expect((parent as Child1).rel[2].child1).toBeInstanceOf(Child1);

expect(parents[0]).toBeInstanceOf(Child1);
expect(parents[0].type).toBe('Child1');
expect(parents[0].qaInfo.length).toBe(0);
expect((parents[0] as Child1).rel.length).toBe(3);
expect((parents[0] as Child1).rel[0]).toBeInstanceOf(Child1Specific);
expect((parents[0] as Child1).rel[0].child1).toBeInstanceOf(Child1);
expect((parents[0] as Child1).rel[1]).toBeInstanceOf(Child1Specific);
expect((parents[0] as Child1).rel[1].child1).toBeInstanceOf(Child1);
expect((parents[0] as Child1).rel[2]).toBeInstanceOf(Child1Specific);
expect((parents[0] as Child1).rel[2].child1).toBeInstanceOf(Child1);
const parents = await orm.em.populate(results as Child1[], ['qaInfo.parent', 'rel']);
expect(parents[1]).toBeInstanceOf(Child2);
expect(parents[1].type).toBe('Child2');
expect(parents[1].qaInfo.length).toBe(3);
Expand Down

0 comments on commit 4c4ec23

Please sign in to comment.