@peter.prib/renderengine 0.2.6 → 0.2.10
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 +70 -9
- package/REFERENCE.md +303 -2
- package/dist/renderengine-workspace.umd.js +8851 -3305
- package/dist/renderengine.umd.js +5621 -1382
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +251 -19
- package/package.json +1 -1
- package/src/bian.js +182 -0
- package/src/charting/DiagramEditor.js +5 -26
- package/src/charting/DiagramTypeRegistry.js +8 -2
- package/src/charting/FormDialog.js +398 -385
- package/src/charting/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -0
- package/src/charting/RowsField.js +127 -0
- package/src/charting/ViewTabStrip.js +94 -81
- package/src/charting/datamodel/BianLandscapeReader.js +185 -0
- package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
- package/src/charting/datamodel/ClassificationLens.js +33 -0
- package/src/charting/datamodel/ClassificationScheme.js +98 -0
- package/src/charting/datamodel/DataModel.js +574 -12
- package/src/charting/datamodel/DataModelChecker.js +338 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelContext.js +40 -10
- package/src/charting/datamodel/DataModelEditCommand.js +47 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +55 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +64 -0
- package/src/charting/datamodel/ServiceDomain.js +186 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/SqlDdl.js +80 -0
- package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +21 -5
- package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
- package/src/charting/datamodel/naming/NameStyle.js +57 -0
- package/src/charting/datamodel/naming/NamingStandard.js +132 -0
- package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
- package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
- package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
- package/src/charting/datamodel/naming/index.js +8 -0
- package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
- package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
- package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
- package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
- package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
- package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
- package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
- package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
- package/src/charting/datamodel/scorecard/index.js +8 -0
- package/src/charting/diagramModel/MergeModelCommand.js +43 -37
- package/src/charting/diagramModel/OpenViewList.js +75 -0
- package/src/charting/diagramModel/ViewSetChecker.js +22 -34
- package/src/charting/diagrams/DataModelDiagram.js +4 -11
- package/src/charting/diagrams/DataModelRelationshipStrategy.js +19 -5
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +173 -158
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/charting/toolbar/ToolbarSelect.js +6 -0
- package/src/datamodel.js +402 -4
- package/src/index.js +6 -4
- package/src/workspace/EditorShell.js +166 -67
- package/src/workspace/PanelLayout.js +56 -0
- package/src/workspace/layout/TabsNode.js +205 -191
- package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
- package/src/workspace/parts/DataModelPart.js +534 -33
- package/src/workspace/parts/DiagramEditorPart.js +2 -8
- package/src/workspace/parts/EditorPart.js +461 -407
- package/src/workspace/parts/ModelFile.js +216 -187
- package/src/workspace/parts/ModelsPart.js +483 -192
- package/src/workspace/storage/BackupFolder.js +145 -0
- package/src/workspace/storage/KeyValueStore.js +92 -0
- package/src/workspace/storage/ModelLibrary.js +65 -0
- package/src/workspace/workArea.js +417 -413
- package/src/yaml/YamlReader.js +372 -0
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { DataModelPerspective } from './DataModelPerspective.js';
|
|
2
2
|
import { DataModelContext } from './DataModelContext.js';
|
|
3
|
+
import { DataType } from './DataType.js';
|
|
4
|
+
import './EnumerationType.js';
|
|
5
|
+
import './StructuredType.js';
|
|
3
6
|
import { LevelSchema } from './LevelSchema.js';
|
|
4
7
|
import { LevelMapping } from './LevelMapping.js';
|
|
5
8
|
import { NodeGeometry } from '../diagrams/NodeGeometry.js';
|
|
9
|
+
import { ModelComparison } from '../diagramModel/ModelComparison.js';
|
|
10
|
+
import { DataModelChecker } from './DataModelChecker.js';
|
|
11
|
+
import { CommandHistory } from '../CommandHistory.js';
|
|
12
|
+
import { MergeModelCommand } from '../diagramModel/MergeModelCommand.js';
|
|
13
|
+
import { DataModelEditCommand } from './DataModelEditCommand.js';
|
|
14
|
+
import { DataModelNotation } from '../../datamodel.js';
|
|
15
|
+
import { NamingStandard } from './naming/NamingStandard.js';
|
|
16
|
+
import { ClassificationScheme } from './ClassificationScheme.js';
|
|
17
|
+
import { ModelScorecard } from './scorecard/ModelScorecard.js';
|
|
18
|
+
import { ServiceLandscape } from './ServiceLandscape.js';
|
|
19
|
+
import {
|
|
20
|
+
LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared,
|
|
21
|
+
} from './DataModelCompared.js';
|
|
6
22
|
|
|
7
23
|
/** The levels, outermost first: each realised by the next. */
|
|
8
24
|
const LEVELS = Object.freeze(['conceptual', 'logical', 'physical']);
|
|
@@ -21,6 +37,10 @@ const LEVELS = Object.freeze(['conceptual', 'logical', 'physical']);
|
|
|
21
37
|
* The older form, one entity per thing with each level's detail on it
|
|
22
38
|
* (`{ entities, relationships }`, as dataModelDiagram takes them), is read as
|
|
23
39
|
* three levels mapped one to one.
|
|
40
|
+
*
|
|
41
|
+
* It knows whether it changed since it was saved (`edited()`, `markSaved()`,
|
|
42
|
+
* `isDirty()`, `onEdit(fn)`), as a ViewSetModel does, so a part can show it,
|
|
43
|
+
* keep it and ask before it is lost.
|
|
24
44
|
*/
|
|
25
45
|
export class DataModel {
|
|
26
46
|
/** The levels, outermost first. */
|
|
@@ -29,6 +49,57 @@ export class DataModel {
|
|
|
29
49
|
/** The view of how the levels map (a `level` for view()). */
|
|
30
50
|
static MAPPING = 'mapping';
|
|
31
51
|
|
|
52
|
+
/** The view of its service domains in their business areas and domains (a `level` for view()). */
|
|
53
|
+
static LANDSCAPE = 'landscape';
|
|
54
|
+
|
|
55
|
+
/** The view of its data types (a `level` for view()). */
|
|
56
|
+
static DATA_TYPES = 'datatypes';
|
|
57
|
+
|
|
58
|
+
/** The view of its star schemas (a `level` for view()). */
|
|
59
|
+
static STAR = 'star';
|
|
60
|
+
|
|
61
|
+
/** How far a star's dimensions sit from its fact, and how far apart stars are. */
|
|
62
|
+
static STAR_RADIUS = 300;
|
|
63
|
+
static STAR_SPACING = 820;
|
|
64
|
+
|
|
65
|
+
/** How many data type boxes side by side in the data types view, and how far apart. */
|
|
66
|
+
static DATA_TYPE_COLUMNS = 4;
|
|
67
|
+
static DATA_TYPE_SPACING = { x: 260, gap: 40 };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The views that are no level of the schema, each one way to read the
|
|
71
|
+
* whole model: its name (a `level` for view()), label and description,
|
|
72
|
+
* whether a model has it to show (`offered`), whether a box dragged in it
|
|
73
|
+
* keeps its place (`keepsPlaces`), and what it draws.
|
|
74
|
+
*/
|
|
75
|
+
static OTHER_VIEWS = Object.freeze([
|
|
76
|
+
Object.freeze({
|
|
77
|
+
name: 'mapping', label: 'Mapping', description: 'The levels side by side, joined where one realises another',
|
|
78
|
+
offered: () => true, keepsPlaces: true,
|
|
79
|
+
view: (model, choice) => model.mappingView(choice),
|
|
80
|
+
}),
|
|
81
|
+
Object.freeze({
|
|
82
|
+
name: 'datatypes', label: 'Data types', description: 'Its data types: enumerations with their values, structures with their fields',
|
|
83
|
+
offered: (model) => model.dataTypes.length > 0, keepsPlaces: false,
|
|
84
|
+
view: (model) => model.dataTypesView(),
|
|
85
|
+
}),
|
|
86
|
+
Object.freeze({
|
|
87
|
+
name: 'star', label: 'Star schemas', description: 'Each fact with its dimensions around it (a dimensional scheme)',
|
|
88
|
+
offered: (model) => !!model.starLevel(), keepsPlaces: false,
|
|
89
|
+
view: (model) => model.starView(),
|
|
90
|
+
}),
|
|
91
|
+
Object.freeze({
|
|
92
|
+
name: 'landscape', label: 'Service landscape', description: 'The service domains in their business areas and business domains (BIAN)',
|
|
93
|
+
offered: (model) => model.serviceDomains().length > 0, keepsPlaces: false,
|
|
94
|
+
view: (model) => ServiceLandscape.view(model.serviceDomains()),
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/** One of OTHER_VIEWS by name, or null for a level of the schema. */
|
|
99
|
+
static otherView(name) {
|
|
100
|
+
return DataModel.OTHER_VIEWS.find(v => v.name === name) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
32
103
|
/** The room between the mapping view's columns, and between the boxes in one. */
|
|
33
104
|
static MAPPING_COLUMN = 380;
|
|
34
105
|
static MAPPING_GAP = 40;
|
|
@@ -46,10 +117,20 @@ export class DataModel {
|
|
|
46
117
|
* (the conceptual level's is the rule's own x, y)
|
|
47
118
|
* @param {Array<Object|DataModelPerspective>} [spec.perspectives]
|
|
48
119
|
* @param {Array<Object|DataModelContext>} [spec.contexts]
|
|
120
|
+
* @param {Array<Object|DataType>} [spec.dataTypes] - Its data types (DataType and its kinds)
|
|
121
|
+
* @param {Object|NamingStandard|null} [spec.namingStandard] - How its names are written (none: no standard)
|
|
122
|
+
* @param {Object|ClassificationScheme} [spec.classificationScheme] - How its data is classified (the default if none)
|
|
123
|
+
* @param {{weights?: Object, reviews?: Object}} [spec.scorecard] - Its reviewer's scores and weights (ModelScorecard)
|
|
49
124
|
* @param {Array<Object>} [spec.entities] - The older, one-entity form (levelsFromCombined)
|
|
50
125
|
* @param {Array<Object>} [spec.relationships]
|
|
126
|
+
* @param {string} [spec.name] - What the model is called
|
|
51
127
|
*/
|
|
52
128
|
constructor(spec = {}) {
|
|
129
|
+
/** What the model is called. */
|
|
130
|
+
this.name = spec.name ?? '';
|
|
131
|
+
this._version = 0;
|
|
132
|
+
this._saved = 0;
|
|
133
|
+
this._editListeners = [];
|
|
53
134
|
const { levels, mappings = [], mappingLayout = {}, noteLayout = {} } = spec.levels ? spec : DataModel.levelsFromCombined(spec);
|
|
54
135
|
this.levels = Object.fromEntries(LEVELS.map(level => [level, new LevelSchema(level, levels?.[level] ?? {})]));
|
|
55
136
|
this.mappings = LEVELS.slice(1).map((to, i) => {
|
|
@@ -59,8 +140,28 @@ export class DataModel {
|
|
|
59
140
|
});
|
|
60
141
|
this.mappingLayout = { ...mappingLayout };
|
|
61
142
|
this.noteLayout = Object.fromEntries(Object.entries(noteLayout).map(([level, at]) => [level, { ...at }]));
|
|
62
|
-
this.perspectives = (spec.perspectives ?? []).map(p => (p instanceof DataModelPerspective ? p :
|
|
143
|
+
this.perspectives = (spec.perspectives ?? []).map(p => (p instanceof DataModelPerspective ? p : DataModelPerspective.from(p)));
|
|
63
144
|
this.contexts = (spec.contexts ?? []).map(c => (c instanceof DataModelContext ? c : new DataModelContext(c)));
|
|
145
|
+
/** Its data types: the types attributes name (`attribute.type`, a data type's id or name). */
|
|
146
|
+
this.dataTypes = (spec.dataTypes ?? []).map(t => (t instanceof DataType ? t : DataType.from(t)));
|
|
147
|
+
/** How its names are written (NamingStandard), or null: no standard, nothing checked. */
|
|
148
|
+
this.namingStandard = spec.namingStandard ? (spec.namingStandard instanceof NamingStandard ? spec.namingStandard : new NamingStandard(spec.namingStandard)) : null;
|
|
149
|
+
/** How its data is classified (ClassificationScheme): the default unless it keeps its own. */
|
|
150
|
+
this.classificationScheme = spec.classificationScheme instanceof ClassificationScheme ? spec.classificationScheme : new ClassificationScheme(spec.classificationScheme ?? {});
|
|
151
|
+
/** What a reviewer said on the Scorecard, and the weights reviewed by (ModelScorecard), or null. */
|
|
152
|
+
this.scorecard = spec.scorecard ? JSON.parse(JSON.stringify(spec.scorecard)) : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The model scored on the Data Model Scorecard (ModelScorecard). */
|
|
156
|
+
scored() {
|
|
157
|
+
return new ModelScorecard(this);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Keep a reviewer's scores and the weights (ModelScorecard.specFrom's), as one undo step. */
|
|
161
|
+
scorecardCommand(spec) {
|
|
162
|
+
return this.editCommand('Score the model', (model) => {
|
|
163
|
+
model.scorecard = spec && Object.keys(spec).length ? JSON.parse(JSON.stringify(spec)) : null;
|
|
164
|
+
});
|
|
64
165
|
}
|
|
65
166
|
|
|
66
167
|
/**
|
|
@@ -85,6 +186,64 @@ export class DataModel {
|
|
|
85
186
|
return { levels, mappings: [oneToOne('conceptual', 'logical'), oneToOne('logical', 'physical')] };
|
|
86
187
|
}
|
|
87
188
|
|
|
189
|
+
// ===== Compared and merged =====
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* What two data models are compared on and merged by, what is needed first:
|
|
193
|
+
* each level's entities, then each level's relationships, the mappings,
|
|
194
|
+
* contexts and perspectives.
|
|
195
|
+
*/
|
|
196
|
+
comparedKinds() {
|
|
197
|
+
return [
|
|
198
|
+
...LEVELS.map(level => new LevelEntitiesCompared(level)),
|
|
199
|
+
...LEVELS.map(level => new LevelRelationshipsCompared(level)),
|
|
200
|
+
new MappingsCompared(),
|
|
201
|
+
new ContextsCompared(),
|
|
202
|
+
new DataTypesCompared(),
|
|
203
|
+
new PerspectivesCompared(),
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** How this model differs from another (ModelComparison), which can merge the other's into it. */
|
|
208
|
+
compareWith(other) {
|
|
209
|
+
return new ModelComparison(this, other);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Its undo history: a merge is one step on it. */
|
|
213
|
+
get history() {
|
|
214
|
+
return (this._history ??= new CommandHistory());
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ===== Changed since saved =====
|
|
218
|
+
|
|
219
|
+
/** Something changed (a perspective or context, a box or note moved): listeners hear of it. */
|
|
220
|
+
edited() {
|
|
221
|
+
this._version++;
|
|
222
|
+
this._emitEdit();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** This state is the saved one (a file opened or saved). */
|
|
226
|
+
markSaved() {
|
|
227
|
+
this._saved = this._version;
|
|
228
|
+
this._emitEdit();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Whether it changed since markSaved. */
|
|
232
|
+
isDirty() {
|
|
233
|
+
return this._version !== this._saved;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Call `fn(model)` after every change (and markSaved). Returns an unsubscribe function. */
|
|
237
|
+
onEdit(fn) {
|
|
238
|
+
this._editListeners.push(fn);
|
|
239
|
+
return () => { this._editListeners = this._editListeners.filter(l => l !== fn); };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @private */
|
|
243
|
+
_emitEdit() {
|
|
244
|
+
for (const fn of this._editListeners) fn(this);
|
|
245
|
+
}
|
|
246
|
+
|
|
88
247
|
/** A level's schema (LevelSchema). */
|
|
89
248
|
level(name) {
|
|
90
249
|
const schema = this.levels[name];
|
|
@@ -123,6 +282,47 @@ export class DataModel {
|
|
|
123
282
|
return [...ids];
|
|
124
283
|
}
|
|
125
284
|
|
|
285
|
+
/**
|
|
286
|
+
* An entity's lineage across the levels: what it realises (`up`, level by
|
|
287
|
+
* level to the concepts) and what realises it (`down`, to the tables), each
|
|
288
|
+
* `{ level, entity }`, as the mappings say, any number either way.
|
|
289
|
+
*/
|
|
290
|
+
lineage(level, id) {
|
|
291
|
+
const at = LEVELS.indexOf(level);
|
|
292
|
+
const walk = (from, step) => {
|
|
293
|
+
const out = [];
|
|
294
|
+
let ids = [id];
|
|
295
|
+
for (let i = from; i >= 0 && i < LEVELS.length; i += step) {
|
|
296
|
+
const [a, b] = step < 0 ? [LEVELS[i], LEVELS[i + 1]] : [LEVELS[i - 1], LEVELS[i]];
|
|
297
|
+
const mapping = this.mapping(a, b);
|
|
298
|
+
ids = [...new Set(ids.flatMap(x => (step < 0 ? mapping.sourcesOf(x) : mapping.targetsOf(x))))];
|
|
299
|
+
out.push(...ids.map(x => ({ level: LEVELS[i], entity: this.level(LEVELS[i]).entity(x) })).filter(e => e.entity));
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
};
|
|
303
|
+
return { up: walk(at - 1, -1), down: walk(at + 1, 1) };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Lineage in words, for a hover: 'Party (conceptual) → Person (logical)'. */
|
|
307
|
+
lineageText(level, id) {
|
|
308
|
+
const { up, down } = this.lineage(level, id);
|
|
309
|
+
const name = ({ level: l, entity }) => `${entity.name} (${l})`;
|
|
310
|
+
return {
|
|
311
|
+
...(up.length && { realises: up.map(name).join(', ') }),
|
|
312
|
+
...(down.length && { 'realised by': down.map(name).join(', ') }),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The perspectives (service domains) that own a business object (a logical entity). */
|
|
317
|
+
ownersOf(entityId) {
|
|
318
|
+
return this.perspectives.filter(p => p.ownedEntities().includes(entityId));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The checks this model is held to (DataModelChecker): ownership, service domains, mappings. */
|
|
322
|
+
checker() {
|
|
323
|
+
return new DataModelChecker(this);
|
|
324
|
+
}
|
|
325
|
+
|
|
126
326
|
/** A perspective by id, or null (the whole schema). */
|
|
127
327
|
perspective(id) {
|
|
128
328
|
return this.perspectives.find(p => p.id === id) ?? null;
|
|
@@ -148,11 +348,14 @@ export class DataModel {
|
|
|
148
348
|
* @returns {{entities, relationships, annotations, links}}
|
|
149
349
|
*/
|
|
150
350
|
view({ perspective = null, context = null, level = 'conceptual' } = {}) {
|
|
151
|
-
|
|
351
|
+
const other = DataModel.otherView(level);
|
|
352
|
+
if (other) return other.view(this, { perspective, context });
|
|
152
353
|
const lens = this.perspective(perspective);
|
|
153
354
|
const meaning = this.context(context);
|
|
154
355
|
const schema = this.level(level);
|
|
155
356
|
const allowed = this.realising(level, lens ? this.concepts.filter(c => lens.shows(c.id)).map(c => c.id) : null);
|
|
357
|
+
// A kind of perspective may show more at a level (a service domain: its control record and qualifiers).
|
|
358
|
+
for (const id of lens?.alsoShows(level) ?? []) allowed?.add(id);
|
|
156
359
|
const shown = schema.entities.filter(e => !allowed || allowed.has(e.id));
|
|
157
360
|
const ids = new Set(shown.map(e => e.id));
|
|
158
361
|
const conceptName = (id) => {
|
|
@@ -163,32 +366,47 @@ export class DataModel {
|
|
|
163
366
|
const concepts = this.conceptsOf(level, entity.id);
|
|
164
367
|
const name = level === 'conceptual' && lens ? lens.nameOf(entity) : entity.name;
|
|
165
368
|
const meanings = meaning ? concepts.map(c => meaning.meaningOf(c)).filter(Boolean) : [];
|
|
166
|
-
const rules = meaning ? [...new Set(concepts.flatMap(c => meaning.constraintsOn(c).map(r => r
|
|
369
|
+
const rules = meaning ? [...new Set(concepts.flatMap(c => meaning.constraintsOn(c).map(r => DataModelContext.ruleText(r))))] : [];
|
|
370
|
+
const owners = level === 'logical' ? this.ownersOf(entity.id).map(o => o.name) : [];
|
|
371
|
+
const typed = this.typedAttributes(entity);
|
|
372
|
+
const iso = this.iso20022Of(entity);
|
|
373
|
+
const classified = ClassificationScheme.describe(this.classificationScheme.effective(entity));
|
|
167
374
|
const details = {
|
|
168
375
|
...entity.details,
|
|
376
|
+
...(owners.length && { 'owned by': owners.join(', ') }),
|
|
377
|
+
...Object.fromEntries(Object.entries(entity.stewardship ?? {}).filter(([, v]) => v)),
|
|
378
|
+
...(classified && { classification: classified }),
|
|
379
|
+
...DataModelNotation.entityRole(entity).details(entity),
|
|
380
|
+
...this.lineageText(level, entity.id),
|
|
381
|
+
...(typed.length && { 'data types': typed.map(({ attribute, type }) => `${attribute.name}: ${type.summary()}`) }),
|
|
382
|
+
...(iso.length && { 'ISO 20022': iso }),
|
|
169
383
|
...(name !== entity.name && { schema: entity.name }),
|
|
170
384
|
...(level !== 'conceptual' && concepts.length && { concepts: concepts.map(conceptName).join(', ') }),
|
|
171
385
|
...(meaning && { context: meaning.name }),
|
|
172
386
|
...(rules.length && { rules }),
|
|
173
387
|
};
|
|
174
388
|
const { attributes, ...rest } = entity;
|
|
389
|
+
const mark = lens?.decorate(entity, level, this) ?? {};
|
|
390
|
+
const allDetails = { ...details, ...mark.details };
|
|
175
391
|
return {
|
|
176
392
|
...rest,
|
|
177
393
|
name,
|
|
394
|
+
...(mark.color && { color: mark.color }),
|
|
178
395
|
description: meanings.length ? meanings.join(' ') : entity.description,
|
|
179
396
|
...(level !== 'conceptual' && { [level]: { attributes: attributes ?? [], ...(level === 'physical' && { tableName: entity.name }) } }),
|
|
180
|
-
...(Object.keys(
|
|
397
|
+
...(Object.keys(allDetails).length && { details: allDetails }),
|
|
181
398
|
};
|
|
182
399
|
});
|
|
183
400
|
const relationships = schema.relationships
|
|
184
401
|
.filter(r => ids.has(r.source) && ids.has(r.target))
|
|
185
402
|
.map(r => (lens && level === 'conceptual' ? { ...r, name: lens.relationshipNameOf(r) } : r));
|
|
186
|
-
const notes =
|
|
187
|
-
.map(c => {
|
|
403
|
+
const notes = [
|
|
404
|
+
...(meaning?.constraints ?? []).map(c => {
|
|
188
405
|
const on = this.realising(level, c.applies) ?? new Set(c.applies);
|
|
189
|
-
return { id: c.id, text: c
|
|
190
|
-
})
|
|
191
|
-
.
|
|
406
|
+
return { id: c.id, text: DataModelContext.ruleText(c), attachTo: [...on].filter(id => ids.has(id)), ...this.notePlace(level, c) };
|
|
407
|
+
}),
|
|
408
|
+
...(lens?.notes(level) ?? []).map(n => ({ ...n, attachTo: n.attachTo.filter(id => ids.has(id)), ...this.notePlace(level, n) })),
|
|
409
|
+
].filter(a => a.attachTo.length);
|
|
192
410
|
// A note with no place on this level goes in a row below every box, so it covers none.
|
|
193
411
|
const below = Math.max(0, ...shown.map(e => (Number.isFinite(e.y) ? e.y : 0))) + DataModel.NOTE_ROW;
|
|
194
412
|
let free = 0;
|
|
@@ -241,10 +459,342 @@ export class DataModel {
|
|
|
241
459
|
|
|
242
460
|
// ===== Perspectives and contexts of one's own =====
|
|
243
461
|
|
|
462
|
+
// ===== Editing what a level holds (each a DataModelEditCommand: one undo step) =====
|
|
463
|
+
|
|
464
|
+
/** Put the model back as a snapshot says (DataModelEditCommand): its levels, mappings, layout, perspectives, contexts, data types. */
|
|
465
|
+
restore(json) {
|
|
466
|
+
const back = new DataModel(JSON.parse(JSON.stringify(json)));
|
|
467
|
+
for (const key of ['name', 'levels', 'mappings', 'mappingLayout', 'noteLayout', 'perspectives', 'contexts', 'dataTypes', 'namingStandard', 'classificationScheme', 'scorecard']) this[key] = back[key];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Set how its data is classified (a ClassificationScheme spec), as one undo step. */
|
|
471
|
+
classificationSchemeCommand(spec) {
|
|
472
|
+
return this.editCommand('Set the classification scheme', (model) => {
|
|
473
|
+
model.classificationScheme = new ClassificationScheme(spec);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Set how its names are written (a NamingStandard spec; null: none), as one undo step. */
|
|
478
|
+
namingStandardCommand(spec) {
|
|
479
|
+
return this.editCommand(spec ? 'Set the naming standard' : 'Remove the naming standard', (model) => {
|
|
480
|
+
model.namingStandard = spec ? new NamingStandard(spec) : null;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** A change made as one undo step. */
|
|
485
|
+
editCommand(label, change) {
|
|
486
|
+
return new DataModelEditCommand(this, label, change);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Add an entity at a level (`id` null: one made from its name) or change
|
|
491
|
+
* one (what the spec leaves out, where it is drawn among it, kept).
|
|
492
|
+
* The command's `result` is the entity.
|
|
493
|
+
*/
|
|
494
|
+
saveEntityCommand(level, id, spec) {
|
|
495
|
+
const noun = DataModelNotation.level(level)?.entityNoun ?? 'entity';
|
|
496
|
+
return this.editCommand(id ? `Edit ${spec.name || id}` : `New ${noun.toLowerCase()} ${spec.name ?? ''}`.trim(), (model) => {
|
|
497
|
+
const schema = model.level(level);
|
|
498
|
+
if (!id) return schema.addEntity({ ...spec, id: spec.id ?? model._freshId(spec.name || noun, schema.entities) });
|
|
499
|
+
const i = schema.entities.findIndex(e => e.id === id);
|
|
500
|
+
const { x, y } = schema.entities[i];
|
|
501
|
+
schema.entities[i] = { x, y, ...spec, id };
|
|
502
|
+
return schema.entities[i];
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Remove an entity from a level, with what refers to it: its
|
|
508
|
+
* relationships, its mapping pairs, and what perspectives and contexts
|
|
509
|
+
* say of it (each forgets it: forgetEntity).
|
|
510
|
+
*/
|
|
511
|
+
removeEntityCommand(level, id) {
|
|
512
|
+
const name = this.level(level).entity(id)?.name ?? id;
|
|
513
|
+
return this.editCommand(`Delete ${name}`, (model) => {
|
|
514
|
+
const schema = model.level(level);
|
|
515
|
+
schema.entities = schema.entities.filter(e => e.id !== id);
|
|
516
|
+
schema.relationships = schema.relationships.filter(r => r.source !== id && r.target !== id);
|
|
517
|
+
for (const m of model.mappings) {
|
|
518
|
+
m.pairs = m.pairs.filter(([a, b]) => !(m.from === level && a === id) && !(m.to === level && b === id));
|
|
519
|
+
}
|
|
520
|
+
for (const holder of [...model.perspectives, ...model.contexts]) holder.forgetEntity(level, id);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Which of a level's relationships something drawn is (a line, or a
|
|
526
|
+
* Chen diamond), by its id, the id a diamond is given, else its ends and
|
|
527
|
+
* name; -1 for none.
|
|
528
|
+
*/
|
|
529
|
+
relationshipIndex(level, drawn) {
|
|
530
|
+
const rels = this.level(level).relationships;
|
|
531
|
+
const diamond = (r, i) => r.id || `${r.source}__${r.target}__rel${i}`;
|
|
532
|
+
const byId = rels.findIndex((r, i) => drawn?.id && (r.id === drawn.id || diamond(r, i) === drawn.id));
|
|
533
|
+
if (byId >= 0) return byId;
|
|
534
|
+
// A line to or from a Chen diamond is its relationship's.
|
|
535
|
+
const ends = [drawn?.source, drawn?.target];
|
|
536
|
+
const toDiamond = rels.findIndex((r, i) => ends.includes(diamond(r, i)));
|
|
537
|
+
if (toDiamond >= 0) return toDiamond;
|
|
538
|
+
return rels.findIndex(r => r.source === drawn?.source && r.target === drawn?.target && (r.name ?? '') === (drawn?.name ?? r.name ?? ''));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Add a relationship at a level (`index` null) or change the one at `index`. The command's `result` is it. */
|
|
542
|
+
saveRelationshipCommand(level, index, spec) {
|
|
543
|
+
return this.editCommand(index == null ? `New relationship ${spec.name ?? ''}`.trim() : `Edit relationship ${spec.name ?? ''}`.trim(), (model) => {
|
|
544
|
+
const rels = model.level(level).relationships;
|
|
545
|
+
if (index == null) {
|
|
546
|
+
const relationship = { id: spec.id ?? model._freshId(`${spec.source}-${spec.target}`, rels), ...spec };
|
|
547
|
+
rels.push(relationship);
|
|
548
|
+
return relationship;
|
|
549
|
+
}
|
|
550
|
+
rels[index] = { ...spec };
|
|
551
|
+
return rels[index];
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
removeRelationshipCommand(level, index) {
|
|
556
|
+
const r = this.level(level).relationships[index];
|
|
557
|
+
return this.editCommand(`Delete relationship ${r?.name ?? ''}`.trim(), (model) => {
|
|
558
|
+
model.level(level).relationships.splice(index, 1);
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ===== Data types =====
|
|
563
|
+
|
|
564
|
+
/** A data type by its id, else its name (any case), or null. */
|
|
565
|
+
dataType(ref) {
|
|
566
|
+
if (!ref) return null;
|
|
567
|
+
const name = String(ref).toLowerCase();
|
|
568
|
+
return this.dataTypes.find(t => t.id === ref) ?? this.dataTypes.find(t => t.name.toLowerCase() === name) ?? null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** An attribute's data type (its `type` naming one), or null for a plain type ('VARCHAR(20)'). */
|
|
572
|
+
typeOf(attribute) {
|
|
573
|
+
return this.dataType(attribute?.type);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** An entity's attributes that have a data type: `[{ attribute, type }]`. */
|
|
577
|
+
typedAttributes(entity) {
|
|
578
|
+
return (entity.attributes ?? []).map(attribute => ({ attribute, type: this.typeOf(attribute) })).filter(a => a.type);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* What an entity says of ISO 20022, a line each: its own type, then each
|
|
583
|
+
* attribute's (its own `iso20022`, else its data type's).
|
|
584
|
+
*/
|
|
585
|
+
iso20022Of(entity) {
|
|
586
|
+
return [
|
|
587
|
+
...(entity.iso20022 ? [`${entity.name}: ${entity.iso20022}`] : []),
|
|
588
|
+
...(entity.attributes ?? []).map(a => [a.name, a.iso20022 || this.typeOf(a)?.iso20022]).filter(([, t]) => t).map(([n, t]) => `${n}: ${t}`),
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Where a data type is used: `{ attributes: [{ level, entity, attribute }], types: [DataType] }`. */
|
|
593
|
+
usesOf(id) {
|
|
594
|
+
const type = this.dataType(id);
|
|
595
|
+
if (!type) return { attributes: [], types: [] };
|
|
596
|
+
const names = (ref) => this.dataType(ref) === type;
|
|
597
|
+
return {
|
|
598
|
+
attributes: LEVELS.flatMap(level => this.level(level).entities.flatMap(entity => (entity.attributes ?? [])
|
|
599
|
+
.filter(a => names(a.type)).map(attribute => ({ level, entity, attribute })))),
|
|
600
|
+
types: this.dataTypes.filter(t => t !== type && t.uses().some(names)),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Add a data type (a DataType spec, its `kind` saying which; an id is made from its name when it has none). */
|
|
605
|
+
addDataType(spec) {
|
|
606
|
+
const type = DataType.from({ ...spec, id: spec.id ?? this._freshId(spec.name, this.dataTypes) });
|
|
607
|
+
this.dataTypes.push(type);
|
|
608
|
+
this.edited();
|
|
609
|
+
return type;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Change a data type (its id and kind stay). */
|
|
613
|
+
updateDataType(id, spec) {
|
|
614
|
+
const i = this.dataTypes.findIndex(t => t.id === id);
|
|
615
|
+
if (i < 0) return null;
|
|
616
|
+
this.dataTypes[i] = DataType.from({ ...spec, kind: this.dataTypes[i].constructor.kind, id });
|
|
617
|
+
this.edited();
|
|
618
|
+
return this.dataTypes[i];
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
removeDataType(id) {
|
|
622
|
+
this.dataTypes = this.dataTypes.filter(t => t.id !== id);
|
|
623
|
+
this.edited();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* The data types view: each a box with a row per value or field (as a
|
|
628
|
+
* table), enumerations and structures coloured apart, in a grid; a link
|
|
629
|
+
* from a structure to each data type its fields are.
|
|
630
|
+
*/
|
|
631
|
+
dataTypesView() {
|
|
632
|
+
const { x: across, gap } = DataModel.DATA_TYPE_SPACING;
|
|
633
|
+
const columns = DataModel.DATA_TYPE_COLUMNS;
|
|
634
|
+
const color = { enumeration: '#fef3c7', structure: '#e0f2fe', type: '#f3f4f6' };
|
|
635
|
+
const heights = this.dataTypes.map(t => DataModel.boxHeight('datatypes', { datatypes: { attributes: t.rows() } }));
|
|
636
|
+
const tops = [];
|
|
637
|
+
this.dataTypes.forEach((t, i) => {
|
|
638
|
+
const above = i - columns;
|
|
639
|
+
tops.push(above < 0 ? 60 : tops[above] + heights[above] + gap);
|
|
640
|
+
});
|
|
641
|
+
const entities = this.dataTypes.map((t, i) => ({
|
|
642
|
+
id: `type:${t.id}`, name: `${t.constructor === DataType ? '' : `«${t.constructor.kindLabel.toLowerCase()}» `}${t.name}`,
|
|
643
|
+
level: 'datatypes', datatypes: { attributes: t.rows().length ? t.rows() : [{ name: t.basedOn || '—' }] },
|
|
644
|
+
x: 140 + (i % columns) * across, y: tops[i] + heights[i] / 2,
|
|
645
|
+
color: color[t.constructor.kind] ?? color.type,
|
|
646
|
+
description: t.description,
|
|
647
|
+
details: { kind: t.constructor.kindLabel, ...t.codeSet, ...(t.iso20022 && { 'ISO 20022': t.iso20022 }), used: this._usedSummary(t) },
|
|
648
|
+
}));
|
|
649
|
+
const links = this.dataTypes.flatMap(t => [...new Set(t.uses().map(ref => this.dataType(ref)).filter(u => u && u !== t))]
|
|
650
|
+
.map(u => ({ id: `uses:${t.id}->${u.id}`, source: `type:${t.id}`, target: `type:${u.id}` })));
|
|
651
|
+
return { entities, relationships: [], annotations: [], links, notation: 'crowsfoot' };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** Where a data type is used, in a few words. @private */
|
|
655
|
+
_usedSummary(type) {
|
|
656
|
+
const { attributes, types } = this.usesOf(type.id);
|
|
657
|
+
const parts = [
|
|
658
|
+
...(attributes.length ? [`${attributes.length} attribute${attributes.length === 1 ? '' : 's'}`] : []),
|
|
659
|
+
...(types.length ? [`by ${types.map(t => t.name).join(', ')}`] : []),
|
|
660
|
+
];
|
|
661
|
+
return parts.join('; ') || 'nowhere';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** The level its dimensional scheme is at: the physical if it has a fact there, else the logical, else null. */
|
|
665
|
+
starLevel() {
|
|
666
|
+
return ['physical', 'logical'].find(level => this.level(level).entities.some(e => DataModelNotation.entityRole(e).role === 'fact')) ?? null;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* The star view: at the level with facts (starLevel), each fact in a row
|
|
671
|
+
* of stars with the dimensions it is related to around it, a dimension two
|
|
672
|
+
* share drawn once, by the first; facts and dimensions in their role's
|
|
673
|
+
* colour, as tables, with the relationships between them.
|
|
674
|
+
*/
|
|
675
|
+
starView() {
|
|
676
|
+
const level = this.starLevel();
|
|
677
|
+
if (!level) return { entities: [], relationships: [], annotations: [], links: [] };
|
|
678
|
+
const schema = this.level(level);
|
|
679
|
+
const roleOf = (e) => DataModelNotation.entityRole(e);
|
|
680
|
+
const facts = schema.entities.filter(e => roleOf(e).role === 'fact');
|
|
681
|
+
const placed = new Map();
|
|
682
|
+
facts.forEach((fact, i) => {
|
|
683
|
+
const centre = { x: 400 + i * DataModel.STAR_SPACING, y: 420 };
|
|
684
|
+
placed.set(fact.id, centre);
|
|
685
|
+
const dims = schema.relationships
|
|
686
|
+
.filter(r => r.source === fact.id || r.target === fact.id)
|
|
687
|
+
.map(r => schema.entity(r.source === fact.id ? r.target : r.source))
|
|
688
|
+
.filter(d => d && roleOf(d).role === 'dimension' && !placed.has(d.id));
|
|
689
|
+
dims.forEach((d, k) => {
|
|
690
|
+
const angle = -Math.PI / 2 + (2 * Math.PI * k) / Math.max(dims.length, 1);
|
|
691
|
+
placed.set(d.id, { x: Math.round(centre.x + DataModel.STAR_RADIUS * Math.cos(angle)), y: Math.round(centre.y + DataModel.STAR_RADIUS * Math.sin(angle)) });
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
const base = this.view({ level });
|
|
695
|
+
const entities = base.entities.filter(e => placed.has(e.id)).map(e => ({ ...e, ...placed.get(e.id), color: roleOf(e).color ?? e.color }));
|
|
696
|
+
const shown = new Set(entities.map(e => e.id));
|
|
697
|
+
return {
|
|
698
|
+
entities, relationships: base.relationships.filter(r => shown.has(r.source) && shown.has(r.target)),
|
|
699
|
+
annotations: [], links: [], level, notation: 'crowsfoot',
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Its service domains (the perspectives that are ServiceDomains). */
|
|
704
|
+
serviceDomains() {
|
|
705
|
+
return this.perspectives.filter(p => p.constructor.kind === 'service-domain');
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Service domains read from elsewhere (BianLandscapeReader), as one undo
|
|
710
|
+
* step (bringCommand). Null when there is nothing to bring.
|
|
711
|
+
* @param {Array<Object>} specs - ServiceDomain specs
|
|
712
|
+
* @param {string} [source] - Where they were read from, for the undo label
|
|
713
|
+
* @returns {MergeModelCommand|null}
|
|
714
|
+
*/
|
|
715
|
+
serviceDomainsCommand(specs, source = 'a service landscape') {
|
|
716
|
+
return this.bringCommand({ serviceDomains: specs },
|
|
717
|
+
`Read ${specs.length} service domain${specs.length === 1 ? '' : 's'} from ${source}`);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Things read from elsewhere (a BIAN landscape, a BIAN semantic API)
|
|
722
|
+
* brought in as one undo step (a MergeModelCommand). A logical entity
|
|
723
|
+
* already here (by id, else by name) stays as it is, and what was read
|
|
724
|
+
* refers to it; so does a data type already here by name. Only the data
|
|
725
|
+
* types what is brought needs come (those its new entities' attributes
|
|
726
|
+
* are, and theirs in turn). A service domain already here (by id, else by
|
|
727
|
+
* name) keeps all it has, taking its business area and domain from what
|
|
728
|
+
* was read, its asset, pattern and control record where it has none, and
|
|
729
|
+
* the behaviour qualifiers it lacks (by name or entity); one that would
|
|
730
|
+
* come out the same is left alone. Null when there is nothing to bring.
|
|
731
|
+
* @param {Object} read
|
|
732
|
+
* @param {Array<Object>} [read.entities] - Logical entities
|
|
733
|
+
* @param {Array<Object>} [read.relationships] - Logical relationships between them
|
|
734
|
+
* @param {Array<Object>} [read.dataTypes] - DataType specs
|
|
735
|
+
* @param {Array<Object>} [read.serviceDomains] - ServiceDomain specs
|
|
736
|
+
* @param {string} label - The undo label
|
|
737
|
+
* @returns {MergeModelCommand|null}
|
|
738
|
+
*/
|
|
739
|
+
bringCommand({ entities = [], relationships = [], dataTypes = [], serviceDomains = [] }, label) {
|
|
740
|
+
const logical = this.level('logical');
|
|
741
|
+
const sameName = (list, name) => list.find(x => String(x.name).toLowerCase() === String(name).toLowerCase());
|
|
742
|
+
// What was read refers to an entity already here by that one's id.
|
|
743
|
+
const idHere = new Map(entities.map(e => [e.id, (logical.entity(e.id) ?? sameName(logical.entities, e.name))?.id ?? null]));
|
|
744
|
+
const as = (id) => idHere.get(id) ?? id;
|
|
745
|
+
const newEntities = entities.filter(e => !idHere.get(e.id));
|
|
746
|
+
const newRelationships = relationships.map(r => ({ ...r, source: as(r.source), target: as(r.target) }))
|
|
747
|
+
.filter(r => !logical.relationships.some(h => h.source === r.source && h.target === r.target && (h.name ?? '') === (r.name ?? '')));
|
|
748
|
+
// The data types the new entities need, then the ones those need; none already here by id or name.
|
|
749
|
+
const readType = (ref) => dataTypes.find(t => t.id === ref) ?? sameName(dataTypes, ref);
|
|
750
|
+
const needed = new Set();
|
|
751
|
+
const need = (ref) => {
|
|
752
|
+
const t = readType(ref);
|
|
753
|
+
if (!t || needed.has(t) || this.dataType(t.id) || sameName(this.dataTypes, t.name)) return;
|
|
754
|
+
needed.add(t);
|
|
755
|
+
DataType.from(t).uses().forEach(need);
|
|
756
|
+
};
|
|
757
|
+
newEntities.flatMap(e => e.attributes ?? []).forEach(a => need(a.type));
|
|
758
|
+
const newTypes = dataTypes.filter(t => needed.has(t));
|
|
759
|
+
const domains = serviceDomains.map(spec => {
|
|
760
|
+
const read = { ...spec, controlRecord: spec.controlRecord ? as(spec.controlRecord) : spec.controlRecord, qualifiers: (spec.qualifiers ?? []).map(q => ({ ...q, ...(q.entity && { entity: as(q.entity) }) })) };
|
|
761
|
+
const here = this.perspective(read.id) ?? this.perspectives.find(p => p.name.toLowerCase() === String(read.name).toLowerCase());
|
|
762
|
+
if (!here) return read;
|
|
763
|
+
const kept = here.toJSON();
|
|
764
|
+
return {
|
|
765
|
+
...kept, kind: read.kind,
|
|
766
|
+
businessArea: read.businessArea || kept.businessArea || '', businessDomain: read.businessDomain || kept.businessDomain || '',
|
|
767
|
+
asset: kept.asset || read.asset || '', pattern: here.functionalPattern ? kept.pattern : (read.pattern || kept.pattern || ''),
|
|
768
|
+
controlRecord: kept.controlRecord || read.controlRecord || null,
|
|
769
|
+
qualifiers: [...kept.qualifiers, ...read.qualifiers.filter(q => !kept.qualifiers.some(k =>
|
|
770
|
+
k.name.toLowerCase() === String(q.name).toLowerCase() || (q.entity && k.entity === q.entity)))],
|
|
771
|
+
};
|
|
772
|
+
}).filter(spec => {
|
|
773
|
+
// One here that would come out the same is nothing to bring.
|
|
774
|
+
const here = this.perspective(spec.id);
|
|
775
|
+
return !here || JSON.stringify(DataModelPerspective.from(spec).toJSON()) !== JSON.stringify(here.toJSON());
|
|
776
|
+
});
|
|
777
|
+
if (!newEntities.length && !newRelationships.length && !newTypes.length && !domains.length) return null;
|
|
778
|
+
const from = new DataModel({
|
|
779
|
+
name: label,
|
|
780
|
+
levels: { logical: { entities: newEntities, relationships: newRelationships } },
|
|
781
|
+
dataTypes: newTypes, perspectives: domains,
|
|
782
|
+
});
|
|
783
|
+
const kinds = new Map(this.comparedKinds().map(k => [k.key, k]));
|
|
784
|
+
const steps = [
|
|
785
|
+
...from.level('logical').entities.map(item => ({ kind: kinds.get('logical-entity'), item })),
|
|
786
|
+
...from.level('logical').relationships.map(item => ({ kind: kinds.get('logical-relationship'), item })),
|
|
787
|
+
...from.dataTypes.map(item => ({ kind: kinds.get('data-type'), item })),
|
|
788
|
+
...from.perspectives.map(item => ({ kind: kinds.get('perspective'), item })),
|
|
789
|
+
];
|
|
790
|
+
return new MergeModelCommand(this, from, steps, label);
|
|
791
|
+
}
|
|
792
|
+
|
|
244
793
|
/** Add a perspective (DataModelPerspective spec; an id is made from its name when it has none). */
|
|
245
794
|
addPerspective(spec) {
|
|
246
|
-
const perspective =
|
|
795
|
+
const perspective = DataModelPerspective.from({ ...spec, id: spec.id ?? this._freshId(spec.name, this.perspectives) });
|
|
247
796
|
this.perspectives.push(perspective);
|
|
797
|
+
this.edited();
|
|
248
798
|
return perspective;
|
|
249
799
|
}
|
|
250
800
|
|
|
@@ -252,17 +802,21 @@ export class DataModel {
|
|
|
252
802
|
updatePerspective(id, spec) {
|
|
253
803
|
const i = this.perspectives.findIndex(p => p.id === id);
|
|
254
804
|
if (i < 0) return null;
|
|
255
|
-
|
|
805
|
+
this.perspectives[i] = DataModelPerspective.from({ kind: this.perspectives[i].constructor.kind, ...spec, id });
|
|
806
|
+
this.edited();
|
|
807
|
+
return this.perspectives[i];
|
|
256
808
|
}
|
|
257
809
|
|
|
258
810
|
removePerspective(id) {
|
|
259
811
|
this.perspectives = this.perspectives.filter(p => p.id !== id);
|
|
812
|
+
this.edited();
|
|
260
813
|
}
|
|
261
814
|
|
|
262
815
|
/** Add a context (DataModelContext spec; an id is made from its name when it has none). */
|
|
263
816
|
addContext(spec) {
|
|
264
817
|
const context = new DataModelContext({ ...spec, id: spec.id ?? this._freshId(spec.name, this.contexts) });
|
|
265
818
|
this.contexts.push(context);
|
|
819
|
+
this.edited();
|
|
266
820
|
return context;
|
|
267
821
|
}
|
|
268
822
|
|
|
@@ -270,13 +824,16 @@ export class DataModel {
|
|
|
270
824
|
updateContext(id, spec) {
|
|
271
825
|
const i = this.contexts.findIndex(c => c.id === id);
|
|
272
826
|
if (i < 0) return null;
|
|
273
|
-
|
|
827
|
+
this.contexts[i] = new DataModelContext({ ...spec, id });
|
|
828
|
+
this.edited();
|
|
829
|
+
return this.contexts[i];
|
|
274
830
|
}
|
|
275
831
|
|
|
276
832
|
/** Remove a context; a perspective read in it is then read in none. */
|
|
277
833
|
removeContext(id) {
|
|
278
834
|
this.contexts = this.contexts.filter(c => c.id !== id);
|
|
279
835
|
for (const p of this.perspectives) if (p.context === id) p.context = null;
|
|
836
|
+
this.edited();
|
|
280
837
|
}
|
|
281
838
|
|
|
282
839
|
/** An id from a name, not yet among `taken`. @private */
|
|
@@ -289,11 +846,16 @@ export class DataModel {
|
|
|
289
846
|
|
|
290
847
|
toJSON() {
|
|
291
848
|
return {
|
|
849
|
+
name: this.name,
|
|
292
850
|
levels: Object.fromEntries(LEVELS.map(level => [level, this.level(level).toJSON()])),
|
|
293
851
|
mappings: this.mappings.map(m => m.toJSON()),
|
|
294
852
|
mappingLayout: { ...this.mappingLayout },
|
|
295
853
|
noteLayout: Object.fromEntries(Object.entries(this.noteLayout).map(([level, at]) => [level, { ...at }])),
|
|
296
854
|
perspectives: this.perspectives.map(p => p.toJSON()), contexts: this.contexts.map(c => c.toJSON()),
|
|
855
|
+
...(this.dataTypes.length && { dataTypes: this.dataTypes.map(t => t.toJSON()) }),
|
|
856
|
+
...(this.namingStandard && { namingStandard: this.namingStandard.toJSON() }),
|
|
857
|
+
...(!this.classificationScheme.isDefault && { classificationScheme: this.classificationScheme.toJSON() }),
|
|
858
|
+
...(this.scorecard && { scorecard: JSON.parse(JSON.stringify(this.scorecard)) }),
|
|
297
859
|
};
|
|
298
860
|
}
|
|
299
861
|
}
|