import { BindingScope , ContextTags , JSONArray , JSONObject } from '@loopback/core' ; import { attribute , Digraph , ICluster , IEdge , INode , toDot } from 'ts-graphviz' ; /**
 * A wrapper class for context, binding, and its level in the chain
 */ export class ContextBinding { public readonly id : string ; constructor ( public readonly context , public readonly binding , public readonly level ) { const keys = Object . keys ( this . context . bindings as JSONObject ) ; const index = keys . indexOf ( this . binding . key as string ) ; this . id = `Binding_${ this . level }_${ index }` ; } } /**
 * A filter function to control if a binding is to be rendered
 */ export type BindingNodeFilter = ( binding : ContextBinding ) => boolean ; /**
 * Options for ContextGraph
 */ export type ContextGraphOptions = { /**
     * A filter function to select bindings for rendering
     */ bindingFilter ? : BindingNodeFilter ; } ; /**
 * A graph for context hierarchy
 */ export class ContextGraph { /**
     * Root diagram
     */ private root = new Digraph ( 'ContextGraph' ) ; /**
     * Context json objects in the chain from root to leaf
     */ private readonly contextChain : JSONObject [ ] = [ ] ; /**
     * Tag indexes for the context chain
     */ private readonly tagIndexes : Record < string , ContextBinding [ ] > [ ] = [ ] ; constructor ( ctx , private readonly options = { } ) { let current = ctx ; while ( current != null ) { this . contextChain . unshift ( current ) ; current = current . parent as JSONObject | undefined ; } this . indexBindings ( ) ; } /**
     * Assign a unique id for each bindings
     */ private indexBindings ( ) { for ( let level = 0 ; level < this . contextChain . length ; level ++ ) { const ctx = this . contextChain [ level ] ; this . tagIndexes [ level ] = { } ; const bindings = ctx . bindings as JSONObject ; for ( const key in bindings ) { const binding = bindings [ key ] as JSONObject ; const tagNames = Object . keys ( ( binding . tags ?? { } ) as JSONObject ) ; for ( const t of tagNames ) { let tagged = this . tagIndexes [ level ] [ t ] ; if ( tagged == null ) { tagged = [ ] ; this . tagIndexes [ level ] [ t ] = tagged ; } tagged . push ( new ContextBinding ( ctx , binding , level ) ) ; } } } } param parent param level private renderContextChain ( parent , level ) { const ctx = this . contextChain [ level ] ; const ctxName = ctx . name as string ; const subgraph = parent . createSubgraph ( `cluster_${ ctxName }` , { [ attribute . label ] : ctxName , [ attribute . labelloc ] : 't' , } ) ; const bindings = ctx . bindings as JSONObject ; for ( const key in bindings ) { const binding = bindings [ key ] as JSONObject ; const ctxBinding = new ContextBinding ( ctx , binding , level ) ; if ( typeof this . options . bindingFilter === 'function' && ! this . options . bindingFilter ( ctxBinding ) ) continue ; this . renderBinding ( subgraph , ctxBinding ) ; this . renderBindingInjections ( subgraph , ctxBinding ) ; this . renderConfig ( subgraph , ctxBinding ) ; } if ( level + 1 < this . contextChain . length ) { this . renderContextChain ( subgraph , level + 1 ) ; } return subgraph ; } param binding param level protected renderConfig ( parent , { binding , level , id } ) { const tagMap = binding . tags as JSONObject ; if ( tagMap ?. [ ContextTags . CONFIGURATION_FOR ] ) { const targetBinding = this . getBinding ( tagMap [ ContextTags . CONFIGURATION_FOR ] as string , level ) ; if ( targetBinding != null ) { return parent . createEdge ( [ targetBinding . id , id ] , { [ attribute . style ] : 'dashed' , [ attribute . arrowhead ] : 'odot' , [ attribute . color ] : 'orange' , } ) ; } } return undefined ; } param parent param binding protected renderBinding ( parent , { binding , id } ) { let style = `filled,rounded` ; if ( binding . scope === BindingScope . SINGLETON ) { style = style + ',bold' ; } const tags = binding . tags as JSONObject ; const tagPairs = [ ] ; if ( tags ) { for ( const t in tags ) { let tagVal = tags [ t ] ; if ( typeof tagVal === 'function' ) { tagVal = ( tagVal as Function ) . name ; } tagPairs . push ( `${ t }:${ tagVal }` ) ; } } const tagLabel = tagPairs . length ? `|${ tagPairs . join ( '\\l' ) }\\l` : '' ; const label = `{${ binding . key }|{${ binding . type }|${ binding . scope }}${ tagLabel }}` ; return parent . createNode ( id , { [ attribute . label ] : label , [ attribute . shape ] : 'record' , [ attribute . style ] : style , [ attribute . fillcolor ] : 'cyan3' , } ) ; } param key param level private getBinding ( key , level ) { for ( let i = level ; i >= 0 ; i -- ) { const ctx = this . contextChain [ i ] ; const bindings = ctx . bindings as JSONObject ; key = key . split ( '#' ) [ 0 ] ; const binding = bindings ?. [ key ] as JSONObject ; if ( binding ) return new ContextBinding ( ctx , binding , i ) ; } return undefined ; } param tag param level private getBindingsByTag ( tag , level ) { const bindings = [ ] ; for ( let i = level ; i >= 0 ; i -- ) { const tagIndex = this . tagIndexes [ i ] ; let tagged = tagIndex [ tag ] ; if ( tagged != null ) { tagged = tagged . filter ( ctxBinding => ! bindings . some ( existing => existing . binding . key === ctxBinding . binding . key ) ) ; bindings . push ( ... tagged ) ; } } return bindings ; } param parent param binding param level private renderBindingInjections ( parent , { binding , level , id } ) { const targetBindings = [ ] ; const ctor = binding . valueConstructor ?? binding . providerConstructor ; if ( ctor ) { const bindingFilter = this . options ?. bindingFilter ?? ( ( ) => true ) ; const injections = [ ] ; const startingLevel = binding . scope === BindingScope . SINGLETON ? level : this . contextChain . length - 1 ; if ( binding . injections ) { const args = ( binding . injections as JSONObject ) . constructorArguments as JSONArray ; const props = ( binding . injections as JSONObject ) . properties as JSONObject ; if ( args ) { let i = 0 ; args . forEach ( arg => { injections . push ( `[${ i ++ }]` ) ; const argInjection = arg as JSONObject ; const targetIds = this . getBindingsForInjection ( argInjection , startingLevel ) . filter ( bindingFilter ) . map ( b => b . id ) ; targetBindings . push ( ... targetIds ) ; } ) ; } if ( props ) { for ( const p in props ) { injections . push ( `${ p }` ) ; const propInjection = props [ p ] as JSONObject ; const targetIds = this . getBindingsForInjection ( propInjection , startingLevel ) . filter ( bindingFilter ) . map ( b => b . id ) ; targetBindings . push ( ... targetIds ) ; } } } let label = ctor as string ; if ( injections . length ) { label += '|{' + injections . join ( '|' ) + '}' ; } const classId = `Class_${ ctor }` ; this . root . createNode ( classId , { [ attribute . label ] : label , [ attribute . style ] : 'filled' , [ attribute . shape ] : 'record' , [ attribute . fillcolor ] : 'khaki' , } ) ; parent . createEdge ( [ id , classId ] , { [ attribute . style ] : 'dashed' } ) ; for ( const b of targetBindings ) { parent . createEdge ( [ classId , b ] , { [ attribute . color ] : 'blue' } ) ; } } } param injection param level private getBindingsForInjection ( injection , level ) { if ( injection . bindingKey ) { const binding = this . getBinding ( injection . bindingKey as string , level ) ; return binding == null ? [ ] : [ binding ] ; } if ( typeof injection . bindingTagPattern === 'string' ) { const bindings = this . getBindingsByTag ( injection . bindingTagPattern as string , level ) ; return bindings ; } return [ ] ; } /**
     * Build a direct graph
     */ build ( ) { this . renderContextChain ( this . root , 0 ) ; } /**
     * Render the context graph in graphviz dot format
     */ render ( ) { this . build ( ) ; return toDot ( this . root ) ; } }