@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
|
@@ -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,36 @@ 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 (the model given): `{ color?, details? }` (nothing here). */
|
|
71
|
+
decorate(entity, level, model) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Forget an entity removed from a level: at the conceptual level, the
|
|
77
|
+
* concepts it shows and the names it gives them.
|
|
78
|
+
*/
|
|
79
|
+
forgetEntity(level, id) {
|
|
80
|
+
if (level !== 'conceptual') return;
|
|
81
|
+
if (this.entities) this.entities = this.entities.filter(e => e !== id);
|
|
82
|
+
delete this.names[id];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The business objects (logical entities) it owns, as a BIAN service domain does (none here). */
|
|
86
|
+
ownedEntities() {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Notes it adds at a level: `[{ id, text, attachTo }]` (none here). */
|
|
91
|
+
notes(level) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
41
95
|
/** What a relationship is called here. */
|
|
42
96
|
relationshipNameOf(relationship) {
|
|
43
97
|
return (relationship.id && this.relationshipNames[relationship.id]) ?? relationship.name;
|
|
@@ -76,6 +130,7 @@ export class DataModelPerspective {
|
|
|
76
130
|
|
|
77
131
|
toJSON() {
|
|
78
132
|
return {
|
|
133
|
+
...(this.constructor.kind !== DataModelPerspective.kind && { kind: this.constructor.kind }),
|
|
79
134
|
id: this.id, name: this.name, description: this.description,
|
|
80
135
|
entities: this.entities ? [...this.entities] : null,
|
|
81
136
|
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,64 @@
|
|
|
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? }]`. As reference data (DMBOK2) a code set also says
|
|
7
|
+
* where it comes from (`source`: ISO 4217, a regulator, the bank), who keeps it
|
|
8
|
+
* (`owner`) and which version it is (`version`). Drawn as a box with a row per value.
|
|
9
|
+
*/
|
|
10
|
+
export class EnumerationType extends DataType {
|
|
11
|
+
static kind = 'enumeration';
|
|
12
|
+
static kindLabel = 'Enumeration';
|
|
13
|
+
|
|
14
|
+
/** @param {Object} spec - As DataType, plus `values: [{ name, description? }]` (or plain names) */
|
|
15
|
+
constructor({ values = [], source = '', owner = '', version = '', ...spec } = {}) {
|
|
16
|
+
super(spec);
|
|
17
|
+
Object.assign(this, { source, owner, version });
|
|
18
|
+
this.values = values.map(v => (typeof v === 'object' && v ? { name: String(v.name), ...(v.description && { description: v.description }) } : { name: String(v) }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
summary() {
|
|
22
|
+
const shown = this.values.slice(0, 6).map(v => v.name).join(', ');
|
|
23
|
+
return `${this.name}: one of ${shown}${this.values.length > 6 ? `, … (${this.values.length})` : ''}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
rows() {
|
|
27
|
+
return this.values.map(v => ({ name: v.name }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Where it comes from, who keeps it, which version: what is said of them (none: {}). */
|
|
31
|
+
get codeSet() {
|
|
32
|
+
return Object.fromEntries([['source', this.source], ['owner', this.owner], ['version', this.version]].filter(([, v]) => v));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toJSON() {
|
|
36
|
+
return { ...super.toJSON(), ...this.codeSet, values: this.values.map(v => ({ ...v })) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A plain type's fields, then its values a line each: `name | what it means`. */
|
|
40
|
+
static fields(model, type = null) {
|
|
41
|
+
return [
|
|
42
|
+
...DataType.fields(model, type),
|
|
43
|
+
{ key: 'source', label: 'Source', type: 'text', value: type?.source ?? '', placeholder: 'ISO 4217, the regulator, the bank' },
|
|
44
|
+
{ key: 'owner', label: 'Owner', type: 'text', value: type?.owner ?? '' },
|
|
45
|
+
{ key: 'version', label: 'Version', type: 'text', value: type?.version ?? '' },
|
|
46
|
+
{
|
|
47
|
+
key: 'values', label: 'Values', type: 'textarea', rows: 6,
|
|
48
|
+
value: (type?.values ?? []).map(v => (v.description ? `${v.name} | ${v.description}` : v.name)).join('\n'),
|
|
49
|
+
hint: 'One a line: value | what it means (optional)',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static specFrom(values) {
|
|
55
|
+
const said = (key) => String(values[key] ?? '').trim();
|
|
56
|
+
return {
|
|
57
|
+
...super.specFrom(values),
|
|
58
|
+
...Object.fromEntries(['source', 'owner', 'version'].map(k => [k, said(k)]).filter(([, v]) => v)),
|
|
59
|
+
values: DataType.lines(values.values).map(([name, description]) => ({ name, ...(description && { description }) })),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
DataType.register(EnumerationType);
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
/** As a perspective, and at the logical level its control record, its qualifiers' entities and what it owns. */
|
|
73
|
+
forgetEntity(level, id) {
|
|
74
|
+
super.forgetEntity(level, id);
|
|
75
|
+
if (level !== 'logical') return;
|
|
76
|
+
if (this.controlRecord === id) this.controlRecord = null;
|
|
77
|
+
this.qualifiers = this.qualifiers.map(q => (q.entity === id ? (({ entity, ...rest }) => rest)(q) : q));
|
|
78
|
+
this.owns = this.owns.filter(e => e !== id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The business objects it owns: its control record, its qualifiers' entities, and those it names (`owns`). */
|
|
82
|
+
ownedEntities() {
|
|
83
|
+
return [...new Set([this.controlRecord, ...this.qualifiers.map(q => q.entity), ...this.owns].filter(Boolean))];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** At the logical level, the business objects it owns. */
|
|
87
|
+
alsoShows(level) {
|
|
88
|
+
return level === 'logical' ? this.ownedEntities() : [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** At the logical level, its control record and qualifiers are coloured and say what they are. */
|
|
92
|
+
decorate(entity, level) {
|
|
93
|
+
if (level !== 'logical') return {};
|
|
94
|
+
if (entity.id === this.controlRecord) {
|
|
95
|
+
return { color: CONTROL_RECORD_COLOR, details: { 'control record': this.controlRecordName, 'functional pattern': this.pattern, 'service domain': this.name } };
|
|
96
|
+
}
|
|
97
|
+
const qualifier = this.qualifiers.find(q => q.entity === entity.id);
|
|
98
|
+
return qualifier
|
|
99
|
+
? { color: QUALIFIER_COLOR, details: { 'behaviour qualifier': `${qualifier.name} (${this.qualifierType(qualifier)})`, 'service domain': this.name } }
|
|
100
|
+
: {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** At the logical level, a note of its service operations, on its control record. */
|
|
104
|
+
notes(level) {
|
|
105
|
+
if (level !== 'logical' || !this.controlRecord) return [];
|
|
106
|
+
return [{ id: `${this.id}-operations`, text: this.operationsSummary(), attachTo: [this.controlRecord] }];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Its service operations in a few lines: the terms, on what. */
|
|
110
|
+
operationsSummary() {
|
|
111
|
+
const ops = this.serviceOperations();
|
|
112
|
+
const terms = (pick) => [...new Set(ops.filter(pick).map(o => o.actionTerm))].join(', ');
|
|
113
|
+
const qualifiers = this.qualifiers.map(q => q.name);
|
|
114
|
+
return [
|
|
115
|
+
`${this.name}: ${terms(o => o.target === this.name)}`,
|
|
116
|
+
`${this.controlRecordName}: ${terms(o => o.target === this.controlRecordName && !o.qualifier)}`,
|
|
117
|
+
...(qualifiers.length ? [`each of ${qualifiers.join(', ')}: the same`] : []),
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
toJSON() {
|
|
122
|
+
return {
|
|
123
|
+
...super.toJSON(),
|
|
124
|
+
businessArea: this.businessArea, businessDomain: this.businessDomain, asset: this.asset, pattern: this.pattern,
|
|
125
|
+
controlRecord: this.controlRecord, qualifiers: this.qualifiers.map(q => ({ ...q })), owns: [...this.owns],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Its dialog's fields: a perspective's, then where it sits, its asset and
|
|
131
|
+
* pattern (BIAN's, each described), its control record (a logical entity),
|
|
132
|
+
* and its qualifiers a line each: `name | type | logical entity`.
|
|
133
|
+
*/
|
|
134
|
+
static fields(model, domain = null) {
|
|
135
|
+
const logical = model.level('logical').entities;
|
|
136
|
+
const entityName = (id) => logical.find(e => e.id === id)?.name ?? id ?? '';
|
|
137
|
+
return [
|
|
138
|
+
...DataModelPerspective.fields(model, domain),
|
|
139
|
+
{ type: 'heading', label: 'Service domain' },
|
|
140
|
+
{ key: 'businessArea', label: 'Business area', type: 'text', value: domain?.businessArea ?? '' },
|
|
141
|
+
{ key: 'businessDomain', label: 'Business domain', type: 'text', value: domain?.businessDomain ?? '' },
|
|
142
|
+
{ key: 'asset', label: 'Asset it manages', type: 'text', value: domain?.asset ?? '', placeholder: 'its name' },
|
|
143
|
+
{
|
|
144
|
+
key: 'pattern', label: 'Functional pattern', type: 'select', value: domain?.pattern ?? 'Fulfill',
|
|
145
|
+
choices: BianNotation.functionalPatterns().map(p => [p.name, `${p.name} — ${p.artifact}`]),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: 'controlRecord', label: 'Control record', type: 'select', value: domain?.controlRecord ?? '',
|
|
149
|
+
choices: [['', 'None yet'], ...logical.map(e => [e.id, e.name])],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: 'qualifiers', label: 'Behaviour qualifiers', type: 'textarea', rows: 4,
|
|
153
|
+
value: (domain?.qualifiers ?? []).map(q => [q.name, q.type ?? '', entityName(q.entity)].join(' | ')).join('\n'),
|
|
154
|
+
hint: `One a line: name | type (${BianNotation.qualifierTypes().slice(0, 4).join(', ')}…; blank: the pattern's) | logical entity (by name)`,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'owns', label: 'Also owns', type: 'checklist', value: domain?.owns ?? [],
|
|
158
|
+
choices: logical.map(e => [e.id, e.name]),
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A service domain's spec from those fields (a qualifier kept keeps its id; a type BIAN lacks is left to the pattern). */
|
|
164
|
+
static specFrom(values, model, domain = null) {
|
|
165
|
+
const logical = model.level('logical').entities;
|
|
166
|
+
const entityOf = (word) => logical.find(e => e.id === word || e.name.toLowerCase() === word.toLowerCase())?.id ?? null;
|
|
167
|
+
const qualifiers = String(values.qualifiers ?? '').split('\n').map(l => l.trim()).filter(Boolean).map(line => {
|
|
168
|
+
const [name, type = '', entity = ''] = line.split('|').map(p => p.trim());
|
|
169
|
+
const kept = domain?.qualifiers.find(q => q.name === name);
|
|
170
|
+
return {
|
|
171
|
+
...(kept && { id: kept.id }), name,
|
|
172
|
+
...(BianNotation.isQualifierType(type) && { type }),
|
|
173
|
+
...(entityOf(entity) && { entity: entityOf(entity) }),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
kind: ServiceDomain.kind,
|
|
178
|
+
...DataModelPerspective.specFrom(values, model),
|
|
179
|
+
businessArea: (values.businessArea ?? '').trim(), businessDomain: (values.businessDomain ?? '').trim(),
|
|
180
|
+
asset: (values.asset ?? '').trim(), pattern: values.pattern || 'Fulfill',
|
|
181
|
+
controlRecord: values.controlRecord || null, qualifiers, owns: [...(values.owns ?? [])],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
/** A text as an SQL string literal. */
|
|
31
|
+
static literal(text) {
|
|
32
|
+
return `'${String(text).replace(/'/g, "''")}'`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The DDL for a model's physical level. @returns {string} */
|
|
36
|
+
write(model) {
|
|
37
|
+
const schema = model.level('physical');
|
|
38
|
+
const tables = schema.entities;
|
|
39
|
+
const find = (ref) => {
|
|
40
|
+
const [table, column] = String(ref).split('.').map(s => s.trim());
|
|
41
|
+
const target = schema.entity(table) ?? tables.find(t => t.name.toLowerCase() === table.toLowerCase());
|
|
42
|
+
return target ? { target, column } : null;
|
|
43
|
+
};
|
|
44
|
+
const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
|
|
45
|
+
for (const t of tables) {
|
|
46
|
+
const columns = t.attributes ?? [];
|
|
47
|
+
const keys = columns.filter(c => c.isPK).map(c => SqlDdl.name(c.name));
|
|
48
|
+
const lines = [
|
|
49
|
+
...columns.map(c => ` ${SqlDdl.name(c.name)} ${c.type || this.defaultType}${c.required || c.isPK ? ' NOT NULL' : ''}`),
|
|
50
|
+
...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
|
|
51
|
+
...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
|
|
52
|
+
];
|
|
53
|
+
out.push(`CREATE TABLE ${SqlDdl.name(t.name)} (`, lines.join(',\n'), ');', '');
|
|
54
|
+
}
|
|
55
|
+
for (const t of tables) {
|
|
56
|
+
for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
|
|
57
|
+
const ref = find(c.references);
|
|
58
|
+
if (!ref) {
|
|
59
|
+
out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const theirs = ref.column ? [ref.column] : (ref.target.attributes ?? []).filter(a => a.isPK).map(a => a.name);
|
|
63
|
+
if (!theirs.length) {
|
|
64
|
+
out.push(`-- ${t.name}.${c.name} references ${ref.target.name}, which has no primary key`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out.push(`ALTER TABLE ${SqlDdl.name(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
68
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${SqlDdl.name(ref.target.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (this.comments) {
|
|
72
|
+
const notes = tables.flatMap(t => [
|
|
73
|
+
...(t.description ? [`COMMENT ON TABLE ${SqlDdl.name(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
|
|
74
|
+
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${SqlDdl.name(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
|
|
75
|
+
]);
|
|
76
|
+
if (notes.length) out.push('', ...notes);
|
|
77
|
+
}
|
|
78
|
+
return `${out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ModelFileFormat } from '../ModelFileFormat.js';
|
|
2
|
+
import { SqlDdl } from './SqlDdl.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SqlDdlFileFormat — a data model's physical level as SQL DDL (SqlDdl), a file
|
|
6
|
+
* to export; DDL is not read back.
|
|
7
|
+
*/
|
|
8
|
+
export class SqlDdlFileFormat extends ModelFileFormat {
|
|
9
|
+
/** @param {Object} [options] - SqlDdl's */
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
super();
|
|
12
|
+
this.ddl = new SqlDdl(options);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get label() { return 'SQL DDL'; }
|
|
16
|
+
get accept() { return '.sql'; }
|
|
17
|
+
get fileName() { return 'model.sql'; }
|
|
18
|
+
get imports() { return false; }
|
|
19
|
+
|
|
20
|
+
export(model) {
|
|
21
|
+
return { data: this.ddl.write(model), mimeType: 'application/sql' };
|
|
22
|
+
}
|
|
23
|
+
}
|