X-Git-Url: https://git.auder.net/?p=erdiag.git;a=blobdiff_plain;f=parser.js;h=f41a755ef87dc3ab7b933aa0bad911a866d3b25e;hp=8a4c52499f17df2fe92e28150b72e825b98c0f69;hb=5fe4fa10fa947c09ae37841bad3bc76e5767fa0b;hpb=d6c9499e06c65780d5f4c95664c2cb851ff6f2de diff --git a/parser.js b/parser.js index 8a4c524..f41a755 100644 --- a/parser.js +++ b/parser.js @@ -3,39 +3,36 @@ class ErDiags { constructor(description) { - this.entities = {}; - this.inheritances = []; - this.associations = []; - this.txt2json(description); - this.tables = []; + this.entities = { }; + this.inheritances = [ ]; + this.associations = [ ]; + this.tables = { }; + this.mcdParsing(description); + this.mldParsing(); // Cache SVG graphs returned by server (in addition to server cache = good perfs) this.mcdGraph = ""; this.mldGraph = ""; this.sqlText = ""; } - static get TYPES() - { - // SQLite storage classes without null - return ["integer","real","text","blob"]; - } - static get CARDINAL() { return { "*": "0,n", "+": "1,n", "?": "0,1", - "1": "1,1" + "1": "1,1", + "?R": "(0,1)", + "1R": "(1,1)", }; } - ////////////////// - // 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 @@ -67,7 +64,7 @@ class ErDiags { case '[': // Entity = { name: { attributes, [weak] } } - let name = lines[start].match(/[^\[\]\s]+/)[0]; + let name = lines[start].match(/[^\[\]"\s]+/)[0]; let entity = { attributes: this.parseAttributes(lines, start+1, end) }; if (lines[start].charAt(1) == '[') entity.weak = true; @@ -79,7 +76,7 @@ class ErDiags case '{': //association // Association = { [name], [attributes], [weak], entities: ArrayOf entity indices } let relationship = { }; - let nameRes = lines[start].match(/[^{}\s]+/); + let nameRes = lines[start].match(/[^{}"\s]+/); if (nameRes !== null) relationship.name = nameRes[0]; if (lines[start].charAt(1) == '{') @@ -92,7 +89,7 @@ class ErDiags // attributes: ArrayOf {name, [isKey], [type], [qualifiers]} parseAttributes(lines, start, end) { - let attributes = []; + let attributes = [ ]; for (let i=start; i { + let newTable = [ ]; //array of fields + this.entities[name].attributes.forEach( attr => { + newTable.push({ + name: attr.name, + type: attr.type, + isKey: attr.isKey, + qualifiers: attr.qualifiers, + }); + }); + 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 || "") + " foreign key references " + inh.parent, + ref: inh.parent, + }); + }); + }); + // Pass 2: parse associations, add foreign keys when cardinality is 0,1 or 1,1 + this.associations.forEach( a => { + let newTableAttrs = [ ]; + a.entities.forEach( e => { + if (['?','1'].includes(e.card[0])) + { + // 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) + { + this.tables[e.name].push({ + isKey: e.card.length >= 2 && e.card[1] == 'R', //"weak tables" foreign keys become part of the key + name: e2.name + "_" + attr.name, + type: attr.type, + qualifiers: "foreign key references " + e2.name + " " + (e.card[0]=='1' ? "not null" : ""), + ref: e2.name, //easier drawMld function (fewer regexps) + }); + } + }); + }); + } + 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 (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 || "") + " foreign key references " + item.entity + " not null", + ref: item.entity, + }); + }); + }); + // 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 + ///////////////////////////////// static AjaxGet(dotInput, callback) { @@ -177,6 +271,7 @@ class ErDiags } // "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); @@ -188,11 +283,14 @@ class ErDiags } // Build dot graph input let mcdDot = 'graph {\n'; + mcdDot += 'rankdir="LR";\n'; // Nodes: - Object.keys(this.entities).forEach( name => { + if (mcdStyle == "compact") + mcdDot += 'node [shape=plaintext];\n'; + _.shuffle(Object.keys(this.entities)).forEach( name => { if (mcdStyle == "bubble") { - mcdDot += name + '[shape=rectangle, label="' + name + '"'; + mcdDot += '"' + name + '" [shape=rectangle, label="' + name + '"'; if (this.entities[name].weak) mcdDot += ', peripheries=2'; mcdDot += '];\n'; @@ -200,14 +298,18 @@ class ErDiags { 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'; + let attrName = name + '_' + a.name; + mcdDot += '"' + attrName + '" [shape=ellipse, label="' + label + '"];\n'; + if (Math.random() < 0.5) + mcdDot += '"' + attrName + '" -- "' + name + '";\n'; + else + mcdDot += '"' + name + '" -- "' + attrName + '";\n'; }); } } else { - mcdDot += name + '[shape=plaintext, label=<'; + mcdDot += '"' + name + '" [label=<'; if (this.entities[name].weak) { mcdDot += '' + @@ -230,32 +332,66 @@ class ErDiags } }); // 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'; + _.shuffle(this.inheritances).forEach( i => { + // TODO: node shape = triangle fill yellow. See + // https://merise.developpez.com/faq/?page=MCD#CIF-ou-dependance-fonctionnelle-de-A-a-Z + // https://merise.developpez.com/faq/?page=MLD#Comment-transformer-un-MCD-en-MLD + // 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 + '":name [dir="forward",arrowhead="vee",'; + else + mcdDot += '"' + i.parent + '":name -- "' + c + '":name [dir="back",arrowtail="vee",'; + mcdDot += 'style="dashed"];\n'; }); }); // Relationships: + if (mcdStyle == "compact") + mcdDot += 'node [shape=rectangle, style=rounded];\n'; 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) + _.shuffle(this.associations).forEach( a => { + let name = a.name || "_assoce" + assoceCounter++; + if (mcdStyle == "bubble") { - 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 += '"' + name + '" [shape="diamond", style="filled", color="lightgrey", label="' + name + '"'; + if (a.weak) + mcdDot += ', peripheries=2'; + mcdDot += '];\n'; + if (!!a.attributes) + { + a.attributes.forEach( attr => { + let label = (attr.isKey ? '#' : '') + attr.name; + mcdDot += '"' + name + '_' + attr.name + '" [shape=ellipse, label="' + label + '"];\n'; + let attrName = name + '_' + attr.name; + if (Math.random() < 0.5) + mcdDot += '"' + attrName + '" -- "' + name + '";\n'; + else + mcdDot += '"' + name + '" -- "' + attrName + '";\n'; + }); + } + } + else + { + let label = '<' + name + '>'; + if (!!a.attributes) + { + a.attributes.forEach( attr => { + let attrLabel = (attr.isKey ? '#' : '') + attr.name; + label += '\\n' + attrLabel; + }); + } + mcdDot += '"' + name + '" [color="lightgrey", label="' + label + '"'; + if (a.weak) + mcdDot += ', peripheries=2'; + mcdDot += '];\n'; } + _.shuffle(a.entities).forEach( e => { + if (Math.random() < 0.5) + mcdDot += '"' + e.name + '":name -- "' + name + '"'; + else + mcdDot += '"' + name + '" -- "' + e.name + '":name'; + mcdDot += '[label="' + ErDiags.CARDINAL[e.card] + '"];\n'; + }); }); mcdDot += '}'; //console.log(mcdDot); @@ -265,7 +401,8 @@ class ErDiags }); } - // "Modèle logique des données" + // "Modèle logique des données", from MCD without anomalies + // TODO: this one should draw links from foreign keys to keys (port=... in
) drawMld(id) { let element = document.getElementById(id); @@ -274,25 +411,40 @@ class ErDiags 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.qualifiers && f.qualifiers.indexOf("foreign")>=0 ? '#' : '') + f.name + (f.isKey ? '' : ''); + mldDot += '\n'; + if (!!f.ref) + { + // Need to find a key attribute in reference entity (the first...) + let keyInRef = ""; + for (let field of this.tables[f.ref]) + { + if (field.isKey) + { + keyInRef = field.name; + break; + } + } + if (Math.random() < 0.5) + links += '"' + f.ref + '":"' + keyInRef + '" -- "' + name+'":"'+f.name + '" [dir="forward",arrowhead="dot"'; + else + links += '"'+name+'":"'+f.name+'" -- "' + f.ref + '":"' + keyInRef + '" [dir="back",arrowtail="dot"'; + links += ']\n;'; + } }); + mldDot += '
' + name + '
' + label + '
>];\n'; }); - // this.graphMld = ... + mldDot += links + '\n'; + mldDot += '}\n'; //console.log(mldDot); ErDiags.AjaxGet(mldDot, graphSvg => { this.mldGraph = graphSvg; @@ -308,6 +460,20 @@ class ErDiags 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 = ""; + this.tables[name].forEach( f => { + sqlText += "\t" + f.name + " " + (f.type || "TEXT") + " " + (f.qualifiers || "") + ",\n"; + if (f.isKey) + key += (key.length>0 ? "," : "") + f.name; + }); + sqlText += "\tPRIMARY KEY (" + key + ")\n"; + sqlText += ");\n"; + }); + //console.log(sqlText); + this.sqlText = sqlText; + element.innerHTML = "
" + sqlText + "
"; } }