From 01c7b89119a95b9478b5a9fd737ae58fb3769784 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Tue, 4 Dec 2018 22:29:06 -0500 Subject: [PATCH 1/2] feat: inclusion --- .../src/controllers/todo-list.controller.ts | 12 ++- .../src/controllers/todo.controller.ts | 6 +- .../src/repositories/todo-list.repository.ts | 4 + .../src/repositories/todo.repository.ts | 4 + packages/repository/src/query.ts | 2 + .../belongs-to/belongs-to-accessor.ts | 4 +- .../has-many/has-many-repository.factory.ts | 6 +- .../repository/src/repositories/inclusion.ts | 96 +++++++++++++++++++ packages/repository/src/repositories/index.ts | 1 + .../src/repositories/legacy-juggler-bridge.ts | 32 ++++++- .../belongs-to.relation.acceptance.ts | 16 ++++ .../has-many.relation.acceptance.ts | 25 +++++ .../repositories/customer.repository.ts | 4 + .../fixtures/repositories/order.repository.ts | 4 + 14 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 packages/repository/src/repositories/inclusion.ts diff --git a/examples/todo-list/src/controllers/todo-list.controller.ts b/examples/todo-list/src/controllers/todo-list.controller.ts index e88613e6606d..998da5621956 100644 --- a/examples/todo-list/src/controllers/todo-list.controller.ts +++ b/examples/todo-list/src/controllers/todo-list.controller.ts @@ -92,8 +92,18 @@ export class TodoListController { }, }, }) - async findById(@param.path.number('id') id: number): Promise { + async findById( return await this.todoListRepository.findById(id); + @param.path.number('id') id: number, + @param.query.object('filter') filter?: Filter, + ): Promise { + // somehow the filter sent in the request query is undefined + // will dig more. + // hardcoded the inclusion filter in the PoC PR + const hardcodedFilterForPoC = { + include: [{relation: 'todos'}], + }; + return await this.todoListRepository.findById(id, hardcodedFilterForPoC); } @patch('/todo-lists/{id}', { diff --git a/examples/todo-list/src/controllers/todo.controller.ts b/examples/todo-list/src/controllers/todo.controller.ts index 3d019a6bc11e..7a5efd7980ec 100644 --- a/examples/todo-list/src/controllers/todo.controller.ts +++ b/examples/todo-list/src/controllers/todo.controller.ts @@ -43,8 +43,12 @@ export class TodoController { async findTodoById( @param.path.number('id') id: number, @param.query.boolean('items') items?: boolean, + @param.query.object('filter') filter?: Filter, ): Promise { - return await this.todoRepo.findById(id); + const hardcodedFilterForPoC = { + include: [{relation: 'todoList'}], + }; + return await this.todoRepo.findById(id, hardcodedFilterForPoC); } @get('/todos', { diff --git a/examples/todo-list/src/repositories/todo-list.repository.ts b/examples/todo-list/src/repositories/todo-list.repository.ts index 8ad2b24221dd..49e989c1e404 100644 --- a/examples/todo-list/src/repositories/todo-list.repository.ts +++ b/examples/todo-list/src/repositories/todo-list.repository.ts @@ -44,6 +44,10 @@ export class TodoListRepository extends DefaultCrudRepository< 'image', todoListImageRepositoryGetter, ); + this._inclusionHandler.registerHandler( + 'todos', + todoRepositoryGetter, + ); } public findByTitle(title: string) { diff --git a/examples/todo-list/src/repositories/todo.repository.ts b/examples/todo-list/src/repositories/todo.repository.ts index 3c09cd83ff71..abf5f78a5007 100644 --- a/examples/todo-list/src/repositories/todo.repository.ts +++ b/examples/todo-list/src/repositories/todo.repository.ts @@ -33,5 +33,9 @@ export class TodoRepository extends DefaultCrudRepository< 'todoList', todoListRepositoryGetter, ); + this._inclusionHandler.registerHandler< + TodoList, + typeof TodoList.prototype.id + >('todoList', todoListRepositoryGetter); } } diff --git a/packages/repository/src/query.ts b/packages/repository/src/query.ts index a0209233929c..056eb906dc56 100644 --- a/packages/repository/src/query.ts +++ b/packages/repository/src/query.ts @@ -155,6 +155,8 @@ export type Order = {[P in keyof MT]: Direction}; */ export type Fields = {[P in keyof MT]?: boolean}; +// The entity type provided for the scope filter is the source model +// while it should be the target(related) model /** * Inclusion of related items * diff --git a/packages/repository/src/relations/belongs-to/belongs-to-accessor.ts b/packages/repository/src/relations/belongs-to/belongs-to-accessor.ts index 53f9a3e1e1f8..cbbc8f10cd74 100644 --- a/packages/repository/src/relations/belongs-to/belongs-to-accessor.ts +++ b/packages/repository/src/relations/belongs-to/belongs-to-accessor.ts @@ -48,7 +48,7 @@ export function createBelongsToAccessor< }; } -type BelongsToResolvedDefinition = BelongsToDefinition & {keyTo: string}; +export type BelongsToResolvedDefinition = BelongsToDefinition & {keyTo: string}; /** * Resolves given belongsTo metadata if target is specified to be a resolver. @@ -56,7 +56,7 @@ type BelongsToResolvedDefinition = BelongsToDefinition & {keyTo: string}; * property id metadata * @param relationMeta belongsTo metadata to resolve */ -function resolveBelongsToMetadata(relationMeta: BelongsToDefinition) { +export function resolveBelongsToMetadata(relationMeta: BelongsToDefinition) { if (!isTypeResolver(relationMeta.target)) { const reason = 'target must be a type resolver'; throw new InvalidRelationError(reason, relationMeta); diff --git a/packages/repository/src/relations/has-many/has-many-repository.factory.ts b/packages/repository/src/relations/has-many/has-many-repository.factory.ts index 626766f4b7a6..0fb3e6c0a399 100644 --- a/packages/repository/src/relations/has-many/has-many-repository.factory.ts +++ b/packages/repository/src/relations/has-many/has-many-repository.factory.ts @@ -56,7 +56,7 @@ export function createHasManyRepositoryFactory< }; } -type HasManyResolvedDefinition = HasManyDefinition & {keyTo: string}; +export type HasManyResolvedDefinition = HasManyDefinition & {keyTo: string}; /** * Resolves given hasMany metadata if target is specified to be a resolver. @@ -64,7 +64,7 @@ type HasManyResolvedDefinition = HasManyDefinition & {keyTo: string}; * belongsTo metadata * @param relationMeta hasMany metadata to resolve */ -function resolveHasManyMetadata( +export function resolveHasManyMetadata( relationMeta: HasManyDefinition, ): HasManyResolvedDefinition { if (!isTypeResolver(relationMeta.target)) { @@ -73,7 +73,7 @@ function resolveHasManyMetadata( } if (relationMeta.keyTo) { - // The explict cast is needed because of a limitation of type inference + // The explicit cast is needed because of a limitation of type inference return relationMeta as HasManyResolvedDefinition; } diff --git a/packages/repository/src/repositories/inclusion.ts b/packages/repository/src/repositories/inclusion.ts new file mode 100644 index 000000000000..78e58a8872fe --- /dev/null +++ b/packages/repository/src/repositories/inclusion.ts @@ -0,0 +1,96 @@ +import {Entity} from '../model'; +import {Getter} from '@loopback/core'; +import { + Filter, + Where, + resolveHasManyMetadata, + resolveBelongsToMetadata, + RelationMetadata, + RelationType, + constrainWhere, +} from '../'; +import {AnyObject} from '..'; +import {DefaultCrudRepository} from './legacy-juggler-bridge'; +import {inspect} from 'util'; +import { + HasManyDefinition, + BelongsToDefinition, + HasManyResolvedDefinition, + BelongsToResolvedDefinition, +} from '../relations'; + +type ResolvedRelationMetadata = + | HasManyResolvedDefinition + | BelongsToResolvedDefinition; + +// SE: the source entity +// TE: the target entity +// SID: the ID of source entity +// TID: the ID of target entity + +export class InclusionHandler { + _handlers: {[relation: string]: Function} = {}; + constructor(public sourceRepository: DefaultCrudRepository) {} + + registerHandler( + relationName: string, + targetRepoGetter: Getter>, + ) { + this._handlers[relationName] = fetchIncludedItems; + const self = this; + + async function fetchIncludedItems( + fks: SID[], + filter?: Filter, + ): Promise { + const targetRepo = await targetRepoGetter(); + const relationDef: ResolvedRelationMetadata = self.getResolvedRelationDefinition( + relationName, + ); + filter = filter || {}; + filter.where = self.buildConstrainedWhere( + fks, + filter.where || {}, + relationDef, + ); + console.log(`inclusion filter: ${inspect(filter)}`); + + return await targetRepo.find(filter); + } + } + + findHandler(relationName: string) { + const errMsg = + `The inclusion handler for relation ${relationName} is not found!` + + `Make sure you defined ${relationName} properly.`; + + return this._handlers[relationName] || new Error(errMsg); + } + + buildConstrainedWhere( + ids: SID[], + whereFilter: Where, + relationDef: ResolvedRelationMetadata, + ): Where { + const keyPropName: string = relationDef.keyTo; + const where: AnyObject = {}; + where[keyPropName] = {inq: ids}; + return constrainWhere(whereFilter, where as Where); + } + + getResolvedRelationDefinition(name: string): ResolvedRelationMetadata { + const relationMetadata: RelationMetadata = this.sourceRepository.entityClass + .definition.relations[name]; + + switch (relationMetadata.type) { + case RelationType.hasMany: + return resolveHasManyMetadata(relationMetadata as HasManyDefinition); + case RelationType.belongsTo: + return resolveBelongsToMetadata( + relationMetadata as BelongsToDefinition, + ); + default: + throw new Error(`Unsupported relation type ${relationMetadata.type}`); + } + } +} diff --git a/packages/repository/src/repositories/index.ts b/packages/repository/src/repositories/index.ts index 60175cd87ba5..bc1db0abace9 100644 --- a/packages/repository/src/repositories/index.ts +++ b/packages/repository/src/repositories/index.ts @@ -8,3 +8,4 @@ export * from './legacy-juggler-bridge'; export * from './kv.repository.bridge'; export * from './repository'; export * from './constraint-utils'; +export * from './inclusion'; diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 9dee24db2e15..ac6536c17e9b 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -31,6 +31,8 @@ import { } from '../relations'; import {resolveType} from '../type-resolver'; import {EntityCrudRepository} from './repository'; +import * as utils from 'util'; +import {InclusionHandler} from './inclusion'; export namespace juggler { export import DataSource = legacy.DataSource; @@ -80,6 +82,7 @@ export function ensurePromise(p: legacy.PromiseOrVoid): Promise { export class DefaultCrudRepository implements EntityCrudRepository { modelClass: juggler.PersistedModelClass; + _inclusionHandler: InclusionHandler; /** * Constructor of DefaultCrudRepository @@ -103,6 +106,7 @@ export class DefaultCrudRepository ); this.setupPersistedModel(definition); + this._inclusionHandler = new InclusionHandler(this); } // Create an internal legacy Model attached to the datasource @@ -252,13 +256,39 @@ export class DefaultCrudRepository } async findById(id: ID, filter?: Filter, options?: Options): Promise { + // advanced discussion: cache the related items + const relatedItems = {} as AnyObject; + if (filter && filter.include) { + for (let i of filter.include) { + relatedItems[i.relation] = await this._fetchIncludedItems( + i.relation, + [id], + i.scope, + ); + } + delete filter.include; + } const model = await ensurePromise( this.modelClass.findById(id, filter as legacy.Filter, options), ); if (!model) { throw new EntityNotFoundError(this.entityClass, id); } - return this.toEntity(model); + return Object.assign(this.toEntity(model), relatedItems); + } + + async _fetchIncludedItems( + relation: string, + ids: ID[], + filter?: Filter, + ) { + const handler = this._inclusionHandler.findHandler(relation); + if (!handler) { + throw new Error('Fetch included items is not supported'); + } + const includedItems = await handler(ids, filter); + return includedItems; + } } update(entity: T, options?: Options): Promise { diff --git a/packages/repository/test/acceptance/belongs-to.relation.acceptance.ts b/packages/repository/test/acceptance/belongs-to.relation.acceptance.ts index 894414f75530..aab8afaf8148 100644 --- a/packages/repository/test/acceptance/belongs-to.relation.acceptance.ts +++ b/packages/repository/test/acceptance/belongs-to.relation.acceptance.ts @@ -39,6 +39,18 @@ describe('BelongsTo relation', () => { expect(result).to.deepEqual(customer); }); + it('can find order includes customer', async () => { + const customer = await customerRepo.create({name: 'Order McForder'}); + const order = await orderRepo.create({ + customerId: customer.id, + description: 'Order from Order McForder, the hoarder of Mordor', + }); + const result = await controller.findOrderIncludesCustomer(order.id); + // The code won't work since Order model doesn't have a property called customer + // expect(result.customer.length).to.equal(1); + // expect(result.customer[0]).to.deepEqual(customer); + }); + //--- HELPERS ---// class OrderController { @@ -49,6 +61,10 @@ describe('BelongsTo relation', () => { async findOwnerOfOrder(orderId: string) { return await this.orderRepository.customer(orderId); } + async findOrderIncludesCustomer(orderId: string) { + const inclusionFilter = {include: [{relation: 'customer'}]}; + return await this.orderRepository.findById(orderId, inclusionFilter); + } } function givenApplicationWithMemoryDB() { diff --git a/packages/repository/test/acceptance/has-many.relation.acceptance.ts b/packages/repository/test/acceptance/has-many.relation.acceptance.ts index 00214ac9dbc2..f93210a2d0ee 100644 --- a/packages/repository/test/acceptance/has-many.relation.acceptance.ts +++ b/packages/repository/test/acceptance/has-many.relation.acceptance.ts @@ -69,6 +69,23 @@ describe('HasMany relation', () => { expect(persisted).to.deepEqual(foundOrders); }); + it('can find related instances with include filter', async () => { + const order = await controller.createCustomerOrders(existingCustomerId, { + description: 'order 1', + }); + const notMyOrder = await controller.createCustomerOrders( + existingCustomerId + 1, + { + description: 'order 2', + }, + ); + const foundCustomer = await controller.findCustomerIncludesOrders( + existingCustomerId, + ); + expect(foundCustomer.orders.length).to.equal(1); + expect(foundCustomer.orders[0]).to.containEql(order); + }); + it('can patch many instances', async () => { await controller.createCustomerOrders(existingCustomerId, { description: 'order 1', @@ -166,6 +183,14 @@ describe('HasMany relation', () => { async deleteCustomerOrders(customerId: number) { return await this.customerRepository.orders(customerId).delete(); } + + async findCustomerIncludesOrders(customerId: number) { + const inclusionFilter = {include: [{relation: 'orders'}]}; + return await this.customerRepository.findById( + customerId, + inclusionFilter, + ); + } } function givenApplicationWithMemoryDB() { diff --git a/packages/repository/test/fixtures/repositories/customer.repository.ts b/packages/repository/test/fixtures/repositories/customer.repository.ts index bd19f9e7b213..ffc107062794 100644 --- a/packages/repository/test/fixtures/repositories/customer.repository.ts +++ b/packages/repository/test/fixtures/repositories/customer.repository.ts @@ -43,5 +43,9 @@ export class CustomerRepository extends DefaultCrudRepository< 'address', addressRepositoryGetter, ); + this._inclusionHandler.registerHandler( + 'orders', + orderRepositoryGetter, + ); } } diff --git a/packages/repository/test/fixtures/repositories/order.repository.ts b/packages/repository/test/fixtures/repositories/order.repository.ts index b8e9e2bd8fc2..78d9df47e3d4 100644 --- a/packages/repository/test/fixtures/repositories/order.repository.ts +++ b/packages/repository/test/fixtures/repositories/order.repository.ts @@ -32,5 +32,9 @@ export class OrderRepository extends DefaultCrudRepository< 'customer', customerRepositoryGetter, ); + this._inclusionHandler.registerHandler< + Customer, + typeof Customer.prototype.id + >('customer', customerRepositoryGetter); } } From 4454686b90be56ee1fbce5ada9c6085f01ef1c97 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Fri, 7 Dec 2018 22:07:15 -0500 Subject: [PATCH 2/2] fix: error --- examples/todo-list/src/controllers/todo-list.controller.ts | 1 - packages/repository/src/repositories/legacy-juggler-bridge.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/todo-list/src/controllers/todo-list.controller.ts b/examples/todo-list/src/controllers/todo-list.controller.ts index 998da5621956..1607bab7f28d 100644 --- a/examples/todo-list/src/controllers/todo-list.controller.ts +++ b/examples/todo-list/src/controllers/todo-list.controller.ts @@ -93,7 +93,6 @@ export class TodoListController { }, }) async findById( - return await this.todoListRepository.findById(id); @param.path.number('id') id: number, @param.query.object('filter') filter?: Filter, ): Promise { diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index ac6536c17e9b..394bd047a279 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -260,11 +260,12 @@ export class DefaultCrudRepository const relatedItems = {} as AnyObject; if (filter && filter.include) { for (let i of filter.include) { - relatedItems[i.relation] = await this._fetchIncludedItems( + const results = await this._fetchIncludedItems( i.relation, [id], i.scope, ); + if (results) relatedItems[i.relation] = results; } delete filter.include; } @@ -289,7 +290,6 @@ export class DefaultCrudRepository const includedItems = await handler(ids, filter); return includedItems; } - } update(entity: T, options?: Options): Promise { return this.updateById(entity.getId(), entity, options);