Skip to content

Commit

Permalink
feat(rest-crud): add CrudRestApiBuilder
Browse files Browse the repository at this point in the history
`CrudRestApiBuilder` is a model API builder that builds the default repository and controller class for a given Entity class.

Co-authored-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
nabdelgadir and bajtos committed Feb 20, 2020
1 parent 3f5cbe1 commit 5286f45
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 12 deletions.
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.
`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
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

0 comments on commit 5286f45

Please sign in to comment.