diff --git a/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md b/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md index f197b0381448..367e174982eb 100644 --- a/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md +++ b/docs/site/tutorials/todo-list/todo-list-tutorial-controller.md @@ -38,6 +38,89 @@ Controller TodoList was created in src/controllers/ And voilĂ ! We now have a set of basic APIs for todo-lists, just like that! +#### Inclusion of Related Models + +In order to get our related `Todo`s for each `TodoList`, let's update the +`schema`. + +In `src/models/todo-list.controller.ts`, first import `getModelSchemaRef` from +`@loopback/rest`. + +Then update the following `schema`s in `responses`'s `content`: + +{% include code-caption.html content="src/models/todo-list.controller.ts" %} + +```ts +@get('/todo-lists', { + responses: { + '200': { + description: 'Array of TodoList model instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(TodoList, {includeRelations: true}), + }, + }, + }, + }, + }, +}) +async find(/*...*/) {/*...*/} + +@get('/todo-lists/{id}', { + responses: { + '200': { + description: 'TodoList model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(TodoList, {includeRelations: true}), + }, + }, + }, + }, +}) +async findById(/*...*/) {/*...*/} +``` + +Let's also update it in the `TodoController`: + +{% include code-caption.html content="src/models/todo.controller.ts" %} + +```ts +@get('/todos', { + responses: { + '200': { + description: 'Array of Todo model instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(Todo, {includeRelations: true}), + }, + }, + }, + }, + }, +}) +}) +async findTodos(/*...*/) {/*...*/} + +@get('/todos/{id}', { + responses: { + '200': { + description: 'Todo model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(Todo, {includeRelations: true}), + }, + }, + }, + }, +}) +async findTodoById(/*...*/) {/*...*/} +``` + ### Create TodoList's Todo controller For the controller handling `Todos` of a `TodoList`, we'll start with an empty @@ -196,8 +279,8 @@ export class TodoListTodoController { } ``` -Check out our todo-list example to see the full source code generated for -TodoListTodo controller: +Check out our `TodoList` example to see the full source code generated for the +`TodoListTodo` controller: [src/controllers/todo-list-todo.controller.ts](https://github.com/strongloop/loopback-next/blob/master/examples/todo-list/src/controllers/todo-list-todo.controller.ts) ### Try it out diff --git a/docs/site/tutorials/todo-list/todo-list-tutorial-model.md b/docs/site/tutorials/todo-list/todo-list-tutorial-model.md index a92774da5feb..fb380e3e4cc4 100644 --- a/docs/site/tutorials/todo-list/todo-list-tutorial-model.md +++ b/docs/site/tutorials/todo-list/todo-list-tutorial-model.md @@ -71,7 +71,8 @@ Model TodoList was created in src/models/ ``` Now that we have our new model, we need to define its relation with the `Todo` -model. Add the following import statements and property to the `TodoList` model: +model. Add the following import statements and property to the `TodoList` model +and update the `TodoListRelations` interface to include `todos`: {% include code-caption.html content="src/models/todo-list.model.ts" %} @@ -101,7 +102,8 @@ suggests, `@hasMany()` informs LoopBack 4 that a todo list can have many todo items. To complement `TodoList`'s relationship to `Todo`, we'll add in the `todoListId` -property on the `Todo` model to define the relation on both ends: +property on the `Todo` model to define the relation on both ends, along with +updating the `TodoRelations` interface to include `todoList`: {% include code-caption.html content="src/models/todo.model.ts" %} diff --git a/docs/site/tutorials/todo-list/todo-list-tutorial-repository.md b/docs/site/tutorials/todo-list/todo-list-tutorial-repository.md index 3a9aa67afa1d..d4d95f3f23a5 100644 --- a/docs/site/tutorials/todo-list/todo-list-tutorial-repository.md +++ b/docs/site/tutorials/todo-list/todo-list-tutorial-repository.md @@ -85,6 +85,145 @@ export class TodoListRepository extends DefaultCrudRepository< } ``` +### Inclusion of Related Models + +To get the related `Todo` object for each `TodoList`, we have to override the +`find` and `findById` functions. + +First add the following imports: + +```ts +import {Filter, Options} from '@loopback/repository'; +import {TodoListWithRelations} from '../models'; +``` + +Add the following two functions after the constructor: + +{% include code-caption.html content="src/repositories/todo-list.repository.ts" %} + +```ts +async find( + filter?: Filter, + options?: Options, +): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...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 + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + 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; +} + +async findById( + id: typeof TodoList.prototype.id, + filter?: Filter, + options?: Options, +): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...filter, include: undefined}; + + const result = await super.findById(id, filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todos in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todos') { + result.todos = await this.todos(result.id).find(); + } + + return result; +} +``` + +Now when you get a `TodoList`, a `todos` property will be included that contains +your related `Todo`s, for example: + +```json +{ + "id": 2, + "title": "My daily chores", + "todos": [ + { + "id": 3, + "title": "play space invaders", + "desc": "Become the very best!", + "todoListId": 2 + } + ] +} +``` + +Let's do the same on the `TodoRepository`: + +{% include code-caption.html content="src/repositories/todo.repository.ts" %} + +```ts +async find( + filter?: Filter, + options?: Options, +): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...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 todo-lists in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todoList') { + await Promise.all( + result.map(async r => { + r.todoList = await this.todoList(r.id); + }), + ); + } + + return result; +} + +async findById( + id: typeof Todo.prototype.id, + filter?: Filter, + options?: Options, +): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...filter, include: undefined}; + + const result = await super.findById(id, filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todo-lists in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todoList') { + result.todoList = await this.todoList(result.id); + } + + return result; +} +``` + We're now ready to expose `TodoList` and its related `Todo` API through the [controller](todo-list-tutorial-controller.md). diff --git a/examples/todo-list/data/db.json b/examples/todo-list/data/db.json index 61799b6d3cc3..39569fd06d05 100644 --- a/examples/todo-list/data/db.json +++ b/examples/todo-list/data/db.json @@ -12,7 +12,7 @@ }, "TodoList": { "1": "{\"title\":\"Sith lord's check list\",\"lastModified\":\"a long time ago\",\"id\":1}", - "2": "{\"title\":\"My daily chores\",,\"lastModified\":\"2018-07-13\",\"id\":2}" + "2": "{\"title\":\"My daily chores\",\"lastModified\":\"2018-07-13\",\"id\":2}" } } } diff --git a/examples/todo-list/package.json b/examples/todo-list/package.json index 31ac987f0013..c81135c744c6 100644 --- a/examples/todo-list/package.json +++ b/examples/todo-list/package.json @@ -50,6 +50,7 @@ "@loopback/build": "^2.0.1", "@loopback/eslint-config": "^1.1.2", "@loopback/http-caching-proxy": "^1.1.3", + "@loopback/repository": "^1.8.0", "@loopback/testlab": "^1.6.1", "@types/lodash": "^4.14.134", "@types/node": "^10.14.9", diff --git a/examples/todo-list/src/__tests__/acceptance/todo-list-image.acceptance.ts b/examples/todo-list/src/__tests__/acceptance/todo-list-image.acceptance.ts index 07eb01a8cd89..1a725756015f 100644 --- a/examples/todo-list/src/__tests__/acceptance/todo-list-image.acceptance.ts +++ b/examples/todo-list/src/__tests__/acceptance/todo-list-image.acceptance.ts @@ -3,31 +3,33 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - Client, - createRestAppClient, - expect, - givenHttpServerConfig, - toJSON, -} from '@loopback/testlab'; +import {Client, createRestAppClient, expect, toJSON} from '@loopback/testlab'; import {TodoListApplication} from '../../application'; import {TodoList, TodoListImage} from '../../models/'; -import {TodoListRepository, TodoListImageRepository} from '../../repositories/'; -import {givenTodoListImage, givenTodoList} from '../helpers'; +import {TodoListImageRepository, TodoListRepository} from '../../repositories/'; +import { + givenRunningApplicationWithCustomConfiguration, + givenTodoListImage, + givenTodoListInstance, + givenTodoListRepositories, +} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; let client: Client; - let todoListImageRepo: TodoListImageRepository; let todoListRepo: TodoListRepository; + let todoListImageRepo: TodoListImageRepository; let persistedTodoList: TodoList; - before(givenRunningApplicationWithCustomConfiguration); + before(async () => { + app = await givenRunningApplicationWithCustomConfiguration(); + }); after(() => app.stop()); - before(givenTodoListImageRepository); - before(givenTodoListRepository); + before(async () => { + ({todoListRepo, todoListImageRepo} = await givenTodoListRepositories(app)); + }); before(() => { client = createRestAppClient(app); }); @@ -38,7 +40,7 @@ describe('TodoListApplication', () => { }); beforeEach(async () => { - persistedTodoList = await givenTodoListInstance(); + persistedTodoList = await givenTodoListInstance(todoListRepo); }); it('creates image for a todoList', async () => { @@ -84,34 +86,6 @@ describe('TodoListApplication', () => { ============================================================================ */ - async function givenRunningApplicationWithCustomConfiguration() { - app = new TodoListApplication({ - rest: givenHttpServerConfig(), - }); - - await app.boot(); - - /** - * Override default config for DataSource for testing so we don't write - * test data to file when using the memory connector. - */ - app.bind('datasources.config.db').to({ - name: 'db', - connector: 'memory', - }); - - // Start Application - await app.start(); - } - - async function givenTodoListImageRepository() { - todoListImageRepo = await app.getRepository(TodoListImageRepository); - } - - async function givenTodoListRepository() { - todoListRepo = await app.getRepository(TodoListRepository); - } - async function givenTodoListImageInstanceOfTodoList( id: typeof TodoList.prototype.id, todoListImage?: Partial, @@ -120,8 +94,4 @@ describe('TodoListApplication', () => { delete data.todoListId; return await todoListRepo.image(id).create(data); } - - async function givenTodoListInstance(todoList?: Partial) { - return await todoListRepo.create(givenTodoList(todoList)); - } }); 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..ddacc4ae3c38 100644 --- a/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts +++ b/examples/todo-list/src/__tests__/acceptance/todo-list.acceptance.ts @@ -4,27 +4,33 @@ // License text available at https://opensource.org/licenses/MIT import {EntityNotFoundError} from '@loopback/repository'; -import { - Client, - createRestAppClient, - expect, - givenHttpServerConfig, - toJSON, -} from '@loopback/testlab'; +import {Client, createRestAppClient, expect, toJSON} from '@loopback/testlab'; import {TodoListApplication} from '../../application'; import {TodoList} from '../../models/'; -import {TodoListRepository} from '../../repositories/'; -import {givenTodoList} from '../helpers'; +import {TodoListRepository, TodoRepository} from '../../repositories/'; +import { + givenRunningApplicationWithCustomConfiguration, + givenTodoInstance, + givenTodoList, + givenTodoListInstance, + givenTodoRepositories, +} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; let client: Client; + let todoRepo: TodoRepository; let todoListRepo: TodoListRepository; - before(givenRunningApplicationWithCustomConfiguration); + before(async () => { + app = await givenRunningApplicationWithCustomConfiguration(); + }); after(() => app.stop()); - before(givenTodoListRepository); + before(async () => { + ({todoRepo, todoListRepo} = await givenTodoRepositories(app)); + }); + before(() => { client = createRestAppClient(app); }); @@ -90,8 +96,14 @@ describe('TodoListApplication', () => { it('updates selected todoLists', async () => { await todoListRepo.deleteAll(); - await givenTodoListInstance({title: 'red-list', color: 'red'}); - await givenTodoListInstance({title: 'green-list', color: 'green'}); + await givenTodoListInstance(todoListRepo, { + title: 'red-list', + color: 'red', + }); + await givenTodoListInstance(todoListRepo, { + title: 'green-list', + color: 'green', + }); const response = await client .patch('/todo-lists') @@ -118,7 +130,7 @@ describe('TodoListApplication', () => { let persistedTodoList: TodoList; beforeEach(async () => { - persistedTodoList = await givenTodoListInstance(); + persistedTodoList = await givenTodoListInstance(todoListRepo); }); it('gets a todoList by ID', async () => { @@ -165,9 +177,9 @@ describe('TodoListApplication', () => { }); it('queries todo-lists with a filter', async () => { - await givenTodoListInstance({title: 'day', color: 'white'}); + await givenTodoListInstance(todoListRepo, {title: 'day', color: 'white'}); - const listInBlack = await givenTodoListInstance({ + const listInBlack = await givenTodoListInstance(todoListRepo, { title: 'night', color: 'black', }); @@ -178,6 +190,20 @@ describe('TodoListApplication', () => { .expect(200, [toJSON(listInBlack)]); }); + it('includes Todos in query result', async () => { + const list = await givenTodoListInstance(todoListRepo); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); + const filter = JSON.stringify({include: [{relation: 'todos'}]}); + + const response = await client.get('/todo-lists').query({filter: filter}); + + expect(response.body).to.have.length(1); + expect(response.body[0]).to.deepEqual({ + ...toJSON(list), + todos: [toJSON(todo)], + }); + }); + /* ============================================================================ TEST HELPERS @@ -190,38 +216,10 @@ describe('TodoListApplication', () => { ============================================================================ */ - async function givenRunningApplicationWithCustomConfiguration() { - app = new TodoListApplication({ - rest: givenHttpServerConfig(), - }); - - await app.boot(); - - /** - * Override default config for DataSource for testing so we don't write - * test data to file when using the memory connector. - */ - app.bind('datasources.config.db').to({ - name: 'db', - connector: 'memory', - }); - - // Start Application - await app.start(); - } - - async function givenTodoListRepository() { - todoListRepo = await app.getRepository(TodoListRepository); - } - - async function givenTodoListInstance(todoList?: Partial) { - return await todoListRepo.create(givenTodoList(todoList)); - } - function givenMutlipleTodoListInstances() { return Promise.all([ - givenTodoListInstance(), - givenTodoListInstance({title: 'so many things to do wow'}), + givenTodoListInstance(todoListRepo), + givenTodoListInstance(todoListRepo, {title: 'so many things to do wow'}), ]); } }); diff --git a/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts b/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts index 61d37703f81d..9b60bfc5497b 100644 --- a/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts +++ b/examples/todo-list/src/__tests__/acceptance/todo.acceptance.ts @@ -4,17 +4,17 @@ // License text available at https://opensource.org/licenses/MIT import {EntityNotFoundError} from '@loopback/repository'; -import { - Client, - createRestAppClient, - expect, - givenHttpServerConfig, - toJSON, -} from '@loopback/testlab'; +import {Client, createRestAppClient, expect, toJSON} from '@loopback/testlab'; import {TodoListApplication} from '../../application'; -import {Todo, TodoList} from '../../models/'; -import {TodoRepository, TodoListRepository} from '../../repositories/'; -import {givenTodo, givenTodoList} from '../helpers'; +import {Todo} from '../../models/'; +import {TodoListRepository, TodoRepository} from '../../repositories/'; +import { + givenRunningApplicationWithCustomConfiguration, + givenTodo, + givenTodoInstance, + givenTodoListInstance, + givenTodoRepositories, +} from '../helpers'; describe('TodoListApplication', () => { let app: TodoListApplication; @@ -22,10 +22,14 @@ describe('TodoListApplication', () => { let todoRepo: TodoRepository; let todoListRepo: TodoListRepository; - before(givenRunningApplicationWithCustomConfiguration); + before(async () => { + app = await givenRunningApplicationWithCustomConfiguration(); + }); after(() => app.stop()); - before(givenTodoRepositories); + before(async () => { + ({todoRepo, todoListRepo} = await givenTodoRepositories(app)); + }); before(() => { client = createRestAppClient(app); }); @@ -58,7 +62,7 @@ describe('TodoListApplication', () => { let persistedTodo: Todo; beforeEach(async () => { - persistedTodo = await givenTodoInstance(); + persistedTodo = await givenTodoInstance(todoRepo); }); it('gets a todo by ID', () => { @@ -128,17 +132,17 @@ describe('TodoListApplication', () => { }); it('returns the owning todo-list', async () => { - const list = await givenTodoListInstance(); - const todo = await givenTodoInstance({todoListId: list.id}); + const list = await givenTodoListInstance(todoListRepo); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); await client.get(`/todos/${todo.id}/todo-list`).expect(200, toJSON(list)); }); }); it('queries todos with a filter', async () => { - await givenTodoInstance({title: 'wake up', isComplete: true}); + await givenTodoInstance(todoRepo, {title: 'wake up', isComplete: true}); - const todoInProgress = await givenTodoInstance({ + const todoInProgress = await givenTodoInstance(todoRepo, { title: 'go to sleep', isComplete: false, }); @@ -149,48 +153,17 @@ describe('TodoListApplication', () => { .expect(200, [toJSON(todoInProgress)]); }); - /* - ============================================================================ - TEST HELPERS - These functions help simplify setup of your test fixtures so that your tests - can: - - operate on a "clean" environment each time (a fresh in-memory database) - - avoid polluting the test with large quantities of setup logic to keep - them clear and easy to read - - keep them DRY (who wants to write the same stuff over and over?) - ============================================================================ - */ - - async function givenRunningApplicationWithCustomConfiguration() { - app = new TodoListApplication({ - rest: givenHttpServerConfig(), - }); + it('includes TodoList in query result', async () => { + const list = await givenTodoListInstance(todoListRepo); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); + const filter = JSON.stringify({include: [{relation: 'todoList'}]}); - await app.boot(); + const response = await client.get('/todos').query({filter: filter}); - /** - * Override default config for DataSource for testing so we don't write - * test data to file when using the memory connector. - */ - app.bind('datasources.config.db').to({ - name: 'db', - connector: 'memory', + expect(response.body).to.have.length(1); + expect(response.body[0]).to.deepEqual({ + ...toJSON(todo), + todoList: toJSON(list), }); - - // Start Application - await app.start(); - } - - async function givenTodoRepositories() { - todoRepo = await app.getRepository(TodoRepository); - todoListRepo = await app.getRepository(TodoListRepository); - } - - async function givenTodoInstance(todo?: Partial) { - return await todoRepo.create(givenTodo(todo)); - } - - async function givenTodoListInstance(data?: Partial) { - return await todoListRepo.create(givenTodoList(data)); - } + }); }); diff --git a/examples/todo-list/src/__tests__/helpers.ts b/examples/todo-list/src/__tests__/helpers.ts index 1f81a61401ec..9b733d7424b4 100644 --- a/examples/todo-list/src/__tests__/helpers.ts +++ b/examples/todo-list/src/__tests__/helpers.ts @@ -3,7 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {juggler} from '@loopback/repository'; +import {givenHttpServerConfig} from '@loopback/testlab'; +import {TodoListApplication} from '../application'; import {Todo, TodoList, TodoListImage} from '../models'; +import { + TodoListImageRepository, + TodoListRepository, + TodoRepository, +} from '../repositories'; /* ============================================================================== @@ -69,3 +77,84 @@ export function givenTodoListImage(todoListImage?: Partial) { ); return new TodoListImage(data); } + +export async function givenRunningApplicationWithCustomConfiguration() { + const app = new TodoListApplication({ + rest: givenHttpServerConfig(), + }); + + await app.boot(); + + /** + * Override default config for DataSource for testing so we don't write + * test data to file when using the memory connector. + */ + app.bind('datasources.config.db').to({ + name: 'db', + connector: 'memory', + }); + + // Start Application + await app.start(); + return app; +} + +export async function givenTodoRepositories(app: TodoListApplication) { + const todoRepo = await app.getRepository(TodoRepository); + const todoListRepo = await app.getRepository(TodoListRepository); + return {todoRepo, todoListRepo}; +} + +export async function givenTodoListRepositories(app: TodoListApplication) { + const todoListRepo = await app.getRepository(TodoListRepository); + const todoListImageRepo = await app.getRepository(TodoListImageRepository); + return {todoListRepo, todoListImageRepo}; +} + +export async function givenTodoInstance( + todoRepo: TodoRepository, + todo?: Partial, +) { + return await todoRepo.create(givenTodo(todo)); +} + +export async function givenTodoListInstance( + todoListRepo: TodoListRepository, + data?: Partial, +) { + return await todoListRepo.create(givenTodoList(data)); +} + +export async function givenTodoListImageInstance( + todoListImageRepo: TodoListImageRepository, + data?: Partial, +) { + return await todoListImageRepo.create(givenTodoListImage(data)); +} + +export async function givenEmptyDatabase() { + const todoRepo: TodoRepository = new TodoRepository( + testdb, + async () => todoListRepo, + ); + + const todoListRepo: TodoListRepository = new TodoListRepository( + testdb, + async () => todoRepo, + async () => todoListImageRepo, + ); + + const todoListImageRepo: TodoListImageRepository = new TodoListImageRepository( + testdb, + async () => todoListRepo, + ); + + await todoRepo.deleteAll(); + await todoListRepo.deleteAll(); + await todoListImageRepo.deleteAll(); +} + +export const testdb: juggler.DataSource = new juggler.DataSource({ + name: 'db', + connector: 'memory', +}); diff --git a/examples/todo-list/src/__tests__/integration/todo-list-image.repository.integration.ts b/examples/todo-list/src/__tests__/integration/todo-list-image.repository.integration.ts new file mode 100644 index 000000000000..dfcfca861e8e --- /dev/null +++ b/examples/todo-list/src/__tests__/integration/todo-list-image.repository.integration.ts @@ -0,0 +1,66 @@ +import {expect, toJSON} from '@loopback/testlab'; +import { + TodoListImageRepository, + TodoListRepository, + TodoRepository, +} from '../../repositories'; +import { + givenEmptyDatabase, + givenTodoListImageInstance, + givenTodoListInstance, + testdb, +} from '../helpers'; + +describe('TodoListImageRepository', () => { + let todoListImageRepo: TodoListImageRepository; + let todoListRepo: TodoListRepository; + let todoRepo: TodoRepository; + + before(async () => { + todoListRepo = new TodoListRepository( + testdb, + async () => todoRepo, + async () => todoListImageRepo, + ); + todoListImageRepo = new TodoListImageRepository( + testdb, + async () => todoListRepo, + ); + }); + + beforeEach(givenEmptyDatabase); + + it('includes TodoList in find method result', async () => { + const list = await givenTodoListInstance(todoListRepo); + const image = await givenTodoListImageInstance(todoListImageRepo, { + todoListId: list.id, + }); + + const response = await todoListImageRepo.find({ + include: [{relation: 'todoList'}], + }); + + expect(toJSON(response)).to.deepEqual([ + { + ...toJSON(image), + todoList: toJSON(list), + }, + ]); + }); + + it('includes TodoList in findById result', async () => { + const list = await givenTodoListInstance(todoListRepo, {}); + const image = await givenTodoListImageInstance(todoListImageRepo, { + todoListId: list.id, + }); + + const response = await todoListImageRepo.findById(image.id, { + include: [{relation: 'todoList'}], + }); + + expect(toJSON(response)).to.deepEqual({ + ...toJSON(image), + todoList: toJSON(list), + }); + }); +}); diff --git a/examples/todo-list/src/__tests__/integration/todo-list.repository.integration.ts b/examples/todo-list/src/__tests__/integration/todo-list.repository.integration.ts new file mode 100644 index 000000000000..471e06c13d23 --- /dev/null +++ b/examples/todo-list/src/__tests__/integration/todo-list.repository.integration.ts @@ -0,0 +1,59 @@ +import {expect, toJSON} from '@loopback/testlab'; +import { + TodoListImageRepository, + TodoListRepository, + TodoRepository, +} from '../../repositories'; +import { + givenEmptyDatabase, + givenTodoInstance, + givenTodoListInstance, + testdb, +} from '../helpers'; + +describe('TodoListRepository', () => { + let todoListImageRepo: TodoListImageRepository; + let todoListRepo: TodoListRepository; + let todoRepo: TodoRepository; + + before(async () => { + todoListRepo = new TodoListRepository( + testdb, + async () => todoRepo, + async () => todoListImageRepo, + ); + todoRepo = new TodoRepository(testdb, async () => todoListRepo); + }); + + beforeEach(givenEmptyDatabase); + + it('includes Todos in find method result', async () => { + const list = await givenTodoListInstance(todoListRepo); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); + + const response = await todoListRepo.find({ + include: [{relation: 'todos'}], + }); + + expect(toJSON(response)).to.deepEqual([ + { + ...toJSON(list), + todos: [toJSON(todo)], + }, + ]); + }); + + it('includes Todos in findById result', async () => { + const list = await givenTodoListInstance(todoListRepo); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); + + const response = await todoListRepo.findById(list.id, { + include: [{relation: 'todos'}], + }); + + expect(toJSON(response)).to.deepEqual({ + ...toJSON(list), + todos: [toJSON(todo)], + }); + }); +}); diff --git a/examples/todo-list/src/__tests__/integration/todo.repository.integration.ts b/examples/todo-list/src/__tests__/integration/todo.repository.integration.ts new file mode 100644 index 000000000000..ca16f4965d0a --- /dev/null +++ b/examples/todo-list/src/__tests__/integration/todo.repository.integration.ts @@ -0,0 +1,59 @@ +import {expect, toJSON} from '@loopback/testlab'; +import { + TodoListImageRepository, + TodoListRepository, + TodoRepository, +} from '../../repositories'; +import { + givenEmptyDatabase, + givenTodoInstance, + givenTodoListInstance, + testdb, +} from '../helpers'; + +describe('TodoRepository', () => { + let todoListImageRepo: TodoListImageRepository; + let todoListRepo: TodoListRepository; + let todoRepo: TodoRepository; + + before(async () => { + todoListRepo = new TodoListRepository( + testdb, + async () => todoRepo, + async () => todoListImageRepo, + ); + todoRepo = new TodoRepository(testdb, async () => todoListRepo); + }); + + beforeEach(givenEmptyDatabase); + + it('includes TodoList in find method result', async () => { + const list = await givenTodoListInstance(todoListRepo); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); + + const response = await todoRepo.find({ + include: [{relation: 'todoList'}], + }); + + expect(toJSON(response)).to.deepEqual([ + { + ...toJSON(todo), + todoList: toJSON(list), + }, + ]); + }); + + it('includes TodoList in findById result', async () => { + const list = await givenTodoListInstance(todoListRepo, {}); + const todo = await givenTodoInstance(todoRepo, {todoListId: list.id}); + + const response = await todoRepo.findById(todo.id, { + include: [{relation: 'todoList'}], + }); + + expect(toJSON(response)).to.deepEqual({ + ...toJSON(todo), + todoList: toJSON(list), + }); + }); +}); diff --git a/examples/todo-list/src/controllers/todo-list-image.controller.ts b/examples/todo-list/src/controllers/todo-list-image.controller.ts index 300ea9760877..dc764934e3a7 100644 --- a/examples/todo-list/src/controllers/todo-list-image.controller.ts +++ b/examples/todo-list/src/controllers/todo-list-image.controller.ts @@ -3,10 +3,10 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {TodoListRepository} from '../repositories'; import {repository} from '@loopback/repository'; -import {param, post, requestBody, get} from '@loopback/rest'; +import {get, getModelSchemaRef, param, post, requestBody} from '@loopback/rest'; import {TodoListImage} from '../models'; +import {TodoListRepository} from '../repositories'; export class TodoListImageController { constructor( @@ -32,7 +32,11 @@ export class TodoListImageController { responses: { '200': { description: 'The image belonging to the TodoList', - content: {'application/json': {schema: {'x-ts-type': TodoListImage}}}, + content: { + 'application/json': { + schema: getModelSchemaRef(TodoListImage, {includeRelations: true}), + }, + }, }, }, }) diff --git a/examples/todo-list/src/controllers/todo-list.controller.ts b/examples/todo-list/src/controllers/todo-list.controller.ts index 7850d8b69e13..443a20ac553c 100644 --- a/examples/todo-list/src/controllers/todo-list.controller.ts +++ b/examples/todo-list/src/controllers/todo-list.controller.ts @@ -14,6 +14,7 @@ import { del, get, getFilterSchemaFor, + getModelSchemaRef, getWhereSchemaFor, param, patch, @@ -60,7 +61,14 @@ export class TodoListController { responses: { '200': { description: 'Array of TodoList model instances', - content: {'application/json': {schema: {'x-ts-type': TodoList}}}, + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(TodoList, {includeRelations: true}), + }, + }, + }, }, }, }) @@ -91,12 +99,20 @@ export class TodoListController { responses: { '200': { description: 'TodoList model instance', - content: {'application/json': {schema: {'x-ts-type': TodoList}}}, + content: { + 'application/json': { + schema: getModelSchemaRef(TodoList, {includeRelations: true}), + }, + }, }, }, }) - 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', getFilterSchemaFor(TodoList)) + filter?: Filter, + ): Promise { + return await this.todoListRepository.findById(id, filter); } @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 8f7ea6c5f1e4..1fc5934d2bf2 100644 --- a/examples/todo-list/src/controllers/todo.controller.ts +++ b/examples/todo-list/src/controllers/todo.controller.ts @@ -8,6 +8,7 @@ import { del, get, getFilterSchemaFor, + getModelSchemaRef, param, patch, post, @@ -36,15 +37,20 @@ export class TodoController { responses: { '200': { description: 'Todo model instance', - content: {'application/json': {schema: {'x-ts-type': Todo}}}, + content: { + 'application/json': { + schema: getModelSchemaRef(Todo, {includeRelations: true}), + }, + }, }, }, }) async findTodoById( @param.path.number('id') id: number, - @param.query.boolean('items') items?: boolean, + @param.query.object('filter', getFilterSchemaFor(Todo)) + filter?: Filter, ): Promise { - return await this.todoRepo.findById(id); + return await this.todoRepo.findById(id, filter); } @get('/todos', { @@ -53,7 +59,10 @@ export class TodoController { description: 'Array of Todo model instances', content: { 'application/json': { - schema: {type: 'array', items: {'x-ts-type': Todo}}, + schema: { + type: 'array', + items: getModelSchemaRef(Todo, {includeRelations: true}), + }, }, }, }, diff --git a/examples/todo-list/src/models/todo.model.ts b/examples/todo-list/src/models/todo.model.ts index 3848282311cd..0ef3ccdeabd4 100644 --- a/examples/todo-list/src/models/todo.model.ts +++ b/examples/todo-list/src/models/todo.model.ts @@ -43,7 +43,7 @@ export class Todo extends Entity { } export interface TodoRelations { - todoList?: TodoListWithRelations[]; + todoList?: TodoListWithRelations; } export type TodoWithRelations = Todo & TodoRelations; diff --git a/examples/todo-list/src/repositories/todo-list-image.repository.ts b/examples/todo-list/src/repositories/todo-list-image.repository.ts index 7fa13fb8041e..e8549a57d805 100644 --- a/examples/todo-list/src/repositories/todo-list-image.repository.ts +++ b/examples/todo-list/src/repositories/todo-list-image.repository.ts @@ -7,10 +7,17 @@ import {Getter, inject} from '@loopback/core'; import { BelongsToAccessor, DefaultCrudRepository, + Filter, + Options, repository, } from '@loopback/repository'; import {DbDataSource} from '../datasources'; -import {TodoList, TodoListImage, TodoListImageRelations} from '../models'; +import { + TodoList, + TodoListImage, + TodoListImageRelations, + TodoListImageWithRelations, +} from '../models'; import {TodoListRepository} from './todo-list.repository'; export class TodoListImageRepository extends DefaultCrudRepository< @@ -28,9 +35,58 @@ export class TodoListImageRepository extends DefaultCrudRepository< protected todoListRepositoryGetter: Getter, ) { super(TodoListImage, dataSource); - this.todoList = this._createBelongsToAccessorFor( + this.todoList = this.createBelongsToAccessorFor( 'todoList', todoListRepositoryGetter, ); } + + async find( + filter?: Filter, + options?: Options, + ): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...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 todo-lists in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todoList') { + await Promise.all( + result.map(async r => { + r.todoList = await this.todoList(r.id); + }), + ); + } + + return result; + } + + async findById( + id: typeof TodoListImage.prototype.id, + filter?: Filter, + options?: Options, + ): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...filter, include: undefined}; + + const result = await super.findById(id, filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todo-lists in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todoList') { + result.todoList = await this.todoList(result.id); + } + + return result; + } } diff --git a/examples/todo-list/src/repositories/todo-list.repository.ts b/examples/todo-list/src/repositories/todo-list.repository.ts index 17004ab276db..be42faa9c1b0 100644 --- a/examples/todo-list/src/repositories/todo-list.repository.ts +++ b/examples/todo-list/src/repositories/todo-list.repository.ts @@ -6,12 +6,20 @@ import {Getter, inject} from '@loopback/core'; import { DefaultCrudRepository, + Filter, HasManyRepositoryFactory, HasOneRepositoryFactory, juggler, + Options, repository, } from '@loopback/repository'; -import {Todo, TodoList, TodoListImage, TodoListRelations} from '../models'; +import { + Todo, + TodoList, + TodoListImage, + TodoListRelations, + TodoListWithRelations, +} from '../models'; import {TodoListImageRepository} from './todo-list-image.repository'; import {TodoRepository} from './todo.repository'; @@ -50,4 +58,52 @@ export class TodoListRepository extends DefaultCrudRepository< public findByTitle(title: string) { return this.findOne({where: {title}}); } + + async find( + filter?: Filter, + options?: Options, + ): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...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 + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + 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; + } + + async findById( + id: typeof TodoList.prototype.id, + filter?: Filter, + options?: Options, + ): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...filter, include: undefined}; + + const result = await super.findById(id, filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todos in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todos') { + result.todos = await this.todos(result.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 a558e747d1bd..7e6ef193e3c3 100644 --- a/examples/todo-list/src/repositories/todo.repository.ts +++ b/examples/todo-list/src/repositories/todo.repository.ts @@ -7,10 +7,12 @@ import {Getter, inject} from '@loopback/core'; import { BelongsToAccessor, DefaultCrudRepository, + Filter, juggler, + Options, repository, } from '@loopback/repository'; -import {Todo, TodoList, TodoRelations} from '../models'; +import {Todo, TodoList, TodoRelations, TodoWithRelations} from '../models'; import {TodoListRepository} from './todo-list.repository'; export class TodoRepository extends DefaultCrudRepository< @@ -35,4 +37,53 @@ export class TodoRepository extends DefaultCrudRepository< todoListRepositoryGetter, ); } + + async find( + filter?: Filter, + options?: Options, + ): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...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 todo-lists in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todoList') { + await Promise.all( + result.map(async r => { + r.todoList = await this.todoList(r.id); + }), + ); + } + + return result; + } + + async findById( + id: typeof Todo.prototype.id, + filter?: Filter, + options?: Options, + ): Promise { + // Prevent juggler for applying "include" filter + // Juggler is not aware of LB4 relations + const include = filter && filter.include; + filter = {...filter, include: undefined}; + + const result = await super.findById(id, filter, options); + + // poor-mans inclusion resolver, this should be handled by DefaultCrudRepo + // and use `inq` operator to fetch related todo-lists in fewer DB queries + // this is a temporary implementation, please see + // https://github.com/strongloop/loopback-next/issues/3195 + if (include && include.length && include[0].relation === 'todoList') { + result.todoList = await this.todoList(result.id); + } + + return result; + } }