Skip to content

Commit

Permalink
fix(core): improve serialization of lazily partially loaded entities
Browse files Browse the repository at this point in the history
Closes #5139
  • Loading branch information
B4nan committed Mar 18, 2024
1 parent ebb966c commit 1c7b446
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 24 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1812,11 +1812,11 @@ export class EntityManager<Driver extends IDatabaseDriver = IDatabaseDriver> {
*/
async populate<
Entity extends object,
Naked extends FromEntityType<Entity> = FromEntityType<Entity>,
Naked extends FromEntityType<UnboxArray<Entity>> = FromEntityType<UnboxArray<Entity>>,
Hint extends string = never,
Fields extends string = '*',
Excludes extends string = never,
>(entities: Entity, populate: AutoPath<UnboxArray<Entity>, Hint, '*'>[] | false, options: EntityLoaderOptions<UnboxArray<Entity>, Fields, Excludes> = {}): Promise<Entity extends object[] ? MergeLoaded<ArrayElement<Entity>, Naked, Hint, Fields, Excludes>[] : MergeLoaded<Entity, Naked, Hint, Fields, Excludes>> {
>(entities: Entity, populate: AutoPath<Naked, Hint, '*'>[] | false, options: EntityLoaderOptions<Naked, Fields, Excludes> = {}): Promise<Entity extends object[] ? MergeLoaded<ArrayElement<Entity>, Naked, Hint, Fields, Excludes>[] : MergeLoaded<Entity, Naked, Hint, Fields, Excludes>> {
const arr = Utils.asArray(entities);

if (arr.length === 0) {
Expand Down
33 changes: 21 additions & 12 deletions packages/core/src/entity/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class EntityLoader {
*/
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)) {
return;
return this.setSerializationContext(entities, populate, options);
}

if ((entities as AnyEntity[]).some(e => !e.__helper)) {
Expand All @@ -77,25 +77,16 @@ export class EntityLoader {
options.refresh ??= false;
options.convertCustomTypes ??= true;
populate = this.normalizePopulate<Entity>(entityName, populate as true, options.strategy, options.lookup);
const exclude = options.exclude as string[] ?? [];
const invalid = populate.find(({ field }) => !this.em.canPopulate(entityName, field));

/* istanbul ignore next */
if (options.validate && invalid) {
throw ValidationError.invalidPropertyName(entityName, invalid.field);
}

for (const entity of entities) {
const context = helper(entity).__serializationContext;
context.populate = context.populate ? context.populate.concat(populate) : populate as PopulateOptions<Entity>[];

if (context.fields && options.fields) {
options.fields.forEach(f => context.fields!.add(f as string));
} else if (options.fields) {
context.fields = new Set(options.fields as string[]);
}
this.setSerializationContext(entities, populate, options);

context.exclude = context.exclude ? context.exclude.concat(exclude) : exclude;
for (const entity of entities) {
visited.add(entity);
}

Expand Down Expand Up @@ -128,6 +119,24 @@ export class EntityLoader {
return this.mergeNestedPopulate(normalized);
}

private setSerializationContext<Entity extends object, Fields extends string = '*'>(entities: Entity[], populate: PopulateOptions<Entity>[] | boolean, options: EntityLoaderOptions<Entity, Fields>): void {
const exclude = options.exclude as string[] ?? [];

for (const entity of entities) {
const context = helper(entity).__serializationContext;
context.populate = context.populate ? context.populate.concat(populate as any) : populate as PopulateOptions<Entity>[];
context.exclude = context.exclude ? context.exclude.concat(exclude) : exclude;

if (context.fields && options.fields) {
options.fields.forEach(f => context.fields!.add(f as string));
} else if (options.fields) {
context.fields = new Set(options.fields as string[]);
} else {
context.fields = new Set(['*']);
}
}
}

private expandDotPaths<Entity>(normalized: PopulateOptions<Entity>[], meta: EntityMetadata<any>) {
normalized.forEach(p => {
if (!p.field.includes('.')) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/entity/EntityRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class EntityRepository<Entity extends object> {
Naked extends FromEntityType<Entity> = FromEntityType<Entity>,
Fields extends string = '*',
Excludes extends string = never,
>(entities: Ent, populate: AutoPath<Entity, Hint, '*'>[] | false, options?: EntityLoaderOptions<Entity, Fields, Excludes>): Promise<Ent extends object[] ? MergeLoaded<ArrayElement<Ent>, Naked, Hint, Fields, Excludes>[] : MergeLoaded<Ent, Naked, Hint, Fields, Excludes>> {
>(entities: Ent, populate: AutoPath<Naked, Hint, '*'>[] | false, options?: EntityLoaderOptions<Naked, Fields, Excludes>): Promise<Ent extends object[] ? MergeLoaded<ArrayElement<Ent>, Naked, Hint, Fields, Excludes>[] : MergeLoaded<Ent, Naked, Hint, Fields, Excludes>> {
this.validateRepositoryType(entities, 'populate');
// @ts-ignore hard to type
return this.getEntityManager().populate(entities, populate, options);
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/schema/SqlSchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Knex } from 'knex';
import type { Knex } from 'knex';
import {
AbstractSchemaGenerator,
Utils,
Expand Down
36 changes: 28 additions & 8 deletions tests/features/serialization/GH5138.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Collection, Entity, ManyToMany, ManyToOne, MikroORM, PrimaryKey, wrap } from '@mikro-orm/sqlite';
import { Collection, Entity, ManyToMany, ManyToOne, MikroORM, PrimaryKey, Property, wrap } from '@mikro-orm/sqlite';

@Entity()
class School {

@PrimaryKey()
id!: number;

@Property()
name!: string;

}

@Entity()
Expand All @@ -14,6 +17,9 @@ class User {
@PrimaryKey()
id!: number;

@Property()
name!: string;

@ManyToMany({
entity: () => Role,
pivotEntity: () => UserRole,
Expand Down Expand Up @@ -55,26 +61,40 @@ beforeAll(async () => {
dbName: ':memory:',
ensureDatabase: { create: true },
});
});

afterAll(() => orm.close());

test('lazy em.populate on m:n', async () => {
const a = orm.em.create(User, {
orm.em.create(User, {
id: 1,
name: 'u',
roles: [{}, {}],
school: {},
school: { name: 's' },
});

await orm.em.flush();
orm.em.clear();
});

beforeEach(() => orm.em.clear());
afterAll(() => orm.close());

test('lazy em.populate on m:n', async () => {
const user = await orm.em.findOneOrFail(User, { id: 1 }, { populate: ['school'] });
await orm.em.populate(user, ['roles']);
const dto = wrap(user).toObject();
expect(dto).toEqual({
id: 1,
name: 'u',
roles: [{ id: 1 }, { id: 2 }],
school: { id: 1 },
school: { id: 1, name: 's' },
});
});

test('lazy em.populate with partial loading', async () => {
const user = await orm.em.findOneOrFail(User, { id: 1 }, {});
await orm.em.populate(user, ['school'], { fields: ['school.name'] });
const dto = wrap(user).toObject();
expect(dto).toEqual({
id: 1,
name: 'u',
school: { id: 1, name: 's' },
});
});

0 comments on commit 1c7b446

Please sign in to comment.