Skip to content

Commit

Permalink
refactor: add support for polymorphic embeddable arrays
Browse files Browse the repository at this point in the history
Related: #2426
Closes #2550
  • Loading branch information
B4nan committed Dec 18, 2021
1 parent 776b58f commit ea124d8
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 11 deletions.
4 changes: 4 additions & 0 deletions packages/core/src/entity/EntityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ export class EntityFactory {

private createEntity<T extends AnyEntity<T>>(data: EntityData<T>, meta: EntityMetadata<T>, options: FactoryOptions): T {
if (options.newEntity || meta.forceConstructor) {
if (!meta.class) {
throw new Error(`Cannot create entity ${meta.className}, class prototype is unknown`);
}

const params = this.extractConstructorParams<T>(meta, data, options);
const Entity = meta.class;
meta.constructorParams.forEach(prop => delete data[prop]);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/entity/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class EntityLoader {
populate = this.normalizePopulate<T>(entityName, populate, options.strategy, options.lookup);
const invalid = populate.find(({ field }) => !this.em.canPopulate(entityName, field));

/* istanbul ignore next */
if (options.validate && invalid) {
throw ValidationError.invalidPropertyName(entityName, invalid.field);
}
Expand Down
19 changes: 13 additions & 6 deletions packages/core/src/hydration/ObjectHydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,8 @@ export class ObjectHydrator extends Hydrator {
return ret;
};

const hydrateEmbedded = (prop: EntityProperty, path: string[], dataKey: string): string[] => {
const entityKey = path.map(k => this.wrap(k)).join('');
const registerEmbeddedPrototype = (prop: EntityProperty, path: string[]): void => {
const convertorKey = path.filter(k => !k.match(/\[idx_\d+]/)).map(k => this.safeKey(k)).join('_');
const ret: string[] = [];
const conds: string[] = [];

if (prop.targetMeta?.polymorphs) {
prop.targetMeta.polymorphs.forEach(meta => {
Expand All @@ -196,14 +193,24 @@ export class ObjectHydrator extends Hydrator {
} else {
context.set(`prototype_${convertorKey}`, prop.embeddable.prototype);
}
};

const parseObjectEmbeddable = (prop: EntityProperty, dataKey: string, ret: string[]): void => {
if (!this.platform.convertsJsonAutomatically() && (prop.object || prop.array)) {
ret.push(
` if (typeof data${dataKey} === 'string') {`,
` data${dataKey} = JSON.parse(data${dataKey});`,
` }`,
);
}
};

const hydrateEmbedded = (prop: EntityProperty, path: string[], dataKey: string): string[] => {
const entityKey = path.map(k => this.wrap(k)).join('');
const ret: string[] = [];
const conds: string[] = [];
registerEmbeddedPrototype(prop, path);
parseObjectEmbeddable(prop, dataKey, ret);

if (prop.object) {
conds.push(`data${dataKey} != null`);
Expand Down Expand Up @@ -243,11 +250,11 @@ export class ObjectHydrator extends Hydrator {

const hydrateEmbeddedArray = (prop: EntityProperty, path: string[], dataKey: string): string[] => {
const entityKey = path.map(k => this.wrap(k)).join('');
const convertorKey = path.filter(k => !k.match(/\[idx_\d+]/)).map(k => this.safeKey(k)).join('_');
const ret: string[] = [];
const idx = this.tmpIndex++;
registerEmbeddedPrototype(prop, path);
parseObjectEmbeddable(prop, dataKey, ret);

context.set(`prototype_${convertorKey}`, prop.embeddable.prototype);
ret.push(` if (Array.isArray(data${dataKey})) {`);
ret.push(` entity${entityKey} = [];`);
ret.push(` data${dataKey}.forEach((_, idx_${idx}) => {`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ exports[`polymorphic embeddables in sqlite diffing 1`] = `
if (data.pet2 && typeof data.pet2.name !== 'undefined') entity.pet2.name = data.pet2.name;
if (data.pet2 && typeof data.pet2.canMeow !== 'undefined') entity.pet2.canMeow = data.pet2.canMeow === null ? null : !!data.pet2.canMeow;
}
if (typeof data.pets === 'string') {
data.pets = JSON.parse(data.pets);
}
if (Array.isArray(data.pets)) {
entity.pets = [];
data.pets.forEach((_, idx_0) => {
if (typeof data.pets[idx_0] === 'string') {
data.pets[idx_0] = JSON.parse(data.pets[idx_0]);
}
if (data.pets[idx_0] != null) {
if (data.pets[idx_0].type == '1') {
entity.pets[idx_0] = factory.createEmbeddable('Dog', data.pets[idx_0], { newEntity, convertCustomTypes });
}
if (data.pets[idx_0].type == '0') {
entity.pets[idx_0] = factory.createEmbeddable('Cat', data.pets[idx_0], { newEntity, convertCustomTypes });
}
if (data.pets && data.pets[idx_0] && typeof data.pets[idx_0].canBark !== 'undefined') entity.pets[idx_0].canBark = data.pets[idx_0].canBark === null ? null : !!data.pets[idx_0].canBark;
if (data.pets && data.pets[idx_0] && typeof data.pets[idx_0].type !== 'undefined') entity.pets[idx_0].type = data.pets[idx_0].type;
if (data.pets && data.pets[idx_0] && typeof data.pets[idx_0].name !== 'undefined') entity.pets[idx_0].name = data.pets[idx_0].name;
if (data.pets && data.pets[idx_0] && typeof data.pets[idx_0].canMeow !== 'undefined') entity.pets[idx_0].canMeow = data.pets[idx_0].canMeow === null ? null : !!data.pets[idx_0].canMeow;
}
});
}
}"
`;

Expand Down Expand Up @@ -76,12 +99,27 @@ exports[`polymorphic embeddables in sqlite diffing 2`] = `
ret.pet2 = cloneEmbeddable(ret.pet2);
}
if (Array.isArray(entity.pets)) {
ret.pets = [];
entity.pets.forEach((_, idx_0) => {
if (entity.pets[idx_0] != null) {
ret.pets[idx_0] = {};
ret.pets[idx_0].canBark = clone(entity.pets[idx_0].canBark);
ret.pets[idx_0].type = clone(entity.pets[idx_0].type);
ret.pets[idx_0].name = clone(entity.pets[idx_0].name);
ret.pets[idx_0].canMeow = clone(entity.pets[idx_0].canMeow);
}
});
ret.pets = cloneEmbeddable(ret.pets);
}
return ret;
}"
`;

exports[`polymorphic embeddables in sqlite schema 1`] = `
"create table \`owner\` (\`id\` integer not null primary key autoincrement, \`name\` text not null, \`pet_can_bark\` integer null, \`pet_type\` integer not null, \`pet_name\` text not null, \`pet_can_meow\` integer null, \`pet2\` json not null);
"create table \`owner\` (\`id\` integer not null primary key autoincrement, \`name\` text not null, \`pet_can_bark\` integer null, \`pet_type\` integer not null, \`pet_name\` text not null, \`pet_can_meow\` integer null, \`pet2\` json not null, \`pets\` json not null);
create index \`owner_pet_type_index\` on \`owner\` (\`pet_type\`);
"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class Owner {
@Embedded(() => [Cat, Dog], { object: true })
pet2!: Cat | Dog;

@Embedded(() => [Cat, Dog], { array: true })
pets: (Cat | Dog)[] = [];

constructor(name: string) {
this.name = name;
}
Expand Down Expand Up @@ -105,6 +108,7 @@ describe('polymorphic embeddables in sqlite', () => {
const ent1 = new Owner('o1');
ent1.pet = new Dog('d1');
ent1.pet2 = new Cat('c3');
ent1.pets.push(ent1.pet, ent1.pet2);
expect(ent1.pet).toBeInstanceOf(Dog);
expect((ent1.pet as Dog).canBark).toBe(true);
expect(ent1.pet2).toBeInstanceOf(Cat);
Expand All @@ -113,11 +117,19 @@ describe('polymorphic embeddables in sqlite', () => {
name: 'o2',
pet: { type: AnimalType.CAT, name: 'c1' },
pet2: { type: AnimalType.DOG, name: 'd4' },
pets: [
{ type: AnimalType.CAT, name: 'cc1' },
{ type: AnimalType.DOG, name: 'dd4' },
],
});
expect(ent2.pet).toBeInstanceOf(Cat);
expect((ent2.pet as Cat).canMeow).toBe(true);
expect(ent2.pet2).toBeInstanceOf(Dog);
expect((ent2.pet2 as Dog).canBark).toBe(true);
expect(ent2.pets[0]).toBeInstanceOf(Cat);
expect((ent2.pets[0] as Cat).canMeow).toBe(true);
expect(ent2.pets[1]).toBeInstanceOf(Dog);
expect((ent2.pets[1] as Dog).canBark).toBe(true);
const ent3 = orm.em.create(Owner, {
name: 'o3',
pet: { type: AnimalType.DOG, name: 'd2' },
Expand All @@ -131,7 +143,7 @@ describe('polymorphic embeddables in sqlite', () => {
const mock = mockLogger(orm, ['query']);
await orm.em.persistAndFlush([ent1, ent2, ent3]);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('insert into `owner` (`name`, `pet_can_bark`, `pet_type`, `pet_name`, `pet_can_meow`, `pet2`) values (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)');
expect(mock.mock.calls[1][0]).toMatch('insert into `owner` (`name`, `pet_can_bark`, `pet_type`, `pet_name`, `pet_can_meow`, `pet2`, `pets`) values (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?)');
expect(mock.mock.calls[2][0]).toMatch('commit');
orm.em.clear();

Expand All @@ -143,27 +155,46 @@ describe('polymorphic embeddables in sqlite', () => {
expect(owners[0].pet.type).toBe(AnimalType.DOG);
expect((owners[0].pet as Cat).canMeow).toBeNull();
expect((owners[0].pet as Dog).canBark).toBe(true);
expect(owners[0].pets[0]).toBeInstanceOf(Dog);
expect(owners[0].pets[0].name).toBe('d1');
expect(owners[0].pets[0].type).toBe(AnimalType.DOG);
expect((owners[0].pets[0] as Cat).canMeow).toBeUndefined();
expect((owners[0].pets[0] as Dog).canBark).toBe(true);
expect(owners[0].pets[0]).not.toBe(owners[0].pet); // no identity map for embeddables as they don't have PKs
expect(owners[0].pets[1]).not.toBe(owners[0].pet2); // no identity map for embeddables as they don't have PKs
expect(owners[0].pets).toMatchObject([
{ canBark: true, name: 'd1', type: 1 },
{ canMeow: true, name: 'c3', type: 0 },
]);
expect(owners[1].pet).toBeInstanceOf(Cat);
expect(owners[1].pet.name).toBe('c1');
expect(owners[1].pet.type).toBe(AnimalType.CAT);
expect((owners[1].pet as Cat).canMeow).toBe(true);
expect((owners[1].pet as Dog).canBark).toBeNull();
expect(owners[1].pets).toMatchObject([
{ canMeow: true, name: 'cc1', type: 0 },
{ canBark: true, name: 'dd4', type: 1 },
]);
expect(owners[2].pet).toBeInstanceOf(Dog);
expect(owners[2].pet.name).toBe('d2');
expect(owners[2].pet.type).toBe(AnimalType.DOG);
expect(owners[2].pets).toEqual([]);

expect(mock.mock.calls).toHaveLength(4);
await orm.em.flush();
expect(mock.mock.calls).toHaveLength(4);

owners[0].pet = new Cat('c2');
owners[0].pets[0].name = 'new dog name';
owners[0].pets[1].name = 'new cat name';
owners[0].pets.push(new Dog('added dog'));
owners[1].pet = new Dog('d3');
owners[2].pet.name = 'old dog';
mock.mock.calls.length = 0;
await orm.em.flush();
expect(mock.mock.calls).toHaveLength(3);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('update `owner` set `pet_can_bark` = case when (`id` = ?) then ? when (`id` = ?) then ? else `pet_can_bark` end, `pet_type` = case when (`id` = ?) then ? when (`id` = ?) then ? else `pet_type` end, `pet_name` = case when (`id` = ?) then ? when (`id` = ?) then ? when (`id` = ?) then ? else `pet_name` end, `pet_can_meow` = case when (`id` = ?) then ? when (`id` = ?) then ? else `pet_can_meow` end where `id` in (?, ?, ?)');
expect(mock.mock.calls[1][0]).toMatch('update `owner` set `pet_can_bark` = case when (`id` = ?) then ? when (`id` = ?) then ? else `pet_can_bark` end, `pet_type` = case when (`id` = ?) then ? when (`id` = ?) then ? else `pet_type` end, `pet_name` = case when (`id` = ?) then ? when (`id` = ?) then ? when (`id` = ?) then ? else `pet_name` end, `pet_can_meow` = case when (`id` = ?) then ? when (`id` = ?) then ? else `pet_can_meow` end, `pets` = case when (`id` = ?) then ? else `pets` end where `id` in (?, ?, ?)');
expect(mock.mock.calls[2][0]).toMatch('commit');
orm.em.clear();

Expand All @@ -174,6 +205,12 @@ describe('polymorphic embeddables in sqlite', () => {
expect(owners2[0].pet).toBeInstanceOf(Cat);
expect(owners2[0].pet.name).toBe('c2');
expect(owners2[0].pet.type).toBe(AnimalType.CAT);
expect(owners2[0].pets[0].name).toBe('new dog name');
expect(owners2[0].pets[1].name).toBe('new cat name');
expect(owners2[0].pets[2].name).toBe('added dog');
expect((owners2[0].pets[0] as Dog).canBark).toBe(true);
expect((owners2[0].pets[1] as Cat).canMeow).toBe(true);
expect((owners2[0].pets[2] as Dog).canBark).toBe(true);
expect((owners2[0].pet as Dog).canBark).toBeNull();
expect((owners2[0].pet as Cat).canMeow).toBe(true);

Expand Down Expand Up @@ -223,25 +260,41 @@ describe('polymorphic embeddables in sqlite', () => {
orm.em.assign(owner, {
pet: { name: 'cat', type: AnimalType.CAT },
pet2: { name: 'dog', type: AnimalType.DOG },
pets: [
{ name: 'dog in array', type: AnimalType.DOG },
{ name: 'cat in array', type: AnimalType.CAT },
],
});
expect(owner.pet).toMatchObject({ name: 'cat', type: AnimalType.CAT });
expect(owner.pet).toBeInstanceOf(Cat);
expect(owner.pet2).toMatchObject({ name: 'dog', type: AnimalType.DOG });
expect(owner.pet2).toBeInstanceOf(Dog);
expect(owner.pets[0]).toMatchObject({ name: 'dog in array', type: AnimalType.DOG });
expect(owner.pets[0]).toBeInstanceOf(Dog);
expect(owner.pets[1]).toMatchObject({ name: 'cat in array', type: AnimalType.CAT });
expect(owner.pets[1]).toBeInstanceOf(Cat);

const mock = mockLogger(orm, ['query']);
await orm.em.persistAndFlush(owner);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('insert into `owner` (`name`, `pet2`, `pet_can_bark`, `pet_can_meow`, `pet_name`, `pet_type`) values (?, ?, ?, ?, ?, ?)');
expect(mock.mock.calls[1][0]).toMatch('insert into `owner` (`name`, `pet2`, `pet_can_bark`, `pet_can_meow`, `pet_name`, `pet_type`, `pets`) values (?, ?, ?, ?, ?, ?, ?)');
expect(mock.mock.calls[2][0]).toMatch('commit');

orm.em.assign(owner, {
pet: { name: 'cat name' },
pet2: { name: 'dog name' },
});
expect(() => orm.em.assign(owner, { pets: [{ name: '...' } ] })).toThrow('Cannot create entity Cat | Dog, class prototype is unknown');
orm.em.assign(owner, {
pets: [
{ name: 'cat in array', type: AnimalType.CAT },
{ name: 'dog in array', type: AnimalType.DOG },
{ name: 'cat in array 2', type: AnimalType.CAT },
],
});
await orm.em.persistAndFlush(owner);
expect(mock.mock.calls[3][0]).toMatch('begin');
expect(mock.mock.calls[4][0]).toMatch('update `owner` set `pet_name` = ?, `pet2` = ? where `id` = ?');
expect(mock.mock.calls[4][0]).toMatch('update `owner` set `pet_name` = ?, `pet2` = ?, `pets` = ? where `id` = ?');
expect(mock.mock.calls[5][0]).toMatch('commit');

expect(wrap(owner).toObject()).toEqual({
Expand All @@ -257,6 +310,11 @@ describe('polymorphic embeddables in sqlite', () => {
name: 'dog name',
type: 1,
},
pets: [
{ canMeow: true, name: 'cat in array', type: 0 },
{ canBark: true, name: 'dog in array', type: 1 },
{ canMeow: true, name: 'cat in array 2', type: 0 },
],
});
});

Expand Down

0 comments on commit ea124d8

Please sign in to comment.