@api-client/core 0.12.1 → 0.12.3

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/bin/plugins/sinon/assert.ts +29 -0
  2. package/bin/test.ts +2 -0
  3. package/build/src/browser.d.ts +4 -0
  4. package/build/src/browser.d.ts.map +1 -1
  5. package/build/src/browser.js +3 -0
  6. package/build/src/browser.js.map +1 -1
  7. package/build/src/index.d.ts +4 -0
  8. package/build/src/index.d.ts.map +1 -1
  9. package/build/src/index.js +3 -0
  10. package/build/src/index.js.map +1 -1
  11. package/build/src/modeling/DataDomain.d.ts +4 -0
  12. package/build/src/modeling/DataDomain.d.ts.map +1 -1
  13. package/build/src/modeling/DataDomain.js +11 -0
  14. package/build/src/modeling/DataDomain.js.map +1 -1
  15. package/build/src/modeling/DomainAssociation.js +1 -1
  16. package/build/src/modeling/DomainAssociation.js.map +1 -1
  17. package/build/src/modeling/DomainEntity.d.ts +46 -0
  18. package/build/src/modeling/DomainEntity.d.ts.map +1 -1
  19. package/build/src/modeling/DomainEntity.js +71 -0
  20. package/build/src/modeling/DomainEntity.js.map +1 -1
  21. package/build/src/modeling/DomainImpactAnalysis.d.ts +31 -8
  22. package/build/src/modeling/DomainImpactAnalysis.d.ts.map +1 -1
  23. package/build/src/modeling/DomainImpactAnalysis.js +118 -46
  24. package/build/src/modeling/DomainImpactAnalysis.js.map +1 -1
  25. package/build/src/modeling/DomainProperty.js +1 -1
  26. package/build/src/modeling/DomainProperty.js.map +1 -1
  27. package/build/src/modeling/validation/association_validation.d.ts +38 -0
  28. package/build/src/modeling/validation/association_validation.d.ts.map +1 -0
  29. package/build/src/modeling/validation/association_validation.js +108 -0
  30. package/build/src/modeling/validation/association_validation.js.map +1 -0
  31. package/build/src/modeling/validation/entity_validation.d.ts +52 -0
  32. package/build/src/modeling/validation/entity_validation.d.ts.map +1 -0
  33. package/build/src/modeling/validation/entity_validation.js +241 -0
  34. package/build/src/modeling/validation/entity_validation.js.map +1 -0
  35. package/build/src/modeling/validation/postgresql.d.ts +2 -0
  36. package/build/src/modeling/validation/postgresql.d.ts.map +1 -0
  37. package/build/src/modeling/validation/postgresql.js +58 -0
  38. package/build/src/modeling/validation/postgresql.js.map +1 -0
  39. package/build/src/modeling/validation/property_validation.d.ts +29 -0
  40. package/build/src/modeling/validation/property_validation.d.ts.map +1 -0
  41. package/build/src/modeling/validation/property_validation.js +58 -0
  42. package/build/src/modeling/validation/property_validation.js.map +1 -0
  43. package/build/src/modeling/validation/rules.d.ts +55 -0
  44. package/build/src/modeling/validation/rules.d.ts.map +1 -0
  45. package/build/src/modeling/validation/rules.js +110 -0
  46. package/build/src/modeling/validation/rules.js.map +1 -0
  47. package/package.json +1 -1
  48. package/src/modeling/DataDomain.ts +12 -0
  49. package/src/modeling/DomainAssociation.ts +1 -1
  50. package/src/modeling/DomainEntity.ts +75 -0
  51. package/src/modeling/DomainImpactAnalysis.ts +144 -54
  52. package/src/modeling/DomainProperty.ts +1 -1
  53. package/src/modeling/validation/association_validation.ts +109 -0
  54. package/src/modeling/validation/entity_validation.ts +246 -0
  55. package/src/modeling/validation/postgresql.ts +57 -0
  56. package/src/modeling/validation/property_validation.ts +58 -0
  57. package/src/modeling/validation/rules.ts +152 -0
  58. package/tests/unit/modeling/data_domain_associations.spec.ts +1 -1
  59. package/tests/unit/modeling/data_domain_property.spec.ts +1 -1
  60. package/tests/unit/modeling/domain.property.spec.ts +7 -7
  61. package/tests/unit/modeling/domain_asociation.spec.ts +3 -3
  62. package/tests/unit/modeling/domain_entity_associations.spec.ts +1 -1
  63. package/tests/unit/modeling/domain_entity_properties.spec.ts +2 -2
  64. package/tests/unit/modeling/domain_impact_analysis.spec.ts +138 -29
  65. package/tests/unit/modeling/validation/association_validation.spec.ts +157 -0
  66. package/tests/unit/modeling/validation/entity_validation.spec.ts +192 -0
  67. package/tests/unit/modeling/validation/property_validation.spec.ts +135 -0
@@ -868,6 +868,18 @@ export class DataDomain extends EventTarget {
868
868
  }
869
869
  }
870
870
 
871
+ /**
872
+ * Lists all entities in the graph that are not part of this domain.
873
+ */
874
+ *listForeignEntities(): Generator<DomainEntity> {
875
+ for (const node of this.graph.nodes()) {
876
+ const value = this.graph.node(node) as DomainGraphNodeType
877
+ if (value.kind === DomainEntityKind && value.domain.key !== this.key) {
878
+ yield value
879
+ }
880
+ }
881
+ }
882
+
871
883
  /**
872
884
  * Finds an entity by its key.
873
885
  *
@@ -150,7 +150,7 @@ export class DomainAssociation extends DomainElement {
150
150
  */
151
151
  static createSchema(input: Partial<DomainAssociationSchema> = {}): DomainAssociationSchema {
152
152
  const { key = nanoid() } = input
153
- const info = Thing.fromJSON(input.info, { name: 'New association' }).toJSON()
153
+ const info = Thing.fromJSON(input.info, { name: 'new_association' }).toJSON()
154
154
  const result: DomainAssociationSchema = {
155
155
  kind: DomainAssociationKind,
156
156
  key,
@@ -92,6 +92,35 @@ export interface DomainEntitySchema extends DomainElementSchema {
92
92
  * const userModel = dataDomain.addModel({ key: 'userModel' });
93
93
  * const userEntity = userModel.addEntity({ key: 'user' });
94
94
  * ```
95
+ *
96
+ * Entity Validation Rules:
97
+ *
98
+ * - **Primary Key:** An entity must have a primary key selected. This can come from a parent
99
+ * entity or be defined in the current entity.
100
+ * - **Parent Entity:** The parent entity must exist in the graph.
101
+ * - **Circular Dependency:** Adding a parent entity that creates a circular dependency is not allowed.
102
+ * - **Unique Parent:** An entity cannot have the same parent more than once.
103
+ * - **Property Shadowing:** A property can shadow a parent property. This means that if a property
104
+ * with the same key exists in both the parent and child entity, the child property will take precedence.
105
+ * - **Association Shadowing:** An association can shadow a parent association. This means that if an
106
+ * association with the same key exists in both the parent and child entity,
107
+ * the child association will take precedence.
108
+ * - **Property Resolution:** When the same property exists more than one parent and the child entity,
109
+ * doesn't explicitly define the property, then:
110
+ * - The last parent entity (iterating from the first entity to the last) that has the property
111
+ * will be used as the source of the property.
112
+ * - **Association Resolution:** When the same association exists more than one parent and the child entity,
113
+ * doesn't explicitly define the association, then:
114
+ * - The last parent entity (iterating from the first entity to the last) that has the association
115
+ * will be used as the source of the association.
116
+ * - **Minimum Required Properties:** An entity must have at least one property defined. Entities without properties
117
+ * are ignored in the system.
118
+ * - **Entity Name**:
119
+ * - An entity must have a name defined.
120
+ * - The name has to follow the same rules as the names in a SQL database. This means that the name must be unique
121
+ * within the data domain and cannot contain special characters.
122
+ * - **Association Targets:** An association must have a target entity defined.
123
+ * This means that the association must point to at least one entity.
95
124
  */
96
125
  export class DomainEntity extends DomainElement {
97
126
  /**
@@ -291,6 +320,13 @@ export class DomainEntity extends DomainElement {
291
320
  this.root.notifyChange()
292
321
  }
293
322
 
323
+ /**
324
+ * A shortcut to `listProperties()`.
325
+ */
326
+ get properties(): Generator<DomainProperty> {
327
+ return this.listProperties()
328
+ }
329
+
294
330
  /**
295
331
  * Lists all properties of this entity.
296
332
  *
@@ -333,6 +369,13 @@ export class DomainEntity extends DomainElement {
333
369
  return this.fields.some((item) => item.type === 'property')
334
370
  }
335
371
 
372
+ /**
373
+ * A shortcut to `listParents()`.
374
+ */
375
+ get parents(): Generator<DomainEntity> {
376
+ return this.listParents()
377
+ }
378
+
336
379
  /**
337
380
  * Lists all parent entities of this entity.
338
381
  *
@@ -622,6 +665,13 @@ export class DomainEntity extends DomainElement {
622
665
  this.root.notifyChange()
623
666
  }
624
667
 
668
+ /**
669
+ * A shortcut to `listAssociations()`.
670
+ */
671
+ get associations(): Generator<DomainAssociation> {
672
+ return this.listAssociations()
673
+ }
674
+
625
675
  /**
626
676
  * Lists all associations of this entity.
627
677
  *
@@ -766,4 +816,29 @@ export class DomainEntity extends DomainElement {
766
816
  }
767
817
  return parent.isChildOf(key)
768
818
  }
819
+
820
+ /**
821
+ * Finds the primary key property of this entity.
822
+ * @returns The primary key property of this entity or undefined if not found.
823
+ */
824
+ primaryKey(): DomainProperty | undefined {
825
+ for (const property of this.properties) {
826
+ if (property.primary) {
827
+ return property
828
+ }
829
+ }
830
+ let property: DomainProperty | undefined
831
+ for (const parent of this.listParents()) {
832
+ const prop = parent.primaryKey()
833
+ if (prop) {
834
+ // According to the validation rules, the last parent
835
+ // that has the property will be used as the source of
836
+ // the property.
837
+ // In the future, we may add a schema definition to the entity that
838
+ // will explicitly define which property to use.
839
+ property = prop
840
+ }
841
+ }
842
+ return property
843
+ }
769
844
  }
@@ -7,6 +7,9 @@ import {
7
7
  DataDomainKind,
8
8
  } from '../models/kinds.js'
9
9
  import type { DataDomain } from './DataDomain.js'
10
+ import { AssociationValidation } from './validation/association_validation.js'
11
+ import { EntityValidation } from './validation/entity_validation.js'
12
+ import { PropertyValidation } from './validation/property_validation.js'
10
13
 
11
14
  export type DomainImpactKinds =
12
15
  | typeof DomainNamespaceKind
@@ -55,14 +58,26 @@ export interface DomainImpactItem {
55
58
  *
56
59
  * - `delete` - The data object would be deleted.
57
60
  */
58
- type: 'delete'
61
+ type: 'delete' | 'publish'
59
62
  /**
60
63
  * The impact description.
64
+ * Explains what will happen to the impacted data object.
65
+ * This is a human-readable description of the impact.
66
+ * It should be clear and concise.
61
67
  */
62
68
  impact: string
69
+ /**
70
+ * The severity of the impact.
71
+ *
72
+ * - `info` - The impact is informational.
73
+ * - `warning` - The impact can potentially cause problems but is not a blocker.
74
+ * - `error` - The impact is a blocker and needs to be resolved before proceeding.
75
+ */
76
+ severity: 'info' | 'warning' | 'error'
63
77
  /**
64
78
  * Whether the impact is blocking the operation.
65
79
  * If true, the operation cannot proceed.
80
+ * @deprecated Use `severity` instead.
66
81
  */
67
82
  blocking: boolean
68
83
  /**
@@ -73,6 +88,11 @@ export interface DomainImpactItem {
73
88
  * The resolution of the conflict if the change will be forced.
74
89
  */
75
90
  resolution?: string
91
+ /**
92
+ * The optional parent of the impacted data object.
93
+ * For example, if the impacted item is a property, this will be the entity it belongs to.
94
+ */
95
+ parent?: string
76
96
  }
77
97
 
78
98
  /**
@@ -317,7 +337,7 @@ export class DomainImpactAnalysis {
317
337
  impact: [],
318
338
  canProceed: true,
319
339
  }
320
- this.report.impact = this.createDeleteImpact(key, kind, key)
340
+ this.createDeleteImpact(key, kind, key)
321
341
  return this.report
322
342
  }
323
343
 
@@ -333,83 +353,156 @@ export class DomainImpactAnalysis {
333
353
  impact: [],
334
354
  canProceed: true,
335
355
  }
336
- this.report.impact = this.createRemoveForeignNamespaceImpact(key)
356
+ this.createRemoveForeignNamespaceImpact(key)
357
+ return this.report
358
+ }
359
+
360
+ /**
361
+ * Analyzes the data domain for publishing. Essentially, it performs a validation of the data domain
362
+ * and returns a report of the impact.
363
+ * @returns The publish impact analysis report.
364
+ */
365
+ publishAnalysis(): DomainImpactReport {
366
+ this.report = {
367
+ key: '',
368
+ kind: DataDomainKind,
369
+ impact: [],
370
+ canProceed: true,
371
+ }
372
+ const entityValidator = new EntityValidation(this.root)
373
+ const propertyValidator = new PropertyValidation(this.root)
374
+ const associationValidator = new AssociationValidation(this.root)
375
+ for (const entity of this.root.listEntities()) {
376
+ if (entity.domain.key !== this.root.key) {
377
+ // we don't need to validate foreign entities
378
+ continue
379
+ }
380
+ const report = entityValidator.validate(entity)
381
+ for (const item of report) {
382
+ const blocking = item.severity === 'error'
383
+ this.report.canProceed = this.report.canProceed && !blocking
384
+ this.report.impact.push({
385
+ key: item.key,
386
+ kind: item.kind,
387
+ type: 'publish',
388
+ impact: item.message,
389
+ blocking,
390
+ resolution: item.help,
391
+ severity: item.severity,
392
+ parent: item.parent,
393
+ })
394
+ }
395
+ for (const property of entity.properties) {
396
+ const report = propertyValidator.validate(property)
397
+ for (const item of report) {
398
+ const blocking = item.severity === 'error'
399
+ this.report.canProceed = this.report.canProceed && !blocking
400
+ this.report.impact.push({
401
+ key: item.key,
402
+ kind: item.kind,
403
+ type: 'publish',
404
+ impact: item.message,
405
+ blocking,
406
+ resolution: item.help,
407
+ severity: item.severity,
408
+ parent: item.parent,
409
+ })
410
+ }
411
+ }
412
+ for (const association of entity.associations) {
413
+ const report = associationValidator.validate(association)
414
+ for (const item of report) {
415
+ const blocking = item.severity === 'error'
416
+ this.report.canProceed = this.report.canProceed && !blocking
417
+ this.report.impact.push({
418
+ key: item.key,
419
+ kind: item.kind,
420
+ type: 'publish',
421
+ impact: item.message,
422
+ blocking,
423
+ resolution: item.help,
424
+ severity: item.severity,
425
+ parent: item.parent,
426
+ })
427
+ }
428
+ }
429
+ }
337
430
  return this.report
338
431
  }
339
432
 
340
- protected createDeleteImpact(key: string, kind: DomainImpactKinds, rootKey: string): DomainImpactItem[] {
433
+ protected createDeleteImpact(key: string, kind: DomainImpactKinds, rootKey: string): void {
341
434
  switch (kind) {
342
435
  case DomainNamespaceKind:
343
- return this.deleteNamespaceAnalysis(key, rootKey)
436
+ this.deleteNamespaceAnalysis(key, rootKey)
437
+ break
344
438
  case DomainModelKind:
345
- return this.deleteDataModelAnalysis(key, rootKey)
439
+ this.deleteDataModelAnalysis(key, rootKey)
440
+ break
346
441
  case DomainEntityKind:
347
- return this.deleteEntityAnalysis(key, rootKey)
442
+ this.deleteEntityAnalysis(key, rootKey)
443
+ break
348
444
  case DomainPropertyKind:
349
- return this.deletePropertyAnalysis(key)
445
+ this.deletePropertyAnalysis(key)
446
+ break
350
447
  case DomainAssociationKind:
351
- return this.deleteAssociationAnalysis(key)
448
+ this.deleteAssociationAnalysis(key)
449
+ break
352
450
  default:
353
- return []
451
+ // ignore unknown kinds
354
452
  }
355
453
  }
356
454
 
357
- protected deleteNamespaceAnalysis(key: string, rootKey: string): DomainImpactItem[] {
358
- const result: DomainImpactItem[] = []
455
+ protected deleteNamespaceAnalysis(key: string, rootKey: string): void {
359
456
  const ns = this.root.findNamespace(key)
360
457
  if (!ns) {
361
- return result
458
+ return
362
459
  }
363
- result.push({
460
+ this.report.impact.push({
364
461
  key: ns.key,
365
462
  kind: ns.kind,
366
463
  type: 'delete',
367
464
  impact: `The ${ns.info.getLabel()} ${this.kindToLabel(DomainNamespaceKind)} will be deleted.`,
368
465
  blocking: false,
466
+ severity: 'info',
369
467
  })
370
468
  for (const child of ns.listNamespaces()) {
371
- const items = this.deleteNamespaceAnalysis(child.key, rootKey)
372
- result.push(...items)
469
+ this.deleteNamespaceAnalysis(child.key, rootKey)
373
470
  }
374
471
  for (const child of ns.listModels()) {
375
- const items = this.deleteDataModelAnalysis(child.key, rootKey)
376
- result.push(...items)
472
+ this.deleteDataModelAnalysis(child.key, rootKey)
377
473
  }
378
- return result
379
474
  }
380
475
 
381
- protected deleteDataModelAnalysis(key: string, rootKey: string): DomainImpactItem[] {
382
- const result: DomainImpactItem[] = []
476
+ protected deleteDataModelAnalysis(key: string, rootKey: string): void {
383
477
  const model = this.root.findModel(key)
384
478
  if (!model) {
385
- return result
479
+ return
386
480
  }
387
- result.push({
481
+ this.report.impact.push({
388
482
  key: model.key,
389
483
  kind: model.kind,
390
484
  type: 'delete',
391
485
  impact: `The ${model.info.getLabel()} ${this.kindToLabel(DomainModelKind)} will be deleted.`,
392
486
  blocking: false,
487
+ severity: 'info',
393
488
  })
394
489
  for (const child of model.listEntities()) {
395
- const items = this.deleteEntityAnalysis(child.key, rootKey)
396
- result.push(...items)
490
+ this.deleteEntityAnalysis(child.key, rootKey)
397
491
  }
398
- return result
399
492
  }
400
493
 
401
- protected deleteEntityAnalysis(key: string, rootKey: string): DomainImpactItem[] {
402
- const result: DomainImpactItem[] = []
494
+ protected deleteEntityAnalysis(key: string, rootKey: string): void {
403
495
  const entity = this.root.findEntity(key)
404
496
  if (!entity) {
405
- return result
497
+ return
406
498
  }
407
- result.push({
499
+ this.report.impact.push({
408
500
  key: entity.key,
409
501
  kind: entity.kind,
410
502
  type: 'delete',
411
503
  impact: `The ${entity.info.getLabel()} ${this.kindToLabel(DomainEntityKind)} will be deleted.`,
412
504
  blocking: false,
505
+ severity: 'info',
413
506
  })
414
507
 
415
508
  // We need to know whether the entity is a parent of another entity
@@ -428,14 +521,15 @@ export class DomainImpactAnalysis {
428
521
  }
429
522
  const pLabel = entity.info.getLabel()
430
523
  const cLabel = childEntity.info.getLabel()
431
- result.push({
524
+ this.report.impact.push({
432
525
  key: childEntity.key,
433
526
  kind: childEntity.kind,
434
527
  type: 'delete',
435
528
  impact: `The "${cLabel}" ${this.kindToLabel(DomainEntityKind)} will become an orphan because it is a child of the "${pLabel}" entity.`,
436
529
  resolution: `The "${pLabel}" entity will be removed as the parent of the "${cLabel}" entity.`,
437
- blocking: true,
530
+ blocking: false,
438
531
  relationship: 'child',
532
+ severity: 'error',
439
533
  })
440
534
  this.report.canProceed = false
441
535
  }
@@ -460,57 +554,53 @@ export class DomainImpactAnalysis {
460
554
 
461
555
  const aLabel = association.info.getLabel()
462
556
  const eLabel = entity.info.getLabel()
463
- result.push({
557
+ this.report.impact.push({
464
558
  key: association.key,
465
559
  kind: association.kind,
466
560
  type: 'delete',
467
561
  impact: `The ${aLabel} ${this.kindToLabel(DomainAssociationKind)} will be broken because it has a target to ${eLabel}.`,
468
562
  resolution: `The ${aLabel} ${this.kindToLabel(DomainAssociationKind)} will be removed from ${eLabel}.`,
469
563
  blocking: true,
564
+ severity: 'error',
470
565
  })
471
566
  this.report.canProceed = false
472
567
  }
473
568
  for (const child of entity.listProperties()) {
474
- const items = this.deletePropertyAnalysis(child.key)
475
- result.push(...items)
569
+ this.deletePropertyAnalysis(child.key)
476
570
  }
477
571
  for (const child of entity.listAssociations()) {
478
- const items = this.deleteAssociationAnalysis(child.key)
479
- result.push(...items)
572
+ this.deleteAssociationAnalysis(child.key)
480
573
  }
481
- return result
482
574
  }
483
575
 
484
- protected deletePropertyAnalysis(key: string): DomainImpactItem[] {
485
- const result: DomainImpactItem[] = []
576
+ protected deletePropertyAnalysis(key: string): void {
486
577
  const property = this.root.findProperty(key)
487
578
  if (!property) {
488
- return result
579
+ return
489
580
  }
490
- result.push({
581
+ this.report.impact.push({
491
582
  key: property.key,
492
583
  kind: property.kind,
493
584
  type: 'delete',
494
585
  impact: `The ${property.info.getLabel()} ${this.kindToLabel(DomainPropertyKind)} will be deleted.`,
495
586
  blocking: false,
587
+ severity: 'info',
496
588
  })
497
- return result
498
589
  }
499
590
 
500
- protected deleteAssociationAnalysis(key: string): DomainImpactItem[] {
501
- const result: DomainImpactItem[] = []
591
+ protected deleteAssociationAnalysis(key: string): void {
502
592
  const association = this.root.findAssociation(key)
503
593
  if (!association) {
504
- return result
594
+ return
505
595
  }
506
- result.push({
596
+ this.report.impact.push({
507
597
  key: association.key,
508
598
  kind: association.kind,
509
599
  type: 'delete',
510
600
  impact: `The ${association.info.getLabel()} ${this.kindToLabel(DomainAssociationKind)} will be deleted.`,
511
601
  blocking: false,
602
+ severity: 'info',
512
603
  })
513
- return result
514
604
  }
515
605
 
516
606
  protected kindToLabel(kind: DomainImpactKinds): string {
@@ -530,11 +620,10 @@ export class DomainImpactAnalysis {
530
620
  }
531
621
  }
532
622
 
533
- protected createRemoveForeignNamespaceImpact(key: string): DomainImpactItem[] {
534
- const result: DomainImpactItem[] = []
623
+ protected createRemoveForeignNamespaceImpact(key: string): void {
535
624
  const foreignNamespace = this.root.dependencies.get(key)
536
625
  if (!foreignNamespace) {
537
- return result
626
+ return
538
627
  }
539
628
  // Check for parent relationships to foreign entities
540
629
  for (const entity of this.root.listEntities()) {
@@ -551,7 +640,7 @@ export class DomainImpactAnalysis {
551
640
  }
552
641
  const eLabel = entity.info.getLabel()
553
642
  const pLabel = parentEntity.info.getLabel()
554
- result.push({
643
+ this.report.impact.push({
555
644
  key: entity.key,
556
645
  kind: entity.kind,
557
646
  type: 'delete',
@@ -559,6 +648,7 @@ export class DomainImpactAnalysis {
559
648
  resolution: `The "${pLabel}" entity will be removed as the parent of the "${eLabel}" entity.`,
560
649
  blocking: true,
561
650
  relationship: 'child',
651
+ severity: 'error',
562
652
  })
563
653
  this.report.canProceed = false
564
654
  } else if (edge.type === 'association') {
@@ -576,13 +666,14 @@ export class DomainImpactAnalysis {
576
666
  const aLabel = association.info.getLabel()
577
667
  const eLabel = entity.info.getLabel()
578
668
  const tLabel = targetEntity.info.getLabel()
579
- result.push({
669
+ this.report.impact.push({
580
670
  key: association.key,
581
671
  kind: association.kind,
582
672
  type: 'delete',
583
673
  impact: `The "${aLabel}" ${this.kindToLabel(DomainAssociationKind)} from "${eLabel}" will be broken because it targets "${tLabel}" in the foreign namespace "${foreignNamespace.key}".`,
584
674
  resolution: `The "${aLabel}" ${this.kindToLabel(DomainAssociationKind)} will be removed from "${eLabel}".`,
585
675
  blocking: true,
676
+ severity: 'error',
586
677
  })
587
678
  this.report.canProceed = false
588
679
  }
@@ -590,6 +681,5 @@ export class DomainImpactAnalysis {
590
681
  }
591
682
  }
592
683
  }
593
- return result
594
684
  }
595
685
  }
@@ -242,7 +242,7 @@ export class DomainProperty extends DomainElement {
242
242
  throw new Error(`Invalid data property type ${type}`)
243
243
  }
244
244
  }
245
- const info = Thing.fromJSON(input.info, { name: 'New property' }).toJSON()
245
+ const info = Thing.fromJSON(input.info, { name: 'new_property' }).toJSON()
246
246
  const result: DomainPropertySchema = {
247
247
  kind: DomainPropertyKind,
248
248
  key,
@@ -0,0 +1,109 @@
1
+ import { DomainAssociationKind } from '../../models/kinds.js'
2
+ import type { DataDomain } from '../DataDomain.js'
3
+ import type { DomainAssociation } from '../DomainAssociation.js'
4
+ import type { DomainEntity } from '../DomainEntity.js'
5
+ import { type DomainValidation, validatePropertyName } from './rules.js'
6
+
7
+ /**
8
+ * AssociationValidation is a class that performs validation on associations in a data domain.
9
+ * Note that an association in most cases is a property of an entity.
10
+ */
11
+ export class AssociationValidation {
12
+ constructor(protected domain: DataDomain) {}
13
+ /**
14
+ * Performs all the validation rules on the association.
15
+ * If you are interested in a specific rule, use the specific method.
16
+ * @param target The target association to validate. Can be a string with
17
+ * the association key or a DomainAssociation object.
18
+ */
19
+ validate(target: string | DomainAssociation): DomainValidation[] {
20
+ const results: DomainValidation[] = []
21
+ let association: DomainAssociation | undefined
22
+ if (typeof target === 'string') {
23
+ association = this.domain.findAssociation(target)
24
+ } else {
25
+ association = target
26
+ }
27
+ if (!association) {
28
+ const message = `The "${target}" association does not exist.`
29
+ const help = `The association must be defined in the domain.`
30
+ results.push({
31
+ field: '*',
32
+ rule: 'exists',
33
+ message,
34
+ help,
35
+ key: target as string,
36
+ kind: DomainAssociationKind,
37
+ severity: 'error',
38
+ })
39
+ return results
40
+ }
41
+ const name = this.validateName(association)
42
+ results.push(...name)
43
+ const targets = this.validateTargets(association)
44
+ results.push(...targets)
45
+ return results
46
+ }
47
+
48
+ /**
49
+ * Validates the association name.
50
+ *
51
+ * @remarks
52
+ * - A association must have a name defined.
53
+ * - The name has to follow the same rules as the names in a PostgreSQL database.
54
+ * - Column names can only contain letters (a-z, A-Z), numbers (0-9), and underscores (_).
55
+ * - The name must start with a letter (a-z, A-Z) or an underscore (_).
56
+ * - PostgreSQL limits column names to a maximum of 59 characters.
57
+ * - (our rule) Column names are case insensitive.
58
+ * - (recommendation) Column names should be in lower case.
59
+ * @param association The association to validate
60
+ */
61
+ validateName(association: DomainAssociation): DomainValidation[] {
62
+ return validatePropertyName(association)
63
+ }
64
+
65
+ /**
66
+ * Validates the association targets.
67
+ * @param association The association to validate
68
+ */
69
+ validateTargets(association: DomainAssociation): DomainValidation[] {
70
+ const results: DomainValidation[] = []
71
+ const label = association.info.getLabel()
72
+ const parentEntity = association.getParentInstance() as DomainEntity
73
+ if (!association.targets.length) {
74
+ const message = `The "${label}" association has no target.`
75
+ const help = `An association must have at least one target.`
76
+ results.push({
77
+ field: 'targets',
78
+ rule: 'required',
79
+ message,
80
+ help,
81
+ severity: 'error',
82
+ key: association.key,
83
+ kind: association.kind,
84
+ parent: parentEntity.key,
85
+ })
86
+ return results
87
+ }
88
+ for (const target of association.targets) {
89
+ const entity = target.domain
90
+ ? this.domain.findForeignEntity(target.key, target.domain)
91
+ : this.domain.findEntity(target.key)
92
+ if (!entity) {
93
+ const message = `The "${label}" association has an invalid target "${target.key}".`
94
+ const help = `The target must be defined in the domain.`
95
+ results.push({
96
+ field: 'targets',
97
+ rule: 'exists',
98
+ message,
99
+ help,
100
+ severity: 'error',
101
+ key: association.key,
102
+ kind: association.kind,
103
+ parent: parentEntity.key,
104
+ })
105
+ }
106
+ }
107
+ return results
108
+ }
109
+ }