@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
@@ -7,12 +7,13 @@ import { ModelCheckDialog } from '../../charting/ModelCheckDialog.js';
7
7
  import { DataModelNotation } from '../../datamodel.js';
8
8
  import {
9
9
  DataModel, DataModelPerspective, DataModelContext, DataModelFileFormat, ServiceDomain, ServiceLandscape, BianLandscapeReader,
10
- BianSemanticApiReader, DataType, EnumerationType, StructuredType,
10
+ BianSemanticApiReader, DataType, EnumerationType, StructuredType, NamingStandard, ClassificationScheme, ClassificationLens,
11
+ SqlDdlFileFormat, ModelScorecard,
11
12
  } from '../../charting/datamodel/index.js';
12
13
  import { pickFile } from '../../charting/toolbar/pickFile.js';
13
14
  import { ModelFile } from './ModelFile.js';
14
15
  import { EditorPart } from './EditorPart.js';
15
- import { downloadPng, downloadSvg } from '../../exportImage.js';
16
+ import { downloadPng, downloadSvg, downloadBlob } from '../../exportImage.js';
16
17
 
17
18
  /**
18
19
  * DataModelPart — a data model (DataModel, drawn by dataModelDiagram) in a
@@ -96,7 +97,13 @@ export class DataModelPart extends ChartPart {
96
97
  this._adoptModel(model);
97
98
  this.perspectiveId = null;
98
99
  this.contextId = null;
99
- if (this.mounted) this.render();
100
+ if (!this.mounted) return;
101
+ // Not rendered again (that would add a second toolbar and canvas): the toolbar
102
+ // shows the new model's choices, and it is drawn. What was on screen is the
103
+ // other model's, so where its boxes were is not kept into this one.
104
+ this._drawnLevel = null;
105
+ this._changed();
106
+ this._redraw();
100
107
  }
101
108
 
102
109
  /** A model opened from a file in a host's new tab (as EditorPart does). */
@@ -208,9 +215,131 @@ export class DataModelPart extends ChartPart {
208
215
  ...this.chartOptions,
209
216
  ...this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: this.level }),
210
217
  ...(this.level === DataModel.LANDSCAPE && { onNodeDoubleClick: (node) => this.openServiceDomain(ServiceLandscape.serviceDomainOf(node.id)) }),
218
+ ...(this.editsLevel && this.editingOptions()),
219
+ ...(this.level === DataModel.MAPPING && this.lineageOptions()),
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Show an entity's lineage: the mapping view, what it realises and what
225
+ * realises it (DataModel#lineage) as they are, the rest greyed.
226
+ */
227
+ showLineage(level, id) {
228
+ this.lineageFocus = { level, id };
229
+ this.setLevel(DataModel.MAPPING);
230
+ }
231
+
232
+ /**
233
+ * In the mapping view: an entity's lineage picked out (`lineageFocus`), and
234
+ * its menus to show another's or show all again.
235
+ */
236
+ lineageOptions() {
237
+ const view = {};
238
+ const focus = this.lineageFocus;
239
+ if (focus) {
240
+ const { up, down } = this.model.lineage(focus.level, focus.id);
241
+ const on = new Set([`${focus.level}:${focus.id}`, ...[...up, ...down].map(({ level, entity }) => `${level}:${entity.id}`)]);
242
+ const drawn = this.model.view({ perspective: this.perspectiveId, context: this.contextId, level: DataModel.MAPPING });
243
+ view.entities = drawn.entities.map(e => (on.has(e.id) ? e : { ...e, color: '#f3f4f6' }));
244
+ view.links = drawn.links.map(l => (on.has(l.source) && on.has(l.target) ? l : { ...l, color: '#e5e7eb' }));
245
+ }
246
+ const split = (nodeId) => { const i = String(nodeId).indexOf(':'); return [nodeId.slice(0, i), nodeId.slice(i + 1)]; };
247
+ return {
248
+ ...view,
249
+ contextMenuItems: [{ label: 'Show lineage', action: (node) => { const [level, id] = split(node.id); this.showLineage(level, id); } }],
250
+ canvasContextMenuItems: [{ label: 'Show all', action: () => { this.lineageFocus = null; this._redraw(); } }],
211
251
  };
212
252
  }
213
253
 
254
+ /** Whether what is shown is a level of the schema (so its entities and relationships can be edited here). */
255
+ get editsLevel() {
256
+ return !DataModel.otherView(this.level);
257
+ }
258
+
259
+ /**
260
+ * At a level of the schema: double-click an entity or a relationship to
261
+ * edit it; right-click one to edit or delete it, or the canvas for a new
262
+ * entity there or a new relationship. (These stand in for graphDiagram's
263
+ * own, which change only the drawing.)
264
+ */
265
+ editingOptions() {
266
+ const relationshipOf = (drawn) => this.model.relationshipIndex(this.level, drawn);
267
+ const isEntity = (node) => !!this.model.level(this.level).entity(node.id);
268
+ return {
269
+ onNodeDoubleClick: (node) => (isEntity(node) ? this.editEntity(node.id) : this.editRelationship(relationshipOf(node))),
270
+ onEdgeDoubleClick: (edge) => this.editRelationship(relationshipOf(edge)),
271
+ contextMenuItems: [
272
+ { label: 'Edit…', action: (node) => (isEntity(node) ? this.editEntity(node.id) : this.editRelationship(relationshipOf(node))) },
273
+ { label: 'Delete', action: (node) => (isEntity(node) ? this.removeEntity(node.id) : this.removeRelationship(relationshipOf(node))) },
274
+ { label: 'Show lineage', action: (node) => { if (isEntity(node)) this.showLineage(this.level, node.id); } },
275
+ ],
276
+ edgeContextMenuItems: [
277
+ { label: 'Edit relationship…', action: (edge) => this.editRelationship(relationshipOf(edge)) },
278
+ { label: 'Delete relationship', action: (edge) => this.removeRelationship(relationshipOf(edge)) },
279
+ ],
280
+ canvasContextMenuItems: [
281
+ { label: `New ${this.levelInfo.entityNoun.toLowerCase()} here…`, action: (subject, engine, point) => this.editEntity(null, point) },
282
+ { label: 'New relationship…', action: () => this.editRelationship(null) },
283
+ ],
284
+ };
285
+ }
286
+
287
+ /** The level shown, as DataModelNotation has it (its nouns, its dialogs' fields). */
288
+ get levelInfo() {
289
+ return DataModelNotation.level(this.level);
290
+ }
291
+
292
+ /**
293
+ * An entity's dialog at the level shown (none: a new one, placed at
294
+ * `point` or below the others): its level's fields (name, definition, and
295
+ * at the logical and physical levels its attributes as a table). Saved, it
296
+ * is one undo step.
297
+ */
298
+ editEntity(id, point = null) {
299
+ if (!this.editsLevel) return null;
300
+ const info = this.levelInfo;
301
+ const schema = this.model.level(this.level);
302
+ const entity = id ? schema.entity(id) : null;
303
+ if (id && !entity) return null;
304
+ const scheme = this.model.classificationScheme;
305
+ const context = { typeNames: this.model.dataTypes.map(t => t.name), sensitivities: scheme.levels.map(l => l.name), categories: scheme.categories };
306
+ const title = entity ? `${info.entityNoun}: ${entity.name}` : `New ${info.entityNoun.toLowerCase()}`;
307
+ return this._dialog(title, info.entityFields(entity, context), (values) => {
308
+ const spec = info.entitySpecFrom(values, entity);
309
+ if (!entity) Object.assign(spec, point ?? this._freeSpot(schema));
310
+ this.runModelEdit(this.model.saveEntityCommand(this.level, id, spec));
311
+ });
312
+ }
313
+
314
+ /** Where a new entity goes with no point given: below what is there. @private */
315
+ _freeSpot(schema) {
316
+ const ys = schema.entities.map(e => e.y).filter(Number.isFinite);
317
+ return { x: 160, y: (ys.length ? Math.max(...ys) : 0) + 160 };
318
+ }
319
+
320
+ removeEntity(id) {
321
+ if (!this.editsLevel || !this.model.level(this.level).entity(id)) return null;
322
+ return this.runModelEdit(this.model.removeEntityCommand(this.level, id));
323
+ }
324
+
325
+ /** A relationship's dialog at the level shown (`index` null or -1: a new one); saved, one undo step. */
326
+ editRelationship(index) {
327
+ if (!this.editsLevel) return null;
328
+ const info = this.levelInfo;
329
+ const schema = this.model.level(this.level);
330
+ const relationship = index == null || index < 0 ? null : schema.relationships[index];
331
+ if (!schema.entities.length) return null;
332
+ const title = relationship ? `Relationship: ${relationship.name || `${relationship.source} – ${relationship.target}`}` : 'New relationship';
333
+ return this._dialog(title, info.relationshipFields(schema.entities, relationship), (values) => {
334
+ this.runModelEdit(this.model.saveRelationshipCommand(this.level, relationship ? index : null, info.relationshipSpecFrom(values, relationship)));
335
+ }, ({ source, target }) => (source && target ? null : 'Choose both ends'));
336
+ }
337
+
338
+ removeRelationship(index) {
339
+ if (!this.editsLevel || index == null || index < 0) return null;
340
+ return this.runModelEdit(this.model.removeRelationshipCommand(this.level, index));
341
+ }
342
+
214
343
  /** See the model through a service domain (an id), at the logical level, where its business objects are. */
215
344
  openServiceDomain(id) {
216
345
  if (!this.model.perspective(id)) return;
@@ -260,8 +389,12 @@ export class DataModelPart extends ChartPart {
260
389
  /** Model ▸: make, change or remove a perspective or a context (the one chosen). */
261
390
  modelItems() {
262
391
  return [
392
+ { label: `New ${this.editsLevel ? this.levelInfo.entityNoun.toLowerCase() : 'entity'}…`, disabled: !this.editsLevel || undefined, action: () => this.editEntity(null) },
393
+ { label: 'New relationship…', disabled: !this.editsLevel || !this.model.level(this.level).entities.length || undefined, action: () => this.editRelationship(null) },
394
+ { separator: true },
263
395
  { label: 'New perspective…', action: () => this.editPerspective(null) },
264
396
  { label: 'New service domain…', action: () => this.editPerspective(null, ServiceDomain) },
397
+ { label: 'New classification lens…', action: () => this.editPerspective(null, ClassificationLens) },
265
398
  { label: 'Read service domains from a BIAN landscape…', action: () => this.readLandscape() },
266
399
  { label: 'Read a BIAN service domain (semantic API)…', action: () => this.readSemanticApi() },
267
400
  { label: 'Edit perspective…', disabled: !this.perspective || undefined, action: () => this.editPerspective(this.perspective) },
@@ -274,6 +407,9 @@ export class DataModelPart extends ChartPart {
274
407
  { label: 'Delete context', disabled: !this.context || undefined, action: () => this.removeContext(this.contextId) },
275
408
  { separator: true },
276
409
  { label: 'Check model…', action: () => this.checkModel() },
410
+ { label: 'Scorecard…', action: () => this.scoreModel() },
411
+ ...EditorPart.menuGroup('Naming standard', this.namingItems()),
412
+ { label: 'Classification scheme…', action: () => this.editClassificationScheme() },
277
413
  ];
278
414
  }
279
415
 
@@ -365,6 +501,51 @@ export class DataModelPart extends ChartPart {
365
501
  return command;
366
502
  }
367
503
 
504
+ /**
505
+ * The Data Model Scorecard (ModelScorecard): each category as the model
506
+ * measures it, a reviewer's score and note, its weight; the total in the
507
+ * title. Saved, the reviews and weights are kept (one undo step).
508
+ */
509
+ scoreModel() {
510
+ const card = this.model.scored();
511
+ const results = card.results();
512
+ const { score, coverage } = card.total(results);
513
+ const title = score == null ? 'Scorecard: nothing scored yet' : `Scorecard: ${score} of 100 (${coverage}% of the weight scored)`;
514
+ return this._dialog(title, card.fields(results), (values) => {
515
+ this.runModelEdit(this.model.scorecardCommand(ModelScorecard.specFrom(values)));
516
+ }, () => null);
517
+ }
518
+
519
+ /** The classification scheme's dialog (its sensitivity levels in order, its categories); saved, one undo step. */
520
+ editClassificationScheme() {
521
+ return this._dialog('Classification scheme', ClassificationScheme.fields(this.model.classificationScheme), (values) => {
522
+ this.runModelEdit(this.model.classificationSchemeCommand(ClassificationScheme.specFrom(values)));
523
+ }, (values) => (ClassificationScheme.specFrom(values).levels.length ? null : 'Give it at least one level'));
524
+ }
525
+
526
+ /** Model ▸ Naming standard ▸: edit it, start from the conventional one, or remove it (Check model… checks names against it). */
527
+ namingItems() {
528
+ const has = !!this.model.namingStandard;
529
+ return [
530
+ { label: has ? 'Edit…' : 'Set…', action: () => this.editNamingStandard() },
531
+ ...(has ? [] : [{ label: 'Use the conventional one', action: () => this.runModelEdit(this.model.namingStandardCommand(NamingStandard.CONVENTIONAL)) }]),
532
+ ...(has ? [{ label: 'Remove', action: () => this.runModelEdit(this.model.namingStandardCommand(null)) }] : []),
533
+ ];
534
+ }
535
+
536
+ /**
537
+ * The naming standard's dialog (NamingStandard.fields: a rule per level for
538
+ * entities and attributes); saved, one undo step. A standard that asks
539
+ * nothing is none.
540
+ */
541
+ editNamingStandard() {
542
+ return this._dialog('Naming standard', NamingStandard.fields(this.model.namingStandard), (values) => {
543
+ const spec = NamingStandard.specFrom(values);
544
+ const asks = Object.values(spec).some(kinds => Object.values(kinds).some(r => r.style || r.classWords.length || r.maxLength || Object.keys(r.abbreviations).length));
545
+ this.runModelEdit(this.model.namingStandardCommand(asks ? spec : null));
546
+ }, () => null);
547
+ }
548
+
368
549
  /** Model ▸ Data types ▸: a new one of each kind, then each to change or remove. */
369
550
  dataTypeItems() {
370
551
  const types = this.model.dataTypes;
@@ -454,12 +635,11 @@ export class DataModelPart extends ChartPart {
454
635
  }
455
636
 
456
637
  /** Open a form (a name is needed). @private */
457
- _dialog(title, fields, apply) {
638
+ _dialog(title, fields, apply, validate = ({ name }) => ((name ?? '').trim() ? null : 'Give it a name')) {
458
639
  return new FormDialog({
459
640
  title, fields, submitLabel: 'Save', idPrefix: 'data-model-',
460
641
  document: this.container?.ownerDocument ?? globalThis.document,
461
- validate: ({ name }) => ((name ?? '').trim() ? null : 'Give it a name'),
462
- onApply: apply,
642
+ validate, onApply: apply,
463
643
  }).open();
464
644
  }
465
645
 
@@ -475,12 +655,23 @@ export class DataModelPart extends ChartPart {
475
655
  return [
476
656
  { label: 'Image (PNG)', action: () => downloadPng(this.canvasHost, { name }) },
477
657
  { label: 'Image (SVG)', action: () => downloadSvg(this.canvasHost, { name }) },
658
+ { separator: true },
659
+ { label: 'SQL DDL (physical level)', action: () => this.exportDdl() },
478
660
  ];
479
661
  }
480
662
 
663
+ /** The physical level as SQL DDL (SqlDdlFileFormat), downloaded as the model's name. Returns the file. */
664
+ exportDdl({ download = (file, fileName) => downloadBlob(new (this.document.defaultView ?? globalThis).Blob([file.data], { type: file.mimeType }), fileName) } = {}) {
665
+ const file = new SqlDdlFileFormat().export(this.model);
666
+ download(file, `${String(this.model.name || this.title || 'model').replace(/[^A-Za-z0-9._-]+/g, '-')}.sql`);
667
+ return file;
668
+ }
669
+
481
670
  /** Redraw now (a choice made), when there is somewhere to draw. @private */
482
671
  _redraw() {
483
672
  if (this.canvasHost) this._draw();
673
+ // Its toolbar shows what is drawn, however it was chosen (a finding, a lineage, a service domain opened).
674
+ this.menuBar?.refresh?.();
484
675
  }
485
676
 
486
677
  /**