From 976334cdb439f83c944b37b1c6428900023cc763 Mon Sep 17 00:00:00 2001 From: jannyHou Date: Thu, 29 Nov 2018 13:20:22 -0500 Subject: [PATCH] feat: poc --- .../src/controllers/todo-list.controller.ts | 14 ++++++- .../src/repositories/todo-list.repository.ts | 12 ++++++ packages/repository/src/query.ts | 3 ++ .../repository/src/repositories/inclusion.ts | 38 +++++++++++++++++++ packages/repository/src/repositories/index.ts | 1 + .../src/repositories/legacy-juggler-bridge.ts | 38 +++++++++++++++++++ 6 files changed, 104 insertions(+), 2 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..b05b9c38d9c0 100644 --- a/examples/todo-list/src/controllers/todo-list.controller.ts +++ b/examples/todo-list/src/controllers/todo-list.controller.ts @@ -9,6 +9,7 @@ import { Filter, repository, Where, + AnyObject, } from '@loopback/repository'; import { del, @@ -92,8 +93,17 @@ export class TodoListController { }, }, }) - async findById(@param.path.number('id') id: number): Promise { - return await this.todoListRepository.findById(id); + async findById( + @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/repositories/todo-list.repository.ts b/examples/todo-list/src/repositories/todo-list.repository.ts index 6d89346d5e01..5d088b61c873 100644 --- a/examples/todo-list/src/repositories/todo-list.repository.ts +++ b/examples/todo-list/src/repositories/todo-list.repository.ts @@ -12,6 +12,7 @@ import { } from '@loopback/repository'; import {Todo, TodoList} from '../models'; import {TodoRepository} from './todo.repository'; +import {InclusionHandlerFactory} from '@loopback/repository'; export class TodoListRepository extends DefaultCrudRepository< TodoList, @@ -32,6 +33,17 @@ export class TodoListRepository extends DefaultCrudRepository< 'todos', todoRepositoryGetter, ); + + // to save time the fk name `todoListId` is provided here as a parameter, + // but should be calculated in the inclusion handler factory, + // details see my comment in the inclusion.ts file + + const todoHandler = InclusionHandlerFactory< + Todo, + typeof Todo.prototype.id, + typeof TodoList.prototype.id + >(todoRepositoryGetter, 'todoListId'); + this._inclusionHandler.todos = todoHandler; } public findByTitle(title: string) { diff --git a/packages/repository/src/query.ts b/packages/repository/src/query.ts index a239b7d2548d..591790b60084 100644 --- a/packages/repository/src/query.ts +++ b/packages/repository/src/query.ts @@ -162,6 +162,9 @@ export type Fields = {[P in keyof MT]?: boolean}; * Example: * `{relation: 'aRelationName', scope: {}}` */ + +// The entity type provided for the scope filter is the source model +// while it should be the target(related) model export interface Inclusion { relation: string; scope?: Filter; diff --git a/packages/repository/src/repositories/inclusion.ts b/packages/repository/src/repositories/inclusion.ts new file mode 100644 index 000000000000..5b7531acd30b --- /dev/null +++ b/packages/repository/src/repositories/inclusion.ts @@ -0,0 +1,38 @@ +// RT means related repository's type +// T +// ID means source model's id type +import {Entity} from '../model'; +import {Getter, inject} from '@loopback/core'; +import {Filter} from '../'; +import {AnyObject} from '..'; +import {DefaultCrudRepository} from './legacy-juggler-bridge'; +import {inspect} from 'util'; + +// TE: the target entity +// TID: the ID of target entity +// SID: the ID of source entity +export function InclusionHandlerFactory( + targetRepoGetter: Getter>, + // to support composed fk, eventually the fkName should be an array + // fkNames: string[] + fkName: string, +) { + return async function inclusionHandler( + fk: SID, + // There are two concerns of the Filter interface + // 1st: the Filter interface doesn't provide the related model's + // type for the inclusion filter, see my comment in query.ts + // 2nd: when apply the fk constraint on `where`, it doesn't recognize + // the fkName as a property defined in the `where` filter, + // but an arbitrary string instead. As a workaround I used + // a loose type: Filter + filter?: Filter, + ): Promise { + const targetRepo = await targetRepoGetter(); + filter = filter || {}; + filter.where = filter.where || {}; + filter.where[fkName] = fk; + // console.log(`inclusion filter: ${inspect(filter)}`); + return await targetRepo.find(filter); + }; +} 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 73a095bac6dc..a65b86ee0efd 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -28,6 +28,7 @@ import { } from '../relations'; import {resolveType} from '../type-resolver'; import {EntityCrudRepository} from './repository'; +import * as utils from 'util'; export namespace juggler { export import DataSource = legacy.DataSource; @@ -77,6 +78,7 @@ export function ensurePromise(p: legacy.PromiseOrVoid): Promise { export class DefaultCrudRepository implements EntityCrudRepository { modelClass: juggler.PersistedModelClass; + _inclusionHandler: {[relation: string]: Function} = {}; /** * Constructor of DefaultCrudRepository @@ -234,15 +236,51 @@ 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); } + // console.log(`related items: ${utils.inspect(relatedItems, {depth: 3})}`); + // console.log(`model: ${utils.inspect(model, {depth: 3})}`); + + Object.assign(model, relatedItems); return this.toEntity(model); } + async _fetchIncludedItems( + relation: string, + id: ID, + filter?: Filter, + ) { + const handler = this._inclusionHandler[relation]; + if (!handler) { + throw new Error('Fetch included items is not supported'); + } + const includedItems = await handler(id, filter); + console.log( + `legacy-juggler-bridge fetch includedItems : ${utils.inspect( + includedItems, + {depth: 3}, + )}`, + ); + return includedItems; + } + update(entity: T, options?: Options): Promise { return this.updateById(entity.getId(), entity, options); }