-
Notifications
You must be signed in to change notification settings - Fork 79
feat(core): update model schema booters #2291
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
Changes from 1 commit
4e29cd1
cb8a02b
a9a4636
ae0750e
e7d2cba
1a2e721
72e5d99
aa894cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import {RestApplication} from '@loopback/rest'; | ||
| import {expect, sinon} from '@loopback/testlab'; | ||
| import { | ||
| ControllerDefaults, | ||
| CoreControllerBooter, | ||
| } from '../../../booters/core-controller.booter'; | ||
|
|
||
| describe('CoreControllerBooter', () => { | ||
| let app: RestApplication; | ||
|
|
||
| class DummyController {} | ||
|
|
||
| class TestBooter extends CoreControllerBooter { | ||
| constructor(app: RestApplication) { | ||
|
Check failure on line 14 in packages/core/src/__tests__/unit/booters/core-controller.booter.unit.test.ts
|
||
| super(app, '.', {}); | ||
| } | ||
|
|
||
| async load(): Promise<void> { | ||
| this.classes = [DummyController]; | ||
| this.classes.forEach(cls => this.bindController(cls)); | ||
| } | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| app = new RestApplication(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| sinon.restore(); | ||
| }); | ||
|
|
||
| it('should bind discovered controllers', async () => { | ||
| const controllerSpy = sinon.spy(app, 'controller'); | ||
|
|
||
| const booter = new TestBooter(app); | ||
| await booter.load(); | ||
|
|
||
| sinon.assert.calledWith(controllerSpy, DummyController); | ||
| expect(app.isBound('controllers.DummyController')).to.be.true(); | ||
| }); | ||
|
|
||
| it('should skip controller binding if already bound', async () => { | ||
| app.controller(DummyController); // Pre-bind it | ||
|
|
||
| const spy = sinon.spy(app, 'controller'); | ||
| const booter = new TestBooter(app); | ||
| await booter.load(); | ||
|
|
||
| sinon.assert.notCalled(spy); | ||
| }); | ||
|
|
||
| it('should use default ControllerDefaults config', () => { | ||
| const config = {}; | ||
| const booter = new CoreControllerBooter(app, '.', config); | ||
|
|
||
| // Access protected property via bracket notation for test purpose | ||
| const options = booter['options']; | ||
|
|
||
| expect(options.dirs).to.deepEqual(ControllerDefaults.dirs); | ||
| expect(options.extensions).to.deepEqual(ControllerDefaults.extensions); | ||
| expect(options.nested).to.equal(ControllerDefaults.nested); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import {Component, Context, MetadataInspector} from '@loopback/core'; | ||
| import {RestApplication} from '@loopback/rest'; | ||
| import {sinon} from '@loopback/testlab'; | ||
| import {CoreModelBooter} from '../../../booters/core-model.booter'; | ||
| import {OVERRIDE_MODEL_SCHEMA_KEY} from '../../../build-schema'; | ||
|
|
||
| describe('CoreModelBooter', () => { | ||
| let app: RestApplication; | ||
| let ctx: Context; | ||
|
|
||
| class DummyBooter extends CoreModelBooter { | ||
| constructor( | ||
| app: RestApplication, | ||
|
Check failure on line 13 in packages/core/src/__tests__/unit/booters/core-model.booter.unit.test.ts
|
||
| basePath: string, | ||
| config: object, | ||
| ctx: Context, | ||
|
Check failure on line 16 in packages/core/src/__tests__/unit/booters/core-model.booter.unit.test.ts
|
||
| ) { | ||
| super(app, basePath, config, ctx); | ||
| } | ||
|
|
||
| // Override discover to prevent requiring real files | ||
| async discover(): Promise<void> { | ||
| // simulate discovery of classes | ||
| this.classes = [class MyComponent {}]; | ||
| } | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| app = new RestApplication(); | ||
| ctx = new Context(app); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| sinon.restore(); | ||
| }); | ||
|
|
||
| it('should skip load when component is not found', async () => { | ||
| const getSyncSpy = sinon.spy(ctx, 'getSync'); | ||
| const booter = new DummyBooter(app, '.', {}, ctx); | ||
| await booter.discover(); | ||
| await booter.load(); | ||
|
|
||
| // Assert context tried to get the component, but failed silently | ||
| sinon.assert.calledWith(getSyncSpy, 'components.MyComponent'); | ||
| }); | ||
|
|
||
| it('should skip load when component has no models', async () => { | ||
| class NoModelComponent implements Component {} | ||
|
|
||
| ctx.bind('components.MyComponent').to(new NoModelComponent()); | ||
|
|
||
| const defineSpy = sinon.spy(MetadataInspector, 'defineMetadata'); | ||
|
|
||
| const booter = new DummyBooter(app, '.', {}, ctx); | ||
| await booter.discover(); | ||
| await booter.load(); | ||
|
|
||
| sinon.assert.notCalled(defineSpy); | ||
| }); | ||
|
|
||
| it('should load models and define metadata', async () => { | ||
| class MyModel {} | ||
| class OverriddenModel {} | ||
|
|
||
| class MyComponent implements Component { | ||
| models = [MyModel]; | ||
| } | ||
|
|
||
| ctx.bind('components.MyComponent').to(new MyComponent()); | ||
| ctx.bind('models.MyModel').to(OverriddenModel); | ||
|
|
||
| const spy = sinon.spy(MetadataInspector, 'defineMetadata'); | ||
|
|
||
| const booter = new DummyBooter(app, '.', {}, ctx); | ||
| await booter.discover(); // sets MyComponent | ||
| await booter.load(); | ||
|
|
||
| sinon.assert.calledOnceWithExactly( | ||
| spy, | ||
| OVERRIDE_MODEL_SCHEMA_KEY, | ||
| OverriddenModel, | ||
| MyModel, | ||
| ); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import {ArtifactOptions, BaseArtifactBooter} from '@loopback/boot'; | ||
| import { | ||
| BindingScope, | ||
| config, | ||
| ControllerClass, | ||
| CoreBindings, | ||
| inject, | ||
| injectable, | ||
| } from '@loopback/core'; | ||
| import {RestApplication} from '@loopback/rest'; | ||
| import path from 'path'; | ||
|
|
||
| @injectable({scope: BindingScope.SINGLETON}) | ||
| export class CoreControllerBooter extends BaseArtifactBooter { | ||
| constructor( | ||
| @inject(CoreBindings.APPLICATION_INSTANCE) | ||
| protected application: RestApplication, | ||
| @inject('paths.base', {optional: true}) | ||
| protected basePath: string = path.resolve(__dirname, '..'), | ||
| @config() | ||
| public controllerConfig: ArtifactOptions = {}, | ||
| ) { | ||
| super( | ||
| basePath, | ||
| // Set Controller Booter Options if passed in via bootConfig | ||
| Object.assign({}, ControllerDefaults, controllerConfig), | ||
| ); | ||
| } | ||
|
|
||
| async load(): Promise<void> { | ||
| await super.load(); | ||
| this.classes.forEach(cls => { | ||
| this.bindController(cls); | ||
| }); | ||
| } | ||
|
|
||
| bindController(controllerClass: ControllerClass<unknown>) { | ||
| const bindingKey = `controllers.${controllerClass.name}`; | ||
| if (!this.application.isBound(bindingKey)) { | ||
| this.application.controller(controllerClass); | ||
| } else { | ||
| return; // If already bound, skip | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Default ArtifactOptions for ControllerBooter. | ||
| */ | ||
| export const ControllerDefaults: ArtifactOptions = { | ||
| dirs: ['controllers'], | ||
| extensions: ['.controller.js'], | ||
| nested: true, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { | ||
| ArtifactOptions, | ||
| BaseArtifactBooter, | ||
| loadClassesFromFiles, | ||
| } from '@loopback/boot'; | ||
| import { | ||
| BindingScope, | ||
| Component, | ||
| config, | ||
| Context, | ||
| CoreBindings, | ||
| inject, | ||
| injectable, | ||
| MetadataInspector, | ||
| } from '@loopback/core'; | ||
| import {RestApplication} from '@loopback/rest'; | ||
| import {glob} from 'glob'; | ||
| import path from 'path'; | ||
| import {OVERRIDE_MODEL_SCHEMA_KEY} from '../build-schema'; | ||
|
|
||
| @injectable({scope: BindingScope.SINGLETON}) | ||
| export class CoreModelBooter extends BaseArtifactBooter { | ||
| constructor( | ||
| @inject(CoreBindings.APPLICATION_INSTANCE) | ||
| protected application: RestApplication, | ||
| @inject('paths.base', {optional: true}) | ||
| protected basePath: string = path.resolve(__dirname, '..'), | ||
| @config() | ||
| public artifactConfig: ArtifactOptions = {}, | ||
| @inject.context() | ||
| protected readonly context: Context, | ||
| ) { | ||
| super(basePath, Object.assign({}, artifactConfig)); | ||
| } | ||
|
|
||
| async discover(): Promise<void> { | ||
| try { | ||
|
Check failure on line 37 in packages/core/src/booters/core-model.booter.ts
|
||
| const pattern = path.join(this.projectRoot, '**', '*component.js'); | ||
| const filePaths = glob.sync(pattern, {nodir: true}); | ||
| this.classes = loadClassesFromFiles(filePaths, this.basePath); | ||
| } catch (error) { | ||
| throw error; // rethrow for visibility | ||
| } | ||
| } | ||
|
|
||
| async load(): Promise<void> { | ||
| await this.discover(); | ||
| this.classes.forEach(cls => { | ||
| let componentInstance: Component; | ||
| try { | ||
| componentInstance = this.context.getSync<Component>( | ||
| `components.${cls.name}`, | ||
| ); | ||
| } catch (_) { | ||
| // If the component is not found, we skip it | ||
| return; | ||
| } | ||
|
|
||
| const models = componentInstance?.models; | ||
|
|
||
| if (models && Array.isArray(models)) { | ||
| for (const model of models) { | ||
| const newModel = this.context.getSync<Function>( | ||
| 'models.' + model.name, | ||
| ); | ||
| MetadataInspector.defineMetadata( | ||
| OVERRIDE_MODEL_SCHEMA_KEY, | ||
| newModel, | ||
| model, | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './core-controller.booter'; | ||
| export * from './core-model.booter'; |
Uh oh!
There was an error while loading. Please reload this page.