Skip to content

Commit

Permalink
feat(boot): add a booter for interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jun 6, 2019
1 parent a60bcb9 commit 552f101
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 3 deletions.
54 changes: 54 additions & 0 deletions packages/boot/src/__tests__/fixtures/interceptor.artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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,
InvocationContext,
InvocationResult,
Provider,
ValueOrPromise,
} from '@loopback/context';

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

/**
* This method is used by LoopBack context to produce an interceptor function
* for the binding.
*
* @returns An interceptor function
*/
value() {
return this.intercept.bind(this);
}

/**
* The logic to intercept an invocation
* @param invocationCtx - Invocation context
* @param next - A function to invoke next interceptor or the target method
*/
async intercept(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<InvocationResult>,
) {
try {
// Add pre-invocation logic here
const result = await next();
// Add post-invocation logic here
return result;
} catch (err) {
// Add error handling logic here
throw err;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 {
bind,
Interceptor,
InvocationContext,
InvocationResult,
Provider,
ValueOrPromise,
} from '@loopback/context';

/**
* This class will be bound to the application as a global `Interceptor` during
* `boot`
*/
@bind({tags: {namespace: 'interceptors', name: 'myInterceptor'}})
export class MyInterceptor implements Provider<Interceptor> {
/*
constructor() {}
*/

/**
* This method is used by LoopBack context to produce an interceptor function
* for the binding.
*
* @returns An interceptor function
*/
value() {
return this.intercept.bind(this);
}

/**
* The logic to intercept an invocation
* @param invocationCtx - Invocation context
* @param next - A function to invoke next interceptor or the target method
*/
async intercept(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<InvocationResult>,
) {
try {
// Add pre-invocation logic here
const result = await next();
// Add post-invocation logic here
return result;
} catch (err) {
// Add error handling logic here
throw err;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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('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(buildAppWithInterceptors);

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,
ContextTags.NAME,
],
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);
});

it('boots non-global interceptors when app.boot() is called', async () => {
const expectedBinding = {
key: `interceptors.myInterceptor`,
tags: [
ContextTags.PROVIDER,
ContextTags.TYPE,
ContextTags.NAMESPACE,
ContextTags.NAME,
],
scope: BindingScope.TRANSIENT,
};

await app.boot();

const binding = app.getBinding('interceptors.myInterceptor');
expect({
key: binding.key,
tags: binding.tagNames,
scope: binding.scope,
}).to.eql(expectedBinding);
});

async function buildAppWithInterceptors() {
await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js'));
await sandbox.copyFile(
resolve(__dirname, '../fixtures/interceptor.artifact.js'),
'interceptors/interceptor.interceptor.js',
);
await sandbox.copyFile(
resolve(__dirname, '../fixtures/non-global-interceptor.artifact.js'),
'interceptors/non-global-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';
76 changes: 76 additions & 0 deletions packages/boot/src/booters/interceptor.booter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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,
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 interceptor: %s', interceptor.name);
const binding = createBindingFromClass(interceptor, {
defaultScope: BindingScope.TRANSIENT,
});
this.app.add(binding);
debug('Binding created for interceptor: %j', binding);
}
}
}

/**
* Default ArtifactOptions for DataSourceBooter.
*/
export const InterceptorProviderDefaults: ArtifactOptions = {
dirs: ['interceptors'],
extensions: ['.interceptor.js'],
nested: true,
};

0 comments on commit 552f101

Please sign in to comment.