@peter.prib/renderengine 0.2.6 → 0.2.7
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 +4 -0
- package/REFERENCE.md +180 -2
- package/dist/renderengine-workspace.umd.js +8704 -5310
- package/dist/renderengine.umd.js +5675 -3398
- package/examples/archimate.html +68 -66
- package/examples/data-model.html +223 -13
- 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/ModelCheckDialog.js +63 -0
- package/src/charting/ModelChecker.js +26 -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/DataModel.js +356 -10
- package/src/charting/datamodel/DataModelChecker.js +92 -0
- package/src/charting/datamodel/DataModelCompared.js +127 -0
- package/src/charting/datamodel/DataModelFileFormat.js +33 -0
- package/src/charting/datamodel/DataModelPerspective.js +45 -0
- package/src/charting/datamodel/DataType.js +108 -0
- package/src/charting/datamodel/EnumerationType.js +51 -0
- package/src/charting/datamodel/ServiceDomain.js +177 -0
- package/src/charting/datamodel/ServiceLandscape.js +104 -0
- package/src/charting/datamodel/StructuredType.js +63 -0
- package/src/charting/datamodel/index.js +10 -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/DataModelRelationshipStrategy.js +12 -4
- package/src/charting/diagrams/EdgeGeometry.js +578 -553
- package/src/charting/diagrams/GraphDiagram.js +11 -1
- package/src/charting/diagrams/NodeGeometry.js +169 -158
- package/src/charting/glyphs/PersonGlyph.js +24 -23
- package/src/index.js +2666 -2664
- 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 +339 -29
- 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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ComparedKind } from '../diagramModel/ComparedKind.js';
|
|
2
|
+
|
|
3
|
+
/** A plain copy of JSON-shaped data. */
|
|
4
|
+
const copy = (value) => (value === undefined ? undefined : JSON.parse(JSON.stringify(value)));
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The kinds two DataModels are compared on and merged by (ModelComparison,
|
|
8
|
+
* as for ArchiMate models): each level's entities and relationships, the
|
|
9
|
+
* mappings between levels, contexts and perspectives, in the order a merge
|
|
10
|
+
* brings them (what is needed first). Where a box sits is layout, not content:
|
|
11
|
+
* two models alike but for where their boxes are, are alike.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A list of a model's held in an array (`list(model)`), each by an id; bring puts a copy in place or adds it. */
|
|
15
|
+
class ListCompared extends ComparedKind {
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
if (new.target === ListCompared) throw new TypeError('ListCompared is abstract — subclass it');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The array the model holds them in. @abstract */
|
|
22
|
+
list(model) { throw new Error(`${this.constructor.name} must implement list()`); }
|
|
23
|
+
|
|
24
|
+
items(model) { return this.list(model); }
|
|
25
|
+
get(model, id) { return this.items(model).find(item => this.idOf(item) === id) ?? null; }
|
|
26
|
+
|
|
27
|
+
/** Put a copy of `item` at its place (in place of the one with its id, else last); undone, as it was. */
|
|
28
|
+
bring(model, item) {
|
|
29
|
+
const list = this.list(model);
|
|
30
|
+
const twin = this.copyOf(item);
|
|
31
|
+
const at = list.findIndex(x => this.idOf(x) === this.idOf(item));
|
|
32
|
+
if (at < 0) {
|
|
33
|
+
list.push(twin);
|
|
34
|
+
return () => { const i = list.indexOf(twin); if (i >= 0) list.splice(i, 1); };
|
|
35
|
+
}
|
|
36
|
+
const old = list[at];
|
|
37
|
+
list[at] = twin;
|
|
38
|
+
return () => { const i = list.indexOf(twin); if (i >= 0) list[i] = old; };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A copy to put in the other model. */
|
|
42
|
+
copyOf(item) { return copy(item); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One level's entities: compared on all but where they are drawn. */
|
|
46
|
+
export class LevelEntitiesCompared extends ListCompared {
|
|
47
|
+
constructor(level) {
|
|
48
|
+
super();
|
|
49
|
+
this.level = level;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get key() { return `${this.level}-entity`; }
|
|
53
|
+
get label() { return `${this.level.charAt(0).toUpperCase()}${this.level.slice(1)} entities`; }
|
|
54
|
+
list(model) { return model.level(this.level).entities; }
|
|
55
|
+
|
|
56
|
+
content(entity) {
|
|
57
|
+
const { x, y, ...rest } = entity;
|
|
58
|
+
return copy(rest);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One level's relationships: known by their id, else by their ends and name
|
|
64
|
+
* (a relationship often has no id); each needs its two ends at that level.
|
|
65
|
+
*/
|
|
66
|
+
export class LevelRelationshipsCompared extends ListCompared {
|
|
67
|
+
constructor(level) {
|
|
68
|
+
super();
|
|
69
|
+
this.level = level;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get key() { return `${this.level}-relationship`; }
|
|
73
|
+
get label() { return `${this.level.charAt(0).toUpperCase()}${this.level.slice(1)} relationships`; }
|
|
74
|
+
list(model) { return model.level(this.level).relationships; }
|
|
75
|
+
idOf(r) { return r.id ?? `${r.source}->${r.target}${r.name ? `:${r.name}` : ''}`; }
|
|
76
|
+
nameOf(r) { return `${r.source} ${r.name || '→'} ${r.target}`; }
|
|
77
|
+
content(r) { return copy(r); }
|
|
78
|
+
requires(r) { return [[`${this.level}-entity`, r.source], [`${this.level}-entity`, r.target]]; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The mappings between levels, a pair each: a pair needs its two entities. */
|
|
82
|
+
export class MappingsCompared extends ComparedKind {
|
|
83
|
+
get key() { return 'mapping'; }
|
|
84
|
+
get label() { return 'Mappings'; }
|
|
85
|
+
|
|
86
|
+
items(model) {
|
|
87
|
+
return model.mappings.flatMap(m => m.pairs.map(([a, b]) => ({ id: `${m.from}:${a}->${m.to}:${b}`, from: m.from, to: m.to, a, b })));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
nameOf(p) { return `${p.a} → ${p.b} (${p.from} to ${p.to})`; }
|
|
91
|
+
content() { return {}; }
|
|
92
|
+
requires(p) { return [[`${p.from}-entity`, p.a], [`${p.to}-entity`, p.b]]; }
|
|
93
|
+
|
|
94
|
+
bring(model, p) {
|
|
95
|
+
const mapping = model.mapping(p.from, p.to);
|
|
96
|
+
if (mapping.pairs.some(([a, b]) => a === p.a && b === p.b)) return () => {};
|
|
97
|
+
mapping.add(p.a, p.b);
|
|
98
|
+
return () => { mapping.pairs = mapping.pairs.filter(([a, b]) => !(a === p.a && b === p.b)); };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Perspectives and contexts are classes; their JSON is what counts, and a copy is one made from it. */
|
|
103
|
+
class ModelPartsCompared extends ListCompared {
|
|
104
|
+
content(item) { return copy(item.toJSON()); }
|
|
105
|
+
copyOf(item) { return new item.constructor(copy(item.toJSON())); }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class ContextsCompared extends ModelPartsCompared {
|
|
109
|
+
get key() { return 'context'; }
|
|
110
|
+
get label() { return 'Contexts'; }
|
|
111
|
+
list(model) { return model.contexts; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Data types: an attribute names one by its id or name; a structure's fields others. */
|
|
115
|
+
export class DataTypesCompared extends ModelPartsCompared {
|
|
116
|
+
get key() { return 'data-type'; }
|
|
117
|
+
get label() { return 'Data types'; }
|
|
118
|
+
list(model) { return model.dataTypes; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A perspective needs the context it is read in. */
|
|
122
|
+
export class PerspectivesCompared extends ModelPartsCompared {
|
|
123
|
+
get key() { return 'perspective'; }
|
|
124
|
+
get label() { return 'Perspectives'; }
|
|
125
|
+
list(model) { return model.perspectives; }
|
|
126
|
+
requires(p) { return p.context ? [['context', p.context]] : []; }
|
|
127
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ModelFileFormat } from '../ModelFileFormat.js';
|
|
2
|
+
import { DataModel } from './DataModel.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DataModelFileFormat — a data model as a file (`.datamodel.json`): its name,
|
|
6
|
+
* its levels, the mappings between them, where its boxes and notes are, and
|
|
7
|
+
* its perspectives and contexts (DataModel#toJSON), read back by `new
|
|
8
|
+
* DataModel(json)`. The older one-entity form reads too.
|
|
9
|
+
*/
|
|
10
|
+
export class DataModelFileFormat extends ModelFileFormat {
|
|
11
|
+
get label() { return '.datamodel.json'; }
|
|
12
|
+
get accept() { return '.datamodel.json,.json'; }
|
|
13
|
+
get fileName() { return 'model.datamodel.json'; }
|
|
14
|
+
get featureName() { return 'DataModel'; }
|
|
15
|
+
|
|
16
|
+
/** JSON with a data model's shape: levels, or the older entities. */
|
|
17
|
+
recognizes(text) {
|
|
18
|
+
try {
|
|
19
|
+
const json = JSON.parse(text);
|
|
20
|
+
return !!json && typeof json === 'object' && (!!json.levels || Array.isArray(json.entities));
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export(model) {
|
|
27
|
+
return { data: JSON.stringify(model.toJSON(), null, 2), mimeType: 'application/json' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
import(text) {
|
|
31
|
+
return new DataModel(JSON.parse(text));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -5,8 +5,32 @@
|
|
|
5
5
|
* Customer, Ledger the Billing), with relationships renamed too
|
|
6
6
|
* (`relationshipNames`, by relationship id), and names the context it is
|
|
7
7
|
* usually read in (`context`, a DataModelContext id).
|
|
8
|
+
*
|
|
9
|
+
* A kind of perspective that says more (a BIAN ServiceDomain) is a subclass,
|
|
10
|
+
* registered by its `kind` (`DataModelPerspective.register`); a spec's `kind`
|
|
11
|
+
* makes one of it (`DataModelPerspective.from`). What it adds to a view, the view
|
|
12
|
+
* asks it: entities it shows besides (`alsoShows`), how an entity is marked
|
|
13
|
+
* (`decorate`), notes (`notes`). The plain perspective adds none.
|
|
8
14
|
*/
|
|
9
15
|
export class DataModelPerspective {
|
|
16
|
+
/** What this kind of perspective is called in a spec (`kind`) and in menus. */
|
|
17
|
+
static kind = 'perspective';
|
|
18
|
+
static kindLabel = 'Perspective';
|
|
19
|
+
|
|
20
|
+
/** Perspective classes by kind. */
|
|
21
|
+
static KINDS = new Map();
|
|
22
|
+
|
|
23
|
+
/** Make a kind of perspective known, so a spec of its `kind` makes one. */
|
|
24
|
+
static register(PerspectiveClass) {
|
|
25
|
+
DataModelPerspective.KINDS.set(PerspectiveClass.kind, PerspectiveClass);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A perspective from a spec, of the class its `kind` names (a plain one without). */
|
|
29
|
+
static from(spec) {
|
|
30
|
+
const Kind = DataModelPerspective.KINDS.get(spec?.kind) ?? DataModelPerspective;
|
|
31
|
+
return new Kind(spec);
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
/**
|
|
11
35
|
* @param {Object} spec
|
|
12
36
|
* @param {string} spec.id
|
|
@@ -38,6 +62,26 @@ export class DataModelPerspective {
|
|
|
38
62
|
return this.names[entity.id] ?? entity.name;
|
|
39
63
|
}
|
|
40
64
|
|
|
65
|
+
/** Entities at a level this perspective shows besides what realises its concepts (none here). */
|
|
66
|
+
alsoShows(level) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** How an entity it shows is marked at a level: `{ color?, details? }` (nothing here). */
|
|
71
|
+
decorate(entity, level) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The business objects (logical entities) it owns, as a BIAN service domain does (none here). */
|
|
76
|
+
ownedEntities() {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Notes it adds at a level: `[{ id, text, attachTo }]` (none here). */
|
|
81
|
+
notes(level) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
41
85
|
/** What a relationship is called here. */
|
|
42
86
|
relationshipNameOf(relationship) {
|
|
43
87
|
return (relationship.id && this.relationshipNames[relationship.id]) ?? relationship.name;
|
|
@@ -76,6 +120,7 @@ export class DataModelPerspective {
|
|
|
76
120
|
|
|
77
121
|
toJSON() {
|
|
78
122
|
return {
|
|
123
|
+
...(this.constructor.kind !== DataModelPerspective.kind && { kind: this.constructor.kind }),
|
|
79
124
|
id: this.id, name: this.name, description: this.description,
|
|
80
125
|
entities: this.entities ? [...this.entities] : null,
|
|
81
126
|
names: { ...this.names }, relationshipNames: { ...this.relationshipNames }, context: this.context,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Where the ISO 20022 standards repository describes a type, by its name. */
|
|
2
|
+
const ISO20022_TYPE_URL = 'https://www.iso20022.org/standardsrepository/type/';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DataType — a type an attribute can have, defined once in the model and named
|
|
6
|
+
* by the attributes of that type (`attribute.type` its id or name): what BIAN
|
|
7
|
+
* publishes as its data types and DAMA-DMBOK2 calls a domain. This base is a
|
|
8
|
+
* plain named type (a text, a value, a date, perhaps based on another:
|
|
9
|
+
* `basedOn`); EnumerationType and StructuredType add their values and fields.
|
|
10
|
+
* A type may say which ISO 20022 type it is (`iso20022`, linked by
|
|
11
|
+
* `iso20022Url`). Kinds register as perspective kinds do (`kind` in a spec).
|
|
12
|
+
*/
|
|
13
|
+
export class DataType {
|
|
14
|
+
/** What this kind is called in a spec (`kind`) and in menus. */
|
|
15
|
+
static kind = 'type';
|
|
16
|
+
static kindLabel = 'Type';
|
|
17
|
+
|
|
18
|
+
/** DataType classes by kind. */
|
|
19
|
+
static KINDS = new Map();
|
|
20
|
+
|
|
21
|
+
/** Make a kind of data type known, so a spec of its `kind` makes one. */
|
|
22
|
+
static register(TypeClass) {
|
|
23
|
+
DataType.KINDS.set(TypeClass.kind, TypeClass);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A data type from a spec, of the class its `kind` names (a plain one without). */
|
|
27
|
+
static from(spec) {
|
|
28
|
+
const Kind = DataType.KINDS.get(spec?.kind) ?? DataType;
|
|
29
|
+
return new Kind(spec);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Where the ISO 20022 standards repository describes a type (its name), or null. */
|
|
33
|
+
static iso20022Url(name) {
|
|
34
|
+
return name ? `${ISO20022_TYPE_URL}${encodeURIComponent(name)}` : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {Object} spec
|
|
39
|
+
* @param {string} spec.id
|
|
40
|
+
* @param {string} [spec.name] - Its id when unset
|
|
41
|
+
* @param {string} [spec.description]
|
|
42
|
+
* @param {string} [spec.basedOn] - The type it narrows ('Text', 'Decimal', another's id)
|
|
43
|
+
* @param {string} [spec.iso20022] - The ISO 20022 type it is ('CurrencyCode')
|
|
44
|
+
*/
|
|
45
|
+
constructor({ id, name = id, description = '', basedOn = '', iso20022 = '' } = {}) {
|
|
46
|
+
if (!id) throw new TypeError('DataType must have an id');
|
|
47
|
+
Object.assign(this, { id, name, description, basedOn, iso20022 });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Its ISO 20022 type's page, or null. */
|
|
51
|
+
get iso20022Url() {
|
|
52
|
+
return DataType.iso20022Url(this.iso20022);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What it is in a few words, for a hover: its kind, and what it holds. */
|
|
56
|
+
summary() {
|
|
57
|
+
return this.basedOn ? `${this.name} (${this.basedOn})` : this.name;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Its rows when drawn as a box: `[{ name, type? }]` (none for a plain type). */
|
|
61
|
+
rows() {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The other data types it names (by id or name), as a structure's fields do (none here). */
|
|
66
|
+
uses() {
|
|
67
|
+
return this.basedOn ? [this.basedOn] : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
toJSON() {
|
|
71
|
+
return {
|
|
72
|
+
...(this.constructor !== DataType && { kind: this.constructor.kind }),
|
|
73
|
+
id: this.id, name: this.name,
|
|
74
|
+
...(this.description && { description: this.description }),
|
|
75
|
+
...(this.basedOn && { basedOn: this.basedOn }),
|
|
76
|
+
...(this.iso20022 && { iso20022: this.iso20022 }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Its dialog's fields: name, description, what it is based on, its ISO 20022 type. */
|
|
81
|
+
static fields(model, type = null) {
|
|
82
|
+
return [
|
|
83
|
+
{ key: 'name', label: 'Name', type: 'text', value: type?.name ?? '' },
|
|
84
|
+
{ key: 'description', label: 'Description', type: 'textarea', rows: 2, value: type?.description ?? '' },
|
|
85
|
+
{ key: 'basedOn', label: 'Based on', type: 'text', value: type?.basedOn ?? '', placeholder: 'Text, Decimal, Date… or another data type' },
|
|
86
|
+
{ key: 'iso20022', label: 'ISO 20022 type', type: 'text', value: type?.iso20022 ?? '', placeholder: 'e.g. CurrencyCode' },
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A spec from those fields. */
|
|
91
|
+
static specFrom(values) {
|
|
92
|
+
return {
|
|
93
|
+
...(this !== DataType && { kind: this.kind }),
|
|
94
|
+
name: String(values.name ?? '').trim(),
|
|
95
|
+
description: String(values.description ?? '').trim(),
|
|
96
|
+
basedOn: String(values.basedOn ?? '').trim(),
|
|
97
|
+
iso20022: String(values.iso20022 ?? '').trim(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Lines `name | more` from a textarea, each split on its first bar. @protected */
|
|
102
|
+
static lines(text) {
|
|
103
|
+
return String(text ?? '').split('\n').map(l => l.trim()).filter(Boolean).map(line => {
|
|
104
|
+
const bar = line.indexOf('|');
|
|
105
|
+
return bar < 0 ? [line, ''] : [line.slice(0, bar).trim(), line.slice(bar + 1).trim()];
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DataType } from './DataType.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EnumerationType — a data type whose values are a fixed list, each perhaps
|
|
5
|
+
* with what it means (BIAN's `…typevalues`, a code list): `values` as
|
|
6
|
+
* `[{ name, description? }]`. Drawn as a box with a row per value.
|
|
7
|
+
*/
|
|
8
|
+
export class EnumerationType extends DataType {
|
|
9
|
+
static kind = 'enumeration';
|
|
10
|
+
static kindLabel = 'Enumeration';
|
|
11
|
+
|
|
12
|
+
/** @param {Object} spec - As DataType, plus `values: [{ name, description? }]` (or plain names) */
|
|
13
|
+
constructor({ values = [], ...spec } = {}) {
|
|
14
|
+
super(spec);
|
|
15
|
+
this.values = values.map(v => (typeof v === 'object' && v ? { name: String(v.name), ...(v.description && { description: v.description }) } : { name: String(v) }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
summary() {
|
|
19
|
+
const shown = this.values.slice(0, 6).map(v => v.name).join(', ');
|
|
20
|
+
return `${this.name}: one of ${shown}${this.values.length > 6 ? `, … (${this.values.length})` : ''}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
rows() {
|
|
24
|
+
return this.values.map(v => ({ name: v.name }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
toJSON() {
|
|
28
|
+
return { ...super.toJSON(), values: this.values.map(v => ({ ...v })) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A plain type's fields, then its values a line each: `name | what it means`. */
|
|
32
|
+
static fields(model, type = null) {
|
|
33
|
+
return [
|
|
34
|
+
...DataType.fields(model, type),
|
|
35
|
+
{
|
|
36
|
+
key: 'values', label: 'Values', type: 'textarea', rows: 6,
|
|
37
|
+
value: (type?.values ?? []).map(v => (v.description ? `${v.name} | ${v.description}` : v.name)).join('\n'),
|
|
38
|
+
hint: 'One a line: value | what it means (optional)',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static specFrom(values) {
|
|
44
|
+
return {
|
|
45
|
+
...super.specFrom(values),
|
|
46
|
+
values: DataType.lines(values.values).map(([name, description]) => ({ name, ...(description && { description }) })),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
DataType.register(EnumerationType);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { DataModelPerspective } from './DataModelPerspective.js';
|
|
2
|
+
import { BianNotation } from '../../bian.js';
|
|
3
|
+
|
|
4
|
+
/** How a service domain's control record and behaviour qualifiers are coloured where it is seen through. */
|
|
5
|
+
const CONTROL_RECORD_COLOR = '#fde68a';
|
|
6
|
+
const QUALIFIER_COLOR = '#e0f2fe';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ServiceDomain — a BIAN service domain as a kind of perspective: it sees the
|
|
10
|
+
* concepts it covers (as any perspective), and it is more. It sits in a business
|
|
11
|
+
* area and business domain, manages one kind of asset (`asset`) with one
|
|
12
|
+
* functional pattern (`pattern`, BianNotation), and keeps a control record (a
|
|
13
|
+
* logical entity: what its instances are, named for the asset and the
|
|
14
|
+
* pattern's generic artifact) broken down by behaviour qualifiers (each a
|
|
15
|
+
* logical entity, of a qualifier type). Its service operations (its semantic
|
|
16
|
+
* API) follow: the pattern's action terms on the control record and each
|
|
17
|
+
* qualifier (BianNotation.serviceOperations).
|
|
18
|
+
*
|
|
19
|
+
* Seen through at the logical level it shows its control record and qualifiers,
|
|
20
|
+
* whatever they realise, marked (colour, and what each is on hover), with a note
|
|
21
|
+
* of its service operations on the control record.
|
|
22
|
+
*/
|
|
23
|
+
export class ServiceDomain extends DataModelPerspective {
|
|
24
|
+
static kind = 'service-domain';
|
|
25
|
+
static kindLabel = 'Service domain';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Object} spec - As DataModelPerspective, plus:
|
|
29
|
+
* @param {string} [spec.businessArea]
|
|
30
|
+
* @param {string} [spec.businessDomain]
|
|
31
|
+
* @param {string} [spec.asset] - What it manages ('Current Account'); its name when unset
|
|
32
|
+
* @param {string} [spec.pattern] - A BIAN functional pattern ('Fulfill')
|
|
33
|
+
* @param {string|null} [spec.controlRecord] - The logical entity its control record is
|
|
34
|
+
* @param {Array<{id?: string, name: string, type?: string, entity?: string}>} [spec.qualifiers]
|
|
35
|
+
* Behaviour qualifiers: a name, a qualifier type (default the pattern's), the logical entity it is
|
|
36
|
+
* @param {string[]} [spec.owns] - Other business objects (logical entities) it owns
|
|
37
|
+
*/
|
|
38
|
+
constructor({ businessArea = '', businessDomain = '', asset = '', pattern = 'Fulfill', controlRecord = null, qualifiers = [], owns = [], ...spec } = {}) {
|
|
39
|
+
super(spec);
|
|
40
|
+
/** Business objects (logical entities) it owns besides its control record and qualifiers. */
|
|
41
|
+
this.owns = [...owns];
|
|
42
|
+
this.businessArea = businessArea;
|
|
43
|
+
this.businessDomain = businessDomain;
|
|
44
|
+
this.asset = asset;
|
|
45
|
+
this.pattern = pattern;
|
|
46
|
+
this.controlRecord = controlRecord;
|
|
47
|
+
this.qualifiers = qualifiers.map((q, i) => ({ ...q, id: q.id ?? `${this.id}-bq-${i + 1}` }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Its functional pattern (BianFunctionalPattern), or null for one BIAN does not have. */
|
|
51
|
+
get functionalPattern() {
|
|
52
|
+
return BianNotation.functionalPattern(this.pattern);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Its control record's name: the asset (else its name) and the pattern's generic artifact. */
|
|
56
|
+
get controlRecordName() {
|
|
57
|
+
return this.functionalPattern?.controlRecordName(this.asset || this.name) ?? (this.asset || this.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A qualifier's type: its own, else its pattern's. */
|
|
61
|
+
qualifierType(qualifier) {
|
|
62
|
+
return qualifier.type || this.functionalPattern?.qualifierType || '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Its default service operations (BianNotation.serviceOperations). */
|
|
66
|
+
serviceOperations() {
|
|
67
|
+
return BianNotation.serviceOperations(this.pattern, {
|
|
68
|
+
serviceDomain: this.name, controlRecord: this.controlRecordName, qualifiers: this.qualifiers.map(q => q.name),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The business objects it owns: its control record, its qualifiers' entities, and those it names (`owns`). */
|
|
73
|
+
ownedEntities() {
|
|
74
|
+
return [...new Set([this.controlRecord, ...this.qualifiers.map(q => q.entity), ...this.owns].filter(Boolean))];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** At the logical level, the business objects it owns. */
|
|
78
|
+
alsoShows(level) {
|
|
79
|
+
return level === 'logical' ? this.ownedEntities() : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** At the logical level, its control record and qualifiers are coloured and say what they are. */
|
|
83
|
+
decorate(entity, level) {
|
|
84
|
+
if (level !== 'logical') return {};
|
|
85
|
+
if (entity.id === this.controlRecord) {
|
|
86
|
+
return { color: CONTROL_RECORD_COLOR, details: { 'control record': this.controlRecordName, 'functional pattern': this.pattern, 'service domain': this.name } };
|
|
87
|
+
}
|
|
88
|
+
const qualifier = this.qualifiers.find(q => q.entity === entity.id);
|
|
89
|
+
return qualifier
|
|
90
|
+
? { color: QUALIFIER_COLOR, details: { 'behaviour qualifier': `${qualifier.name} (${this.qualifierType(qualifier)})`, 'service domain': this.name } }
|
|
91
|
+
: {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** At the logical level, a note of its service operations, on its control record. */
|
|
95
|
+
notes(level) {
|
|
96
|
+
if (level !== 'logical' || !this.controlRecord) return [];
|
|
97
|
+
return [{ id: `${this.id}-operations`, text: this.operationsSummary(), attachTo: [this.controlRecord] }];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Its service operations in a few lines: the terms, on what. */
|
|
101
|
+
operationsSummary() {
|
|
102
|
+
const ops = this.serviceOperations();
|
|
103
|
+
const terms = (pick) => [...new Set(ops.filter(pick).map(o => o.actionTerm))].join(', ');
|
|
104
|
+
const qualifiers = this.qualifiers.map(q => q.name);
|
|
105
|
+
return [
|
|
106
|
+
`${this.name}: ${terms(o => o.target === this.name)}`,
|
|
107
|
+
`${this.controlRecordName}: ${terms(o => o.target === this.controlRecordName && !o.qualifier)}`,
|
|
108
|
+
...(qualifiers.length ? [`each of ${qualifiers.join(', ')}: the same`] : []),
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
toJSON() {
|
|
113
|
+
return {
|
|
114
|
+
...super.toJSON(),
|
|
115
|
+
businessArea: this.businessArea, businessDomain: this.businessDomain, asset: this.asset, pattern: this.pattern,
|
|
116
|
+
controlRecord: this.controlRecord, qualifiers: this.qualifiers.map(q => ({ ...q })), owns: [...this.owns],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Its dialog's fields: a perspective's, then where it sits, its asset and
|
|
122
|
+
* pattern (BIAN's, each described), its control record (a logical entity),
|
|
123
|
+
* and its qualifiers a line each: `name | type | logical entity`.
|
|
124
|
+
*/
|
|
125
|
+
static fields(model, domain = null) {
|
|
126
|
+
const logical = model.level('logical').entities;
|
|
127
|
+
const entityName = (id) => logical.find(e => e.id === id)?.name ?? id ?? '';
|
|
128
|
+
return [
|
|
129
|
+
...DataModelPerspective.fields(model, domain),
|
|
130
|
+
{ type: 'heading', label: 'Service domain' },
|
|
131
|
+
{ key: 'businessArea', label: 'Business area', type: 'text', value: domain?.businessArea ?? '' },
|
|
132
|
+
{ key: 'businessDomain', label: 'Business domain', type: 'text', value: domain?.businessDomain ?? '' },
|
|
133
|
+
{ key: 'asset', label: 'Asset it manages', type: 'text', value: domain?.asset ?? '', placeholder: 'its name' },
|
|
134
|
+
{
|
|
135
|
+
key: 'pattern', label: 'Functional pattern', type: 'select', value: domain?.pattern ?? 'Fulfill',
|
|
136
|
+
choices: BianNotation.functionalPatterns().map(p => [p.name, `${p.name} — ${p.artifact}`]),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: 'controlRecord', label: 'Control record', type: 'select', value: domain?.controlRecord ?? '',
|
|
140
|
+
choices: [['', 'None yet'], ...logical.map(e => [e.id, e.name])],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
key: 'qualifiers', label: 'Behaviour qualifiers', type: 'textarea', rows: 4,
|
|
144
|
+
value: (domain?.qualifiers ?? []).map(q => [q.name, q.type ?? '', entityName(q.entity)].join(' | ')).join('\n'),
|
|
145
|
+
hint: `One a line: name | type (${BianNotation.qualifierTypes().slice(0, 4).join(', ')}…; blank: the pattern's) | logical entity (by name)`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: 'owns', label: 'Also owns', type: 'checklist', value: domain?.owns ?? [],
|
|
149
|
+
choices: logical.map(e => [e.id, e.name]),
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A service domain's spec from those fields (a qualifier kept keeps its id; a type BIAN lacks is left to the pattern). */
|
|
155
|
+
static specFrom(values, model, domain = null) {
|
|
156
|
+
const logical = model.level('logical').entities;
|
|
157
|
+
const entityOf = (word) => logical.find(e => e.id === word || e.name.toLowerCase() === word.toLowerCase())?.id ?? null;
|
|
158
|
+
const qualifiers = String(values.qualifiers ?? '').split('\n').map(l => l.trim()).filter(Boolean).map(line => {
|
|
159
|
+
const [name, type = '', entity = ''] = line.split('|').map(p => p.trim());
|
|
160
|
+
const kept = domain?.qualifiers.find(q => q.name === name);
|
|
161
|
+
return {
|
|
162
|
+
...(kept && { id: kept.id }), name,
|
|
163
|
+
...(BianNotation.isQualifierType(type) && { type }),
|
|
164
|
+
...(entityOf(entity) && { entity: entityOf(entity) }),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
kind: ServiceDomain.kind,
|
|
169
|
+
...DataModelPerspective.specFrom(values, model),
|
|
170
|
+
businessArea: (values.businessArea ?? '').trim(), businessDomain: (values.businessDomain ?? '').trim(),
|
|
171
|
+
asset: (values.asset ?? '').trim(), pattern: values.pattern || 'Fulfill',
|
|
172
|
+
controlRecord: values.controlRecord || null, qualifiers, owns: [...(values.owns ?? [])],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
DataModelPerspective.register(ServiceDomain);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { BianNotation } from '../../bian.js';
|
|
2
|
+
|
|
3
|
+
/** The colour of a service domain's box, by its functional pattern's place among BIAN's (none: grey). */
|
|
4
|
+
const PATTERN_COLORS = ['#fde68a', '#bbf7d0', '#bfdbfe', '#fbcfe8', '#ddd6fe', '#fed7aa', '#a5f3fc', '#e9d5ff', '#fecaca', '#d9f99d'];
|
|
5
|
+
const NO_PATTERN_COLOR = '#e5e7eb';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ServiceLandscape — a model's service domains laid out as BIAN draws its
|
|
9
|
+
* service landscape: a column per business area, in it a frame per business
|
|
10
|
+
* domain, in that the service domains (each a box coloured by its functional
|
|
11
|
+
* pattern, and on hover its pattern, control record and asset). Service
|
|
12
|
+
* domains with no area or domain go under "No business area" / "No business
|
|
13
|
+
* domain". The layout follows from the structure, so it is made anew each time.
|
|
14
|
+
*
|
|
15
|
+
* `view(serviceDomains)` gives what dataModelDiagram draws: entities (a box per
|
|
16
|
+
* service domain, its id `landscape:<id>`), and the frames as graphDiagram
|
|
17
|
+
* folders (`folders`, `elementFolders`).
|
|
18
|
+
*/
|
|
19
|
+
export class ServiceLandscape {
|
|
20
|
+
static BOX = Object.freeze({ width: 160, height: 56 });
|
|
21
|
+
static GAP = 16;
|
|
22
|
+
static HEADER = 24;
|
|
23
|
+
static PAD = 12;
|
|
24
|
+
/** Service domains side by side in a business domain's frame. */
|
|
25
|
+
static ACROSS = 2;
|
|
26
|
+
static NO_AREA = 'No business area';
|
|
27
|
+
static NO_DOMAIN = 'No business domain';
|
|
28
|
+
static PREFIX = 'landscape:';
|
|
29
|
+
|
|
30
|
+
/** A service domain's id from a landscape box's id, or null. */
|
|
31
|
+
static serviceDomainOf(nodeId) {
|
|
32
|
+
return String(nodeId).startsWith(ServiceLandscape.PREFIX) ? String(nodeId).slice(ServiceLandscape.PREFIX.length) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The colour of a functional pattern's service domains. */
|
|
36
|
+
static colorOf(pattern) {
|
|
37
|
+
const i = BianNotation.functionalPatterns().findIndex(p => p.name === pattern);
|
|
38
|
+
return i < 0 ? NO_PATTERN_COLOR : PATTERN_COLORS[i % PATTERN_COLORS.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Business areas, each with its business domains, each with its service
|
|
43
|
+
* domains, in the order they first come.
|
|
44
|
+
* @param {Array<ServiceDomain>} serviceDomains
|
|
45
|
+
* @returns {Array<{name: string, domains: Array<{name: string, serviceDomains: Array}>}>}
|
|
46
|
+
*/
|
|
47
|
+
static areas(serviceDomains) {
|
|
48
|
+
const areas = new Map();
|
|
49
|
+
for (const sd of serviceDomains) {
|
|
50
|
+
const area = sd.businessArea || ServiceLandscape.NO_AREA;
|
|
51
|
+
const domain = sd.businessDomain || ServiceLandscape.NO_DOMAIN;
|
|
52
|
+
if (!areas.has(area)) areas.set(area, new Map());
|
|
53
|
+
const domains = areas.get(area);
|
|
54
|
+
if (!domains.has(domain)) domains.set(domain, []);
|
|
55
|
+
domains.get(domain).push(sd);
|
|
56
|
+
}
|
|
57
|
+
return [...areas].map(([name, domains]) => ({ name, domains: [...domains].map(([d, list]) => ({ name: d, serviceDomains: list })) }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* What to draw (see the class).
|
|
62
|
+
* @param {Array<ServiceDomain>} serviceDomains
|
|
63
|
+
* @returns {{entities, relationships, annotations, links, folders, elementFolders}}
|
|
64
|
+
*/
|
|
65
|
+
static view(serviceDomains) {
|
|
66
|
+
const { BOX, GAP, HEADER, PAD, ACROSS, PREFIX } = ServiceLandscape;
|
|
67
|
+
const domainWidth = ACROSS * BOX.width + (ACROSS - 1) * GAP + 2 * PAD;
|
|
68
|
+
const areaWidth = domainWidth + 2 * PAD;
|
|
69
|
+
const entities = [], areaFolders = [], domainFolders = [], elementFolders = [];
|
|
70
|
+
ServiceLandscape.areas(serviceDomains).forEach((area, a) => {
|
|
71
|
+
const areaId = `landscape-area-${a + 1}`;
|
|
72
|
+
const x = 20 + a * (areaWidth + GAP);
|
|
73
|
+
let y = 20 + HEADER + PAD;
|
|
74
|
+
area.domains.forEach((domain, d) => {
|
|
75
|
+
const domainId = `${areaId}-domain-${d + 1}`;
|
|
76
|
+
const rows = Math.ceil(domain.serviceDomains.length / ACROSS);
|
|
77
|
+
const height = HEADER + PAD + rows * BOX.height + (rows - 1) * GAP + PAD;
|
|
78
|
+
domainFolders.push({ id: domainId, name: domain.name, parentId: areaId, x: x + PAD, y, width: domainWidth, height });
|
|
79
|
+
domain.serviceDomains.forEach((sd, i) => {
|
|
80
|
+
const id = `${PREFIX}${sd.id}`;
|
|
81
|
+
const pattern = sd.functionalPattern;
|
|
82
|
+
entities.push({
|
|
83
|
+
id, name: sd.name, level: 'landscape',
|
|
84
|
+
x: x + 2 * PAD + (i % ACROSS) * (BOX.width + GAP) + BOX.width / 2,
|
|
85
|
+
y: y + HEADER + PAD + Math.floor(i / ACROSS) * (BOX.height + GAP) + BOX.height / 2,
|
|
86
|
+
width: BOX.width, height: BOX.height,
|
|
87
|
+
color: ServiceLandscape.colorOf(sd.pattern),
|
|
88
|
+
description: sd.description,
|
|
89
|
+
details: {
|
|
90
|
+
'functional pattern': pattern ? `${pattern.name} (${pattern.artifact})` : (sd.pattern || 'none'),
|
|
91
|
+
'control record': sd.controlRecordName,
|
|
92
|
+
...(sd.asset && { asset: sd.asset }),
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
elementFolders.push([id, domainId]);
|
|
96
|
+
});
|
|
97
|
+
y += height + GAP;
|
|
98
|
+
});
|
|
99
|
+
areaFolders.push({ id: areaId, name: area.name, x, y: 20, width: areaWidth, height: y - 20 - GAP + PAD });
|
|
100
|
+
});
|
|
101
|
+
// Areas first, so they are drawn behind their domains.
|
|
102
|
+
return { entities, relationships: [], annotations: [], links: [], folders: [...areaFolders, ...domainFolders], elementFolders };
|
|
103
|
+
}
|
|
104
|
+
}
|