diff --git a/src/diagrams/er/erDb.js b/src/diagrams/er/erDb.js new file mode 100644 index 000000000..1ab913287 --- /dev/null +++ b/src/diagrams/er/erDb.js @@ -0,0 +1,77 @@ +/** + * + */ +import { logger } from '../../logger'; + +let entities = {}; +let relationships = []; +let title = ''; + +const Cardinality = { + ONLY_ONE_TO_ONE_OR_MORE : 'ONLY_ONE_TO_ONE_OR_MORE', + ONLY_ONE_TO_ZERO_OR_MORE : 'ONLY_ONE_TO_ZERO_OR_MORE', + ZERO_OR_ONE_TO_ZERO_OR_MORE : 'ZERO_OR_ONE_TO_ZERO_OR_MORE', + ZERO_OR_ONE_TO_ONE_OR_MORE : 'ZERO_OR_ONE_TO_ONE_OR_MORE', + ONE_OR_MORE_TO_ONLY_ONE : 'ONE_OR_MORE_TO_ONLY_ONE', + ZERO_OR_MORE_TO_ONLY_ONE : 'ZERO_OR_MORE_TO_ONLY_ONE', + ZERO_OR_MORE_TO_ZERO_OR_ONE : 'ZERO_OR_MORE_TO_ZERO_OR_ONE', + ONE_OR_MORE_TO_ZERO_OR_ONE : 'ONE_OR_MORE_TO_ZERO_OR_ONE', + ZERO_OR_ONE_TO_ONLY_ONE : 'ZERO_OR_ONE_TO_ONLY_ONE', + ONLY_ONE_TO_ONLY_ONE : 'ONLY_ONE_TO_ONLY_ONE', + ONLY_ONE_TO_ZERO_OR_ONE : 'ONLY_ONE_TO_ZERO_OR_ONE', + ZERO_OR_ONE_TO_ZERO_OR_ONE : 'ZERO_OR_ONE_TO_ZERO_OR_ONE', + ZERO_OR_MORE_TO_ZERO_OR_MORE: 'ZERO_OR_MORE_TO_ZERO_OR_MORE', + ZERO_OR_MORE_TO_ONE_OR_MORE : 'ZERO_OR_MORE_TO_ONE_OR_MORE', + ONE_OR_MORE_TO_ZERO_OR_MORE : 'ONE_OR_MORE_TO_ZERO_OR_MORE', + ONE_OR_MORE_TO_ONE_OR_MORE : 'ONE_OR_MORE_TO_ONE_OR_MORE' +}; + +const addEntity = function(name) { + if (typeof entities[name] === 'undefined') { + entities[name] = name; + logger.debug('Added new entity :', name); + } +}; + +const getEntities = () => entities; + +const addRelationship = function(entA, rolA, entB, rolB, card) { + let rel = { + entityA : entA, + roleA : rolA, + entityB : entB, + roleB : rolB, + cardinality : card + }; + + relationships.push(rel); + logger.debug('Added new relationship :', rel); +} + +const getRelationships = () => relationships; + +// Keep this - TODO: revisit...allow the diagram to have a title +const setTitle = function(txt) { + title = txt; +}; + +const getTitle = function() { + return title; +}; + +const clear = function() { + entities = {}; + relationships = []; + title = ''; +}; + +export default { + Cardinality, + addEntity, + getEntities, + addRelationship, + getRelationships, + clear, + setTitle, + getTitle +}; diff --git a/src/diagrams/er/parser/erDiagram.jison b/src/diagrams/er/parser/erDiagram.jison new file mode 100644 index 000000000..c254c9099 --- /dev/null +++ b/src/diagrams/er/parser/erDiagram.jison @@ -0,0 +1,84 @@ +%lex + +%x string +%options case-insensitive + +%% +\s+ /* skip whitespace */ +[\s]+ return 'SPACE'; +["] { this.begin("string");} +["] { this.popState(); } +[^"]* { return 'STR'; } +"erDiagram" return 'ER_DIAGRAM'; +[A-Za-z][A-Za-z0-9]* return 'ALPHANUM'; +\>\?\-\?\< return 'ZERO_OR_MORE_TO_ZERO_OR_MORE'; +\>\?\-\!\< return 'ZERO_OR_MORE_TO_ONE_OR_MORE'; +\>\!\-\!\< return 'ONE_OR_MORE_TO_ONE_OR_MORE'; +\>\!\-\?\< return 'ONE_OR_MORE_TO_ZERO_OR_MORE'; +\!\-\!\< return 'ONLY_ONE_TO_ONE_OR_MORE'; +\!\-\?\< return 'ONLY_ONE_TO_ZERO_OR_MORE'; +\?\-\?\< return 'ZERO_OR_ONE_TO_ZERO_OR_MORE'; +\?\-\!\< return 'ZERO_OR_ONE_TO_ONE_OR_MORE'; +\>\!\-\! return 'ONE_OR_MORE_TO_ONLY_ONE'; +\>\?\-\! return 'ZERO_OR_MORE_TO_ONLY_ONE'; +\>\?\-\? return 'ZERO_OR_MORE_TO_ZERO_OR_ONE'; +\>\!\-\? return 'ONE_OR_MORE_TO_ZERO_OR_ONE'; +\?\-\! return 'ZERO_OR_ONE_TO_ONLY_ONE'; +\!\-\! return 'ONLY_ONE_TO_ONLY_ONE'; +\!\-\? return 'ONLY_ONE_TO_ZERO_OR_ONE'; +\?\-\? return 'ZERO_OR_ONE_TO_ZERO_OR_ONE'; +. return yytext[0]; +<> return 'EOF'; + + +/lex + +%start start +%% /* language grammar */ + +start + : 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ } + ; + +document + : /* empty */ + | document statement + ; + +statement + : entityName relationship entityName ':' role ',' role + { + yy.addEntity($1); + yy.addEntity($3); + yy.addRelationship($1, $5, $3, $7, $2); + /*console.log($1 + $2 + $3 + ':' + $5 + ',' + $7);*/ + }; + +entityName + : 'ALPHANUM' { $$ = $1; } + ; + +relationship + : 'ONLY_ONE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ONLY_ONE_TO_ONE_OR_MORE; } + | 'ONLY_ONE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE; } + | 'ZERO_OR_ONE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE; } + | 'ZERO_OR_ONE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE; } + | 'ONE_OR_MORE_TO_ONLY_ONE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ONLY_ONE; } + | 'ZERO_OR_MORE_TO_ONLY_ONE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE; } + | 'ZERO_OR_MORE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE; } + | 'ONE_OR_MORE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE; } + | 'ZERO_OR_ONE_TO_ONLY_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE; } + | 'ONLY_ONE_TO_ONLY_ONE' { $$ = yy.Cardinality.ONLY_ONE_TO_ONLY_ONE; } + | 'ONLY_ONE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE; } + | 'ZERO_OR_ONE_TO_ZERO_OR_ONE' { $$ = yy.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE; } + | 'ZERO_OR_MORE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE; } + | 'ZERO_OR_MORE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE; } + | 'ONE_OR_MORE_TO_ONE_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE; } + | 'ONE_OR_MORE_TO_ZERO_OR_MORE' { $$ = yy.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE; } + ; + +role + : 'STR' { $$ = $1; } + | 'ALPHANUM' { $$ = $1; } + ; +%% diff --git a/src/diagrams/er/parser/erDiagram.spec.js b/src/diagrams/er/parser/erDiagram.spec.js new file mode 100644 index 000000000..e04c7130d --- /dev/null +++ b/src/diagrams/er/parser/erDiagram.spec.js @@ -0,0 +1,226 @@ +import erDb from '../erDb'; +import erDiagram from './erDiagram'; +import { setConfig } from '../../../config'; +import logger from '../../../logger'; + +setConfig({ + securityLevel: 'strict' +}); + +describe('when parsing ER diagram it...', function() { + + beforeEach(function() { + erDiagram.parser.yy = erDb; + erDiagram.parser.yy.clear(); + }); + + it('should associate two entities correctly', function() { + erDiagram.parser.parse('erDiagram\nCAR !-?< DRIVER : "insured for", "can drive"'); + const entities = erDb.getEntities(); + const relationships = erDb.getRelationships(); + const carEntity = entities.CAR; + const driverEntity = entities.DRIVER; + + expect(carEntity).toBe('CAR'); + expect(driverEntity).toBe('DRIVER'); + expect(relationships.length).toBe(1); + expect(relationships[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE); + }); + + it('should not create duplicate entities', function() { + const line1 = 'CAR !-?< DRIVER : "insured for", "can drive"'; + const line2 = 'DRIVER !-! LICENSE : has, "belongs to"'; + erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`); + const entities = erDb.getEntities(); + + expect(Object.keys(entities).length).toBe(3); + }); + + it('should create the roles specified', function() { + const teacherRole = 'is teacher of'; + const studentRole = 'is student of'; + const line1 = `TEACHER >?-?< STUDENT : "${teacherRole}", "${studentRole}"`; + erDiagram.parser.parse(`erDiagram\n${line1}`); + const rels = erDb.getRelationships(); + + expect(rels[0].roleA).toBe(`${teacherRole}`); + expect(rels[0].roleB).toBe(`${studentRole}`); + }); + + it('should allow recursive relationships', function() { + erDiagram.parser.parse('erDiagram\nNODE !-?< NODE : "leads to", "comes from"'); + expect(Object.keys(erDb.getEntities()).length).toBe(1); + }); + + it('should allow more than one relationship between the same two entities', function() { + const line1 = 'CAR !-?< PERSON : "insured for", "may drive"'; + const line2 = 'CAR >?-! PERSON : "owned by", "owns"'; + erDiagram.parser.parse(`erDiagram\n${line1}\n${line2}`); + const entities = erDb.getEntities(); + const rels = erDb.getRelationships(); + + expect(Object.keys(entities).length).toBe(2); + expect(rels.length).toBe(2); + }); + + it('should limit the number of relationships between the same two entities', function() { + /* TODO */ + }); + + it ('should not allow relationships between the same two entities unless the roles are different', function() { + /* TODO */ + }); + + it('should handle only-one-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ONE_OR_MORE); + }); + + it('should handle only-one-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_MORE); + + }); + + it('should handle zero-or-one-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_MORE); + }); + + it('should handle zero-or-one-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ONE_OR_MORE); + }); + + it('should handle one-or-more-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ONLY_ONE); + }); + + it('should handle zero-or-more-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ONLY_ONE); + }); + + it('should handle zero-or-more-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_ONE); + }); + + it('should handle one-or-more-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_ONE); + }); + + it('should handle zero-or-one-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ONLY_ONE); + }); + + it('should handle only-one-to-only-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-! B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ONLY_ONE); + }); + + it('should handle only-one-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA !-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONLY_ONE_TO_ZERO_OR_ONE); + }); + + it('should handle zero-or-one-to-zero-or-one relationships', function() { + erDiagram.parser.parse('erDiagram\nA ?-? B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_ONE_TO_ZERO_OR_ONE); + }); + + it('should handle zero-or-more-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ZERO_OR_MORE); + }); + + it('should handle one-or-more-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ONE_OR_MORE); + }); + + it('should handle zero-or-more-to-one-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >?-!< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ZERO_OR_MORE_TO_ONE_OR_MORE); + }); + + it('should handle one-or-more-to-zero-or-more relationships', function() { + erDiagram.parser.parse('erDiagram\nA >!-?< B : has, has'); + const rels = erDb.getRelationships(); + + expect(Object.keys(erDb.getEntities()).length).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].cardinality).toBe(erDb.Cardinality.ONE_OR_MORE_TO_ZERO_OR_MORE); + }); + + it('should not accept a syntax error', function() { + const doc = 'erDiagram\nA xxx B : has, has'; + expect(() => { + erDiagram.parser.parse(doc); + }).toThrowError(); + }); + +});