Skip to content
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

feat(rest-crud): add CrudRestApiBuilder #4589

Merged
merged 1 commit into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/boot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@loopback/eslint-config": "^5.0.3",
"@loopback/openapi-v3": "^2.0.0",
"@loopback/rest": "^2.0.0",
"@loopback/rest-crud": "^0.6.6",
"@loopback/testlab": "^1.10.3",
"@types/node": "^10.17.16"
},
Expand Down
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();
}
});
15 changes: 15 additions & 0 deletions packages/boot/src/__tests__/fixtures/no-entity.model.ts
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;
}
17 changes: 17 additions & 0 deletions packages/boot/src/__tests__/fixtures/product.repository.ts
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);
}
}
67 changes: 58 additions & 9 deletions packages/rest-crud/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
agnes512 marked this conversation as resolved.
Show resolved Hide resolved
`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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are 2 ways that a developer can create the default controller and default repository for Product?

  1. Performing Basic steps and not performing Advanced steps
  2. Performing Advanced steps and not performing Basic steps

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So they are mutually exclusive then? If so let's specify that Basic and Advanced are mutually exclusive.

Yeah, I'll specify that, thanks!

Another question: why would someone want to do Advanced steps if it is shorter to perform Basic steps?

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
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions packages/rest-crud/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions packages/rest-crud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,23 @@
"author": "IBM Corp.",
"copyright.owner": "IBM Corp.",
"license": "MIT",
"dependencies": {
"@loopback/model-api-builder": "^1.1.3",
"debug": "^4.1.1"
},
"devDependencies": {
"@loopback/build": "^1.7.1",
"@loopback/core": "^1.12.4",
"@loopback/repository": "^1.19.1",
"@loopback/rest": "^2.0.0",
"@loopback/testlab": "^1.10.3",
"@types/node": "^10.17.16"
"@types/node": "^10.17.16",
"@types/debug": "^4.1.5"
},
"peerDependencies": {
"@loopback/repository": "^1.12.0",
"@loopback/rest": "^1.17.0"
"@loopback/core": "^1.12.4",
"@loopback/repository": "^1.19.1",
"@loopback/rest": "^2.0.0"
},
"files": [
"README.md",
Expand Down
Loading