@peter.prib/renderengine 0.2.5 → 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.
Files changed (67) hide show
  1. package/README.md +219 -213
  2. package/REFERENCE.md +272 -6
  3. package/dist/renderengine-workspace.umd.js +11980 -7421
  4. package/dist/renderengine.umd.js +8991 -5784
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +534 -0
  7. package/package.json +70 -68
  8. package/src/bian.js +182 -0
  9. package/src/charting/CheckboxTreeSelector.js +373 -304
  10. package/src/charting/DiagramEditor.js +53 -32
  11. package/src/charting/DiagramTypeRegistry.js +13 -3
  12. package/src/charting/ModelCheckDialog.js +63 -0
  13. package/src/charting/ModelChecker.js +26 -0
  14. package/src/charting/ViewTabStrip.js +94 -81
  15. package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
  16. package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
  17. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  18. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  19. package/src/charting/datamodel/DataModel.js +645 -0
  20. package/src/charting/datamodel/DataModelChecker.js +92 -0
  21. package/src/charting/datamodel/DataModelCompared.js +127 -0
  22. package/src/charting/datamodel/DataModelContext.js +108 -0
  23. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  24. package/src/charting/datamodel/DataModelPerspective.js +129 -0
  25. package/src/charting/datamodel/DataType.js +108 -0
  26. package/src/charting/datamodel/EnumerationType.js +51 -0
  27. package/src/charting/datamodel/LevelMapping.js +38 -0
  28. package/src/charting/datamodel/LevelSchema.js +41 -0
  29. package/src/charting/datamodel/ServiceDomain.js +177 -0
  30. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  31. package/src/charting/datamodel/StructuredType.js +63 -0
  32. package/src/charting/datamodel/index.js +15 -0
  33. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  34. package/src/charting/diagramModel/OpenViewList.js +75 -0
  35. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  36. package/src/charting/diagrams/DataModelDiagram.js +160 -86
  37. package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
  38. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  39. package/src/charting/diagrams/GraphDiagram.js +11 -1
  40. package/src/charting/diagrams/NodeGeometry.js +169 -158
  41. package/src/charting/diagrams/nesting.js +31 -0
  42. package/src/charting/glyphs/PersonGlyph.js +24 -23
  43. package/src/charting/modelTree/ModelTree.js +202 -164
  44. package/src/charting/modelTree/ModelTreePanel.js +36 -3
  45. package/src/charting/modelTree/TreeViewMode.js +131 -110
  46. package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
  47. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
  48. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
  49. package/src/datamodel.js +132 -122
  50. package/src/diagramCatalog.js +35 -30
  51. package/src/index.js +2666 -2659
  52. package/src/workspace/EditorShell.js +166 -67
  53. package/src/workspace/PanelLayout.js +56 -0
  54. package/src/workspace/index.js +1 -0
  55. package/src/workspace/layout/TabsNode.js +205 -191
  56. package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
  57. package/src/workspace/parts/ChartPart.js +6 -1
  58. package/src/workspace/parts/DataModelPart.js +544 -0
  59. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  60. package/src/workspace/parts/EditorPart.js +461 -407
  61. package/src/workspace/parts/ModelFile.js +216 -187
  62. package/src/workspace/parts/ModelsPart.js +483 -192
  63. package/src/workspace/storage/BackupFolder.js +145 -0
  64. package/src/workspace/storage/KeyValueStore.js +92 -0
  65. package/src/workspace/storage/ModelLibrary.js +65 -0
  66. package/src/workspace/workArea.js +417 -413
  67. package/src/yaml/YamlReader.js +372 -0
@@ -0,0 +1,92 @@
1
+ import { ModelChecker } from '../ModelChecker.js';
2
+ import { BianNotation } from '../../bian.js';
3
+
4
+ /**
5
+ * DataModelChecker — checks over a DataModel (a ModelChecker): each business
6
+ * object (a logical entity) owned by one service domain, as BIAN has it
7
+ * (none, once there are service domains, is worth a look; more than one is
8
+ * wrong); service domains complete (a control record, qualifiers that are
9
+ * entities, a pattern BIAN has); mappings joining entities that are there;
10
+ * data types sound (an enumeration with values, each once; one name a type)
11
+ * and used. Each finding names what it is about (`kind`: 'entity',
12
+ * 'perspective', 'mapping', 'data-type') and, for an entity, its `level`.
13
+ */
14
+ export class DataModelChecker extends ModelChecker {
15
+ checks() {
16
+ return [this.ownedTwice, this.unowned, this.serviceDomains, this.mappings, this.dataTypes];
17
+ }
18
+
19
+ /** The service domains (perspectives that own business objects). @private */
20
+ _owners() {
21
+ return this.model.perspectives.filter(p => p.ownedEntities().length);
22
+ }
23
+
24
+ /** A business object owned by more than one service domain. */
25
+ ownedTwice() {
26
+ return this.model.level('logical').entities
27
+ .map(e => [e, this.model.ownersOf(e.id)])
28
+ .filter(([, owners]) => owners.length > 1)
29
+ .map(([e, owners]) => ({
30
+ code: 'owned-twice', severity: 'warning', kind: 'entity', level: 'logical', ids: [e.id],
31
+ message: `"${e.name}" is owned by ${owners.length} service domains (${owners.map(o => o.name).join(', ')}); BIAN gives a business object one`,
32
+ }));
33
+ }
34
+
35
+ /** A business object no service domain owns, once some own business objects. */
36
+ unowned() {
37
+ if (!this._owners().length) return [];
38
+ return this.model.level('logical').entities
39
+ .filter(e => !this.model.ownersOf(e.id).length)
40
+ .map(e => ({
41
+ code: 'unowned', severity: 'info', kind: 'entity', level: 'logical', ids: [e.id],
42
+ message: `"${e.name}" is owned by no service domain`,
43
+ }));
44
+ }
45
+
46
+ /** A service domain with no control record, a pattern BIAN lacks, or a qualifier that is no entity. */
47
+ serviceDomains() {
48
+ const logical = this.model.level('logical');
49
+ return this._serviceDomains().flatMap(sd => [
50
+ ...(!sd.controlRecord ? [{ code: 'no-control-record', severity: 'warning', message: `Service domain "${sd.name}" has no control record` }]
51
+ : !logical.entity(sd.controlRecord) ? [{ code: 'control-record-missing', severity: 'warning', message: `Service domain "${sd.name}": its control record "${sd.controlRecord}" is no logical entity` }]
52
+ : []),
53
+ ...(!BianNotation.functionalPattern(sd.pattern) ? [{ code: 'unknown-pattern', severity: 'warning', message: `Service domain "${sd.name}": "${sd.pattern}" is no BIAN functional pattern` }] : []),
54
+ ...sd.qualifiers.filter(q => !q.entity || !logical.entity(q.entity)).map(q => ({
55
+ code: 'qualifier-entity-missing', severity: 'info', message: `Service domain "${sd.name}": behaviour qualifier "${q.name}" is no logical entity`,
56
+ })),
57
+ ].map(f => ({ ...f, kind: 'perspective', ids: [sd.id] })));
58
+ }
59
+
60
+ /** The service domains among the perspectives. @private */
61
+ _serviceDomains() {
62
+ return this.model.perspectives.filter(p => p.constructor.kind === 'service-domain');
63
+ }
64
+
65
+ /** An enumeration with no values or a value twice, two data types of one name, a data type nothing uses. */
66
+ dataTypes() {
67
+ const types = this.model.dataTypes;
68
+ const finding = (t, code, severity, message) => ({ code, severity, kind: 'data-type', ids: [t.id], message });
69
+ return types.flatMap(t => {
70
+ const values = (t.values ?? []).map(v => v.name);
71
+ const twice = [...new Set(values.filter((v, i) => values.indexOf(v) !== i))];
72
+ const sameName = types.filter(o => o !== t && o.name.toLowerCase() === t.name.toLowerCase());
73
+ const { attributes, types: users } = this.model.usesOf(t.id);
74
+ return [
75
+ ...(t.values && !values.length ? [finding(t, 'enumeration-empty', 'warning', `Enumeration "${t.name}" has no values`)] : []),
76
+ ...(twice.length ? [finding(t, 'value-twice', 'warning', `Enumeration "${t.name}" has ${twice.map(v => `"${v}"`).join(', ')} more than once`)] : []),
77
+ ...(sameName.length && types.indexOf(t) < types.indexOf(sameName[0]) ? [finding(t, 'type-name-twice', 'warning', `${sameName.length + 1} data types are called "${t.name}": an attribute naming it gets the first`)] : []),
78
+ ...(!attributes.length && !users.length ? [finding(t, 'type-unused', 'info', `Data type "${t.name}" is used by no attribute or other data type`)] : []),
79
+ ];
80
+ });
81
+ }
82
+
83
+ /** A mapping pair joining an entity that is not there. */
84
+ mappings() {
85
+ return this.model.mappings.flatMap(m => m.pairs
86
+ .filter(([a, b]) => !this.model.level(m.from).entity(a) || !this.model.level(m.to).entity(b))
87
+ .map(([a, b]) => ({
88
+ code: 'mapping-dangling', severity: 'warning', kind: 'mapping', ids: [`${m.from}:${a}->${m.to}:${b}`],
89
+ message: `A mapping from ${m.from} "${a}" to ${m.to} "${b}" joins an entity that is not there`,
90
+ })));
91
+ }
92
+ }
@@ -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,108 @@
1
+ /**
2
+ * DataModelContext — the semantic environment a data model is read in: what
3
+ * its entities mean here (`meanings`, by entity id: "Account" in banking is
4
+ * not "Account" in a CRM), the rules that hold here (`constraints`, each on
5
+ * some entities), and where and when that applies (`jurisdiction`,
6
+ * `validFrom`, `validTo`). The schema stays as it is; a context gives it
7
+ * meaning, scope and constraints.
8
+ */
9
+ export class DataModelContext {
10
+ /**
11
+ * @param {Object} spec
12
+ * @param {string} spec.id
13
+ * @param {string} [spec.name]
14
+ * @param {string} [spec.description]
15
+ * @param {Object<string, string>} [spec.meanings] - What an entity means here, by entity id
16
+ * @param {Array<{id?: string, text: string, applies?: string[], x?: number, y?: number}>} [spec.constraints]
17
+ * Rules that hold here, each on the entities it `applies` to (drawn as notes attached to them)
18
+ * @param {string} [spec.jurisdiction]
19
+ * @param {string} [spec.validFrom] - ISO date
20
+ * @param {string} [spec.validTo] - ISO date
21
+ */
22
+ constructor({ id, name = id, description = '', meanings = {}, constraints = [], jurisdiction = '', validFrom = '', validTo = '' } = {}) {
23
+ if (!id) throw new TypeError('DataModelContext must have an id');
24
+ this.id = id;
25
+ this.name = name;
26
+ this.description = description;
27
+ this.meanings = { ...meanings };
28
+ this.constraints = constraints.map((c, i) => ({ ...c, id: c.id ?? `${id}-rule-${i + 1}`, applies: [...(c.applies ?? [])] }));
29
+ this.jurisdiction = jurisdiction;
30
+ this.validFrom = validFrom;
31
+ this.validTo = validTo;
32
+ }
33
+
34
+ /** What an entity means here, or null (then its schema description holds). */
35
+ meaningOf(entityId) {
36
+ return this.meanings[entityId] ?? null;
37
+ }
38
+
39
+ /** The rules that hold here on an entity. */
40
+ constraintsOn(entityId) {
41
+ return this.constraints.filter(c => c.applies.includes(entityId));
42
+ }
43
+
44
+ /** Where and when this context applies, in a few words ('AU · from 2024-07-01'), or ''. */
45
+ get scope() {
46
+ const when = this.validFrom && this.validTo ? `${this.validFrom} – ${this.validTo}`
47
+ : this.validFrom ? `from ${this.validFrom}`
48
+ : this.validTo ? `until ${this.validTo}` : '';
49
+ return [this.jurisdiction, when].filter(Boolean).join(' · ');
50
+ }
51
+
52
+ /**
53
+ * The fields of a dialog making or changing a context of `model`
54
+ * (FormDialog fields): its name, description, jurisdiction and dates, what
55
+ * each concept means in it, and its rules, one a line: the rule, then
56
+ * after '|' the concepts it applies to.
57
+ * @param {DataModel} model
58
+ * @param {DataModelContext|null} [context] - The one changed; none for a new one
59
+ */
60
+ static fields(model, context = null) {
61
+ const concepts = model.concepts;
62
+ const nameOf = (id) => model.level('conceptual').entity(id)?.name ?? id;
63
+ return [
64
+ { key: 'name', label: 'Name', type: 'text', value: context?.name ?? '' },
65
+ { key: 'description', label: 'Description', type: 'textarea', rows: 2, value: context?.description ?? '' },
66
+ { key: 'jurisdiction', label: 'Jurisdiction', type: 'text', value: context?.jurisdiction ?? '' },
67
+ { key: 'validFrom', label: 'Valid from', type: 'date', value: context?.validFrom ?? '' },
68
+ { key: 'validTo', label: 'Valid to', type: 'date', value: context?.validTo ?? '' },
69
+ { type: 'heading', label: 'Means here' },
70
+ ...concepts.map(c => ({ key: `meaning:${c.id}`, label: c.name, type: 'textarea', rows: 2, value: context?.meanings[c.id] ?? '', placeholder: 'as the schema describes it' })),
71
+ { type: 'heading', label: 'Rules' },
72
+ {
73
+ key: 'rules', label: '', type: 'textarea', rows: 5,
74
+ value: (context?.constraints ?? []).map(c => `${c.text} | ${c.applies.map(nameOf).join(', ')}`).join('\n'),
75
+ hint: 'One a line: the rule | the concepts it applies to (by name)',
76
+ },
77
+ ];
78
+ }
79
+
80
+ /**
81
+ * A context's spec from those fields' values. A rule kept from `context`
82
+ * (the same text) keeps its id and place; a concept is named by name or id.
83
+ */
84
+ static specFrom(values, model, context = null) {
85
+ const concepts = model.concepts;
86
+ const conceptOf = (word) => concepts.find(c => c.id === word || c.name.toLowerCase() === word.toLowerCase())?.id ?? null;
87
+ const meanings = Object.fromEntries(concepts.map(c => [c.id, (values[`meaning:${c.id}`] ?? '').trim()]).filter(([, text]) => text));
88
+ const constraints = String(values.rules ?? '').split('\n').map(line => line.trim()).filter(Boolean).map(line => {
89
+ const [text, on = ''] = line.split('|').map(part => part.trim());
90
+ const applies = on.split(',').map(w => w.trim()).filter(Boolean).map(conceptOf).filter(Boolean);
91
+ const kept = context?.constraints.find(c => c.text === text);
92
+ return { text, applies, ...(kept && { id: kept.id, ...(Number.isFinite(kept.x) && { x: kept.x, y: kept.y }) }) };
93
+ });
94
+ return {
95
+ name: (values.name ?? '').trim(), description: (values.description ?? '').trim(),
96
+ jurisdiction: (values.jurisdiction ?? '').trim(), validFrom: values.validFrom ?? '', validTo: values.validTo ?? '',
97
+ meanings, constraints,
98
+ };
99
+ }
100
+
101
+ toJSON() {
102
+ return {
103
+ id: this.id, name: this.name, description: this.description, meanings: { ...this.meanings },
104
+ constraints: this.constraints.map(c => ({ ...c, applies: [...c.applies] })),
105
+ jurisdiction: this.jurisdiction, validFrom: this.validFrom, validTo: this.validTo,
106
+ };
107
+ }
108
+ }
@@ -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
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * DataModelPerspective — how one stakeholder sees a data model: a lens on the
3
+ * schema, not another schema. It shows some of the entities (`entities`; all
4
+ * when unset) under the names that stakeholder uses (`names`: Party is the
5
+ * Customer, Ledger the Billing), with relationships renamed too
6
+ * (`relationshipNames`, by relationship id), and names the context it is
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.
14
+ */
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
+
34
+ /**
35
+ * @param {Object} spec
36
+ * @param {string} spec.id
37
+ * @param {string} [spec.name]
38
+ * @param {string} [spec.description]
39
+ * @param {string[]|null} [spec.entities] - The entity ids it shows; null: every one
40
+ * @param {Object<string, string>} [spec.names] - An entity's name here, by entity id
41
+ * @param {Object<string, string>} [spec.relationshipNames] - A relationship's name here, by relationship id
42
+ * @param {string|null} [spec.context] - The context it is read in by default
43
+ */
44
+ constructor({ id, name = id, description = '', entities = null, names = {}, relationshipNames = {}, context = null } = {}) {
45
+ if (!id) throw new TypeError('DataModelPerspective must have an id');
46
+ this.id = id;
47
+ this.name = name;
48
+ this.description = description;
49
+ this.entities = entities ? [...entities] : null;
50
+ this.names = { ...names };
51
+ this.relationshipNames = { ...relationshipNames };
52
+ this.context = context;
53
+ }
54
+
55
+ /** Whether it shows an entity. */
56
+ shows(entityId) {
57
+ return !this.entities || this.entities.includes(entityId);
58
+ }
59
+
60
+ /** What an entity is called here: its name in this perspective, else the schema's. */
61
+ nameOf(entity) {
62
+ return this.names[entity.id] ?? entity.name;
63
+ }
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
+
85
+ /** What a relationship is called here. */
86
+ relationshipNameOf(relationship) {
87
+ return (relationship.id && this.relationshipNames[relationship.id]) ?? relationship.name;
88
+ }
89
+
90
+ /**
91
+ * The fields of a dialog making or changing a perspective of `model`
92
+ * (FormDialog fields): its name, description, the context it is read in,
93
+ * the concepts it shows, and what it calls each.
94
+ * @param {DataModel} model
95
+ * @param {DataModelPerspective|null} [perspective] - The one changed; none for a new one
96
+ */
97
+ static fields(model, perspective = null) {
98
+ const concepts = model.concepts;
99
+ return [
100
+ { key: 'name', label: 'Name', type: 'text', value: perspective?.name ?? '' },
101
+ { key: 'description', label: 'Whose view', type: 'textarea', rows: 2, value: perspective?.description ?? '' },
102
+ { key: 'context', label: 'Read in', type: 'select', value: perspective?.context ?? '', choices: [['', 'No context'], ...model.contexts.map(c => [c.id, c.name])] },
103
+ { type: 'heading', label: 'Shows' },
104
+ { key: 'entities', label: '', type: 'checklist', value: perspective?.entities ?? concepts.map(c => c.id), choices: concepts.map(c => [c.id, c.name]) },
105
+ { type: 'heading', label: 'Called here' },
106
+ ...concepts.map(c => ({ key: `name:${c.id}`, label: c.name, type: 'text', value: perspective?.names[c.id] ?? '', placeholder: 'as in the schema' })),
107
+ ];
108
+ }
109
+
110
+ /** A perspective's spec from those fields' values (all shown: `entities` null; names left blank: none). */
111
+ static specFrom(values, model) {
112
+ const ids = model.concepts.map(c => c.id);
113
+ const shown = values.entities ?? ids;
114
+ const names = Object.fromEntries(ids.map(id => [id, (values[`name:${id}`] ?? '').trim()]).filter(([, name]) => name));
115
+ return {
116
+ name: (values.name ?? '').trim(), description: (values.description ?? '').trim(), context: values.context || null,
117
+ entities: shown.length === ids.length ? null : [...shown], names,
118
+ };
119
+ }
120
+
121
+ toJSON() {
122
+ return {
123
+ ...(this.constructor.kind !== DataModelPerspective.kind && { kind: this.constructor.kind }),
124
+ id: this.id, name: this.name, description: this.description,
125
+ entities: this.entities ? [...this.entities] : null,
126
+ names: { ...this.names }, relationshipNames: { ...this.relationshipNames }, context: this.context,
127
+ };
128
+ }
129
+ }
@@ -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);