@peter.prib/renderengine 0.2.7 → 0.2.11

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 (46) hide show
  1. package/README.md +66 -9
  2. package/REFERENCE.md +129 -0
  3. package/dist/renderengine-workspace.umd.js +5068 -2892
  4. package/dist/renderengine.umd.js +4874 -2888
  5. package/examples/data-model.html +40 -18
  6. package/package.json +1 -1
  7. package/src/charting/FormDialog.js +398 -385
  8. package/src/charting/RowsField.js +127 -0
  9. package/src/charting/datamodel/ClassificationLens.js +33 -0
  10. package/src/charting/datamodel/ClassificationScheme.js +98 -0
  11. package/src/charting/datamodel/DataModel.js +220 -4
  12. package/src/charting/datamodel/DataModelChecker.js +244 -2
  13. package/src/charting/datamodel/DataModelContext.js +40 -10
  14. package/src/charting/datamodel/DataModelEditCommand.js +47 -0
  15. package/src/charting/datamodel/DataModelPerspective.js +12 -2
  16. package/src/charting/datamodel/EnumerationType.js +64 -51
  17. package/src/charting/datamodel/LevelSchema.js +23 -0
  18. package/src/charting/datamodel/ServiceDomain.js +9 -0
  19. package/src/charting/datamodel/SqlDdl.js +85 -0
  20. package/src/charting/datamodel/SqlDdlFileFormat.js +23 -0
  21. package/src/charting/datamodel/index.js +21 -15
  22. package/src/charting/datamodel/naming/CamelCaseStyle.js +13 -0
  23. package/src/charting/datamodel/naming/LowerWordsStyle.js +13 -0
  24. package/src/charting/datamodel/naming/NameStyle.js +57 -0
  25. package/src/charting/datamodel/naming/NamingStandard.js +132 -0
  26. package/src/charting/datamodel/naming/PascalCaseStyle.js +13 -0
  27. package/src/charting/datamodel/naming/SnakeCaseStyle.js +13 -0
  28. package/src/charting/datamodel/naming/TitleCaseStyle.js +25 -0
  29. package/src/charting/datamodel/naming/UpperSnakeStyle.js +13 -0
  30. package/src/charting/datamodel/naming/index.js +8 -0
  31. package/src/charting/datamodel/scorecard/CompletenessCategory.js +30 -0
  32. package/src/charting/datamodel/scorecard/ConsistencyCategory.js +33 -0
  33. package/src/charting/datamodel/scorecard/DefinitionsCategory.js +33 -0
  34. package/src/charting/datamodel/scorecard/ModelScorecard.js +113 -0
  35. package/src/charting/datamodel/scorecard/ReadabilityCategory.js +37 -0
  36. package/src/charting/datamodel/scorecard/ScorecardCategory.js +46 -0
  37. package/src/charting/datamodel/scorecard/StandardsCategory.js +29 -0
  38. package/src/charting/datamodel/scorecard/StructureCategory.js +32 -0
  39. package/src/charting/datamodel/scorecard/index.js +8 -0
  40. package/src/charting/diagrams/DataModelDiagram.js +4 -11
  41. package/src/charting/diagrams/DataModelRelationshipStrategy.js +7 -1
  42. package/src/charting/diagrams/NodeGeometry.js +6 -2
  43. package/src/charting/toolbar/ToolbarSelect.js +6 -0
  44. package/src/datamodel.js +402 -4
  45. package/src/index.js +2666 -2666
  46. package/src/workspace/parts/DataModelPart.js +197 -6
package/src/datamodel.js CHANGED
@@ -35,7 +35,19 @@
35
35
  * the same calibration archimateDiagram's own icons use.
36
36
  */
37
37
 
38
- /** One data-modelling abstraction level. See DataModelNotation.levels(). */
38
+ /** Text from a dialog field, trimmed. */
39
+ const text = (v) => String(v ?? '').trim();
40
+ /** Categories as written ('PII, PCI') as a list. */
41
+ const list = (v) => (Array.isArray(v) ? v : String(v ?? '').split(/[,;]/)).map(x => String(x).trim()).filter(Boolean);
42
+ /** Who answers for an entity (DMBOK2's stewardship), in its dialog's order. */
43
+ const STEWARDS = Object.freeze([['owner', 'Business owner'], ['steward', 'Data steward'], ['custodian', 'Technical custodian']]);
44
+
45
+ /**
46
+ * One data-modelling abstraction level (see DataModelNotation.levels()): what
47
+ * it is, and what its entities and relationships hold, as the dialogs that
48
+ * edit them show it. A level's entity has a name and a definition; a level
49
+ * whose entities have attributes (AttributedLevel) adds them as a table.
50
+ */
39
51
  export class DataModelLevel {
40
52
  constructor(name, description) {
41
53
  this.name = name;
@@ -46,6 +58,195 @@ export class DataModelLevel {
46
58
  get label() {
47
59
  return this.name.charAt(0).toUpperCase() + this.name.slice(1);
48
60
  }
61
+
62
+ /** What an entity is called at this level ('Concept', 'Entity', 'Table'). */
63
+ get entityNoun() {
64
+ return 'Entity';
65
+ }
66
+
67
+ /**
68
+ * An entity's dialog fields (FormDialog): its name and definition, who
69
+ * answers for it (business owner, data steward, technical custodian), and
70
+ * how it is classified (a sensitivity of the model's scheme, categories).
71
+ * @param {Object|null} entity - null for a new one
72
+ * @param {Object} [context] - What the fields may offer: `typeNames` (the model's data types),
73
+ * `sensitivities` and `categories` (its classification scheme's)
74
+ */
75
+ entityFields(entity, context = {}) {
76
+ const { sensitivities = [], categories = [] } = context;
77
+ return [
78
+ { key: 'name', label: 'Name', type: 'text', value: entity?.name ?? '' },
79
+ { key: 'description', label: 'Definition', type: 'textarea', rows: 3, value: entity?.description ?? '', placeholder: 'What it is, in the business\'s words' },
80
+ { type: 'heading', label: 'Stewardship' },
81
+ ...STEWARDS.map(([key, label]) => ({ key: `stewardship.${key}`, label, type: 'text', value: entity?.stewardship?.[key] ?? '' })),
82
+ { type: 'heading', label: 'Classification' },
83
+ { key: 'sensitivity', label: 'Sensitivity', type: 'select', value: entity?.sensitivity ?? '', choices: [['', 'Unclassified'], ...sensitivities.map(s => [s, s])] },
84
+ { key: 'categories', label: 'Categories', type: 'text', value: list(entity?.categories).join(', '), placeholder: categories.join(', ') },
85
+ ];
86
+ }
87
+
88
+ /** What those fields say, laid over the entity (what they do not show kept; an empty one taken away). */
89
+ entitySpecFrom(values, entity = null) {
90
+ const out = { ...(entity ?? {}), name: text(values.name) };
91
+ const keep = (key, value) => { if (value === '' || (Array.isArray(value) && !value.length)) delete out[key]; else out[key] = value; };
92
+ keep('description', text(values.description));
93
+ const stewardship = Object.fromEntries(STEWARDS.map(([key]) => [key, text(values[`stewardship.${key}`])]).filter(([, v]) => v));
94
+ keep('stewardship', Object.keys(stewardship).length ? stewardship : '');
95
+ keep('sensitivity', text(values.sensitivity));
96
+ keep('categories', list(values.categories));
97
+ return out;
98
+ }
99
+
100
+ /**
101
+ * A relationship's dialog fields: its two ends (entities at this level),
102
+ * its name, each end's cardinality, whether it is identifying, a definition.
103
+ * @param {Array<Object>} entities - The level's entities
104
+ * @param {Object|null} relationship - null for a new one
105
+ */
106
+ relationshipFields(entities, relationship = null) {
107
+ const ends = entities.map(e => [e.id, e.name]);
108
+ const cardinalities = DataModelNotation.cardinalities().map(c => [c, c]);
109
+ return [
110
+ {
111
+ key: 'kind', label: 'Kind', type: 'select', value: RelationshipKind.of(relationship).kind,
112
+ choices: RelationshipKind.all().map(k => [k.kind, k.label]),
113
+ hint: 'A generalization: From is a subtype of To; its cardinalities are not used',
114
+ },
115
+ { key: 'source', label: 'From', type: 'select', value: relationship?.source ?? ends[0]?.[0] ?? '', choices: ends },
116
+ { key: 'sourceCardinality', label: 'Its cardinality', type: 'select', value: relationship?.sourceCardinality ?? '1', choices: cardinalities },
117
+ { key: 'name', label: 'Name', type: 'text', value: relationship?.name ?? '', placeholder: 'has, places, is for…' },
118
+ { key: 'target', label: 'To', type: 'select', value: relationship?.target ?? ends[1]?.[0] ?? ends[0]?.[0] ?? '', choices: ends },
119
+ { key: 'targetCardinality', label: 'Its cardinality', type: 'select', value: relationship?.targetCardinality ?? '0..*', choices: cardinalities },
120
+ { key: 'identifying', label: 'Identifying', type: 'checkbox', value: relationship ? relationship.identifying !== false : true },
121
+ { key: 'complete', label: 'Complete (generalization)', type: 'checkbox', value: !!relationship?.complete },
122
+ { key: 'disjoint', label: 'Disjoint (generalization)', type: 'checkbox', value: relationship ? relationship.disjoint !== false : true },
123
+ { key: 'description', label: 'Definition', type: 'textarea', rows: 2, value: relationship?.description ?? '' },
124
+ ];
125
+ }
126
+
127
+ /** What those fields say, laid over the relationship. */
128
+ relationshipSpecFrom(values, relationship = null) {
129
+ const out = {
130
+ ...(relationship ?? {}),
131
+ source: values.source, target: values.target, name: text(values.name),
132
+ sourceCardinality: values.sourceCardinality, targetCardinality: values.targetCardinality,
133
+ };
134
+ if (values.identifying) delete out.identifying; else out.identifying = false;
135
+ if (text(values.description)) out.description = text(values.description); else delete out.description;
136
+ const kind = RelationshipKind.KINDS.get(values.kind) ?? RelationshipKind.of(relationship);
137
+ delete out.complete; delete out.disjoint; delete out.kind;
138
+ if (kind.kind !== AssociationKind.kind) out.kind = kind.kind;
139
+ if (!kind.hasCardinality) {
140
+ delete out.sourceCardinality; delete out.targetCardinality; delete out.identifying;
141
+ out.complete = !!values.complete;
142
+ out.disjoint = values.disjoint !== false;
143
+ }
144
+ return out;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * A level whose entities have attributes (logical attributes, physical
150
+ * columns): its entity dialog adds them as a table (a FormDialog `rows`
151
+ * field), a row each: the attribute's name, type, keys, whether it is
152
+ * required, what a foreign key references, its definition.
153
+ */
154
+ export class AttributedLevel extends DataModelLevel {
155
+ /** What its attributes are called ('Attributes', 'Columns'). */
156
+ get attributeNoun() {
157
+ return 'Attributes';
158
+ }
159
+
160
+ /** The types its type column suggests. @param {Object} context */
161
+ typeSuggestions(context) {
162
+ return [];
163
+ }
164
+
165
+ /** The attribute table's columns. */
166
+ attributeColumns(context = {}) {
167
+ return [
168
+ { key: 'name', label: 'Name' },
169
+ { key: 'type', label: 'Type', suggestions: this.typeSuggestions(context), width: '130px' },
170
+ { key: 'isPK', label: 'PK', type: 'checkbox' },
171
+ { key: 'isFK', label: 'FK', type: 'checkbox' },
172
+ { key: 'isUnique', label: 'Unique', type: 'checkbox' },
173
+ { key: 'required', label: 'Required', type: 'checkbox' },
174
+ { key: 'references', label: 'References', placeholder: 'entity.attribute', width: '140px' },
175
+ { key: 'description', label: 'Definition' },
176
+ { key: 'sensitivity', label: 'Sensitivity', type: 'select', choices: [['', '—'], ...(context.sensitivities ?? []).map(x => [x, x])] },
177
+ { key: 'categories', label: 'Categories', placeholder: (context.categories ?? []).slice(0, 2).join(', '), width: '90px' },
178
+ { key: 'measure', label: 'Measure', type: 'select', choices: [['', '—'], ...MEASURES.map(m => [m, m])] },
179
+ ];
180
+ }
181
+
182
+ entityFields(entity, context = {}) {
183
+ return [
184
+ ...super.entityFields(entity, context),
185
+ { type: 'heading', label: 'Dimensional' },
186
+ { key: 'role', label: 'Role', type: 'select', value: EntityRole.of(entity).role, choices: EntityRole.all().map(r => [r.role, r.label]) },
187
+ { key: 'grain', label: 'Grain (a fact)', type: 'text', value: entity?.grain ?? '', placeholder: 'One row per account per day' },
188
+ { key: 'scdType', label: 'History (a dimension)', type: 'select', value: entity?.scdType ?? '', choices: [['', '—'], ...DimensionRole.SCD_TYPES] },
189
+ { key: 'conformed', label: 'Conformed (a dimension)', type: 'checkbox', value: !!entity?.conformed },
190
+ {
191
+ key: 'attributes', label: this.attributeNoun, type: 'rows', columns: this.attributeColumns(context),
192
+ // Categories shown as written, a list read back (entitySpecFrom).
193
+ value: (entity?.attributes ?? []).map(a => (Array.isArray(a.categories) ? { ...a, categories: a.categories.join(', ') } : a)),
194
+ addLabel: this.attributeNoun.replace(/s$/, '').toLowerCase(),
195
+ },
196
+ ];
197
+ }
198
+
199
+ entitySpecFrom(values, entity = null) {
200
+ // An attribute's categories as a list, as an entity's are.
201
+ const attributes = (values.attributes ?? []).filter(a => text(a.name)).map(a => {
202
+ if (a.categories === undefined) return a;
203
+ const categories = list(a.categories);
204
+ const { categories: _, ...rest } = a;
205
+ return categories.length ? { ...rest, categories } : rest;
206
+ });
207
+ const out = { ...super.entitySpecFrom(values, entity), attributes };
208
+ // Its role, and only what that role has (a fact's grain, a dimension's history).
209
+ const role = EntityRole.ROLES.get(values.role ?? EntityRole.of(entity).role) ?? EntityRole.of(null);
210
+ for (const key of ['role', 'grain', 'scdType', 'conformed']) delete out[key];
211
+ if (role.role) out.role = role.role;
212
+ if (role.keys.includes('grain') && text(values.grain)) out.grain = text(values.grain);
213
+ if (role.keys.includes('scdType') && values.scdType !== '' && values.scdType != null) out.scdType = String(values.scdType);
214
+ if (role.keys.includes('conformed') && values.conformed) out.conformed = true;
215
+ return out;
216
+ }
217
+ }
218
+
219
+ /** The logical level: entities and their attributes, typed by the model's data types or plainly. */
220
+ export class LogicalLevel extends AttributedLevel {
221
+ static PLAIN_TYPES = Object.freeze(['Text', 'Number', 'Decimal', 'Amount', 'Date', 'Date/Time', 'Boolean', 'Identifier', 'Code']);
222
+
223
+ typeSuggestions({ typeNames = [] } = {}) {
224
+ return [...typeNames, ...LogicalLevel.PLAIN_TYPES];
225
+ }
226
+ }
227
+
228
+ /** The physical level: tables and their columns, typed as a database types them. */
229
+ export class PhysicalLevel extends AttributedLevel {
230
+ static SQL_TYPES = Object.freeze(['VARCHAR(40)', 'TEXT', 'INT', 'BIGINT', 'DECIMAL(18,2)', 'DATE', 'TIMESTAMP', 'BOOLEAN', 'CHAR(3)', 'JSONB']);
231
+
232
+ get entityNoun() {
233
+ return 'Table';
234
+ }
235
+
236
+ get attributeNoun() {
237
+ return 'Columns';
238
+ }
239
+
240
+ typeSuggestions() {
241
+ return PhysicalLevel.SQL_TYPES;
242
+ }
243
+ }
244
+
245
+ /** The conceptual level: concepts with a name and a definition, no attributes. */
246
+ export class ConceptualLevel extends DataModelLevel {
247
+ get entityNoun() {
248
+ return 'Concept';
249
+ }
49
250
  }
50
251
 
51
252
  /** The canonical cardinality vocabulary every NotationStyle renders
@@ -91,9 +292,9 @@ export class NotationStyle {
91
292
  }
92
293
 
93
294
  const LEVELS = {
94
- conceptual: new DataModelLevel('conceptual', 'Entities and their relationships only — no attributes, no keys, no types.'),
95
- logical: new DataModelLevel('logical', 'Attributes and keys, still database-agnostic — no column types or engine specifics.'),
96
- physical: new DataModelLevel('physical', 'Real column types, keys, and constraints as they exist in an actual database.'),
295
+ conceptual: new ConceptualLevel('conceptual', 'Entities and their relationships only — no attributes, no keys, no types.'),
296
+ logical: new LogicalLevel('logical', 'Attributes and keys, still database-agnostic — no column types or engine specifics.'),
297
+ physical: new PhysicalLevel('physical', 'Real column types, keys, and constraints as they exist in an actual database.'),
97
298
  };
98
299
 
99
300
  const NOTATIONS = {
@@ -120,6 +321,197 @@ const NOTATIONS = {
120
321
  }),
121
322
  };
122
323
 
324
+ /**
325
+ * RelationshipKind — what a relationship between two entities is: an
326
+ * association (`AssociationKind`, a cardinality at each end, a Chen diamond)
327
+ * or a generalization (`GeneralizationKind`, the source a subtype of the
328
+ * target). A relationship's `kind` names one (none: an association); each
329
+ * draws its own edge in a notation, says whether a notation draws it as a node
330
+ * of its own, and whether it has cardinalities.
331
+ */
332
+ export class RelationshipKind {
333
+ static kind = '';
334
+ static label = '';
335
+
336
+ /** Relationship kinds by their `kind`. */
337
+ static KINDS = new Map();
338
+
339
+ static register(KindClass) {
340
+ RelationshipKind.KINDS.set(KindClass.kind, new KindClass());
341
+ }
342
+
343
+ /** A relationship's kind (an association when it names none, or one not known). */
344
+ static of(relationship) {
345
+ return RelationshipKind.KINDS.get(relationship?.kind) ?? RelationshipKind.KINDS.get('association');
346
+ }
347
+
348
+ /** Every kind, in the order registered. */
349
+ static all() {
350
+ return [...RelationshipKind.KINDS.values()];
351
+ }
352
+
353
+ get kind() { return this.constructor.kind; }
354
+ get label() { return this.constructor.label; }
355
+
356
+ /** Whether it has a cardinality at each end. */
357
+ get hasCardinality() {
358
+ return true;
359
+ }
360
+
361
+ /** Whether a notation draws it as a node of its own (Chen's diamond). */
362
+ drawnAsNode(notation) {
363
+ return false;
364
+ }
365
+
366
+ /** The graphDiagram edge it is in a notation. @abstract */
367
+ edge(relationship, notation) {
368
+ throw new Error(`${this.constructor.name} must implement edge()`);
369
+ }
370
+ }
371
+
372
+ /** An association: a cardinality at each end, drawn as each notation draws one (a Chen diamond). */
373
+ export class AssociationKind extends RelationshipKind {
374
+ static kind = 'association';
375
+ static label = 'Association';
376
+
377
+ drawnAsNode(notation) {
378
+ return !!notation.relationshipAsNode;
379
+ }
380
+
381
+ edge(relationship, notation) {
382
+ return {
383
+ ...relationship,
384
+ label: relationship.name,
385
+ lineStyle: relationship.identifying === false ? 'dashed' : 'solid',
386
+ arrowheadStart: notation.cardinalityArrowhead(relationship.sourceCardinality) || 'none',
387
+ arrowhead: notation.cardinalityArrowhead(relationship.targetCardinality) || 'none',
388
+ sourceLabel: notation.cardinalityLabel(relationship.sourceCardinality),
389
+ targetLabel: notation.cardinalityLabel(relationship.targetCardinality),
390
+ };
391
+ }
392
+ }
393
+
394
+ /**
395
+ * A generalization (DMBOK2's subtypes and supertypes): the source a subtype
396
+ * of the target. `complete` (every instance of the supertype is one of its
397
+ * subtypes) and `disjoint` (an instance is one subtype at most) are said on
398
+ * it. Drawn in every notation as a line to the supertype ending in a hollow
399
+ * triangle, its constraints its label (Chen's an ISA one); no cardinalities.
400
+ */
401
+ export class GeneralizationKind extends RelationshipKind {
402
+ static kind = 'generalization';
403
+ static label = 'Generalization (subtype of)';
404
+
405
+ get hasCardinality() {
406
+ return false;
407
+ }
408
+
409
+ /** Its constraints in words: '{complete, disjoint}'. */
410
+ static constraints(relationship) {
411
+ return `{${relationship.complete ? 'complete' : 'incomplete'}, ${relationship.disjoint === false ? 'overlapping' : 'disjoint'}}`;
412
+ }
413
+
414
+ edge(relationship, notation) {
415
+ const { sourceCardinality, targetCardinality, ...rest } = relationship;
416
+ const words = GeneralizationKind.constraints(relationship);
417
+ return {
418
+ ...rest,
419
+ label: notation.relationshipAsNode ? `ISA ${words}` : words,
420
+ lineStyle: 'solid', arrowhead: 'triangle-hollow', arrowheadStart: 'none',
421
+ };
422
+ }
423
+ }
424
+
425
+ RelationshipKind.register(AssociationKind);
426
+ RelationshipKind.register(GeneralizationKind);
427
+
428
+ /**
429
+ * EntityRole — what an entity is in a dimensional scheme (DMBOK2's data
430
+ * warehousing and BI, after Kimball): a plain entity (`EntityRole`, none), a
431
+ * fact (`FactRole`: what is measured, at a stated `grain`, its attributes
432
+ * `measure`s) or a dimension (`DimensionRole`: what facts are looked at by,
433
+ * its `scdType` how it keeps history, `conformed` when facts share it). An
434
+ * entity's `role` names one; each says how it is drawn in the star view.
435
+ */
436
+ export class EntityRole {
437
+ static role = '';
438
+ static label = 'None';
439
+ /** The colour its entities are drawn in, in the star view. */
440
+ static color = null;
441
+
442
+ /** Entity roles by their `role` ('' the plain one). */
443
+ static ROLES = new Map();
444
+
445
+ static register(RoleClass) {
446
+ EntityRole.ROLES.set(RoleClass.role, new RoleClass());
447
+ }
448
+
449
+ /** An entity's role (the plain one when it names none, or one not known). */
450
+ static of(entity) {
451
+ return EntityRole.ROLES.get(entity?.role ?? '') ?? EntityRole.ROLES.get('');
452
+ }
453
+
454
+ static all() {
455
+ return [...EntityRole.ROLES.values()];
456
+ }
457
+
458
+ get role() { return this.constructor.role; }
459
+ get label() { return this.constructor.label; }
460
+ get color() { return this.constructor.color; }
461
+
462
+ /** What is said of an entity in this role, for a hover: `{}` here. */
463
+ details(entity) {
464
+ return {};
465
+ }
466
+
467
+ /** The keys this role adds to an entity (FactRole's grain...): none here. */
468
+ get keys() {
469
+ return [];
470
+ }
471
+ }
472
+
473
+ /** A fact: measured, at a grain. */
474
+ export class FactRole extends EntityRole {
475
+ static role = 'fact';
476
+ static label = 'Fact';
477
+ static color = '#fde68a';
478
+
479
+ details(entity) {
480
+ return { role: 'fact', ...(entity.grain && { grain: entity.grain }) };
481
+ }
482
+
483
+ get keys() {
484
+ return ['grain'];
485
+ }
486
+ }
487
+
488
+ /** A dimension: what facts are looked at by. */
489
+ export class DimensionRole extends EntityRole {
490
+ static role = 'dimension';
491
+ static label = 'Dimension';
492
+ static color = '#bfdbfe';
493
+ /** Slowly changing dimension types: how a dimension keeps history. */
494
+ static SCD_TYPES = Object.freeze([['0', 'Type 0: never changes'], ['1', 'Type 1: overwritten'], ['2', 'Type 2: a row per version'], ['3', 'Type 3: the previous value kept']]);
495
+
496
+ details(entity) {
497
+ return {
498
+ role: entity.conformed ? 'conformed dimension' : 'dimension',
499
+ ...(entity.scdType !== undefined && entity.scdType !== '' && { history: DimensionRole.SCD_TYPES.find(([t]) => t === String(entity.scdType))?.[1] ?? `type ${entity.scdType}` }),
500
+ };
501
+ }
502
+
503
+ get keys() {
504
+ return ['scdType', 'conformed'];
505
+ }
506
+ }
507
+
508
+ EntityRole.register(EntityRole);
509
+ EntityRole.register(FactRole);
510
+ EntityRole.register(DimensionRole);
511
+
512
+ /** How a fact's measure adds up across its dimensions. */
513
+ const MEASURES = Object.freeze(['additive', 'semi-additive', 'non-additive']);
514
+
123
515
  export const DataModelNotation = {
124
516
  levels() { return Object.values(LEVELS); },
125
517
  levelNames() { return Object.keys(LEVELS); },
@@ -129,4 +521,10 @@ export const DataModelNotation = {
129
521
  notation(name) { return NOTATIONS[name] || NOTATIONS.crowsfoot; },
130
522
  isCardinality(value) { return CARDINALITIES.has(value); },
131
523
  cardinalities() { return [...CARDINALITIES]; },
524
+ /** A relationship's kind (RelationshipKind). */
525
+ relationshipKind(relationship) { return RelationshipKind.of(relationship); },
526
+ /** An entity's role in a dimensional scheme (EntityRole). */
527
+ entityRole(entity) { return EntityRole.of(entity); },
528
+ /** How a fact's measures add up: additive, semi-additive, non-additive. */
529
+ measures() { return [...MEASURES]; },
132
530
  };