From 5fb119b7ed44ab362e322b95769865e317791df0 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Wed, 15 May 2019 16:49:39 -0700 Subject: [PATCH] feat(boot): add a booter for interceptors --- .../fixtures/interceptor.artifact.ts | 56 +++++++++++++ .../non-global-interceptor.artifact.ts | 56 +++++++++++++ .../interceptor.booter.integration.ts | 82 +++++++++++++++++++ packages/boot/src/boot.component.ts | 4 +- packages/boot/src/booters/index.ts | 5 +- .../boot/src/booters/interceptor.booter.ts | 76 +++++++++++++++++ 6 files changed, 276 insertions(+), 3 deletions(-) create mode 100644 packages/boot/src/__tests__/fixtures/interceptor.artifact.ts create mode 100644 packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts create mode 100644 packages/boot/src/__tests__/integration/interceptor.booter.integration.ts create mode 100644 packages/boot/src/booters/interceptor.booter.ts diff --git a/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts b/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts new file mode 100644 index 000000000000..286fbfb5df03 --- /dev/null +++ b/packages/boot/src/__tests__/fixtures/interceptor.artifact.ts @@ -0,0 +1,56 @@ +// 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 { + /* + constructor() {} + */ + + /** + * This method is used by LoopBack context to produce an interceptor function + * for the binding. + * + * @returns An interceptor function + */ + value() { + const interceptor: Interceptor = (invocationCtx, next) => + this.intercept(invocationCtx, next); + return interceptor; + } + + /** + * 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, + ) { + 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; + } + } +} diff --git a/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts b/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts new file mode 100644 index 000000000000..a431588d6056 --- /dev/null +++ b/packages/boot/src/__tests__/fixtures/non-global-interceptor.artifact.ts @@ -0,0 +1,56 @@ +// 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 { + /* + constructor() {} + */ + + /** + * This method is used by LoopBack context to produce an interceptor function + * for the binding. + * + * @returns An interceptor function + */ + value() { + const interceptor: Interceptor = (invocationCtx, next) => + this.intercept(invocationCtx, next); + return interceptor; + } + + /** + * 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, + ) { + 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; + } + } +} diff --git a/packages/boot/src/__tests__/integration/interceptor.booter.integration.ts b/packages/boot/src/__tests__/integration/interceptor.booter.integration.ts new file mode 100644 index 000000000000..ab0a37abe111 --- /dev/null +++ b/packages/boot/src/__tests__/integration/interceptor.booter.integration.ts @@ -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(); + } +}); diff --git a/packages/boot/src/boot.component.ts b/packages/boot/src/boot.component.ts index 41bd2698cb5b..4da843bada63 100644 --- a/packages/boot/src/boot.component.ts +++ b/packages/boot/src/boot.component.ts @@ -9,9 +9,10 @@ import { ApplicationMetadataBooter, ControllerBooter, DataSourceBooter, + InterceptorProviderBooter, + LifeCycleObserverBooter, RepositoryBooter, ServiceBooter, - LifeCycleObserverBooter, } from './booters'; import {Bootstrapper} from './bootstrapper'; import {BootBindings} from './keys'; @@ -31,6 +32,7 @@ export class BootComponent implements Component { ServiceBooter, DataSourceBooter, LifeCycleObserverBooter, + InterceptorProviderBooter, ]; /** diff --git a/packages/boot/src/booters/index.ts b/packages/boot/src/booters/index.ts index fb5057c6e7b8..838406023316 100644 --- a/packages/boot/src/booters/index.ts +++ b/packages/boot/src/booters/index.ts @@ -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'; diff --git a/packages/boot/src/booters/interceptor.booter.ts b/packages/boot/src/booters/interceptor.booter.ts new file mode 100644 index 000000000000..a25540a4b3c6 --- /dev/null +++ b/packages/boot/src/booters/interceptor.booter.ts @@ -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>; + +/** + * 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, +};