Skip to content

Commit

Permalink
feat(rest-crud): add model api booter in the CrudRestComponent
Browse files Browse the repository at this point in the history
Signed-off-by: Raymond Feng <[email protected]>
  • Loading branch information
raymondfeng committed Aug 19, 2020
1 parent d57387e commit 13c4343
Show file tree
Hide file tree
Showing 13 changed files with 525 additions and 5 deletions.
4 changes: 3 additions & 1 deletion packages/rest-crud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"tslib": "^2.0.1"
},
"devDependencies": {
"@loopback/boot": "^2.3.2",
"@loopback/build": "^1.7.1",
"@loopback/core": "^2.9.3",
"@loopback/repository": "^2.11.0",
Expand All @@ -37,7 +38,8 @@
"peerDependencies": {
"@loopback/core": "^2.9.3",
"@loopback/repository": "^2.11.0",
"@loopback/rest": "^6.0.0"
"@loopback/rest": "^6.0.0",
"@loopback/boot": "^2.3.2"
},
"files": [
"README.md",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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 {BootMixin} from '@loopback/boot';
import {ApplicationConfig} from '@loopback/core';
import {ModelApiBooter} from '@loopback/model-api-builder';
import {juggler, RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {expect, givenHttpServerConfig, TestSandbox} from '@loopback/testlab';
import {resolve} from 'path';
import {CrudRestComponent} from '../../..';
import {ProductRepository} from '../../fixtures/product.repository';

describe('CRUD rest builder acceptance tests', () => {
let app: BooterApp;
const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox'));

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') await app?.stop();
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Copyright IBM Corp. 2019,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 {BootMixin} from '@loopback/boot';
import {ApplicationConfig} from '@loopback/core';
import {ModelApiBooter} from '@loopback/model-api-builder';
import {juggler, RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {
expect,
givenHttpServerConfig,
TestSandbox,
toJSON,
} from '@loopback/testlab';
import {resolve} from 'path';
import {Product} from '../../fixtures/product.model';
import {
buildCalls,
samePatternBuildCalls,
SamePatternModelApiBuilderComponent,
similarPatternBuildCalls,
SimilarPatternModelApiBuilderComponent,
StubModelApiBuilderComponent,
} from '../../fixtures/stub-model-api-builder';

describe('model API booter acceptance tests', () => {
let app: BooterApp;
const sandbox = new TestSandbox(resolve(__dirname, '../../../.sandbox'));

beforeEach('reset sandbox', () => sandbox.reset());
beforeEach(givenAppWithDataSource);

afterEach(stopApp);

it('uses the correct model API builder', 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: 'stub',
dataSource: 'db',
basePath: '/products',
};
`,
);

// Boot & start the application
await app.boot();
await app.start();

expect(toJSON(buildCalls)).to.deepEqual(
toJSON([
{
application: app,
modelClass: Product,
config: {
basePath: '/products',
dataSource: 'db',
pattern: 'stub',
},
},
]),
);
});

it('uses the API builder registered first if there is a duplicate pattern name', 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: 'same',
dataSource: 'db',
basePath: '/products',
};
`,
);

// Boot & start the application
await app.boot();
await app.start();

// registered first
expect(toJSON(samePatternBuildCalls)).to.eql([toJSON(app)]);

expect(similarPatternBuildCalls).to.be.empty();
});

it('throws if there are no patterns matching', 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: 'doesntExist',
dataSource: 'db',
basePath: '/products',
};
`,
);

await expect(app.boot()).to.be.rejectedWith(
/Unsupported API pattern "doesntExist"/,
);
});

it('throws if the model class is invalid', 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 = 'product'
module.exports = {
model: Product,
pattern: 'stub',
dataSource: 'db',
basePath: '/products',
};
`,
);

await expect(app.boot()).to.be.rejectedWith(
/Invalid "model" field\. Expected a Model class, found product/,
);
});

class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) {
constructor(options?: ApplicationConfig) {
super(options);
this.projectRoot = sandbox.path;
this.booters(ModelApiBooter);
this.component(StubModelApiBuilderComponent);
this.component(SamePatternModelApiBuilderComponent);
this.component(SimilarPatternModelApiBuilderComponent);
}
}

async function givenAppWithDataSource() {
app = new BooterApp({
rest: givenHttpServerConfig(),
});
app.dataSource(new juggler.DataSource({connector: 'memory'}), 'db');
}

async function stopApp() {
if (app?.state === 'started') await app?.stop();
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('CrudRestController for a simple Product model', () => {
Object.freeze(PATCH_DATA);

before(setupTestScenario);
after(stopTheApp);
after(stopApp);
beforeEach(cleanDatabase);

describe('create', () => {
Expand Down Expand Up @@ -311,8 +311,8 @@ describe('CrudRestController for a simple Product model', () => {
client = createRestAppClient(app);
}

async function stopTheApp() {
await app.stop();
async function stopApp() {
if (app?.state === 'started') await app?.stop();
}

async function cleanDatabase() {
Expand Down
Loading

0 comments on commit 13c4343

Please sign in to comment.