@loopback/context-explorer
Version:
Visualize context hierarchy, bindings, configurations, and dependencies
276 lines • 10.5 kB
JavaScript
;
// 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