UNPKG

undeexcepturi

Version:

TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.

459 lines (389 loc) 20.1 kB
import type { ObjectHydrator } from '@mikro-orm/core'; import { Embeddable, Embedded, Entity, ManyToOne, MikroORM, PrimaryKey, Property, wrap } from '@mikro-orm/core'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; import { mockLogger } from '../../helpers'; @Entity() class Source { @PrimaryKey() id!: number; @Property() name: string; constructor(name: string) { this.name = name; } } @Embeddable() class IdentityMeta { @Property() foo?: string; @Property() bar?: string; @ManyToOne(() => Source, { nullable: true }) source?: Source; constructor(foo?: string, bar?: string) { this.foo = foo; this.bar = bar; } } @Embeddable() class IdentityLink { @Property({ nullable: true }) url?: string; @Property({ nullable: true }) createdAt?: Date; @Embedded(() => IdentityMeta) meta?: IdentityMeta; @Embedded(() => IdentityMeta, { array: true }) metas: IdentityMeta[] = []; @ManyToOne(() => Source, { nullable: true }) source?: Source; 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')); } } @Embeddable() class Identity { @Property() email: string; @Embedded(() => IdentityMeta, { nullable: true }) meta?: IdentityMeta; @Embedded(() => IdentityLink, { array: true }) links: IdentityLink[] = []; @ManyToOne(() => Source, { nullable: true }) source?: Source; constructor(email: string, meta?: IdentityMeta) { this.email = email; this.meta = meta; } } @Embeddable() class Profile { @Property() username: string; @Embedded(() => Identity) identity: Identity; @ManyToOne(() => Source, { nullable: true }) source?: Source; constructor(username: string, identity: Identity) { this.username = username; this.identity = identity; } } @Entity() class User { @PrimaryKey() id!: number; @Property() name!: string; @Embedded(() => Profile) profile1!: Profile; @Embedded(() => Profile, { object: true }) profile2!: Profile; } describe('embedded entities in postgres', () => { let orm: MikroORM<PostgreSqlDriver>; beforeAll(async () => { orm = await MikroORM.init({ entities: [User], driver: PostgreSqlDriver, dbName: `mikro_orm_test_entities_in_embedddables`, }); await orm.schema.refreshDatabase(); }); beforeEach(async () => { const tables = Object.values(orm.getMetadata().getAll()) .filter(meta => !meta.embeddable) .map(meta => meta.tableName); for (const table of tables) { await orm.em.createQueryBuilder(table).truncate().execute(); } }); afterAll(async () => { await orm.close(true); }); test('schema', async () => { await expect(orm.schema.getCreateSchemaSQL({ wrap: false })).resolves.toMatchSnapshot('nested embeddables 1'); await expect(orm.schema.getUpdateSchemaSQL({ wrap: false })).resolves.toMatchSnapshot('nested embeddables 2'); await expect(orm.schema.getDropSchemaSQL({ wrap: false })).resolves.toMatchSnapshot('nested embeddables 3'); }); test('diffing', async () => { expect(orm.em.getComparator().getSnapshotGenerator('User').toString()).toMatchSnapshot(); const metadata = orm.getMetadata(); const hydrator = orm.config.getHydrator(metadata) as ObjectHydrator; expect(hydrator.getEntityHydrator(metadata.get('User'), 'full').toString()).toMatchSnapshot(); }); async function createUsers() { const user1 = new User(); user1.name = 'Uwe'; user1.profile1 = new Profile('u1', new Identity('e1', new IdentityMeta('f1', 'b1'))); user1.profile1.source = new Source('s1'); user1.profile1.identity.source = new Source('is1'); user1.profile1.identity.meta!.source = new Source('ims1'); user1.profile2 = new Profile('u2', new Identity('e2', new IdentityMeta('f2', 'b2'))); user1.profile2.source = new Source('s2'); user1.profile2.identity.source = new Source('is2'); user1.profile2.identity.meta!.source = new Source('ims2'); const user2 = new User(); user2.name = 'Uschi'; user2.profile1 = new Profile('u3', new Identity('e3')); user2.profile1.source = new Source('s3'); user2.profile1.identity.links.push(new IdentityLink('l1'), new IdentityLink('l2')); user2.profile1.identity.source = new Source('is3'); user2.profile1.identity.links[0].source = new Source('ils31'); user2.profile1.identity.links[1].source = new Source('ils32'); user2.profile1.identity.links[0].metas[0].source = new Source('ilms311'); user2.profile1.identity.links[0].metas[1].source = new Source('ilms312'); user2.profile1.identity.links[0].metas[2].source = new Source('ilms313'); user2.profile1.identity.links[1].metas[0].source = new Source('ilms321'); user2.profile1.identity.links[1].metas[1].source = new Source('ilms322'); user2.profile1.identity.links[1].metas[2].source = new Source('ilms323'); user2.profile2 = new Profile('u4', new Identity('e4', new IdentityMeta('f4'))); user2.profile2.source = new Source('s4'); user2.profile2.identity.links.push(new IdentityLink('l3'), new IdentityLink('l4')); user2.profile2.identity.source = new Source('is4'); user2.profile2.identity.links[0].source = new Source('ils41'); user2.profile2.identity.links[1].source = new Source('ils42'); await orm.em.persistAndFlush([user1, user2]); orm.em.clear(); return { user1, user2 }; } test('persist and load', async () => { const mock = mockLogger(orm); const { user1, user2 } = await createUsers(); expect(mock.mock.calls[0][0]).toMatch(`begin`); expect(mock.mock.calls[1][0]).toMatch(`insert into "source" ("name") values ('s1'), ('is1'), ('ims1'), ('s2'), ('is2'), ('ims2'), ('s3'), ('is3'), ('ils31'), ('ils32'), ('ilms311'), ('ilms312'), ('ilms313'), ('ilms321'), ('ilms322'), ('ilms323'), ('s4'), ('is4'), ('ils41'), ('ils42') returning "id"`); // expect(mock.mock.calls[2][0]).toMatch(`insert into "user" ("name", "profile1_username", "profile1_identity_email", "profile1_identity_meta_foo", "profile1_identity_meta_bar", "profile1_identity_meta_source_id", "profile1_identity_links", "profile1_identity_source_id", "profile1_source_id", "profile2") values ('Uwe', 'u1', 'e1', 'f1', 'b1', 3, '[]', 2, 1, '{"username":"u2","identity":{"email":"e2","meta":{"foo":"f2","bar":"b2","source_id":6},"links":[],"source_id":5},"source_id":4}'), ('Uschi', 'u3', 'e3', NULL, NULL, NULL, '[{"url":"l1","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2","source_id":11},{"foo":"f3","bar":"b3","source_id":12},{"foo":"f4","bar":"b4","source_id":13}],"source_id":9},{"url":"l2","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2","source_id":14},{"foo":"f3","bar":"b3","source_id":15},{"foo":"f4","bar":"b4","source_id":16}],"source_id":10}]', 8, 7, '{"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"}],"source_id":19},{"url":"l4","meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}],"source_id":20}],"source_id":18},"source_id":17}') returning "id"`); expect(mock.mock.calls[3][0]).toMatch(`commit`); const u1 = await orm.em.findOneOrFail(User, user1.id); const u2 = await orm.em.findOneOrFail(User, user2.id); expect(mock.mock.calls[4][0]).toMatch(`select "u0".* from "user" as "u0" where "u0"."id" = 1 limit 1`); expect(u1.profile1).toBeInstanceOf(Profile); expect(u1.profile1.identity).toBeInstanceOf(Identity); expect(u1.profile1.identity.meta).toBeInstanceOf(IdentityMeta); expect(u1.profile1.source).toBeInstanceOf(Source); expect(u1.profile1.identity.source).toBeInstanceOf(Source); expect(wrap(u1.profile1.identity.source!).isInitialized()).toBe(false); expect(u1.profile1).toMatchObject({ username: 'u1', identity: { email: 'e1', links: [], meta: { bar: 'b1', foo: 'f1', source: { id: u1.profile1.identity.meta!.source!.id }, }, }, source: { id: u1.profile1.source!.id }, }); expect(u1.profile2).toBeInstanceOf(Profile); expect(u1.profile2.identity).toBeInstanceOf(Identity); expect(u1.profile2.source).toBeInstanceOf(Source); expect(u1.profile2.identity.meta).toBeInstanceOf(IdentityMeta); expect(u1.profile2.identity.source).toBeInstanceOf(Source); expect(u1.profile2).toMatchObject({ username: 'u2', identity: { email: 'e2', links: [], meta: { bar: 'b2', foo: 'f2', source: { id: u1.profile2.identity.meta!.source!.id }, }, source: { id: u1.profile2.identity.source!.id }, }, source: { id: u1.profile2.source!.id }, }); expect(u2.profile1).toBeInstanceOf(Profile); expect(u2.profile1.identity).toBeInstanceOf(Identity); expect(u2.profile1.identity.links[0]).toBeInstanceOf(IdentityLink); expect(u2.profile1.source).toBeInstanceOf(Source); expect(u2.profile1.identity.source).toBeInstanceOf(Source); expect(u2.profile1.identity.links[0].source).toBeInstanceOf(Source); expect(u2.profile1.identity.links[1].source).toBeInstanceOf(Source); expect(u2.profile1.identity.links[1].metas[0].source).toBeInstanceOf(Source); expect(u2.profile1).toMatchObject({ username: 'u3', identity: { email: 'e3', links: [ { url: 'l1', meta: { bar: 'b1', foo: 'f1' }, source: { id: u2.profile1.identity.links[0].source!.id }, metas: [ { bar: 'b2', foo: 'f2', source: { id: u2.profile1.identity.links[0].metas[0].source!.id } }, { bar: 'b3', foo: 'f3', source: { id: u2.profile1.identity.links[0].metas[1].source!.id } }, { bar: 'b4', foo: 'f4', source: { id: u2.profile1.identity.links[0].metas[2].source!.id } }, ], }, { url: 'l2', meta: { bar: 'b1', foo: 'f1' }, source: { id: u2.profile1.identity.links[1].source!.id }, metas: [ { bar: 'b2', foo: 'f2', source: { id: u2.profile1.identity.links[1].metas[0].source!.id } }, { bar: 'b3', foo: 'f3', source: { id: u2.profile1.identity.links[1].metas[1].source!.id } }, { bar: 'b4', foo: 'f4', source: { id: u2.profile1.identity.links[1].metas[2].source!.id } }, ], }, ], source: { id: u2.profile1.identity.source!.id }, }, source: { id: u2.profile1.source!.id }, }); expect(u2.profile2).toBeInstanceOf(Profile); expect(u2.profile2.identity).toBeInstanceOf(Identity); expect(u2.profile2.identity.links[0]).toBeInstanceOf(IdentityLink); expect(u2.profile2.identity.meta).toBeInstanceOf(IdentityMeta); expect(u2.profile2.source).toBeInstanceOf(Source); expect(u2.profile2.identity.source).toBeInstanceOf(Source); expect(u2.profile2.identity.links[0].source).toBeInstanceOf(Source); expect(u2.profile2.identity.links[1].source).toBeInstanceOf(Source); expect(u2.profile2).toMatchObject({ username: 'u4', identity: { email: 'e4', links: [ { url: 'l3', meta: { bar: 'b1', foo: 'f1' }, source: { id: u2.profile2.identity.links[0].source!.id }, metas: [ { bar: 'b2', foo: 'f2' }, { bar: 'b3', foo: 'f3' }, { bar: 'b4', foo: 'f4' }, ], }, { url: 'l4', meta: { bar: 'b1', foo: 'f1' }, source: { id: u2.profile2.identity.links[1].source!.id }, metas: [ { bar: 'b2', foo: 'f2' }, { bar: 'b3', foo: 'f3' }, { bar: 'b4', foo: 'f4' }, ], }, ], meta: { foo: 'f4', }, source: { id: u2.profile2.identity.source!.id }, }, source: { id: u2.profile2.source!.id }, }); expect(mock.mock.calls.length).toBe(6); await orm.em.flush(); expect(mock.mock.calls.length).toBe(6); u1.profile1!.identity.email = 'e123'; u1.profile1!.identity.meta!.foo = 'foooooooo'; u1.profile2!.identity.meta!.bar = 'bababar'; u1.profile2!.identity.links.push(new IdentityLink('l5')); 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[7][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","source_id":4,"identity":{"email":"e2","source_id":5,"meta":{"foo":"f2","bar":"bababar","source_id":6},"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","source_id":17,"identity":{"email":"e4","source_id":18,"meta":{"foo":"f4"},"links":[{"url":"l3","source_id":19,"meta":{"foo":"f1","bar":"b1"},"metas":[{"foo":"f2","bar":"b2"},{"foo":"f3","bar":"b3"},{"foo":"f4","bar":"b4"}]},{"url":"l4","source_id":20,"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; const u3 = await orm.em.findOneOrFail(User, { profile1: { identity: { email: 'e123', meta: { foo: 'foooooooo' } } }, profile2: { identity: { email: 'e2', meta: { foo: 'f2', bar: 'bababar' } } }, }); expect(mock.mock.calls[0][0]).toMatch(`select "u0".* from "user" as "u0" where "u0"."profile1_identity_email" = 'e123' and "u0"."profile1_identity_meta_foo" = 'foooooooo' and "u0"."profile2"->'identity'->>'email' = 'e2' and "u0"."profile2"->'identity'->'meta'->>'foo' = 'f2' and "u0"."profile2"->'identity'->'meta'->>'bar' = 'bababar' limit 1`); expect(u3.id).toEqual(u1.id); orm.em.clear(); mock.mock.calls.length = 0; const u4 = await orm.em.findOneOrFail(User, { profile1: { identity: { email: 'e123', meta: { foo: { $re: 'fo+' } } } }, profile2: { identity: { email: 'e2', meta: { foo: 'f2', bar: { $re: '(ba)+r' } } } }, }); expect(u4.id).toEqual(u1.id); expect(mock.mock.calls[0][0]).toMatch(`select "u0".* from "user" as "u0" where "u0"."profile1_identity_email" = 'e123' and "u0"."profile1_identity_meta_foo" ~ 'fo+' and "u0"."profile2"->'identity'->>'email' = 'e2' and "u0"."profile2"->'identity'->'meta'->>'foo' = 'f2' and "u0"."profile2"->'identity'->'meta'->>'bar' ~ '(ba)+r' limit 1`); orm.em.clear(); mock.mock.calls.length = 0; const u5 = await orm.em.findOneOrFail(User, { $or: [{ profile1: { identity: { meta: { foo: 'foooooooo' } } } }, { profile2: { identity: { meta: { bar: 'bababar' } } } }] }); expect(mock.mock.calls[0][0]).toMatch(`select "u0".* from "user" as "u0" where ("u0"."profile1_identity_meta_foo" = 'foooooooo' or "u0"."profile2"->'identity'->'meta'->>'bar' = 'bababar') limit 1`); expect(u5.id).toEqual(u1.id); }); test('invalid embedded property query', async () => { const err1 = `Invalid query for entity 'User', property 'city' does not exist in embeddable 'Identity'`; await expect(orm.em.findOneOrFail(User, { profile1: { identity: { city: 'London 1' } as any } })).rejects.toThrow(err1); const err2 = `Invalid query for entity 'User', property 'city' does not exist in embeddable 'Identity'`; await expect(orm.em.findOneOrFail(User, { profile2: { identity: { city: 'London 1' } as any } })).rejects.toThrow(err2); }); test('populating entities in embeddables', async () => { await createUsers(); const mock = mockLogger(orm); const users = await orm.em.find(User, {}, { populate: [ 'profile1.source', 'profile1.identity.source', 'profile1.identity.meta.source', 'profile1.identity.links.metas.source', ], orderBy: { name: 'desc' }, }); expect(mock.mock.calls[0][0]).toMatch(`select "u0".* from "user" as "u0" order by "u0"."name" desc`); expect(mock.mock.calls[1][0]).toMatch(`select "s0".* from "source" as "s0" where "s0"."id" in (1, 7)`); expect(mock.mock.calls[2][0]).toMatch(`select "s0".* from "source" as "s0" where "s0"."id" in (2, 8)`); expect(mock.mock.calls[3][0]).toMatch(`select "s0".* from "source" as "s0" where "s0"."id" in (3)`); expect(mock.mock.calls[4][0]).toMatch(`select "s0".* from "source" as "s0" where "s0"."id" in (11, 12, 13, 14, 15, 16)`); expect(wrap(users[1].profile1.identity.links[1].metas[2].source!).isInitialized()).toBe(true); expect(users[1].profile1.identity.links[1].metas[2].source!.name).toBe('ilms323'); // test serialization context expect(wrap(users[0]).toObject()).toMatchObject({ profile1: { source: { name: 's1' }, identity: { source: { name: 'is1' }, meta: { source: { name: 'ims1' } }, }, }, }); expect(wrap(users[1]).toObject()).toMatchObject({ profile1: { source: { name: 's3' }, identity: { source: { name: 'is3' }, links: [ { metas: [{ source: { name: 'ilms311' } }, { source: { name: 'ilms312' } }, { source: { name: 'ilms313' } }] }, { metas: [{ source: { name: 'ilms321' } }, { source: { name: 'ilms322' } }, { source: { name: 'ilms323' } }] }, ], }, }, }); }); test('#assign() works with nested embeddables', async () => { const jon = new User(); orm.em.assign(jon, { profile1: { username: 'u1', identity: { email: 'e1', meta: { bar: 'b1', foo: 'f1' } } }, profile2: { username: 'u2', identity: { email: 'e2', meta: { bar: 'b2', foo: 'f2' } } }, }); expect(jon.profile1).toMatchObject({ username: 'u1', identity: { email: 'e1', meta: { bar: 'b1', foo: 'f1' } } }); expect(jon.profile1).toBeInstanceOf(Profile); expect(jon.profile1.identity).toBeInstanceOf(Identity); expect(jon.profile1.identity.meta).toBeInstanceOf(IdentityMeta); expect(jon.profile2).toMatchObject({ username: 'u2', identity: { email: 'e2', meta: { bar: 'b2', foo: 'f2' } } }); expect(jon.profile2).toBeInstanceOf(Profile); expect(jon.profile2.identity).toBeInstanceOf(Identity); expect(jon.profile2.identity.meta).toBeInstanceOf(IdentityMeta); orm.em.assign(jon, { profile1: { identity: { email: 'e3' } } }); expect(jon.profile1.username).toBe('u1'); expect(jon.profile1.identity.email).toBe('e3'); expect(jon.profile1.identity.meta).not.toBeUndefined(); delete jon.profile1.identity.meta; orm.em.assign(jon, { profile1: { identity: { meta: { foo: 'f' } } } }); expect(jon.profile1.identity.meta!.foo).toBe('f'); expect(jon.profile1.identity.meta).toBeInstanceOf(IdentityMeta); orm.em.assign(jon, { profile1: { identity: { email: 'e4' } } }, { mergeObjectProperties: false }); expect(jon.profile1.username).toBeUndefined(); expect(jon.profile1.identity.email).toBe('e4'); expect(jon.profile1.identity.meta).toBeUndefined(); }); });