mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-28 12:46:42 +02:00
Support attribute definitions for entities in ERDs
This commit is contained in:
@@ -157,5 +157,33 @@ describe('Entity Relationship Diagram', () => {
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render entities with and without attributes', () => {
|
||||
renderGraph(
|
||||
`
|
||||
erDiagram
|
||||
BOOK { string title }
|
||||
AUTHOR }|..|{ BOOK : writes
|
||||
BOOK { float price }
|
||||
`,
|
||||
{ logLevel : 1 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
it('should render entities and attributes with big and small entity names', () => {
|
||||
renderGraph(
|
||||
`
|
||||
erDiagram
|
||||
PRIVATE_FINANCIAL_INSTITUTION {
|
||||
string name
|
||||
int turnover
|
||||
}
|
||||
PRIVATE_FINANCIAL_INSTITUTION ||..|{ EMPLOYEE : employs
|
||||
EMPLOYEE { bool officer_of_firm }
|
||||
`,
|
||||
{ logLevel : 1 }
|
||||
);
|
||||
cy.get('svg');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -25,9 +25,49 @@ Entity names are often capitalised, although there is no accepted standard on th
|
||||
|
||||
Relationships between entities are represented by lines with end markers representing cardinality. Mermaid uses the most popular crow's foot notation. The crow's foot intuitively conveys the possibility of many instances of the entity that it connects to.
|
||||
|
||||
## Status
|
||||
ER diagrams can be used for various purposes, ranging from abstract logical models devoid of any implementation details, through to physical models of relational database tables. It can be useful to include attribute definitions on ER diagrams to aid comprehension of the purpose and meaning of entities. These do not necessarily need to be exhaustive; often a small subset of attributes is enough. Mermaid allows to be defined in terms of their *type* and *name*.
|
||||
|
||||
ER diagrams are a relatively new feature in Mermaid, so there are likely to be a few bugs and constraints, and enhancements will be made in due course. Currently you can only define entities and relationships, but not attributes. Inclusion of attributes is now actively being worked on.
|
||||
```markdown
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
CUSTOMER {
|
||||
string name
|
||||
string custNumber
|
||||
string sector
|
||||
}
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
ORDER {
|
||||
int orderNumber
|
||||
string deliveryAddress
|
||||
}
|
||||
LINE-ITEM {
|
||||
string productCode
|
||||
int quantity
|
||||
float pricePerUnit
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
CUSTOMER ||--o{ ORDER : places
|
||||
CUSTOMER {
|
||||
string name
|
||||
string custNumber
|
||||
string sector
|
||||
}
|
||||
ORDER ||--|{ LINE-ITEM : contains
|
||||
ORDER {
|
||||
int orderNumber
|
||||
string deliveryAddress
|
||||
}
|
||||
LINE-ITEM {
|
||||
string productCode
|
||||
int quantity
|
||||
float pricePerUnit
|
||||
}
|
||||
```
|
||||
|
||||
When including attributes on ER diagrams, you must decide whether to include foreign keys as attributes. This probably depends on how closely you are trying to represent relational table structures. If your diagram is a *logical* model which is not meant to imply a relational implementation, then it is better to leave these out because the associative relationships already convey the way that entities are associated. For example, a JSON data structure can implement a one-to-many relationship without the need for foreign key properties, using arrays. Similarly an object-oriented programming language may use pointers or references to collections. Even for models that are intended for relational implementation, you might decide that inclusion of foreign key attributes duplicates information already portrayed by the relationships, and does not add meaning to entities. Ultimately, it's your choice.
|
||||
|
||||
## Syntax
|
||||
|
||||
@@ -82,6 +122,43 @@ Relationships may be classified as either *identifying* or *non-identifying* and
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Attributes can be defined for entities by specifying the entity name followed by a block containing multiple `type name` pairs, where a block is delimited by an opening `{` and a closing `}`. For example:
|
||||
|
||||
```markdown
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
CAR {
|
||||
string registrationNumber
|
||||
string make
|
||||
string model
|
||||
}
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
PERSON {
|
||||
string firstName
|
||||
string lastName
|
||||
int age
|
||||
}
|
||||
```
|
||||
The attributes are rendered inside the entity boxes:
|
||||
|
||||
```mermaid
|
||||
CAR ||--o{ NAMED-DRIVER : allows
|
||||
CAR {
|
||||
string registrationNumber
|
||||
string make
|
||||
string model
|
||||
}
|
||||
PERSON ||--o{ NAMED-DRIVER : is
|
||||
PERSON {
|
||||
string firstName
|
||||
string lastName
|
||||
int age
|
||||
}
|
||||
```
|
||||
|
||||
The `type` and `name` values must begin with an alphabetic character and may contain digits, hyphens or underscores. Other than that, there are no restrictions, and there is no implicit set of valid data types.
|
||||
|
||||
### Other Things
|
||||
|
||||
- If you want the relationship label to be more than one word, you must use double quotes around the phrase
|
||||
@@ -93,10 +170,10 @@ Relationships may be classified as either *identifying* or *non-identifying* and
|
||||
|
||||
For simple color customization:
|
||||
|
||||
| Name | Used as |
|
||||
| :------- | :------------------------------------------------------ |
|
||||
| `fill` | Background color of an entity |
|
||||
| `stroke` | Border color of an entity, line color of a relationship |
|
||||
| Name | Used as |
|
||||
| :------- | :------------------------------------------------------------------- |
|
||||
| `fill` | Background color of an entity or attribute |
|
||||
| `stroke` | Border color of an entity or attribute, line color of a relationship |
|
||||
|
||||
### Classes used
|
||||
|
||||
@@ -104,6 +181,8 @@ The following CSS class selectors are available for richer styling:
|
||||
|
||||
| Selector | Description |
|
||||
| :------------------------- | :---------------------------------------------------- |
|
||||
| `.er.attributeBoxEven` | The box containing attributes on even-numbered rows |
|
||||
| `.er.attributeBoxOdd` | The box containing attributes on odd-numbered rows |
|
||||
| `.er.entityBox` | The box representing an entity |
|
||||
| `.er.entityLabel` | The label for an entity |
|
||||
| `.er.relationshipLabel` | The label for a relationship |
|
||||
|
@@ -108,7 +108,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
|
||||
// Add rectangular boxes for the attribute types/names
|
||||
let heightOffset = labelBBox.height + heightPadding * 2; // Start at the bottom of the entity label
|
||||
let attribStyle = 'attributeBox1'; // We will flip the style on alternate rows to achieve a banded effect
|
||||
let attribStyle = 'attributeBoxOdd'; // We will flip the style on alternate rows to achieve a banded effect
|
||||
|
||||
attributeNodes.forEach(nodePair => {
|
||||
// Calculate the alignment y co-ordinate for the type/name of the attribute
|
||||
@@ -157,7 +157,7 @@ const drawAttributes = (groupNode, entityTextNode, attributes) => {
|
||||
heightPadding * 2;
|
||||
|
||||
// Flip the attribute style for row banding
|
||||
attribStyle = attribStyle == 'attributeBox1' ? 'attributeBox2' : 'attributeBox1';
|
||||
attribStyle = attribStyle == 'attributeBoxOdd' ? 'attributeBoxEven' : 'attributeBoxOdd';
|
||||
});
|
||||
} else {
|
||||
// Ensure the entity box is a decent size without any attributes
|
||||
|
@@ -80,6 +80,7 @@ statement
|
||||
yy.addAttributes($1, $3);
|
||||
/* console.log('handled block'); */
|
||||
}
|
||||
| entityName BLOCK_START BLOCK_STOP { yy.addEntity($1); }
|
||||
| entityName { yy.addEntity($1); }
|
||||
;
|
||||
|
||||
|
@@ -33,6 +33,85 @@ describe('when parsing ER diagram it...', function() {
|
||||
expect(entities.hasOwnProperty('CHARACTER_SET')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow an entity with a single attribute to be defined', function() {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string title';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow an entity with multiple attributes to be defined', function() {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'string title';
|
||||
const attribute2 = 'string author';
|
||||
const attribute3 = 'float price';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute1}\n${attribute2}\n${attribute3}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities[entity].attributes.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should allow attribute definitions to be split into multiple blocks', function() {
|
||||
const entity = 'BOOK';
|
||||
const attribute1 = 'string title';
|
||||
const attribute2 = 'string author';
|
||||
const attribute3 = 'float price';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n${attribute1}\n}\n${entity} {\n${attribute2}\n${attribute3}\n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities[entity].attributes.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should allow an empty attribute block', function() {
|
||||
const entity = 'BOOK';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty('BOOK')).toBe(true);
|
||||
expect(entities[entity].attributes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow an attribute block to start immediately after the entity name', function() {
|
||||
const entity = 'BOOK';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity}{}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty('BOOK')).toBe(true);
|
||||
expect(entities[entity].attributes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow an attribute block to be separated from the entity name by spaces', function() {
|
||||
const entity = 'BOOK';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(entities.hasOwnProperty('BOOK')).toBe(true);
|
||||
expect(entities[entity].attributes.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow whitespace before and after attribute definitions', function() {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string title';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity} {\n \n\n ${attribute}\n\n \n}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should allow no whitespace before and after attribute definitions', function() {
|
||||
const entity = 'BOOK';
|
||||
const attribute = 'string title';
|
||||
|
||||
erDiagram.parser.parse(`erDiagram\n${entity}{${attribute}}`);
|
||||
const entities = erDb.getEntities();
|
||||
expect(Object.keys(entities).length).toBe(1);
|
||||
expect(entities[entity].attributes.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should associate two entities correctly', function() {
|
||||
erDiagram.parser.parse('erDiagram\nCAR ||--o{ DRIVER : "insured for"');
|
||||
const entities = erDb.getEntities();
|
||||
|
@@ -5,12 +5,12 @@ const getStyles = options =>
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
||||
.attributeBox1 {
|
||||
.attributeBoxOdd {
|
||||
fill: #ffffff;
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
||||
.attributeBox2 {
|
||||
.attributeBoxEven {
|
||||
fill: #f2f2f2;
|
||||
stroke: ${options.nodeBorder};
|
||||
}
|
||||
|
Reference in New Issue
Block a user