-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(rest-crud): add CrudRestApiBuilder #4589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| // Copyright IBM Corp. 2020. All Rights Reserved. | ||
| // Node module: @loopback/boot | ||
| // This file is licensed under the MIT License. | ||
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| import {ApplicationConfig} from '@loopback/core'; | ||
| import {juggler, RepositoryMixin} from '@loopback/repository'; | ||
| import {RestApplication} from '@loopback/rest'; | ||
| import {CrudRestComponent} from '@loopback/rest-crud'; | ||
| import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab'; | ||
| import {resolve} from 'path'; | ||
| import {BootMixin, ModelApiBooter} from '../..'; | ||
| import {ProductRepository} from '../fixtures/product.repository'; | ||
|
|
||
| describe('CRUD rest builder acceptance tests', () => { | ||
| let app: BooterApp; | ||
| const SANDBOX_PATH = resolve(__dirname, '../../.sandbox'); | ||
| const sandbox = new TestSandbox(SANDBOX_PATH); | ||
|
|
||
| beforeEach('reset sandbox', () => sandbox.reset()); | ||
| beforeEach(givenAppWithDataSource); | ||
|
|
||
| afterEach(stopApp); | ||
|
|
||
| it('binds the controller and repository to the application', async () => { | ||
| await sandbox.copyFile( | ||
| resolve(__dirname, '../fixtures/product.model.js'), | ||
| 'models/product.model.js', | ||
| ); | ||
|
|
||
| // when creating the config file in a real app, make sure to use | ||
| // module.exports = <ModelCrudRestApiConfig>{...} | ||
| // it's not used here because this is a .js file | ||
| await sandbox.writeTextFile( | ||
| 'model-endpoints/product.rest-config.js', | ||
| ` | ||
| const {Product} = require('../models/product.model'); | ||
| module.exports = { | ||
| model: Product, | ||
| pattern: 'CrudRest', | ||
| dataSource: 'db', | ||
| basePath: '/products', | ||
| }; | ||
| `, | ||
| ); | ||
|
|
||
| // Boot & start the application | ||
| await app.boot(); | ||
| await app.start(); | ||
|
|
||
| expect(app.getBinding('repositories.ProductRepository').key).to.eql( | ||
| 'repositories.ProductRepository', | ||
| ); | ||
|
|
||
| expect(app.getBinding('controllers.ProductController').key).to.eql( | ||
| 'controllers.ProductController', | ||
| ); | ||
| }); | ||
|
|
||
| it('uses bound repository class if it exists', async () => { | ||
| await sandbox.copyFile( | ||
| resolve(__dirname, '../fixtures/product.model.js'), | ||
| 'models/product.model.js', | ||
| ); | ||
|
|
||
| await sandbox.writeTextFile( | ||
| 'model-endpoints/product.rest-config.js', | ||
| ` | ||
| const {Product} = require('../models/product.model'); | ||
| module.exports = { | ||
| model: Product, | ||
| pattern: 'CrudRest', | ||
| dataSource: 'db', | ||
| basePath: '/products', | ||
| }; | ||
| `, | ||
| ); | ||
|
|
||
| app.repository(ProductRepository); | ||
|
|
||
| const bindingName = 'repositories.ProductRepository'; | ||
|
|
||
| const binding = app.getBinding(bindingName); | ||
| expect(binding.valueConstructor).to.eql(ProductRepository); | ||
|
|
||
| // Boot & start the application | ||
| await app.boot(); | ||
| await app.start(); | ||
|
|
||
| // Make sure it is still equal to the defined ProductRepository after | ||
| // booting | ||
| expect(app.getBinding(bindingName).valueConstructor).to.eql( | ||
| ProductRepository, | ||
| ); | ||
|
|
||
| expect(app.getBinding('controllers.ProductController').key).to.eql( | ||
| 'controllers.ProductController', | ||
| ); | ||
| }); | ||
|
|
||
| it('throws if there is no base path in the config', async () => { | ||
| await sandbox.copyFile( | ||
| resolve(__dirname, '../fixtures/product.model.js'), | ||
| 'models/product.model.js', | ||
| ); | ||
|
|
||
| await sandbox.writeTextFile( | ||
| 'model-endpoints/product.rest-config.js', | ||
| ` | ||
| const {Product} = require('../models/product.model'); | ||
| module.exports = { | ||
| model: Product, | ||
| pattern: 'CrudRest', | ||
| dataSource: 'db', | ||
| // basePath not specified | ||
| }; | ||
| `, | ||
| ); | ||
|
|
||
| // Boot the application | ||
| await expect(app.boot()).to.be.rejectedWith( | ||
| /Missing required field "basePath" in configuration for model Product./, | ||
| ); | ||
| }); | ||
|
|
||
| it('throws if a Model is used instead of an Entity', async () => { | ||
| await sandbox.copyFile( | ||
| resolve(__dirname, '../fixtures/no-entity.model.js'), | ||
| 'models/no-entity.model.js', | ||
| ); | ||
|
|
||
| await sandbox.writeTextFile( | ||
| 'model-endpoints/no-entity.rest-config.js', | ||
| ` | ||
| const {NoEntity} = require('../models/no-entity.model'); | ||
| module.exports = { | ||
| // this model extends Model, not Entity | ||
| model: NoEntity, | ||
| pattern: 'CrudRest', | ||
| dataSource: 'db', | ||
| basePath: '/no-entities', | ||
| }; | ||
| `, | ||
| ); | ||
|
|
||
| // Boot the application | ||
| await expect(app.boot()).to.be.rejectedWith( | ||
| /CrudRestController requires a model that extends 'Entity'./, | ||
| ); | ||
| }); | ||
|
|
||
| class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) { | ||
| constructor(options?: ApplicationConfig) { | ||
| super(options); | ||
| this.projectRoot = sandbox.path; | ||
| this.booters(ModelApiBooter); | ||
| this.component(CrudRestComponent); | ||
| } | ||
| } | ||
|
|
||
| async function givenAppWithDataSource() { | ||
| app = new BooterApp({ | ||
| rest: givenHttpServerConfig(), | ||
| }); | ||
| app.dataSource(new juggler.DataSource({connector: 'memory'}), 'db'); | ||
| } | ||
|
|
||
| async function stopApp() { | ||
| if (app.state !== 'started') return; | ||
| await app.stop(); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Copyright IBM Corp. 2020. All Rights Reserved. | ||
| // Node module: @loopback/boot | ||
| // This file is licensed under the MIT License. | ||
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| import {model, Model, property} from '@loopback/repository'; | ||
|
|
||
| @model() | ||
| export class NoEntity extends Model { | ||
| @property({id: true}) | ||
| id: number; | ||
|
|
||
| @property({required: true}) | ||
| name: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // Copyright IBM Corp. 2020. All Rights Reserved. | ||
| // Node module: @loopback/boot | ||
| // This file is licensed under the MIT License. | ||
| // License text available at https://opensource.org/licenses/MIT | ||
|
|
||
| import {inject} from '@loopback/core'; | ||
| import {DefaultCrudRepository, juggler} from '@loopback/repository'; | ||
| import {Product} from './product.model'; | ||
|
|
||
| export class ProductRepository extends DefaultCrudRepository< | ||
| Product, | ||
| typeof Product.prototype.id | ||
| > { | ||
| constructor(@inject('datasources.db') dataSource: juggler.DataSource) { | ||
| super(Product, dataSource); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,11 +15,60 @@ npm install --save @loopback/rest-crud | |
|
|
||
| ## Basic use | ||
|
|
||
| `@loopback/rest-crud` exposes two helper methods (`defineCrudRestController` and | ||
| `defineCrudRepositoryClass`) for creating controllers and respositories using | ||
| code. | ||
| `@loopback/rest-crud` can be used along with the built-in `ModelApiBooter` to | ||
| easily create a repository class and a controller class for your model. The | ||
| following use is a simple approach for this creation, however, you can look at | ||
| the "Advanced use" section instead for a more flexible approach. | ||
|
|
||
| For the examples in the following sections, we are assuming a model named | ||
| `Product` and a datasource named `db` have already been created. | ||
|
|
||
| In your `src/application.ts` file: | ||
|
|
||
| ```ts | ||
| // add the following import | ||
| import {CrudRestComponent} from '@loopback/rest-crud'; | ||
|
|
||
| export class TryApplication extends BootMixin( | ||
| ServiceMixin(RepositoryMixin(RestApplication)), | ||
| ) { | ||
| constructor(options: ApplicationConfig = {}) { | ||
| // other code | ||
|
|
||
| // add the following line | ||
| this.component(CrudRestComponent); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Create a new file for the configuration, e.g. | ||
| `src/model-endpoints/product.rest-config.ts` that defines the `model`, | ||
| `pattern`, `dataSource`, and `basePath` properties: | ||
|
|
||
| ```ts | ||
| import {ModelCrudRestApiConfig} from '@loopback/rest-crud'; | ||
| import {Product} from '../models'; | ||
|
|
||
| module.exports = <ModelCrudRestApiConfig>{ | ||
| model: Product, | ||
| pattern: 'CrudRest', // make sure to use this pattern | ||
| dataSource: 'db', | ||
| basePath: '/products', | ||
| }; | ||
| ``` | ||
|
|
||
| Now your `Product` model will have a default repository and default controller | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So there are 2 ways that a developer can create the
So they are mutually exclusive then? If so let's specify that Basic and Advanced are mutually exclusive. Another question: why would someone want to do Advanced steps if it is shorter to perform Basic steps?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, I'll specify that, thanks!
The advanced one helps if you only want to create the default controller or default repository but not both. |
||
| class defined without the need for a repository or controller class file. | ||
|
|
||
| ## Advanced use | ||
|
|
||
| If you would like more flexibility, e.g. if you would only like to define a | ||
| default `CrudRest` controller or repository, you can use the two helper methods | ||
| (`defineCrudRestController` and `defineCrudRepositoryClass`) exposed from | ||
| `@loopback/rest-crud`. These functions will help you create controllers and | ||
| respositories using code. | ||
|
|
||
| For the examples in the following sections, we are also assuming a model named | ||
| `Product`, and a datasource named `db` have already been created. | ||
|
|
||
| ### Creating a CRUD Controller | ||
|
|
@@ -37,7 +86,7 @@ endpoints of an existing model with a respository. | |
| >(Product, {basePath: '/products'}); | ||
| ``` | ||
|
|
||
| 2. Set up dependency injection for the ProductController. | ||
| 2. Set up dependency injection for the `ProductController`. | ||
|
|
||
| ```ts | ||
| inject('repositories.ProductRepository')(ProductController, undefined, 0); | ||
|
|
@@ -73,10 +122,10 @@ export class TryApplication extends BootMixin( | |
| ServiceMixin(RepositoryMixin(RestApplication)), | ||
| ) { | ||
| constructor(options: ApplicationConfig = {}) { | ||
| ... | ||
| // ... | ||
| } | ||
|
|
||
| async boot():Promise<void> { | ||
| async boot(): Promise<void> { | ||
| await super.boot(); | ||
|
|
||
| const ProductRepository = defineCrudRepositoryClass(Product); | ||
|
|
@@ -85,9 +134,9 @@ export class TryApplication extends BootMixin( | |
| inject('datasources.db')(ProductRepository, undefined, 0); | ||
|
|
||
| const ProductController = defineCrudRestController< | ||
| Product, | ||
| typeof Product.prototype.id, | ||
| 'id' | ||
| Product, | ||
| typeof Product.prototype.id, | ||
| 'id' | ||
| >(Product, {basePath: '/products'}); | ||
|
|
||
| inject(repoBinding.key)(ProductController, undefined, 0); | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.