@peter.prib/renderengine 0.14.0 → 0.15.0

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 (109) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49391 -39748
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +38 -0
  6. package/package.json +6 -1
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +219 -0
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +3 -0
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +114 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/charts/AlluvialDiagram.js +311 -0
  34. package/src/charting/charts/AreaChart.js +52 -15
  35. package/src/charting/charts/BarChart.js +38 -3
  36. package/src/charting/charts/BarChart3D.js +5 -3
  37. package/src/charting/charts/BarChartBase.js +26 -6
  38. package/src/charting/charts/BeeswarmChart.js +164 -0
  39. package/src/charting/charts/BoxPlotChart.js +23 -1
  40. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  41. package/src/charting/charts/CandlestickChart.js +172 -4
  42. package/src/charting/charts/ChordDiagram.js +105 -2
  43. package/src/charting/charts/CirclePackChart.js +81 -0
  44. package/src/charting/charts/ComboChart.js +233 -0
  45. package/src/charting/charts/ContourChart.js +310 -0
  46. package/src/charting/charts/DonutChart3D.js +13 -0
  47. package/src/charting/charts/DumbbellChart.js +217 -0
  48. package/src/charting/charts/EcdfChart.js +150 -0
  49. package/src/charting/charts/FunnelChart.js +4 -1
  50. package/src/charting/charts/GeoMapChart.js +132 -11
  51. package/src/charting/charts/GroupedBarChart.js +44 -3
  52. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  53. package/src/charting/charts/HeatmapChart.js +2 -2
  54. package/src/charting/charts/HexbinChart.js +137 -0
  55. package/src/charting/charts/HierarchyChart.js +77 -3
  56. package/src/charting/charts/HorizonChart.js +105 -0
  57. package/src/charting/charts/KagiChart.js +167 -0
  58. package/src/charting/charts/LineChart.js +74 -18
  59. package/src/charting/charts/OhlcChart.js +79 -0
  60. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  61. package/src/charting/charts/PieChart.js +195 -17
  62. package/src/charting/charts/PieChart3D.js +137 -0
  63. package/src/charting/charts/PointAndFigureChart.js +198 -0
  64. package/src/charting/charts/PolarAreaChart.js +189 -0
  65. package/src/charting/charts/RadarChart.js +7 -1
  66. package/src/charting/charts/RadialBarChart.js +5 -2
  67. package/src/charting/charts/RenkoChart.js +178 -0
  68. package/src/charting/charts/RidgelineChart.js +113 -0
  69. package/src/charting/charts/SankeyDiagram.js +127 -4
  70. package/src/charting/charts/ScatterChart.js +13 -0
  71. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  72. package/src/charting/charts/SlopegraphChart.js +205 -0
  73. package/src/charting/charts/SparklineTableChart.js +166 -0
  74. package/src/charting/charts/StackedBarChart.js +48 -5
  75. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  76. package/src/charting/charts/StreamGraphChart.js +10 -3
  77. package/src/charting/charts/SunburstChart.js +27 -7
  78. package/src/charting/charts/SurfaceChart3D.js +157 -0
  79. package/src/charting/charts/TreemapChart.js +28 -8
  80. package/src/charting/charts/ViolinChart.js +18 -0
  81. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  82. package/src/charting/datamodel/DataLineage.js +150 -0
  83. package/src/charting/datamodel/DataModel.js +83 -1
  84. package/src/charting/datamodel/ModelMapping.js +87 -0
  85. package/src/charting/datamodel/SqlDdl.js +110 -85
  86. package/src/charting/datamodel/index.js +3 -0
  87. package/src/charting/decision/DecisionWorkbench.js +295 -0
  88. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  89. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  90. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  91. package/src/charting/diagrams/SketchView.js +121 -0
  92. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  93. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  94. package/src/charting/modelTree/NavigatorModel.js +410 -0
  95. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  96. package/src/charting/pivot/PivotChart.js +109 -4
  97. package/src/charting/pivot/PivotGridChart.js +321 -33
  98. package/src/charting/pivot/PivotView.js +82 -0
  99. package/src/charting/report/TemplatedReport.js +547 -0
  100. package/src/charting/templates/BoxTemplate.js +28 -27
  101. package/src/charting/templates/PackageTemplate.js +26 -25
  102. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  103. package/src/charting/templates/Template.js +219 -212
  104. package/src/cli.js +146 -0
  105. package/src/index.js +3370 -2995
  106. package/src/workspace/Workspace.js +126 -7
  107. package/src/workspace/archimateServices.js +4 -0
  108. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  109. 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
- constructor({ defaultType = 'VARCHAR(255)', comments = true } = {}) {
20
- this.defaultType = defaultType;
21
- this.comments = comments;
22
- }
23
-
24
- /** A name as SQL takes it: bare when it can be, else double-quoted. */
25
- static name(text) {
26
- const s = String(text);
27
- return /^[a-z_][a-z0-9_]*$/.test(s) && !RESERVED.has(s) ? s : `"${s.replace(/"/g, '""')}"`;
28
- }
29
-
30
- /**
31
- * A table's name as SQL takes it: a schema-qualified one (`sales.customer`)
32
- * each part as name() has it, so it names the table in its schema rather
33
- * than one table called "sales.customer".
34
- */
35
- static tableName(text) {
36
- return String(text).split('.').map(SqlDdl.name).join('.');
37
- }
38
-
39
- /** A text as an SQL string literal. */
40
- static literal(text) {
41
- return `'${String(text).replace(/'/g, "''")}'`;
42
- }
43
-
44
- /** The DDL for a model's physical level. @returns {string} */
45
- write(model) {
46
- const schema = model.level('physical');
47
- const tables = schema.entities;
48
- const table = SqlDdl.tableName;
49
- const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
50
- for (const t of tables) {
51
- const columns = t.attributes ?? [];
52
- const keys = columns.filter(c => c.isPK).map(c => SqlDdl.name(c.name));
53
- const lines = [
54
- ...columns.map(c => ` ${SqlDdl.name(c.name)} ${c.type || this.defaultType}${c.required || c.isPK ? ' NOT NULL' : ''}`),
55
- ...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
56
- ...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
57
- ];
58
- out.push(`CREATE TABLE ${table(t.name)} (`, lines.join(',\n'), ');', '');
59
- }
60
- for (const t of tables) {
61
- for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
62
- const ref = schema.resolveReference(c.references);
63
- if (!ref) {
64
- out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
65
- continue;
66
- }
67
- const theirs = ref.column ? [ref.column] : (ref.entity.attributes ?? []).filter(a => a.isPK).map(a => a.name);
68
- if (!theirs.length) {
69
- out.push(`-- ${t.name}.${c.name} references ${ref.entity.name}, which has no primary key`);
70
- continue;
71
- }
72
- out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
73
- + ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(ref.entity.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
74
- }
75
- }
76
- if (this.comments) {
77
- const notes = tables.flatMap(t => [
78
- ...(t.description ? [`COMMENT ON TABLE ${table(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
79
- ...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${table(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
80
- ]);
81
- if (notes.length) out.push('', ...notes);
82
- }
83
- return `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
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';