Skip to content

Commit

Permalink
feat(boot): add a booter for global interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed May 23, 2019
1 parent 94ba1d5 commit da707f3
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 7 deletions.
27 changes: 27 additions & 0 deletions packages/boot/src/__tests__/fixtures/interceptor.artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 {globalInterceptor, Interceptor, Provider} from '@loopback/context';

/**
* This class will be bound to the application as a global `Interceptor` during
* `boot`
*/
@globalInterceptor('auth')
export class MyGlobalInterceptor implements Provider<Interceptor> {
/*
constructor() {}
*/

value() {
const interceptor: Interceptor = async (invocationCtx, next) => {
// Add pre-invocation logic here
const result = await next();
// Add post-invocation logic here
return result;
};
return interceptor;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 {
BindingScope,
ContextTags,
GLOBAL_INTERCEPTOR_NAMESPACE,
} from '@loopback/core';
import {expect, TestSandbox} from '@loopback/testlab';
import {resolve} from 'path';
import {BooterApp} from '../fixtures/application';

describe('global interceptor script booter integration tests', () => {
const SANDBOX_PATH = resolve(__dirname, '../../.sandbox');
const sandbox = new TestSandbox(SANDBOX_PATH);

let app: BooterApp;

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

it('boots global interceptors when app.boot() is called', async () => {
const expectedBinding = {
key: `${GLOBAL_INTERCEPTOR_NAMESPACE}.MyGlobalInterceptor`,
tags: [
ContextTags.PROVIDER,
ContextTags.TYPE,
ContextTags.GLOBAL_INTERCEPTOR,
ContextTags.NAMESPACE,
ContextTags.GLOBAL_INTERCEPTOR_GROUP,
],
scope: BindingScope.TRANSIENT,
};

await app.boot();

const bindings = app
.findByTag(ContextTags.GLOBAL_INTERCEPTOR)
.map(b => ({key: b.key, tags: b.tagNames, scope: b.scope}));
expect(bindings).to.containEql(expectedBinding);
});

async function getApp() {
await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js'));
await sandbox.copyFile(
resolve(__dirname, '../fixtures/interceptor.artifact.js'),
'interceptors/interceptor.interceptor.js',
);

const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp;
app = new MyApp();
}
});
4 changes: 3 additions & 1 deletion packages/boot/src/boot.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {
ApplicationMetadataBooter,
ControllerBooter,
DataSourceBooter,
InterceptorProviderBooter,
LifeCycleObserverBooter,
RepositoryBooter,
ServiceBooter,
LifeCycleObserverBooter,
} from './booters';
import {Bootstrapper} from './bootstrapper';
import {BootBindings} from './keys';
Expand All @@ -31,6 +32,7 @@ export class BootComponent implements Component {
ServiceBooter,
DataSourceBooter,
LifeCycleObserverBooter,
InterceptorProviderBooter,
];

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/boot/src/booters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './application-metadata.booter';
export * from './base-artifact.booter';
export * from './booter-utils';
export * from './controller.booter';
export * from './datasource.booter';
export * from './interceptor.booter';
export * from './lifecyle-observer.booter';
export * from './repository.booter';
export * from './service.booter';
export * from './application-metadata.booter';
export * from './lifecyle-observer.booter';
78 changes: 78 additions & 0 deletions packages/boot/src/booters/interceptor.booter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright IBM Corp. 2018. 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 {
BindingScope,
Constructor,
createBindingFromClass,
GLOBAL_INTERCEPTOR_NAMESPACE,
inject,
Interceptor,
Provider,
} from '@loopback/context';
import {Application, CoreBindings} from '@loopback/core';
import * as debugFactory from 'debug';
import {ArtifactOptions} from '../interfaces';
import {BootBindings} from '../keys';
import {BaseArtifactBooter} from './base-artifact.booter';

const debug = debugFactory('loopback:boot:interceptor-booter');

type InterceptorProviderClass = Constructor<Provider<Interceptor>>;

/**
* A class that extends BaseArtifactBooter to boot the 'InterceptorProvider' artifact type.
*
* Supported phases: configure, discover, load
*
* @param app Application instance
* @param projectRoot Root of User Project relative to which all paths are resolved
* @param [bootConfig] InterceptorProvider Artifact Options Object
*/
export class InterceptorProviderBooter extends BaseArtifactBooter {
interceptors: InterceptorProviderClass[];

constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
public app: Application,
@inject(BootBindings.PROJECT_ROOT) projectRoot: string,
@inject(`${BootBindings.BOOT_OPTIONS}#interceptors`)
public interceptorConfig: ArtifactOptions = {},
) {
super(
projectRoot,
// Set InterceptorProvider Booter Options if passed in via bootConfig
Object.assign({}, InterceptorProviderDefaults, interceptorConfig),
);
}

/**
* Uses super method to get a list of Artifact classes. Boot each file by
* creating a DataSourceConstructor and binding it to the application class.
*/
async load() {
await super.load();

this.interceptors = this.classes as InterceptorProviderClass[];
for (const interceptor of this.interceptors) {
debug('Bind global interceptor: %s', interceptor.name);
const binding = createBindingFromClass(interceptor, {
namespace: GLOBAL_INTERCEPTOR_NAMESPACE,
defaultScope: BindingScope.TRANSIENT,
});
this.app.add(binding);
debug('Binding created for global interceptor: %j', binding);
}
}
}

/**
* Default ArtifactOptions for DataSourceBooter.
*/
export const InterceptorProviderDefaults: ArtifactOptions = {
dirs: ['interceptors'],
extensions: ['.interceptor.js'],
nested: true,
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
/* inject, */
asGlobalInterceptor,
globalInterceptor,
bind,
Interceptor,
Provider,
Expand All @@ -10,7 +10,7 @@ import {
* This class will be bound to the application as a global `Interceptor` during
* `boot`
*/
@bind(asGlobalInterceptor('<%= group %>'))
@globalInterceptor('<%= group %>')
export class <%= className %> implements Provider<Interceptor> {
/*
constructor() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ function verifyGeneratedScript(group = '') {
'my-interceptor.interceptor.ts',
);
assert.file(expectedFile);
assert.fileContent(expectedFile, 'bind');
assert.fileContent(expectedFile, 'globalInterceptor');
assert.fileContent(
expectedFile,
/export class MyInterceptorInterceptor implements Provider<Interceptor> {/,
);
assert.fileContent(expectedFile, `@bind(asGlobalInterceptor('${group}'))`);
assert.fileContent(expectedFile, `@globalInterceptor('${group}')`);
assert.fileContent(expectedFile, /value\(\) {/);
assert.fileContent(expectedFile, /async \(invocationCtx, next\) => \{/);
assert.file(INDEX_FILE);
Expand Down

0 comments on commit da707f3

Please sign in to comment.