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
  • Loading branch information
raymondfeng committed Jun 1, 2020
1 parent dd5143f commit a30c458
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 1 deletion.
2 changes: 2 additions & 0 deletions packages/rest-crud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"tslib": "^2.0.0"
},
"devDependencies": {
"@loopback/boot": "^2.3.2",
"@loopback/build": "^1.7.1",
"@loopback/core": "^2.7.1",
"@loopback/repository": "^2.6.0",
Expand All @@ -35,6 +36,7 @@
"@types/node": "^10.17.24"
},
"peerDependencies": {
"@loopback/boot": "^2.3.2",
"@loopback/core": "^2.7.1",
"@loopback/repository": "^2.6.0",
"@loopback/rest": "^5.1.0"
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 {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') return;
await app.stop();
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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() {
try {
await app.stop();
} catch (err) {
// console.error('Cannot stop the app, ignoring the error.', err);
}
}
});
16 changes: 16 additions & 0 deletions packages/rest-crud/src/__tests__/fixtures/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright IBM Corp. 2019. 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 {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';

export class BooterApp extends BootMixin(RepositoryMixin(RestApplication)) {
constructor(options?: ApplicationConfig) {
super(options);
this.projectRoot = __dirname;
}
}
14 changes: 14 additions & 0 deletions packages/rest-crud/src/__tests__/fixtures/datasource.artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright IBM Corp. 2019. 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 {juggler} from '@loopback/repository';

export class DbDataSource extends juggler.DataSource {
static dataSourceName = 'db';

constructor() {
super({name: 'db'});
}
}
Loading

0 comments on commit a30c458

Please sign in to comment.