import { Binding , BindingScope , BINDING_METADATA_KEY , Context , ContextEvent , ContextView , createBindingFromClass , Getter , injectable , MetadataInspector , } from '@loopback/context' ; import { expect } from '@loopback/testlab' ; import { types } from 'util' ; import { addExtension , CoreTags , extensionFilter , extensionFor , extensionPoint , extensions , } from '../..' ; describe ( 'extension point' , ( ) => { describe ( '@extensionPoint' , ( ) => { it ( 'specifies name of the extension point as a binding tag' , ( ) => { @ extensionPoint ( 'greeters' , { scope : BindingScope . SINGLETON } ) class GreetingService { @ extensions ( ) public greeters : Getter < Greeter [ ] > ; } const bindingMetadata = MetadataInspector . getClassMetadata ( BINDING_METADATA_KEY , GreetingService ) ; expect ( bindingMetadata ) . to . not . undefined ( ) ; expect ( bindingMetadata ! . templates ) . to . be . an . Array ( ) ; const binding = createBindingFromClass ( GreetingService ) ; expect ( binding . tagMap ) . to . containEql ( { [ CoreTags . EXTENSION_POINT ] : 'greeters' , } ) ; expect ( binding . scope ) . to . eql ( BindingScope . SINGLETON ) ; } ) ; } ) ; describe ( '@extensions' , ( ) => { let ctx ; beforeEach ( givenContext ) ; it ( 'injects a getter function of extensions' , async ( ) => { @ extensionPoint ( 'greeters' ) class GreetingService { @ extensions ( ) public greeters : Getter < Greeter [ ] > ; } const binding = createBindingFromClass ( GreetingService , { key : 'greeter-service' , } ) ; ctx . add ( binding ) ; registerGreeters ( 'greeters' ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const greeters = await greeterService . greeters ( ) ; assertGreeterExtensions ( greeters ) ; } ) ; it ( 'injects a view of extensions' , async ( ) => { @ extensionPoint ( 'greeters' ) class GreetingService { readonly bindings : Set < Readonly < Binding < unknown > > > ; constructor ( @ extensions . view ( ) public readonly greetersView ) { this . bindings = new Set ( this . greetersView . bindings ) ; this . greetersView . on ( 'bind' , ( event ) => { this . bindings . add ( event . binding ) ; } ) ; this . greetersView . on ( 'unbind' , ( event ) => { this . bindings . delete ( event . binding ) ; } ) ; } } const binding = createBindingFromClass ( GreetingService , { key : 'greeter-service' , } ) ; ctx . add ( binding ) ; const registeredBindings = registerGreeters ( 'greeters' ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; expect ( Array . from ( greeterService . bindings ) ) . to . eql ( registeredBindings ) ; let greeters = await greeterService . greetersView . values ( ) ; assertGreeterExtensions ( greeters ) ; expect ( greeters . length ) . to . equal ( 2 ) ; ctx . unbind ( registeredBindings [ 0 ] . key ) ; greeters = await greeterService . greetersView . values ( ) ; expect ( greeters . length ) . to . equal ( 1 ) ; expect ( Array . from ( greeterService . bindings ) ) . to . eql ( [ registeredBindings [ 1 ] , ] ) ; } ) ; it ( 'injects a list of extensions' , async ( ) => { @ extensionPoint ( 'greeters' ) class GreetingService { @ extensions . list ( ) public greeters : Greeter [ ] ; } const binding = createBindingFromClass ( GreetingService , { key : 'greeter-service' , } ) ; ctx . add ( binding ) ; const registeredBindings = registerGreeters ( 'greeters' ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; expect ( greeterService . greeters . length ) . to . eql ( registeredBindings . length ) ; assertGreeterExtensions ( greeterService . greeters ) ; const copy = Array . from ( greeterService . greeters ) ; ctx . unbind ( registeredBindings [ 0 ] . key ) ; expect ( greeterService . greeters ) . to . eql ( copy ) ; } ) ; it ( 'injects extensions based on `name` tag of the extension point binding' , async ( ) => { class GreetingService { @ extensions ( ) public greeters : Getter < Greeter [ ] > ; } ctx . bind ( 'greeter-service' ) . toClass ( GreetingService ) . tag ( { name : 'greeters' } ) ; registerGreeters ( 'greeters' ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const greeters = await greeterService . greeters ( ) ; assertGreeterExtensions ( greeters ) ; } ) ; it ( 'injects extensions based on class name of the extension point' , async ( ) => { class GreetingService { constructor ( @ extensions ( ) public greeters ) { } } ctx . bind ( 'greeter-service' ) . toClass ( GreetingService ) ; registerGreeters ( GreetingService . name ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const loadedGreeters = await greeterService . greeters ( ) ; assertGreeterExtensions ( loadedGreeters ) ; } ) ; it ( 'injects extensions based on class name of the extension point using property' , async ( ) => { class GreetingService { @ extensions ( ) public greeters : Getter < Greeter [ ] > ; } ctx . bind ( 'greeter-service' ) . toClass ( GreetingService ) ; registerGreeters ( GreetingService . name ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const greeters = await greeterService . greeters ( ) ; assertGreeterExtensions ( greeters ) ; } ) ; it ( 'injects extensions based on extension point name from @extensions' , async ( ) => { class GreetingService { @ extensions ( 'greeters' ) public greeters : Getter < Greeter [ ] > ; } ctx . bind ( 'greeter-service' ) . toClass ( GreetingService ) ; registerGreeters ( 'greeters' ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const greeters = await greeterService . greeters ( ) ; assertGreeterExtensions ( greeters ) ; } ) ; it ( 'injects extensions with metadata' , async ( ) => { class GreetingService { @ extensions ( 'greeters' , { asProxyWithInterceptors : true } ) public greeters : Getter < Greeter [ ] > ; } ctx . bind ( 'greeter-service' ) . toClass ( GreetingService ) ; registerGreeters ( 'greeters' ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const greeters = await greeterService . greeters ( ) ; greeters . forEach ( g => expect ( types . isProxy ( g ) ) . to . be . true ( ) ) ; } ) ; it ( 'injects multiple types of extensions' , async ( ) => { interface Logger { log ( message ) : void ; } class ConsoleLogger implements Logger { log ( message ) { console . log ( message ) ; } } class GreetingService { constructor ( @ extensions ( 'greeters' ) public greeters , @ extensions ( 'loggers' ) public loggers ) { } } ctx . bind ( 'greeter-service' ) . toClass ( GreetingService ) ; registerGreeters ( 'greeters' ) ; addExtension ( ctx , 'loggers' , ConsoleLogger ) ; const greeterService = await ctx . get < GreetingService > ( 'greeter-service' ) ; const loadedGreeters = await greeterService . greeters ( ) ; assertGreeterExtensions ( loadedGreeters ) ; const loadedLoggers = await greeterService . loggers ( ) ; expect ( loadedLoggers ) . to . be . an . Array ( ) ; expect ( loadedLoggers . length ) . to . equal ( 1 ) ; expect ( loadedLoggers [ 0 ] ) . to . be . instanceOf ( ConsoleLogger ) ; } ) ; it ( 'allows an extension to contribute to multiple extension points' , ( ) => { @ injectable ( extensionFor ( 'extensionPoint-1' ) , extensionFor ( 'extensionPoint-2' ) ) class MyExtension { } const binding = createBindingFromClass ( MyExtension ) ; expect ( binding . tagMap [ CoreTags . EXTENSION_FOR ] ) . to . eql ( [ 'extensionPoint-1' , 'extensionPoint-2' , ] ) ; } ) ; it ( 'allows an extension of multiple extension points with extensionFor' , ( ) => { class MyExtension { } const binding = ctx . bind ( 'my-extension' ) . toClass ( MyExtension ) ; extensionFor ( 'extensionPoint-1' ) ( binding ) ; expect ( binding . tagMap [ CoreTags . EXTENSION_FOR ] ) . to . eql ( 'extensionPoint-1' ) ; extensionFor ( 'extensionPoint-2' ) ( binding ) ; expect ( binding . tagMap [ CoreTags . EXTENSION_FOR ] ) . to . eql ( [ 'extensionPoint-1' , 'extensionPoint-2' , ] ) ; extensionFor ( 'extensionPoint-1' ) ( binding ) ; expect ( binding . tagMap [ CoreTags . EXTENSION_FOR ] ) . to . eql ( [ 'extensionPoint-1' , 'extensionPoint-2' , ] ) ; } ) ; it ( 'allows multiple extension points for extensionFor' , ( ) => { class MyExtension { } const binding = ctx . bind ( 'my-extension' ) . toClass ( MyExtension ) ; extensionFor ( 'extensionPoint-1' , 'extensionPoint-2' ) ( binding ) ; expect ( extensionFilter ( 'extensionPoint-1' ) ( binding ) ) . to . be . true ( ) ; expect ( extensionFilter ( 'extensionPoint-2' ) ( binding ) ) . to . be . true ( ) ; expect ( extensionFilter ( 'extensionPoint-3' ) ( binding ) ) . to . be . false ( ) ; } ) ; it ( 'allows multiple extension points for extensionFilter' , ( ) => { class MyExtension { } const binding = ctx . bind ( 'my-extension' ) . toClass ( MyExtension ) ; extensionFor ( 'extensionPoint-1' , 'extensionPoint-2' ) ( binding ) ; expect ( extensionFilter ( 'extensionPoint-1' , 'extensionPoint3' ) ( binding ) ) . to . be . true ( ) ; expect ( extensionFilter ( 'extensionPoint-2' , 'extensionPoint3' ) ( binding ) ) . to . be . true ( ) ; } ) ; it ( 'allows multiple extension points for @extensions' , async ( ) => { @ extensionPoint ( 'extensionPoint-1' ) class MyExtensionPoint1 { @ extensions ( ) getMyExtensions : Getter < MyExtension [ ] > ; } @ extensionPoint ( 'extensionPoint-2' ) class MyExtensionPoint2 { @ extensions ( ) getMyExtensions : Getter < MyExtension [ ] > ; } @ injectable ( extensionFor ( 'extensionPoint-1' , 'extensionPoint-2' ) ) class MyExtension { } ctx . add ( createBindingFromClass ( MyExtensionPoint1 , { key : 'extensionPoint1' } ) ) ; ctx . add ( createBindingFromClass ( MyExtensionPoint2 , { key : 'extensionPoint2' } ) ) ; ctx . add ( createBindingFromClass ( MyExtension ) ) ; const ep1 = await ctx . get ( 'extensionPoint1' ) ; const ep2 = await ctx . get ( 'extensionPoint2' ) ; const extensionsFor1 = await ep1 . getMyExtensions ( ) ; const extensionsFor2 = await ep2 . getMyExtensions ( ) ; expect ( extensionsFor1 . length ) . to . eql ( 1 ) ; expect ( extensionsFor1 [ 0 ] ) . to . be . instanceOf ( MyExtension ) ; expect ( extensionsFor2 . length ) . to . eql ( 1 ) ; expect ( extensionsFor2 [ 0 ] ) . to . be . instanceOf ( MyExtension ) ; } ) ; function givenContext ( ) { ctx = new Context ( ) ; } function registerGreeters ( extensionPointName ) { const g1 = addExtension ( ctx , extensionPointName , EnglishGreeter , { namespace : 'greeters' , } ) ; const g2 = addExtension ( ctx , extensionPointName , ChineseGreeter , { namespace : 'greeters' , } ) ; return [ g1 , g2 ] ; } } ) ; interface Greeter { language : string ; greet ( name ) : string ; } class EnglishGreeter implements Greeter { language = 'en' ; greet ( name ) { return `Hello, ${ name }!` ; } } class ChineseGreeter implements Greeter { language = 'zh' ; greet ( name ) { return `你好，${ name }！` ; } } function assertGreeterExtensions ( greeters ) { const languages = greeters . map ( greeter => greeter . language ) . sort ( ) ; expect ( languages ) . to . eql ( [ 'en' , 'zh' ] ) ; } } ) ;