Skip to content

Commit

Permalink
fix(core): support nested embeddables inside embedded arrays
Browse files Browse the repository at this point in the history
Closes #1585
  • Loading branch information
B4nan committed Mar 23, 2021
1 parent 9dde3c7 commit 088c65d
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 36 deletions.
13 changes: 8 additions & 5 deletions packages/core/src/hydration/ObjectHydrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class ObjectHydrator extends Hydrator {
returning: new Map<string, EntityHydrator<any>>(),
};

private tmpIndex = 0;

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -72,7 +74,7 @@ export class ObjectHydrator extends Hydrator {
const hydrateScalar = <T, U>(prop: EntityProperty<T>, object: boolean | undefined, path: string[], dataKey: string): string[] => {
const entityKey = path.join('.');
const preCond = preCondition(dataKey);
const convertorKey = path.join('_').replace(/\[idx]/, '');
const convertorKey = path.join('_').replace(/\[idx_[\d+]]/g, '');
const ret: string[] = [];

if (prop.type.toLowerCase() === 'date') {
Expand Down Expand Up @@ -179,7 +181,7 @@ export class ObjectHydrator extends Hydrator {

const hydrateEmbedded = (prop: EntityProperty, object: boolean | undefined, path: string[], dataKey: string): string[] => {
const entityKey = path.join('.');
const convertorKey = path.join('_').replace(/\[idx]/, '');
const convertorKey = path.join('_').replace(/\[idx_[\d+]]/g, '');
const ret: string[] = [];
const conds: string[] = [];
context.set(`prototype_${convertorKey}`, prop.embeddable.prototype);
Expand Down Expand Up @@ -208,15 +210,16 @@ export class ObjectHydrator extends Hydrator {

const hydrateEmbeddedArray = (prop: EntityProperty, path: string[], dataKey: string): string[] => {
const entityKey = path.join('.');
const convertorKey = path.join('_').replace(/\[idx]/, '');
const convertorKey = path.join('_').replace(/\[idx_[\d+]]/g, '');
const ret: string[] = [];
const idx = this.tmpIndex++;

context.set(`prototype_${convertorKey}`, prop.embeddable.prototype);
ret.push(` if (Array.isArray(data.${dataKey})) {`);
ret.push(` entity.${entityKey} = [];`);
ret.push(` data.${dataKey}.forEach((_, idx) => {`);
ret.push(` data.${dataKey}.forEach((_, idx_${idx}) => {`);
const last = path.pop();
ret.push(...hydrateEmbedded(prop, true, [...path, last + '[idx]'], dataKey + '[idx]').map(l => ' ' + l));
ret.push(...hydrateEmbedded(prop, true, [...path, `${last}[idx_${idx}]`], `${dataKey}[idx_${idx}]`).map(l => ' ' + l));
ret.push(` });`);
ret.push(` }`);

Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/utils/EntityComparator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class EntityComparator {
private readonly snapshotGenerators = new Map<string, SnapshotGenerator<any>>();
private readonly pkGetters = new Map<string, PkGetter<any>>();
private readonly pkSerializers = new Map<string, PkSerializer<any>>();
private tmpIndex = 0;

constructor(private readonly metadata: IMetadataStorage,
private readonly platform: Platform) { }
Expand Down Expand Up @@ -227,12 +228,13 @@ export class EntityComparator {
dataKey = dataKey ?? entityKey;
const ret: string[] = [];
const padding = ' '.repeat(level * 2);
const idx = this.tmpIndex++;

ret.push(`${padding}if (Array.isArray(entity.${entityKey})) {`);
ret.push(`${padding} ret.${dataKey} = [];`);
ret.push(`${padding} entity.${entityKey}.forEach((_, idx) => {`);
ret.push(`${padding} entity.${entityKey}.forEach((_, idx_${idx}) => {`);
const last = path.pop();
ret.push(this.getEmbeddedPropertySnapshot(meta, prop, context, level + 2, [...path, last + '[idx]'], dataKey + '[idx]', true));
ret.push(this.getEmbeddedPropertySnapshot(meta, prop, context, level + 2, [...path, `${last}[idx_${idx}]`], `${dataKey}[idx_${idx}]`, true));
ret.push(`${padding} });`);

if (this.shouldSerialize(prop, dataKey)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,29 @@ exports[`embedded entities in postgres diffing 1`] = `
if (Array.isArray(entity.profile1.identity.links)) {
ret.profile1_identity_links = [];
entity.profile1.identity.links.forEach((_, idx) => {
entity.profile1.identity.links.forEach((_, idx_0) => {
if (entity.profile1.identity.links[idx] != null) {
ret.profile1_identity_links[idx] = {};
ret.profile1_identity_links[idx].url = clone(entity.profile1.identity.links[idx].url);
ret.profile1_identity_links[idx].createdAt = clone(entity.profile1.identity.links[idx].createdAt);
if (entity.profile1.identity.links[idx_0] != null) {
ret.profile1_identity_links[idx_0] = {};
ret.profile1_identity_links[idx_0].url = clone(entity.profile1.identity.links[idx_0].url);
ret.profile1_identity_links[idx_0].createdAt = clone(entity.profile1.identity.links[idx_0].createdAt);
if (entity.profile1.identity.links[idx].meta != null) {
ret.profile1_identity_links[idx].meta = {};
ret.profile1_identity_links[idx].meta.foo = clone(entity.profile1.identity.links[idx].meta.foo);
ret.profile1_identity_links[idx].meta.bar = clone(entity.profile1.identity.links[idx].meta.bar);
if (entity.profile1.identity.links[idx_0].meta != null) {
ret.profile1_identity_links[idx_0].meta = {};
ret.profile1_identity_links[idx_0].meta.foo = clone(entity.profile1.identity.links[idx_0].meta.foo);
ret.profile1_identity_links[idx_0].meta.bar = clone(entity.profile1.identity.links[idx_0].meta.bar);
}
if (Array.isArray(entity.profile1.identity.links[idx_0].metas)) {
ret.profile1_identity_links[idx_0].metas = [];
entity.profile1.identity.links[idx_0].metas.forEach((_, idx_1) => {
if (entity.profile1.identity.links[idx_0].metas[idx_1] != null) {
ret.profile1_identity_links[idx_0].metas[idx_1] = {};
ret.profile1_identity_links[idx_0].metas[idx_1].foo = clone(entity.profile1.identity.links[idx_0].metas[idx_1].foo);
ret.profile1_identity_links[idx_0].metas[idx_1].bar = clone(entity.profile1.identity.links[idx_0].metas[idx_1].bar);
}
});
}
}
Expand Down Expand Up @@ -62,17 +74,29 @@ exports[`embedded entities in postgres diffing 1`] = `
if (Array.isArray(entity.profile2.identity.links)) {
ret.profile2.identity.links = [];
entity.profile2.identity.links.forEach((_, idx) => {
entity.profile2.identity.links.forEach((_, idx_2) => {
if (entity.profile2.identity.links[idx_2] != null) {
ret.profile2.identity.links[idx_2] = {};
ret.profile2.identity.links[idx_2].url = clone(entity.profile2.identity.links[idx_2].url);
ret.profile2.identity.links[idx_2].createdAt = clone(entity.profile2.identity.links[idx_2].createdAt);
if (entity.profile2.identity.links[idx_2].meta != null) {
ret.profile2.identity.links[idx_2].meta = {};
ret.profile2.identity.links[idx_2].meta.foo = clone(entity.profile2.identity.links[idx_2].meta.foo);
ret.profile2.identity.links[idx_2].meta.bar = clone(entity.profile2.identity.links[idx_2].meta.bar);
}
if (entity.profile2.identity.links[idx] != null) {
ret.profile2.identity.links[idx] = {};
ret.profile2.identity.links[idx].url = clone(entity.profile2.identity.links[idx].url);
ret.profile2.identity.links[idx].createdAt = clone(entity.profile2.identity.links[idx].createdAt);
if (Array.isArray(entity.profile2.identity.links[idx_2].metas)) {
ret.profile2.identity.links[idx_2].metas = [];
entity.profile2.identity.links[idx_2].metas.forEach((_, idx_3) => {
if (entity.profile2.identity.links[idx].meta != null) {
ret.profile2.identity.links[idx].meta = {};
ret.profile2.identity.links[idx].meta.foo = clone(entity.profile2.identity.links[idx].meta.foo);
ret.profile2.identity.links[idx].meta.bar = clone(entity.profile2.identity.links[idx].meta.bar);
if (entity.profile2.identity.links[idx_2].metas[idx_3] != null) {
ret.profile2.identity.links[idx_2].metas[idx_3] = {};
ret.profile2.identity.links[idx_2].metas[idx_3].foo = clone(entity.profile2.identity.links[idx_2].metas[idx_3].foo);
ret.profile2.identity.links[idx_2].metas[idx_3].bar = clone(entity.profile2.identity.links[idx_2].metas[idx_3].bar);
}
});
}
}
Expand Down
38 changes: 27 additions & 11 deletions tests/features/embeddables/nested-embeddables.postgres.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ class IdentityLink {
@Embedded(() => IdentityMeta)
meta?: IdentityMeta;

// @Embedded(() => IdentityMeta, { array: true })
// metas: IdentityMeta[] = [];
@Embedded(() => IdentityMeta, { array: true })
metas: IdentityMeta[] = [];

constructor(url: string) {
this.url = url;
this.meta = new IdentityMeta('f1', 'b1');
// this.metas.push(new IdentityMeta('f2', 'b2'));
// this.metas.push(new IdentityMeta('f3', 'b3'));
// this.metas.push(new IdentityMeta('f4', 'b4'));
this.metas.push(new IdentityMeta('f2', 'b2'));
this.metas.push(new IdentityMeta('f3', 'b3'));
this.metas.push(new IdentityMeta('f4', 'b4'));
}

}
Expand Down Expand Up @@ -142,7 +142,7 @@ describe('embedded entities in postgres', () => {
await orm.em.persistAndFlush([user1, user2]);
orm.em.clear();
expect(mock.mock.calls[0][0]).toMatch(`begin`);
expect(mock.mock.calls[1][0]).toMatch(`insert into "user" ("name", "profile1_username", "profile1_identity_email", "profile1_identity_meta_foo", "profile1_identity_meta_bar", "profile1_identity_links", "profile2") values ('Uwe', 'u1', 'e1', 'f1', 'b1', '[]', '{"username":"u2","identity":{"email":"e2","meta":{"foo":"f2","bar":"b2"},"links":[]}}'), ('Uschi', 'u3', 'e3', NULL, NULL, '[{"url":"l1","meta":{"foo":"f1","bar":"b1"}},{"url":"l2","meta":{"foo":"f1","bar":"b1"}}]', '{"username":"u4","identity":{"email":"e4","meta":{"foo":"f4"},"links":[{"url":"l3","meta":{"foo":"f1","bar":"b1"}},{"url":"l4","meta":{"foo":"f1","bar":"b1"}}]}}') returning "id"`);
expect(mock.mock.calls[1][0]).toMatch(`insert into "user" ("name", "profile1_username", "profile1_identity_email", "profile1_identity_meta_foo", "profile1_identity_meta_bar", "profile1_identity_links", "profile2") values ('Uwe', 'u1', 'e1', 'f1', 'b1', '[]', '{"username":"u2","identity":{"email":"e2","meta":{"foo":"f2","bar":"b2"},"links":[]}}'), ('Uschi', 'u3', 'e3', NULL, NULL, '[{"url":"l1","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]},{"url":"l2","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]}]', '{"username":"u4","identity":{"email":"e4","meta":{"foo":"f4"},"links":[{"url":"l3","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]},{"url":"l4","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]}]}}') returning "id"`);
expect(mock.mock.calls[2][0]).toMatch(`commit`);

const u1 = await orm.em.findOneOrFail(User, user1.id);
Expand Down Expand Up @@ -184,8 +184,16 @@ describe('embedded entities in postgres', () => {
identity: {
email: 'e3',
links: [
{ url: 'l1', meta: { bar: 'b1', foo: 'f1' } },
{ url: 'l2', meta: { bar: 'b1', foo: 'f1' } },
{ url: 'l1', meta: { bar: 'b1', foo: 'f1' }, metas: [
{ bar: 'b2', foo: 'f2' },
{ bar: 'b3', foo: 'f3' },
{ bar: 'b4', foo: 'f4' },
] },
{ url: 'l2', meta: { bar: 'b1', foo: 'f1' }, metas: [
{ bar: 'b2', foo: 'f2' },
{ bar: 'b3', foo: 'f3' },
{ bar: 'b4', foo: 'f4' },
] },
],
},
});
Expand All @@ -198,8 +206,16 @@ describe('embedded entities in postgres', () => {
identity: {
email: 'e4',
links: [
{ url: 'l3', meta: { bar: 'b1', foo: 'f1' } },
{ url: 'l4', meta: { bar: 'b1', foo: 'f1' } },
{ url: 'l3', meta: { bar: 'b1', foo: 'f1' }, metas: [
{ bar: 'b2', foo: 'f2' },
{ bar: 'b3', foo: 'f3' },
{ bar: 'b4', foo: 'f4' },
] },
{ url: 'l4', meta: { bar: 'b1', foo: 'f1' }, metas: [
{ bar: 'b2', foo: 'f2' },
{ bar: 'b3', foo: 'f3' },
{ bar: 'b4', foo: 'f4' },
] },
],
meta: {
foo: 'f4',
Expand All @@ -218,7 +234,7 @@ describe('embedded entities in postgres', () => {
u2.profile1!.identity.links = [new IdentityLink('l6'), new IdentityLink('l7')];
u2.profile2!.identity.links.push(new IdentityLink('l8'));
await orm.em.flush();
expect(mock.mock.calls[6][0]).toMatch(`update "user" set "profile1_identity_email" = case when ("id" = 1) then 'e123' else "profile1_identity_email" end, "profile1_identity_meta_foo" = case when ("id" = 1) then 'foooooooo' else "profile1_identity_meta_foo" end, "profile2" = case when ("id" = 1) then '{"username":"u2","identity":{"email":"e2","meta":{"foo":"f2","bar":"bababar"},"links":[{"url":"l5","meta":{"foo":"f1","bar":"b1"}}]}}' when ("id" = 2) then '{"username":"u4","identity":{"email":"e4","meta":{"foo":"f4"},"links":[{"url":"l3","meta":{"foo":"f1","bar":"b1"}},{"url":"l4","meta":{"foo":"f1","bar":"b1"}},{"url":"l8","meta":{"foo":"f1","bar":"b1"}}]}}' else "profile2" end, "profile1_identity_links" = case when ("id" = 2) then '[{"url":"l6","meta":{"foo":"f1","bar":"b1"}},{"url":"l7","meta":{"foo":"f1","bar":"b1"}}]' else "profile1_identity_links" end where "id" in (1, 2)`);
expect(mock.mock.calls[6][0]).toMatch(`update "user" set "profile1_identity_email" = case when ("id" = 1) then 'e123' else "profile1_identity_email" end, "profile1_identity_meta_foo" = case when ("id" = 1) then 'foooooooo' else "profile1_identity_meta_foo" end, "profile2" = case when ("id" = 1) then '{"username":"u2","identity":{"email":"e2","meta":{"foo":"f2","bar":"bababar"},"links":[{"url":"l5","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]}]}}' when ("id" = 2) then '{"username":"u4","identity":{"email":"e4","meta":{"foo":"f4"},"links":[{"url":"l3","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]},{"url":"l4","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]},{"url":"l8","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]}]}}' else "profile2" end, "profile1_identity_links" = case when ("id" = 2) then '[{"url":"l6","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]},{"url":"l7","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]}]' else "profile1_identity_links" end where "id" in (1, 2)`);
orm.em.clear();
mock.mock.calls.length = 0;

Expand Down

0 comments on commit 088c65d

Please sign in to comment.