1 // ER diagram description parser
4 constructor(description
)
7 this.inheritances
= [ ];
8 this.associations
= [ ];
9 this.txt2json(description
);
10 // Cache SVG graphs returned by server (in addition to server cache = good perfs)
18 // SQLite storage classes without null
19 return ["integer","real","text","blob"];
38 // Parse a textual description into a json object
41 let lines
= text
.split("\n");
42 lines
.push(""); //easier parsing: always empty line at the end
44 for (let i
=0; i
< lines
.length
; i
++)
46 lines
[i
] = lines
[i
].trim();
48 if (lines
[i
].length
== 0)
50 if (start
>= 0) //there is some group of lines to parse
52 this.parseThing(lines
, start
, i
);
56 else //not empty line: just register starting point
64 // Parse a group of lines into entity, association, ...
65 parseThing(lines
, start
, end
) //start included, end excluded
67 switch (lines
[start
].charAt(0))
70 // Entity = { name: { attributes, [weak] } }
71 let name
= lines
[start
].match(/[^\[\]"\s]+/)[0];
72 let entity
= { attributes: this.parseAttributes(lines
, start
+1, end
) };
73 if (lines
[start
].charAt(1) == '[')
75 this.entities
[name
] = entity
;
77 case 'i': //inheritance (arrows)
78 this.inheritances
= this.inheritances
.concat(this.parseInheritance(lines
, start
+1, end
));
80 case '{': //association
81 // Association = { [name], [attributes], [weak], entities: ArrayOf entity indices }
82 let relationship
= { };
83 let nameRes
= lines
[start
].match(/[^{}"\s]+/);
85 relationship
.name
= nameRes
[0];
86 if (lines
[start
].charAt(1) == '{')
87 relationship
.weak
= true;
88 this.associations
.push(Object
.assign({}, relationship
, this.parseAssociation(lines
, start
+1, end
)));
93 // attributes: ArrayOf {name, [isKey], [type], [qualifiers]}
94 parseAttributes(lines
, start
, end
)
97 for (let i
=start
; i
<end
; i
++)
101 if (line
.charAt(0) == '#')
104 line
= line
.slice(1);
106 field
.name
= line
.match(/[^()"\s]+/)[0];
107 let parenthesis
= line
.match(/\((.+)\)/);
108 if (parenthesis
!== null)
110 let sqlClues
= parenthesis
[1];
111 let qualifiers
= sqlClues
;
112 let firstWord
= sqlClues
.match(/[^\s]+/)[0];
113 if (ErDiags
.TYPES
.includes(firstWord
))
115 field
.type
= firstWord
;
116 qualifiers
= sqlClues
.substring(firstWord
.length
).trim();
118 field
.qualifiers
= qualifiers
;
120 attributes
.push(field
);
125 // GroupOf Inheritance: { parent, children: ArrayOf entity indices }
126 parseInheritance(lines
, start
, end
)
128 let inheritance
= [];
129 for (let i
=start
; i
<end
; i
++)
131 let lineParts
= lines
[i
].split(" ");
133 for (let j
=1; j
<lineParts
.length
; j
++)
134 children
.push(lineParts
[j
]);
135 inheritance
.push({ parent:lineParts
[0], children: children
});
140 // Association (parsed here): {
141 // entities: ArrayOf entity names + cardinality,
142 // [attributes: ArrayOf {name, [isKey], [type], [qualifiers]}]
144 parseAssociation(lines
, start
, end
)
151 if (lines
[i
].charAt(0) == '-')
153 assoce
.attributes
= this.parseAttributes(lines
, i
+1, end
);
158 // Read entity name + cardinality
159 let lineParts
= lines
[i
].split(" ");
160 entities
.push({ name:lineParts
[0], card:lineParts
[1] });
164 assoce
.entities
= entities
;
172 static AjaxGet(dotInput
, callback
)
174 let xhr
= new XMLHttpRequest();
175 xhr
.onreadystatechange = function() {
176 if (this.readyState
== 4 && this.status
== 200)
177 callback(this.responseText
);
179 xhr
.open("GET", "scripts/getGraphSvg.php?dot=" + encodeURIComponent(dotInput
), true);
183 // "Modèle conceptuel des données". TODO: option for graph size
184 // NOTE: randomizing helps to obtain better graphs (sometimes)
185 drawMcd(id
, mcdStyle
) //mcdStyle: bubble, or compact
187 let element
= document
.getElementById(id
);
188 mcdStyle
= mcdStyle
|| "compact";
189 if (this.mcdGraph
.length
> 0)
191 element
.innerHTML
= this.mcdGraph
;
194 // Build dot graph input
195 let mcdDot
= 'graph {\n';
196 mcdDot
+= 'rankdir="LR";\n';
198 if (mcdStyle
== "compact")
199 mcdDot
+= 'node [shape=plaintext];\n';
200 _
.shuffle(Object
.keys(this.entities
)).forEach( name
=> {
201 if (mcdStyle
== "bubble")
203 mcdDot
+= '"' + name
+ '" [shape=rectangle, label="' + name
+ '"';
204 if (this.entities
[name
].weak
)
205 mcdDot
+= ', peripheries=2';
207 if (!!this.entities
[name
].attributes
)
209 this.entities
[name
].attributes
.forEach( a
=> {
210 let label
= (a
.isKey
? '#' : '') + a
.name
;
211 let attrName
= name
+ '_' + a
.name
;
212 mcdDot
+= '"' + attrName
+ '" [shape=ellipse, label="' + label
+ '"];\n';
213 if (Math
.random() < 0.5)
214 mcdDot
+= '"' + attrName
+ '" -- "' + name
+ '";\n';
216 mcdDot
+= '"' + name
+ '" -- "' + attrName
+ '";\n';
222 mcdDot
+= '"' + name
+ '" [label=<';
223 if (this.entities
[name
].weak
)
225 mcdDot
+= '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="0" CELLSPACING="3" CELLBORDER="0">' +
226 '<tr><td><table BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
229 mcdDot
+= '<table port="name" BORDER="1" ALIGN="LEFT" CELLPADDING="5" CELLSPACING="0">\n';
230 mcdDot
+= '<tr><td BGCOLOR="#ae7d4e" BORDER="0"><font COLOR="#FFFFFF">' + name
+ '</font></td></tr>\n';
231 if (!!this.entities
[name
].attributes
)
233 this.entities
[name
].attributes
.forEach( a
=> {
234 let label
= (a
.isKey
? '<u>' : '') + a
.name
+ (a
.isKey
? '</u>' : '');
235 mcdDot
+= '<tr><td BGCOLOR="#FFFFFF" BORDER="0" ALIGN="LEFT"><font COLOR="#000000" >' + label
+ '</font></td></tr>\n';
238 mcdDot
+= '</table>';
239 if (this.entities
[name
].weak
)
240 mcdDot
+= '</td></tr></table>';
245 _
.shuffle(this.inheritances
).forEach( i
=> {
246 // TODO: node shape = triangle fill yellow. See
247 // https://merise.developpez.com/faq/?page=MCD#CIF-ou-dependance-fonctionnelle-de-A-a-Z
248 // https://merise.developpez.com/faq/?page=MLD#Comment-transformer-un-MCD-en-MLD
249 // https://www.developpez.net/forums/d1088964/general-developpement/alm/modelisation/structure-agregation-l-association-d-association/
250 _
.shuffle(i
.children
).forEach( c
=> {
251 if (Math
.random() < 0.5)
252 mcdDot
+= '"' + c
+ '":name -- "' + i
.parent
;
254 mcdDot
+= '"' + i
.parent
+ '":name -- "' + c
;
255 mcdDot
+= '":name [dir="forward", arrowhead="vee", style="dashed"];\n';
259 if (mcdStyle
== "compact")
260 mcdDot
+= 'node [shape=rectangle, style=rounded];\n';
261 let assoceCounter
= 0;
262 _
.shuffle(this.associations
).forEach( a
=> {
263 let name
= !!a
.name
&& a
.name
.length
> 0
265 : '_assoce' + assoceCounter
++;
266 if (mcdStyle
== "bubble")
268 mcdDot
+= '"' + name
+ '" [shape="diamond", style="filled", color="lightgrey", label="' + name
+ '"';
270 mcdDot
+= ', peripheries=2';
274 a
.attributes
.forEach( attr
=> {
275 let label
= (attr
.isKey
? '#' : '') + attr
.name
;
276 mcdDot
+= '"' + name
+ '_' + attr
.name
+ '" [shape=ellipse, label="' + label
+ '"];\n';
277 let attrName
= name
+ '_' + attr
.name
;
278 if (Math
.random() < 0.5)
279 mcdDot
+= '"' + attrName
+ '" -- "' + name
+ '";\n';
281 mcdDot
+= '"' + name
+ '" -- "' + attrName
+ '";\n';
287 let label
= '<' + name
+ '>';
290 a
.attributes
.forEach( attr
=> {
291 let attrLabel
= (attr
.isKey
? '#' : '') + attr
.name
;
292 label
+= '\\n' + attrLabel
;
295 mcdDot
+= '"' + name
+ '" [color="lightgrey", label="' + label
+ '"';
297 mcdDot
+= ', peripheries=2';
300 _
.shuffle(a
.entities
).forEach( e
=> {
301 if (Math
.random() < 0.5)
302 mcdDot
+= '"' + e
.name
+ '":name -- "' + name
+ '"';
304 mcdDot
+= '"' + name
+ '" -- "' + e
.name
+ '":name';
305 mcdDot
+= '[label="' + ErDiags
.CARDINAL
[e
.card
] + '"];\n';
310 ErDiags
.AjaxGet(mcdDot
, graphSvg
=> {
311 this.mcdGraph
= graphSvg
;
312 element
.innerHTML
= graphSvg
;
316 // "Modèle logique des données"
317 // TODO: this one should draw links from foreign keys to keys (port=... in <TD>)
320 let element
= document
.getElementById(id
);
321 if (this.mldGraph
.length
> 0)
323 element
.innerHTML
= this.mcdGraph
;
326 // Build dot graph input (assuming foreign keys not already present...)
327 let mldDot
= 'graph {\n';
328 // Pass 1: initialize tables
330 Object
.keys(this.entities
).forEach( name
=> {
331 tables
.push({ name: this.entities
[name
] }); //TODO: should be a (deep) copy
333 // Pass 2: parse associations, add foreign keys + new tables
334 this.associations
.forEach( a
=> {
335 a
.entities
.forEach( e
=> { // e.card e.name ...
339 case '?R': //"weak tables" foreign keys become part of the key
341 // multi-arite : sub-loop si 0,1 ou 1,1 : aspiré comme attribut de l'association (phase 1)
342 // ensuite, que du 0,n ou 1,n : si == 1, OK une table
343 // si 2 ou + : n tables + 1 pour l'assoce, avec attrs clés étrangères
344 // clé étrangère NOT NULL si 1,1
347 // this.graphMld = ...
348 //console.log(mldDot);
349 ErDiags
.AjaxGet(mldDot
, graphSvg
=> {
350 this.mldGraph
= graphSvg
;
351 element
.innerHTML
= graphSvg
;
357 let element
= document
.getElementById(id
);
358 if (this.sqlText
.length
> 0)
360 element
.innerHTML
= this.sqlText
;
363 //UNIMPLEMENTED (should be straightforward from MLD)