fix: bug #2346 "ER-attribute comments not work"

This commit is contained in:
Eirik Bjornset
2021-12-29 21:27:51 +01:00
parent 066b7a0d0b
commit d3577eb59b
5 changed files with 126 additions and 47 deletions

View File

@@ -183,11 +183,58 @@ describe('Entity Relationship Diagram', () => {
cy.get('svg'); cy.get('svg');
}); });
it('should render entities with keys', () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
float price
string author FK
string title PK
}
`,
{ logLevel: 1 }
);
cy.get('svg');
});
it('should render entities with comments', () => {
renderGraph(
`
erDiagram
AUTHOR_WITH_LONG_ENTITY_NAME {
string name "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string author
string title "author comment"
float price "price comment"
}
`,
{ logLevel: 1 }
);
cy.get('svg');
});
it('should render entities with keys and comments', () => { it('should render entities with keys and comments', () => {
renderGraph( renderGraph(
` `
erDiagram erDiagram
BOOK { string title PK "comment"} AUTHOR_WITH_LONG_ENTITY_NAME {
string name PK "comment"
}
AUTHOR_WITH_LONG_ENTITY_NAME }|..|{ BOOK : writes
BOOK {
string description
float price "price comment"
string title PK "title comment"
string author FK
}
`, `,
{ logLevel: 1 } { logLevel: 1 }
); );

View File

@@ -137,20 +137,20 @@ The `type` and `name` values must begin with an alphabetic character and may con
#### Attribute Keys and Comments #### Attribute Keys and Comments
Attributes may also have a `key` or comment defined. Keys can be "PK" or "FK", for Primary Key or Foreign Key. And a `comment` is defined by quotes at the end of an attribute. Comments themselves cannot have quote characters in them. Attributes may also have a `key` or comment defined. Keys can be "PK" or "FK", for Primary Key or Foreign Key. And a `comment` is defined by double quotes at the end of an attribute. Comments themselves cannot have double-quote characters in them.
```mermaid-example ```mermaid-example
erDiagram erDiagram
CAR ||--o{ NAMED-DRIVER : allows CAR ||--o{ NAMED-DRIVER : allows
CAR { CAR {
string allowedDriver FK 'The license of the allowed driver' string allowedDriver FK "The license of the allowed driver"
string registrationNumber string registrationNumber
string make string make
string model string model
} }
PERSON ||--o{ NAMED-DRIVER : is PERSON ||--o{ NAMED-DRIVER : is
PERSON { PERSON {
string driversLicense PK 'The license #' string driversLicense PK "The license #"
string firstName string firstName
string lastName string lastName
int age int age

View File

@@ -40,7 +40,6 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
const attributeNodes = []; // Intermediate storage for attribute nodes created so that we can do a second pass const attributeNodes = []; // Intermediate storage for attribute nodes created so that we can do a second pass
let hasKeyType = false; let hasKeyType = false;
let hasComment = false; let hasComment = false;
let maxWidth = 0;
let maxTypeWidth = 0; let maxTypeWidth = 0;
let maxNameWidth = 0; let maxNameWidth = 0;
let maxKeyWidth = 0; let maxKeyWidth = 0;
@@ -48,9 +47,19 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
let cumulativeHeight = labelBBox.height + heightPadding * 2; let cumulativeHeight = labelBBox.height + heightPadding * 2;
let attrNum = 1; let attrNum = 1;
// Check to see if any of the attributes has a key or a comment
attributes.forEach((item) => {
if (item.attributeKeyType !== undefined) {
hasKeyType = true;
}
if (item.attributeComment !== undefined) {
hasComment = true;
}
});
attributes.forEach((item) => { attributes.forEach((item) => {
const attrPrefix = `${entityTextNode.node().id}-attr-${attrNum}`; const attrPrefix = `${entityTextNode.node().id}-attr-${attrNum}`;
let nodeWidth = 0;
let nodeHeight = 0; let nodeHeight = 0;
// Add a text node for the attribute type // Add a text node for the attribute type
@@ -91,16 +100,14 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
const nameBBox = nameNode.node().getBBox(); const nameBBox = nameNode.node().getBBox();
maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width); maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width);
maxNameWidth = Math.max(maxNameWidth, nameBBox.width); maxNameWidth = Math.max(maxNameWidth, nameBBox.width);
nodeWidth += typeBBox.width;
nodeWidth += nameBBox.width;
nodeHeight = Math.max(typeBBox.height, nameBBox.height); nodeHeight = Math.max(typeBBox.height, nameBBox.height);
if (hasKeyType || item.attributeKeyType !== undefined) { if (hasKeyType) {
const keyTypeNode = groupNode const keyTypeNode = groupNode
.append('text') .append('text')
.attr('class', 'er entityLabel') .attr('class', 'er entityLabel')
.attr('id', `${attrPrefix}-name`) .attr('id', `${attrPrefix}-key`)
.attr('x', 0) .attr('x', 0)
.attr('y', 0) .attr('y', 0)
.attr('dominant-baseline', 'middle') .attr('dominant-baseline', 'middle')
@@ -113,17 +120,15 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
attributeNode.kn = keyTypeNode; attributeNode.kn = keyTypeNode;
const keyTypeBBox = keyTypeNode.node().getBBox(); const keyTypeBBox = keyTypeNode.node().getBBox();
nodeWidth += keyTypeBBox.width; maxKeyWidth = Math.max(maxKeyWidth, keyTypeBBox.width);
maxKeyWidth = Math.max(maxKeyWidth, nodeWidth);
nodeHeight = Math.max(nodeHeight, keyTypeBBox.height); nodeHeight = Math.max(nodeHeight, keyTypeBBox.height);
hasKeyType = true;
} }
if (hasComment || item.attributeComment !== undefined) { if (hasComment) {
const commentNode = groupNode const commentNode = groupNode
.append('text') .append('text')
.attr('class', 'er entityLabel') .attr('class', 'er entityLabel')
.attr('id', `${attrPrefix}-name`) .attr('id', `${attrPrefix}-comment`)
.attr('x', 0) .attr('x', 0)
.attr('y', 0) .attr('y', 0)
.attr('dominant-baseline', 'middle') .attr('dominant-baseline', 'middle')
@@ -136,25 +141,35 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
attributeNode.cn = commentNode; attributeNode.cn = commentNode;
const commentNodeBBox = commentNode.node().getBBox(); const commentNodeBBox = commentNode.node().getBBox();
nodeWidth += commentNodeBBox.width; maxCommentWidth = Math.max(maxCommentWidth, commentNodeBBox.width);
maxCommentWidth = Math.max(nodeWidth, nameBBox.width);
nodeHeight = Math.max(nodeHeight, commentNodeBBox.height); nodeHeight = Math.max(nodeHeight, commentNodeBBox.height);
hasComment = true;
} }
attributeNode.height = nodeHeight; attributeNode.height = nodeHeight;
// Keep a reference to the nodes so that we can iterate through them later // Keep a reference to the nodes so that we can iterate through them later
attributeNodes.push(attributeNode); attributeNodes.push(attributeNode);
maxWidth = Math.max(maxWidth, nodeWidth);
cumulativeHeight += nodeHeight + heightPadding * 2; cumulativeHeight += nodeHeight + heightPadding * 2;
attrNum += 1; attrNum += 1;
}); });
let widthPaddingFactor = 4;
if (hasKeyType) {
widthPaddingFactor += 2;
}
if (hasComment) {
widthPaddingFactor += 2;
}
const maxWidth = maxTypeWidth + maxNameWidth + maxKeyWidth + maxCommentWidth;
// Calculate the new bounding box of the overall entity, now that attributes have been added // Calculate the new bounding box of the overall entity, now that attributes have been added
const bBox = { const bBox = {
width: Math.max( width: Math.max(
conf.minEntityWidth, conf.minEntityWidth,
Math.max(labelBBox.width + conf.entityPadding * 2, maxWidth + widthPadding * 4) Math.max(
labelBBox.width + conf.entityPadding * 2,
maxWidth + widthPadding * widthPaddingFactor
)
), ),
height: height:
attributes.length > 0 attributes.length > 0
@@ -162,10 +177,13 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
: Math.max(conf.minEntityHeight, labelBBox.height + conf.entityPadding * 2), : Math.max(conf.minEntityHeight, labelBBox.height + conf.entityPadding * 2),
}; };
// There might be some spare width for padding out attributes if the entity name is very long
const spareWidth = Math.max(0, bBox.width - maxWidth - widthPadding * 4);
if (attributes.length > 0) { if (attributes.length > 0) {
// There might be some spare width for padding out attributes if the entity name is very long
const spareColumnWidth = Math.max(
0,
(bBox.width - maxWidth - widthPadding * widthPaddingFactor) / (widthPaddingFactor / 2)
);
// Position the entity label near the top of the entity bounding box // Position the entity label near the top of the entity bounding box
entityTextNode.attr( entityTextNode.attr(
'transform', 'transform',
@@ -180,9 +198,10 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
// Calculate the alignment y co-ordinate for the type/name of the attribute // Calculate the alignment y co-ordinate for the type/name of the attribute
const alignY = heightOffset + heightPadding + attributeNode.height / 2; const alignY = heightOffset + heightPadding + attributeNode.height / 2;
// Position the type of the attribute // Position the type attribute
attributeNode.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')'); attributeNode.tn.attr('transform', 'translate(' + widthPadding + ',' + alignY + ')');
// TODO Handle spareWidth in attr('width')
// Insert a rectangle for the type // Insert a rectangle for the type
const typeRect = groupNode const typeRect = groupNode
.insert('rect', '#' + attributeNode.tn.node().id) .insert('rect', '#' + attributeNode.tn.node().id)
@@ -192,65 +211,73 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
.attr('stroke', conf.stroke) .attr('stroke', conf.stroke)
.attr('x', 0) .attr('x', 0)
.attr('y', heightOffset) .attr('y', heightOffset)
.attr('width', maxTypeWidth * 2 + spareWidth / 2) .attr('width', maxTypeWidth + widthPadding * 2 + spareColumnWidth)
.attr('height', attributeNode.tn.node().getBBox().height + heightPadding * 2); .attr('height', attributeNode.height + heightPadding * 2);
// Position the name of the attribute const nameXOffset = parseFloat(typeRect.attr('x')) + parseFloat(typeRect.attr('width'));
// Position the name attribute
attributeNode.nn.attr( attributeNode.nn.attr(
'transform', 'transform',
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')' 'translate(' + (nameXOffset + widthPadding) + ',' + alignY + ')'
); );
// Insert a rectangle for the name // Insert a rectangle for the name
groupNode const nameRect = groupNode
.insert('rect', '#' + attributeNode.nn.node().id) .insert('rect', '#' + attributeNode.nn.node().id)
.attr('class', `er ${attribStyle}`) .attr('class', `er ${attribStyle}`)
.attr('fill', conf.fill) .attr('fill', conf.fill)
.attr('fill-opacity', '100%') .attr('fill-opacity', '100%')
.attr('stroke', conf.stroke) .attr('stroke', conf.stroke)
.attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`) .attr('x', nameXOffset)
.attr('y', heightOffset) .attr('y', heightOffset)
.attr('width', maxNameWidth + widthPadding * 2 + spareWidth / 2) .attr('width', maxNameWidth + widthPadding * 2 + spareColumnWidth)
.attr('height', attributeNode.nn.node().getBBox().height + heightPadding * 2); .attr('height', attributeNode.height + heightPadding * 2);
let keyTypeAndCommentXOffset =
parseFloat(nameRect.attr('x')) + parseFloat(nameRect.attr('width'));
if (hasKeyType) { if (hasKeyType) {
// Position the name of the attribute // Position the key type attribute
attributeNode.kn.attr( attributeNode.kn.attr(
'transform', 'transform',
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')' 'translate(' + (keyTypeAndCommentXOffset + widthPadding) + ',' + alignY + ')'
); );
// Insert a rectangle for the name // Insert a rectangle for the key type
groupNode const keyTypeRect = groupNode
.insert('rect', '#' + attributeNode.kn.node().id) .insert('rect', '#' + attributeNode.kn.node().id)
.attr('class', `er ${attribStyle}`) .attr('class', `er ${attribStyle}`)
.attr('fill', conf.fill) .attr('fill', conf.fill)
.attr('fill-opacity', '100%') .attr('fill-opacity', '100%')
.attr('stroke', conf.stroke) .attr('stroke', conf.stroke)
.attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`) .attr('x', keyTypeAndCommentXOffset)
.attr('y', heightOffset) .attr('y', heightOffset)
.attr('width', maxKeyWidth + widthPadding * 2 + spareWidth / 2) .attr('width', maxKeyWidth + widthPadding * 2 + spareColumnWidth)
.attr('height', attributeNode.kn.node().getBBox().height + heightPadding * 2); .attr('height', attributeNode.height + heightPadding * 2);
keyTypeAndCommentXOffset =
parseFloat(keyTypeRect.attr('x')) + parseFloat(keyTypeRect.attr('width'));
} }
if (hasComment) { if (hasComment) {
// Position the name of the attribute // Position the comment attribute
attributeNode.cn.attr( attributeNode.cn.attr(
'transform', 'transform',
'translate(' + (parseFloat(typeRect.attr('width')) + widthPadding) + ',' + alignY + ')' 'translate(' + (keyTypeAndCommentXOffset + widthPadding) + ',' + alignY + ')'
); );
// Insert a rectangle for the name // Insert a rectangle for the comment
groupNode groupNode
.insert('rect', '#' + attributeNode.cn.node().id) .insert('rect', '#' + attributeNode.cn.node().id)
.attr('class', `er ${attribStyle}`) .attr('class', `er ${attribStyle}`)
.attr('fill', conf.fill) .attr('fill', conf.fill)
.attr('fill-opacity', '100%') .attr('fill-opacity', '100%')
.attr('stroke', conf.stroke) .attr('stroke', conf.stroke)
.attr('x', `${typeRect.attr('x') + typeRect.attr('width')}`) .attr('x', keyTypeAndCommentXOffset)
.attr('y', heightOffset) .attr('y', heightOffset)
.attr('width', maxCommentWidth + widthPadding * 2 + spareWidth / 2) .attr('width', maxCommentWidth + widthPadding * 2 + spareColumnWidth)
.attr('height', attributeNode.cn.node().getBBox().height + heightPadding * 2); .attr('height', attributeNode.height + heightPadding * 2);
} }
// Increment the height offset to move to the next row // Increment the height offset to move to the next row

View File

@@ -98,8 +98,8 @@ attributes
attribute attribute
: attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; } : attributeType attributeName { $$ = { attributeType: $1, attributeName: $2 }; }
| attributeType attributeName attributeKeyType { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3 }; } | attributeType attributeName attributeKeyType { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3 }; }
| attributeType attributeName COMMENT { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; } | attributeType attributeName attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeComment: $3 }; }
| attributeType attributeName attributeKeyType COMMENT { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3, attributeComment: $4 }; } | attributeType attributeName attributeKeyType attributeComment { $$ = { attributeType: $1, attributeName: $2, attributeKeyType: $3, attributeComment: $4 }; }
; ;
attributeType attributeType
@@ -114,6 +114,10 @@ attributeKeyType
: ATTRIBUTE_KEY { $$=$1; } : ATTRIBUTE_KEY { $$=$1; }
; ;
attributeComment
: COMMENT { $$=$1.replace(/"/g, ''); }
;
relSpec relSpec
: cardinality relType cardinality : cardinality relType cardinality
{ {

View File

@@ -59,6 +59,7 @@ describe('when parsing ER diagram it...', function () {
const entities = erDb.getEntities(); const entities = erDb.getEntities();
expect(Object.keys(entities).length).toBe(1); expect(Object.keys(entities).length).toBe(1);
expect(entities[entity].attributes.length).toBe(1); expect(entities[entity].attributes.length).toBe(1);
expect(entities[entity].attributes[0].attributeComment).toBe('comment');
}); });
it('should allow an entity with a single attribute to be defined with a key and a comment', function () { it('should allow an entity with a single attribute to be defined with a key and a comment', function () {