From 90ed072d069d10b118974b1f273f355f27234ce1 Mon Sep 17 00:00:00 2001 From: cons Date: Tue, 10 Nov 2020 17:40:19 +0100 Subject: [PATCH 1/2] wip: add convert script to scripts folder --- scripts/model_definitions_to_gv.js | 178 +++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 scripts/model_definitions_to_gv.js diff --git a/scripts/model_definitions_to_gv.js b/scripts/model_definitions_to_gv.js new file mode 100644 index 0000000..52c9fd2 --- /dev/null +++ b/scripts/model_definitions_to_gv.js @@ -0,0 +1,178 @@ +/** + * ----------------------------------------------------------------------- + * model_definitions_to_gv.js + * + * DESCRIPTION + * utility node script to create a dot readable graphviz (.gv) file, representing a graph visual representation of models found in a data model definitions folder. + * The .gv output will be written to STDOUT + * + * USAGE + * node model_definitions_to_gv path_to_model_folder > out.gv + * + * ARGUMENTS + * argv[0] - path to folder + * argv[1] - optional String representation of a desired order in the graph output. By default the order is depending on the folder itself. + * for example: + * node model_definitions_to_gv path_to_model_folder "model2,model1,model3,..." + * + * CONVERT TO PDF + * make sure to install graphviz (https://graphviz.org/download/) + * + * dot out.gv -Tpdf -o out.pdf + * + */ + +const fs = require('fs'); +const path = require('path'); + +const argv = process.argv.slice(2); + +/** + * Configuration of the graph + */ +const graphConfig = "digraph hierarchy {\n \ +node[shape=record,style=filled,fillcolor=gray95, fontname=Courier, fontsize=15]\n \ +graph [splines=ortho]\n \ +edge[arrowsize=1.5, style=bold]\n \ +ranksep=0.5\n \ +nodesep=1\n \ +esep=1\n"; + +// globals +let associations = {}; +let attributes = {}; +let fKattributes = {}; +let idattributes = {}; +let longestAttribute = {}; + +/** + * run + * + * main function + */ +function run() { + + if(argv.length < 1) { + console.error('Please provide the path to your data model definitions folder'); + process.exit(1); + } + + fs.readdirSync(argv[0]) + .filter(function (file) { + return (file.indexOf('.') !== 0) && (file.slice(-5) === '.json') && !file.includes('_to_'); + }) + .forEach(function (file) { + let json = require(path.relative(__dirname, path.join(argv[0], file))); + parseModel(json); + }); + process.stdout.write(graphConfig); + createNodes(attributes); + createEdges(associations); + process.stdout.write('}') +} + +/** + * parse the json input of a model and write to the global variables + * @param {JSON} json the json input for a model definition + */ +function parseModel(json) { + attributes[json['model']] = {}; + associations[json['model']] = {}; + fKattributes[json['model']] = []; + + Object.assign(attributes[json['model']], json['attributes']); + Object.assign(associations[json['model']], json['associations']); + + idattributes[json['model']] = json['internalId'] ? json['internalId'] : 'id'; + longestAttribute[json['model']] = 0; + Object.keys(attributes[json['model']]).forEach(attr => { + if (attr.length + attributes[json['model']][attr].length > longestAttribute[json['model']]) { + longestAttribute[json['model']] = attr.length + attributes[json['model']][attr].length; + } + }) + if (json['associations'] !== undefined) { + Object.keys(json['associations']).forEach(assocName => { + let assoc = json['associations'][assocName]; + if (assoc['keyIn'] === json['model']) { + if (assoc['reverseAssociationType'] === 'to_many') { + fKattributes[json['model']].push(assoc['sourceKey']); + } else { + fKattributes[json['model']].push(assoc['targetKey']); + } + } + }); + } +} + +/** + * create the .gv Nodes for the output graph + * @param {object} attributes + */ +function createNodes(attributes) { + const order = argv[1] ? argv[1].split(',').reduce((a,c) => (a[c] = '', a), {}) : attributes; + Object.keys(order).forEach(model => { + let sortedAttributes = [idattributes[model]]; + process.stdout.write(` ${model} [label = < {${model[0].toUpperCase()}${model.slice(1)}|`); + + Object.keys(attributes[model]).forEach(attr => { + if (idattributes[model] !== attr && !fKattributes[model].includes(attr)) { + sortedAttributes.push(attr); + } + }); + + sortedAttributes.forEach(attr => { + let spaces = calculateSpaces(attr.length + attributes[model][attr].length, longestAttribute[model]); + + if (idattributes[model] === attr) { + process.stdout.write(`${attr}`); + process.stdout.write(`${" ".repeat(spaces)}`); + process.stdout.write(`${attributes[model][attr]}
`) + } else { + process.stdout.write(`${attr}`); + process.stdout.write(`${" ".repeat(spaces)}`); + process.stdout.write(`${attributes[model][attr]}
`) + } + + }); + + fKattributes[model].forEach(fK => { + let spaces = calculateSpaces(fK.length + attributes[model][fK].length, longestAttribute[model]); + + process.stdout.write(`${fK}`); + process.stdout.write(`${" ".repeat(spaces)}`); + process.stdout.write(`${attributes[model][fK]}
`); + }) + process.stdout.write(`}>]\n`); + + }); +} + +/** + * create the .gv edges for the output graph + * @param {object} associations + */ +function createEdges(associations) { + Object.keys(associations).forEach(model => { + Object.keys(associations[model]).forEach(assoc => { + process.stdout.write(` ${model} -> ${associations[model][assoc]['target']}`); + if (associations[model][assoc]['type'] === 'to_one') { + process.stdout.write(`[color=navy]`); + } else { + process.stdout.write(`[color=crimson]`) + } + process.stdout.write('\n'); + }) + }) +} + +/** + * calculate the number of spaces needed for each attribute + type to be lined up correctly in the graph + * @param {int} length + * @param {int} maxLength + */ +function calculateSpaces(length, maxLength) { + let longestSpace = maxLength + 4; + return longestSpace - length; +} + +run(); \ No newline at end of file From d95e486b7ed5135366affa39b2dae1372a69fdb6 Mon Sep 17 00:00:00 2001 From: coeit Date: Fri, 30 Sep 2022 14:05:35 +0200 Subject: [PATCH 2/2] update uml script --- scripts/model_definitions_to_gv.js | 243 ++++++++++++++++++++--------- 1 file changed, 167 insertions(+), 76 deletions(-) diff --git a/scripts/model_definitions_to_gv.js b/scripts/model_definitions_to_gv.js index 52c9fd2..3bfec3b 100644 --- a/scripts/model_definitions_to_gv.js +++ b/scripts/model_definitions_to_gv.js @@ -1,45 +1,48 @@ /** * ----------------------------------------------------------------------- * model_definitions_to_gv.js - * + * * DESCRIPTION * utility node script to create a dot readable graphviz (.gv) file, representing a graph visual representation of models found in a data model definitions folder. * The .gv output will be written to STDOUT - * + * * USAGE * node model_definitions_to_gv path_to_model_folder > out.gv - * + * * ARGUMENTS * argv[0] - path to folder - * argv[1] - optional String representation of a desired order in the graph output. By default the order is depending on the folder itself. + * argv[1] - set to "printFK" to print the foreignKeys. Everything else won't print the foreign keys. + * argv[2] - optional String representation of a desired order in the graph output. By default the order is depending on the folder itself. * for example: * node model_definitions_to_gv path_to_model_folder "model2,model1,model3,..." - * + * * CONVERT TO PDF * make sure to install graphviz (https://graphviz.org/download/) - * + * * dot out.gv -Tpdf -o out.pdf - * + * */ -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); const argv = process.argv.slice(2); /** * Configuration of the graph */ -const graphConfig = "digraph hierarchy {\n \ +const graphConfig = + "digraph hierarchy {\n \ node[shape=record,style=filled,fillcolor=gray95, fontname=Courier, fontsize=15]\n \ graph [splines=ortho]\n \ edge[arrowsize=1.5, style=bold]\n \ ranksep=0.5\n \ nodesep=1\n \ -esep=1\n"; +esep=0.1\n"; // globals let associations = {}; +let parsedAssociations = []; let attributes = {}; let fKattributes = {}; let idattributes = {}; @@ -47,132 +50,220 @@ let longestAttribute = {}; /** * run - * + * * main function */ function run() { - - if(argv.length < 1) { - console.error('Please provide the path to your data model definitions folder'); + if (argv.length < 1) { + console.error( + "Please provide the path to your data model definitions folder" + ); process.exit(1); } + const printFK = argv[1] === "printFK" ? true : false; fs.readdirSync(argv[0]) .filter(function (file) { - return (file.indexOf('.') !== 0) && (file.slice(-5) === '.json') && !file.includes('_to_'); + return ( + file.indexOf(".") !== 0 && + file.slice(-5) === ".json" && + !file.includes("_to_") + ); }) .forEach(function (file) { let json = require(path.relative(__dirname, path.join(argv[0], file))); - parseModel(json); + parseModel(json, printFK); }); + process.stdout.write(graphConfig); - createNodes(attributes); - createEdges(associations); - process.stdout.write('}') + createNodes(attributes, printFK); + createEdges(parsedAssociations); + process.stdout.write("}"); + + // console.log(JSON.stringify(fKattributes,null,2)); } /** * parse the json input of a model and write to the global variables * @param {JSON} json the json input for a model definition + * @param {boolean} printFK */ -function parseModel(json) { - attributes[json['model']] = {}; - associations[json['model']] = {}; - fKattributes[json['model']] = []; - - Object.assign(attributes[json['model']], json['attributes']); - Object.assign(associations[json['model']], json['associations']); - - idattributes[json['model']] = json['internalId'] ? json['internalId'] : 'id'; - longestAttribute[json['model']] = 0; - Object.keys(attributes[json['model']]).forEach(attr => { - if (attr.length + attributes[json['model']][attr].length > longestAttribute[json['model']]) { - longestAttribute[json['model']] = attr.length + attributes[json['model']][attr].length; - } - }) - if (json['associations'] !== undefined) { - Object.keys(json['associations']).forEach(assocName => { - let assoc = json['associations'][assocName]; - if (assoc['keyIn'] === json['model']) { - if (assoc['reverseAssociationType'] === 'to_many') { - fKattributes[json['model']].push(assoc['sourceKey']); +function parseModel(json, printFK) { + attributes[json["model"]] = {}; + associations[json["model"]] = {}; + fKattributes[json["model"]] = []; + + Object.assign(attributes[json["model"]], json["attributes"]); + Object.assign(associations[json["model"]], json["associations"]); + + idattributes[json["model"]] = json["internalId"] ? json["internalId"] : "id"; + longestAttribute[json["model"]] = json["model"].length; + + if (!json["internalId"]) { + attributes[json["model"]].id = "Int"; + } + + if (json["associations"] !== undefined) { + Object.keys(json["associations"]).forEach((assocName) => { + let association = json["associations"][assocName]; + + if (association["keysIn"] === json["model"]) { + if ( + association["type"] === "many_to_many" && + association["implementation"] === "foreignkeys" + ) { + fKattributes[json["model"]].push(association["sourceKey"]); } else { - fKattributes[json['model']].push(assoc['targetKey']); + fKattributes[json["model"]].push(association["targetKey"]); } } + if ( + parsedAssociations.find( + (assoc) => + association.reverseAssociation === assoc.name && + association.target === assoc.model + ) + ) { + return; + } else { + parsedAssociations.push({ + model: json["model"], + name: assocName, + target: association["target"], + type: association["type"], + }); + } }); } + + Object.keys(attributes[json["model"]]).forEach((attr) => { + const validAttr = printFK + ? true + : !fKattributes[json["model"]].includes(attr); + if ( + validAttr && + attr.length + attributes[json["model"]][attr].length > + longestAttribute[json["model"]] + ) { + longestAttribute[json["model"]] = + attr.length + attributes[json["model"]][attr].length; + } + }); } /** * create the .gv Nodes for the output graph - * @param {object} attributes + * @param {object} attributes + * @param {boolean} printFK */ -function createNodes(attributes) { - const order = argv[1] ? argv[1].split(',').reduce((a,c) => (a[c] = '', a), {}) : attributes; - Object.keys(order).forEach(model => { +function createNodes(attributes, printFK) { + const order = argv[2] + ? argv[2].split(",").reduce((a, c) => ((a[c] = ""), a), {}) + : attributes; + Object.keys(order).forEach((model) => { let sortedAttributes = [idattributes[model]]; - process.stdout.write(` ${model} [label = < {${model[0].toUpperCase()}${model.slice(1)}|`); + process.stdout.write( + ` ${model} [label = < {${model[0].toUpperCase()}${model.slice(1)}|` + ); - Object.keys(attributes[model]).forEach(attr => { + Object.keys(attributes[model]).forEach((attr) => { if (idattributes[model] !== attr && !fKattributes[model].includes(attr)) { sortedAttributes.push(attr); } }); - sortedAttributes.forEach(attr => { - let spaces = calculateSpaces(attr.length + attributes[model][attr].length, longestAttribute[model]); + sortedAttributes.forEach((attr) => { + const typeLength = attributes[model][attr].length; + let spaces = calculateSpaces( + attr.length + typeLength, + longestAttribute[model] + ); if (idattributes[model] === attr) { process.stdout.write(`${attr}`); process.stdout.write(`${" ".repeat(spaces)}`); - process.stdout.write(`${attributes[model][attr]}
`) + process.stdout.write( + `${attributes[model][attr]}
` + ); } else { process.stdout.write(`${attr}`); process.stdout.write(`${" ".repeat(spaces)}`); - process.stdout.write(`${attributes[model][attr]}
`) + process.stdout.write( + `${attributes[model][attr]}
` + ); } - }); - fKattributes[model].forEach(fK => { - let spaces = calculateSpaces(fK.length + attributes[model][fK].length, longestAttribute[model]); + if (printFK) { + fKattributes[model].forEach((fK) => { + let spaces = calculateSpaces( + fK.length + attributes[model][fK].length, + longestAttribute[model] + ); - process.stdout.write(`${fK}`); - process.stdout.write(`${" ".repeat(spaces)}`); - process.stdout.write(`${attributes[model][fK]}
`); - }) + process.stdout.write(`${fK}`); + process.stdout.write(`${" ".repeat(spaces)}`); + process.stdout.write( + `${attributes[model][fK]}
` + ); + }); + } process.stdout.write(`}>]\n`); - }); } /** * create the .gv edges for the output graph - * @param {object} associations + * @param {object} associations */ -function createEdges(associations) { - Object.keys(associations).forEach(model => { - Object.keys(associations[model]).forEach(assoc => { - process.stdout.write(` ${model} -> ${associations[model][assoc]['target']}`); - if (associations[model][assoc]['type'] === 'to_one') { - process.stdout.write(`[color=navy]`); - } else { - process.stdout.write(`[color=crimson]`) - } - process.stdout.write('\n'); - }) - }) +function createEdges(parsedAssociations) { + parsedAssociations.forEach((assoc) => { + const relation = translateRelation(assoc); + process.stdout.write( + ` ${assoc.model} -> ${assoc.target} [minlen=2 color=navy headlabel=${relation[1]} taillabel=${relation[0]} labeldistance=2 arrowhead=none lp=5]\n` + // ` ${assoc.model} -> ${assoc.target} [dir=both minlen=2 color=navy arrowhead=${relation[1]} arrowtail=${relation[0]}]\n` + ); + }); +} + +function translateRelation(relation) { + switch (relation.type) { + case "one_to_one": + return ["1", "1"]; + case "one_to_many": + return ["1", "n"]; + case "many_to_one": + return ["n", "1"]; + case "many_to_many": + return ["n", "m"]; + default: + break; + } +} + +function translateRelation2(relation) { + switch (relation.type) { + case "one_to_one": + return ["dot", "dot"]; + case "one_to_many": + return ["dot", "inv"]; + case "many_to_one": + return ["inv", "dot"]; + case "many_to_many": + return ["inv", "inv"]; + default: + break; + } } /** * calculate the number of spaces needed for each attribute + type to be lined up correctly in the graph - * @param {int} length - * @param {int} maxLength + * @param {int} length + * @param {int} maxLength */ function calculateSpaces(length, maxLength) { let longestSpace = maxLength + 4; return longestSpace - length; } -run(); \ No newline at end of file +run();