UNPKG

@loopback/context-explorer

Version:

Visualize context hierarchy, bindings, configurations, and dependencies

276 lines 10.5 kB
"use strict"; // Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved. // Node module: @loopback/context-explorer // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT Object.defineProperty(exports, "__esModule", { value: true }); exports.ContextGraph = exports.ContextBinding = void 0; const core_1 = require("@loopback/core"); const ts_graphviz_1 = require("ts-graphviz"); /** * A wrapper class for context, binding, and its level in the chain */ class ContextBinding { constructor(context, binding, level) { this.context = context; this.binding = binding; this.level = level; const keys = Object.keys(this.context.bindings); const index = keys.indexOf(this.binding.key); this.id = `Binding_${this.level}_${index}`; } } exports.ContextBinding = ContextBinding; /** * A graph for context hierarchy */ class ContextGraph { constructor(ctx, options = {}) { this.options = options; /** * Root diagram */ this.root = new ts_graphviz_1.Digraph('ContextGraph'); /** * Context json objects in the chain from root to leaf */ this.contextChain = []; /** * Tag indexes for the context chain */ this.tagIndexes = []; let current = ctx; while (current != null) { this.contextChain.unshift(current); current = current.parent; } this.indexBindings(); } /** * Assign a unique id for each bindings */ indexBindings() { var _a; for (let level = 0; level < this.contextChain.length; level++) { const ctx = this.contextChain[level]; this.tagIndexes[level] = {}; const bindings = ctx.bindings; for (const key in bindings) { const binding = bindings[key]; const tagNames = Object.keys(((_a = binding.tags) !== null && _a !== void 0 ? _a : {})); 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)); } } } } /** * Recursive render the chain of contexts as subgraphs * @param parent - Parent subgraph * @param level - Level of the context in the chain */ renderContextChain(parent, level) { const ctx = this.contextChain[level]; const ctxName = ctx.name; const subgraph = parent.createSubgraph(`cluster_${ctxName}`, { [ts_graphviz_1.attribute.label]: ctxName, [ts_graphviz_1.attribute.labelloc]: 't', }); const bindings = ctx.bindings; for (const key in bindings) { const binding = bindings[key]; 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; } /** * Create an edge for a binding to its configuration * @param binding - Binding object * @param level - Context level */ renderConfig(parent, { binding, level, id }) { const tagMap = binding.tags; if (tagMap === null || tagMap === void 0 ? void 0 : tagMap[core_1.ContextTags.CONFIGURATION_FOR]) { const targetBinding = this.getBinding(tagMap[core_1.ContextTags.CONFIGURATION_FOR], level); if (targetBinding != null) { return parent.createEdge([targetBinding.id, id], { [ts_graphviz_1.attribute.style]: 'dashed', [ts_graphviz_1.attribute.arrowhead]: 'odot', [ts_graphviz_1.attribute.color]: 'orange', }); } } return undefined; } /** * Render a binding object * @param parent - Parent subgraph * @param binding - Context Binding object */ renderBinding(parent, { binding, id }) { let style = `filled,rounded`; if (binding.scope === core_1.BindingScope.SINGLETON) { style = style + ',bold'; } const tags = binding.tags; const tagPairs = []; if (tags) { for (const t in tags) { let tagVal = tags[t]; if (typeof tagVal === 'function') { tagVal = tagVal.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, { [ts_graphviz_1.attribute.label]: label, [ts_graphviz_1.attribute.shape]: 'record', [ts_graphviz_1.attribute.style]: style, [ts_graphviz_1.attribute.fillcolor]: 'cyan3', }); } /** * Find the binding id by key * @param key - Binding key * @param level - Context level */ getBinding(key, level) { for (let i = level; i >= 0; i--) { const ctx = this.contextChain[i]; const bindings = ctx.bindings; key = key.split('#')[0]; const binding = bindings === null || bindings === void 0 ? void 0 : bindings[key]; if (binding) return new ContextBinding(ctx, binding, i); } return undefined; } /** * Find bindings by tag * @param tag - Tag name * @param level - Context level */ getBindingsByTag(tag, level) { const bindings = []; for (let i = level; i >= 0; i--) { const tagIndex = this.tagIndexes[i]; let tagged = tagIndex[tag]; if (tagged != null) { // Exclude bindings if their keys are already in the list tagged = tagged.filter(ctxBinding => !bindings.some(existing => existing.binding.key === ctxBinding.binding.key)); bindings.push(...tagged); } } return bindings; } /** * Render injections for a binding * @param parent - Parent subgraph * @param binding - Binding object * @param level - Context level */ renderBindingInjections(parent, { binding, level, id }) { var _a, _b, _c; const targetBindings = []; const ctor = (_a = binding.valueConstructor) !== null && _a !== void 0 ? _a : binding.providerConstructor; if (ctor) { const bindingFilter = (_c = (_b = this.options) === null || _b === void 0 ? void 0 : _b.bindingFilter) !== null && _c !== void 0 ? _c : (() => true); const injections = []; // For singletons, search this level and up const startingLevel = binding.scope === core_1.BindingScope.SINGLETON ? level : this.contextChain.length - 1; if (binding.injections) { const args = binding.injections .constructorArguments; const props = binding.injections .properties; if (args) { let i = 0; args.forEach(arg => { injections.push(`[${i++}]`); const argInjection = arg; 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]; const targetIds = this.getBindingsForInjection(propInjection, startingLevel) .filter(bindingFilter) .map(b => b.id); targetBindings.push(...targetIds); } } } let label = ctor; if (injections.length) { label += '|{' + injections.join('|') + '}'; } // FIXME(rfeng): We might have classes with the same name const classId = `Class_${ctor}`; this.root.createNode(classId, { [ts_graphviz_1.attribute.label]: label, [ts_graphviz_1.attribute.style]: 'filled', [ts_graphviz_1.attribute.shape]: 'record', [ts_graphviz_1.attribute.fillcolor]: 'khaki', }); parent.createEdge([id, classId], { [ts_graphviz_1.attribute.style]: 'dashed' }); for (const b of targetBindings) { parent.createEdge([classId, b], { [ts_graphviz_1.attribute.color]: 'blue' }); } } } /** * Find target bindings for an injection * @param injection - Injection object * @param level - Context level */ getBindingsForInjection(injection, level) { if (injection.bindingKey) { const binding = this.getBinding(injection.bindingKey, level); return binding == null ? [] : [binding]; } if (typeof injection.bindingTagPattern === 'string') { const bindings = this.getBindingsByTag(injection.bindingTagPattern, 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 (0, ts_graphviz_1.toDot)(this.root); } } exports.ContextGraph = ContextGraph; //# sourceMappingURL=context-graph.js.map