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.
197 lines (164 loc) • 8.76 kB
text/typescript
import { Author } from '../../entities';
import type { ChangeSet, ChangeSetComputer, EventSubscriber, FlushEventArgs, MikroORM } from '@mikro-orm/core';
import { ChangeSetType, EntityValidator, IdentityMap, UnitOfWork } from '@mikro-orm/core';
import { initORMMongo, mockLogger } from '../../bootstrap';
import FooBar from '../../entities/FooBar';
import { FooBaz } from '../../entities/FooBaz';
import { Dummy } from '../../entities/Dummy';
describe('UnitOfWork', () => {
let orm: MikroORM;
let uow: UnitOfWork;
let computer: ChangeSetComputer;
beforeAll(async () => {
orm = await initORMMongo();
uow = new UnitOfWork(orm.em);
// @ts-ignore
computer = uow.changeSetComputer;
});
beforeEach(async () => orm.schema.clearDatabase());
test('entity validation when persisting [not strict]', async () => {
// number instead of string will throw
const author = new Author('test', 'test');
Object.assign(author, { name: 111, email: 222 });
expect(() => computer.computeChangeSet(author)).toThrow(`Trying to set Author.name of type 'string' to '111' of type 'number'`);
// string date with unknown format will throw
Object.assign(author, { name: '333', email: '444', createdAt: 'asd' });
expect(() => computer.computeChangeSet(author)).toThrow(`Trying to set Author.createdAt of type 'Date' to 'asd' of type 'string'`);
delete author.createdAt;
// number bool with other value than 0/1 will throw
Object.assign(author, { termsAccepted: 2 });
expect(() => computer.computeChangeSet(author)).toThrow(`Trying to set Author.termsAccepted of type 'boolean' to '2' of type 'number'`);
// string date with correct format will be auto-corrected
Object.assign(author, { name: '333', email: '444', createdAt: '2018-01-01', termsAccepted: 1 });
let changeSet = computer.computeChangeSet(author)!;
expect(typeof changeSet.payload.name).toBe('string');
expect(changeSet.payload.name).toBe('333');
expect(typeof changeSet.payload.email).toBe('string');
expect(changeSet.payload.email).toBe('444');
expect(typeof changeSet.payload.termsAccepted).toBe('boolean');
expect(changeSet.payload.termsAccepted).toBe(true);
expect(changeSet.payload.createdAt instanceof Date).toBe(true);
// Date object will be ok
Object.assign(author, { createdAt: new Date() });
changeSet = (await computer.computeChangeSet(author))!;
expect(changeSet.payload.createdAt instanceof Date).toBe(true);
// null will be ok
Object.assign(author, { createdAt: null });
changeSet = (await computer.computeChangeSet(author))!;
expect(changeSet.payload.createdAt).toBeNull();
// string number with correct format will be auto-corrected
Object.assign(author, { age: '21' });
changeSet = (await computer.computeChangeSet(author))!;
expect(typeof changeSet.payload.age).toBe('number');
expect(changeSet.payload.age).toBe(21);
// string instead of number with will throw
Object.assign(author, { age: 'asd' });
expect(() => computer.computeChangeSet(author)).toThrow(`Trying to set Author.age of type 'number' to 'asd' of type 'string'`);
Object.assign(author, { age: new Date() });
expect(() => computer.computeChangeSet(author)).toThrow(/Trying to set Author\.age of type 'number' to '.*' of type 'Date'/);
Object.assign(author, { age: false });
expect(() => computer.computeChangeSet(author)).toThrow(`Trying to set Author.age of type 'number' to 'false' of type 'boolean'`);
author.age = 21;
// missing collection instance in m:n and 1:m relations
// @ts-ignore
delete author.books;
expect(() => computer.computeChangeSet(author)).toThrow(`Author.books is not initialized, define it as 'books = new Collection<Book>(this);'`);
});
test('entity validation when persisting [strict]', async () => {
const validator = new EntityValidator(true);
const author = new Author('test', 'test');
// string date with correct format will not be auto-corrected in strict mode
const payload = { name: '333', email: '444', createdAt: '2018-01-01', termsAccepted: 1 };
expect(() => validator.validate(author, payload, orm.getMetadata().get(Author.name))).toThrow(`Trying to set Author.createdAt of type 'Date' to '2018-01-01' of type 'string'`);
});
test('changeSet is null for empty payload', async () => {
const author = orm.em.create(Author, { id: '00000001885f0a3cc37dc9f0', name: 'test', email: 'test' });
expect(uow.getIdentityMap().get('Author-00000001885f0a3cc37dc9f0')).toBeUndefined();
uow.merge(author); // add entity to IM first
const changeSet = await computer.computeChangeSet(author); // then try to persist it again
expect(changeSet).toBeNull();
expect(uow.getIdentityMap()).not.toEqual(new IdentityMap());
expect(uow.getIdentityMap().get('Author-00000001885f0a3cc37dc9f0')).not.toBeUndefined();
expect(uow.getIdentityMap().get('Author-00000001885f0a3cc37dc9f2')).toBeUndefined();
uow.clear();
expect(uow.getIdentityMap()).toEqual(new IdentityMap());
expect(uow.getIdentityMap().get('Author-00000001885f0a3cc37dc9f0')).toBeUndefined();
});
test('changeSet is null for readonly entity', async () => {
const dummy = new Dummy();
uow.merge(dummy);
const changeSet = await computer.computeChangeSet(dummy);
expect(changeSet).toBeNull();
});
test('persist and remove will add entity to given stack only once', async () => {
const author = orm.em.create(Author, { id: '00000001885f0a3cc37dc9f0', name: 'test', email: 'test' });
uow.persist(author);
expect(uow.getPersistStack().size).toBe(1);
uow.persist(author);
expect(uow.getPersistStack().size).toBe(1);
uow.remove(author);
expect(uow.getPersistStack().size).toBe(0);
uow.remove(author);
expect(uow.getRemoveStack().size).toBe(1);
uow.remove(author);
expect(uow.getRemoveStack().size).toBe(1);
expect(uow.getCollectionUpdates().length).toBe(0);
expect(uow.getExtraUpdates().size).toBe(0);
});
test('getters', async () => {
const uow = new UnitOfWork(orm.em);
const author = orm.em.create(Author, { id: '00000001885f0a3cc37dc9f0', name: 'test', email: 'test' }, { managed: true });
uow.persist(author);
expect([...uow.getPersistStack()]).toEqual([author]);
expect([...uow.getRemoveStack()]).toEqual([]);
uow.remove(author);
expect([...uow.getRemoveStack()]).toEqual([author]);
expect(() => uow.recomputeSingleChangeSet(author)).not.toThrow();
expect(() => uow.computeChangeSet(author)).not.toThrow();
expect(() => uow.recomputeSingleChangeSet(author)).not.toThrow();
expect(() => uow.computeChangeSet(author)).not.toThrow();
});
test('manually changing the UoW state during flush', async () => {
let changeSets: ChangeSet<any>[] = [];
class Subscriber implements EventSubscriber {
async onFlush(args: FlushEventArgs): Promise<void> {
const changeSets = args.uow.getChangeSets();
const cs = changeSets.find(cs => cs.type === ChangeSetType.CREATE && cs.entity instanceof FooBar);
if (cs) {
const baz = new FooBaz();
baz.name = 'dynamic';
cs.entity.baz = baz;
args.uow.computeChangeSet(baz);
args.uow.recomputeSingleChangeSet(cs.entity);
}
const toRemove = changeSets.find(cs => cs.entity instanceof FooBar && cs.entity.name === 'remove me');
if (toRemove) {
args.uow.computeChangeSet(toRemove.entity, ChangeSetType.DELETE);
}
}
async afterFlush(args: FlushEventArgs): Promise<void> {
changeSets = [...args.uow.getChangeSets()];
}
}
const em = orm.em.fork();
em.getEventManager().registerSubscriber(new Subscriber());
const bar = new FooBar();
bar.name = 'bar';
const mock = mockLogger(orm);
await em.persistAndFlush(bar);
expect(mock.mock.calls[0][0]).toMatch(`db.getCollection('foo-baz').insertMany([ { name: 'dynamic' } ], {})`);
expect(mock.mock.calls[1][0]).toMatch(/db\.getCollection\('foo-bar'\)\.insertMany\(\[ { name: 'bar', onCreateTest: true, onUpdateTest: true, baz: ObjectId\('\w+'\) } ], {}\)/);
expect(changeSets.map(cs => [cs.type, cs.name])).toEqual([
[],
[],
]);
mock.mockReset();
bar.name = 'remove me';
await em.flush();
expect(mock.mock.calls[0][0]).toMatch(/db\.getCollection\('foo-bar'\)\.deleteMany\(\{ _id: \{ '\$in': \[ ObjectId\('\w{24}'\) ] } }, \{}\)/);
expect(changeSets.map(cs => [cs.type, cs.name])).toEqual([
[],
]);
});
afterAll(async () => orm.close(true));
});