@peter.prib/renderengine 0.2.7 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +66 -9
  2. package/REFERENCE.md +129 -0
  3. package/dist/renderengine-workspace.umd.js +5068 -2892
  4. package/dist/renderengine.umd.js +4874 -2888
  5. package/examples/data-model.html +40 -18
  6. package/package.json +1 -1
  7. package/src/charting/FormDialog.js +398 -385
  8. package/src/charting/RowsField.js +127 -0
  9. package/src/charting/datamodel/ClassificationLens.js +33 -0
  10. package/src/charting/datamodel/ClassificationScheme.js +98 -0
  11. package/src/charting/datamodel/DataModel.js +220 -4
  12. package/src/charting/datamodel/DataModelChecker.js +244 -2
  13. package/src/charting/datamodel/DataModelContext.js +40 -10
  14. package/src/charting/datamodel/DataModelEditCommand.js +47 -0
  15. package/src/charting/datamodel/DataModelPerspective.js +12 -2
  16. package/src/charting/datamodel/EnumerationType.js +64 -51
  17. package/src/charting/datamodel/LevelSchema.js +23 -0
  18. package/src/charting/datamodel/ServiceDomain.js +9 -0
  19. package/src/charting/datamodel/SqlDdl.js +85 -0
  20. package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
  21. package/src/charting/datamodel/index.js +21 -15
  22. package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
  23. package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
  24. package/src/charting/datamodel/naming/NameStyle.js +57 -0
  25. package/src/charting/datamodel/naming/NamingStandard.js +132 -0
  26. package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
  27. package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
  28. package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
  29. package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
  30. package/src/charting/datamodel/naming/index.js +8 -0
  31. package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
  32. package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
  33. package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
  34. package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
  35. package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
  36. package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
  37. package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
  38. package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
  39. package/src/charting/datamodel/scorecard/index.js +8 -0
  40. package/src/charting/diagrams/DataModelDiagram.js +4 -11
  41. package/src/charting/diagrams/DataModelRelationshipStrategy.js +7 -1
  42. package/src/charting/diagrams/NodeGeometry.js +6 -2
  43. package/src/charting/toolbar/ToolbarSelect.js +6 -0
  44. package/src/datamodel.js +402 -4
  45. package/src/index.js +2666 -2666
  46. package/src/workspace/parts/DataModelPart.js +197 -6
@@ -0,0 +1,113 @@
1
+ import { ScorecardCategory } from './ScorecardCategory.js';
2
+ import { CompletenessCategory } from './CompletenessCategory.js';
3
+ import { StructureCategory } from './StructureCategory.js';
4
+ import { StandardsCategory } from './StandardsCategory.js';
5
+ import { DefinitionsCategory } from './DefinitionsCategory.js';
6
+ import { ConsistencyCategory } from './ConsistencyCategory.js';
7
+ import { ReadabilityCategory } from './ReadabilityCategory.js';
8
+
9
+ /**
10
+ * The ten categories in the Scorecard's order: those the model measures, and
11
+ * those a reviewer scores (Correctness, Scheme, Abstraction, Data).
12
+ */
13
+ const CATEGORIES = Object.freeze([
14
+ new ScorecardCategory('correctness', 'Correctness', 'Does the model capture the requirements?'),
15
+ new CompletenessCategory(),
16
+ new ScorecardCategory('scheme', 'Scheme', 'Is the scheme (relational, dimensional...) the right one for what it is for?'),
17
+ new StructureCategory(),
18
+ new ScorecardCategory('abstraction', 'Abstraction', 'Is it as general as it should be, and no more?'),
19
+ new StandardsCategory(),
20
+ new ReadabilityCategory(),
21
+ new DefinitionsCategory(),
22
+ new ConsistencyCategory(),
23
+ new ScorecardCategory('data', 'Data', 'Does the data in the database match what the model says of it?'),
24
+ ]);
25
+
26
+ /**
27
+ * ModelScorecard — a data model scored as DMBOK2 has a model reviewed, on the
28
+ * Data Model Scorecard's ten categories (Steve Hoberman, 2015). Six the model
29
+ * measures (ScorecardCategory subclasses); four a reviewer scores; a reviewer's
30
+ * score, kept in the model (`model.scorecard.reviews[key]: { score, note }`),
31
+ * overrides a measured one too. The total is the weighted average of the
32
+ * categories with a score, out of 100. Weights are the model's own
33
+ * (`model.scorecard.weights`); by default equal, 10 each: the book gives its
34
+ * own weighting, and an organisation sets the one it reviews by.
35
+ */
36
+ export class ModelScorecard {
37
+ static CATEGORIES = CATEGORIES;
38
+ static DEFAULT_WEIGHT = 10;
39
+
40
+ /** @param {DataModel} model */
41
+ constructor(model) {
42
+ this.model = model;
43
+ }
44
+
45
+ /** What the model keeps of the scorecard: `{ weights, reviews }`. */
46
+ get kept() {
47
+ return this.model.scorecard ?? {};
48
+ }
49
+
50
+ /** A category's weight: the model's, else the default. */
51
+ weight(key) {
52
+ const w = Number(this.kept.weights?.[key]);
53
+ return Number.isFinite(w) && w >= 0 ? w : ModelScorecard.DEFAULT_WEIGHT;
54
+ }
55
+
56
+ /**
57
+ * Each category: `{ category, weight, measured: {score, notes}, review: {score, note}|null,
58
+ * score (the review's, else the measured, or null), source: 'reviewed'|'measured'|null }`.
59
+ */
60
+ results() {
61
+ const findings = this.model.checker().run();
62
+ return CATEGORIES.map(category => {
63
+ const measured = category.measure(this.model, findings);
64
+ const kept = this.kept.reviews?.[category.key];
65
+ const review = Number.isFinite(Number(kept?.score)) && kept?.score !== '' && kept?.score != null ? { score: Number(kept.score), note: kept.note ?? '' } : null;
66
+ const score = review ? review.score : measured.score;
67
+ return { category, weight: this.weight(category.key), measured, review, score, source: review ? 'reviewed' : (measured.score == null ? null : 'measured') };
68
+ });
69
+ }
70
+
71
+ /** The weighted average of the categories with a score (null: none has), and how much of the weight that is. */
72
+ total(results = this.results()) {
73
+ const scored = results.filter(r => r.score != null && r.weight > 0);
74
+ const weight = scored.reduce((s, r) => s + r.weight, 0);
75
+ const all = results.reduce((s, r) => s + r.weight, 0);
76
+ return {
77
+ score: weight ? Math.round(scored.reduce((s, r) => s + r.score * r.weight, 0) / weight) : null,
78
+ coverage: all ? Math.round((100 * weight) / all) : 0,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Its dialog's fields: a heading per category (its question as the hint),
84
+ * what the model measured (shown), the reviewer's score and note, its weight.
85
+ * The score and weight are text, not number, fields: a number field reads
86
+ * empty as 0, and an empty score means "not scored", not nought.
87
+ */
88
+ fields(results = this.results()) {
89
+ return results.flatMap(({ category, measured, review, weight }) => [
90
+ { type: 'heading', label: category.label },
91
+ {
92
+ key: `${category.key}.measured`, label: 'Measured', type: 'text', readOnly: true,
93
+ value: measured.score == null ? '—' : `${measured.score}`, hint: [category.question, ...measured.notes].join(' · '),
94
+ },
95
+ { key: `${category.key}.score`, label: 'Reviewer\'s score', type: 'text', value: review?.score ?? '', placeholder: category.measured ? 'the measured one' : '0–100' },
96
+ { key: `${category.key}.note`, label: 'Note', type: 'text', value: review?.note ?? '' },
97
+ { key: `${category.key}.weight`, label: 'Weight', type: 'text', value: weight },
98
+ ]);
99
+ }
100
+
101
+ /** What the model keeps from those fields: weights other than the default, and the reviews given. */
102
+ static specFrom(values) {
103
+ const weights = {}, reviews = {};
104
+ for (const { key } of CATEGORIES) {
105
+ const weight = values[`${key}.weight`];
106
+ if (String(weight ?? '').trim() !== '' && Number(weight) !== ModelScorecard.DEFAULT_WEIGHT && Number.isFinite(Number(weight))) weights[key] = Number(weight);
107
+ const score = values[`${key}.score`];
108
+ const note = String(values[`${key}.note`] ?? '').trim();
109
+ if (String(score ?? '').trim() !== '' && Number.isFinite(Number(score))) reviews[key] = { score: Math.max(0, Math.min(100, Number(score))), ...(note && { note }) };
110
+ }
111
+ return { ...(Object.keys(weights).length && { weights }), ...(Object.keys(reviews).length && { reviews }) };
112
+ }
113
+ }
@@ -0,0 +1,37 @@
1
+ import { MeasuredCategory, percent } from './ScorecardCategory.js';
2
+ import { DataModelDiagram } from '../../diagrams/DataModelDiagram.js';
3
+ import { DataModelNotation } from '../../../datamodel.js';
4
+
5
+ /**
6
+ * Readability: can it be read as drawn? Measured as the average of the share
7
+ * of placed boxes, at each level, clear of every other box, and the share of
8
+ * associations with a name (a generalization needs none).
9
+ */
10
+ export class ReadabilityCategory extends MeasuredCategory {
11
+ constructor() {
12
+ super('readability', 'Readability', 'Can it be read as drawn: boxes clear of each other, relationships named?');
13
+ }
14
+
15
+ measure(model) {
16
+ let placed = 0, clear = 0, associations = 0, named = 0;
17
+ for (const level of model.constructor.LEVELS) {
18
+ const notation = DataModelNotation.notation('crowsfoot');
19
+ const boxes = model.level(level).entities
20
+ .map(e => DataModelDiagram.boxOf(DataModelDiagram.buildNode({ ...e, [level]: { attributes: e.attributes ?? [] } }, { level, notation })))
21
+ .filter(Boolean);
22
+ placed += boxes.length;
23
+ clear += boxes.filter((a, i) => !boxes.some((b, j) => j !== i && a.x0 < b.x1 && b.x0 < a.x1 && a.y0 < b.y1 && b.y0 < a.y1)).length;
24
+ for (const r of model.level(level).relationships) {
25
+ if (!DataModelNotation.relationshipKind(r).hasCardinality) continue;
26
+ associations++;
27
+ if (String(r.name ?? '').trim()) named++;
28
+ }
29
+ }
30
+ const shares = [['boxes clear of the others', clear, placed], ['relationships named', named, associations]].filter(([, , whole]) => whole);
31
+ if (!shares.length) return { score: null, notes: ['nothing drawn yet'] };
32
+ return {
33
+ score: Math.round(shares.reduce((sum, [, part, whole]) => sum + percent(part, whole), 0) / shares.length),
34
+ notes: shares.map(([what, part, whole]) => `${part} of ${whole} ${what}`),
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,46 @@
1
+ /** A ratio as a whole score out of 100 (none: null). */
2
+ export const percent = (part, whole) => (whole ? Math.round((100 * part) / whole) : null);
3
+
4
+ /**
5
+ * ScorecardCategory — one of the Data Model Scorecard's ten categories (Steve
6
+ * Hoberman, *Data Model Scorecard*, 2015): its `key`, `label` and the question
7
+ * it asks. This base is one a reviewer scores (`measure` says nothing); a
8
+ * subclass measures it from the model, a score out of 100 and notes on why,
9
+ * or null when the model cannot say. A reviewer's score overrides either.
10
+ */
11
+ export class ScorecardCategory {
12
+ /**
13
+ * @param {string} key - 'correctness'
14
+ * @param {string} label - 'Correctness'
15
+ * @param {string} question - What it asks of the model
16
+ */
17
+ constructor(key, label, question) {
18
+ Object.assign(this, { key, label, question });
19
+ }
20
+
21
+ /** Whether the model says how it scores (else a reviewer must). */
22
+ get measured() {
23
+ return false;
24
+ }
25
+
26
+ /**
27
+ * Its score from the model: `{ score: 0..100 | null, notes: string[] }`.
28
+ * @param {DataModel} model
29
+ * @param {Array<Object>} findings - The model check's findings (DataModelChecker#run)
30
+ */
31
+ measure(model, findings) {
32
+ return { score: null, notes: ['for a reviewer to score'] };
33
+ }
34
+ }
35
+
36
+ /** A category measured from the model: a subclass says how. */
37
+ export class MeasuredCategory extends ScorecardCategory {
38
+ get measured() {
39
+ return true;
40
+ }
41
+
42
+ /** @abstract */
43
+ measure(model, findings) {
44
+ throw new Error(`${this.constructor.name} must implement measure()`);
45
+ }
46
+ }
@@ -0,0 +1,29 @@
1
+ import { MeasuredCategory, percent } from './ScorecardCategory.js';
2
+
3
+ /**
4
+ * Standards: are names as the model's naming standard says? Measured as the
5
+ * share of entity and attribute names that keep it; without a standard the
6
+ * model cannot say (a reviewer may).
7
+ */
8
+ export class StandardsCategory extends MeasuredCategory {
9
+ constructor() {
10
+ super('standards', 'Standards', 'Are names written as the naming standard says?');
11
+ }
12
+
13
+ measure(model) {
14
+ const standard = model.namingStandard;
15
+ if (!standard) return { score: null, notes: ['no naming standard: set one to measure this'] };
16
+ let names = 0, kept = 0;
17
+ for (const level of model.constructor.LEVELS) {
18
+ for (const e of model.level(level).entities) {
19
+ names++;
20
+ if (!standard.check(level, 'entities', e.name).length) kept++;
21
+ for (const a of e.attributes ?? []) {
22
+ names++;
23
+ if (!standard.check(level, 'attributes', a.name).length) kept++;
24
+ }
25
+ }
26
+ }
27
+ return names ? { score: percent(kept, names), notes: [`${kept} of ${names} names keep the naming standard`] } : { score: null, notes: ['no names'] };
28
+ }
29
+ }
@@ -0,0 +1,32 @@
1
+ import { MeasuredCategory, percent } from './ScorecardCategory.js';
2
+
3
+ /** The model check's findings that are about structure. */
4
+ const STRUCTURAL = new Set([
5
+ 'no-primary-key', 'reference-missing', 'foreign-key-without-relationship', 'foreign-key-unrelated',
6
+ 'many-to-many', 'repeating-group', 'generalization-cycle',
7
+ ]);
8
+
9
+ /**
10
+ * Structure: is the model sound? Measured as the share of logical entities and
11
+ * tables the model check finds nothing structural on (keys, foreign keys,
12
+ * many-to-many, repeating groups, generalizations going round).
13
+ */
14
+ export class StructureCategory extends MeasuredCategory {
15
+ static STRUCTURAL = STRUCTURAL;
16
+
17
+ constructor() {
18
+ super('structure', 'Structure', 'Is it sound: keys, foreign keys, no repeating groups or unresolved many-to-many?');
19
+ }
20
+
21
+ measure(model, findings) {
22
+ const entities = ['logical', 'physical'].flatMap(level => model.level(level).entities.map(e => `${level}:${e.id}`));
23
+ if (!entities.length) return { score: null, notes: ['no logical entities or tables'] };
24
+ const structural = findings.filter(f => STRUCTURAL.has(f.code));
25
+ const touched = new Set(structural.flatMap(f => (f.ids ?? []).map(id => `${f.level}:${id}`)).filter(k => entities.includes(k)));
26
+ const counts = structural.reduce((m, f) => ((m[f.code] = (m[f.code] ?? 0) + 1), m), {});
27
+ return {
28
+ score: percent(entities.length - touched.size, entities.length),
29
+ notes: [`${entities.length - touched.size} of ${entities.length} logical entities and tables sound`, ...Object.entries(counts).map(([code, n]) => `${n} ${code}`)],
30
+ };
31
+ }
32
+ }
@@ -0,0 +1,8 @@
1
+ export { ScorecardCategory, MeasuredCategory } from './ScorecardCategory.js';
2
+ export { CompletenessCategory } from './CompletenessCategory.js';
3
+ export { StructureCategory } from './StructureCategory.js';
4
+ export { StandardsCategory } from './StandardsCategory.js';
5
+ export { DefinitionsCategory } from './DefinitionsCategory.js';
6
+ export { ConsistencyCategory } from './ConsistencyCategory.js';
7
+ export { ReadabilityCategory } from './ReadabilityCategory.js';
8
+ export { ModelScorecard } from './ModelScorecard.js';
@@ -94,8 +94,8 @@ export class DataModelDiagram extends Diagram {
94
94
  * note to what it is about, or (the `links` option) a mapping between levels.
95
95
  * @param {{id?: string, source: string, target: string, label?: string}} link
96
96
  */
97
- static buildLink({ id, source, target, label }) {
98
- return { ...(id && { id }), source, target, ...(label && { label }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
97
+ static buildLink({ id, source, target, label, color }) {
98
+ return { ...(id && { id }), source, target, ...(label && { label }), ...(color && { color }), lineStyle: 'dotted', arrowhead: 'none', arrowheadStart: 'none' };
99
99
  }
100
100
 
101
101
  static typeGroup(type) {
@@ -146,15 +146,8 @@ export class DataModelDiagram extends Diagram {
146
146
  };
147
147
  }
148
148
 
149
+ /** A relationship's edge in a notation, as its kind draws it (DataModelNotation.relationshipKind). */
149
150
  static buildEdge(relationship, notation) {
150
- return {
151
- ...relationship,
152
- label: relationship.name,
153
- lineStyle: relationship.identifying === false ? 'dashed' : 'solid',
154
- arrowheadStart: notation.cardinalityArrowhead(relationship.sourceCardinality) || 'none',
155
- arrowhead: notation.cardinalityArrowhead(relationship.targetCardinality) || 'none',
156
- sourceLabel: notation.cardinalityLabel(relationship.sourceCardinality),
157
- targetLabel: notation.cardinalityLabel(relationship.targetCardinality),
158
- };
151
+ return DataModelNotation.relationshipKind(relationship).edge(relationship, notation);
159
152
  }
160
153
  }
@@ -1,3 +1,4 @@
1
+ import { DataModelNotation } from '../../datamodel.js';
1
2
  /**
2
3
  * Strategy interface for turning data-model relationships into diagram nodes
3
4
  * and edges. Crow's Foot and UML keep relationships as direct edges; Chen
@@ -68,12 +69,17 @@ export class RelationshipAsNodeStrategy extends DataModelRelationshipStrategy {
68
69
  return best; // none clear: the least covered
69
70
  }
70
71
 
71
- apply(relationships, { notation, entity = () => undefined, occupied = [] }) {
72
+ apply(relationships, { notation, buildEdge, entity = () => undefined, occupied = [] }) {
72
73
  const nodes = [];
73
74
  const edges = [];
74
75
  const taken = [...occupied];
75
76
  const { width, height } = RelationshipAsNodeStrategy.DIAMOND;
76
77
  relationships.forEach((relationship, i) => {
78
+ // A kind drawn as a line in this notation (a generalization) is one.
79
+ if (!DataModelNotation.relationshipKind(relationship).drawnAsNode(notation)) {
80
+ edges.push(buildEdge(relationship, notation));
81
+ return;
82
+ }
77
83
  const relId = relationship.id || `${relationship.source}__${relationship.target}__rel${i}`;
78
84
  const lineStyle = relationship.identifying === false ? 'dashed' : 'solid';
79
85
  const [from, to] = [entity(relationship.source), entity(relationship.target)];
@@ -55,7 +55,11 @@ export class NodeGeometry {
55
55
  return posEntry.y - size.h / 2 + headerH + idx * rowH + rowH / 2;
56
56
  }
57
57
 
58
- /** Draws a table-card node's header + one row per column into wrapper `g`. */
58
+ /**
59
+ * Draws a table-card node's header + one row per column into wrapper `g`;
60
+ * the node's own `color` tints the card's body (a lens, a marked control
61
+ * record), the header staying as it is so its name reads.
62
+ */
59
63
  static buildTableNodeContent(renderer, g, node) {
60
64
  const { headerH, rowH } = NodeGeometry.TABLE_NODE;
61
65
  const { w, h } = NodeGeometry.tableNodeSize(node);
@@ -65,7 +69,7 @@ export class NodeGeometry {
65
69
  const borderWidth = node.selected ? 2 : 1;
66
70
 
67
71
  const TYPE_COLORS = { NUMBER: '#74b9ff', TEXT: '#a29bfe', TIMESTAMP: '#55efc4', BOOLEAN: '#ffeaa7', JSON: '#fd79a8', UNKNOWN: '#dfe6e9' };
68
- g.appendChild(renderer.rect(x0, y0, w, h, '#ffffff', { color: borderColor, width: borderWidth }));
72
+ g.appendChild(renderer.rect(x0, y0, w, h, node.color || '#ffffff', { color: borderColor, width: borderWidth }));
69
73
  g.appendChild(renderer.rect(x0, y0, w, headerH, '#1976d2', null));
70
74
  g.appendChild(renderer.text(LabelHelper.resolveNodeLabel(node), x0 + 8, y0 + headerH / 2, {
71
75
  fill: '#fff', textAnchor: 'start', verticalAlign: 'middle', fontSize: '11px', fontWeight: 'bold', 'pointer-events': 'none',
@@ -35,4 +35,10 @@ export class ToolbarSelect extends ToolbarItem {
35
35
  ],
36
36
  };
37
37
  }
38
+
39
+ /** Show the value as it is now (it may have been chosen other than through this select). */
40
+ refresh(node) {
41
+ const select = [...(node?.children ?? [])].find(c => c.tagName === 'SELECT');
42
+ if (select) select.value = this.value();
43
+ }
38
44
  }