@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.
- package/bin/plugins/sinon/assert.ts +29 -0
- package/bin/test.ts +2 -0
- package/build/src/browser.d.ts +4 -0
- package/build/src/browser.d.ts.map +1 -1
- package/build/src/browser.js +3 -0
- package/build/src/browser.js.map +1 -1
- package/build/src/index.d.ts +4 -0
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +3 -0
- package/build/src/index.js.map +1 -1
- package/build/src/modeling/DataDomain.d.ts +4 -0
- package/build/src/modeling/DataDomain.d.ts.map +1 -1
- package/build/src/modeling/DataDomain.js +11 -0
- package/build/src/modeling/DataDomain.js.map +1 -1
- package/build/src/modeling/DomainAssociation.js +1 -1
- package/build/src/modeling/DomainAssociation.js.map +1 -1
- package/build/src/modeling/DomainEntity.d.ts +46 -0
- package/build/src/modeling/DomainEntity.d.ts.map +1 -1
- package/build/src/modeling/DomainEntity.js +71 -0
- package/build/src/modeling/DomainEntity.js.map +1 -1
- package/build/src/modeling/DomainImpactAnalysis.d.ts +31 -8
- package/build/src/modeling/DomainImpactAnalysis.d.ts.map +1 -1
- package/build/src/modeling/DomainImpactAnalysis.js +118 -46
- package/build/src/modeling/DomainImpactAnalysis.js.map +1 -1
- package/build/src/modeling/DomainProperty.js +1 -1
- package/build/src/modeling/DomainProperty.js.map +1 -1
- package/build/src/modeling/validation/association_validation.d.ts +38 -0
- package/build/src/modeling/validation/association_validation.d.ts.map +1 -0
- package/build/src/modeling/validation/association_validation.js +108 -0
- package/build/src/modeling/validation/association_validation.js.map +1 -0
- package/build/src/modeling/validation/entity_validation.d.ts +52 -0
- package/build/src/modeling/validation/entity_validation.d.ts.map +1 -0
- package/build/src/modeling/validation/entity_validation.js +241 -0
- package/build/src/modeling/validation/entity_validation.js.map +1 -0
- package/build/src/modeling/validation/postgresql.d.ts +2 -0
- package/build/src/modeling/validation/postgresql.d.ts.map +1 -0
- package/build/src/modeling/validation/postgresql.js +58 -0
- package/build/src/modeling/validation/postgresql.js.map +1 -0
- package/build/src/modeling/validation/property_validation.d.ts +29 -0
- package/build/src/modeling/validation/property_validation.d.ts.map +1 -0
- package/build/src/modeling/validation/property_validation.js +58 -0
- package/build/src/modeling/validation/property_validation.js.map +1 -0
- package/build/src/modeling/validation/rules.d.ts +55 -0
- package/build/src/modeling/validation/rules.d.ts.map +1 -0
- package/build/src/modeling/validation/rules.js +110 -0
- package/build/src/modeling/validation/rules.js.map +1 -0
- package/package.json +1 -1
- package/src/modeling/DataDomain.ts +12 -0
- package/src/modeling/DomainAssociation.ts +1 -1
- package/src/modeling/DomainEntity.ts +75 -0
- package/src/modeling/DomainImpactAnalysis.ts +144 -54
- package/src/modeling/DomainProperty.ts +1 -1
- package/src/modeling/validation/association_validation.ts +109 -0
- package/src/modeling/validation/entity_validation.ts +246 -0
- package/src/modeling/validation/postgresql.ts +57 -0
- package/src/modeling/validation/property_validation.ts +58 -0
- package/src/modeling/validation/rules.ts +152 -0
- package/tests/unit/modeling/data_domain_associations.spec.ts +1 -1
- package/tests/unit/modeling/data_domain_property.spec.ts +1 -1
- package/tests/unit/modeling/domain.property.spec.ts +7 -7
- package/tests/unit/modeling/domain_asociation.spec.ts +3 -3
- package/tests/unit/modeling/domain_entity_associations.spec.ts +1 -1
- package/tests/unit/modeling/domain_entity_properties.spec.ts +2 -2
- package/tests/unit/modeling/domain_impact_analysis.spec.ts +138 -29
- package/tests/unit/modeling/validation/association_validation.spec.ts +157 -0
- package/tests/unit/modeling/validation/entity_validation.spec.ts +192 -0
- 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: '
|
|
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.
|
|
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.
|
|
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):
|
|
433
|
+
protected createDeleteImpact(key: string, kind: DomainImpactKinds, rootKey: string): void {
|
|
341
434
|
switch (kind) {
|
|
342
435
|
case DomainNamespaceKind:
|
|
343
|
-
|
|
436
|
+
this.deleteNamespaceAnalysis(key, rootKey)
|
|
437
|
+
break
|
|
344
438
|
case DomainModelKind:
|
|
345
|
-
|
|
439
|
+
this.deleteDataModelAnalysis(key, rootKey)
|
|
440
|
+
break
|
|
346
441
|
case DomainEntityKind:
|
|
347
|
-
|
|
442
|
+
this.deleteEntityAnalysis(key, rootKey)
|
|
443
|
+
break
|
|
348
444
|
case DomainPropertyKind:
|
|
349
|
-
|
|
445
|
+
this.deletePropertyAnalysis(key)
|
|
446
|
+
break
|
|
350
447
|
case DomainAssociationKind:
|
|
351
|
-
|
|
448
|
+
this.deleteAssociationAnalysis(key)
|
|
449
|
+
break
|
|
352
450
|
default:
|
|
353
|
-
|
|
451
|
+
// ignore unknown kinds
|
|
354
452
|
}
|
|
355
453
|
}
|
|
356
454
|
|
|
357
|
-
protected deleteNamespaceAnalysis(key: string, rootKey: string):
|
|
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
|
|
458
|
+
return
|
|
362
459
|
}
|
|
363
|
-
|
|
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
|
-
|
|
372
|
-
result.push(...items)
|
|
469
|
+
this.deleteNamespaceAnalysis(child.key, rootKey)
|
|
373
470
|
}
|
|
374
471
|
for (const child of ns.listModels()) {
|
|
375
|
-
|
|
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):
|
|
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
|
|
479
|
+
return
|
|
386
480
|
}
|
|
387
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
|
497
|
+
return
|
|
406
498
|
}
|
|
407
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
result.push(...items)
|
|
569
|
+
this.deletePropertyAnalysis(child.key)
|
|
476
570
|
}
|
|
477
571
|
for (const child of entity.listAssociations()) {
|
|
478
|
-
|
|
479
|
-
result.push(...items)
|
|
572
|
+
this.deleteAssociationAnalysis(child.key)
|
|
480
573
|
}
|
|
481
|
-
return result
|
|
482
574
|
}
|
|
483
575
|
|
|
484
|
-
protected deletePropertyAnalysis(key: string):
|
|
485
|
-
const result: DomainImpactItem[] = []
|
|
576
|
+
protected deletePropertyAnalysis(key: string): void {
|
|
486
577
|
const property = this.root.findProperty(key)
|
|
487
578
|
if (!property) {
|
|
488
|
-
return
|
|
579
|
+
return
|
|
489
580
|
}
|
|
490
|
-
|
|
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):
|
|
501
|
-
const result: DomainImpactItem[] = []
|
|
591
|
+
protected deleteAssociationAnalysis(key: string): void {
|
|
502
592
|
const association = this.root.findAssociation(key)
|
|
503
593
|
if (!association) {
|
|
504
|
-
return
|
|
594
|
+
return
|
|
505
595
|
}
|
|
506
|
-
|
|
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):
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
+
}
|