X-Git-Url: https://git.auder.net/?p=erdiag.git;a=blobdiff_plain;f=parser.js;h=0ab6a4321a15f59a62718d39cbfc699d17563e39;hp=8fc65edc3330eedf29ac02c7d9a9e957ab217a3f;hb=HEAD;hpb=c728aeca9ef83bb786ed0c886a2b3dc4faf3dc45 diff --git a/parser.js b/parser.js index 8fc65ed..0ab6a43 100644 --- a/parser.js +++ b/parser.js @@ -1,43 +1,37 @@ // ER diagram description parser class ErDiags { - constructor(description) + constructor(description, output, image) { - this.entities = {}; - this.inheritances = []; - this.associations = []; - this.txt2json(description); - this.tables = []; - // Cache SVG graphs returned by server (in addition to server cache = good perfs) - this.mcdGraph = ""; - this.mldGraph = ""; - this.sqlText = ""; + this.entities = { }; + this.inheritances = [ ]; + this.associations = [ ]; + this.tables = { }; + this.mcdParsing(description); + this.mldParsing(); + this.output = output || "graph"; + this.image = image || "svg"; } - static get TYPES() + static CARDINAL(symbol) { - // SQLite storage classes without null - return ["integer","real","text","blob"]; - } - - static get CARDINAL() - { - return { - "*": "0,n", - "+": "1,n", - "?": "0,1", - "1": "1,1", - "?R": "(0,1)", - "1R": "(1,1)", - }; + let res = { "*": "0,n", "+": "1,n", "?": "0,1", "1": "1,1" } [ symbol[0] ]; + if (symbol.length >= 2) + { + if (symbol[1] == 'R') + res = '(' + res + ')'; + else if (['>','<'].includes(symbol[1])) + res += symbol[1]; + } + return res; } - ////////////////// - // PARSING STAGE 1 - ////////////////// + /////////////////////////////// + // PARSING STAGE 1: text to MCD + /////////////////////////////// // Parse a textual description into a json object - txt2json(text) + mcdParsing(text) { let lines = text.split("\n"); lines.push(""); //easier parsing: always empty line at the end @@ -94,29 +88,22 @@ class ErDiags // attributes: ArrayOf {name, [isKey], [type], [qualifiers]} parseAttributes(lines, start, end) { - let attributes = []; + let attributes = [ ]; for (let i=start; i 0) { - let sqlClues = parenthesis[1]; - let qualifiers = sqlClues; - let firstWord = sqlClues.match(/[^\s]+/)[0]; - if (ErDiags.TYPES.includes(firstWord)) - { - field.type = firstWord; - qualifiers = sqlClues.substring(firstWord.length).trim(); - } - field.qualifiers = qualifiers; + field.type = sqlClues.match(/[^\s]+/)[0]; //type is always the first indication (mandatory) + field.qualifiers = sqlClues.substring(field.type.length); } attributes.push(field); } @@ -166,32 +153,141 @@ class ErDiags return assoce; } - ////////////////// - // PARSING STAGE 2 - ////////////////// + ////////////////////////////// + // PARSING STAGE 2: MCD to MLD + ////////////////////////////// - static AjaxGet(dotInput, callback) + // From entities + relationships to tables + mldParsing() { - let xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (this.readyState == 4 && this.status == 200) - callback(this.responseText); - }; - xhr.open("GET", "scripts/getGraphSvg.php?dot=" + encodeURIComponent(dotInput), true); - xhr.send(); + // Pass 1: initialize tables + Object.keys(this.entities).forEach( name => { + let newTable = [ ]; //array of fields + this.entities[name].attributes.forEach( attr => { + let newField = { + name: attr.name, + type: attr.type, + isKey: attr.isKey, + }; + if (!!attr.qualifiers && !!attr.qualifiers.match(/references/i)) + { + Object.assign(newField, {ref: attr.qualifiers.match(/references ([^\s]+)/i)[1]}); + newField.qualifiers = attr.qualifiers.replace(/references [^\s]+/i, ""); + } + newTable.push(newField); + }); + this.tables[name] = newTable; + }); + // Add foreign keys information for children (inheritance). TODO: allow several levels + // NOTE: modelisation assume each child has its own table, refering parent (other options exist) + this.inheritances.forEach( inh => { + let idx = this.tables[inh.parent].findIndex( item => { return item.isKey; }); + inh.children.forEach( c => { + this.tables[c].push({ + name: inh.parent + "_id", + type: this.tables[inh.parent][idx].type, + isKey: true, + qualifiers: this.tables[inh.parent][idx].qualifiers || "", + ref: inh.parent + "(" + this.tables[inh.parent][idx].name + ")", + }); + }); + }); + // Pass 2: parse associations, add foreign keys when cardinality is 0,1 or 1,1 + this.associations.forEach( a => { + let newTableAttrs = [ ]; + let hasZeroOne = false; + a.entities.forEach( e => { + if (['?','1'].includes(e.card[0])) + { + hasZeroOne = true; + // Foreign key apparition (for each entity in association minus current one, for each identifying attribute) + a.entities.forEach( e2 => { + if (e2.name == e.name) + return; + this.entities[e2.name].attributes.forEach( attr => { + if (attr.isKey) + { + // For "weak tables", foreign keys become part of the key + const isKey = e.card.length >= 2 && e.card[1] == 'R'; + this.tables[e.name].push({ + isKey: isKey, + name: e2.name + "_" + attr.name, + type: attr.type, + qualifiers: !isKey && e.card[0]=='1' ? "not null" : "", + ref: e2.name + "(" + attr.name + ")", + }); + } + }); + }); + } + else + { + // Add all keys in current entity + let fields = this.entities[e.name].attributes.filter( attr => { return attr.isKey; }); + newTableAttrs.push({ + fields: fields, + entity: e.name, + }); + } + }); + if (!hasZeroOne && newTableAttrs.length > 1) + { + // Ok, really create a new table + let newTable = { + name: a.name || newTableAttrs.map( item => { return item.entity; }).join("_"), + fields: [ ], + }; + newTableAttrs.forEach( item => { + item.fields.forEach( f => { + newTable.fields.push({ + name: item.entity + "_" + f.name, + isKey: true, + type: f.type, + qualifiers: f.qualifiers || "", + ref: item.entity + "(" + f.name + ")", + }); + }); + }); + // Check for duplicates (in case of self-relationship), rename if needed + newTable.fields.forEach( (f,i) => { + const idx = newTable.fields.findIndex( item => { return item.name == f.name; }); + if (idx < i) + { + // Current field is a duplicate + let suffix = 2; + let newName = f.name + suffix; + while (newTable.fields.findIndex( item => { return item.name == newName; }) >= 0) + { + suffix++; + newName = f.name + suffix; + } + f.name = newName; + } + }); + // Add relationship potential own attributes + (a.attributes || [ ]).forEach( attr => { + newTable.fields.push({ + name: attr.name, + isKey: false, + type: attr.type, + qualifiers: attr.qualifiers, + }); + }); + this.tables[newTable.name] = newTable.fields; + } + }); } + ///////////////////////////////// + // DRAWING + GET SQL FROM PARSING + ///////////////////////////////// + // "Modèle conceptuel des données". TODO: option for graph size // NOTE: randomizing helps to obtain better graphs (sometimes) drawMcd(id, mcdStyle) //mcdStyle: bubble, or compact { let element = document.getElementById(id); mcdStyle = mcdStyle || "compact"; - if (this.mcdGraph.length > 0) - { - element.innerHTML = this.mcdGraph; - return; - } // Build dot graph input let mcdDot = 'graph {\n'; mcdDot += 'rankdir="LR";\n'; @@ -250,10 +346,10 @@ class ErDiags // https://www.developpez.net/forums/d1088964/general-developpement/alm/modelisation/structure-agregation-l-association-d-association/ _.shuffle(i.children).forEach( c => { if (Math.random() < 0.5) - mcdDot += '"' + c + '":name -- "' + i.parent; + mcdDot += '"' + c + '":name -- "' + i.parent + '":name [dir="forward",arrowhead="vee",'; else - mcdDot += '"' + i.parent + '":name -- "' + c; - mcdDot += '":name [dir="forward", arrowhead="vee", style="dashed"];\n'; + mcdDot += '"' + i.parent + '":name -- "' + c + '":name [dir="back",arrowtail="vee",'; + mcdDot += 'style="dashed"];\n'; }); }); // Relationships: @@ -261,9 +357,7 @@ class ErDiags mcdDot += 'node [shape=rectangle, style=rounded];\n'; let assoceCounter = 0; _.shuffle(this.associations).forEach( a => { - let name = !!a.name && a.name.length > 0 - ? a.name - : '_assoce' + assoceCounter++; + let name = a.name || "_assoce" + assoceCounter++; if (mcdStyle == "bubble") { mcdDot += '"' + name + '" [shape="diamond", style="filled", color="lightgrey", label="' + name + '"'; @@ -285,12 +379,12 @@ class ErDiags } else { - let label = name; + let label = '<' + name + '>'; if (!!a.attributes) { a.attributes.forEach( attr => { let attrLabel = (attr.isKey ? '#' : '') + attr.name; - label += '\\n<' + attrLabel + '>'; + label += '\\n' + attrLabel; }); } mcdDot += '"' + name + '" [color="lightgrey", label="' + label + '"'; @@ -303,61 +397,82 @@ class ErDiags mcdDot += '"' + e.name + '":name -- "' + name + '"'; else mcdDot += '"' + name + '" -- "' + e.name + '":name'; - mcdDot += '[label="' + ErDiags.CARDINAL[e.card] + '"];\n'; + mcdDot += '[label="' + ErDiags.CARDINAL(e.card) + '"];\n'; }); }); mcdDot += '}'; - console.log(mcdDot); - ErDiags.AjaxGet(mcdDot, graphSvg => { - this.mcdGraph = graphSvg; - element.innerHTML = graphSvg; - }); + if (this.output == "graph") //draw graph in element + element.innerHTML = ""; + else //output = "text": just show dot input + element.innerHTML = mcdDot.replace(//g,">"); } - // "Modèle logique des données" - // TODO: this one should draw links from foreign keys to keys (port=... in ) + // "Modèle logique des données", from MCD without anomalies drawMld(id) { let element = document.getElementById(id); - if (this.mldGraph.length > 0) - { - element.innerHTML = this.mcdGraph; - return; - } - // Build dot graph input + // Build dot graph input (assuming foreign keys not already present...) let mldDot = 'graph {\n'; - // Nodes: - Object.keys(this.entities).forEach( name => { - //mld. ... --> devient table - // mldDot = ... - }); - // Relationships: - this.associations.forEach( a => { - a.entities.forEach( e => { // e.card e.name ... - // Pass 1 : entites deviennent tables - // Pass 2 : sur les assoces - // multi-arite : sub-loop si 0,1 ou 1,1 : aspiré comme attribut de l'association (phase 1) - // ensuite, que du 0,n ou 1,n : si == 1, OK une table - // si 2 ou + : n tables + 1 pour l'assoce, avec attrs clés étrangères - // clé étrangère NOT NULL si 1,1 + mldDot += 'rankdir="LR";\n'; + mldDot += 'node [shape=plaintext];\n'; + let links = ""; + _.shuffle(Object.keys(this.tables)).forEach( name => { + mldDot += '"' + name + '" [label=<\n'; + mldDot += '\n'; + this.tables[name].forEach( f => { + let label = (f.isKey ? '' : '') + (!!f.ref ? '#' : '') + f.name + (f.isKey ? '' : ''); + mldDot += '\n'; + if (!!f.ref) + { + const refPort = f.ref.slice(0,-1).replace('(',':'); + if (Math.random() < 0.5) + links += refPort + ' -- "' + name+'":"'+f.name + '" [dir="forward",arrowhead="dot"'; + else + links += '"'+name+'":"'+f.name+'" -- ' + refPort + ' [dir="back",arrowtail="dot"'; + links += ']\n;'; + } }); + mldDot += '
' + name + '
' + label + '
>];\n'; }); - // this.graphMld = ... - //console.log(mldDot); - ErDiags.AjaxGet(mldDot, graphSvg => { - this.mldGraph = graphSvg; - element.innerHTML = graphSvg; - }); + mldDot += links + '\n'; + mldDot += '}'; + if (this.output == "graph") + element.innerHTML = ""; + else + element.innerHTML = mldDot.replace(//g,">"); } fillSql(id) { let element = document.getElementById(id); - if (this.sqlText.length > 0) + if (!!this.sqlText) { element.innerHTML = this.sqlText; return; } - //UNIMPLEMENTED (should be straightforward from MLD) + let sqlText = ""; + Object.keys(this.tables).forEach( name => { + sqlText += "CREATE TABLE " + name + " (\n"; + let key = ""; + let foreignKey = [ ]; + this.tables[name].forEach( f => { + let type = f.type || (f.isKey ? "INTEGER" : "TEXT"); + if (!!f.ref) + foreignKey.push({name: f.name, ref: f.ref}); + sqlText += "\t" + f.name + " " + type + " " + (f.qualifiers || "") + ",\n"; + if (f.isKey) + key += (key.length>0 ? "," : "") + f.name; + }); + sqlText += "\tPRIMARY KEY (" + key + ")"; + foreignKey.forEach( f => { + let refParts = f.ref.split("("); + const table = refParts[0]; + const field = refParts[1].slice(0,-1); //remove last parenthesis + sqlText += ",\n\tFOREIGN KEY (" + f.name + ") REFERENCES " + table + "(" + field + ")"; + }); + sqlText += "\n);\n"; + }); + this.sqlText = sqlText; + element.innerHTML = "
" + sqlText + "
"; } }