diff --git a/packages/repository/src/repositories/atomic.repository.ts b/packages/repository/src/repositories/atomic.repository.ts index b341244dec1c..5b7359ed7eb9 100644 --- a/packages/repository/src/repositories/atomic.repository.ts +++ b/packages/repository/src/repositories/atomic.repository.ts @@ -10,6 +10,11 @@ import { import * as assert from 'assert'; import * as legacy from 'loopback-datasource-juggler'; +export interface FindOrCreateResult { + entity: T; + found: boolean; +} + export interface AtomicCrudRepository extends EntityCrudRepository { /** @@ -30,7 +35,7 @@ export interface AtomicCrudRepository filter: Filter, entity: DataObject, options?: Options, - ): Promise<[T, boolean]>; + ): FindOrCreateResult; } export class DefaultAtomicCrudRepository @@ -53,17 +58,18 @@ export class DefaultAtomicCrudRepository filter: Filter, entity: DataObject, options?: AnyObject | undefined, - ): Promise<[T, boolean]> { - if ( - this.dataSource.connector && - typeof this.dataSource.connector.findOrCreate === 'function' - ) { - const result = await ensurePromise( - this.modelClass.findOrCreate(filter as legacy.Filter, entity, options), - ); - return [this.toEntity(result[0]), result[1]]; - } else { + ): Promise> { + const canRunAtomically = + typeof this.dataSource.connector!.findOrCreate === 'function'; + if (!canRunAtomically) { throw new Error('Method not implemented.'); + // FIXME add machine-readable `err.code` } + + const result = await ensurePromise( + this.modelClass.findOrCreate(filter as legacy.Filter, entity, options), + ); + + return {entity: this.toEntity(result[0]), found: !result[1]}; } } diff --git a/packages/repository/test/unit/repositories/atomic.repository.unit.ts b/packages/repository/test/unit/repositories/atomic.repository.unit.ts index 09f290284ace..243d1c06b6e9 100644 --- a/packages/repository/test/unit/repositories/atomic.repository.unit.ts +++ b/packages/repository/test/unit/repositories/atomic.repository.unit.ts @@ -10,6 +10,7 @@ import { juggler, ModelDefinition, DefaultAtomicCrudRepository, + DefaultCrudRepository, } from '../../..'; describe('AtomicCrudRepository', () => { @@ -69,31 +70,6 @@ describe('AtomicCrudRepository', () => { toVisit: String[]; } - it('converts PropertyDefinition with array type', () => { - const originalPropertyDefinition = Object.assign( - {}, - ShoppingList.definition.properties, - ); - const listDefinition = new DefaultAtomicCrudRepository(ShoppingList, ds) - .modelClass.definition; - const jugglerPropertyDefinition = { - created: {type: Date}, - toBuy: { - type: [String], - }, - toVisit: { - type: [String], - }, - }; - - expect(listDefinition.properties).to.containDeep( - jugglerPropertyDefinition, - ); - expect(ShoppingList.definition.properties).to.containDeep( - originalPropertyDefinition, - ); - }); - it('throws if a connector instance is not defined for a datasource', () => { const dsWithoutConnector = new juggler.DataSource({ name: 'ds2', @@ -108,212 +84,38 @@ describe('AtomicCrudRepository', () => { /Connector instance must exist and support atomic operations/, ); }); - - it('shares the backing PersistedModel across repo instances', () => { - const model1 = new DefaultAtomicCrudRepository(Note, ds).modelClass; - const model2 = new DefaultAtomicCrudRepository(Note, ds).modelClass; - - expect(model1 === model2).to.be.true(); - }); }); - context('Non atomic CRUD operations', () => { - it('implements Repository.create()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.create({title: 't3', content: 'c3'}); - const result = await repo.findById(note.id); - expect(result.toJSON()).to.eql(note.toJSON()); - }); - - it('implements Repository.createAll()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const notes = await repo.createAll([ - {title: 't3', content: 'c3'}, - {title: 't4', content: 'c4'}, - ]); - expect(notes.length).to.eql(2); - }); - - it('implements Repository.find()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.createAll([ - {title: 't1', content: 'c1'}, - {title: 't2', content: 'c2'}, - ]); - const notes = await repo.find({where: {title: 't1'}}); - expect(notes.length).to.eql(1); - }); - - it('implements Repository.findOne()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.createAll([ - {title: 't1', content: 'c1'}, - {title: 't1', content: 'c2'}, - ]); - const note = await repo.findOne({ - where: {title: 't1'}, - order: ['content DESC'], - }); - expect(note).to.not.be.null(); - expect(note && note.title).to.eql('t1'); - expect(note && note.content).to.eql('c2'); - }); - it('returns null if Repository.findOne() does not return a value', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.createAll([ - {title: 't1', content: 'c1'}, - {title: 't1', content: 'c2'}, - ]); - const note = await repo.findOne({ - where: {title: 't5'}, - order: ['content DESC'], - }); - expect(note).to.be.null(); - }); - - describe('findById', () => { - it('returns the correct instance', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.create({ - title: 'a-title', - content: 'a-content', - }); - const result = await repo.findById(note.id); - expect(result && result.toJSON()).to.eql(note.toJSON()); - }); - - it('throws when the instance does not exist', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await expect(repo.findById(999999)).to.be.rejectedWith({ - code: 'ENTITY_NOT_FOUND', - message: 'Entity not found: Note with id 999999', - }); - }); - }); - - it('implements Repository.delete()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.create({title: 't3', content: 'c3'}); - - await repo.delete(note); - - const found = await repo.find({where: {id: note.id}}); - expect(found).to.be.empty(); - }); - - it('implements Repository.deleteById()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.create({title: 't3', content: 'c3'}); - - await repo.deleteById(note.id); - - const found = await repo.find({where: {id: note.id}}); - expect(found).to.be.empty(); - }); - - it('throws EntityNotFoundError when deleting an unknown id', async () => { + context('DefaultCrudRepository', () => { + it('Implements same functionalities as DefaultCrudRepo', async () => { const repo = new DefaultAtomicCrudRepository(Note, ds); - await expect(repo.deleteById(99999)).to.be.rejectedWith( - EntityNotFoundError, + expect(repo.create).to.equal(DefaultCrudRepository.prototype.create); + expect(repo.createAll).to.equal( + DefaultCrudRepository.prototype.createAll, ); - }); - - it('implements Repository.deleteAll()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.create({title: 't3', content: 'c3'}); - await repo.create({title: 't4', content: 'c4'}); - const result = await repo.deleteAll({title: 't3'}); - expect(result.count).to.eql(1); - }); - - it('implements Repository.updateById()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.create({title: 't3', content: 'c3'}); - - const id = note.id; - const delta = {content: 'c4'}; - await repo.updateById(id, delta); - - const updated = await repo.findById(id); - expect(updated.toJSON()).to.eql(Object.assign(note.toJSON(), delta)); - }); - - it('throws EntityNotFound error when updating an unknown id', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await expect(repo.updateById(9999, {title: 't4'})).to.be.rejectedWith( - EntityNotFoundError, + expect(repo.find).to.equal(DefaultCrudRepository.prototype.find); + expect(repo.findOne).to.equal(DefaultCrudRepository.prototype.findOne); + expect(repo.findById).to.equal(DefaultCrudRepository.prototype.findById); + expect(repo.delete).to.equal(DefaultCrudRepository.prototype.delete); + expect(repo.deleteAll).to.equal( + DefaultCrudRepository.prototype.deleteAll, ); - }); - - it('implements Repository.updateAll()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.create({title: 't3', content: 'c3'}); - await repo.create({title: 't4', content: 'c4'}); - const result = await repo.updateAll({content: 'c5'}, {}); - expect(result.count).to.eql(2); - const notes = await repo.find({where: {title: 't3'}}); - expect(notes[0].content).to.eql('c5'); - }); - - it('implements Repository.updateAll() without a where object', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.create({title: 't3', content: 'c3'}); - await repo.create({title: 't4', content: 'c4'}); - const result = await repo.updateAll({content: 'c5'}); - expect(result.count).to.eql(2); - const notes = await repo.find(); - const titles = notes.map(n => `${n.title}:${n.content}`); - expect(titles).to.deepEqual(['t3:c5', 't4:c5']); - }); - - it('implements Repository.count()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await repo.create({title: 't3', content: 'c3'}); - await repo.create({title: 't4', content: 'c4'}); - const result = await repo.count(); - expect(result.count).to.eql(2); - }); - - it('implements Repository.save() without id', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.save(new Note({title: 't3', content: 'c3'})); - const result = await repo.findById(note!.id); - expect(result.toJSON()).to.eql(note!.toJSON()); - }); - - it('implements Repository.save() with id', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note1 = await repo.create({title: 't3', content: 'c3'}); - note1.content = 'c4'; - const note = await repo.save(note1); - const result = await repo.findById(note!.id); - expect(result.toJSON()).to.eql(note1.toJSON()); - }); - - it('implements Repository.replaceById()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note = await repo.create({title: 't3', content: 'c3'}); - await repo.replaceById(note.id, {title: 't4', content: undefined}); - const result = await repo.findById(note.id); - expect(result.toJSON()).to.eql({ - id: note.id, - title: 't4', - content: undefined, - }); - }); - - it('throws EntityNotFound error when replacing an unknown id', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - await expect(repo.replaceById(9999, {title: 't4'})).to.be.rejectedWith( - EntityNotFoundError, + expect(repo.deleteById).to.equal( + DefaultCrudRepository.prototype.deleteById, ); - }); - - it('implements Repository.exists()', async () => { - const repo = new DefaultAtomicCrudRepository(Note, ds); - const note1 = await repo.create({title: 't3', content: 'c3'}); - const ok = await repo.exists(note1.id); - expect(ok).to.be.true(); + expect(repo.update).to.equal(DefaultCrudRepository.prototype.update); + expect(repo.updateAll).to.equal( + DefaultCrudRepository.prototype.updateAll, + ); + expect(repo.updateById).to.equal( + DefaultCrudRepository.prototype.updateById, + ); + expect(repo.count).to.equal(DefaultCrudRepository.prototype.count); + expect(repo.save).to.equal(DefaultCrudRepository.prototype.save); + expect(repo.replaceById).to.equal( + DefaultCrudRepository.prototype.replaceById, + ); + expect(repo.exists).to.equal(DefaultCrudRepository.prototype.exists); }); });