@api-client/core 0.18.13 → 0.18.15
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/build/src/modeling/DomainValidation.d.ts +78 -0
- package/build/src/modeling/DomainValidation.d.ts.map +1 -1
- package/build/src/modeling/DomainValidation.js +78 -0
- package/build/src/modeling/DomainValidation.js.map +1 -1
- package/build/src/modeling/importers/FilteringJsonSchemaImporter.d.ts +84 -0
- package/build/src/modeling/importers/FilteringJsonSchemaImporter.d.ts.map +1 -0
- package/build/src/modeling/importers/FilteringJsonSchemaImporter.js +254 -0
- package/build/src/modeling/importers/FilteringJsonSchemaImporter.js.map +1 -0
- package/build/src/modeling/importers/SchemaFilteringStrategy.d.ts +72 -0
- package/build/src/modeling/importers/SchemaFilteringStrategy.d.ts.map +1 -0
- package/build/src/modeling/importers/SchemaFilteringStrategy.js +140 -0
- package/build/src/modeling/importers/SchemaFilteringStrategy.js.map +1 -0
- package/build/src/modeling/validation/entity_validation.d.ts +17 -3
- package/build/src/modeling/validation/entity_validation.d.ts.map +1 -1
- package/build/src/modeling/validation/entity_validation.js +51 -10
- package/build/src/modeling/validation/entity_validation.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +15 -15
- package/package.json +1 -1
- package/src/modeling/DomainValidation.ts +78 -0
- package/src/modeling/importers/FilteringJsonSchemaImporter.ts +324 -0
- package/src/modeling/importers/SchemaFilteringStrategy.ts +201 -0
- package/src/modeling/validation/entity_validation.ts +59 -10
- package/tests/unit/modeling/importers/schema_filtering.spec.ts +764 -0
- package/tests/unit/modeling/validation/entity_validation.spec.ts +95 -0
|
@@ -42065,10 +42065,10 @@
|
|
|
42065
42065
|
"@id": "#191"
|
|
42066
42066
|
},
|
|
42067
42067
|
{
|
|
42068
|
-
"@id": "#
|
|
42068
|
+
"@id": "#194"
|
|
42069
42069
|
},
|
|
42070
42070
|
{
|
|
42071
|
-
"@id": "#
|
|
42071
|
+
"@id": "#197"
|
|
42072
42072
|
},
|
|
42073
42073
|
{
|
|
42074
42074
|
"@id": "#200"
|
|
@@ -42813,13 +42813,13 @@
|
|
|
42813
42813
|
"@id": "#210"
|
|
42814
42814
|
},
|
|
42815
42815
|
{
|
|
42816
|
-
"@id": "#
|
|
42816
|
+
"@id": "#219"
|
|
42817
42817
|
},
|
|
42818
42818
|
{
|
|
42819
|
-
"@id": "#
|
|
42819
|
+
"@id": "#213"
|
|
42820
42820
|
},
|
|
42821
42821
|
{
|
|
42822
|
-
"@id": "#
|
|
42822
|
+
"@id": "#216"
|
|
42823
42823
|
}
|
|
42824
42824
|
],
|
|
42825
42825
|
"doc:root": false,
|
|
@@ -43457,7 +43457,7 @@
|
|
|
43457
43457
|
"doc:ExternalDomainElement",
|
|
43458
43458
|
"doc:DomainElement"
|
|
43459
43459
|
],
|
|
43460
|
-
"doc:raw": "
|
|
43460
|
+
"doc:raw": "addressType: 'REGISTERED-OFFICE-ADDRESS'\nstreetName: 'UITBREIDINGSTRAAT'\nhouseNumber: '84'\nhouseNumberAddition: '/1'\npostalCode: '2600'\ncity: 'BERCHEM (ANTWERPEN)'\ncountry: 'Belgium'\ncountryCode: 'BE'\nfullFormatedAddress: \"UITBREIDINGSTRAAT 84 /1, 2600 BERCHEM (ANTWERPEN), BELIUM\"\n",
|
|
43461
43461
|
"core:mediaType": "application/yaml",
|
|
43462
43462
|
"sourcemaps:sources": [
|
|
43463
43463
|
{
|
|
@@ -43478,7 +43478,7 @@
|
|
|
43478
43478
|
"doc:ExternalDomainElement",
|
|
43479
43479
|
"doc:DomainElement"
|
|
43480
43480
|
],
|
|
43481
|
-
"doc:raw": "
|
|
43481
|
+
"doc:raw": "code: '5'\ndescription: 'Limited company'\n",
|
|
43482
43482
|
"core:mediaType": "application/yaml",
|
|
43483
43483
|
"sourcemaps:sources": [
|
|
43484
43484
|
{
|
|
@@ -44253,7 +44253,7 @@
|
|
|
44253
44253
|
"doc:ExternalDomainElement",
|
|
44254
44254
|
"doc:DomainElement"
|
|
44255
44255
|
],
|
|
44256
|
-
"doc:raw": "type: 'GENERAL'\
|
|
44256
|
+
"doc:raw": "-\n type: 'GENERAL'\n value: 'info@company.be'\n-\n type: 'IT_DEPT'\n value: 'it-service@company.be'\n",
|
|
44257
44257
|
"core:mediaType": "application/yaml",
|
|
44258
44258
|
"sourcemaps:sources": [
|
|
44259
44259
|
{
|
|
@@ -44274,7 +44274,7 @@
|
|
|
44274
44274
|
"doc:ExternalDomainElement",
|
|
44275
44275
|
"doc:DomainElement"
|
|
44276
44276
|
],
|
|
44277
|
-
"doc:raw": "
|
|
44277
|
+
"doc:raw": "type: \"GENERAL\"\nvalue: \"www.company.be\"\n",
|
|
44278
44278
|
"core:mediaType": "application/yaml",
|
|
44279
44279
|
"sourcemaps:sources": [
|
|
44280
44280
|
{
|
|
@@ -44295,7 +44295,7 @@
|
|
|
44295
44295
|
"doc:ExternalDomainElement",
|
|
44296
44296
|
"doc:DomainElement"
|
|
44297
44297
|
],
|
|
44298
|
-
"doc:raw": "type:
|
|
44298
|
+
"doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
|
|
44299
44299
|
"core:mediaType": "application/yaml",
|
|
44300
44300
|
"sourcemaps:sources": [
|
|
44301
44301
|
{
|
|
@@ -44761,12 +44761,12 @@
|
|
|
44761
44761
|
{
|
|
44762
44762
|
"@id": "#196/source-map/lexical/element_0",
|
|
44763
44763
|
"sourcemaps:element": "amf://id#196",
|
|
44764
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44764
|
+
"sourcemaps:value": "[(1,0)-(10,0)]"
|
|
44765
44765
|
},
|
|
44766
44766
|
{
|
|
44767
44767
|
"@id": "#199/source-map/lexical/element_0",
|
|
44768
44768
|
"sourcemaps:element": "amf://id#199",
|
|
44769
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44769
|
+
"sourcemaps:value": "[(1,0)-(3,0)]"
|
|
44770
44770
|
},
|
|
44771
44771
|
{
|
|
44772
44772
|
"@id": "#202/source-map/lexical/element_0",
|
|
@@ -45121,17 +45121,17 @@
|
|
|
45121
45121
|
{
|
|
45122
45122
|
"@id": "#215/source-map/lexical/element_0",
|
|
45123
45123
|
"sourcemaps:element": "amf://id#215",
|
|
45124
|
-
"sourcemaps:value": "[(1,0)-(
|
|
45124
|
+
"sourcemaps:value": "[(1,0)-(7,0)]"
|
|
45125
45125
|
},
|
|
45126
45126
|
{
|
|
45127
45127
|
"@id": "#218/source-map/lexical/element_0",
|
|
45128
45128
|
"sourcemaps:element": "amf://id#218",
|
|
45129
|
-
"sourcemaps:value": "[(1,0)-(
|
|
45129
|
+
"sourcemaps:value": "[(1,0)-(3,0)]"
|
|
45130
45130
|
},
|
|
45131
45131
|
{
|
|
45132
45132
|
"@id": "#221/source-map/lexical/element_0",
|
|
45133
45133
|
"sourcemaps:element": "amf://id#221",
|
|
45134
|
-
"sourcemaps:value": "[(1,0)-(
|
|
45134
|
+
"sourcemaps:value": "[(1,0)-(6,0)]"
|
|
45135
45135
|
},
|
|
45136
45136
|
{
|
|
45137
45137
|
"@id": "#338/source-map/synthesized-field/element_1",
|
package/package.json
CHANGED
|
@@ -6,6 +6,75 @@ import { EntityValidation } from './validation/entity_validation.js'
|
|
|
6
6
|
import { PropertyValidation } from './validation/property_validation.js'
|
|
7
7
|
import { SemanticValidation } from './validation/semantic_validation.js'
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* DomainValidation performs comprehensive validation on a data domain and its components.
|
|
11
|
+
*
|
|
12
|
+
* This class orchestrates validation across all domain elements including entities, properties,
|
|
13
|
+
* associations, and semantics. It ensures that the data domain is well-formed and follows
|
|
14
|
+
* established conventions before it can be published or used in API generation.
|
|
15
|
+
*
|
|
16
|
+
* ## Validation Scope
|
|
17
|
+
*
|
|
18
|
+
* The validation process covers:
|
|
19
|
+
* - **Entities**: Structure, naming, primary keys, and inheritance
|
|
20
|
+
* - **Properties**: Naming conventions, data types, and constraints
|
|
21
|
+
* - **Associations**: Relationships, targets, and naming
|
|
22
|
+
* - **Semantics**: Recommended patterns and best practices
|
|
23
|
+
*
|
|
24
|
+
* ## Validation Severity Levels
|
|
25
|
+
*
|
|
26
|
+
* - **Error**: Blocking issues that prevent domain publication
|
|
27
|
+
* - **Warning**: Issues that may cause problems but don't block publication
|
|
28
|
+
* - **Info**: Recommendations for best practices
|
|
29
|
+
*
|
|
30
|
+
* ## Usage
|
|
31
|
+
*
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const domain = new DataDomain()
|
|
34
|
+
* // ... add entities, properties, associations
|
|
35
|
+
*
|
|
36
|
+
* const validator = new DomainValidation(domain)
|
|
37
|
+
* const report = validator.validate()
|
|
38
|
+
*
|
|
39
|
+
* if (report.canProceed) {
|
|
40
|
+
* // Domain is valid and can be published
|
|
41
|
+
* } else {
|
|
42
|
+
* // Handle validation errors
|
|
43
|
+
* console.log(report.impact)
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* ## Validation Rules
|
|
48
|
+
*
|
|
49
|
+
* ### Entity Validation Rules
|
|
50
|
+
* - **Primary Key**: Each entity must have a primary key (can be inherited)
|
|
51
|
+
* - **Minimum Properties**: Entities should have properties or associations (can be inherited)
|
|
52
|
+
* - **Naming**: Entity names must follow PostgreSQL naming conventions
|
|
53
|
+
* - **Uniqueness**: Entity names must be unique within the domain
|
|
54
|
+
*
|
|
55
|
+
* ### Property Validation Rules
|
|
56
|
+
* - **Naming**: Properties must follow PostgreSQL column naming conventions
|
|
57
|
+
* - **Length**: Names must be 2-59 characters long
|
|
58
|
+
* - **Format**: Names must start with letter/underscore, contain only alphanumeric/underscore
|
|
59
|
+
* - **Reserved Words**: Names cannot be PostgreSQL reserved keywords
|
|
60
|
+
* - **Case**: Snake case is recommended (lowercase with underscores)
|
|
61
|
+
*
|
|
62
|
+
* ### Association Validation Rules
|
|
63
|
+
* - **Targets**: Associations must have at least one target entity
|
|
64
|
+
* - **Target Existence**: Target entities must exist in the domain
|
|
65
|
+
* - **Naming**: Same rules as properties
|
|
66
|
+
*
|
|
67
|
+
* ### Semantic Validation Rules
|
|
68
|
+
* - **User Entity**: Recommended to have at least one entity with User semantic
|
|
69
|
+
* - **Timestamps**: Recommended to have CreatedTimestamp and UpdatedTimestamp properties
|
|
70
|
+
* - **Soft Delete**: Recommended to have soft delete capability for entities
|
|
71
|
+
* - **Data Types**: Semantic-specific data type validation
|
|
72
|
+
*
|
|
73
|
+
* @see {@link EntityValidation} for detailed entity validation rules
|
|
74
|
+
* @see {@link PropertyValidation} for detailed property validation rules
|
|
75
|
+
* @see {@link AssociationValidation} for detailed association validation rules
|
|
76
|
+
* @see {@link SemanticValidation} for detailed semantic validation rules
|
|
77
|
+
*/
|
|
9
78
|
export class DomainValidation {
|
|
10
79
|
private root: DataDomain
|
|
11
80
|
|
|
@@ -13,6 +82,15 @@ export class DomainValidation {
|
|
|
13
82
|
this.root = root
|
|
14
83
|
}
|
|
15
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Performs comprehensive validation on the entire data domain.
|
|
87
|
+
*
|
|
88
|
+
* This method validates all entities, properties, associations, and semantics
|
|
89
|
+
* in the domain. It returns a detailed report of all validation issues found,
|
|
90
|
+
* categorized by severity level.
|
|
91
|
+
*
|
|
92
|
+
* @returns A comprehensive validation report with all issues found
|
|
93
|
+
*/
|
|
16
94
|
validate(): DomainImpactReport {
|
|
17
95
|
const result: DomainImpactReport = {
|
|
18
96
|
key: '',
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import type { JSONSchema7 } from 'json-schema'
|
|
2
|
+
import type { JsonImportReport, InMemorySchema } from './JsonSchemaImporter.js'
|
|
3
|
+
import { JsonSchemaImporter } from './JsonSchemaImporter.js'
|
|
4
|
+
import {
|
|
5
|
+
SchemaFilteringStrategy,
|
|
6
|
+
type SchemaFilteringOptions,
|
|
7
|
+
FILTERING_STRATEGIES,
|
|
8
|
+
} from './SchemaFilteringStrategy.js'
|
|
9
|
+
import type { DataDomain } from '../DataDomain.js'
|
|
10
|
+
import type { DomainEntity } from '../DomainEntity.js'
|
|
11
|
+
import type { ImportMessage } from './JsonSchemaImporter.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extended JsonSchemaImporter that supports filtering out non-structural schemas.
|
|
15
|
+
* This version can eliminate schemas that don't contribute meaningful structure
|
|
16
|
+
* to the data model, such as empty objects or conceptual marker types.
|
|
17
|
+
*/
|
|
18
|
+
export class FilteringJsonSchemaImporter extends JsonSchemaImporter {
|
|
19
|
+
private filteringStrategy: SchemaFilteringStrategy
|
|
20
|
+
|
|
21
|
+
constructor(domain: DataDomain, filteringOptions: SchemaFilteringOptions = {}) {
|
|
22
|
+
super(domain)
|
|
23
|
+
this.filteringStrategy = new SchemaFilteringStrategy(filteringOptions)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates an importer with a predefined filtering strategy.
|
|
28
|
+
*/
|
|
29
|
+
static withStrategy(
|
|
30
|
+
domain: DataDomain,
|
|
31
|
+
strategyName: keyof typeof FILTERING_STRATEGIES
|
|
32
|
+
): FilteringJsonSchemaImporter {
|
|
33
|
+
const strategy = FILTERING_STRATEGIES[strategyName]
|
|
34
|
+
// Access private options through a public method (would need to be added to SchemaFilteringStrategy)
|
|
35
|
+
const options = strategy['options'] as SchemaFilteringOptions
|
|
36
|
+
return new FilteringJsonSchemaImporter(domain, options)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates an importer optimized for API development.
|
|
41
|
+
* Excludes schemas that don't contribute to API structure.
|
|
42
|
+
*/
|
|
43
|
+
static forApiModeling(domain: DataDomain): FilteringJsonSchemaImporter {
|
|
44
|
+
return new FilteringJsonSchemaImporter(domain, {
|
|
45
|
+
excludeEmptyObjects: true,
|
|
46
|
+
excludeConceptualMarkers: true,
|
|
47
|
+
excludePatterns: [
|
|
48
|
+
/.*Enumeration$/, // Exclude *Enumeration schemas
|
|
49
|
+
/.*Type$/, // Exclude conceptual *Type schemas that are just markers
|
|
50
|
+
],
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates an importer optimized for database schema generation.
|
|
56
|
+
* Very strict filtering - only includes schemas with concrete properties.
|
|
57
|
+
*/
|
|
58
|
+
static forDatabaseModeling(domain: DataDomain): FilteringJsonSchemaImporter {
|
|
59
|
+
return new FilteringJsonSchemaImporter(domain, {
|
|
60
|
+
excludeEmptyObjects: true,
|
|
61
|
+
excludeConceptualMarkers: true,
|
|
62
|
+
customExclusionFilter: (schema: JSONSchema7) => {
|
|
63
|
+
// For database modeling, exclude anything that doesn't have properties or clear structure
|
|
64
|
+
return (
|
|
65
|
+
schema.type === 'object' &&
|
|
66
|
+
!schema.properties &&
|
|
67
|
+
!schema.additionalProperties &&
|
|
68
|
+
!schema.allOf?.some(
|
|
69
|
+
(subSchema) => typeof subSchema === 'object' && 'properties' in subSchema && subSchema.properties
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Override the base import method to apply filtering.
|
|
78
|
+
*/
|
|
79
|
+
override async import(schemas: InMemorySchema[], modelName: string): Promise<JsonImportReport> {
|
|
80
|
+
// Apply pre-filtering to the schemas before import
|
|
81
|
+
const filteredSchemas = this.preFilterSchemas(schemas)
|
|
82
|
+
|
|
83
|
+
// Get the raw import result with filtered schemas
|
|
84
|
+
const rawResult = await super.import(filteredSchemas.schemas, modelName)
|
|
85
|
+
|
|
86
|
+
// Apply post-filtering to clean up any remaining issues
|
|
87
|
+
const finalReport = this.postFilterReport(rawResult, filteredSchemas.messages)
|
|
88
|
+
|
|
89
|
+
return finalReport
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Pre-filters schemas before they are processed by the base importer.
|
|
94
|
+
*/
|
|
95
|
+
private preFilterSchemas(schemas: InMemorySchema[]): {
|
|
96
|
+
schemas: InMemorySchema[]
|
|
97
|
+
messages: ImportMessage[]
|
|
98
|
+
} {
|
|
99
|
+
const filteredSchemas: InMemorySchema[] = []
|
|
100
|
+
const messages: ImportMessage[] = []
|
|
101
|
+
|
|
102
|
+
for (const schema of schemas) {
|
|
103
|
+
const schemaId = schema.contents.$id || schema.path
|
|
104
|
+
|
|
105
|
+
if (this.filteringStrategy.shouldExcludeSchema(schema.contents, schemaId)) {
|
|
106
|
+
const reason = this.filteringStrategy.getExclusionReason(schema.contents, schemaId)
|
|
107
|
+
messages.push({
|
|
108
|
+
level: 'info',
|
|
109
|
+
message: `Pre-filtered schema '${schemaId}': ${reason}`,
|
|
110
|
+
location: schema.path,
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
filteredSchemas.push(schema)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (messages.length > 0) {
|
|
118
|
+
messages.push({
|
|
119
|
+
level: 'info',
|
|
120
|
+
message: `Pre-filtering removed ${messages.length} non-structural schema(s)`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { schemas: filteredSchemas, messages }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Post-processes the import report to clean up any remaining filtered entities.
|
|
129
|
+
*/
|
|
130
|
+
private postFilterReport(report: JsonImportReport, preFilterMessages: ImportMessage[]): JsonImportReport {
|
|
131
|
+
const { model, messages } = report
|
|
132
|
+
const allMessages = [...preFilterMessages, ...messages]
|
|
133
|
+
let removedCount = 0
|
|
134
|
+
|
|
135
|
+
// Get entities as array for easier processing
|
|
136
|
+
const entities = Array.from(model.listEntities())
|
|
137
|
+
const entitiesToRemove: string[] = []
|
|
138
|
+
|
|
139
|
+
// Check each entity to see if it should be filtered out
|
|
140
|
+
for (const entity of entities) {
|
|
141
|
+
const shouldFilter = this.shouldFilterEntity(entity)
|
|
142
|
+
|
|
143
|
+
if (shouldFilter) {
|
|
144
|
+
entitiesToRemove.push(entity.key)
|
|
145
|
+
const reason = this.getEntityFilterReason(entity)
|
|
146
|
+
allMessages.push({
|
|
147
|
+
level: 'info',
|
|
148
|
+
message: `Post-filtered entity '${entity.info.name}': ${reason}`,
|
|
149
|
+
location: entity.key,
|
|
150
|
+
})
|
|
151
|
+
removedCount++
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Remove the filtered entities
|
|
156
|
+
for (const entityKey of entitiesToRemove) {
|
|
157
|
+
try {
|
|
158
|
+
model.removeEntity(entityKey)
|
|
159
|
+
} catch (error) {
|
|
160
|
+
allMessages.push({
|
|
161
|
+
level: 'warning',
|
|
162
|
+
message: `Failed to remove entity '${entityKey}': ${error}`,
|
|
163
|
+
location: entityKey,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Clean up orphaned associations
|
|
169
|
+
this.cleanupOrphanedAssociations(entities, entitiesToRemove, allMessages)
|
|
170
|
+
|
|
171
|
+
if (removedCount > 0) {
|
|
172
|
+
allMessages.push({
|
|
173
|
+
level: 'info',
|
|
174
|
+
message: `Post-filtering removed ${removedCount} additional non-structural entity/entities`,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
model,
|
|
180
|
+
messages: allMessages,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Determines if an entity should be filtered out based on its characteristics.
|
|
186
|
+
*/
|
|
187
|
+
private shouldFilterEntity(entity: DomainEntity): boolean {
|
|
188
|
+
// Check if entity has no properties and no meaningful associations
|
|
189
|
+
const hasProperties = Array.from(entity.listProperties()).length > 0
|
|
190
|
+
const hasAssociations = Array.from(entity.listAssociations()).length > 0
|
|
191
|
+
|
|
192
|
+
// If it has neither properties nor associations, it's likely a conceptual marker
|
|
193
|
+
if (!hasProperties && !hasAssociations) {
|
|
194
|
+
return this.filteringStrategy.shouldExcludeSchema(
|
|
195
|
+
{
|
|
196
|
+
type: 'object',
|
|
197
|
+
title: entity.info.name,
|
|
198
|
+
description: entity.info.description,
|
|
199
|
+
} as JSONSchema7,
|
|
200
|
+
entity.info.name
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Gets a human-readable reason for filtering an entity.
|
|
209
|
+
*/
|
|
210
|
+
private getEntityFilterReason(entity: DomainEntity): string {
|
|
211
|
+
const hasProperties = Array.from(entity.listProperties()).length > 0
|
|
212
|
+
const hasAssociations = Array.from(entity.listAssociations()).length > 0
|
|
213
|
+
|
|
214
|
+
if (!hasProperties && !hasAssociations) {
|
|
215
|
+
return 'Entity has no properties or associations (likely a conceptual marker)'
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return 'Entity matches exclusion pattern'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Removes associations that reference entities that were filtered out.
|
|
223
|
+
*/
|
|
224
|
+
private cleanupOrphanedAssociations(
|
|
225
|
+
entities: DomainEntity[],
|
|
226
|
+
removedEntityKeys: string[],
|
|
227
|
+
messages: ImportMessage[]
|
|
228
|
+
): void {
|
|
229
|
+
for (const entity of entities) {
|
|
230
|
+
const associationsToRemove: string[] = []
|
|
231
|
+
|
|
232
|
+
for (const association of entity.listAssociations()) {
|
|
233
|
+
const hasOrphanedTargets = association.targets?.some((target) => removedEntityKeys.includes(target.key))
|
|
234
|
+
|
|
235
|
+
if (hasOrphanedTargets) {
|
|
236
|
+
associationsToRemove.push(association.key)
|
|
237
|
+
messages.push({
|
|
238
|
+
level: 'info',
|
|
239
|
+
message: `Removed association '${association.info.name}' from '${entity.info.name}' due to filtered target entity`,
|
|
240
|
+
location: `${entity.key}.${association.key}`,
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const associationKey of associationsToRemove) {
|
|
246
|
+
try {
|
|
247
|
+
entity.removeAssociation(associationKey)
|
|
248
|
+
} catch (error) {
|
|
249
|
+
messages.push({
|
|
250
|
+
level: 'warning',
|
|
251
|
+
message: `Failed to remove association '${associationKey}' from '${entity.info.name}': ${error}`,
|
|
252
|
+
location: `${entity.key}.${associationKey}`,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Utility functions for working with filtered imports.
|
|
262
|
+
*/
|
|
263
|
+
export const SchemaFilteringUtils = {
|
|
264
|
+
/**
|
|
265
|
+
* Analyzes a JSON schema to determine if it would be filtered by the given strategy.
|
|
266
|
+
*/
|
|
267
|
+
wouldBeFiltered(schema: JSONSchema7, strategy: SchemaFilteringStrategy): boolean {
|
|
268
|
+
return strategy.shouldExcludeSchema(schema)
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Gets statistics about what would be filtered from a set of schemas.
|
|
273
|
+
*/
|
|
274
|
+
getFilteringStats(
|
|
275
|
+
schemas: { id: string; schema: JSONSchema7 }[],
|
|
276
|
+
strategy: SchemaFilteringStrategy
|
|
277
|
+
): {
|
|
278
|
+
total: number
|
|
279
|
+
filtered: number
|
|
280
|
+
remaining: number
|
|
281
|
+
filteredSchemas: { id: string; reason: string }[]
|
|
282
|
+
} {
|
|
283
|
+
const filteredSchemas: { id: string; reason: string }[] = []
|
|
284
|
+
|
|
285
|
+
for (const { id, schema } of schemas) {
|
|
286
|
+
if (strategy.shouldExcludeSchema(schema, id)) {
|
|
287
|
+
filteredSchemas.push({
|
|
288
|
+
id,
|
|
289
|
+
reason: strategy.getExclusionReason(schema, id),
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
total: schemas.length,
|
|
296
|
+
filtered: filteredSchemas.length,
|
|
297
|
+
remaining: schemas.length - filteredSchemas.length,
|
|
298
|
+
filteredSchemas,
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Creates a report showing what would be filtered from a schema collection.
|
|
304
|
+
*/
|
|
305
|
+
createFilteringReport(schemas: { id: string; schema: JSONSchema7 }[], strategy: SchemaFilteringStrategy): string {
|
|
306
|
+
const stats = this.getFilteringStats(schemas, strategy)
|
|
307
|
+
|
|
308
|
+
let report = `Schema Filtering Report\n`
|
|
309
|
+
report += `======================\n\n`
|
|
310
|
+
report += `Total schemas: ${stats.total}\n`
|
|
311
|
+
report += `Would be filtered: ${stats.filtered}\n`
|
|
312
|
+
report += `Would remain: ${stats.remaining}\n\n`
|
|
313
|
+
|
|
314
|
+
if (stats.filteredSchemas.length > 0) {
|
|
315
|
+
report += `Schemas that would be filtered:\n`
|
|
316
|
+
report += `-------------------------------\n`
|
|
317
|
+
for (const { id, reason } of stats.filteredSchemas) {
|
|
318
|
+
report += `• ${id}: ${reason}\n`
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return report
|
|
323
|
+
},
|
|
324
|
+
}
|