From 525c4d2aa73ae124da17a3c64a2ca44d13fcc49d Mon Sep 17 00:00:00 2001 From: Benjamin Auder Date: Thu, 25 Jan 2018 00:10:04 +0100 Subject: [PATCH] First commit --- .gitignore | 1 + README.md | 134 +++++++++++++++++++ example.html | 33 +++++ parser.js | 287 ++++++++++++++++++++++++++++++++++++++++ scripts/getGraphSvg.php | 8 ++ 5 files changed, 463 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example.html create mode 100644 parser.js create mode 100644 scripts/getGraphSvg.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9cf9fd --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# erdiag: Entity-Relationship Diagrams Generator + +Inspired by [this repository](https://code.google.com/archive/p/merisier/). + +This parser reads ER diagrams definition files, and produces two types of diagrams + SQL code. +[Graphviz](https://www.graphviz.org/) is used on server side to translate parsed graph descriptions into SVG objects. + +*Note:* at the moment, only the conceptual graph is implemented, and no comments are allowed in textual descriptions. +At least the former is planned, and also probably a way to indicate relative identifiers. + +----- + +An entity is defined as follow + [Entity] + #attr1 (*) + attr2 (*) +with (\*) = optional SQL indications, and # denoting a (part of) a key. + +A relationship is defined in this way + {Relation} + Entity C1 + Entity2 C2 + -- + attr1 (*) + attr2 (*) +where attributes are optional, and C1 (resp. C2) = cardinality for entity 1 (resp. 2). +Defining relationships with more than two attributes is easy: just add entities. +Cardinality dictionary: + * \* = 0..n + * + = 1..n + * 1 = 1..1 + * ? = 0..1 + +To mark a weak entity, just surround its name by extra-brackets + [[WeakEntity]] +In the same way, a weak relation can be written + {{WeakRelation}} +The syntax for these two last is then the same as in the non-weak versions. + +To indicate an inheritance relation, proceed as follow + is_a + Animal Cat Fish + Planet Mars Venus + +Finally, blocks must be separated by new lines. For a usage example, see example.html + +Note that the "drawMcd" method can take a second argument, which indicates the type of graph. + * "bubble" draws the standard graph, as seen [here](https://en.wikipedia.org/wiki/Entity%E2%80%93relationship_model#/media/File:ER_Diagram_MMORPG.png) for example + * "compact" (default) use the same box for an entity and its attributes + +----- + +Here is how the example file should render: + + + +%3 + + +Musician + +Musician + +id + +name + +band + +role + + + +Play + +Play + + +Musician:name--Play + +1,n + + +Instrument + +Instrument + +name + +family + + + +Instrument:name--Play + +0,n + + +Piano + +Piano + +type + + + +Piano:name--Instrument:name + + + + +Guitar + +Guitar + +type + + + +Guitar:name--Instrument:name + + + + +Play_event + +event + + +Play_event--Play + + + + diff --git a/example.html b/example.html new file mode 100644 index 0000000..22d2d3a --- /dev/null +++ b/example.html @@ -0,0 +1,33 @@ +
+ + + diff --git a/parser.js b/parser.js new file mode 100644 index 0000000..8a09321 --- /dev/null +++ b/parser.js @@ -0,0 +1,287 @@ +// ER diagram description parser +class ErDiags +{ + constructor(description) + { + this.entities = {}; + this.inheritances = []; + this.associations = []; + this.txt2json(description); + // Cache SVG graphs returned by server (in addition to server cache = good perfs) + this.mcdGraph = ""; + this.mldGraph = ""; + this.sqlText = ""; + } + + static get TYPES() + { + // SQLite types without null (TODO: be more general) + return ["integer","real","text","blob"]; + } + + static get CARDINAL() + { + return { + "*": "0,n", + "+": "1,n", + "?": "0,1", + "1": "1,1" + }; + } + + ////////////////// + // PARSING STAGE 1 + ////////////////// + + // Parse a textual description into a json object + txt2json(text) + { + let lines = text.split("\n"); + lines.push(""); //easier parsing: always empty line at the end + let start = -1; + for (let i=0; i < lines.length; i++) + { + lines[i] = lines[i].trim(); + // Empty line ? + if (lines[i].length == 0) + { + if (start >= 0) //there is some group of lines to parse + { + this.parseThing(lines, start, i); + start = -1; + } + } + else //not empty line: just register starting point + { + if (start < 0) + start = i; + } + } + } + + // Parse a group of lines into entity, association, ... + parseThing(lines, start, end) //start included, end excluded + { + switch (lines[start].charAt(0)) + { + case '[': + // Entity = { name: { attributes, [weak] } } + let name = lines[start].match(/\w+/)[0]; + let entity = { attributes: this.parseAttributes(lines, start+1, end) }; + if (lines[start].charAt(1) == '[') + entity.weak = true; + this.entities[name] = entity; + break; + case 'i': //inheritance (arrows) + this.inheritances = this.inheritances.concat(this.parseInheritance(lines, start+1, end)); + break; + case '{': //association + // Association = { [name], [attributes], [weak], entities: ArrayOf entity indices } + let relationship = { }; + let nameRes = lines[start].match(/\w+/); + if (nameRes !== null) + relationship.name = nameRes[0]; + if (lines[start].charAt(1) == '{') + relationship.weak = true; + this.associations.push(Object.assign({}, relationship, this.parseAssociation(lines, start+1, end))); + break; + } + } + + // attributes: ArrayOf {name, [isKey], [type], [qualifiers]} + parseAttributes(lines, start, end) + { + let attributes = []; + for (let i=start; i 0) + { + element.innerHTML = this.mcdGraph; + return; + } + // Build dot graph input + let mcdDot = 'graph {\n'; + // Nodes: + Object.keys(this.entities).forEach( name => { + if (mcdStyle == "bubble") + { + mcdDot += name + '[shape=rectangle, label="' + name + '"'; + if (this.entities[name].weak) + mcdDot += ', peripheries=2'; + mcdDot += '];\n'; + if (!!this.entities[name].attributes) + { + this.entities[name].attributes.forEach( a => { + let label = (a.isKey ? '#' : '') + a.name; + mcdDot += name + '_' + a.name + '[shape=ellipse, label="' + label + '"];\n'; + mcdDot += name + '_' + a.name + ' -- ' + name + ';\n'; + }); + } + } + else + { + mcdDot += name + '[shape=plaintext, label=<'; + if (this.entities[name].weak) + { + mcdDot += '' + + '
\n'; + } + else + mcdDot += '
\n'; + mcdDot += '\n'; + if (!!this.entities[name].attributes) + { + this.entities[name].attributes.forEach( a => { + let label = (a.isKey ? '' : '') + a.name + (a.isKey ? '' : ''); + mcdDot += '\n'; + }); + } + mcdDot += '
' + name + '
' + label + '
'; + if (this.entities[name].weak) + mcdDot += '
'; + mcdDot += '>];\n'; + } + }); + // Inheritances: + this.inheritances.forEach( i => { + i.children.forEach( c => { + mcdDot += c + ':name -- ' + i.parent + ':name [len="1.00", dir="forward", arrowhead="vee", style="dashed"];\n'; + }); + }); + // Relationships: + let assoceCounter = 0; + this.associations.forEach( a => { + let name = !!a.name && a.name.length > 0 + ? a.name + : '_assoce' + assoceCounter++; + mcdDot += name + '[shape="diamond", style="filled", color="lightgrey", label="' + (!!a.name ? a.name : '') + '"'; + if (a.weak) + mcdDot += ', peripheries=2'; + mcdDot += '];\n'; + a.entities.forEach( e => { + mcdDot += e.name + ':name -- ' + name + '[len="1.00", label="' + ErDiags.CARDINAL[e.card] + '"];\n'; + }); + if (!!a.attributes) + { + a.attributes.forEach( attr => { + let label = (attr.isKey ? '#' : '') + attr.name; + mcdDot += name + '_' + attr.name + '[len="1.00", shape=ellipse, label="' + label + '"];\n'; + mcdDot += name + '_' + attr.name + ' -- ' + name + ';\n'; + }); + } + }); + mcdDot += '}'; + //console.log(mcdDot); + ErDiags.AjaxGet(mcdDot, graphSvg => { + this.mcdGraph = graphSvg; + element.innerHTML = graphSvg; + }) + } + + // "Modèle logique des données" + drawMld(id) + { + let element = document.getElementById(id); + if (this.mldGraph.length > 0) + { + element.innerHTML = this.mcdGraph; + return; + } + //UNIMPLEMENTED + // TODO: analyze cardinalities (eat attributes, create new tables...) + // mldDot = ... + // this.graphMld = ... + } + + fillSql(id) + { + let element = document.getElementById(id); + if (this.sqlText.length > 0) + { + element.innerHTML = this.sqlText; + return; + } + //UNIMPLEMENTED (should be straightforward from MLD) + } +} diff --git a/scripts/getGraphSvg.php b/scripts/getGraphSvg.php new file mode 100644 index 0000000..659b44d --- /dev/null +++ b/scripts/getGraphSvg.php @@ -0,0 +1,8 @@ + -- 2.44.0