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.
1,207 lines (1,052 loc) • 125 kB
text/typescript
import { v4 } from 'uuid';
import { inspect } from 'util';
import { SqlHighlighter } from '@mikro-orm/sql-highlighter';
import {
Collection,
Configuration,
EntityManager,
LockMode,
MikroORM,
QueryFlag,
QueryOrder,
Reference,
ValidationError,
wrap,
UniqueConstraintViolationException,
TableNotFoundException,
TableExistsException,
SyntaxErrorException,
NonUniqueFieldNameException,
InvalidFieldNameException,
IsolationLevel,
NullHighlighter,
PopulateHint,
raw,
ref,
RawQueryFragment,
} from '@mikro-orm/core';
import { MySqlDriver, MySqlConnection, ScalarReference } from '@mikro-orm/mysql';
import { Address2, Author2, Book2, BookTag2, FooBar2, FooBaz2, Publisher2, PublisherType, Test2 } from './entities-sql';
import { initORMMySql, mockLogger } from './bootstrap';
import { Author2Subscriber } from './subscribers/Author2Subscriber';
import { EverythingSubscriber } from './subscribers/EverythingSubscriber';
import { FlushSubscriber } from './subscribers/FlushSubscriber';
import { Test2Subscriber } from './subscribers/Test2Subscriber';
describe('EntityManagerMySql', () => {
let orm: MikroORM<MySqlDriver>;
beforeAll(async () => orm = await initORMMySql('mysql', {}, true));
beforeEach(async () => orm.schema.clearDatabase());
afterEach(() => {
expect(RawQueryFragment.checkCacheSize()).toBe(0);
orm.config.set('debug', false);
Author2Subscriber.log.length = 0;
EverythingSubscriber.log.length = 0;
FlushSubscriber.log.length = 0;
Test2Subscriber.log.length = 0;
});
afterAll(async () => {
await orm.schema.dropDatabase();
await orm.close(true);
});
test('isConnected()', async () => {
expect(await orm.isConnected()).toBe(true);
expect(await orm.checkConnection()).toEqual({
ok: true,
});
await orm.close(true);
expect(await orm.isConnected()).toBe(false);
const check = await orm.checkConnection();
expect(check).toMatchObject({
ok: false,
error: expect.any(Error),
reason: 'Unable to acquire a connection',
});
await orm.connect();
expect(await orm.isConnected()).toBe(true);
expect(await orm.checkConnection()).toEqual({
ok: true,
});
expect(inspect(orm.em)).toBe(`[EntityManager<${orm.em.id}>]`);
});
test('getConnectionOptions()', async () => {
const config = new Configuration({
driver: MySqlDriver,
clientUrl: 'mysql://root@127.0.0.1:3308/db_name',
host: '127.0.0.10',
password: 'secret',
user: 'user',
logger: jest.fn(),
forceUtcTimezone: true,
} as any, false);
config.reset('debug');
const driver = new MySqlDriver(config);
expect(driver.getConnection().getConnectionOptions()).toMatchObject({
database: 'db_name',
host: '127.0.0.10',
password: 'secret',
port: 3308,
user: 'user',
timezone: 'Z',
dateStrings: true,
supportBigNumbers: true,
});
});
test('raw query with array param', async () => {
const q1 = await orm.em.getPlatform().formatQuery(`select * from author2 where id in (?) limit ?`, [[1, 2, 3], 3]);
expect(q1).toBe('select * from author2 where id in (1, 2, 3) limit 3');
const q2 = await orm.em.getPlatform().formatQuery(`select * from author2 where id in (?) limit ?`, [['1', '2', '3'], 3]);
expect(q2).toBe(`select * from author2 where id in ('1', '2', '3') limit 3`);
});
test('should return mysql driver', async () => {
const driver = orm.em.getDriver();
expect(driver).toBeInstanceOf(MySqlDriver);
await expect(driver.findOne<Book2>(Book2.name, { title: 'bar' })).resolves.toBeNull();
await expect(driver.findOne<Book2>(Book2.name, 'uuid')).resolves.toBeNull();
const author = await driver.nativeInsert(Author2.name, { name: 'author', email: 'email' });
const tag = await driver.nativeInsert(BookTag2.name, { name: 'tag name' });
expect((await driver.nativeInsert(Book2.name, { uuid: v4(), author: author.insertId, tags: [tag.insertId] })).insertId).not.toBeNull();
await expect(driver.getConnection().execute('select 1 as count')).resolves.toEqual([{ count: 1 }]);
await expect(driver.getConnection().execute('select 1 as count', [], 'get')).resolves.toEqual({ count: 1 });
await expect(driver.getConnection().execute('select 1 as count', [], 'run')).resolves.toEqual([{ count: 1 }]);
await expect(driver.getConnection().execute('insert into test2 (name) values (?)', ['test'], 'run')).resolves.toEqual({
affectedRows: 1,
insertId: 1,
rows: [],
});
await expect(driver.getConnection().execute('update test2 set name = ? where name = ?', ['test 2', 'test'], 'run')).resolves.toEqual({
affectedRows: 1,
insertId: 0,
rows: [],
});
await expect(driver.getConnection().execute('delete from test2 where name = ?', ['test 2'], 'run')).resolves.toEqual({
affectedRows: 1,
insertId: 0,
rows: [],
});
expect(driver.getPlatform().usesImplicitTransactions()).toBe(true);
expect(driver.getPlatform().denormalizePrimaryKey(1)).toBe(1);
expect(driver.getPlatform().denormalizePrimaryKey('1')).toBe('1');
await expect(driver.find<BookTag2>(BookTag2.name, { books: { $in: ['1'] } })).resolves.not.toBeNull();
await expect(driver.count(BookTag2.name, {})).resolves.toBe(1);
const conn = driver.getConnection();
const tx = await conn.begin();
await conn.execute('select 1', [], 'all', tx);
await conn.execute(orm.em.getKnex().raw('select 1'), [], 'all', tx);
await conn.execute(orm.em.getRepository(Author2).getKnex().raw('select 1'), [], 'all', tx);
await conn.commit(tx);
// multi inserts
const res = await driver.nativeInsertMany(Publisher2.name, [
{ name: 'test 1', type: PublisherType.GLOBAL },
{ name: 'test 2', type: PublisherType.LOCAL },
{ name: 'test 3', type: PublisherType.GLOBAL },
]);
// mysql returns the first inserted id
expect(res).toMatchObject({ insertId: 1, affectedRows: 3, row: { id: 1 }, rows: [{ id: 1 }, { id: 2 }, { id: 3 }] });
const res2 = await driver.find(Publisher2.name, {});
expect(res2).toMatchObject([
{ id: 1, name: 'test 1', type: PublisherType.GLOBAL },
{ id: 2, name: 'test 2', type: PublisherType.LOCAL },
{ id: 3, name: 'test 3', type: PublisherType.GLOBAL },
]);
// multi updates
const res3 = await driver.nativeUpdateMany<Publisher2>(Publisher2.name, [1, 2, 3], [
{ name: 'test 11', type: PublisherType.LOCAL },
{ type: PublisherType.GLOBAL },
{ name: 'test 33', type: PublisherType.LOCAL },
]);
const res4 = await driver.find(Publisher2.name, {});
expect(res4).toMatchObject([
{ id: 1, name: 'test 11', type: PublisherType.LOCAL },
{ id: 2, name: 'test 2', type: PublisherType.GLOBAL },
{ id: 3, name: 'test 33', type: PublisherType.LOCAL },
]);
});
test('driver appends errored query', async () => {
const driver = orm.em.getDriver();
const err1 = `insert into \`not_existing\` (\`foo\`) values ('bar') - Table '${orm.config.get('dbName')}.not_existing' doesn't exist`;
await expect(driver.nativeInsert('not_existing', { foo: 'bar' })).rejects.toThrow(err1);
const err2 = `delete from \`not_existing\` - Table '${orm.config.get('dbName')}.not_existing' doesn't exist`;
await expect(driver.nativeDelete('not_existing', {})).rejects.toThrow(err2);
});
test('connection returns correct URL', async () => {
const conn1 = new MySqlConnection(new Configuration({
driver: MySqlDriver,
clientUrl: 'mysql://example.host.com',
port: 1234,
user: 'usr',
password: 'pw',
} as any, false));
await expect(conn1.getClientUrl()).toBe('mysql://usr:*****@example.host.com:1234');
const conn2 = new MySqlConnection(new Configuration({ driver: MySqlDriver, port: 3307 } as any, false));
await expect(conn2.getClientUrl()).toBe('mysql://root@127.0.0.1:3307');
});
test('should convert entity to PK when trying to search by entity', async () => {
const repo = orm.em.getRepository(Author2);
const author = new Author2('name', 'email');
author.termsAccepted = true;
author.favouriteAuthor = author;
await orm.em.persistAndFlush(author);
const a = await repo.findOne(author);
const authors = await repo.find({ favouriteAuthor: author });
expect(a).toBe(author);
expect(authors[0]).toBe(author);
expect(await repo.findOne({ termsAccepted: false })).toBeNull();
});
test('should allow to find by array of PKs', async () => {
await orm.em.getDriver().nativeInsertMany(Author2.name, [
{ id: 1, name: 'n1', email: 'e1' },
{ id: 2, name: 'n2', email: 'e2' },
{ id: 3, name: 'n3', email: 'e3' },
]);
const repo = orm.em.getRepository(Author2);
const res = await repo.find([1, 2, 3]);
expect(res.map(a => a.id)).toEqual([1, 2, 3]);
});
test('should allow shadow properties in EM.create()', async () => {
const repo = orm.em.getRepository(Author2);
const author = repo.create({ name: 'name', email: 'email', version: 123 });
await expect(author.version).toBe(123);
});
test('should create UUID value when using EM.create()', async () => {
const repo = orm.em.getRepository(Book2);
const book = repo.create({ title: 'name', author: 123 });
expect(book.uuid).toBeDefined();
});
test('manual mapping of raw DB results to entities vie EM.map()', async () => {
const repo = orm.em.getRepository(Book2);
const book = repo.map({
uuid_pk: '123-dsa',
title: 'name',
created_at: '2019-06-09T07:50:25.722Z',
author_id: 123,
publisher_id: 321,
tags: [1n, 2n, 3n],
})!;
expect(book.uuid).toBe('123-dsa');
expect(book.title).toBe('name');
expect(book.createdAt).toBeInstanceOf(Date);
expect(book.author).toBeInstanceOf(Author2);
expect(book.author.id).toBe(123);
expect(book.publisher).toBeInstanceOf(Reference);
expect(book.publisher!.unwrap()).toBeInstanceOf(Publisher2);
expect(book.publisher!.id).toBe(321);
expect(book.tags.length).toBe(3);
expect(book.tags[0]).toBeInstanceOf(BookTag2);
expect(book.tags[0].id).toBe(1n);
expect(book.tags[1].id).toBe(2n);
expect(book.tags[2].id).toBe(3n);
expect(repo.getReference(book.uuid)).toBe(book);
});
test('should work with boolean values', async () => {
const repo = orm.em.getRepository(Author2);
const author = new Author2('name', 'email');
await orm.em.persistAndFlush(author);
expect(author.termsAccepted).toBe(false);
author.termsAccepted = true;
await orm.em.persistAndFlush(author);
expect(author.termsAccepted).toBe(true);
orm.em.clear();
const a1 = await repo.findOne({ termsAccepted: false });
expect(a1).toBeNull();
const a2 = (await repo.findOne({ termsAccepted: true }))!;
expect(a2).not.toBeNull();
a2.termsAccepted = false;
await orm.em.persistAndFlush(a2);
orm.em.clear();
const a3 = (await repo.findOne({ termsAccepted: false }))!;
expect(a3).not.toBeNull();
expect(a3.termsAccepted).toBe(false);
const a4 = await repo.findOne({ termsAccepted: true });
expect(a4).toBeNull();
});
test(`populating inverse side of 1:1 also back-links inverse side's owner`, async () => {
const bar = FooBar2.create('fb');
bar.baz = new FooBaz2('fz');
await orm.em.persistAndFlush(bar);
orm.em.clear();
const repo = orm.em.getRepository(FooBar2);
const a = await repo.findOne(bar.id, { populate: ['baz'], flags: [QueryFlag.DISTINCT] });
expect(wrap(a!.baz!).isInitialized()).toBe(true);
expect(wrap(a!.baz!.bar!).isInitialized()).toBe(true);
});
test('factory should support a primary key value of 0', async () => {
const factory = orm.em.getEntityFactory();
const p1 = new Publisher2(); // calls constructor, so uses default name
expect(p1.name).toBe('asd');
expect(p1).toBeInstanceOf(Publisher2);
expect(p1.books).toBeInstanceOf(Collection);
expect(p1.tests).toBeInstanceOf(Collection);
const p2 = factory.create(Publisher2, { id: 0 }); // shouldn't call constructor
expect(p2).toBeInstanceOf(Publisher2);
expect(p2.name).toBeUndefined();
expect(p2.books).toBeInstanceOf(Collection);
expect(p2.tests).toBeInstanceOf(Collection);
});
test(`1:1 relationships with an inverse side primary key of 0 should link`, async () => {
// Set up static data with id of 0
const response = await orm.em.execute('set sql_mode = \'NO_AUTO_VALUE_ON_ZERO\'; insert into foo_baz2 (id, name) values (?, ?); set sql_mode = \'\'', [0, 'testBaz'], 'run');
expect(response[1]).toMatchObject({
affectedRows: 1,
insertId: 0,
});
const fooBazRef = orm.em.getReference<FooBaz2>(FooBaz2, 0);
const fooBar = FooBar2.create('testBar');
fooBar.baz = fooBazRef;
await orm.em.persistAndFlush(fooBar);
orm.em.clear();
const repo = orm.em.getRepository(FooBar2);
const a = await repo.findOne(fooBar.id, { populate: ['baz'] });
expect(wrap(a!.baz!).isInitialized()).toBe(true);
expect(a!.baz!.id).toBe(0);
expect(a!.baz!.name).toBe('testBaz');
});
test('inverse side of 1:1 is ignored in change set', async () => {
const bar = FooBar2.create('fb');
bar.baz = new FooBaz2('fz 1');
await orm.em.persistAndFlush(bar);
bar.baz = new FooBaz2('fz 2');
await orm.em.flush();
});
test('partial loading of 1:1 owner from inverse side', async () => {
const bar = FooBar2.create('fb');
bar.baz = new FooBaz2('fz');
await orm.em.fork().persistAndFlush(bar);
const a1 = await orm.em.findOneOrFail(FooBaz2, bar.baz, {
fields: ['name', 'bar'],
populate: [], // otherwise it would be inferred from `fields` and populate the `bar` automatically
});
expect(a1.name).toBe('fz');
expect(a1.bar).toBeInstanceOf(FooBar2);
// @ts-expect-error
expect(a1.version).toBeUndefined();
expect(wrap(a1.bar!).isInitialized()).toBe(false);
});
test('transactions', async () => {
const god1 = new Author2('God1', 'hello@heaven1.god');
await orm.em.begin();
orm.em.persist(god1);
await orm.em.rollback();
const res1 = await orm.em.findOne(Author2, { name: 'God1' });
expect(res1).toBeNull();
await orm.em.begin();
const god2 = new Author2('God2', 'hello@heaven2.god');
orm.em.persist(god2);
await orm.em.commit();
const res2 = await orm.em.findOne(Author2, { name: 'God2' });
expect(res2).not.toBeNull();
await orm.em.transactional(async em => {
const god3 = new Author2('God3', 'hello@heaven3.god');
await em.persist(god3);
});
const res3 = await orm.em.findOne(Author2, { name: 'God3' });
expect(res3).not.toBeNull();
const err = new Error('Test');
try {
await orm.em.transactional(async em => {
const god4 = new Author2('God4', 'hello@heaven4.god');
await em.persist(god4);
throw err;
});
} catch (e) {
expect(e).toBe(err);
const res4 = await orm.em.findOne(Author2, { name: 'God4' });
expect(res4).toBeNull();
}
});
test('transactions with isolation levels', async () => {
const mock = mockLogger(orm, ['query']);
const god1 = new Author2('God1', 'hello@heaven1.god');
try {
await orm.em.transactional(async em => {
await em.persistAndFlush(god1);
throw new Error(); // rollback the transaction
}, { isolationLevel: IsolationLevel.READ_UNCOMMITTED });
} catch { }
expect(mock.mock.calls[0][0]).toMatch('set transaction isolation level read uncommitted');
expect(mock.mock.calls[1][0]).toMatch('begin');
expect(mock.mock.calls[2][0]).toMatch('insert into `author2` (`created_at`, `updated_at`, `name`, `email`, `terms_accepted`) values (?, ?, ?, ?, ?)');
expect(mock.mock.calls[3][0]).toMatch('rollback');
});
test('nested transactions with save-points', async () => {
await orm.em.transactional(async em => {
const god1 = new Author2('God1', 'hello1@heaven.god');
try {
await em.transactional(async em2 => {
await em2.persist(god1);
throw new Error(); // rollback the transaction
});
} catch { }
const res1 = await em.findOne(Author2, { name: 'God1' });
expect(res1).toBeNull();
await em.transactional(async em2 => {
const god2 = new Author2('God2', 'hello2@heaven.god');
em2.persist(god2);
});
const res2 = await em.findOne(Author2, { name: 'God2' });
expect(res2).not.toBeNull();
});
});
test('nested transaction rollback with save-points will commit the outer one', async () => {
const mock = mockLogger(orm, ['query']);
// start outer transaction
const transaction = orm.em.transactional(async em => {
// do stuff inside inner transaction and rollback
try {
await em.transactional(async em2 => {
await em2.persistAndFlush(new Author2('God', 'hello@heaven.god'));
throw new Error(); // rollback the transaction
});
} catch { }
await em.persist(new Author2('God Persisted!', 'hello-persisted@heaven.god'));
});
// try to commit the outer transaction
await expect(transaction).resolves.toBeUndefined();
expect(mock.mock.calls.length).toBe(6);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('savepoint trx');
expect(mock.mock.calls[2][0]).toMatch('insert into `author2` (`created_at`, `updated_at`, `name`, `email`, `terms_accepted`) values (?, ?, ?, ?, ?)');
expect(mock.mock.calls[3][0]).toMatch('rollback to savepoint trx');
expect(mock.mock.calls[4][0]).toMatch('insert into `author2` (`created_at`, `updated_at`, `name`, `email`, `terms_accepted`) values (?, ?, ?, ?, ?)');
expect(mock.mock.calls[5][0]).toMatch('commit');
await expect(orm.em.findOne(Author2, { name: 'God Persisted!' })).resolves.not.toBeNull();
});
test('should load entities', async () => {
expect(orm).toBeInstanceOf(MikroORM);
expect(orm.em).toBeInstanceOf(EntityManager);
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
await orm.em.persistAndFlush(bible);
const author = new Author2('Jon Snow', 'snow@wall.st');
author.born = '1990-03-23';
author.bornTime = '00:23:59';
author.favouriteBook = bible;
const publisher = new Publisher2('7K publisher', PublisherType.GLOBAL);
// as we order by Book.createdAt when populating collection, we need to make sure values will be sequential
const book1 = new Book2('My Life on The Wall, part 1', author);
book1.createdAt = new Date(Date.now() + 1);
book1.publisher = wrap(publisher).toReference();
const book2 = new Book2('My Life on The Wall, part 2', author);
book2.createdAt = new Date(Date.now() + 2);
book2.publisher = wrap(publisher).toReference();
const book3 = new Book2('My Life on The Wall, part 3', author);
book3.createdAt = new Date(Date.now() + 3);
book3.publisher = wrap(publisher).toReference();
orm.em.persist(book1);
orm.em.persist(book2);
orm.em.persist(book3);
await orm.em.flush();
orm.em.clear();
const publisher7k = (await orm.em.getRepository(Publisher2).findOne({ name: '7K publisher' }))!;
expect(publisher7k).not.toBeNull();
expect(publisher7k.tests).toBeInstanceOf(Collection);
expect(publisher7k.tests.isInitialized()).toBe(false);
orm.em.clear();
const authorRepository = orm.em.getRepository(Author2);
const booksRepository = orm.em.getRepository(Book2);
const books = await booksRepository.findAll({ populate: ['author'] });
expect(wrap(books[0].author).isInitialized()).toBe(true);
expect(await authorRepository.findOne({ favouriteBook: bible.uuid })).not.toBe(null);
orm.em.clear();
const noBooks = await booksRepository.find({ title: 'not existing' }, { populate: ['author'] });
expect(noBooks.length).toBe(0);
orm.em.clear();
const jon = (await authorRepository.findOne({ name: 'Jon Snow' }))!;
await orm.em.populate(jon, ['books', 'favouriteBook']);
const authors = await authorRepository.findAll();
await orm.em.populate(authors, ['books', 'favouriteBook'], { where: { books: '123' } });
expect(await authorRepository.findOne({ email: 'not existing' })).toBeNull();
await expect(orm.em.populate([] as Author2[], ['books', 'favouriteBook'])).resolves.toEqual([]);
// full text search test
const fullTextBooks = (await booksRepository.find({ title: { $fulltext: 'life wall' } }))!;
expect(fullTextBooks.length).toBe(3);
// count test
const count = await authorRepository.count();
expect(count).toBe(authors.length);
const count2 = await orm.em.count(Author2);
expect(count2).toBe(authors.length);
const count3 = await orm.em.getRepository(Author2).count({}, {
groupBy: ['termsAccepted'],
having: { termsAccepted: false },
});
expect(count3).toBe(authors.length);
// identity map test
authors.shift(); // shift the god away, as that entity is detached from IM
expect(jon).toBe(authors[0]);
expect(jon).toBe(await authorRepository.findOne(jon.id));
// serialization test
const o = wrap(jon).toJSON();
expect(o).toMatchObject({
id: jon.id,
createdAt: jon.createdAt,
updatedAt: jon.updatedAt,
books: [
{ author: jon.id, publisher: publisher.id, title: 'My Life on The Wall, part 1' },
{ author: jon.id, publisher: publisher.id, title: 'My Life on The Wall, part 2' },
{ author: jon.id, publisher: publisher.id, title: 'My Life on The Wall, part 3' },
],
favouriteBook: { author: god.id, title: 'Bible' },
born: '1990-03-23',
bornTime: '00:23:59',
email: 'snow@wall.st',
name: 'Jon Snow',
});
expect(wrap(jon).toJSON()).toEqual(o);
expect(jon.books.getIdentifiers()).toBeInstanceOf(Array);
expect(typeof jon.books.getIdentifiers()[0]).toBe('string');
for (const author of authors) {
expect(author.books).toBeInstanceOf(Collection);
expect(author.books.isInitialized()).toBe(true);
// iterator test
for (const book of author.books) {
expect(book.title).toMatch(/My Life on The Wall, part \d/);
expect(book.author).toBeInstanceOf(Author2);
expect(wrap(book.author).isInitialized()).toBe(true);
expect(book.publisher).toBeInstanceOf(Reference);
expect(book.publisher!.unwrap()).toBeInstanceOf(Publisher2);
expect(wrap(book.publisher!).isInitialized()).toBe(false);
}
}
const booksByTitleAsc = await booksRepository.find({ author: jon.id }, { orderBy: { title: QueryOrder.ASC } });
expect(booksByTitleAsc[0].title).toBe('My Life on The Wall, part 1');
expect(booksByTitleAsc[1].title).toBe('My Life on The Wall, part 2');
expect(booksByTitleAsc[2].title).toBe('My Life on The Wall, part 3');
const booksByTitleDesc = await booksRepository.find({ author: jon.id }, { orderBy: { title: QueryOrder.DESC } });
expect(booksByTitleDesc[0].title).toBe('My Life on The Wall, part 3');
expect(booksByTitleDesc[1].title).toBe('My Life on The Wall, part 2');
expect(booksByTitleDesc[2].title).toBe('My Life on The Wall, part 1');
const twoBooks = await booksRepository.find({ author: jon.id }, { orderBy: { title: QueryOrder.DESC }, limit: 2 });
expect(twoBooks.length).toBe(2);
expect(twoBooks[0].title).toBe('My Life on The Wall, part 3');
expect(twoBooks[1].title).toBe('My Life on The Wall, part 2');
const lastBook = await booksRepository.find({ author: jon.id }, {
populate: ['author'],
orderBy: { title: QueryOrder.DESC },
limit: 2,
offset: 2,
});
expect(lastBook.length).toBe(1);
expect(lastBook[0].title).toBe('My Life on The Wall, part 1');
expect(lastBook[0].author).toBeInstanceOf(Author2);
expect(wrap(lastBook[0].author).isInitialized()).toBe(true);
await orm.em.remove(lastBook[0]).flush();
});
test('json properties', async () => {
const god = new Author2('God', 'hello@heaven.god');
god.identities = ['fb-123', 'pw-231', 'tw-321'];
const bible = new Book2('Bible', god);
bible.meta = { category: 'god like', items: 3, valid: true, nested: { foo: '123', bar: 321, deep: { baz: 59, qux: false } } };
await orm.em.persistAndFlush(bible);
orm.em.clear();
const g = await orm.em.findOneOrFail(Author2, god.id, { populate: ['books'] });
expect(Array.isArray(g.identities)).toBe(true);
expect(g.identities).toEqual(['fb-123', 'pw-231', 'tw-321']);
expect(typeof g.books[0].meta).toBe('object');
expect(g.books[0].meta).toEqual({ category: 'god like', items: 3, valid: true, nested: { foo: '123', bar: 321, deep: { baz: 59, qux: false } } });
orm.em.clear();
const b1 = await orm.em.findOneOrFail(Book2, { meta: { category: 'god like' } });
const b2 = await orm.em.findOneOrFail(Book2, { meta: { category: 'god like', items: 3 } });
const b3 = await orm.em.findOneOrFail(Book2, { meta: { nested: { bar: 321 } } });
const b4 = await orm.em.findOneOrFail(Book2, { meta: { nested: { foo: '123', bar: 321 } } });
const b5 = await orm.em.findOneOrFail(Book2, { meta: { valid: true, nested: { foo: '123', bar: 321 } } });
const b6 = await orm.em.findOneOrFail(Book2, { meta: { valid: true, nested: { foo: '123', bar: 321, deep: { baz: 59 } } } });
const b7 = await orm.em.findOneOrFail(Book2, { meta: { valid: true, nested: { foo: '123', bar: 321, deep: { baz: 59, qux: false } } } });
expect(b1).toBe(b2);
expect(b1).toBe(b3);
expect(b1).toBe(b4);
expect(b1).toBe(b5);
expect(b1).toBe(b6);
expect(b1).toBe(b7);
});
test('findOne should initialize entity that is already in IM', async () => {
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
await orm.em.persistAndFlush(bible);
orm.em.clear();
const ref = orm.em.getReference(Author2, god.id);
expect(wrap(ref).isInitialized()).toBe(false);
const newGod = await orm.em.findOne(Author2, god.id);
expect(ref).toBe(newGod);
expect(wrap(ref).isInitialized()).toBe(true);
});
test('findOne supports regexps', async () => {
const author1 = new Author2('Author 1', 'a1@example.com');
const author2 = new Author2('Author 2', 'a2@example.com');
const author3 = new Author2('Author 3', 'a3@example.com');
await orm.em.persistAndFlush([author1, author2, author3]);
orm.em.clear();
const authors = await orm.em.find(Author2, { email: /exa.*le\.c.m$/ });
expect(authors.length).toBe(3);
expect(authors[0].name).toBe('Author 1');
expect(authors[1].name).toBe('Author 2');
expect(authors[2].name).toBe('Author 3');
orm.em.clear();
const authors2 = await orm.em.find(Author2, { email: { $re: 'exa.*le.c.m$' } });
expect(authors2.length).toBe(3);
expect(authors2[0].name).toBe('Author 1');
expect(authors2[1].name).toBe('Author 2');
expect(authors2[2].name).toBe('Author 3');
});
test('findOne supports optimistic locking [testMultipleFlushesDoIncrementalUpdates]', async () => {
expect(Test2Subscriber.log).toEqual([]);
const test = new Test2();
for (let i = 0; i < 5; i++) {
test.name = 'test' + i;
await orm.em.persistAndFlush(test);
expect(typeof test.version).toBe('number');
expect(test.version).toBe(i + 1);
}
expect(Test2Subscriber.log.map(r => r[0])).toEqual([
'onFlush',
'afterFlush',
'onFlush',
'afterFlush',
'onFlush',
'afterFlush',
'onFlush',
'afterFlush',
'onFlush',
'afterFlush',
]);
});
test('findOne supports optimistic locking [testStandardFailureThrowsException]', async () => {
const test = new Test2();
test.name = 'test';
await orm.em.persistAndFlush(test);
expect(typeof test.version).toBe('number');
expect(test.version).toBe(1);
orm.em.clear();
const test2 = await orm.em.findOne(Test2, test.id);
await orm.em.nativeUpdate(Test2, { id: test.id }, { name: 'Changed!' }); // simulate concurrent update
test2!.name = 'WHATT???';
try {
await orm.em.flush();
expect(1).toBe('should be unreachable');
} catch (e: any) {
expect(e).toBeInstanceOf(ValidationError);
expect(e.message).toBe(`The optimistic lock on entity Test2 failed`);
expect((e as ValidationError).getEntity()).toBe(test2);
}
});
test('findOne supports optimistic locking [versioned proxy]', async () => {
const test = new Test2();
test.name = 'test';
await orm.em.persistAndFlush(test);
orm.em.clear();
const proxy = orm.em.getReference(Test2, test.id);
await orm.em.lock(proxy, LockMode.OPTIMISTIC, 1);
expect(wrap(proxy).isInitialized()).toBe(true);
});
test('findOne supports optimistic locking [versioned entity]', async () => {
const test = new Test2();
test.name = 'test';
await orm.em.persistAndFlush(test);
orm.em.clear();
const test2 = await orm.em.findOne(Test2, test.id, { lockMode: LockMode.OPTIMISTIC, lockVersion: test.version });
await orm.em.lock(test2!, LockMode.OPTIMISTIC, test.version);
const test3 = await orm.em.findOne(Test2, test.id, { lockMode: LockMode.OPTIMISTIC, lockVersion: test.version });
expect(test3).toBe(test2);
});
test('findOne supports optimistic locking [testOptimisticTimestampLockFailureThrowsException]', async () => {
const bar = FooBar2.create('Testing');
expect(bar.version).toBeUndefined();
await orm.em.persistAndFlush(bar);
expect(bar.version).toBeInstanceOf(Date);
orm.em.clear();
const bar2 = (await orm.em.findOne(FooBar2, bar.id))!;
expect(bar2.version).toBeInstanceOf(Date);
try {
// Try to lock the record with an older timestamp and it should throw an exception
const expectedVersionExpired = new Date(+bar2.version - 3600);
await orm.em.lock(bar2, LockMode.OPTIMISTIC, expectedVersionExpired);
expect(1).toBe('should be unreachable');
} catch (e) {
expect((e as ValidationError).getEntity()).toBe(bar2);
}
});
test('findOne supports optimistic locking [unversioned entity]', async () => {
const author = new Author2('name', 'email');
await orm.em.persistAndFlush(author);
await expect(orm.em.lock(author, LockMode.OPTIMISTIC)).rejects.toThrow('Cannot obtain optimistic lock on unversioned entity Author2');
await expect(orm.em.findOne(Author2, author.id, { lockMode: LockMode.OPTIMISTIC })).rejects.toThrow('Cannot obtain optimistic lock on unversioned entity Author2');
});
test('lock supports optimistic locking [versioned entity]', async () => {
const test = new Test2();
test.name = 'test';
await orm.em.persistAndFlush(test);
await orm.em.lock(test, undefined!);
await orm.em.lock(test, LockMode.OPTIMISTIC);
await orm.em.lock(test, LockMode.OPTIMISTIC, test.version);
});
test('lock supports optimistic locking [version mismatch]', async () => {
const test = new Test2();
test.name = 'test';
await orm.em.persistAndFlush(test);
await expect(orm.em.lock(test, LockMode.OPTIMISTIC, test.version + 1)).rejects.toThrow('The optimistic lock failed, version 2 was expected, but is actually 1');
});
test('lock supports optimistic locking [testLockUnmanagedEntityThrowsException]', async () => {
const test = new Test2();
test.name = 'test';
await expect(orm.em.lock(test, LockMode.OPTIMISTIC)).rejects.toThrow('Entity Test2 is not managed. An entity is managed if its fetched from the database or registered as new through EntityManager.persist()');
});
test('pessimistic locking requires active transaction', async () => {
const test = Test2.create('Lock test');
await orm.em.persistAndFlush(test);
await expect(orm.em.findOne(Test2, test.id, { lockMode: LockMode.PESSIMISTIC_READ })).rejects.toThrow('An open transaction is required for this operation');
await expect(orm.em.findOne(Test2, test.id, { lockMode: LockMode.PESSIMISTIC_WRITE })).rejects.toThrow('An open transaction is required for this operation');
await expect(orm.em.lock(test, LockMode.PESSIMISTIC_READ)).rejects.toThrow('An open transaction is required for this operation');
await expect(orm.em.lock(test, LockMode.PESSIMISTIC_WRITE)).rejects.toThrow('An open transaction is required for this operation');
});
test('lock supports pessimistic locking [pessimistic write]', async () => {
const author = new Author2('name', 'email');
await orm.em.persistAndFlush(author);
const mock = mockLogger(orm, ['query']);
await orm.em.transactional(async em => {
await em.lock(author, LockMode.PESSIMISTIC_WRITE);
});
expect(mock.mock.calls.length).toBe(3);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('select 1 from `author2` as `a0` where `a0`.`id` = ? for update');
expect(mock.mock.calls[2][0]).toMatch('commit');
});
test('lock supports pessimistic locking [pessimistic read]', async () => {
const author = new Author2('name', 'email');
await orm.em.persistAndFlush(author);
const mock = mockLogger(orm, ['query']);
await orm.em.transactional(async em => {
await em.lock(author, LockMode.PESSIMISTIC_READ);
});
expect(mock.mock.calls.length).toBe(3);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('select 1 from `author2` as `a0` where `a0`.`id` = ? lock in share mode');
expect(mock.mock.calls[2][0]).toMatch('commit');
orm.em.clear();
mock.mock.calls.length = 0;
await orm.em.transactional(async em => {
await em.findOne(Author2, { email: 'foo' }, { lockMode: LockMode.PESSIMISTIC_READ, strategy: 'select-in' });
});
expect(mock.mock.calls.length).toBe(3);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch(' from `author2` as `a0` left join `address2` as `a1` on `a0`.`id` = `a1`.`author_id` where `a0`.`email` = ? limit ? lock in share mode');
expect(mock.mock.calls[2][0]).toMatch('commit');
});
test('custom query expressions via query builder', async () => {
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
bible.price = 100;
bible.meta = { category: 'foo', items: 1 };
await orm.em.persistAndFlush(bible);
orm.em.clear();
const qb1 = orm.em.fork().createQueryBuilder(Book2);
const res1 = await qb1.select('*').where({ [raw('JSON_CONTAINS(`b0`.`meta`, ?)', [{ foo: 'bar' }])]: false }).execute('get');
expect(res1.createdAt).toBeDefined();
// @ts-expect-error
expect(res1.created_at).not.toBeDefined();
expect(res1.meta).toEqual({ category: 'foo', items: 1 });
const qb2 = orm.em.fork().createQueryBuilder(Book2);
const res2 = await qb2.select('*').where({ [raw('JSON_CONTAINS(meta, ?)', [{ category: 'foo' }])]: true }).execute('get', false);
expect(res2.createdAt).not.toBeDefined();
// @ts-expect-error
expect(res2.created_at).toBeDefined();
expect(res2.meta).toEqual({ category: 'foo', items: 1 });
const qb3 = orm.em.fork().createQueryBuilder(Book2);
const res3 = await qb3.select('*').where({ [raw('JSON_CONTAINS(meta, ?)', [{ category: 'foo' }])]: true }).getSingleResult();
expect(res3).toBeInstanceOf(Book2);
expect(res3!.createdAt).toBeDefined();
expect(res3!.meta).toEqual({ category: 'foo', items: 1 });
const res4 = await orm.em.fork().findOneOrFail(Book2, { [raw('JSON_CONTAINS(meta, ?)', [{ items: 1 }])]: true });
expect(res4).toBeInstanceOf(Book2);
expect(res4.createdAt).toBeDefined();
expect(res4.meta).toEqual({ category: 'foo', items: 1 });
});
test('tuple comparison', async () => {
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
bible.price = 100;
bible.meta = { category: 'foo', items: 1 };
await orm.em.persistAndFlush(bible);
orm.em.clear();
const mock = mockLogger(orm, ['query']);
const res4 = await orm.em.findOneOrFail(Book2, { [raw<Book2>(['price', 'createdAt'])]: { $lte: [100, new Date()] } });
expect(res4).toBeInstanceOf(Book2);
expect(res4.createdAt).toBeDefined();
expect(res4.price).toBe(100.00);
expect(res4.meta).toEqual({ category: 'foo', items: 1 });
expect(mock.mock.calls[0][0]).toMatch('where `b0`.`author_id` is not null and (`b0`.`price`, `b0`.`created_at`) <= (?, ?)');
});
test('query builder getResult() and getSingleResult() return entities', async () => {
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
bible.meta = { category: 'foo', items: 1 };
await orm.em.persistAndFlush(bible);
orm.em.clear();
const qb1 = orm.em.createQueryBuilder(Book2);
const res1 = await qb1.select('*').getSingleResult();
expect(res1).toBeInstanceOf(Book2);
expect(res1!.createdAt).toBeDefined();
expect((res1 as any).created_at).not.toBeDefined();
expect(res1!.meta).toEqual({ category: 'foo', items: 1 });
expect(wrap(res1!).isInitialized()).toBe(true);
const qb2 = orm.em.createQueryBuilder(Book2);
const res2 = await qb2.select('*').where({ title: 'not exists' }).getSingleResult();
expect(res2).toBeNull();
const res3 = await qb1.clone().select('*').getResult();
expect(res3).toHaveLength(1);
});
test('stable results of serialization', async () => {
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
const bible2 = new Book2('Bible pt. 2', god);
const bible3 = new Book2('Bible pt. 3', new Author2('Lol', 'lol@lol.lol'));
await orm.em.persistAndFlush([bible, bible2, bible3]);
orm.em.clear();
const books0 = await orm.em.find(Book2, []);
expect(books0).toHaveLength(0);
const newGod = (await orm.em.findOne(Author2, god.id))!;
const books = await orm.em.find(Book2, {});
await wrap(newGod).init();
for (const book of books) {
expect(wrap(book).toJSON()).toMatchObject({
author: book.author.id,
});
}
});
test('stable results of serialization (collection)', async () => {
const pub = new Publisher2('Publisher2');
await orm.em.persistAndFlush(pub);
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
bible.publisher = wrap(pub).toReference();
const bible2 = new Book2('Bible pt. 2', god);
bible2.publisher = wrap(pub).toReference();
const bible3 = new Book2('Bible pt. 3', new Author2('Lol', 'lol@lol.lol'));
bible3.publisher = wrap(pub).toReference();
await orm.em.persistAndFlush([bible, bible2, bible3]);
orm.em.clear();
const newGod = orm.em.getReference(Author2, god.id);
const publisher = (await orm.em.findOne(Publisher2, pub.id, { populate: ['books'] }))!;
await wrap(newGod).init();
const json = wrap(publisher).toJSON().books;
for (const book of publisher.books) {
expect(json.find(b => b.uuid === book.uuid)).toMatchObject({
author: book.author.id,
});
}
});
test('findOne by id', async () => {
const authorRepository = orm.em.getRepository(Author2);
const jon = new Author2('Jon Snow', 'snow@wall.st');
await orm.em.persistAndFlush(jon);
orm.em.clear();
let author = (await authorRepository.findOne(jon.id))!;
expect(author).not.toBeNull();
expect(author.name).toBe('Jon Snow');
orm.em.clear();
author = (await authorRepository.findOne({ id: jon.id }))!;
expect(author).not.toBeNull();
expect(author.name).toBe('Jon Snow');
});
test('populate ManyToOne relation', async () => {
const authorRepository = orm.em.getRepository(Author2);
const god = new Author2('God', 'hello@heaven.god');
const bible = new Book2('Bible', god);
await orm.em.persistAndFlush(bible);
let jon = new Author2('Jon Snow', 'snow@wall.st');
jon.born = '2023-03-23';
jon.favouriteBook = bible;
await orm.em.persistAndFlush(jon);
orm.em.clear();
jon = (await authorRepository.findOne(jon.id))!;
expect(jon).not.toBeNull();
expect(jon.name).toBe('Jon Snow');
expect(jon.favouriteBook).toBeInstanceOf(Book2);
expect(wrap(jon.favouriteBook!).isInitialized()).toBe(false);
await wrap(jon.favouriteBook!).init();
expect(jon.favouriteBook).toBeInstanceOf(Book2);
expect(wrap(jon.favouriteBook!).isInitialized()).toBe(true);
expect(jon.favouriteBook!.title).toBe('Bible');
});
test('populate OneToOne relation', async () => {
const bar = FooBar2.create('bar');
const baz = new FooBaz2('baz');
bar.baz = baz;
await orm.em.persistAndFlush(bar);
orm.em.clear();
const b1 = (await orm.em.findOne(FooBar2, { id: bar.id }, { populate: ['baz'] }))!;
expect(b1.baz).toBeInstanceOf(FooBaz2);
expect(b1.baz!.id).toBe(baz.id);
expect(wrap(b1).toJSON()).toMatchObject({ baz: { id: baz.id, bar: bar.id, name: 'baz' } });
});
test('populate OneToOne relation on inverse side', async () => {
const bar = FooBar2.create('bar');
const baz = new FooBaz2('baz');
bar.baz = baz;
await orm.em.persistAndFlush(bar);
orm.em.clear();
const mock = mockLogger(orm, ['query']);
const b0 = (await orm.em.findOne(FooBaz2, { id: baz.id }, { strategy: 'select-in' }))!;
expect(mock.mock.calls[0][0]).toMatch('select `f0`.*, `f1`.`id` as `bar_id` from `foo_baz2` as `f0` left join `foo_bar2` as `f1` on `f0`.`id` = `f1`.`baz_id` where `f0`.`id` = ? limit ?');
expect(b0.bar).toBeDefined();
expect(b0.bar).toBeInstanceOf(FooBar2);
expect(wrap(b0.bar!).isInitialized()).toBe(false);
orm.em.clear();
const b1 = (await orm.em.findOne(FooBaz2, { id: baz.id }, { populate: ['bar'], strategy: 'select-in' }))!;
expect(mock.mock.calls[1][0]).toMatch('select `f0`.*, `f1`.`id` as `bar_id` from `foo_baz2` as `f0` left join `foo_bar2` as `f1` on `f0`.`id` = `f1`.`baz_id` where `f0`.`id` = ? limit ?');
expect(mock.mock.calls[2][0]).toMatch('select `f0`.*, (select 123) as `random` from `foo_bar2` as `f0` where `f0`.`id` in (?)');
expect(b1.bar).toBeInstanceOf(FooBar2);
expect(b1.bar!.id).toBe(bar.id);
expect(wrap(b1).toJSON()).toMatchObject({ bar: { id: bar.id, baz: baz.id, name: 'bar' } });
orm.em.clear();
const b2 = (await orm.em.findOne(FooBaz2, { bar: bar.id }, { populate: ['bar'], strategy: 'select-in' }))!;
expect(mock.mock.calls[3][0]).toMatch('select `f0`.*, `f1`.`id` as `bar_id` from `foo_baz2` as `f0` left join `foo_bar2` as `f1` on `f0`.`id` = `f1`.`baz_id` where `f1`.`id` = ? limit ?');
expect(mock.mock.calls[4][0]).toMatch('select `f0`.*, (select 123) as `random` from `foo_bar2` as `f0` where `f0`.`id` in (?)');
expect(b2.bar).toBeInstanceOf(FooBar2);
expect(b2.bar!.id).toBe(bar.id);
expect(wrap(b2).toJSON()).toMatchObject({ bar: { id: bar.id, baz: baz.id, name: 'bar' } });
});
test('populate OneToOne relation with uuid PK', async () => {
const author = new Author2('name', 'email');
const book = new Book2('b1', author);
const test = Test2.create('t');
test.book = book;
await orm.em.persistAndFlush(test);
orm.em.clear();
const b1 = (await orm.em.findOne(Book2, { test: test.id }, { populate: ['test.config'] }))!;
expect(b1.uuid).not.toBeNull();
expect(wrap(b1).toJSON()).toMatchObject({ test: { id: test.id, book: test.book.uuid, name: 't' } });
});
test('populate passes nested where and orderBy', async () => {
const author = new Author2('name', 'email');
const b1 = new Book2('b1', author);
const b2 = new Book2('b2', author);
const b3 = new Book2('b3', author);
const b4 = new Book2('b4', author);
const b5 = new Book2('b5', author);
const tag1 = new BookTag2('silly');
const tag2 = new BookTag2('funny');
const tag3 = new BookTag2('sick');
const tag4 = new BookTag2('strange');
const tag5 = new BookTag2('sexy');
b1.tags.add(tag1, tag3);
b2.tags.add(tag1, tag2, tag5);
b3.tags.add(tag5);
b4.tags.add(tag2, tag4, tag5);
b5.tags.add(tag5);
await orm.em.persistAndFlush([b1, b2, b3, b4, b5]);
orm.em.clear();
const a0 = await orm.em.find(Author2, author, {
populate: ['books'],
orderBy: { books: { title: 'asc' } },
});
expect(a0[0].books.map(b => b.title)).toEqual(['b1', 'b2', 'b3', 'b4', 'b5']);
orm.em.clear();
const a1 = await orm.em.find(Author2, author, {
populate: ['books'],
orderBy: { books: { title: QueryOrder.DESC } },
});
expect(a1[0].books.map(b => b.title)).toEqual(['b5', 'b4', 'b3', 'b2', 'b1']);
orm.em.clear();
const a2 = await orm.em.findOneOrFail(Author2, author, {
populate: ['books'],
orderBy: [{ books: { title: QueryOrder.DESC } }],
});
expect(a2.books.map(b => b.title)).toEqual(['b5', 'b4', 'b3', 'b2', 'b1']);
orm.em.clear();
const a3 = await orm.em.findOneOrFail(Author2, { books: { tags: { name: { $in: ['silly', 'strange'] } } } }, {
populate: ['books.tags'],
populateWhere: PopulateHint.INFER,
orderBy: { books: { tags: { name: QueryOrder.DESC }, title: QueryOrder.ASC } },
});
expect(a3.books.map(b => b.title)).toEqual(['b4', 'b1', 'b2']); // first strange tag (desc), then silly by name (asc)
orm.em.clear();
const a4 = await orm.em.findOneOrFail(Author2, { books: { tags: { name: { $in: ['silly', 'strange'] } } } }, {
populate: ['books.tags'],
populateWhere: PopulateHint.INFER,
orderBy: [
{ books: { tags: { name: QueryOrder.DESC } } },
{ books: { title: QueryOrder.ASC } },
],
});
expect(a4.books.map(b => b.title)).toEqual(['b4', 'b1', 'b2']); // first strange tag (desc), then silly by name (asc)
orm.em.clear();
const a5 = await orm.em.findOneOrFail(Author2, { books: { tags: { name: { $in: ['silly', 'strange'] } } } }, {
populate: ['books.tags'],
populateWhere: PopulateHint.ALL,
orderBy: [
{ books: { tags: { name: QueryOrder.DESC } } },
{ books: { title: QueryOrder.ASC } },
],
});
expect(a5.books.map(b => b.title)).toEqual(['b4', 'b1', 'b2', 'b3', 'b5']);
orm.em.clear();
const a6 = await orm.em.findOneOrFail(Author2, { books: { tags: { name: { $in: ['silly', 'strange'] } } } }, {
populate: ['books.tags'],
populateWhere: {},
orderBy: [
{ books: { tags: { name: QueryOrder.DESC } } },
{ books: { title: QueryOrder.ASC } },
],
});
expect(a6.books.map(b => b.title)).toEqual(['b4', 'b1', 'b2', 'b3', 'b5']);
});
test('many to many relation', async () => {
const author = new Author2('Jon Snow', 'snow@wall.st');
const book1 = new Book2('My Life on The Wall, part 1', author);
const book2 = new Book2('My Life on The Wall, part 2', author);
const book3 = new Book2('My Life on The Wall, part 3', author);
const tag1 = new BookTag2('silly');
const tag2 = new BookTag2('funny');
const tag3 = new BookTag2('sick');
const tag4 = new BookTag2('strange');
const tag5 = new BookTag2('sexy');
book1.tags.add(tag1, tag3);
book2.tags.add(tag1, tag2, tag5);
book3.tags.add(tag2, tag4, tag5);
orm.em.persist(book1);
orm.em.persist(book2);
await orm.em.persistAndFlush(book3);
expect(typeof tag1.id).toBe('bigint');
expect(typeof tag2.id).toBe('bigint');
expect(typeof tag3.id).toBe('bigint');
expect(typeof tag4.id).toBe('bigint');
expect(typeof tag5.id).toBe('bigint');
// test inverse side
const tagRepository = orm.em.getRepository(BookTag2);
let tags = await tagRepository.findAll();
expect(tags).toBeInstanceOf(Array);
expect(tags.length).toBe(5);
expect(tags[0]).toBeInstanceOf(BookTag2);
expect(tags[0].name).toBe('silly');
expect(tags[0].books).toBeInstanceOf(Collection);
expect(tags[0].books.isInitialized()).toBe(true);
expect(tags[0].books.isDirty()).toBe(false);
expect(tags[0].books.count()).toBe(2);
expect(tags[0].books.length).toBe(2);
orm.em.clear();
tags = await orm.em.find(BookTag2, {});
expect(tags[0].books.isInitialized()).toBe(false);
expect(tags[0].books.isDirty()).toBe(false);
expect(() => tags[0].books.getItems()).toThrow(/Collection<Book2> of entity BookTag2\[\d+] not initialized/);
expect(() => tags[0].books.remove(book1, book2)).toThrow(/Collection<Book2> of entity BookTag2\[\d+] not initialized/);
expect(() => tags[0].books.contains(book1)).toThrow(/Collection<Book2> of entity BookTag2\[\d+] not initialized/);
// test M:N lazy load
orm.em.clear();
tags = await tagRepository.findAll();
await tags[0].books.init();
expect(tags[0].books.count()).toBe(2);
expect(tags[0].books.getItems()[0]).toBeInstanceOf(Book2);
expect(tags[0].books.getItems()[0].uuid).toBeDefined();
expect(wrap(tags[0].books.getItems()[0]).isInitialized()).toBe(true);
expect(tags[0].books.isInitialized()).toBe(true);
const old = tags[0];
expect(tags[1].books.isInitialized()).toBe(false);
tags = await tagRepository.findAll({ populate: ['books'] as const });
expect(tags[1].books.isInitialized()).toBe(true);
expect(tags[0].id).toBe(old.id);
expect(tags[0]).toBe(old);
expect(tags[0].books).toBe(old.books);
// test M:N lazy load
orm.em.clear();
let book = (await orm.em.findOne(Book2, book1.uuid))!;
expect(book.tags.isInitialized()).toBe(false);
expect(book.tags.toJSON()).toEqual([]);
await book.tags.init();