From 43baf2f8181ea4e026470adbc9eb1109df486061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 14 Mar 2019 10:09:54 +0100 Subject: [PATCH] feat: typescript API and example impl for inclusion of related models --- .../acceptance/todo-list.acceptance.ts | 25 ++++++++++++++-- .../__tests__/acceptance/todo.acceptance.ts | 14 +++++++++ .../src/models/todo-list-image.model.ts | 8 +++-- .../todo-list/src/models/todo-list.model.ts | 9 ++++-- examples/todo-list/src/models/todo.model.ts | 6 +++- .../src/repositories/todo-list.repository.ts | 30 +++++++++++++++++-- .../src/repositories/todo.repository.ts | 30 +++++++++++++++++-- packages/repository/src/model.ts | 6 ++++ .../src/repositories/legacy-juggler-bridge.ts | 26 +++++++++++----- .../repository/src/repositories/repository.ts | 22 +++++++++----- 10 files changed, 149 insertions(+), 27 deletions(-) diff --git a/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts b/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts index 867f54b99198..38691e5f0323 100644 --- a/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts +++ b/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts @@ -12,9 +12,9 @@ import { toJSON, } from '@loopback/testlab'; import {TodoListApplication} from '../../application'; -import {TodoList} from '../../models/'; -import {TodoListRepository} from '../../repositories/'; -import {givenTodoList} from '../helpers'; +import {Todo, TodoList} from '../../models'; +import {TodoListRepository, TodoRepository} from '../../repositories'; +import {givenTodo, givenTodoList} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; @@ -178,6 +178,20 @@ describe('TodoListApplication', () => { .expect(200, [toJSON(listInBlack)]); }); + it('includes Todos in query result', async () => { + const list = await givenTodoListInstance(); + const todo = await givenTodoInstance({todoListId: list.id}); + + const response = await client.get('/todo-lists').query({ + filter: JSON.stringify({include: [{relation: 'todos'}]}), + }); + expect(response.body).to.have.length(1); + expect(response.body[0]).to.deepEqual({ + ...toJSON(list), + todos: [toJSON(todo)], + }); + }); + /* ============================================================================ TEST HELPERS @@ -218,6 +232,11 @@ describe('TodoListApplication', () => { return await todoListRepo.create(givenTodoList(todoList)); } + async function givenTodoInstance(todo?: Partial) { + const repo = await app.getRepository(TodoRepository); + return await repo.create(givenTodo(todo)); + } + function givenMutlipleTodoListInstances() { return Promise.all([ givenTodoListInstance(), diff --git a/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts b/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts index 61d37703f81d..326071d88551 100644 --- a/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts +++ b/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts @@ -149,6 +149,20 @@ describe('TodoListApplication', () => { .expect(200, [toJSON(todoInProgress)]); }); + it('includes TodoList in query result', async () => { + const list = await givenTodoListInstance(); + const todo = await givenTodoInstance({todoListId: list.id}); + + const response = await client.get('/todos').query({ + filter: JSON.stringify({include: [{relation: 'todoList'}]}), + }); + expect(response.body).to.have.length(1); + expect(response.body[0]).to.deepEqual({ + ...toJSON(todo), + todoList: toJSON(list), + }); + }); + /* ============================================================================ TEST HELPERS diff --git a/examples/todo-list/src/models/todo-list-image.model.ts b/examples/todo-list/src/models/todo-list-image.model.ts index 92c5077cec8a..7b174f0a6864 100644 --- a/examples/todo-list/src/models/todo-list-image.model.ts +++ b/examples/todo-list/src/models/todo-list-image.model.ts @@ -3,8 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, model, property, belongsTo} from '@loopback/repository'; -import {TodoList} from './todo-list.model'; +import {belongsTo, Entity, model, property} from '@loopback/repository'; +import {TodoList, TodoListLinks} from './todo-list.model'; @model() export class TodoListImage extends Entity { @@ -29,3 +29,7 @@ export class TodoListImage extends Entity { super(data); } } + +export interface TodoListImageLinks { + todoList?: TodoList & TodoListLinks; +} diff --git a/examples/todo-list/src/models/todo-list.model.ts b/examples/todo-list/src/models/todo-list.model.ts index bdb1eb8d1382..efae1f7034bf 100644 --- a/examples/todo-list/src/models/todo-list.model.ts +++ b/examples/todo-list/src/models/todo-list.model.ts @@ -3,9 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Entity, model, property, hasMany, hasOne} from '@loopback/repository'; +import {Entity, hasMany, hasOne, model, property} from '@loopback/repository'; +import {TodoListImage, TodoListImageLinks} from './todo-list-image.model'; import {Todo} from './todo.model'; -import {TodoListImage} from './todo-list-image.model'; @model() export class TodoList extends Entity { @@ -36,3 +36,8 @@ export class TodoList extends Entity { super(data); } } + +export interface TodoListLinks { + todos?: (Todo & TodoListLinks)[]; + image?: TodoListImage & TodoListImageLinks; +} diff --git a/examples/todo-list/src/models/todo.model.ts b/examples/todo-list/src/models/todo.model.ts index bc30ce80def0..083657367c4a 100644 --- a/examples/todo-list/src/models/todo.model.ts +++ b/examples/todo-list/src/models/todo.model.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {Entity, property, model, belongsTo} from '@loopback/repository'; -import {TodoList} from './todo-list.model'; +import {TodoList, TodoListLinks} from './todo-list.model'; @model() export class Todo extends Entity { @@ -41,3 +41,7 @@ export class Todo extends Entity { super(data); } } + +export interface TodoLinks { + todoList?: TodoList & TodoListLinks; +} diff --git a/examples/todo-list/src/repositories/todo-list.repository.ts b/examples/todo-list/src/repositories/todo-list.repository.ts index 4b1d63fe8f71..298ff23050c1 100644 --- a/examples/todo-list/src/repositories/todo-list.repository.ts +++ b/examples/todo-list/src/repositories/todo-list.repository.ts @@ -10,14 +10,17 @@ import { juggler, repository, HasOneRepositoryFactory, + Filter, + Options, } from '@loopback/repository'; -import {Todo, TodoList, TodoListImage} from '../models'; +import {Todo, TodoList, TodoListImage, TodoListLinks} from '../models'; import {TodoRepository} from './todo.repository'; import {TodoListImageRepository} from './todo-list-image.repository'; export class TodoListRepository extends DefaultCrudRepository< TodoList, - typeof TodoList.prototype.id + typeof TodoList.prototype.id, + TodoListLinks > { public readonly todos: HasManyRepositoryFactory< Todo, @@ -49,4 +52,27 @@ export class TodoListRepository extends DefaultCrudRepository< public findByTitle(title: string) { return this.findOne({where: {title}}); } + + async find( + filter?: Filter, + options?: Options, + ): Promise<(TodoList & Partial)[]> { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = filter && Object.assign(filter, {include: undefined}); + + const result = await super.find(filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todos in fewer DB queries + if (include && include.length && include[0].relation === 'todos') { + await Promise.all( + result.map(async r => { + r.todos = await this.todos(r.id).find(); + }), + ); + } + return result; + } } diff --git a/examples/todo-list/src/repositories/todo.repository.ts b/examples/todo-list/src/repositories/todo.repository.ts index 4f26693b602c..46800015a808 100644 --- a/examples/todo-list/src/repositories/todo.repository.ts +++ b/examples/todo-list/src/repositories/todo.repository.ts @@ -9,13 +9,16 @@ import { DefaultCrudRepository, juggler, repository, + Options, + Filter, } from '@loopback/repository'; -import {Todo, TodoList} from '../models'; +import {Todo, TodoList, TodoLinks} from '../models'; import {TodoListRepository} from './todo-list.repository'; export class TodoRepository extends DefaultCrudRepository< Todo, - typeof Todo.prototype.id + typeof Todo.prototype.id, + TodoLinks > { public readonly todoList: BelongsToAccessor< TodoList, @@ -34,4 +37,27 @@ export class TodoRepository extends DefaultCrudRepository< todoListRepositoryGetter, ); } + + async find( + filter?: Filter, + options?: Options, + ): Promise<(Todo & Partial)[]> { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = filter && Object.assign(filter, {include: undefined}); + + const result = await super.find(filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todos in fewer DB queries + if (include && include.length && include[0].relation === 'todoList') { + await Promise.all( + result.map(async r => { + r.todoList = await this.todoList(r.id); + }), + ); + } + return result; + } } diff --git a/packages/repository/src/model.ts b/packages/repository/src/model.ts index 8586f9375644..300d3cb428d0 100644 --- a/packages/repository/src/model.ts +++ b/packages/repository/src/model.ts @@ -209,6 +209,12 @@ export abstract class Model { json[p] = asJSON((this as AnyObject)[p]); } } + // Include relational links in the JSON representation + for (const r in def.relations) { + if (r in this) { + json[r] = asJSON((this as AnyObject)[r]); + } + } return json; } diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index bd522d8e5392..12fd567e3530 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -88,8 +88,11 @@ export function ensurePromise(p: legacy.PromiseOrVoid): Promise { * Default implementation of CRUD repository using legacy juggler model * and data source */ -export class DefaultCrudRepository - implements EntityCrudRepository { +export class DefaultCrudRepository< + T extends Entity, + ID, + Links extends object = {} +> implements EntityCrudRepository { modelClass: juggler.PersistedModelClass; /** @@ -334,7 +337,10 @@ export class DefaultCrudRepository } } - async find(filter?: Filter, options?: Options): Promise { + async find( + filter?: Filter, + options?: Options, + ): Promise<(T & Partial)[]> { const models = await ensurePromise( this.modelClass.find(filter as legacy.Filter, options), ); @@ -349,14 +355,18 @@ export class DefaultCrudRepository return this.toEntity(model); } - async findById(id: ID, filter?: Filter, options?: Options): Promise { + async findById( + id: ID, + filter?: Filter, + options?: Options, + ): Promise> { 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 this.toEntity>(model); } update(entity: T, options?: Options): Promise { @@ -441,11 +451,11 @@ export class DefaultCrudRepository throw new Error('Not implemented'); } - protected toEntity(model: juggler.PersistedModel): T { - return new this.entityClass(model.toObject()) as T; + protected toEntity(model: juggler.PersistedModel): R { + return new this.entityClass(model.toObject()) as R; } - protected toEntities(models: juggler.PersistedModel[]): T[] { + protected toEntities(models: juggler.PersistedModel[]): R[] { return models.map(m => this.toEntity(m)); } } diff --git a/packages/repository/src/repositories/repository.ts b/packages/repository/src/repositories/repository.ts index 1f8a36d9ea49..f15a946cbb06 100644 --- a/packages/repository/src/repositories/repository.ts +++ b/packages/repository/src/repositories/repository.ts @@ -40,8 +40,10 @@ export interface ExecutableRepository extends Repository { /** * Basic CRUD operations for ValueObject and Entity. No ID is required. */ -export interface CrudRepository - extends Repository { +export interface CrudRepository< + T extends ValueObject | Entity, + Links extends object = {} +> extends Repository { /** * Create a new record * @param dataObject The data to be created @@ -64,7 +66,7 @@ export interface CrudRepository * @param options Options for the operations * @returns A promise of an array of records found */ - find(filter?: Filter, options?: Options): Promise; + find(filter?: Filter, options?: Options): Promise<(T & Partial)[]>; /** * Updating matching records with attributes from the data object @@ -105,9 +107,11 @@ export interface EntityRepository /** * CRUD operations for a repository of entities */ -export interface EntityCrudRepository - extends EntityRepository, - CrudRepository { +export interface EntityCrudRepository< + T extends Entity, + ID, + Links extends object = {} +> extends EntityRepository, CrudRepository { // entityClass should have type "typeof T", but that's not supported by TSC entityClass: typeof Entity & {prototype: T}; @@ -146,7 +150,11 @@ export interface EntityCrudRepository * @param options Options for the operations * @returns A promise of an entity found for the id */ - findById(id: ID, filter?: Filter, options?: Options): Promise; + findById( + id: ID, + filter?: Filter, + options?: Options, + ): Promise>; /** * Update an entity by id with property/value pairs in the data object