-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rest-crud): add model api booter in the CrudRestComponent
Signed-off-by: Raymond Feng <[email protected]>
- Loading branch information
1 parent
3c5cf4b
commit 4bc457f
Showing
13 changed files
with
524 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
packages/rest-crud/src/__tests__/acceptance/booters/crud-rest.api-builder.acceptance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); |
173 changes: 173 additions & 0 deletions
173
packages/rest-crud/src/__tests__/acceptance/booters/model-api.booter.acceptance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.