Update erDb to use class to wrap data

This commit is contained in:
yari-dewalt
2025-01-27 10:31:47 -08:00
parent aeec4b7f77
commit 1a9a9f43e6
4 changed files with 218 additions and 206 deletions

View File

@@ -2,7 +2,6 @@ import { log } from '../../logger.js';
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { Edge, Node } from '../../rendering-util/types.js'; import type { Edge, Node } from '../../rendering-util/types.js';
import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js'; import type { EntityNode, Attribute, Relationship, EntityClass, RelSpec } from './erTypes.js';
import { import {
setAccTitle, setAccTitle,
getAccTitle, getAccTitle,
@@ -13,228 +12,239 @@ import {
getDiagramTitle, getDiagramTitle,
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import { getEdgeId } from '../../utils.js'; import { getEdgeId } from '../../utils.js';
import type { DiagramDB } from '../../diagram-api/types.js';
let entities = new Map<string, EntityNode>(); export class ErDB implements DiagramDB {
let relationships: Relationship[] = []; private entities = new Map<string, EntityNode>();
let classes = new Map<string, EntityClass>(); private relationships: Relationship[] = [];
let direction = 'TB'; private classes = new Map<string, EntityClass>();
private direction = 'TB';
const Cardinality = { private Cardinality = {
ZERO_OR_ONE: 'ZERO_OR_ONE', ZERO_OR_ONE: 'ZERO_OR_ONE',
ZERO_OR_MORE: 'ZERO_OR_MORE', ZERO_OR_MORE: 'ZERO_OR_MORE',
ONE_OR_MORE: 'ONE_OR_MORE', ONE_OR_MORE: 'ONE_OR_MORE',
ONLY_ONE: 'ONLY_ONE', ONLY_ONE: 'ONLY_ONE',
MD_PARENT: 'MD_PARENT', MD_PARENT: 'MD_PARENT',
};
const Identification = {
NON_IDENTIFYING: 'NON_IDENTIFYING',
IDENTIFYING: 'IDENTIFYING',
};
/**
* Add entity
* @param name - The name of the entity
* @param alias - The alias of the entity
*/
const addEntity = function (name: string, alias = ''): EntityNode {
if (!entities.has(name)) {
entities.set(name, {
id: `entity-${name}-${entities.size}`,
label: name,
attributes: [],
alias,
shape: 'erBox',
look: getConfig().look || 'default',
cssClasses: 'default',
cssStyles: [],
});
log.info('Added new entity :', name);
} else if (!entities.get(name)?.alias && alias) {
entities.get(name)!.alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
return entities.get(name)!;
};
const getEntity = function (name: string) {
return entities.get(name);
};
const getEntities = () => entities;
const getClasses = () => classes;
const addAttributes = function (entityName: string, attribs: Attribute[]) {
const entity = addEntity(entityName); // May do nothing (if entity has already been added)
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
let i;
for (i = attribs.length - 1; i >= 0; i--) {
if (!attribs[i].keys) {
attribs[i].keys = [];
}
if (!attribs[i].comment) {
attribs[i].comment = '';
}
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].name);
}
};
/**
* Add a relationship
*
* @param entA - The first entity in the relationship
* @param rolA - The role played by the first entity in relation to the second
* @param entB - The second entity in the relationship
* @param rSpec - The details of the relationship between the two entities
*/
const addRelationship = function (entA: string, rolA: string, entB: string, rSpec: RelSpec) {
const entityA = entities.get(entA);
const entityB = entities.get(entB);
if (!entityA || !entityB) {
return;
}
const rel = {
entityA: entityA.id,
roleA: rolA,
entityB: entityB.id,
relSpec: rSpec,
}; };
relationships.push(rel); private Identification = {
log.debug('Added new relationship :', rel); NON_IDENTIFYING: 'NON_IDENTIFYING',
}; IDENTIFYING: 'IDENTIFYING',
};
const getRelationships = () => relationships; constructor() {
this.clear();
this.addEntity = this.addEntity.bind(this);
this.addAttributes = this.addAttributes.bind(this);
this.addRelationship = this.addRelationship.bind(this);
this.setDirection = this.setDirection.bind(this);
this.addCssStyles = this.addCssStyles.bind(this);
this.addClass = this.addClass.bind(this);
this.setClass = this.setClass.bind(this);
this.setAccTitle = this.setAccTitle.bind(this);
this.setAccDescription = this.setAccDescription.bind(this);
}
export const getDirection = () => direction; /**
const setDirection = (dir: string) => { * Add entity
direction = dir; * @param name - The name of the entity
}; * @param alias - The alias of the entity
*/
public addEntity(name: string, alias = ''): EntityNode {
if (!this.entities.has(name)) {
this.entities.set(name, {
id: `entity-${name}-${this.entities.size}`,
label: name,
attributes: [],
alias,
shape: 'erBox',
look: getConfig().look ?? 'default',
cssClasses: 'default',
cssStyles: [],
});
log.info('Added new entity :', name);
} else if (!this.entities.get(name)?.alias && alias) {
this.entities.get(name)!.alias = alias;
log.info(`Add alias '${alias}' to entity '${name}'`);
}
const clear = function () { return this.entities.get(name)!;
entities = new Map(); }
classes = new Map();
relationships = [];
commonClear();
};
export const getData = function () { public getEntity(name: string) {
const nodes: Node[] = []; return this.entities.get(name);
const edges: Edge[] = []; }
const config = getConfig();
for (const entityKey of entities.keys()) { public getEntities() {
const entityNode = entities.get(entityKey); return this.entities;
if (entityNode) { }
entityNode.cssCompiledStyles = getCompiledStyles(entityNode.cssClasses!.split(' '));
nodes.push(entityNode as unknown as Node); public getClasses() {
return this.classes;
}
public addAttributes(entityName: string, attribs: Attribute[]) {
const entity = this.addEntity(entityName); // May do nothing (if entity has already been added)
// Process attribs in reverse order due to effect of recursive construction (last attribute is first)
let i;
for (i = attribs.length - 1; i >= 0; i--) {
if (!attribs[i].keys) {
attribs[i].keys = [];
}
if (!attribs[i].comment) {
attribs[i].comment = '';
}
entity.attributes.push(attribs[i]);
log.debug('Added attribute ', attribs[i].name);
} }
} }
let count = 0; /**
for (const relationship of relationships) { * Add a relationship
const edge: Edge = { *
id: getEdgeId(relationship.entityA, relationship.entityB, { prefix: 'id', counter: count++ }), * @param entA - The first entity in the relationship
type: 'normal', * @param rolA - The role played by the first entity in relation to the second
start: relationship.entityA, * @param entB - The second entity in the relationship
end: relationship.entityB, * @param rSpec - The details of the relationship between the two entities
label: relationship.roleA, */
labelpos: 'c', public addRelationship(entA: string, rolA: string, entB: string, rSpec: RelSpec) {
thickness: 'normal', const entityA = this.entities.get(entA);
classes: 'relationshipLine', const entityB = this.entities.get(entB);
arrowTypeStart: relationship.relSpec.cardB.toLowerCase(), if (!entityA || !entityB) {
arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(),
pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed',
look: config.look,
};
edges.push(edge);
}
return { nodes, edges, other: {}, config, direction: 'TB' };
};
export const addCssStyles = function (ids: string[], styles: string[]) {
for (const id of ids) {
const entity = entities.get(id);
if (!styles || !entity) {
return; return;
} }
for (const style of styles) {
entity.cssStyles!.push(style); const rel = {
} entityA: entityA.id,
roleA: rolA,
entityB: entityB.id,
relSpec: rSpec,
};
this.relationships.push(rel);
log.debug('Added new relationship :', rel);
} }
};
export const addClass = function (ids: string[], style: string[]) { public getRelationships() {
ids.forEach(function (id) { return this.relationships;
let classNode = classes.get(id); }
if (classNode === undefined) {
classNode = { id, styles: [], textStyles: [] }; public getDirection() {
classes.set(id, classNode); return this.direction;
}
public setDirection(dir: string) {
this.direction = dir;
}
private getCompiledStyles(classDefs: string[]) {
let compiledStyles: string[] = [];
for (const customClass of classDefs) {
const cssClass = this.classes.get(customClass);
if (cssClass?.styles) {
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
}
if (cssClass?.textStyles) {
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
}
} }
return compiledStyles;
}
if (style) { public addCssStyles(ids: string[], styles: string[]) {
style.forEach(function (s) { for (const id of ids) {
if (/color/.exec(s)) { const entity = this.entities.get(id);
const newStyle = s.replace('fill', 'bgFill'); if (!styles || !entity) {
classNode.textStyles.push(newStyle); return;
} }
classNode.styles.push(s); for (const style of styles) {
}); entity.cssStyles!.push(style);
}
});
};
export const setClass = function (ids: string[], classNames: string[]) {
for (const id of ids) {
const entity = entities.get(id);
if (entity) {
for (const className of classNames) {
entity.cssClasses += ' ' + className;
} }
} }
} }
};
function getCompiledStyles(classDefs: string[]) { public addClass(ids: string[], style: string[]) {
let compiledStyles: string[] = []; ids.forEach((id) => {
for (const customClass of classDefs) { let classNode = this.classes.get(id);
const cssClass = classes.get(customClass); if (classNode === undefined) {
if (cssClass?.styles) { classNode = { id, styles: [], textStyles: [] };
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim()); this.classes.set(id, classNode);
} }
if (cssClass?.textStyles) {
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim()); if (style) {
style.forEach(function (s) {
if (/color/.exec(s)) {
const newStyle = s.replace('fill', 'bgFill');
classNode.textStyles.push(newStyle);
}
classNode.styles.push(s);
});
}
});
}
public setClass(ids: string[], classNames: string[]) {
for (const id of ids) {
const entity = this.entities.get(id);
if (entity) {
for (const className of classNames) {
entity.cssClasses += ' ' + className;
}
}
} }
} }
return compiledStyles;
}
export default { public clear() {
Cardinality, this.entities = new Map();
Identification, this.classes = new Map();
getConfig: () => getConfig().er, this.relationships = [];
addEntity, commonClear();
addAttributes, }
getEntities,
getEntity, public getData() {
getClasses, const nodes: Node[] = [];
addRelationship, const edges: Edge[] = [];
getRelationships, const config = getConfig();
clear,
getDirection, for (const entityKey of this.entities.keys()) {
setDirection, const entityNode = this.entities.get(entityKey);
setAccTitle, if (entityNode) {
getAccTitle, entityNode.cssCompiledStyles = this.getCompiledStyles(entityNode.cssClasses!.split(' '));
setAccDescription, nodes.push(entityNode as unknown as Node);
getAccDescription, }
setDiagramTitle, }
getDiagramTitle,
getData, let count = 0;
addCssStyles, for (const relationship of this.relationships) {
addClass, const edge: Edge = {
setClass, id: getEdgeId(relationship.entityA, relationship.entityB, {
}; prefix: 'id',
counter: count++,
}),
type: 'normal',
start: relationship.entityA,
end: relationship.entityB,
label: relationship.roleA,
labelpos: 'c',
thickness: 'normal',
classes: 'relationshipLine',
arrowTypeStart: relationship.relSpec.cardB.toLowerCase(),
arrowTypeEnd: relationship.relSpec.cardA.toLowerCase(),
pattern: relationship.relSpec.relType == 'IDENTIFYING' ? 'solid' : 'dashed',
look: config.look,
};
edges.push(edge);
}
return { nodes, edges, other: {}, config, direction: 'TB' };
}
public setAccTitle = setAccTitle;
public getAccTitle = getAccTitle;
public setAccDescription = setAccDescription;
public getAccDescription = getAccDescription;
public setDiagramTitle = setDiagramTitle;
public getDiagramTitle = getDiagramTitle;
public getConfig = () => getConfig().er;
}

View File

@@ -1,12 +1,14 @@
// @ts-ignore: TODO: Fix ts errors // @ts-ignore: TODO: Fix ts errors
import erParser from './parser/erDiagram.jison'; import erParser from './parser/erDiagram.jison';
import erDb from './erDb.js'; import { ErDB } from './erDb.js';
import erRenderer from './erRenderer-unified.js'; import erRenderer from './erRenderer-unified.js';
import erStyles from './styles.js'; import erStyles from './styles.js';
export const diagram = { export const diagram = {
parser: erParser, parser: erParser,
db: erDb, get db() {
return new ErDB();
},
renderer: erRenderer, renderer: erRenderer,
styles: erStyles, styles: erStyles,
}; };

View File

@@ -4,7 +4,6 @@ import { getDiagramElement } from '../../rendering-util/insertElementsForSize.js
import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js'; import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/render.js';
import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js';
import type { LayoutData } from '../../rendering-util/types.js'; import type { LayoutData } from '../../rendering-util/types.js';
import { getDirection } from './erDb.js';
import utils from '../../utils.js'; import utils from '../../utils.js';
import { select } from 'd3'; import { select } from 'd3';
@@ -26,7 +25,7 @@ export const draw = async function (text: string, id: string, _version: string,
// Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing? // Workaround as when rendering and setting up the graph it uses flowchart spacing before data4Layout spacing?
data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140; data4Layout.config.flowchart!.nodeSpacing = conf?.nodeSpacing || 140;
data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80; data4Layout.config.flowchart!.rankSpacing = conf?.rankSpacing || 80;
data4Layout.direction = getDirection(); data4Layout.direction = diag.db.getDirection();
data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more']; data4Layout.markers = ['only_one', 'zero_or_one', 'one_or_more', 'zero_or_more'];
data4Layout.diagramId = id; data4Layout.diagramId = id;

View File

@@ -1,5 +1,5 @@
import { setConfig } from '../../../config.js'; import { setConfig } from '../../../config.js';
import erDb from '../erDb.js'; import { ErDb } from '../erDb.js';
import erDiagram from './erDiagram.jison'; // jison file import erDiagram from './erDiagram.jison'; // jison file
setConfig({ setConfig({
@@ -7,6 +7,7 @@ setConfig({
}); });
describe('when parsing ER diagram it...', function () { describe('when parsing ER diagram it...', function () {
const erDb = new ErDb();
beforeEach(function () { beforeEach(function () {
erDiagram.parser.yy = erDb; erDiagram.parser.yy = erDb;
erDiagram.parser.yy.clear(); erDiagram.parser.yy.clear();