From 306d437e081188c135d3e2b69f4d8f04c7a5ed16 Mon Sep 17 00:00:00 2001 From: shimks Date: Thu, 5 Jul 2018 16:28:29 -0400 Subject: [PATCH] feat(example-todo-list): add TodoList package/tutorial --- docs/site/Examples-and-tutorials.md | 12 +- docs/site/sidebars/lb4_sidebar.yml | 17 ++ docs/site/todo-list-tutorial-controller.md | 169 +++++++++++++++++ docs/site/todo-list-tutorial-model.md | 115 ++++++++++++ docs/site/todo-list-tutorial-repository.md | 82 +++++++++ docs/site/todo-list-tutorial.md | 12 ++ .../site/todo-tutorial-putting-it-together.md | 25 ++- examples/todo-list/.npmrc | 1 + examples/todo-list/.prettierignore | 2 + examples/todo-list/.prettierrc | 6 + examples/todo-list/.vscode/settings.json | 21 +++ examples/todo-list/.vscode/tasks.json | 29 +++ examples/todo-list/LICENSE | 25 +++ examples/todo-list/README.md | 111 ++++++++++++ examples/todo-list/data/db.json | 18 ++ .../todo-list/imgs/todo-list-overview.PNG | Bin 0 -> 55319 bytes examples/todo-list/index.d.ts | 6 + examples/todo-list/index.js | 16 ++ examples/todo-list/index.ts | 8 + examples/todo-list/package.json | 70 ++++++++ examples/todo-list/src/application.ts | 43 +++++ examples/todo-list/src/controllers/index.ts | 8 + .../controllers/todo-list-todo.controller.ts | 45 +++++ .../src/controllers/todo-list.controller.ts | 64 +++++++ .../src/controllers/todo.controller.ts | 52 ++++++ .../src/datasources/db.datasource.json | 6 + .../src/datasources/db.datasource.ts | 19 ++ examples/todo-list/src/datasources/index.ts | 6 + examples/todo-list/src/index.ts | 19 ++ examples/todo-list/src/models/index.ts | 7 + .../todo-list/src/models/todo-list.model.ts | 33 ++++ examples/todo-list/src/models/todo.model.ts | 41 +++++ examples/todo-list/src/repositories/index.ts | 7 + .../src/repositories/todo-list.repository.ts | 32 ++++ .../src/repositories/todo.repository.ts | 19 ++ examples/todo-list/src/sequence.ts | 41 +++++ .../acceptance/todo-list-todo.acceptance.ts | 166 +++++++++++++++++ .../test/acceptance/todo-list.acceptance.ts | 170 ++++++++++++++++++ .../test/acceptance/todo.acceptance.ts | 145 +++++++++++++++ examples/todo-list/test/helpers.ts | 52 ++++++ .../todo-list-todo.controller.unit.ts | 156 ++++++++++++++++ .../controllers/todo-list.controller.unit.ts | 170 ++++++++++++++++++ .../unit/controllers/todo.controller.unit.ts | 141 +++++++++++++++ examples/todo-list/tsconfig.build.json | 8 + examples/todo-list/tslint.build.json | 4 + examples/todo-list/tslint.json | 4 + examples/todo/README.md | 7 +- .../unit/controllers/todo.controller.unit.ts | 8 +- packages/cli/generators/example/index.js | 6 +- 49 files changed, 2202 insertions(+), 22 deletions(-) create mode 100644 docs/site/todo-list-tutorial-controller.md create mode 100644 docs/site/todo-list-tutorial-model.md create mode 100644 docs/site/todo-list-tutorial-repository.md create mode 100644 docs/site/todo-list-tutorial.md create mode 100644 examples/todo-list/.npmrc create mode 100644 examples/todo-list/.prettierignore create mode 100644 examples/todo-list/.prettierrc create mode 100644 examples/todo-list/.vscode/settings.json create mode 100644 examples/todo-list/.vscode/tasks.json create mode 100644 examples/todo-list/LICENSE create mode 100644 examples/todo-list/README.md create mode 100644 examples/todo-list/data/db.json create mode 100644 examples/todo-list/imgs/todo-list-overview.PNG create mode 100644 examples/todo-list/index.d.ts create mode 100644 examples/todo-list/index.js create mode 100644 examples/todo-list/index.ts create mode 100644 examples/todo-list/package.json create mode 100644 examples/todo-list/src/application.ts create mode 100644 examples/todo-list/src/controllers/index.ts create mode 100644 examples/todo-list/src/controllers/todo-list-todo.controller.ts create mode 100644 examples/todo-list/src/controllers/todo-list.controller.ts create mode 100644 examples/todo-list/src/controllers/todo.controller.ts create mode 100644 examples/todo-list/src/datasources/db.datasource.json create mode 100644 examples/todo-list/src/datasources/db.datasource.ts create mode 100644 examples/todo-list/src/datasources/index.ts create mode 100644 examples/todo-list/src/index.ts create mode 100644 examples/todo-list/src/models/index.ts create mode 100644 examples/todo-list/src/models/todo-list.model.ts create mode 100644 examples/todo-list/src/models/todo.model.ts create mode 100644 examples/todo-list/src/repositories/index.ts create mode 100644 examples/todo-list/src/repositories/todo-list.repository.ts create mode 100644 examples/todo-list/src/repositories/todo.repository.ts create mode 100644 examples/todo-list/src/sequence.ts create mode 100644 examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts create mode 100644 examples/todo-list/test/acceptance/todo-list.acceptance.ts create mode 100644 examples/todo-list/test/acceptance/todo.acceptance.ts create mode 100644 examples/todo-list/test/helpers.ts create mode 100644 examples/todo-list/test/unit/controllers/todo-list-todo.controller.unit.ts create mode 100644 examples/todo-list/test/unit/controllers/todo-list.controller.unit.ts create mode 100644 examples/todo-list/test/unit/controllers/todo.controller.unit.ts create mode 100644 examples/todo-list/tsconfig.build.json create mode 100644 examples/todo-list/tslint.build.json create mode 100644 examples/todo-list/tslint.json diff --git a/docs/site/Examples-and-tutorials.md b/docs/site/Examples-and-tutorials.md index 03f247e9f01c..9db68d1565fe 100644 --- a/docs/site/Examples-and-tutorials.md +++ b/docs/site/Examples-and-tutorials.md @@ -16,6 +16,9 @@ LoopBack 4 comes with the following example projects: - **[todo](todo-tutorial.md)**: Tutorial on building a simple application with LoopBack 4 key concepts using bottom-up approach. +- **[todo-list](todo-list-tutorial.md)**: Tutorial on introducing related models + and building their API from the Todo tutorial + - **[log-extension](https://github.com/strongloop/loopback-next/tree/master/examples/log-extension)**: Tutorial on building a log extension. @@ -25,11 +28,12 @@ LoopBack 4 comes with the following example projects: You can download any of the example projects usig our CLI tool `lb4`: ```sh -lb4 example +$ lb4 example ? What example would you like to clone? (Use arrow keys) -❯ todo: Tutorial example on how to build an application with LoopBack 4. - hello-world: A simple hello-world Application using LoopBack 4 - log-extension: An example extension project for LoopBack 4 +> todo: Tutorial example on how to build an application with LoopBack 4. + todo-list: Continuation of the todo example using relations in LoopBack 4. + hello-world: A simple hello-world Application using LoopBack 4. + log-extension: An example extension project for LoopBack 4. rpc-server: A basic RPC server using a made-up protocol. ``` diff --git a/docs/site/sidebars/lb4_sidebar.yml b/docs/site/sidebars/lb4_sidebar.yml index ec0fe4f0c467..5d81645d0482 100644 --- a/docs/site/sidebars/lb4_sidebar.yml +++ b/docs/site/sidebars/lb4_sidebar.yml @@ -57,6 +57,23 @@ children: url: todo-tutorial-geocoding-service.html output: 'web, pdf' + - title: 'TodoList Tutorial' + url: todo-list-tutorial.html + output: 'web, pdf' + children: + + - title: 'Add TodoList Model' + url: todo-list-tutorial-model.html + output: 'web, pdf' + + - title: 'Add TodoList Repository' + url: todo-list-tutorial-repository.html + output: 'web, pdf' + + - title: 'Add TodoList Controller' + url: todo-list-tutorial-controller.html + output: 'web, pdf' + - title: 'Key concepts' url: Concepts.html output: 'web, pdf' diff --git a/docs/site/todo-list-tutorial-controller.md b/docs/site/todo-list-tutorial-controller.md new file mode 100644 index 000000000000..70e34bcef6f4 --- /dev/null +++ b/docs/site/todo-list-tutorial-controller.md @@ -0,0 +1,169 @@ +--- +lang: en +title: "Add TodoList and TodoList's Todo Controller" +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/todo-list-tutorial-controller.html +summary: LoopBack 4 TodoList Application Tutorial - Add TodoList and TodoList's Todo Controller +--- + +### Controllers with related models + +Defining business logic to handle requests to related models isn't too different +from handling requests for standalone models. We'll create controllers to handle +requests for todo-lists and todo items under a todo-list. + +### Create TodoList controller + +Run the CLI command for creating a RESTful CRUD controller for our `TodoList` +routes with the following inputs: + +```sh +$ lb4 controller +? Controller class name: TodoList +? What kind of controller would you like to generate? REST Controller with CRUD functions +? What is the name of the model to use with this CRUD repository? TodoList +? What is the name of your CRUD repository? TodoRepository +? What is the type of your ID? number +? What is the base HTTP path name of the CRUD operations? /todo-lists + create src/controllers/todo-list.controller.ts + update src/controllers/index.ts + +Controller TodoList was created in src/controllers/ +``` + +And voilà! We now have a set of basic APIs for todo-lists just like that! + +### Create TodoList's Todo controller + +For the controller handling `Todos` of a `TodoList`, we'll start with an empty +controller: + +```sh +$ lb4 controller +? Controller class name: TodoListTodo +? What kind of controller would you like to generate? Empty Controller + create src/controllers/todo-list-todo.controller.ts + update src/controllers/index.ts + +Controller TodoListTodo was created in src/controllers/ +``` + +Let's add in an injection for our `TodoListRepository`: + +#### src/controllers/todo-list-todo.controller.ts + +```ts +import {repository} from '@loopback/repository'; +import {TodoListRepository} from '../repositories'; + +export class TodoListTodoController { + constructor( + @repository(TodoListRepository) protected todoListRepo: TodoListRepository, + ) {} +} +``` + +With this, we're now ready to add in some routes for our todo requests. To call +the CRUD methods on a todo-list's todo items, we'll first need to create a +constrained `TodoRepository`. We can achieve this by using our repository +instance's `todos` factory function that we defined earlier in +`TodoListRepository`. + +The `POST` request from `/todo-lists/{id}/todos` should look something like +this: + +#### src/controllers/todo-list-todo.controller.ts + +```ts +import {repository} from '@loopback/repository'; +import {TodoListRepository} from '../repositories'; +import {post, param, requestBody} from '@loopback/rest'; +import {Todo} from '../models'; + +export class TodoListTodoController { + constructor( + @repository(TodoListRepository) protected todoListRepo: TodoListRepository, + ) {} + + @post('/todo-lists/{id}/todos') + async create(@param.path.number('id') id: number, @requestBody() todo: Todo) { + return await this.todoListRepo.todos(id).create(todo); + } +} +``` + +Using our constraining factory as we did with the `POST` request, we'll define +the controller methods for the rest of the HTTP verbs for the route. The +completed controller should look as follows: + +#### src/controllers/todo-list-todo.controller.ts + +```ts +import {repository, Filter, Where} from '@loopback/repository'; +import {TodoListRepository} from '../repositories'; +import {post, get, patch, del, param, requestBody} from '@loopback/rest'; +import {Todo} from '../models'; + +export class TodoListTodoController { + constructor( + @repository(TodoListRepository) protected todoListRepo: TodoListRepository, + ) {} + + @post('/todo-lists/{id}/todos') + async create(@param.path.number('id') id: number, @requestBody() todo: Todo) { + return await this.todoListRepo.todos(id).create(todo); + } + + @get('/todo-lists/{id}/todos') + async find( + @param.path.number('id') id: number, + @param.query.string('filter') filter?: Filter, + ) { + return await this.todoListRepo.todos(id).find(filter); + } + + @patch('/todo-lists/{id}/todos') + async patch( + @param.path.number('id') id: number, + @requestBody() todo: Partial, + @param.query.string('where') where?: Where, + ) { + return await this.todoListRepo.todos(id).patch(todo, where); + } + + @del('/todo-lists/{id}/todos') + async delete( + @param.path.number('id') id: number, + @param.query.string('where') where?: Where, + ) { + return await this.todoListRepo.todos(id).delete(where); + } +} +``` + +### Try it out + +With the controllers complete, your application is ready to start up again! +`@loopback/boot` should wire up everything for us when we start the application, +so there's nothing else we need to do before we try out our new routes. + +```sh +$ npm start +Server is running at http://127.0.0.1:3000 +``` + +Here are some new requests you can try out: + +- `POST /todo-lists` with a body of `{ "title": "grocery list" }`. +- `POST /todo-lists/{id}/todos` using the ID you got back from the previous + `POST` request and a body for a todo. Notice that response body you get back + contains property `todoListId` with the ID from before. +- `GET /todos/{id}/todos` and see if you get the todo you created from before. + +And there you have it! You now have the power to define API for related models! + +### Navigation + +Previous step: [Add TodoList repository](todo-list-tutorial-repository.md) diff --git a/docs/site/todo-list-tutorial-model.md b/docs/site/todo-list-tutorial-model.md new file mode 100644 index 000000000000..5d3c197b6a17 --- /dev/null +++ b/docs/site/todo-list-tutorial-model.md @@ -0,0 +1,115 @@ +--- +lang: en +title: 'Add TodoList Model' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/todo-list-tutorial-model.html +summary: LoopBack 4 TodoList Application Tutorial - Add TodoList Model +--- + +### Building a checklist for your Todo models + +A todo item is often grouped into a checklist along with other todo items so +that it can be used to measure the progress of a bigger picture that the item is +a part of. + +A set of data can often be related to another; an entity may be able to provide +access to another based on its relationship with the other entity. To take +`TodoListApplication` one step further and establish relations with the existing +`Todo` model as real-world applications often tend to do, we'll introduce the +model `TodoList`. + +We'll create `TodoList` model to represent a checklist that contains multiple +Todo items. Let's define TodoList model with the following properties: + +- a unique id +- a title +- a color to represent the TodoList with + +We can use the `lb4 model` command and answer the prompts to generate the model +for us as follows: + +```sh +$ lb4 model +? Model class name: TodoList + +Let's add a property to TodoList +Enter an empty property name when done + +? Enter the property name: id +? Property type: number +? Is ID field? Yes +? Required?: No +? Default value [leave blank for none]: + +Let's add another property to TodoList +Enter an empty property name when done + +? Enter the property name: title +? Property type: string +? Required?: Yes +? Default value [leave blank for none]: + +Let's add another property to TodoList +Enter an empty property name when done + +? Enter the property name: color +? Property type: string +? Required?: No +? Default value [leave blank for none]: + +Let's add another property to TodoList +Enter an empty property name when done + +? Enter the property name: + create src/models/todo-list.model.ts + update src/models/index.ts + +Model TodoList was created in src/models/ +``` + +Now that we have our new model, it's time to define its relation with the `Todo` +model. To `TodoList` model, add in the following property: + +#### src/models/todo-list.model.ts + +```ts +@model() +export class TodoList extends Entity { + // ...properties defined by the CLI... + + @hasMany(Todo) todos?: Todo[]; + + // ...constructor def... +} +``` + +Notice that `@hasMany()` decorator is used to define this property. As the +decorator's name suggests, this will let LoopBack 4 know that a todo list can +have many todo items. + +To complement `TodoList`'s relationship to `Todo`, we'll add in `todoListId` +property on `Todo` model to complete defining the relation on both ends: + +### src/models/todo.model.ts + +```ts +@model() +export class Todo extends Entity { + // ...properties defined by the CLI... + + @property() todoListId: number; + + // ...constructor def... +} +``` + +Once the models have been completely configured, it's time to move on to adding +a [repository](todo-list-tutorial-repository.md) for `TodoList`. + +### Navigation + +Introduction: [TodoList Tutorial](todo-list-tutorial.md) + +Next step: [Add TodoList repository](todo-list-tutorial-repository.md) diff --git a/docs/site/todo-list-tutorial-repository.md b/docs/site/todo-list-tutorial-repository.md new file mode 100644 index 000000000000..349189c8914a --- /dev/null +++ b/docs/site/todo-list-tutorial-repository.md @@ -0,0 +1,82 @@ +--- +lang: en +title: 'Add TodoList Repository' +keywords: LoopBack 4.0, LoopBack 4 +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/todo-list-tutorial-repository.html +summary: LoopBack 4 TodoList Application Tutorial - Add TodoList Repository +--- + +### Repositories with related models + +One great feature a related model's repository has is its ability to expose a +factory function, a function that return a newly instantiated object, that can +return a 'constrained' version of the related model's repository. This factory +function is useful because it allows you to create a repository whose operations +are limited by the data set that the factory function takes in. + +In this section, we'll build `TodoListRepository` to have the capability of +building a constrained version of `TodoRepository`. + +### Create your repository + +In the `src/repositories` directory: + +- create `todo-list.repository.ts` +- update `index.ts` to export the newly created repository + +Like `TodoRepository`, we'll use `DefaultCrudRepository` to extend our +`TodoListRepository`. Since we're going to be using the same database used for +`TodoRepository`, inject `datasources.db` in this repository as well. From there +we'll need to make two more additions: + +- define `todos` property; this property will be used to build a constrained + `TodoRepository` +- inject `TodoRepository` instance + +Once the property type for `todos` have been defined, use +`this._createHasManyRepositoryFactoryFor` to assign it a repository contraining +factory function. Pass in the name of the relationship (`todos`) and the Todo +repository instance to constrain as the arguments for the function. + +#### src/repositories/todo-list.repository.ts + +```ts +import { + DefaultCrudRepository, + juggler, + HasManyRepositoryFactory, + repository, +} from '@loopback/repository'; +import {TodoList, Todo} from '../models'; +import {inject} from '@loopback/core'; +import {TodoRepository} from './todo.repository'; + +export class TodoListRepository extends DefaultCrudRepository< + TodoList, + typeof TodoList.prototype.id +> { + public todos: HasManyRepositoryFactory; + + constructor( + @inject('datasources.db') protected datasource: juggler.DataSource, + @repository(TodoRepository) protected todoRepository: TodoRepository, + ) { + super(TodoList, datasource); + this.todos = this._createHasManyRepositoryFactoryFor( + 'todos', + todoRepository, + ); + } +} +``` + +We're now ready to expose `TodoList` and its related `Todo` API through the +[controller](todo-list-tutorial-controller.md). + +### Navigation + +Previous step: [Add TodoList model](todo-list-tutorial-model.md) + +Last step: [Add TodoList controller](todo-list-tutorial-controller.md) diff --git a/docs/site/todo-list-tutorial.md b/docs/site/todo-list-tutorial.md new file mode 100644 index 000000000000..e6e670614abb --- /dev/null +++ b/docs/site/todo-list-tutorial.md @@ -0,0 +1,12 @@ +--- +lang: en +title: 'TodoList tutorial' +keywords: LoopBack 4.0, LoopBack 4 +layout: readme +source: loopback-next +file: examples/todo-list/README.md +tags: +sidebar: lb4_sidebar +permalink: /doc/en/lb4/todo-list-tutorial.html +summary: LoopBack 4 TodoList Application Tutorial +--- diff --git a/docs/site/todo-tutorial-putting-it-together.md b/docs/site/todo-tutorial-putting-it-together.md index efbad8bd9c0b..2efeeba61cde 100644 --- a/docs/site/todo-tutorial-putting-it-together.md +++ b/docs/site/todo-tutorial-putting-it-together.md @@ -105,12 +105,22 @@ Here are some requests you can try: That's it! You've just created your first LoopBack 4 application! -### Bonus: Integrate with a REST based geo-coding service - -A typical REST API server needs to access data from a variety of sources, -including SOAP or REST services. Continue to the bonus section to learn how -LoopBack connectors make it super easy to fetch data from other services and -[enhance your Todo application with location-based reminders](todo-tutorial-geocoding-service.md). +### Where to go from here + +There are still a ton of features you can use to build on top of the +`TodoListApplication`. Here are some tutorials that continues off from where we +left off here to guide you through adding in an additional feature: + +- **Integrate with a REST based geo-coding service**: A typical REST API server + needs to access data from a variety of sources, including SOAP or REST + services. Continue to the bonus section to learn how LoopBack connectors make + it super easy to fetch data from other services and + [enhance your Todo application with location-based reminders](todo-tutorial-geocoding-service.md). +- **Add related Model with TodoListApplication**: If you would like to try out + using some of the more advanced features of LoopBack 4 such as relations, try + out the + [TodoList tutorial](https://loopback.io/doc/en/lb4/todo-list-tutorial.html) + which continues off from where we leave here. ### More examples and tutorials @@ -121,6 +131,3 @@ creating your own custom components, sequences and more! ### Navigation Previous step: [Add a controller](todo-tutorial-controller.md) - -Next step: -[Integrate with a geo-coding service](todo-tutorial-geocoding-service.md) diff --git a/examples/todo-list/.npmrc b/examples/todo-list/.npmrc new file mode 100644 index 000000000000..43c97e719a5a --- /dev/null +++ b/examples/todo-list/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/examples/todo-list/.prettierignore b/examples/todo-list/.prettierignore new file mode 100644 index 000000000000..bc1199efffac --- /dev/null +++ b/examples/todo-list/.prettierignore @@ -0,0 +1,2 @@ +dist* +*.json diff --git a/examples/todo-list/.prettierrc b/examples/todo-list/.prettierrc new file mode 100644 index 000000000000..f58b81dd7be2 --- /dev/null +++ b/examples/todo-list/.prettierrc @@ -0,0 +1,6 @@ +{ + "bracketSpacing": false, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "all" +} diff --git a/examples/todo-list/.vscode/settings.json b/examples/todo-list/.vscode/settings.json new file mode 100644 index 000000000000..0e1c0089f810 --- /dev/null +++ b/examples/todo-list/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "editor.rulers": [80], + "editor.tabCompletion": true, + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "editor.formatOnSave": true, + + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.hg": true, + "**/.svn": true, + "**/CVS": true, + "dist*": true, + }, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + + "tslint.ignoreDefinitionFiles": true, + "typescript.tsdk": "./node_modules/typescript/lib" +} diff --git a/examples/todo-list/.vscode/tasks.json b/examples/todo-list/.vscode/tasks.json new file mode 100644 index 000000000000..c3003aa764e3 --- /dev/null +++ b/examples/todo-list/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Watch and Compile Project", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "build:watch"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc-watch" + }, + { + "label": "Build, Test and Lint", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "test:dev"], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": ["$tsc", "$tslint5"] + } + ] +} diff --git a/examples/todo-list/LICENSE b/examples/todo-list/LICENSE new file mode 100644 index 000000000000..feba1b17e9f2 --- /dev/null +++ b/examples/todo-list/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2018. All Rights Reserved. +Node module: @loopback/example-todo-list +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/todo-list/README.md b/examples/todo-list/README.md new file mode 100644 index 000000000000..5b614a1ce632 --- /dev/null +++ b/examples/todo-list/README.md @@ -0,0 +1,111 @@ +# @loopback/example-todo-list + +This is an extended tutorial that builds on top of `@loopback/example-todo`. + +## Overview + +This tutorial demonstrates how to create a set of APIs for models that are +related to one another. + +![todo-tutorial-overview](./imgs/todo-list-overview.png) + +## Setup + +If you're following from the tutorial in `@loopback/example-todo`, you can jump +straight to our first step: +[Add TodoList model](http://loopback.io/doc/en/lb4/todo-list-tutorial-model.html) + +If not, you'll need to make sure you have a couple of things installed before we +get started: + +- [Node.js](https://nodejs.org/en/) at v8.x or greater + +Next, you'll need to install the LoopBack 4 CLI toolkit: + +```sh +npm i -g @loopback/cli +``` + +We recommend that you start with the +[todo tutorial](http://loopback.io/doc/en/lb4/todo-tutorial.html) if you're not +familiar with LoopBack4, but if you are and don't want to start from scratch +again, you can use the LoopBack 4 CLI tool to catch up to where this tutorial +will continue from: + +```sh +lb4 example todo +``` + +It should be noted that this tutorial does not assume the +[optional geo-coding step](https://loopback.io/doc/en/lb4/todo-tutorial-geocoding-service.html) +has been completed. Whether the step has been completed or not, the content and +the steps listed in this tutorial remain the same. + +## Tutorial + +Once you're ready to start the tutorial, let's begin by +[adding a TodoList model](http://loopback.io/doc/en/lb4/todo-list-tutorial-model.html) + +### Steps + +1. [Add TodoList Model](http://loopback.io/doc/en/lb4/todo-list-tutorial-model.html) +2. [Add TodoList Repository](http://loopback.io/doc/en/lb4/todo-list-tutorial-repository.html) +3. [Add TodoList and TodoList's Todo Controller](http://loopback.io/doc/en/lb4/todo-list-tutorial-controller.html) + +## Try it out + +If you'd like to see the final results of this tutorial as an example +application, follow these steps: + +1. Run the `lb4 example` command to select and clone the todo repository: + +```sh +$ lb4 example +? What example would you like to clone? (Use arrow keys) + todo: Tutorial example on how to build an application with LoopBack 4. +❯ todo-list: Continuation of the todo example using relations in LoopBack 4. + hello-world: A simple hello-world Application using LoopBack 4. + log-extension: An example extension project for LoopBack 4. + rpc-server: A basic RPC server using a made-up protocol. +``` + +2. Jump into the directory and then install the required dependencies: + +```sh +cd loopback4-example-todo-list && npm i +``` + +3. Finally, start the application! + + ```sh + $ npm start + + Server is running on port 3000 + ``` + +Feel free to look around in the application's code to get a feel for how it +works. If you're interested in how it's been built or why we do things a certain +way, then continue on with this tutorial! + +### Bugs/Feedback + +Open an issue in [loopback-next](https://github.com/strongloop/loopback-next) +and we'll take a look! + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/examples/todo-list/data/db.json b/examples/todo-list/data/db.json new file mode 100644 index 000000000000..855f9efaf7ab --- /dev/null +++ b/examples/todo-list/data/db.json @@ -0,0 +1,18 @@ +{ + "ids": { + "Todo": 5, + "TodoList": 3 + }, + "models": { + "Todo": { + "1": "{\"title\":\"Take over the galaxy\",\"desc\":\"MWAHAHAHAHAHAHAHAHAHAHAHAHAMWAHAHAHAHAHAHAHAHAHAHAHAHA\",\"todoListId\":1,\"id\":1}", + "2": "{\"title\":\"destroy alderaan\",\"desc\":\"Make sure there are no survivors left!\",\"todoListId\":1,\"id\":2}", + "3": "{\"title\":\"terrorize senate\",\"desc\":\"Tell them they're getting a budget cut.\",\"todoListId\":2,\"id\":3}", + "4": "{\"title\":\"crush rebel scum\",\"desc\":\"Every.Last.One.\",\"todoListId\":1,\"id\":4}" + }, + "TodoList": { + "1": "{\"title\":\"Sith lord's check list\",\"lastModified\":\"a long time ago\",\"id\":1}", + "2": "{\"title\":\"daily routine of POTUS\",\"lastModified\":\"2018-07-13\",\"id\":2}" + } + } +} diff --git a/examples/todo-list/imgs/todo-list-overview.PNG b/examples/todo-list/imgs/todo-list-overview.PNG new file mode 100644 index 0000000000000000000000000000000000000000..37e23e4979dfd34f61fa3c4646e71e7265db859d GIT binary patch literal 55319 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

eY=%vs=gcsG?`L@ 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 C3)~q1 literal 0 HcmV?d00001 diff --git a/examples/todo-list/index.d.ts b/examples/todo-list/index.d.ts new file mode 100644 index 000000000000..19e5f31aa77d --- /dev/null +++ b/examples/todo-list/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist8'; diff --git a/examples/todo-list/index.js b/examples/todo-list/index.js new file mode 100644 index 000000000000..e7999bd9ac08 --- /dev/null +++ b/examples/todo-list/index.js @@ -0,0 +1,16 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +const application = require('@loopback/dist-util').loadDist(__dirname); + +module.exports = application; + +if (require.main === module) { + // Run the application + application.main().catch(err => { + console.error('Cannot start the application.', err); + process.exit(1); + }); +} diff --git a/examples/todo-list/index.ts b/examples/todo-list/index.ts new file mode 100644 index 000000000000..470092e3090d --- /dev/null +++ b/examples/todo-list/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/examples/todo-list/package.json b/examples/todo-list/package.json new file mode 100644 index 000000000000..bc2e6746558f --- /dev/null +++ b/examples/todo-list/package.json @@ -0,0 +1,70 @@ +{ + "name": "@loopback/example-todo-list", + "version": "0.1.0", + "description": "Continuation of the todo example using relations in LoopBack 4.", + "main": "index.js", + "engines": { + "node": ">=8" + }, + "scripts": { + "build": "npm run build:dist8 && npm run build:dist10", + "build:apidocs": "lb-apidocs", + "build:current": "lb-tsc", + "build:dist8": "lb-tsc es2017", + "build:dist10": "lb-tsc es2018", + "build:watch": "lb-tsc --watch", + "clean": "lb-clean *example-todo-list*.tgz dist* package api-docs", + "lint": "npm run prettier:check && npm run tslint", + "lint:fix": "npm run prettier:fix && npm run tslint:fix", + "prettier:cli": "lb-prettier \"**/*.ts\"", + "prettier:check": "npm run prettier:cli -- -l", + "prettier:fix": "npm run prettier:cli -- --write", + "tslint": "lb-tslint", + "tslint:fix": "npm run tslint -- --fix", + "pretest": "npm run build:current", + "test": "lb-mocha \"DIST/test/*/**/*.js\"", + "test:dev": "lb-mocha --allow-console-logs DIST/test/**/*.js && npm run posttest", + "verify": "npm pack && tar xf loopback-todo-list*.tgz && tree package && npm run clean", + "prestart": "npm run build:current", + "start": "node ." + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "dependencies": { + "@loopback/boot": "^0.11.9", + "@loopback/context": "^0.11.11", + "@loopback/core": "^0.10.3", + "@loopback/dist-util": "^0.3.3", + "@loopback/openapi-v3": "^0.11.3", + "@loopback/openapi-v3-types": "^0.7.11", + "@loopback/repository": "^0.13.3", + "@loopback/rest": "^0.18.0", + "@loopback/service-proxy": "^0.5.14", + "loopback-connector-rest": "^3.1.1" + }, + "devDependencies": { + "@loopback/build": "^0.6.11", + "@loopback/http-caching-proxy": "^0.2.7", + "@loopback/testlab": "^0.10.11", + "@types/lodash": "^4.14.109", + "@types/node": "^10.1.1", + "lodash": "^4.17.10" + }, + "keywords": [ + "loopback", + "LoopBack", + "example", + "tutorial", + "relations", + "CRUD", + "models", + "todo", + "HasMany" + ] +} diff --git a/examples/todo-list/src/application.ts b/examples/todo-list/src/application.ts new file mode 100644 index 000000000000..aee875e43b05 --- /dev/null +++ b/examples/todo-list/src/application.ts @@ -0,0 +1,43 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {ApplicationConfig} from '@loopback/core'; +import {RestApplication} from '@loopback/rest'; +import {MySequence} from './sequence'; + +/* tslint:disable:no-unused-variable */ +// Binding and Booter imports are required to infer types for BootMixin! +import {BootMixin, Booter, Binding} from '@loopback/boot'; + +// juggler imports are required to infer types for RepositoryMixin! +import { + Class, + Repository, + RepositoryMixin, + juggler, +} from '@loopback/repository'; +/* tslint:enable:no-unused-variable */ + +export class TodoListApplication extends BootMixin( + RepositoryMixin(RestApplication), +) { + constructor(options?: ApplicationConfig) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + // Customize ControllerBooter Conventions here + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + }, + }; + } +} diff --git a/examples/todo-list/src/controllers/index.ts b/examples/todo-list/src/controllers/index.ts new file mode 100644 index 000000000000..496eb6210e0d --- /dev/null +++ b/examples/todo-list/src/controllers/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './todo.controller'; +export * from './todo-list.controller'; +export * from './todo-list-todo.controller'; diff --git a/examples/todo-list/src/controllers/todo-list-todo.controller.ts b/examples/todo-list/src/controllers/todo-list-todo.controller.ts new file mode 100644 index 000000000000..0dffa4b3de3e --- /dev/null +++ b/examples/todo-list/src/controllers/todo-list-todo.controller.ts @@ -0,0 +1,45 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {repository, Filter, Where} from '@loopback/repository'; +import {TodoListRepository} from '../repositories'; +import {post, get, patch, del, param, requestBody} from '@loopback/rest'; +import {Todo} from '../models'; + +export class TodoListTodoController { + constructor( + @repository(TodoListRepository) protected todoListRepo: TodoListRepository, + ) {} + + @post('/todo-lists/{id}/todos') + async create(@param.path.number('id') id: number, @requestBody() todo: Todo) { + return await this.todoListRepo.todos(id).create(todo); + } + + @get('/todo-lists/{id}/todos') + async find( + @param.path.number('id') id: number, + @param.query.string('filter') filter?: Filter, + ) { + return await this.todoListRepo.todos(id).find(filter); + } + + @patch('/todo-lists/{id}/todos') + async patch( + @param.path.number('id') id: number, + @requestBody() todo: Partial, + @param.query.string('where') where?: Where, + ) { + return await this.todoListRepo.todos(id).patch(todo, where); + } + + @del('/todo-lists/{id}/todos') + async delete( + @param.path.number('id') id: number, + @param.query.string('where') where?: Where, + ) { + return await this.todoListRepo.todos(id).delete(where); + } +} diff --git a/examples/todo-list/src/controllers/todo-list.controller.ts b/examples/todo-list/src/controllers/todo-list.controller.ts new file mode 100644 index 000000000000..55e1cc1ceeda --- /dev/null +++ b/examples/todo-list/src/controllers/todo-list.controller.ts @@ -0,0 +1,64 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Filter, Where, repository} from '@loopback/repository'; +import {post, param, get, patch, del, requestBody} from '@loopback/rest'; +import {TodoList} from '../models'; +import {TodoListRepository} from '../repositories'; + +export class TodoListController { + constructor( + @repository(TodoListRepository) + public todoListRepository: TodoListRepository, + ) {} + + @post('/todo-lists') + async create(@requestBody() obj: TodoList): Promise { + return await this.todoListRepository.create(obj); + } + + @get('/todo-lists/count') + async count(@param.query.string('where') where?: Where): Promise { + return await this.todoListRepository.count(where); + } + + @get('/todo-lists') + async find( + @param.query.string('filter') filter?: Filter, + ): Promise { + return await this.todoListRepository.find(filter); + } + + @patch('/todo-lists') + async updateAll( + @requestBody() obj: Partial, + @param.query.string('where') where?: Where, + ): Promise { + return await this.todoListRepository.updateAll(obj, where); + } + + @del('/todo-lists') + async deleteAll(@param.query.string('where') where?: Where): Promise { + return await this.todoListRepository.deleteAll(where); + } + + @get('/todo-lists/{id}') + async findById(@param.path.number('id') id: number): Promise { + return await this.todoListRepository.findById(id); + } + + @patch('/todo-lists/{id}') + async updateById( + @param.path.number('id') id: number, + @requestBody() obj: TodoList, + ): Promise { + return await this.todoListRepository.updateById(id, obj); + } + + @del('/todo-lists/{id}') + async deleteById(@param.path.number('id') id: number): Promise { + return await this.todoListRepository.deleteById(id); + } +} diff --git a/examples/todo-list/src/controllers/todo.controller.ts b/examples/todo-list/src/controllers/todo.controller.ts new file mode 100644 index 000000000000..bdd8a506e39f --- /dev/null +++ b/examples/todo-list/src/controllers/todo.controller.ts @@ -0,0 +1,52 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {repository} from '@loopback/repository'; +import {del, get, param, patch, post, put, requestBody} from '@loopback/rest'; +import {Todo} from '../models'; +import {TodoRepository} from '../repositories'; + +export class TodoController { + constructor(@repository(TodoRepository) protected todoRepo: TodoRepository) {} + + @post('/todos') + async createTodo(@requestBody() todo: Todo) { + return await this.todoRepo.create(todo); + } + + @get('/todos/{id}') + async findTodoById( + @param.path.number('id') id: number, + @param.query.boolean('items') items?: boolean, + ): Promise { + return await this.todoRepo.findById(id); + } + + @get('/todos') + async findTodos(): Promise { + return await this.todoRepo.find(); + } + + @put('/todos/{id}') + async replaceTodo( + @param.path.number('id') id: number, + @requestBody() todo: Todo, + ): Promise { + return await this.todoRepo.replaceById(id, todo); + } + + @patch('/todos/{id}') + async updateTodo( + @param.path.number('id') id: number, + @requestBody() todo: Todo, + ): Promise { + return await this.todoRepo.updateById(id, todo); + } + + @del('/todos/{id}') + async deleteTodo(@param.path.number('id') id: number): Promise { + return await this.todoRepo.deleteById(id); + } +} diff --git a/examples/todo-list/src/datasources/db.datasource.json b/examples/todo-list/src/datasources/db.datasource.json new file mode 100644 index 000000000000..a68f220be986 --- /dev/null +++ b/examples/todo-list/src/datasources/db.datasource.json @@ -0,0 +1,6 @@ +{ + "name": "db", + "connector": "memory", + "localStorage": "", + "file": "./data/db.json" +} diff --git a/examples/todo-list/src/datasources/db.datasource.ts b/examples/todo-list/src/datasources/db.datasource.ts new file mode 100644 index 000000000000..942301e1e165 --- /dev/null +++ b/examples/todo-list/src/datasources/db.datasource.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject} from '@loopback/core'; +import {juggler, DataSource} from '@loopback/repository'; +const config = require('./db.datasource.json'); + +export class DbDataSource extends juggler.DataSource { + static dataSourceName = 'db'; + + constructor( + @inject('datasources.config.db', {optional: true}) + dsConfig: DataSource = config, + ) { + super(dsConfig); + } +} diff --git a/examples/todo-list/src/datasources/index.ts b/examples/todo-list/src/datasources/index.ts new file mode 100644 index 000000000000..b81ca9f2218e --- /dev/null +++ b/examples/todo-list/src/datasources/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './db.datasource'; diff --git a/examples/todo-list/src/index.ts b/examples/todo-list/src/index.ts new file mode 100644 index 000000000000..0306aef025d1 --- /dev/null +++ b/examples/todo-list/src/index.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {TodoListApplication} from './application'; +import {ApplicationConfig} from '@loopback/core'; +import {RestServer} from '@loopback/rest'; + +export async function main(options?: ApplicationConfig) { + const app = new TodoListApplication(options); + await app.boot(); + await app.start(); + + const server = await app.getServer(RestServer); + const port = await server.get('rest.port'); + console.log(`Server is running at http://127.0.0.1:${port}`); + return app; +} diff --git a/examples/todo-list/src/models/index.ts b/examples/todo-list/src/models/index.ts new file mode 100644 index 000000000000..f1e08c1a8b61 --- /dev/null +++ b/examples/todo-list/src/models/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './todo.model'; +export * from './todo-list.model'; diff --git a/examples/todo-list/src/models/todo-list.model.ts b/examples/todo-list/src/models/todo-list.model.ts new file mode 100644 index 000000000000..dd3569789d8d --- /dev/null +++ b/examples/todo-list/src/models/todo-list.model.ts @@ -0,0 +1,33 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, model, property, hasMany} from '@loopback/repository'; +import {Todo} from './todo.model'; + +@model() +export class TodoList extends Entity { + @property({ + type: 'number', + id: true, + }) + id?: number; + + @property({ + type: 'string', + required: true, + }) + title: string; + + @property({ + type: 'string', + }) + color?: string; + + @hasMany(Todo) todos: Todo[]; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/examples/todo-list/src/models/todo.model.ts b/examples/todo-list/src/models/todo.model.ts new file mode 100644 index 000000000000..606b3733b701 --- /dev/null +++ b/examples/todo-list/src/models/todo.model.ts @@ -0,0 +1,41 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Entity, property, model} from '@loopback/repository'; + +@model() +export class Todo extends Entity { + @property({ + type: 'number', + id: true, + }) + id?: number; + + @property({ + type: 'string', + required: true, + }) + title: string; + + @property({ + type: 'string', + }) + desc?: string; + + @property({ + type: 'boolean', + }) + isComplete: boolean; + + @property() todoListId: number; + + getId() { + return this.id; + } + + constructor(data?: Partial) { + super(data); + } +} diff --git a/examples/todo-list/src/repositories/index.ts b/examples/todo-list/src/repositories/index.ts new file mode 100644 index 000000000000..ca53ed66422d --- /dev/null +++ b/examples/todo-list/src/repositories/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './todo.repository'; +export * from './todo-list.repository'; diff --git a/examples/todo-list/src/repositories/todo-list.repository.ts b/examples/todo-list/src/repositories/todo-list.repository.ts new file mode 100644 index 000000000000..7fb84eda2623 --- /dev/null +++ b/examples/todo-list/src/repositories/todo-list.repository.ts @@ -0,0 +1,32 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + DefaultCrudRepository, + juggler, + HasManyRepositoryFactory, + repository, +} from '@loopback/repository'; +import {TodoList, Todo} from '../models'; +import {inject} from '@loopback/core'; +import {TodoRepository} from './todo.repository'; + +export class TodoListRepository extends DefaultCrudRepository< + TodoList, + typeof TodoList.prototype.id +> { + public todos: HasManyRepositoryFactory; + + constructor( + @inject('datasources.db') protected datasource: juggler.DataSource, + @repository(TodoRepository) protected todoRepository: TodoRepository, + ) { + super(TodoList, datasource); + this.todos = this._createHasManyRepositoryFactoryFor( + 'todos', + todoRepository, + ); + } +} diff --git a/examples/todo-list/src/repositories/todo.repository.ts b/examples/todo-list/src/repositories/todo.repository.ts new file mode 100644 index 000000000000..76b8ee5f2d97 --- /dev/null +++ b/examples/todo-list/src/repositories/todo.repository.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {DefaultCrudRepository, juggler} from '@loopback/repository'; +import {Todo} from '../models'; +import {inject} from '@loopback/core'; + +export class TodoRepository extends DefaultCrudRepository< + Todo, + typeof Todo.prototype.id +> { + constructor( + @inject('datasources.db') protected datasource: juggler.DataSource, + ) { + super(Todo, datasource); + } +} diff --git a/examples/todo-list/src/sequence.ts b/examples/todo-list/src/sequence.ts new file mode 100644 index 000000000000..d8a22e67a3b3 --- /dev/null +++ b/examples/todo-list/src/sequence.ts @@ -0,0 +1,41 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Context, inject} from '@loopback/context'; +import { + FindRoute, + InvokeMethod, + ParseParams, + Reject, + RequestContext, + RestBindings, + Send, + SequenceHandler, +} from '@loopback/rest'; + +const SequenceActions = RestBindings.SequenceActions; + +export class MySequence implements SequenceHandler { + constructor( + @inject(RestBindings.Http.CONTEXT) public ctx: Context, + @inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute, + @inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams, + @inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod, + @inject(SequenceActions.SEND) public send: Send, + @inject(SequenceActions.REJECT) public reject: Reject, + ) {} + + async handle(context: RequestContext) { + try { + const {request, response} = context; + const route = this.findRoute(request); + const args = await this.parseParams(request, route); + const result = await this.invoke(route, args); + this.send(response, result); + } catch (error) { + this.reject(context, error); + } + } +} diff --git a/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts b/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts new file mode 100644 index 000000000000..674133876f25 --- /dev/null +++ b/examples/todo-list/test/acceptance/todo-list-todo.acceptance.ts @@ -0,0 +1,166 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {TodoListApplication} from '../../src/application'; +import {Todo, TodoList} from '../../src/models/'; +import {TodoRepository, TodoListRepository} from '../../src/repositories/'; +import {givenTodo, givenTodoList} from '../helpers'; + +describe('Application', () => { + let app: TodoListApplication; + let client: supertest.SuperTest; + let todoRepo: TodoRepository; + let todoListRepo: TodoListRepository; + + before(givenRunningApplicationWithCustomConfiguration); + after(() => app.stop()); + + before(givenTodoRepository); + before(givenTodoListRepository); + before(() => { + client = createClientForHandler(app.requestHandler); + }); + + beforeEach(async () => { + await todoRepo.deleteAll(); + await todoListRepo.deleteAll(); + }); + + it('creates todo for a todoList', async () => { + const todoList = await givenTodoListInstance(); + const todo = givenTodo(); + const response = await client + .post(`/todo-lists/${todoList.id}/todos`) + .send(todo) + .expect(200); + + expect(response.body).to.containDeep(todo); + }); + + it('finds todos for a todoList', async () => { + const todoList = await givenTodoListInstance(); + const notMyTodo = await givenTodoInstance({ + title: 'someone else does a thing', + }); + const myTodos = [ + await givenTodoInstanceOfTodoList(todoList.id), + await givenTodoInstanceOfTodoList(todoList.id, { + title: 'another thing needs doing', + }), + ]; + const response = await client + .get(`/todo-lists/${todoList.id}/todos`) + .send() + .expect(200); + + expect(response.body) + .to.containDeep(myTodos) + .and.not.containEql(notMyTodo.toJSON()); // is this assertion necessary? + }); + + it('updates todos for a todoList', async () => { + const todoList = await givenTodoListInstance(); + const notMyTodo = await givenTodoInstance({ + title: 'someone else does a thing', + }); + const myTodos = [ + await givenTodoInstanceOfTodoList(todoList.id), + await givenTodoInstanceOfTodoList(todoList.id, { + title: 'another thing needs doing', + }), + ]; + const patchedIsCompleteTodo = {isComplete: true}; + const response = await client + .patch(`/todo-lists/${todoList.id}/todos`) + .send(patchedIsCompleteTodo) + .expect(200); + + expect(response.body).to.eql(myTodos.length); + const updatedTodos = await todoListRepo.todos(todoList.id).find(); + const notUpdatedTodo = await todoRepo.findById(notMyTodo.id); + for (const todo of updatedTodos) { + expect(todo.toJSON()).to.containEql(patchedIsCompleteTodo); + } + expect(notUpdatedTodo.toJSON()).to.not.containEql(patchedIsCompleteTodo); + }); + + it('deletes todos for a todoList', async () => { + const todoList = await givenTodoListInstance(); + const notMyTodo = await givenTodoInstance({ + title: 'someone else does a thing', + }); + await givenTodoInstanceOfTodoList(todoList.id); + await givenTodoInstanceOfTodoList(todoList.id, { + title: 'another thing needs doing', + }); + await client + .del(`/todo-lists/${todoList.id}/todos`) + .send() + .expect(200); + + const myDeletedTodos = await todoListRepo.todos(todoList.id).find(); + const notDeletedTodo = await todoRepo.findById(notMyTodo.id); + expect(myDeletedTodos).to.be.empty(); + expect(notDeletedTodo).to.eql(notMyTodo); + }); + + /* + ============================================================================ + 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: { + port: 0, + }, + }); + + 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 givenTodoRepository() { + todoRepo = await app.getRepository(TodoRepository); + } + + async function givenTodoListRepository() { + todoListRepo = await app.getRepository(TodoListRepository); + } + + async function givenTodoInstance(todo?: Partial) { + return await todoRepo.create(givenTodo(todo)); + } + + async function givenTodoInstanceOfTodoList( + id: typeof Todo.prototype.id, + todo?: Partial, + ) { + return await todoListRepo.todos(id).create(givenTodo(todo)); + } + + async function givenTodoListInstance(todoList?: Partial) { + return await todoListRepo.create(givenTodoList(todoList)); + } +}); diff --git a/examples/todo-list/test/acceptance/todo-list.acceptance.ts b/examples/todo-list/test/acceptance/todo-list.acceptance.ts new file mode 100644 index 000000000000..7de9ff54c6fe --- /dev/null +++ b/examples/todo-list/test/acceptance/todo-list.acceptance.ts @@ -0,0 +1,170 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {TodoListApplication} from '../../src/application'; +import {TodoList} from '../../src/models/'; +import {TodoListRepository} from '../../src/repositories/'; +import {givenTodoList} from '../helpers'; + +describe('Application', () => { + let app: TodoListApplication; + let client: supertest.SuperTest; + let todoListRepo: TodoListRepository; + + before(givenRunningApplicationWithCustomConfiguration); + after(() => app.stop()); + + before(givenTodoListRepository); + before(() => { + client = createClientForHandler(app.requestHandler); + }); + + beforeEach(async () => { + await todoListRepo.deleteAll(); + }); + + it('creates a todoList', async () => { + const todoList = givenTodoList(); + const response = await client + .post('/todo-lists') + .send(todoList) + .expect(200); + expect(response.body).to.containDeep(todoList); + const result = await todoListRepo.findById(response.body.id); + expect(result).to.containDeep(todoList); + }); + + it('counts todoLists', async () => { + const todoLists = []; + todoLists.push(await givenTodoListInstance()); + todoLists.push( + await givenTodoListInstance({title: 'so many things to do wow'}), + ); + const response = await client + .get('/todo-lists/count') + .send() + .expect(200); + expect(response.body).to.eql(todoLists.length); + }); + + it('finds all todoLists', async () => { + const todoLists = []; + todoLists.push(await givenTodoListInstance()); + todoLists.push( + await givenTodoListInstance({title: 'so many things to do wow'}), + ); + const response = await client + .get('/todo-lists') + .send() + .expect(200); + expect(response.body).to.containDeep(todoLists); + }); + + it('updates all todoLists', async () => { + const todoLists = []; + todoLists.push(await givenTodoListInstance()); + todoLists.push( + await givenTodoListInstance({title: 'so many things to do wow'}), + ); + const patchedColorTodo = {color: 'purple'}; + const response = await client + .patch('/todo-lists') + .send(patchedColorTodo) + .expect(200); + expect(response.body).to.eql(todoLists.length); + const updatedTodoLists = await todoListRepo.find(); + for (const todoList of updatedTodoLists) { + expect(todoList.color).to.eql(patchedColorTodo.color); + } + }); + + it('deletes all todoLists', async () => { + await givenTodoListInstance(); + await givenTodoListInstance({title: 'so many things to do wow'}); + await client + .del('/todo-lists') + .send() + .expect(200); + expect(await todoListRepo.find()).to.be.empty(); + }); + + it('gets a todoList by ID', async () => { + const todoList = await givenTodoListInstance(); + const result = await client + .get(`/todo-lists/${todoList.id}`) + .send() + .expect(200); + // Remove any undefined properties that cannot be represented in JSON/REST + const expected = JSON.parse(JSON.stringify(todoList)); + expect(result.body).to.deepEqual(expected); + }); + + it('updates a todoList by ID ', async () => { + const todoList = await givenTodoListInstance(); + const updatedTodoList = givenTodoList({ + title: 'A different title to the todo list', + }); + await client + .patch(`/todo-lists/${todoList.id}`) + .send(updatedTodoList) + .expect(200); + const result = await todoListRepo.findById(todoList.id); + expect(result).to.containEql(updatedTodoList); + }); + + it('deletes a todoList by ID', async () => { + const todoList = await givenTodoListInstance(); + await client + .del(`/todo-lists/${todoList.id}`) + .send() + .expect(200); + await expect(todoListRepo.findById(todoList.id)).to.be.rejectedWith( + /no TodoList found with id/, + ); + }); + + /* + ============================================================================ + 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: { + port: 0, + }, + }); + + 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)); + } +}); diff --git a/examples/todo-list/test/acceptance/todo.acceptance.ts b/examples/todo-list/test/acceptance/todo.acceptance.ts new file mode 100644 index 000000000000..d348cd7a7ae3 --- /dev/null +++ b/examples/todo-list/test/acceptance/todo.acceptance.ts @@ -0,0 +1,145 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createClientForHandler, expect, supertest} from '@loopback/testlab'; +import {TodoListApplication} from '../../src/application'; +import {Todo} from '../../src/models/'; +import {TodoRepository} from '../../src/repositories/'; +import {givenTodo} from '../helpers'; + +describe('Application', () => { + let app: TodoListApplication; + let client: supertest.SuperTest; + let todoRepo: TodoRepository; + + before(givenRunningApplicationWithCustomConfiguration); + after(() => app.stop()); + + before(givenTodoRepository); + before(() => { + client = createClientForHandler(app.requestHandler); + }); + + beforeEach(async () => { + await todoRepo.deleteAll(); + }); + + it('creates a todo', async function() { + const todo = givenTodo(); + const response = await client + .post('/todos') + .send(todo) + .expect(200); + expect(response.body).to.containDeep(todo); + const result = await todoRepo.findById(response.body.id); + expect(result).to.containDeep(todo); + }); + + it('rejects requests to create a todo with no title', async () => { + const todo = givenTodo(); + delete todo.title; + await client + .post('/todos') + .send(todo) + .expect(422); + }); + + it('gets a todo by ID', async () => { + const todo = await givenTodoInstance(); + const result = await client + .get(`/todos/${todo.id}`) + .send() + .expect(200); + // Remove any undefined properties that cannot be represented in JSON/REST + const expected = JSON.parse(JSON.stringify(todo)); + expect(result.body).to.deepEqual(expected); + }); + + it('replaces the todo by ID', async () => { + const todo = await givenTodoInstance(); + const updatedTodo = givenTodo({ + title: 'DO SOMETHING AWESOME', + desc: 'It has to be something ridiculous', + isComplete: true, + }); + await client + .put(`/todos/${todo.id}`) + .send(updatedTodo) + .expect(200); + const result = await todoRepo.findById(todo.id); + expect(result).to.containEql(updatedTodo); + }); + + it('updates the todo by ID ', async () => { + const todo = await givenTodoInstance(); + const updatedTodo = givenTodo({ + title: 'DO SOMETHING AWESOME', + isComplete: true, + }); + await client + .patch(`/todos/${todo.id}`) + .send(updatedTodo) + .expect(200); + const result = await todoRepo.findById(todo.id); + expect(result).to.containEql(updatedTodo); + }); + + it('deletes the todo', async () => { + const todo = await givenTodoInstance(); + await client + .del(`/todos/${todo.id}`) + .send() + .expect(200); + try { + await todoRepo.findById(todo.id); + } catch (err) { + expect(err).to.match(/No Todo found with id/); + return; + } + throw new Error('No error was thrown!'); + }); + + /* + ============================================================================ + 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: { + port: 0, + }, + }); + + 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 givenTodoRepository() { + todoRepo = await app.getRepository(TodoRepository); + } + + async function givenTodoInstance(todo?: Partial) { + return await todoRepo.create(givenTodo(todo)); + } +}); diff --git a/examples/todo-list/test/helpers.ts b/examples/todo-list/test/helpers.ts new file mode 100644 index 000000000000..648376dfb277 --- /dev/null +++ b/examples/todo-list/test/helpers.ts @@ -0,0 +1,52 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Todo, TodoList} from '../src/models'; + +/* + ============================================================================== + HELPER FUNCTIONS + If you find yourself creating the same helper functions across different + test files, then extracting those functions into helper modules is an easy + way to reduce duplication. + + Other tips: + + - Using the super awesome Partial type in conjunction with Object.assign + means you can: + * customize the object you get back based only on what's important + to you during a particular test + * avoid writing test logic that is brittle with respect to the properties + of your object + - Making the input itself optional means you don't need to do anything special + for tests where the particular details of the input don't matter. + ============================================================================== + * + +/** + * Generate a complete Todo object for use with tests. + * @param todo A partial (or complete) Todo object. + */ +export function givenTodo(todo?: Partial) { + const data = Object.assign( + { + title: 'do a thing', + desc: 'There are some things that need doing', + isComplete: false, + }, + todo, + ); + return new Todo(data); +} + +export function givenTodoList(todoList?: Partial) { + const data = Object.assign( + { + title: 'List of things', + }, + todoList, + ); + return new TodoList(data); +} diff --git a/examples/todo-list/test/unit/controllers/todo-list-todo.controller.unit.ts b/examples/todo-list/test/unit/controllers/todo-list-todo.controller.unit.ts new file mode 100644 index 000000000000..6a36227d4e07 --- /dev/null +++ b/examples/todo-list/test/unit/controllers/todo-list-todo.controller.unit.ts @@ -0,0 +1,156 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, sinon} from '@loopback/testlab'; +import {TodoListTodoController} from '../../../src/controllers'; +import {TodoList, Todo} from '../../../src/models'; +import {TodoListRepository} from '../../../src/repositories'; +import {givenTodoList, givenTodo} from '../../helpers'; +import { + DefaultHasManyEntityCrudRepository, + HasManyRepository, +} from '@loopback/repository'; + +describe('TodoController', () => { + let todoListRepo: TodoListRepository; + let constrainedTodoRepo: HasManyRepository; + + /* + ============================================================================= + REPOSITORY FACTORY STUB + This handle give us a quick way to fake the response of our repository + without needing to wrangle fake repository objects or manage real ones + in our tests themselves. + ============================================================================= + */ + let todos: sinon.SinonStub; + + /* + ============================================================================= + METHOD STUBS + These handles give us a quick way to fake the response of our repository + without needing to wrangle fake repository objects or manage real ones + in our tests themselves. + ============================================================================= + */ + let create: sinon.SinonStub; + let find: sinon.SinonStub; + let patch: sinon.SinonStub; + let del: sinon.SinonStub; + + /* + ============================================================================= + TEST VARIABLES + Combining top-level objects with our resetRepositories method means we don't + need to duplicate several variable assignments (and generation statements) + in all of our test logic. + + NOTE: If you wanted to parallelize your test runs, you should avoid this + pattern since each of these tests is sharing references. + ============================================================================= + */ + let controller: TodoListTodoController; + let aTodoListWithId: TodoList; + let aTodo: Todo; + let aTodoWithId: Todo; + let aListOfTodos: Todo[]; + let aTodoToPatchTo: Todo; + let aChangedTodo: Todo; + + beforeEach(resetRepositories); + + describe('create()', () => { + it('creates a todo on a todoList', async () => { + create.resolves(aTodoWithId); + expect(await controller.create(aTodoListWithId.id!, aTodo)).to.eql( + aTodoWithId, + ); + sinon.assert.calledWith(todos, aTodoListWithId.id!); + sinon.assert.calledWith(create, aTodo); + }); + }); + + describe('find()', () => { + it('returns multiple todos if they exist', async () => { + find.resolves(aListOfTodos); + expect(await controller.find(aTodoListWithId.id!)).to.eql(aListOfTodos); + sinon.assert.calledWith(todos, aTodoListWithId.id!); + sinon.assert.called(find); + }); + + it('returns empty list if no todos exist', async () => { + const expected: Todo[] = []; + find.resolves(expected); + expect(await controller.find(aTodoListWithId.id!)).to.eql(expected); + sinon.assert.calledWith(todos, aTodoListWithId.id!); + sinon.assert.called(find); + }); + }); + + describe('patch()', () => { + it('returns a number of todos updated', async () => { + patch.resolves([aChangedTodo].length); + const where = {title: aTodoWithId.title}; + expect( + await controller.patch(aTodoListWithId.id!, aTodoToPatchTo, where), + ).to.eql(1); + sinon.assert.calledWith(todos, aTodoListWithId.id!); + sinon.assert.calledWith(patch, aTodoToPatchTo, where); + }); + }); + + describe('deleteAll()', () => { + it('successfully deletes existing items', async () => { + del.resolves(aListOfTodos.length); + expect(await controller.delete(aTodoListWithId.id!)).to.eql( + aListOfTodos.length, + ); + sinon.assert.calledWith(todos, aTodoListWithId.id!); + sinon.assert.called(del); + }); + }); + + function resetRepositories() { + todoListRepo = sinon.createStubInstance(TodoListRepository); + constrainedTodoRepo = sinon.createStubInstance( + DefaultHasManyEntityCrudRepository, + ); + + aTodoListWithId = givenTodoList({ + id: 1, + }); + + aTodo = givenTodo(); + aTodoWithId = givenTodo({id: 1}); + aListOfTodos = [ + aTodoWithId, + givenTodo({ + id: 2, + title: 'do another thing', + }), + ] as Todo[]; + aTodoToPatchTo = givenTodo({ + title: 'revised thing to do', + }); + aChangedTodo = givenTodo({ + id: aTodoWithId.id, + title: aTodoToPatchTo.title, + }); + + todoListRepo.todos = sinon + .stub() + .withArgs(aTodoListWithId.id!) + .returns(constrainedTodoRepo); + todos = todoListRepo.todos as sinon.SinonStub; + + // Setup CRUD fakes + create = constrainedTodoRepo.create as sinon.SinonStub; + find = constrainedTodoRepo.find as sinon.SinonStub; + patch = constrainedTodoRepo.patch as sinon.SinonStub; + del = constrainedTodoRepo.delete as sinon.SinonStub; + + controller = new TodoListTodoController(todoListRepo); + } +}); diff --git a/examples/todo-list/test/unit/controllers/todo-list.controller.unit.ts b/examples/todo-list/test/unit/controllers/todo-list.controller.unit.ts new file mode 100644 index 000000000000..58dcaa8b9f5a --- /dev/null +++ b/examples/todo-list/test/unit/controllers/todo-list.controller.unit.ts @@ -0,0 +1,170 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, sinon} from '@loopback/testlab'; +import {TodoListController} from '../../../src/controllers'; +import {TodoList} from '../../../src/models'; +import {TodoListRepository} from '../../../src/repositories'; +import {givenTodoList} from '../../helpers'; + +describe('TodoController', () => { + let todoListRepo: TodoListRepository; + + /* + ============================================================================= + METHOD STUBS + These handles give us a quick way to fake the response of our repository + without needing to wrangle fake repository objects or manage real ones + in our tests themselves. + ============================================================================= + */ + let create: sinon.SinonStub; + let count: sinon.SinonStub; + let find: sinon.SinonStub; + let updateAll: sinon.SinonStub; + let deleteAll: sinon.SinonStub; + let findById: sinon.SinonStub; + let updateById: sinon.SinonStub; + let deleteById: sinon.SinonStub; + + /* + ============================================================================= + TEST VARIABLES + Combining top-level objects with our resetRepositories method means we don't + need to duplicate several variable assignments (and generation statements) + in all of our test logic. + + NOTE: If you wanted to parallelize your test runs, you should avoid this + pattern since each of these tests is sharing references. + ============================================================================= + */ + let controller: TodoListController; + let aTodoList: TodoList; + let aTodoListWithId: TodoList; + let aTodoListToPatchTo: TodoList; + let aChangedTodoList: TodoList; + let aListOfTodoLists: TodoList[]; + + beforeEach(resetRepositories); + + describe('create()', () => { + it('creates a TodoList', async () => { + create.resolves(aTodoListWithId); + expect(await controller.create(aTodoList)).to.eql(aTodoListWithId); + sinon.assert.calledWith(create, aTodoList); + }); + }); + + describe('count()', () => { + it('returns the number of existing todoLists', async () => { + count.resolves(aListOfTodoLists.length); + expect(await controller.count()).to.eql(aListOfTodoLists.length); + sinon.assert.called(count); + }); + }); + + describe('find()', () => { + it('returns multiple todos if they exist', async () => { + find.resolves(aListOfTodoLists); + expect(await controller.find()).to.eql(aListOfTodoLists); + sinon.assert.called(find); + }); + + it('returns empty list if no todos exist', async () => { + const expected: TodoList[] = []; + find.resolves(expected); + expect(await controller.find()).to.eql(expected); + sinon.assert.called(find); + }); + }); + + describe('updateAll()', () => { + it('returns a number of todos updated', async () => { + updateAll.resolves([aChangedTodoList].length); + const where = {title: aTodoListWithId.title}; + expect(await controller.updateAll(aTodoListToPatchTo, where)).to.eql(1); + sinon.assert.calledWith(updateAll, aTodoListToPatchTo, where); + }); + }); + + describe('deleteAll()', () => { + it('successfully deletes existing items', async () => { + deleteAll.resolves(aListOfTodoLists.length); + expect(await controller.deleteAll()).to.eql(aListOfTodoLists.length); + sinon.assert.called(deleteAll); + }); + }); + + describe('findById()', () => { + it('returns a todo if it exists', async () => { + findById.resolves(aTodoListWithId); + expect(await controller.findById(aTodoListWithId.id as number)).to.eql( + aTodoListWithId, + ); + sinon.assert.calledWith(findById, aTodoListWithId.id); + }); + }); + + describe('updateById', () => { + it('successfully updates existing items', async () => { + updateById.resolves(true); + expect( + await controller.updateById( + aTodoListWithId.id as number, + aTodoListToPatchTo, + ), + ).to.eql(true); + sinon.assert.calledWith( + updateById, + aTodoListWithId.id, + aTodoListToPatchTo, + ); + }); + }); + + describe('deleteById', () => { + it('successfully deletes existing items', async () => { + deleteById.resolves(true); + expect(await controller.deleteById(aTodoListWithId.id as number)).to.eql( + true, + ); + sinon.assert.calledWith(deleteById, aTodoListWithId.id); + }); + }); + + function resetRepositories() { + todoListRepo = sinon.createStubInstance(TodoListRepository); + aTodoList = givenTodoList(); + aTodoListWithId = givenTodoList({ + id: 1, + }); + aListOfTodoLists = [ + aTodoListWithId, + givenTodoList({ + id: 2, + title: 'a lot of todos', + }), + ] as TodoList[]; + aTodoListToPatchTo = givenTodoList({ + title: 'changed list of todos', + }); + aChangedTodoList = givenTodoList({ + id: aTodoListWithId.id, + title: aTodoListToPatchTo.title, + }); + + // Setup CRUD fakes + create = todoListRepo.create as sinon.SinonStub; + count = todoListRepo.count as sinon.SinonStub; + find = todoListRepo.find as sinon.SinonStub; + updateAll = todoListRepo.updateAll as sinon.SinonStub; + deleteAll = todoListRepo.deleteAll as sinon.SinonStub; + findById = todoListRepo.findById as sinon.SinonStub; + updateById = todoListRepo.updateById as sinon.SinonStub; + deleteById = todoListRepo.deleteById as sinon.SinonStub; + + controller = new TodoListController(todoListRepo); + } +}); diff --git a/examples/todo-list/test/unit/controllers/todo.controller.unit.ts b/examples/todo-list/test/unit/controllers/todo.controller.unit.ts new file mode 100644 index 000000000000..db3f21492897 --- /dev/null +++ b/examples/todo-list/test/unit/controllers/todo.controller.unit.ts @@ -0,0 +1,141 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/example-todo-list +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {expect, sinon} from '@loopback/testlab'; +import {TodoController} from '../../../src/controllers'; +import {Todo} from '../../../src/models'; +import {TodoRepository} from '../../../src/repositories'; +import {givenTodo} from '../../helpers'; + +describe('TodoController', () => { + let todoRepo: TodoRepository; + + /* + ============================================================================= + METHOD STUBS + These handles give us a quick way to fake the response of our repository + without needing to wrangle fake repository objects or manage real ones + in our tests themselves. + ============================================================================= + */ + let create: sinon.SinonStub; + let findById: sinon.SinonStub; + let find: sinon.SinonStub; + let replaceById: sinon.SinonStub; + let updateById: sinon.SinonStub; + let deleteById: sinon.SinonStub; + + /* + ============================================================================= + TEST VARIABLES + Combining top-level objects with our resetRepositories method means we don't + need to duplicate several variable assignments (and generation statements) + in all of our test logic. + + NOTE: If you wanted to parallelize your test runs, you should avoid this + pattern since each of these tests is sharing references. + ============================================================================= + */ + let controller: TodoController; + let aTodo: Todo; + let aTodoWithId: Todo; + let aChangedTodo: Todo; + let aListOfTodos: Todo[]; + + beforeEach(resetRepositories); + + describe('createTodo', () => { + it('creates a Todo', async () => { + create.resolves(aTodoWithId); + const result = await controller.createTodo(aTodo); + expect(result).to.eql(aTodoWithId); + sinon.assert.calledWith(create, aTodo); + }); + }); + + describe('findTodoById', () => { + it('returns a todo if it exists', async () => { + findById.resolves(aTodoWithId); + expect(await controller.findTodoById(aTodoWithId.id as number)).to.eql( + aTodoWithId, + ); + sinon.assert.calledWith(findById, aTodoWithId.id); + }); + }); + + describe('findTodos', () => { + it('returns multiple todos if they exist', async () => { + find.resolves(aListOfTodos); + expect(await controller.findTodos()).to.eql(aListOfTodos); + sinon.assert.called(find); + }); + + it('returns empty list if no todos exist', async () => { + const expected: Todo[] = []; + find.resolves(expected); + expect(await controller.findTodos()).to.eql(expected); + sinon.assert.called(find); + }); + }); + + describe('replaceTodo', () => { + it('successfully replaces existing items', async () => { + replaceById.resolves(true); + expect( + await controller.replaceTodo(aTodoWithId.id as number, aChangedTodo), + ).to.eql(true); + sinon.assert.calledWith(replaceById, aTodoWithId.id, aChangedTodo); + }); + }); + + describe('updateTodo', () => { + it('successfully updates existing items', async () => { + updateById.resolves(true); + expect( + await controller.updateTodo(aTodoWithId.id as number, aChangedTodo), + ).to.eql(true); + sinon.assert.calledWith(updateById, aTodoWithId.id, aChangedTodo); + }); + }); + + describe('deleteTodo', () => { + it('successfully deletes existing items', async () => { + deleteById.resolves(true); + expect(await controller.deleteTodo(aTodoWithId.id as number)).to.eql( + true, + ); + sinon.assert.calledWith(deleteById, aTodoWithId.id); + }); + }); + + function resetRepositories() { + todoRepo = sinon.createStubInstance(TodoRepository); + aTodo = givenTodo(); + aTodoWithId = givenTodo({ + id: 1, + }); + aListOfTodos = [ + aTodoWithId, + givenTodo({ + id: 2, + title: 'so many things to do', + }), + ] as Todo[]; + aChangedTodo = givenTodo({ + id: aTodoWithId.id, + title: 'Do some important things', + }); + + // Setup CRUD fakes + create = todoRepo.create as sinon.SinonStub; + findById = todoRepo.findById as sinon.SinonStub; + find = todoRepo.find as sinon.SinonStub; + updateById = todoRepo.updateById as sinon.SinonStub; + replaceById = todoRepo.replaceById as sinon.SinonStub; + deleteById = todoRepo.deleteById as sinon.SinonStub; + + controller = new TodoController(todoRepo); + } +}); diff --git a/examples/todo-list/tsconfig.build.json b/examples/todo-list/tsconfig.build.json new file mode 100644 index 000000000000..d9dc5d30c3df --- /dev/null +++ b/examples/todo-list/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "./node_modules/@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["index.ts", "src", "test"] +} diff --git a/examples/todo-list/tslint.build.json b/examples/todo-list/tslint.build.json new file mode 100644 index 000000000000..0ace3417fa90 --- /dev/null +++ b/examples/todo-list/tslint.build.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/tslint", + "extends": ["./node_modules/@loopback/build/config/tslint.build.json"] +} diff --git a/examples/todo-list/tslint.json b/examples/todo-list/tslint.json new file mode 100644 index 000000000000..098c9615a68d --- /dev/null +++ b/examples/todo-list/tslint.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/tslint", + "extends": ["./node_modules/@loopback/build/config/tslint.common.json"] +} diff --git a/examples/todo/README.md b/examples/todo/README.md index fb70b47bc53a..e1057c69fb7c 100644 --- a/examples/todo/README.md +++ b/examples/todo/README.md @@ -55,9 +55,10 @@ application, follow these steps: ```sh $ lb4 example ? What example would you like to clone? (Use arrow keys) -❯ todo: Tutorial example on how to build an application with LoopBack 4.. - hello-world: A simple hello-world Application using LoopBack 4 - log-extension: An example extension project for LoopBack 4 +> todo: Tutorial example on how to build an application with LoopBack 4. + todo-list: Continuation of the todo example using relations in LoopBack 4. + hello-world: A simple hello-world Application using LoopBack 4. + log-extension: An example extension project for LoopBack 4. rpc-server: A basic RPC server using a made-up protocol. ``` diff --git a/examples/todo/test/unit/controllers/todo.controller.unit.ts b/examples/todo/test/unit/controllers/todo.controller.unit.ts index bc8f2b253349..cec015d6b715 100644 --- a/examples/todo/test/unit/controllers/todo.controller.unit.ts +++ b/examples/todo/test/unit/controllers/todo.controller.unit.ts @@ -45,7 +45,7 @@ describe('TodoController', () => { let aTodo: Todo; let aTodoWithId: Todo; let aChangedTodo: Todo; - let aTodoList: Todo[]; + let aListOfTodos: Todo[]; beforeEach(resetRepositories); @@ -88,8 +88,8 @@ describe('TodoController', () => { describe('findTodos', () => { it('returns multiple todos if they exist', async () => { - find.resolves(aTodoList); - expect(await controller.findTodos()).to.eql(aTodoList); + find.resolves(aListOfTodos); + expect(await controller.findTodos()).to.eql(aListOfTodos); sinon.assert.called(find); }); @@ -137,7 +137,7 @@ describe('TodoController', () => { aTodoWithId = givenTodo({ id: 1, }); - aTodoList = [ + aListOfTodos = [ aTodoWithId, givenTodo({ id: 2, diff --git a/packages/cli/generators/example/index.js b/packages/cli/generators/example/index.js index 8132d75bcfa2..20739d3d523b 100644 --- a/packages/cli/generators/example/index.js +++ b/packages/cli/generators/example/index.js @@ -13,8 +13,10 @@ const utils = require('../../lib/utils'); const EXAMPLES = { todo: 'Tutorial example on how to build an application with LoopBack 4.', - 'hello-world': 'A simple hello-world Application using LoopBack 4', - 'log-extension': 'An example extension project for LoopBack 4', + 'todo-list': + 'Continuation of the todo example using relations in LoopBack 4.', + 'hello-world': 'A simple hello-world Application using LoopBack 4.', + 'log-extension': 'An example extension project for LoopBack 4.', 'rpc-server': 'A basic RPC server using a made-up protocol.', }; Object.freeze(EXAMPLES);