@peter.prib/renderengine 0.14.0 → 0.15.1
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.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49641 -39747
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +206 -0
- package/examples/decision-workbench.html +26 -0
- package/package.json +7 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/index.js +3 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
- package/src/workspace/parts/ViewsPart.js +196 -0
- package/src/workspace/styles.js +36 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArchimateDataLink — connects data model concepts to ArchiMate model elements.
|
|
3
|
+
* Aligns with DMBOK2's Data Architecture and TOGAF/ArchiMate standards:
|
|
4
|
+
* - Conceptual concepts realise ArchiMate Business Objects (Business Layer)
|
|
5
|
+
* - Logical and physical entities realise ArchiMate Data Objects (Application Layer) or Artifacts (Technology Layer)
|
|
6
|
+
* - Service domains link to ArchiMate Business Services or Application Services
|
|
7
|
+
*/
|
|
8
|
+
export class ArchimateDataLink {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} spec
|
|
11
|
+
* @param {string} spec.dataModelEntityId - Entity id in DataModel
|
|
12
|
+
* @param {'conceptual'|'logical'|'physical'} [spec.level='conceptual']
|
|
13
|
+
* @param {string} spec.archimateElementId - ArchiMate element id
|
|
14
|
+
* @param {string} [spec.archimateType='business-object'] - 'business-object', 'data-object', 'artifact', etc.
|
|
15
|
+
* @param {string} [spec.archimateModelId]
|
|
16
|
+
* @param {string} [spec.notes]
|
|
17
|
+
*/
|
|
18
|
+
constructor({
|
|
19
|
+
dataModelEntityId,
|
|
20
|
+
level = 'conceptual',
|
|
21
|
+
archimateElementId,
|
|
22
|
+
archimateType = 'business-object',
|
|
23
|
+
archimateModelId = null,
|
|
24
|
+
notes = '',
|
|
25
|
+
} = {}) {
|
|
26
|
+
if (!dataModelEntityId || !archimateElementId) {
|
|
27
|
+
throw new TypeError('ArchimateDataLink requires dataModelEntityId and archimateElementId');
|
|
28
|
+
}
|
|
29
|
+
this.dataModelEntityId = dataModelEntityId;
|
|
30
|
+
this.level = level;
|
|
31
|
+
this.archimateElementId = archimateElementId;
|
|
32
|
+
this.archimateType = archimateType;
|
|
33
|
+
this.archimateModelId = archimateModelId;
|
|
34
|
+
this.notes = notes;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validates whether the ArchiMate type aligns with DMBOK2 layer expectations.
|
|
39
|
+
* Conceptual entities should realise Business Objects;
|
|
40
|
+
* Logical and physical entities should realise Data Objects or Artifacts.
|
|
41
|
+
* @returns {{ valid: boolean, recommendation?: string }}
|
|
42
|
+
*/
|
|
43
|
+
validateLayerAlignment() {
|
|
44
|
+
if (this.level === 'conceptual' && this.archimateType !== 'business-object') {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
recommendation: 'Conceptual data model concepts should realise ArchiMate Business Objects in the Business layer.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if ((this.level === 'logical' || this.level === 'physical') &&
|
|
51
|
+
!['data-object', 'artifact'].includes(this.archimateType)) {
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
recommendation: `${this.level} data entities should realise ArchiMate Data Objects (Application layer) or Artifacts (Technology layer).`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { valid: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toJSON() {
|
|
61
|
+
return {
|
|
62
|
+
dataModelEntityId: this.dataModelEntityId,
|
|
63
|
+
level: this.level,
|
|
64
|
+
archimateElementId: this.archimateElementId,
|
|
65
|
+
archimateType: this.archimateType,
|
|
66
|
+
archimateModelId: this.archimateModelId,
|
|
67
|
+
notes: this.notes,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataLineage — end-to-end data lineage and impact analysis across models and levels.
|
|
3
|
+
* Traces source-to-target provenance forward and backward (DMBOK2 Integration & Interoperability).
|
|
4
|
+
*/
|
|
5
|
+
export class DataLineage {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} [options]
|
|
8
|
+
* @param {Array<ModelMapping>} [options.modelMappings]
|
|
9
|
+
* @param {Array<LevelMapping>} [options.levelMappings]
|
|
10
|
+
*/
|
|
11
|
+
constructor({ modelMappings = [], levelMappings = [] } = {}) {
|
|
12
|
+
this.modelMappings = [...modelMappings];
|
|
13
|
+
this.levelMappings = [...levelMappings];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
addModelMapping(mapping) {
|
|
17
|
+
this.modelMappings.push(mapping);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Trace upstream lineage of a target attribute back to its source attributes.
|
|
22
|
+
* Traverses multi-hop transformations across models.
|
|
23
|
+
* @param {string} targetEntityId
|
|
24
|
+
* @param {string} [targetAttribute]
|
|
25
|
+
* @returns {Array<Object>} Chain of lineage hops with transformations
|
|
26
|
+
*/
|
|
27
|
+
traceLineage(targetEntityId, targetAttribute = null) {
|
|
28
|
+
const chain = [];
|
|
29
|
+
const visited = new Set();
|
|
30
|
+
const queue = [{ entity: targetEntityId, attribute: targetAttribute, depth: 0 }];
|
|
31
|
+
|
|
32
|
+
while (queue.length) {
|
|
33
|
+
const current = queue.shift();
|
|
34
|
+
const key = `${current.entity}:${current.attribute || '*'}`;
|
|
35
|
+
if (visited.has(key)) continue;
|
|
36
|
+
visited.add(key);
|
|
37
|
+
|
|
38
|
+
for (const map of this.modelMappings) {
|
|
39
|
+
for (const attrMap of map.attributeMappings) {
|
|
40
|
+
if (attrMap.targetEntityId === current.entity &&
|
|
41
|
+
(!current.attribute || attrMap.targetAttribute === current.attribute)) {
|
|
42
|
+
const step = {
|
|
43
|
+
sourceModel: map.sourceModel,
|
|
44
|
+
targetModel: map.targetModel,
|
|
45
|
+
sourceEntityId: attrMap.sourceEntityId,
|
|
46
|
+
sourceAttribute: attrMap.sourceAttribute,
|
|
47
|
+
targetEntityId: attrMap.targetEntityId,
|
|
48
|
+
targetAttribute: attrMap.targetAttribute,
|
|
49
|
+
transformation: attrMap.transformation,
|
|
50
|
+
rule: attrMap.rule,
|
|
51
|
+
depth: current.depth + 1,
|
|
52
|
+
};
|
|
53
|
+
chain.push(step);
|
|
54
|
+
queue.push({
|
|
55
|
+
entity: attrMap.sourceEntityId,
|
|
56
|
+
attribute: attrMap.sourceAttribute,
|
|
57
|
+
depth: current.depth + 1,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return chain;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Trace downstream impact of changing a source attribute forward to targets.
|
|
68
|
+
* Traverses multi-hop dependencies across models.
|
|
69
|
+
* @param {string} sourceEntityId
|
|
70
|
+
* @param {string} [sourceAttribute]
|
|
71
|
+
* @returns {Array<Object>} Affected downstream entities and attributes
|
|
72
|
+
*/
|
|
73
|
+
traceImpact(sourceEntityId, sourceAttribute = null) {
|
|
74
|
+
const impacts = [];
|
|
75
|
+
const visited = new Set();
|
|
76
|
+
const queue = [{ entity: sourceEntityId, attribute: sourceAttribute, depth: 0 }];
|
|
77
|
+
|
|
78
|
+
while (queue.length) {
|
|
79
|
+
const current = queue.shift();
|
|
80
|
+
const key = `${current.entity}:${current.attribute || '*'}`;
|
|
81
|
+
if (visited.has(key)) continue;
|
|
82
|
+
visited.add(key);
|
|
83
|
+
|
|
84
|
+
for (const map of this.modelMappings) {
|
|
85
|
+
for (const attrMap of map.attributeMappings) {
|
|
86
|
+
if (attrMap.sourceEntityId === current.entity &&
|
|
87
|
+
(!current.attribute || attrMap.sourceAttribute === current.attribute)) {
|
|
88
|
+
const step = {
|
|
89
|
+
sourceModel: map.sourceModel,
|
|
90
|
+
targetModel: map.targetModel,
|
|
91
|
+
sourceEntityId: attrMap.sourceEntityId,
|
|
92
|
+
sourceAttribute: attrMap.sourceAttribute,
|
|
93
|
+
targetEntityId: attrMap.targetEntityId,
|
|
94
|
+
targetAttribute: attrMap.targetAttribute,
|
|
95
|
+
transformation: attrMap.transformation,
|
|
96
|
+
rule: attrMap.rule,
|
|
97
|
+
depth: current.depth + 1,
|
|
98
|
+
};
|
|
99
|
+
impacts.push(step);
|
|
100
|
+
queue.push({
|
|
101
|
+
entity: attrMap.targetEntityId,
|
|
102
|
+
attribute: attrMap.targetAttribute,
|
|
103
|
+
depth: current.depth + 1,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return impacts;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generates a graph representation of the lineage suitable for diagramming or export.
|
|
114
|
+
* @returns {{ nodes: Array<{id: string, entity: string, attribute: string, model: string}>, edges: Array<{source: string, target: string, transformation: string, rule: string}> }}
|
|
115
|
+
*/
|
|
116
|
+
toGraph() {
|
|
117
|
+
const nodes = new Map();
|
|
118
|
+
const edges = [];
|
|
119
|
+
|
|
120
|
+
for (const map of this.modelMappings) {
|
|
121
|
+
for (const m of map.attributeMappings) {
|
|
122
|
+
const srcId = `${map.sourceModel || 'source'}:${m.sourceEntityId}.${m.sourceAttribute}`;
|
|
123
|
+
const tgtId = `${map.targetModel || 'target'}:${m.targetEntityId}.${m.targetAttribute}`;
|
|
124
|
+
if (!nodes.has(srcId)) {
|
|
125
|
+
nodes.set(srcId, {
|
|
126
|
+
id: srcId,
|
|
127
|
+
model: map.sourceModel || 'source',
|
|
128
|
+
entity: m.sourceEntityId,
|
|
129
|
+
attribute: m.sourceAttribute,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!nodes.has(tgtId)) {
|
|
133
|
+
nodes.set(tgtId, {
|
|
134
|
+
id: tgtId,
|
|
135
|
+
model: map.targetModel || 'target',
|
|
136
|
+
entity: m.targetEntityId,
|
|
137
|
+
attribute: m.targetAttribute,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
edges.push({
|
|
141
|
+
source: srcId,
|
|
142
|
+
target: tgtId,
|
|
143
|
+
transformation: m.transformation,
|
|
144
|
+
rule: m.rule,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { nodes: [...nodes.values()], edges };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -15,6 +15,9 @@ import { NamingStandard } from './naming/NamingStandard.js';
|
|
|
15
15
|
import { ClassificationScheme } from './ClassificationScheme.js';
|
|
16
16
|
import { ModelScorecard } from './scorecard/ModelScorecard.js';
|
|
17
17
|
import { ServiceLandscape } from './ServiceLandscape.js';
|
|
18
|
+
import { ModelMapping } from './ModelMapping.js';
|
|
19
|
+
import { DataLineage } from './DataLineage.js';
|
|
20
|
+
import { ArchimateDataLink } from './ArchimateDataLink.js';
|
|
18
21
|
import {
|
|
19
22
|
LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared,
|
|
20
23
|
} from './DataModelCompared.js';
|
|
@@ -44,7 +47,7 @@ const LEVELS = Object.freeze(['conceptual', 'logical', 'physical']);
|
|
|
44
47
|
*/
|
|
45
48
|
export class DataModel extends EditableModel {
|
|
46
49
|
/** What a snapshot puts back (EditableModel#restore). */
|
|
47
|
-
static STATE = Object.freeze(['name', 'levels', 'mappings', 'mappingLayout', 'noteLayout', 'perspectives', 'contexts', 'dataTypes', 'namingStandard', 'classificationScheme', 'scorecard']);
|
|
50
|
+
static STATE = Object.freeze(['name', 'levels', 'mappings', 'mappingLayout', 'noteLayout', 'perspectives', 'contexts', 'dataTypes', 'namingStandard', 'classificationScheme', 'scorecard', 'modelMappings', 'archimateLinks']);
|
|
48
51
|
|
|
49
52
|
/** The levels, outermost first. */
|
|
50
53
|
static LEVELS = LEVELS;
|
|
@@ -149,6 +152,8 @@ export class DataModel extends EditableModel {
|
|
|
149
152
|
this.classificationScheme = spec.classificationScheme instanceof ClassificationScheme ? spec.classificationScheme : new ClassificationScheme(spec.classificationScheme ?? {});
|
|
150
153
|
/** What a reviewer said on the Scorecard, and the weights reviewed by (ModelScorecard), or null. */
|
|
151
154
|
this.scorecard = spec.scorecard ? JSON.parse(JSON.stringify(spec.scorecard)) : null;
|
|
155
|
+
this.modelMappings = (spec.modelMappings ?? []).map(m => m instanceof ModelMapping ? m : new ModelMapping(m));
|
|
156
|
+
this.archimateLinks = (spec.archimateLinks ?? []).map(l => l instanceof ArchimateDataLink ? l : new ArchimateDataLink(l));
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
/** The model scored on the Data Model Scorecard (ModelScorecard). */
|
|
@@ -919,6 +924,83 @@ ${theirs}` : theirs;
|
|
|
919
924
|
...(this.namingStandard && { namingStandard: this.namingStandard.toJSON() }),
|
|
920
925
|
...(!this.classificationScheme.isDefault && { classificationScheme: this.classificationScheme.toJSON() }),
|
|
921
926
|
...(this.scorecard && { scorecard: JSON.parse(JSON.stringify(this.scorecard)) }),
|
|
927
|
+
...(this.modelMappings.length && { modelMappings: this.modelMappings.map(m => m.toJSON()) }),
|
|
928
|
+
...(this.archimateLinks.length && { archimateLinks: this.archimateLinks.map(l => l.toJSON()) }),
|
|
922
929
|
};
|
|
923
930
|
}
|
|
931
|
+
|
|
932
|
+
/** Add a cross-model mapping (DMBOK2 Integration & Interoperability). */
|
|
933
|
+
addModelMapping(mapping) {
|
|
934
|
+
const m = mapping instanceof ModelMapping ? mapping : new ModelMapping(mapping);
|
|
935
|
+
this.modelMappings.push(m);
|
|
936
|
+
return m;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** End-to-end data lineage across models and internal level mappings (DMBOK2). */
|
|
940
|
+
crossModelLineage() {
|
|
941
|
+
return new DataLineage({ modelMappings: this.modelMappings, levelMappings: this.mappings });
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** Link a data model concept to an ArchiMate model element (DMBOK2 Data Architecture). */
|
|
945
|
+
linkToArchimate({ entityId, level = 'conceptual', archimateElementId, archimateType = 'business-object', archimateModelId = null, notes = '' }) {
|
|
946
|
+
this.unlinkFromArchimate(entityId, archimateElementId);
|
|
947
|
+
const link = new ArchimateDataLink({
|
|
948
|
+
dataModelEntityId: entityId,
|
|
949
|
+
level,
|
|
950
|
+
archimateElementId,
|
|
951
|
+
archimateType,
|
|
952
|
+
archimateModelId,
|
|
953
|
+
notes,
|
|
954
|
+
});
|
|
955
|
+
this.archimateLinks.push(link);
|
|
956
|
+
return link;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Remove a link to an ArchiMate element. */
|
|
960
|
+
unlinkFromArchimate(entityId, archimateElementId) {
|
|
961
|
+
this.archimateLinks = this.archimateLinks.filter(l => !(l.dataModelEntityId === entityId && l.archimateElementId === archimateElementId));
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** Links for a specific data model entity. */
|
|
965
|
+
archimateLinksFor(entityId) {
|
|
966
|
+
return this.archimateLinks.filter(l => l.dataModelEntityId === entityId);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Validate linked ArchiMate elements against an ArchiMate model (DMBOK2 Data Architecture).
|
|
971
|
+
* Verifies existence, element type and layer alignment.
|
|
972
|
+
* @param {ArchimateModel} archimateModel
|
|
973
|
+
* @returns {Array<{ link: ArchimateDataLink, valid: boolean, errors: Array<string> }>}
|
|
974
|
+
*/
|
|
975
|
+
validateArchimateRealization(archimateModel) {
|
|
976
|
+
const results = [];
|
|
977
|
+
for (const link of this.archimateLinks) {
|
|
978
|
+
const errors = [];
|
|
979
|
+
const el = typeof archimateModel.getElement === 'function' ? archimateModel.getElement(link.archimateElementId) : null;
|
|
980
|
+
if (!el) {
|
|
981
|
+
errors.push(`Referenced ArchiMate element "${link.archimateElementId}" not found in ArchiMate model`);
|
|
982
|
+
} else if (el.type !== link.archimateType) {
|
|
983
|
+
errors.push(`Type mismatch: link specifies "${link.archimateType}" but model element is "${el.type}"`);
|
|
984
|
+
}
|
|
985
|
+
const layerCheck = link.validateLayerAlignment();
|
|
986
|
+
if (!layerCheck.valid) {
|
|
987
|
+
errors.push(layerCheck.recommendation);
|
|
988
|
+
}
|
|
989
|
+
results.push({ link, valid: errors.length === 0, errors });
|
|
990
|
+
}
|
|
991
|
+
return results;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Create an ArchiMate realization view on the given ArchiMate model showing the data architecture realization.
|
|
996
|
+
* @param {ArchimateModel} archimateModel
|
|
997
|
+
* @param {string} [viewTitle='Data Architecture Realization']
|
|
998
|
+
* @returns {ArchimateView}
|
|
999
|
+
*/
|
|
1000
|
+
createArchimateRealizationView(archimateModel, viewTitle = 'Data Architecture Realization') {
|
|
1001
|
+
const elementIds = [...new Set(this.archimateLinks.map(l => l.archimateElementId))];
|
|
1002
|
+
const view = archimateModel.createView(viewTitle);
|
|
1003
|
+
view.elementIds = elementIds;
|
|
1004
|
+
return view;
|
|
1005
|
+
}
|
|
924
1006
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModelMapping — mappings between distinct data models (DMBOK2 Integration & Interoperability).
|
|
3
|
+
* Maps source model entities and attributes to target model entities and attributes,
|
|
4
|
+
* with transformation rules and expressions.
|
|
5
|
+
*/
|
|
6
|
+
export class ModelMapping {
|
|
7
|
+
/**
|
|
8
|
+
* @param {Object} spec
|
|
9
|
+
* @param {string} [spec.id]
|
|
10
|
+
* @param {string} [spec.name]
|
|
11
|
+
* @param {string} [spec.sourceModel] - Name or identifier of the source data model
|
|
12
|
+
* @param {string} [spec.targetModel] - Name or identifier of the target data model
|
|
13
|
+
* @param {Array<Object>} [spec.entityMappings]
|
|
14
|
+
* @param {Array<Object>} [spec.attributeMappings]
|
|
15
|
+
* @param {string} [spec.documentation]
|
|
16
|
+
*/
|
|
17
|
+
constructor({
|
|
18
|
+
id,
|
|
19
|
+
name = 'Cross-Model Mapping',
|
|
20
|
+
sourceModel = '',
|
|
21
|
+
targetModel = '',
|
|
22
|
+
entityMappings = [],
|
|
23
|
+
attributeMappings = [],
|
|
24
|
+
documentation = '',
|
|
25
|
+
} = {}) {
|
|
26
|
+
this.id = id || `mapping-${Date.now().toString(36)}`;
|
|
27
|
+
this.name = name;
|
|
28
|
+
this.sourceModel = sourceModel;
|
|
29
|
+
this.targetModel = targetModel;
|
|
30
|
+
this.documentation = documentation;
|
|
31
|
+
this.entityMappings = entityMappings.map(e => ({ ...e }));
|
|
32
|
+
this.attributeMappings = attributeMappings.map(a => ({ ...a }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add an entity-level mapping between source and target entities.
|
|
37
|
+
*/
|
|
38
|
+
addEntityMapping({ sourceEntityId, targetEntityId, name = '', description = '' } = {}) {
|
|
39
|
+
if (!sourceEntityId || !targetEntityId) {
|
|
40
|
+
throw new TypeError('Entity mapping requires sourceEntityId and targetEntityId');
|
|
41
|
+
}
|
|
42
|
+
if (!this.entityMappings.some(m => m.sourceEntityId === sourceEntityId && m.targetEntityId === targetEntityId)) {
|
|
43
|
+
this.entityMappings.push({ sourceEntityId, targetEntityId, name, description });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Add an attribute-level field mapping with transformation rule.
|
|
49
|
+
*/
|
|
50
|
+
addAttributeMapping({ sourceEntityId, sourceAttribute, targetEntityId, targetAttribute, transformation = 'direct', rule = '' } = {}) {
|
|
51
|
+
if (!sourceEntityId || !sourceAttribute || !targetEntityId || !targetAttribute) {
|
|
52
|
+
throw new TypeError('Attribute mapping requires sourceEntityId, sourceAttribute, targetEntityId, and targetAttribute');
|
|
53
|
+
}
|
|
54
|
+
const existing = this.attributeMappings.find(m =>
|
|
55
|
+
m.sourceEntityId === sourceEntityId && m.sourceAttribute === sourceAttribute &&
|
|
56
|
+
m.targetEntityId === targetEntityId && m.targetAttribute === targetAttribute
|
|
57
|
+
);
|
|
58
|
+
if (existing) {
|
|
59
|
+
existing.transformation = transformation;
|
|
60
|
+
existing.rule = rule;
|
|
61
|
+
} else {
|
|
62
|
+
this.attributeMappings.push({ sourceEntityId, sourceAttribute, targetEntityId, targetAttribute, transformation, rule });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** All attribute mappings targeting an entity. */
|
|
67
|
+
attributesTargeting(entityId) {
|
|
68
|
+
return this.attributeMappings.filter(m => m.targetEntityId === entityId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** All attribute mappings sourced from an entity. */
|
|
72
|
+
attributesSourcedFrom(entityId) {
|
|
73
|
+
return this.attributeMappings.filter(m => m.sourceEntityId === entityId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
toJSON() {
|
|
77
|
+
return {
|
|
78
|
+
id: this.id,
|
|
79
|
+
name: this.name,
|
|
80
|
+
sourceModel: this.sourceModel,
|
|
81
|
+
targetModel: this.targetModel,
|
|
82
|
+
documentation: this.documentation,
|
|
83
|
+
entityMappings: this.entityMappings.map(e => ({ ...e })),
|
|
84
|
+
attributeMappings: this.attributeMappings.map(a => ({ ...a })),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,85 +1,110 @@
|
|
|
1
|
-
/** Words SQL keeps for itself that a name may not be left bare as. */
|
|
2
|
-
const RESERVED = new Set(['user', 'order', 'group', 'table', 'select', 'from', 'where', 'by', 'to', 'key', 'check', 'default', 'index', 'primary', 'foreign', 'references', 'unique', 'column', 'constraint', 'value', 'values', 'date', 'time', 'timestamp', 'level', 'position', 'role']);
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* SqlDdl — a model's physical level as SQL DDL (DMBOK2's data integration: the
|
|
6
|
-
* design handed to a database): a CREATE TABLE per table, its columns with
|
|
7
|
-
* their types (`defaultType` where a column has none) and NOT NULL where
|
|
8
|
-
* required, its primary key and UNIQUE columns; a FOREIGN KEY per column that
|
|
9
|
-
* references another (`references`: a table, or `table.column`, by id or name;
|
|
10
|
-
* a table alone: its primary key); and COMMENT ONs for definitions. Names are
|
|
11
|
-
* quoted where SQL would not take them bare. ANSI SQL, as PostgreSQL reads it.
|
|
12
|
-
*/
|
|
13
|
-
export class SqlDdl {
|
|
14
|
-
/**
|
|
15
|
-
* @param {Object} [options]
|
|
16
|
-
* @param {string} [options.defaultType] - A column's type when it names none
|
|
17
|
-
* @param {boolean} [options.comments] - Whether definitions become COMMENT ONs
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
...columns.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
1
|
+
/** Words SQL keeps for itself that a name may not be left bare as. */
|
|
2
|
+
const RESERVED = new Set(['user', 'order', 'group', 'table', 'select', 'from', 'where', 'by', 'to', 'key', 'check', 'default', 'index', 'primary', 'foreign', 'references', 'unique', 'column', 'constraint', 'value', 'values', 'date', 'time', 'timestamp', 'level', 'position', 'role']);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SqlDdl — a model's physical level as SQL DDL (DMBOK2's data integration: the
|
|
6
|
+
* design handed to a database): a CREATE TABLE per table, its columns with
|
|
7
|
+
* their types (`defaultType` where a column has none) and NOT NULL where
|
|
8
|
+
* required, its primary key and UNIQUE columns; a FOREIGN KEY per column that
|
|
9
|
+
* references another (`references`: a table, or `table.column`, by id or name;
|
|
10
|
+
* a table alone: its primary key); and COMMENT ONs for definitions. Names are
|
|
11
|
+
* quoted where SQL would not take them bare. ANSI SQL, as PostgreSQL reads it.
|
|
12
|
+
*/
|
|
13
|
+
export class SqlDdl {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {string} [options.defaultType] - A column's type when it names none
|
|
17
|
+
* @param {boolean} [options.comments] - Whether definitions become COMMENT ONs
|
|
18
|
+
* @param {boolean|Array<string>|Set<string>|Object} [options.external] - Trust foreign keys to tables outside the model
|
|
19
|
+
*/
|
|
20
|
+
constructor({ defaultType = 'VARCHAR(255)', comments = true, external = false } = {}) {
|
|
21
|
+
this.defaultType = defaultType;
|
|
22
|
+
this.comments = comments;
|
|
23
|
+
this.external = external;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A name as SQL takes it: bare when it can be, else double-quoted. */
|
|
27
|
+
static name(text) {
|
|
28
|
+
const s = String(text);
|
|
29
|
+
return /^[a-z_][a-z0-9_]*$/.test(s) && !RESERVED.has(s) ? s : `"${s.replace(/"/g, '""')}"`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A table's name as SQL takes it: a schema-qualified one (`sales.customer`)
|
|
34
|
+
* each part as name() has it, so it names the table in its schema rather
|
|
35
|
+
* than one table called "sales.customer".
|
|
36
|
+
*/
|
|
37
|
+
static tableName(text) {
|
|
38
|
+
return String(text).split('.').map(SqlDdl.name).join('.');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A text as an SQL string literal. */
|
|
42
|
+
static literal(text) {
|
|
43
|
+
return `'${String(text).replace(/'/g, "''")}'`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The DDL for a model's physical level. @returns {string} */
|
|
47
|
+
write(model, { external = this.external } = {}) {
|
|
48
|
+
const schema = model.level('physical');
|
|
49
|
+
const tables = schema.entities;
|
|
50
|
+
const table = SqlDdl.tableName;
|
|
51
|
+
const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
|
|
52
|
+
for (const t of tables) {
|
|
53
|
+
const columns = t.attributes ?? [];
|
|
54
|
+
const keys = columns.filter(c => c.isPK).map(c => SqlDdl.name(c.name));
|
|
55
|
+
const lines = [
|
|
56
|
+
...columns.map(c => ` ${SqlDdl.name(c.name)} ${c.type || this.defaultType}${c.required || c.isPK ? ' NOT NULL' : ''}`),
|
|
57
|
+
...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
|
|
58
|
+
...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
|
|
59
|
+
];
|
|
60
|
+
out.push(`CREATE TABLE ${table(t.name)} (`, lines.join(',\n'), ');', '');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isExternalTrusted = (tableName) => {
|
|
64
|
+
if (external === true) return true;
|
|
65
|
+
if (external instanceof Set) return external.has(tableName);
|
|
66
|
+
if (Array.isArray(external)) return external.includes(tableName);
|
|
67
|
+
if (external && typeof external === 'object') return !!external[tableName];
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const t of tables) {
|
|
72
|
+
for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
|
|
73
|
+
const ref = schema.resolveReference(c.references);
|
|
74
|
+
if (!ref) {
|
|
75
|
+
const raw = String(c.references).trim();
|
|
76
|
+
const dot = raw.lastIndexOf('.');
|
|
77
|
+
let targetTable = raw;
|
|
78
|
+
let targetCol = null;
|
|
79
|
+
if (dot > 0) {
|
|
80
|
+
targetTable = raw.slice(0, dot);
|
|
81
|
+
targetCol = raw.slice(dot + 1);
|
|
82
|
+
}
|
|
83
|
+
if (isExternalTrusted(targetTable)) {
|
|
84
|
+
const colRef = targetCol ? ` (${SqlDdl.name(targetCol)})` : '';
|
|
85
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
86
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(targetTable)}${colRef};`);
|
|
87
|
+
} else {
|
|
88
|
+
out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const theirs = ref.column ? [ref.column] : (ref.entity.attributes ?? []).filter(a => a.isPK).map(a => a.name);
|
|
93
|
+
if (!theirs.length) {
|
|
94
|
+
out.push(`-- ${t.name}.${c.name} references ${ref.entity.name}, which has no primary key`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
98
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(ref.entity.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (this.comments) {
|
|
102
|
+
const notes = tables.flatMap(t => [
|
|
103
|
+
...(t.description ? [`COMMENT ON TABLE ${table(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
|
|
104
|
+
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${table(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
|
|
105
|
+
]);
|
|
106
|
+
if (notes.length) out.push('', ...notes);
|
|
107
|
+
}
|
|
108
|
+
return `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -19,3 +19,6 @@ export { DataModelFileFormat } from './DataModelFileFormat.js';
|
|
|
19
19
|
export { SqlDdl } from './SqlDdl.js';
|
|
20
20
|
export { SqlDdlFileFormat } from './SqlDdlFileFormat.js';
|
|
21
21
|
export { LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared } from './DataModelCompared.js';
|
|
22
|
+
export { ModelMapping } from './ModelMapping.js';
|
|
23
|
+
export { DataLineage } from './DataLineage.js';
|
|
24
|
+
export { ArchimateDataLink } from './ArchimateDataLink.js';
|