@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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { BianNotation } from '../../bian.js';
|
|
2
|
+
import { YamlReader } from '../../yaml/YamlReader.js';
|
|
3
|
+
import { ServiceDomain } from './ServiceDomain.js';
|
|
4
|
+
import { EnumerationType } from './EnumerationType.js';
|
|
5
|
+
import { StructuredType } from './StructuredType.js';
|
|
6
|
+
import { DataType } from './DataType.js';
|
|
7
|
+
|
|
8
|
+
/** 'CurrentAccountFacility' → 'Current Account Facility'. */
|
|
9
|
+
const spaced = (name) => String(name).replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
10
|
+
/** 'Current Account Facility' → 'current-account-facility'. */
|
|
11
|
+
const idOf = (name) => spaced(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
12
|
+
/** A schema's name from a `$ref` ('#/components/schemas/Amount' → 'Amount'), of an item's for an array. */
|
|
13
|
+
const refName = (prop) => (prop?.$ref ?? prop?.items?.$ref)?.split('/').pop() ?? null;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* BianSemanticApiReader — a service domain from one of BIAN's semantic APIs
|
|
17
|
+
* (an OpenAPI file, YAML or JSON, one per service domain, as BIAN publishes
|
|
18
|
+
* them: the plain ones and those extended with ISO 20022), rather than retyped.
|
|
19
|
+
*
|
|
20
|
+
* Its operations' tags name its control record (`CR - CurrentAccountFacility`)
|
|
21
|
+
* and behaviour qualifiers (`BQ - Interest`); each becomes a logical entity
|
|
22
|
+
* with the attributes its schema has (BIAN's names), the qualifiers joined to
|
|
23
|
+
* the control record. The schemas those attributes are become data types, as
|
|
24
|
+
* deep as `depth` (1: those the attributes name): an `enum` an EnumerationType
|
|
25
|
+
* (each value's meaning from BIAN's description), an object a StructuredType,
|
|
26
|
+
* any other a DataType. A schema named as a logical entity the model already
|
|
27
|
+
* has (`entityNames`) stays a reference to it. Where the ISO 20022 version
|
|
28
|
+
* names an attribute's ISO 20022 type, the attribute (and its data type) keep it.
|
|
29
|
+
*
|
|
30
|
+
* The service domain is named by the API's title, its pattern by the artifact
|
|
31
|
+
* its control record's name ends with (BianNotation.patternOfControlRecord),
|
|
32
|
+
* its asset by the rest of that name.
|
|
33
|
+
*/
|
|
34
|
+
export class BianSemanticApiReader {
|
|
35
|
+
/**
|
|
36
|
+
* @param {Object} [options]
|
|
37
|
+
* @param {number} [options.depth] - How deep schemas become data types (0: none)
|
|
38
|
+
* @param {Iterable<string>} [options.entityNames] - Names of logical entities already there (any case)
|
|
39
|
+
*/
|
|
40
|
+
constructor({ depth = 1, entityNames = [] } = {}) {
|
|
41
|
+
this.depth = depth;
|
|
42
|
+
this.entityNames = new Set([...entityNames].map(n => String(n).toLowerCase()));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A specification from a file's text: JSON, else YAML (YamlReader). */
|
|
46
|
+
static parse(text) {
|
|
47
|
+
const body = String(text).replace(/^/, '');
|
|
48
|
+
return /^\s*\{/.test(body) ? JSON.parse(body) : YamlReader.parse(body);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What a semantic API says, for DataModel#bringCommand.
|
|
53
|
+
* @param {Object} spec - An OpenAPI document (BianSemanticApiReader.parse)
|
|
54
|
+
* @returns {{ serviceDomains: Array<Object>, entities: Array<Object>, relationships: Array<Object>, dataTypes: Array<Object> }}
|
|
55
|
+
*/
|
|
56
|
+
read(spec) {
|
|
57
|
+
const title = spec?.info?.title;
|
|
58
|
+
if (!title || !spec.paths) throw new Error('Not an OpenAPI specification (no info.title or paths)');
|
|
59
|
+
const schemas = spec.components?.schemas ?? {};
|
|
60
|
+
const { controlRecords, qualifiers } = BianSemanticApiReader.parts(spec);
|
|
61
|
+
// Some of BIAN's APIs have no control record operations (or none at all): what is there is read.
|
|
62
|
+
const [cr = null] = controlRecords;
|
|
63
|
+
const schemaOf = (name) => schemas[name] ?? ['Retrieve', 'Initiate', 'Update', 'Register', 'Create', 'Evaluate', 'Provide']
|
|
64
|
+
.map(verb => schemas[`${verb}${name}`]).find(Boolean) ?? null;
|
|
65
|
+
|
|
66
|
+
const used = new Map(); // schema name -> depth first reached
|
|
67
|
+
const attributesOf = (schema, depth) => Object.entries(schema?.properties ?? {}).map(([name, prop]) => {
|
|
68
|
+
const ref = refName(prop);
|
|
69
|
+
const iso20022 = BianSemanticApiReader.iso20022Of(prop.description);
|
|
70
|
+
if (ref && !used.has(ref)) used.set(ref, depth);
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
...(ref ? { type: ref } : (prop.format || prop.type) && { type: prop.format || prop.type }),
|
|
74
|
+
...(BianSemanticApiReader.cleanDescription(prop.description) && { description: BianSemanticApiReader.cleanDescription(prop.description) }),
|
|
75
|
+
...(iso20022 && { iso20022 }),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const entity = (name) => ({
|
|
79
|
+
id: idOf(name), name: spaced(name),
|
|
80
|
+
...(BianSemanticApiReader.cleanDescription(schemaOf(name)?.description) && { description: BianSemanticApiReader.cleanDescription(schemaOf(name)?.description) }),
|
|
81
|
+
attributes: attributesOf(schemaOf(name), 1),
|
|
82
|
+
});
|
|
83
|
+
const entities = [...(cr ? [cr] : []), ...qualifiers].map(entity);
|
|
84
|
+
const relationships = !cr ? [] : qualifiers.map(q => ({ id: `${idOf(cr)}-has-${idOf(q)}`, source: idOf(cr), target: idOf(q), name: 'has', sourceCardinality: '1', targetCardinality: '0..*' }));
|
|
85
|
+
|
|
86
|
+
// The schemas the attributes are, then theirs, as deep as asked: data types, unless entities already.
|
|
87
|
+
const dataTypes = [];
|
|
88
|
+
const isoOf = new Map();
|
|
89
|
+
for (const a of entities.flatMap(e => e.attributes)) if (a.iso20022 && a.type && !isoOf.has(a.type)) isoOf.set(a.type, a.iso20022);
|
|
90
|
+
const done = new Set();
|
|
91
|
+
for (let level = 1; level <= this.depth; level++) {
|
|
92
|
+
for (const [name, at] of [...used]) {
|
|
93
|
+
if (at !== level || done.has(name)) continue;
|
|
94
|
+
done.add(name);
|
|
95
|
+
if (this.entityNames.has(name.toLowerCase()) || this.entityNames.has(spaced(name).toLowerCase())) continue;
|
|
96
|
+
const schema = schemas[name];
|
|
97
|
+
if (!schema) continue;
|
|
98
|
+
const type = this.dataType(name, schema, level + 1, attributesOf);
|
|
99
|
+
if (isoOf.has(name) && !type.iso20022) type.iso20022 = isoOf.get(name);
|
|
100
|
+
dataTypes.push(type);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Its pattern by its control record's artifact; with none, by its own name's ('Operations Log').
|
|
105
|
+
const crName = spaced(cr ?? title);
|
|
106
|
+
const pattern = BianNotation.patternOfControlRecord(crName);
|
|
107
|
+
const ending = pattern ? [pattern.artifact, ...pattern.formerArtifacts].find(a => crName.toLowerCase().endsWith(a.toLowerCase())) : null;
|
|
108
|
+
const serviceDomain = {
|
|
109
|
+
kind: ServiceDomain.kind,
|
|
110
|
+
id: `sd-${idOf(title)}`,
|
|
111
|
+
name: title,
|
|
112
|
+
description: BianSemanticApiReader.cleanDescription(spec.info.description),
|
|
113
|
+
asset: ending ? crName.slice(0, -ending.length).trim() : crName,
|
|
114
|
+
pattern: pattern?.name ?? '',
|
|
115
|
+
controlRecord: cr ? idOf(cr) : null,
|
|
116
|
+
qualifiers: qualifiers.map(q => ({ name: spaced(q), entity: idOf(q) })),
|
|
117
|
+
};
|
|
118
|
+
return { serviceDomains: [serviceDomain], entities, relationships, dataTypes };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A data type spec for a schema: an enumeration, a structure, or a plain type. */
|
|
122
|
+
dataType(name, schema, depth, attributesOf) {
|
|
123
|
+
const base = { id: `bian-${idOf(name)}`, name, ...(BianSemanticApiReader.enumerationMeanings(schema.description).size === 0 && BianSemanticApiReader.cleanDescription(schema.description) && { description: BianSemanticApiReader.cleanDescription(schema.description) }) };
|
|
124
|
+
if (Array.isArray(schema.enum)) {
|
|
125
|
+
const meanings = BianSemanticApiReader.enumerationMeanings(schema.description);
|
|
126
|
+
return { kind: EnumerationType.kind, ...base, values: schema.enum.map(v => ({ name: String(v), ...(meanings.get(String(v)) && { description: meanings.get(String(v)) }) })) };
|
|
127
|
+
}
|
|
128
|
+
if (schema.properties) {
|
|
129
|
+
return { kind: StructuredType.kind, ...base, fields: attributesOf(schema, depth) };
|
|
130
|
+
}
|
|
131
|
+
return { kind: DataType.kind, ...base, ...((schema.format || schema.type) && { basedOn: schema.format || schema.type }) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The control records and behaviour qualifiers its operations' tags name, in order. */
|
|
135
|
+
static parts(spec) {
|
|
136
|
+
const controlRecords = [], qualifiers = [];
|
|
137
|
+
for (const path of Object.values(spec.paths ?? {})) {
|
|
138
|
+
for (const op of Object.values(path ?? {})) {
|
|
139
|
+
for (const tag of op?.tags ?? []) {
|
|
140
|
+
const m = /^(CR|BQ)\b[^-]*-\s*(\S+)\s*$/.exec(String(tag));
|
|
141
|
+
if (!m) continue;
|
|
142
|
+
const list = m[1] === 'CR' ? controlRecords : qualifiers;
|
|
143
|
+
if (!list.includes(m[2])) list.push(m[2]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { controlRecords, qualifiers };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The ISO 20022 type a description links to (`…/standardsrepository/type/CurrencyCode`), or null. */
|
|
151
|
+
static iso20022Of(description) {
|
|
152
|
+
return /iso20022\.org\/standardsrepository\/type\/([A-Za-z0-9_]+)/.exec(String(description ?? ''))?.[1] ?? null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** What an enumeration's description says each value means: lines "* `Value` - meaning". */
|
|
156
|
+
static enumerationMeanings(description) {
|
|
157
|
+
const meanings = new Map();
|
|
158
|
+
for (const m of String(description ?? '').matchAll(/\*\s*`([^`]+)`\s*-\s*([^\r\n]*)/g)) {
|
|
159
|
+
if (m[2].trim()) meanings.set(m[1], m[2].trim());
|
|
160
|
+
}
|
|
161
|
+
return meanings;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A description as said: without the ISO 20022 version's "(DDD Entity) - "
|
|
166
|
+
* and "BIAN Service Domain (…) | … |" prefixes, or its trailing
|
|
167
|
+
* "Type|Type ([link])|Standard|ISO20022" reference.
|
|
168
|
+
*/
|
|
169
|
+
static cleanDescription(description) {
|
|
170
|
+
let text = String(description ?? '').replace(/\r/g, '').trim();
|
|
171
|
+
if (!text || text.startsWith('>')) return '';
|
|
172
|
+
text = text.replace(/^\(DDD [^)]*\)\s*-\s*/, '');
|
|
173
|
+
if (/^BIAN Service Domain/.test(text)) text = text.slice(text.lastIndexOf('|') + 1).trim();
|
|
174
|
+
const bar = text.indexOf('|');
|
|
175
|
+
if (bar >= 0) {
|
|
176
|
+
// "… instance CashAccountContract|CashAccountContract (link)|Standard|ISO20022": the type named twice goes too.
|
|
177
|
+
const repeated = text.slice(bar + 1).trim().split(/[\s(|]/)[0];
|
|
178
|
+
text = text.slice(0, bar).trim();
|
|
179
|
+
if (repeated && text.endsWith(repeated)) text = text.slice(0, -repeated.length).trim();
|
|
180
|
+
}
|
|
181
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DataModelPerspective } from './DataModelPerspective.js';
|
|
2
|
+
import { ClassificationScheme } from './ClassificationScheme.js';
|
|
3
|
+
|
|
4
|
+
/** How an entity with no classification is drawn through the lens. */
|
|
5
|
+
const UNCLASSIFIED = '#f3f4f6';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ClassificationLens — a perspective that colours what it shows by its
|
|
9
|
+
* classification (DMBOK2's data security): each entity in its sensitivity's
|
|
10
|
+
* colour (the model's ClassificationScheme), at its effective classification
|
|
11
|
+
* (the highest of its own and its attributes'), unclassified ones grey, and
|
|
12
|
+
* the classification on hover. It sees the concepts any perspective does.
|
|
13
|
+
*/
|
|
14
|
+
export class ClassificationLens extends DataModelPerspective {
|
|
15
|
+
static kind = 'classification';
|
|
16
|
+
static kindLabel = 'Classification lens';
|
|
17
|
+
|
|
18
|
+
decorate(entity, level, model) {
|
|
19
|
+
const scheme = model?.classificationScheme ?? new ClassificationScheme();
|
|
20
|
+
const effective = scheme.effective(entity);
|
|
21
|
+
return {
|
|
22
|
+
color: scheme.colorOf(effective.sensitivity) ?? UNCLASSIFIED,
|
|
23
|
+
details: { classification: ClassificationScheme.describe(effective) || 'unclassified' },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** As a perspective's, its kind kept. */
|
|
28
|
+
static specFrom(values, model) {
|
|
29
|
+
return { ...DataModelPerspective.specFrom(values, model), kind: ClassificationLens.kind };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
DataModelPerspective.register(ClassificationLens);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** The colours sensitivity levels are drawn in, lowest first, when a scheme names none. */
|
|
2
|
+
const LEVEL_COLORS = ['#dcfce7', '#e0f2fe', '#fef3c7', '#fecaca', '#e9d5ff', '#fed7aa'];
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ClassificationScheme — how a model's data is classified (DMBOK2's data
|
|
6
|
+
* security): sensitivity `levels` in order, lowest first, each with the colour
|
|
7
|
+
* a classification lens draws it in, and `categories` a piece of data may fall
|
|
8
|
+
* in (PII, PCI...). The default is the common four levels and categories; a
|
|
9
|
+
* model may keep its own.
|
|
10
|
+
*
|
|
11
|
+
* A classification, on an entity or an attribute, is its `sensitivity` (a
|
|
12
|
+
* level's name) and its `categories`; an entity's effective one is the highest
|
|
13
|
+
* of its own and its attributes', with all their categories.
|
|
14
|
+
*/
|
|
15
|
+
export class ClassificationScheme {
|
|
16
|
+
static DEFAULT = Object.freeze({
|
|
17
|
+
levels: [
|
|
18
|
+
{ name: 'Public', color: LEVEL_COLORS[0] },
|
|
19
|
+
{ name: 'Internal', color: LEVEL_COLORS[1] },
|
|
20
|
+
{ name: 'Confidential', color: LEVEL_COLORS[2] },
|
|
21
|
+
{ name: 'Restricted', color: LEVEL_COLORS[3] },
|
|
22
|
+
],
|
|
23
|
+
categories: ['PII', 'PCI', 'PHI', 'Financial'],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** @param {{levels?: Array<{name: string, color?: string}|string>, categories?: string[]}} [spec] */
|
|
27
|
+
constructor({ levels = ClassificationScheme.DEFAULT.levels, categories = ClassificationScheme.DEFAULT.categories } = {}) {
|
|
28
|
+
this.levels = levels.map((l, i) => {
|
|
29
|
+
const level = typeof l === 'string' ? { name: l } : l;
|
|
30
|
+
return { name: String(level.name), color: level.color || LEVEL_COLORS[i % LEVEL_COLORS.length] };
|
|
31
|
+
});
|
|
32
|
+
this.categories = [...categories].map(String);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A sensitivity's rank (0 the lowest), or -1 for none or one the scheme lacks. */
|
|
36
|
+
rank(sensitivity) {
|
|
37
|
+
return this.levels.findIndex(l => l.name.toLowerCase() === String(sensitivity ?? '').toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The colour a sensitivity is drawn in, or null. */
|
|
41
|
+
colorOf(sensitivity) {
|
|
42
|
+
return this.levels[this.rank(sensitivity)]?.color ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Categories as written ('PII, PCI' or a list) as a list. */
|
|
46
|
+
static categoriesOf(value) {
|
|
47
|
+
return (Array.isArray(value) ? value : String(value ?? '').split(/[,;]/)).map(c => String(c).trim()).filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* An entity's effective classification: the highest sensitivity of its own
|
|
52
|
+
* and its attributes', and all their categories.
|
|
53
|
+
* @returns {{sensitivity: string|null, categories: string[]}}
|
|
54
|
+
*/
|
|
55
|
+
effective(entity) {
|
|
56
|
+
const parts = [entity, ...(entity?.attributes ?? [])];
|
|
57
|
+
let best = null;
|
|
58
|
+
for (const p of parts) if (this.rank(p?.sensitivity) > this.rank(best)) best = this.levels[this.rank(p.sensitivity)].name;
|
|
59
|
+
const categories = [...new Set(parts.flatMap(p => ClassificationScheme.categoriesOf(p?.categories)))];
|
|
60
|
+
return { sensitivity: best, categories };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A classification in words: 'Confidential (PII, PCI)', or '' for none. */
|
|
64
|
+
static describe({ sensitivity, categories = [] } = {}) {
|
|
65
|
+
if (!sensitivity && !categories.length) return '';
|
|
66
|
+
return [sensitivity || 'Unclassified', categories.length ? `(${categories.join(', ')})` : ''].filter(Boolean).join(' ');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
toJSON() {
|
|
70
|
+
return { levels: this.levels.map(l => ({ ...l })), categories: [...this.categories] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Whether it is the default (so a model need not keep it). */
|
|
74
|
+
get isDefault() {
|
|
75
|
+
return JSON.stringify(this.toJSON()) === JSON.stringify(new ClassificationScheme().toJSON());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Its dialog's fields: its levels a line each, lowest first (`name | colour`), its categories. */
|
|
79
|
+
static fields(scheme) {
|
|
80
|
+
return [
|
|
81
|
+
{
|
|
82
|
+
key: 'levels', label: 'Sensitivity levels', type: 'textarea', rows: 5,
|
|
83
|
+
value: scheme.levels.map(l => `${l.name} | ${l.color}`).join('\n'),
|
|
84
|
+
hint: 'Lowest first, one a line: name | colour',
|
|
85
|
+
},
|
|
86
|
+
{ key: 'categories', label: 'Categories', type: 'text', value: scheme.categories.join(', '), placeholder: 'PII, PCI, PHI' },
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A scheme's spec from those fields. */
|
|
91
|
+
static specFrom(values) {
|
|
92
|
+
return {
|
|
93
|
+
levels: String(values.levels ?? '').split('\n').map(l => l.split('|').map(p => p.trim())).filter(([name]) => name)
|
|
94
|
+
.map(([name, color]) => ({ name, ...(color && { color }) })),
|
|
95
|
+
categories: ClassificationScheme.categoriesOf(values.categories),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|