Skip to content

Commit

Permalink
feat: add crud relation methods
Browse files Browse the repository at this point in the history
  • Loading branch information
jannyHou committed Jun 13, 2018
1 parent 19f9d61 commit ae98552
Show file tree
Hide file tree
Showing 2 changed files with 229 additions and 17 deletions.
100 changes: 95 additions & 5 deletions packages/repository/src/repositories/relation.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,7 +30,47 @@ export interface HasManyEntityCrudRepository<T extends Entity, ID> {
* @param options Options for the operation
* @returns A promise which resolves with the found target instance(s)
*/
find(filter?: Filter | undefined, options?: Options): Promise<T[]>;
find(filter?: Filter, options?: Options): Promise<T[]>;
/**
*
* @param entity
* @param options
*/
patch(entity: DataObject<T>, options?: Options): Promise<boolean>;
/**
*
* @param entity
* @param options
*/
delete(entity: DataObject<T>, options?: Options): Promise<boolean>;
/**
*
* @param entity
* @param options
*/
replace(entity: DataObject<T>, options?: Options): Promise<boolean>;
/**
*
* @param dataObjects
*/
createAll(dataObjects: DataObject<T>[]): Promise<T[]>;
/**
*
* @param where
* @param options
*/
deleteAll(where?: Where, options?: Options): Promise<number>;
/**
*
* @param dataObject
* @param where
* @param options
*/
patchAll(
dataObject: DataObject<T>,
where?: Where,
options?: Options,
): Promise<number>;
}

export class DefaultHasManyEntityCrudRepository<
Expand All @@ -53,10 +97,56 @@ export class DefaultHasManyEntityCrudRepository<
);
}

async find(filter?: Filter | undefined, options?: Options): Promise<T[]> {
async find(filter?: Filter, options?: Options): Promise<T[]> {
return await this.targetRepository.find(
constrainFilter(filter, this.constraint),
options,
);
}

async patch(entity: DataObject<T>, options?: Options): Promise<boolean> {
return await this.targetRepository.update(
constrainDataObject(entity, this.constraint),
options,
);
}

async delete(entity: DataObject<T>, options?: Options): Promise<boolean> {
return await this.targetRepository.delete(
constrainDataObject(entity, this.constraint),
options,
);
}

async replace(entity: DataObject<T>, options?: Options): Promise<boolean> {
return await this.targetRepository.replaceById(
entity.getId(),
constrainDataObject(entity, this.constraint),
options,
);
}

async createAll(entities: DataObject<T>[]): Promise<T[]> {
entities = entities.map(e => constrainDataObject(e, this.constraint));
return await this.targetRepository.createAll(entities);
}

async deleteAll(where?: Where, options?: Options): Promise<number> {
return await this.targetRepository.deleteAll(
constrainWhere(where, this.constraint),
options,
);
}

async patchAll(
dataObject: DataObject<T>,
where?: Where,
options?: Options,
): Promise<number> {
return await this.targetRepository.updateAll(
dataObject,
constrainWhere(where, this.constraint),
options,
);
}
}
146 changes: 134 additions & 12 deletions packages/repository/test/acceptance/has-many.relation.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
hasManyRepositoryFactory,
HasManyDefinition,
RelationType,
HasManyEntityCrudRepository,
} from '../..';
import {expect} from '@loopback/testlab';

Expand All @@ -24,32 +25,33 @@ describe('HasMany relation', () => {
let existingCustomerId: number;
//FIXME: this should be inferred from relational decorators
let customerHasManyOrdersRelationMeta: HasManyDefinition;
let customerOrders: HasManyEntityCrudRepository<Order, number>;

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<Order>) {
// 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,
Expand All @@ -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');

Expand Down Expand Up @@ -92,6 +204,12 @@ describe('HasMany relation', () => {
})
description: string;

@property({
type: 'boolean',
required: false,
})
isDelivered: boolean;

@property({
type: 'number',
required: true,
Expand Down Expand Up @@ -123,4 +241,8 @@ describe('HasMany relation', () => {
type: RelationType.hasMany,
};
}

function toOrderEntity(data: Partial<Order>) {
return new Order(data);
}
});

0 comments on commit ae98552

Please sign in to comment.