From ae985525cd4bd5244b3c86f7c8536afdf2752b14 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Fri, 8 Jun 2018 14:37:13 -0400 Subject: [PATCH] feat: add crud relation methods --- .../src/repositories/relation.repository.ts | 100 +++++++++++- .../has-many.relation.acceptance.ts | 146 ++++++++++++++++-- 2 files changed, 229 insertions(+), 17 deletions(-) diff --git a/packages/repository/src/repositories/relation.repository.ts b/packages/repository/src/repositories/relation.repository.ts index ab075210b0e5..c3b5429790a6 100644 --- a/packages/repository/src/repositories/relation.repository.ts +++ b/packages/repository/src/repositories/relation.repository.ts @@ -4,10 +4,14 @@ // License text available at https://opensource.org/licenses/MIT import {EntityCrudRepository} from './repository'; -import {constrainDataObject, constrainFilter} from './constraint-utils'; -import {AnyObject, Options} from '../common-types'; +import { + constrainDataObject, + constrainFilter, + constrainWhere, +} from './constraint-utils'; +import {DataObject, AnyObject, Options} from '../common-types'; import {Entity} from '../model'; -import {Filter} from '../query'; +import {Filter, Where} from '../query'; /** * CRUD operations for a target repository of a HasMany relation @@ -26,7 +30,47 @@ export interface HasManyEntityCrudRepository { * @param options Options for the operation * @returns A promise which resolves with the found target instance(s) */ - find(filter?: Filter | undefined, options?: Options): Promise; + find(filter?: Filter, options?: Options): Promise; + /** + * + * @param entity + * @param options + */ + patch(entity: DataObject, options?: Options): Promise; + /** + * + * @param entity + * @param options + */ + delete(entity: DataObject, options?: Options): Promise; + /** + * + * @param entity + * @param options + */ + replace(entity: DataObject, options?: Options): Promise; + /** + * + * @param dataObjects + */ + createAll(dataObjects: DataObject[]): Promise; + /** + * + * @param where + * @param options + */ + deleteAll(where?: Where, options?: Options): Promise; + /** + * + * @param dataObject + * @param where + * @param options + */ + patchAll( + dataObject: DataObject, + where?: Where, + options?: Options, + ): Promise; } export class DefaultHasManyEntityCrudRepository< @@ -53,10 +97,56 @@ export class DefaultHasManyEntityCrudRepository< ); } - async find(filter?: Filter | undefined, options?: Options): Promise { + async find(filter?: Filter, options?: Options): Promise { return await this.targetRepository.find( constrainFilter(filter, this.constraint), options, ); } + + async patch(entity: DataObject, options?: Options): Promise { + return await this.targetRepository.update( + constrainDataObject(entity, this.constraint), + options, + ); + } + + async delete(entity: DataObject, options?: Options): Promise { + return await this.targetRepository.delete( + constrainDataObject(entity, this.constraint), + options, + ); + } + + async replace(entity: DataObject, options?: Options): Promise { + return await this.targetRepository.replaceById( + entity.getId(), + constrainDataObject(entity, this.constraint), + options, + ); + } + + async createAll(entities: DataObject[]): Promise { + entities = entities.map(e => constrainDataObject(e, this.constraint)); + return await this.targetRepository.createAll(entities); + } + + async deleteAll(where?: Where, options?: Options): Promise { + return await this.targetRepository.deleteAll( + constrainWhere(where, this.constraint), + options, + ); + } + + async patchAll( + dataObject: DataObject, + where?: Where, + options?: Options, + ): Promise { + return await this.targetRepository.updateAll( + dataObject, + constrainWhere(where, this.constraint), + options, + ); + } } diff --git a/packages/repository/test/acceptance/has-many.relation.acceptance.ts b/packages/repository/test/acceptance/has-many.relation.acceptance.ts index beebc440d4af..601d49473549 100644 --- a/packages/repository/test/acceptance/has-many.relation.acceptance.ts +++ b/packages/repository/test/acceptance/has-many.relation.acceptance.ts @@ -13,6 +13,7 @@ import { hasManyRepositoryFactory, HasManyDefinition, RelationType, + HasManyEntityCrudRepository, } from '../..'; import {expect} from '@loopback/testlab'; @@ -24,32 +25,33 @@ describe('HasMany relation', () => { let existingCustomerId: number; //FIXME: this should be inferred from relational decorators let customerHasManyOrdersRelationMeta: HasManyDefinition; + let customerOrders: HasManyEntityCrudRepository; beforeEach(async () => { existingCustomerId = (await givenPersistedCustomerInstance()).id; customerHasManyOrdersRelationMeta = givenHasManyRelationMetadata(); + // Ideally, we would like to write + // customerRepo.orders.create(customerId, orderData); + // or customerRepo.orders({id: customerId}).* + // The initial "involved" implementation is below + + //FIXME: should be automagically instantiated via DI or other means + customerOrders = hasManyRepositoryFactory( + existingCustomerId, + customerHasManyOrdersRelationMeta, + orderRepo, + ); }); it('can create an instance of the related model', async () => { // A controller method - CustomerOrdersController.create() // customerRepo and orderRepo would be injected via constructor arguments async function create(customerId: number, orderData: Partial) { - // Ideally, we would like to write - // customerRepo.orders.create(customerId, orderData); - // or customerRepo.orders({id: customerId}).* - // The initial "involved" implementation is below - - //FIXME: should be automagically instantiated via DI or other means - const customerOrders = hasManyRepositoryFactory( - customerId, - customerHasManyOrdersRelationMeta, - orderRepo, - ); return await customerOrders.create(orderData); } const description = 'an order desc'; - const order = await create(existingCustomerId, {description}); + const order = await customerOrders.create({description}); expect(order.toObject()).to.containDeep({ customerId: existingCustomerId, @@ -59,6 +61,116 @@ describe('HasMany relation', () => { expect(persisted.toObject()).to.deepEqual(order.toObject()); }); + it('can patch an instance of the related model', async () => { + const originalData = { + description: 'an order', + isDelivered: false, + }; + const order = await customerOrders.create(originalData); + + const fieldToPatch = { + isDelivered: true, + }; + const newData = Object.assign({id: order.id}, fieldToPatch); + const isPatched = await customerOrders.patch(toOrderEntity(newData)); + expect(isPatched).to.be.true; + + const expectedResult = { + id: order.id, + description: 'an order', + isDelivered: true, + customerId: existingCustomerId, + }; + const patchedData = await orderRepo.findById(order.id); + expect(patchedData.toObject()).to.deepEqual(expectedResult); + }); + + it('can delete an instance of the related model', async () => { + const originalData = { + description: 'an order', + }; + const order = await customerOrders.create(originalData); + delete order.customerId; + + const isDeleted = await customerOrders.delete(toOrderEntity(order)); + expect(isDeleted).to.be.true; + + async function findDeletedData() { + return await orderRepo.findById(order.id); + } + await expect(findDeletedData()).to.be.rejectedWith( + /no Order found with id/, + ); + }); + + it('can replace an instance of the related model', async () => { + const originalData = { + description: 'an order', + isDelivered: false, + }; + const order = await customerOrders.create(originalData); + + const newData = { + id: order.id, + description: 'new order', + }; + const newEntity = toOrderEntity(newData); + const isReplaced = await customerOrders.replace(newEntity); + expect(isReplaced).to.be.true; + + const expectedResult = { + id: order.id, + description: 'new order', + // @jannyhou: investigating why it's undefined, pretty sure it's not caused by `constrainData` + isDelivered: undefined, + customerId: existingCustomerId, + }; + const replacedData = await orderRepo.findById(order.id); + expect(replacedData.toObject()).to.deepEqual(expectedResult); + }); + + it('can run batch operations', async () => { + // write test in one `it` to save time + // Will split them into 3 seperate `it` tests + // if we decide to keep them + const originalData = [ + toOrderEntity({description: 'order 1'}), + toOrderEntity({description: 'order 2'}), + ]; + await testCreateAll(); + await testPatchAll(); + await testDeleteAll(); + + async function testCreateAll() { + const createdData = await customerOrders.createAll(originalData); + originalData.forEach(order => { + const createdOrder = createdData.filter(d => { + return d.description === order.description; + }); + expect(createdOrder).to.have.lengthOf(1); + expect(createdOrder[0].customerId).to.eql(existingCustomerId); + }); + } + + async function testPatchAll() { + const patchObject = {description: 'new order'}; + const arePatched = await customerOrders.patchAll(patchObject); + expect(arePatched).to.be.true; + const patchedItems = await customerOrders.find(); + expect(patchedItems).to.have.length(2); + patchedItems.forEach(order => { + expect(order.description).to.eql('new order'); + expect(order.customerId).to.eql(existingCustomerId); + }); + } + + async function testDeleteAll() { + await customerOrders.deleteAll(); + const relatedOrders = await customerOrders.find(); + expect(relatedOrders).to.be.empty; + } + }); + // This should be enforced by the database to avoid race conditions it.skip('reject create request when the customer does not exist'); @@ -92,6 +204,12 @@ describe('HasMany relation', () => { }) description: string; + @property({ + type: 'boolean', + required: false, + }) + isDelivered: boolean; + @property({ type: 'number', required: true, @@ -123,4 +241,8 @@ describe('HasMany relation', () => { type: RelationType.hasMany, }; } + + function toOrderEntity(data: Partial) { + return new Order(data); + } });