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.

255 lines (200 loc) 10.3 kB
import { Entity, LoadStrategy, ManyToOne, MikroORM, PrimaryKey, Property, t, Type } from '@mikro-orm/core'; import { MySqlDriver } from '@mikro-orm/mysql'; import { mockLogger } from '../../helpers'; export class Point { constructor(public latitude: number, public longitude: number) { } } export class PointType extends Type<Point | undefined, string | undefined> { override convertToDatabaseValue(value: Point | undefined): string | undefined { if (!value) { return value; } return `point(${value.latitude} ${value.longitude})`; } override convertToJSValue(value: string | undefined): Point | undefined { const m = value?.match(/point\((\d+(\.\d+)?) (\d+(\.\d+)?)\)/i); if (!m) { return undefined; } return new Point(+m[1], +m[3]); } override convertToJSValueSQL(key: string) { return `ST_AsText(${key})`; } override convertToDatabaseValueSQL(key: string) { return `ST_PointFromText(${key})`; } override getColumnType(): string { return 'point'; } } export class ExtendedPointType extends PointType { } @Entity() export class Location { @PrimaryKey() id!: number; @Property({ type: t.float, nullable: true }) rank?: number; @Property({ type: 'point', nullable: true }) point?: Point; @Property({ type: 'point-extended', nullable: true }) extendedPoint?: Point; } @Entity() export class Address { @PrimaryKey() id!: number; @ManyToOne() location: Location; constructor(location: Location) { this.location = location; } } describe('custom types [mysql]', () => { let orm: MikroORM<MySqlDriver>; beforeAll(async () => { orm = await MikroORM.init<MySqlDriver>({ entities: [Location, Address], dbName: `mikro_orm_test_custom_types`, driver: MySqlDriver, port: 3308, discovery: { getMappedType(type) { switch (type) { case 'point': return new PointType(); case 'point-extended': return new ExtendedPointType(); } return undefined; }, }, }); await orm.schema.refreshDatabase(); }); beforeEach(async () => { await orm.em.nativeDelete(Address, {}); await orm.em.nativeDelete(Location, {}); }); afterAll(async () => orm.close(true)); test('advanced custom types', async () => { const mock = mockLogger(orm, ['query', 'query-params']); const loc = new Location(); const addr = new Address(loc); loc.point = new Point(1.23, 4.56); loc.extendedPoint = new Point(5.23, 9.56); await orm.em.persistAndFlush(addr); orm.em.clear(); const l1 = await orm.em.findOneOrFail(Location, loc); expect(l1.point).toBeInstanceOf(Point); expect(l1.point).toMatchObject({ latitude: 1.23, longitude: 4.56 }); expect(l1.extendedPoint).toBeInstanceOf(Point); expect(l1.extendedPoint).toMatchObject({ latitude: 5.23, longitude: 9.56 }); expect(mock.mock.calls[0][0]).toMatch('begin'); expect(mock.mock.calls[1][0]).toMatch('insert into `location` (`point`, `extended_point`) values (ST_PointFromText(\'point(1.23 4.56)\'), ST_PointFromText(\'point(5.23 9.56)\'))'); expect(mock.mock.calls[2][0]).toMatch('insert into `address` (`location_id`) values (1)'); expect(mock.mock.calls[3][0]).toMatch('commit'); expect(mock.mock.calls[4][0]).toMatch('select `l0`.*, ST_AsText(`l0`.`point`) as `point`, ST_AsText(`l0`.`extended_point`) as `extended_point` from `location` as `l0` where `l0`.`id` = 1 limit 1'); expect(mock.mock.calls).toHaveLength(5); await orm.em.flush(); // ensure we do not fire queries when nothing changed expect(mock.mock.calls).toHaveLength(5); l1.point = new Point(2.34, 9.87); await orm.em.flush(); expect(mock.mock.calls).toHaveLength(8); expect(mock.mock.calls[5][0]).toMatch('begin'); expect(mock.mock.calls[6][0]).toMatch('update `location` set `point` = ST_PointFromText(\'point(2.34 9.87)\') where `id` = 1'); expect(mock.mock.calls[7][0]).toMatch('commit'); orm.em.clear(); const qb1 = orm.em.createQueryBuilder(Location, 'l'); const res1 = await qb1.select('*').where({ id: loc.id }).getSingleResult(); expect(mock.mock.calls[8][0]).toMatch('select `l`.*, ST_AsText(`l`.`point`) as `point`, ST_AsText(`l`.`extended_point`) as `extended_point` from `location` as `l` where `l`.`id` = 1'); expect(res1).toMatchObject(l1); orm.em.clear(); const qb2 = orm.em.createQueryBuilder(Location); const res2 = await qb2.select(['l0.*']).where({ id: loc.id }).getSingleResult(); expect(mock.mock.calls[9][0]).toMatch('select `l0`.*, ST_AsText(`l0`.`point`) as `point`, ST_AsText(`l0`.`extended_point`) as `extended_point` from `location` as `l0` where `l0`.`id` = 1'); expect(res2).toMatchObject(l1); mock.mock.calls.length = 0; orm.em.clear(); // custom types with SQL fragments with joined strategy (GH #1594) const a2 = await orm.em.findOneOrFail(Address, addr, { populate: ['location'], strategy: LoadStrategy.JOINED }); expect(mock.mock.calls[0][0]).toMatch('select `a0`.*, `l1`.`id` as `l1__id`, `l1`.`rank` as `l1__rank`, ST_AsText(`l1`.`point`) as `l1__point`, ST_AsText(`l1`.`extended_point`) as `l1__extended_point` from `address` as `a0` left join `location` as `l1` on `a0`.`location_id` = `l1`.`id` where `a0`.`id` = 1'); expect(a2.location.point).toBeInstanceOf(Point); expect(a2.location.point).toMatchObject({ latitude: 2.34, longitude: 9.87 }); expect(a2.location.extendedPoint).toBeInstanceOf(Point); expect(a2.location.extendedPoint).toMatchObject({ latitude: 5.23, longitude: 9.56 }); expect(mock.mock.calls).toHaveLength(1); await orm.em.flush(); // ensure we do not fire queries when nothing changed expect(mock.mock.calls).toHaveLength(1); }); test('extending custom types (gh issue 1442)', async () => { const meta = orm.getMetadata().get('Location'); expect(meta.properties.point.customType).toBeInstanceOf(PointType); expect(meta.properties.extendedPoint.customType).toBeInstanceOf(ExtendedPointType); }); test('partial loading with custom types (gh issue 4622)', async () => { const mock = mockLogger(orm, ['query']); const loc = new Location(); const addr = new Address(loc); loc.point = new Point(1.23, 4.56); loc.extendedPoint = new Point(5.23, 9.56); await orm.em.persistAndFlush(addr); orm.em.clear(); const l1 = await orm.em.findOneOrFail(Location, loc, { fields: ['point', 'extendedPoint'] }); expect(l1.point).toBeInstanceOf(Point); expect(l1.point).toMatchObject({ latitude: 1.23, longitude: 4.56 }); expect(l1.extendedPoint).toBeInstanceOf(Point); expect(l1.extendedPoint).toMatchObject({ latitude: 5.23, longitude: 9.56 }); expect(mock.mock.calls[0][0]).toMatch('begin'); expect(mock.mock.calls[1][0]).toMatch('insert into `location` (`point`, `extended_point`) values (ST_PointFromText(?), ST_PointFromText(?))'); expect(mock.mock.calls[2][0]).toMatch('insert into `address` (`location_id`) values (?)'); expect(mock.mock.calls[3][0]).toMatch('commit'); expect(mock.mock.calls[4][0]).toMatch('select `l0`.`id`, ST_AsText(`l0`.`point`) as `point`, ST_AsText(`l0`.`extended_point`) as `extended_point` from `location` as `l0` where `l0`.`id` = ? limit ?'); expect(mock.mock.calls).toHaveLength(5); }); test('create and update many records with custom types (gh issue 1625)', async () => { const mock = mockLogger(orm, ['query', 'query-params']); const locations = [new Location(), new Location()]; locations[0].point = new Point(-1.23, -4.56); locations[1].point = new Point(-7.89, -0.12); await orm.em.persistAndFlush(locations); locations[0].point = new Point(1.23, 4.56); locations[1].point = new Point(7.89, 0.12); await orm.em.persistAndFlush(locations); expect(mock.mock.calls[0][0]).toMatch('begin'); expect(mock.mock.calls[1][0]).toMatch('insert into `location` (`point`) values (ST_PointFromText(\'point(-1.23 -4.56)\')), (ST_PointFromText(\'point(-7.89 -0.12)\'))'); expect(mock.mock.calls[2][0]).toMatch('commit'); expect(mock.mock.calls[3][0]).toMatch('begin'); expect(mock.mock.calls[4][0]).toMatch(`update \`location\` set \`point\` = case when (\`id\` = ${locations[0].id}) then ST_PointFromText('point(1.23 4.56)') when (\`id\` = ${locations[1].id}) then ST_PointFromText('point(7.89 0.12)') else \`point\` end where \`id\` in (${locations[0].id}, ${locations[1].id})`); expect(mock.mock.calls[5][0]).toMatch('commit'); expect(mock.mock.calls).toHaveLength(6); }); test('find entity by custom types (gh issue 1630)', async () => { const location = new Location(); location.point = new Point(1, 1); await orm.em.persistAndFlush(location); orm.em.clear(); const mock = mockLogger(orm, ['query', 'query-params']); const foundLocation = await orm.em.findOne(Location, { point: new Point(1, 1), extendedPoint: null, }); expect(mock.mock.calls[0][0]).toMatch('select `l0`.*, ST_AsText(`l0`.`point`) as `point`, ST_AsText(`l0`.`extended_point`) as `extended_point` from `location` as `l0` where ST_AsText(`l0`.`point`) = \'point(1 1)\' and ST_AsText(`l0`.`extended_point`) is null limit 1'); expect(mock.mock.calls).toHaveLength(1); expect(foundLocation).toBeInstanceOf(Location); }); test('find entity by custom types with object subconditions', async () => { const location = new Location(); location.point = new Point(1, 1); location.extendedPoint = new Point(1, 1); await orm.em.persistAndFlush(location); orm.em.clear(); const mock = mockLogger(orm, ['query', 'query-params']); const foundLocation = await orm.em.findOne(Location, { point: new Point(1, 1), extendedPoint: { $ne: null }, }); expect(mock.mock.calls[0][0]).toMatch('select `l0`.*, ST_AsText(`l0`.`point`) as `point`, ST_AsText(`l0`.`extended_point`) as `extended_point` from `location` as `l0` where ST_AsText(`l0`.`point`) = \'point(1 1)\' and ST_AsText(`l0`.`extended_point`) is not null limit 1'); expect(mock.mock.calls).toHaveLength(1); expect(foundLocation).toBeInstanceOf(Location); }); });