@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.
Files changed (78) hide show
  1. package/README.md +70 -9
  2. package/REFERENCE.md +303 -2
  3. package/dist/renderengine-workspace.umd.js +8851 -3305
  4. package/dist/renderengine.umd.js +5621 -1382
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +251 -19
  7. package/package.json +1 -1
  8. package/src/bian.js +182 -0
  9. package/src/charting/DiagramEditor.js +5 -26
  10. package/src/charting/DiagramTypeRegistry.js +8 -2
  11. package/src/charting/FormDialog.js +398 -385
  12. package/src/charting/ModelCheckDialog.js +63 -0
  13. package/src/charting/ModelChecker.js +26 -0
  14. package/src/charting/RowsField.js +127 -0
  15. package/src/charting/ViewTabStrip.js +94 -81
  16. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  17. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  18. package/src/charting/datamodel/ClassificationLens.js +33 -0
  19. package/src/charting/datamodel/ClassificationScheme.js +98 -0
  20. package/src/charting/datamodel/DataModel.js +574 -12
  21. package/src/charting/datamodel/DataModelChecker.js +338 -0
  22. package/src/charting/datamodel/DataModelCompared.js +127 -0
  23. package/src/charting/datamodel/DataModelContext.js +40 -10
  24. package/src/charting/datamodel/DataModelEditCommand.js +47 -0
  25. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  26. package/src/charting/datamodel/DataModelPerspective.js +55 -0
  27. package/src/charting/datamodel/DataType.js +108 -0
  28. package/src/charting/datamodel/EnumerationType.js +64 -0
  29. package/src/charting/datamodel/ServiceDomain.js +186 -0
  30. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  31. package/src/charting/datamodel/SqlDdl.js +80 -0
  32. package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
  33. package/src/charting/datamodel/StructuredType.js +63 -0
  34. package/src/charting/datamodel/index.js +21 -5
  35. package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
  36. package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
  37. package/src/charting/datamodel/naming/NameStyle.js +57 -0
  38. package/src/charting/datamodel/naming/NamingStandard.js +132 -0
  39. package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
  40. package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
  41. package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
  42. package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
  43. package/src/charting/datamodel/naming/index.js +8 -0
  44. package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
  45. package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
  46. package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
  47. package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
  48. package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
  49. package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
  50. package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
  51. package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
  52. package/src/charting/datamodel/scorecard/index.js +8 -0
  53. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  54. package/src/charting/diagramModel/OpenViewList.js +75 -0
  55. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  56. package/src/charting/diagrams/DataModelDiagram.js +4 -11
  57. package/src/charting/diagrams/DataModelRelationshipStrategy.js +19 -5
  58. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  59. package/src/charting/diagrams/GraphDiagram.js +11 -1
  60. package/src/charting/diagrams/NodeGeometry.js +173 -158
  61. package/src/charting/glyphs/PersonGlyph.js +24 -23
  62. package/src/charting/toolbar/ToolbarSelect.js +6 -0
  63. package/src/datamodel.js +402 -4
  64. package/src/index.js +6 -4
  65. package/src/workspace/EditorShell.js +166 -67
  66. package/src/workspace/PanelLayout.js +56 -0
  67. package/src/workspace/layout/TabsNode.js +205 -191
  68. package/src/workspace/parts/ArchimateCanvasPart.js +5 -7
  69. package/src/workspace/parts/DataModelPart.js +534 -33
  70. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  71. package/src/workspace/parts/EditorPart.js +461 -407
  72. package/src/workspace/parts/ModelFile.js +216 -187
  73. package/src/workspace/parts/ModelsPart.js +483 -192
  74. package/src/workspace/storage/BackupFolder.js +145 -0
  75. package/src/workspace/storage/KeyValueStore.js +92 -0
  76. package/src/workspace/storage/ModelLibrary.js +65 -0
  77. package/src/workspace/workArea.js +417 -413
  78. 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
+ }