├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codecov.yml ├── index.js ├── package.json ├── preview.png ├── template.ejs └── test └── unit-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | env: 4 | - CODECOV_TOKEN="70be989e-dd91-4fd3-b1fd-7a52ec06188f" 5 | node_js: 6 | - "6" 7 | - "5" 8 | - "4" 9 | 10 | after_success: 11 | - npm run coverage 12 | - codecov 13 | - cat ./coverage/lcov.info | coveralls 14 | - bash <(curl -s https://codecov.io/bash) 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 yantrashala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-component-visualizer 2 | 3 | [![Build status](https://travis-ci.org/yantrashala/loopback-component-visualizer.svg)](https://travis-ci.org/yantrashala/loopback-component-visualizer) [![Dep Badge](https://david-dm.org/yantrashala/loopback-component-visualizer.svg)](https://david-dm.org/yantrashala/loopback-component-visualizer.svg) [![codecov](https://codecov.io/gh/yantrashala/loopback-component-visualizer/branch/master/graph/badge.svg)](https://codecov.io/gh/yantrashala/loopback-component-visualizer) [![GitHub issues](https://img.shields.io/github/issues/yantrashala/loopback-component-visualizer.svg)](https://github.com/yantrashala/loopback-component-visualizer/issues) 4 | 5 | ### Introduction 6 | 7 | Visualizing a model is sometimes a difficult task. When the data model gets larger, it becomes even more difficult to understand how models relate to each other. 8 | 9 | loopback-component-visualizer helps you in creating a model diagram with a representation of all the properties, methods and relationships of your models for your loopback application. 10 | 11 | ### Table of contents 12 | * Installation 13 | * Usage 14 | 15 | #### Installation 16 | 17 | Install the module in your loopback application folder. 18 | 19 | ```sh 20 | $ npm install loopback-component-visualizer --save 21 | ``` 22 | 23 | #### Usage 24 | 25 | Inside your component-config.json, add the loopback-component-visualizer and a '/visualize' api will be mounted to your server. 26 | 27 | You can browse @ http://host:port/visualize 28 | 29 | ```sh 30 | "loopback-component-visualizer": { 31 | "mountPath": "/visualize" 32 | } 33 | ``` 34 | 35 | You can also get the options passed to the visualizer as below 36 | 37 | ```sh 38 | loopbackApp.get('loopback-component-visualizer') 39 | ``` 40 | 41 | #### Preview 42 | 43 | ![A Relational Model](https://github.com/yantrashala/loopback-component-visualizer/blob/master/preview.png?raw=true "A Relational Model") 44 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = visualize; 4 | 5 | var _defaults = require('lodash').defaults; 6 | 7 | function visualize(loopbackApp, options) { 8 | options = _defaults({}, options, { 9 | mountPath: '/visualize', 10 | }); 11 | loopbackApp.use(options.mountPath, createDiagram(loopbackApp, options)); 12 | loopbackApp.set('loopback-component-visualizer', options); 13 | } 14 | 15 | function createDiagram(app, options) { 16 | 17 | var finalObj = prepareJson(app); 18 | 19 | return respond(finalObj, app); 20 | } 21 | 22 | function prepareJson(app) { 23 | 24 | var models = (app && app.models && app.models()) || {}; 25 | var remotes = (app && app.remoteObjects && app.remoteObjects()) || {}; 26 | var level = 0; 27 | var x = 0; 28 | var propArr = []; 29 | var edgeArr = []; 30 | var idx = 0; 31 | var modIdxObj = {}; 32 | var finalObj = {}; 33 | 34 | for (var i in models) { 35 | modIdxObj[models[i].modelName] = i; 36 | } 37 | 38 | models.length && models.forEach(function(Model) { 39 | var remotes = []; 40 | Model.sharedClass.methods().forEach(function(method) { 41 | remotes.push(method.name); 42 | }); 43 | var modelDefinition = app.models[Model.modelName].definition; 44 | if (modelDefinition && modelDefinition.properties) { 45 | var propString = Object.keys(modelDefinition.properties).toString().split(',').join('
  • '); 46 | 47 | var propObj = {}; 48 | propObj.id = idx; 49 | propObj.label = Model.modelName; 50 | propObj.props = ''; 51 | propObj.level = level; 52 | propObj.remotes = remotes && ''; 53 | propObj.title = '

    ' + propObj.label + '

    Props:

    \n' + propObj.props + '\n

    Remotes:

    ' + propObj.remotes 54 | propArr.push(propObj); 55 | x++; 56 | if (x == 5) { 57 | x = 0; 58 | level++; 59 | } 60 | } 61 | //edges 62 | if (Model.settings.relations) { 63 | var relArr = Object.keys(Model.settings.relations); 64 | for (var rel in relArr) { 65 | if (Model.settings.relations[relArr[rel]].model) { 66 | var edgeObj = {}; 67 | edgeObj.from = modIdxObj[Model.modelName]; 68 | edgeObj.to = modIdxObj[Model.settings.relations[relArr[rel]].model]; 69 | edgeObj.label = relArr[rel] || ''; 70 | edgeObj.title = JSON.stringify(Model.settings.relations[relArr[rel]]); 71 | edgeObj.arrows = 'to'; 72 | edgeArr.push(edgeObj); 73 | } 74 | } 75 | } 76 | 77 | 78 | idx++; 79 | }); 80 | 81 | 82 | finalObj.nodes = propArr; 83 | finalObj.edges = edgeArr; 84 | 85 | return finalObj; 86 | } 87 | 88 | function respond(finalObj, app) { 89 | 90 | return function(req, res, next) { 91 | var data = finalObj || {}; 92 | var render = app.loopback.template(__dirname + '/template.ejs'); 93 | var html = render(data); 94 | res.status(200).send(html); 95 | }; 96 | 97 | } 98 | 99 | module.exports.prepareJson = prepareJson; 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-component-visualizer", 3 | "version": "1.2.1", 4 | "description": "Loopback component to visualize models and their relationships", 5 | "main": "./index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "coverage": "istanbul cover _mocha" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/yantrashala/loopback-component-visualizer.git" 13 | }, 14 | "contributors": ["prashantbaid", "pbhadauria2000", "mennu"], 15 | "author": "prashantbaid, pbhadauria2000", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/yantrashala/loopback-component-visualizer/issues" 19 | }, 20 | "homepage": "https://github.com/yantrashala/loopback-component-visualizer#readme", 21 | "dependencies": { 22 | "lodash": "^4.12.0" 23 | }, 24 | "devDependencies": { 25 | "chai": "^3.5.0", 26 | "loopback": "^2.28.0", 27 | "loopback-datasource-juggler": "^2.46.0", 28 | "mocha": "^2.4.5", 29 | "supertest": "^1.2.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantrashala/loopback-component-visualizer/09010b036a867962abd363af6f44aeb33f79be6f/preview.png -------------------------------------------------------------------------------- /template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Domain Model Visualizer 5 | 6 | 7 | 8 | 9 | 84 | 85 | 86 | 91 |
    92 | 133 |
    134 |
    135 |
    136 | 137 | 138 | -------------------------------------------------------------------------------- /test/unit-test.js: -------------------------------------------------------------------------------- 1 | var loopback = require('loopback'); 2 | var visualize = require('../'); 3 | var request = require('supertest'); 4 | var expect = require('chai').expect; 5 | var application; 6 | 7 | describe('checkFinalObject', function() { 8 | beforeEach(function() { 9 | application = loopback({localRegistry: true, loadBuiltinModels : true}); 10 | var customerProperties = { 11 | name: {type: String, required: true} 12 | }; 13 | var customerOptions = { 14 | "relations": { 15 | "address": { 16 | "type": "embedsOne", 17 | "model": "Address", 18 | "property": "billingAddress", 19 | "options": { 20 | "validate": true, 21 | "forceId": false 22 | } 23 | } 24 | } 25 | } 26 | 27 | var addressProperties = { 28 | street: {type: String, required: true}, 29 | city: {type: String, required: true}, 30 | state: {type: String, required: true} 31 | }; 32 | 33 | var Address = loopback.Model.extend('Address', addressProperties); 34 | 35 | application.model(Address); 36 | 37 | var Customer = loopback.Model.extend('Customer', customerProperties, customerOptions); 38 | 39 | Customer.greet = function(msg, cb) { 40 | cb(null, 'Greetings... ' + msg); 41 | } 42 | 43 | Customer.remoteMethod( 44 | 'greet', 45 | { 46 | accepts: {arg: 'msg', type: 'string'}, 47 | returns: {arg: 'greeting', type: 'string'} 48 | } 49 | ); 50 | 51 | application.model(Customer); 52 | 53 | }); 54 | 55 | it('should compare the finalObject for address and customer.', function(done) { 56 | var finalObj = visualize.prepareJson(application); 57 | expect(finalObj.nodes).to.have.lengthOf(2); 58 | expect(finalObj.nodes[0]).to.contain.all.keys({'label': 'Address'}); 59 | expect(finalObj.nodes[0]).to.contain.all.keys({'label': 'Customer'}); 60 | done(); 61 | }); 62 | 63 | it('should compare the finalObject by sending an empty object.', function(done) { 64 | var finalObj = visualize.prepareJson({}); 65 | expect(finalObj.nodes).to.have.lengthOf(0); 66 | expect(finalObj.edges).to.have.lengthOf(0); 67 | done(); 68 | }); 69 | 70 | }); 71 | 72 | describe('visualize', function() { 73 | describe('with default config', function() { 74 | beforeEach(createLoopbackApplication()); 75 | 76 | it('should redirect to /visualize/', function(done) { 77 | request(this.app) 78 | .get('/visualize') 79 | .expect(200) 80 | .end(done); 81 | }); 82 | 83 | 84 | }); 85 | }); 86 | 87 | 88 | function createLoopbackApplication(basePath) { 89 | return function(done) { 90 | var app = this.app = loopback({localRegistry: true, loadBuiltinModels : true}); 91 | 92 | var customerProperties = { 93 | name: {type: String, required: true} 94 | }; 95 | var customerOptions = { 96 | "relations": { 97 | "address": { 98 | "type": "embedsOne", 99 | "model": "Address", 100 | "property": "billingAddress", 101 | "options": { 102 | "validate": true, 103 | "forceId": false 104 | } 105 | } 106 | } 107 | } 108 | 109 | 110 | 111 | var addressProperties = { 112 | street: {type: String, required: true}, 113 | city: {type: String, required: true}, 114 | state: {type: String, required: true} 115 | }; 116 | 117 | var Address = loopback.Model.extend('Address', addressProperties); 118 | 119 | app.model(Address); 120 | 121 | var BillingAddress = loopback.Model.extend('BillingAddress', addressProperties); 122 | 123 | app.model(BillingAddress); 124 | 125 | var OfficeAddress = loopback.Model.extend('OfficeAddress', addressProperties); 126 | 127 | app.model(OfficeAddress); 128 | 129 | var bookProperties = { 130 | name: {type: String, required: true} 131 | }; 132 | 133 | var Book = loopback.Model.extend('Book', bookProperties); 134 | 135 | app.model(Book); 136 | 137 | var Address = loopback.Model.extend('Address', addressProperties); 138 | 139 | app.model(Address); 140 | 141 | var Address = loopback.Model.extend('Address', addressProperties); 142 | 143 | app.model(Address); 144 | 145 | var Customer = loopback.Model.extend('Customer', customerProperties, customerOptions); 146 | 147 | Customer.greet = function(msg, cb) { 148 | cb(null, 'Greetings... ' + msg); 149 | } 150 | 151 | Customer.remoteMethod( 152 | 'greet', 153 | { 154 | accepts: {arg: 'msg', type: 'string'}, 155 | returns: {arg: 'greeting', type: 'string'} 156 | } 157 | ); 158 | 159 | app.model(Customer); 160 | 161 | visualize(app); 162 | app.use(app.get('restApiRoot') || '/', loopback.rest()); 163 | done(); 164 | }; 165 | } 166 | --------------------------------------------------------------------------------