@api-client/core 0.18.14 → 0.18.16
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/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 +134 -0
- package/build/src/modeling/importers/SchemaFilteringStrategy.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +10 -10
- package/package.json +1 -1
- package/src/modeling/importers/FilteringJsonSchemaImporter.ts +324 -0
- package/src/modeling/importers/SchemaFilteringStrategy.ts +193 -0
- package/tests/unit/modeling/importers/schema_filtering.spec.ts +808 -0
|
@@ -42071,10 +42071,10 @@
|
|
|
42071
42071
|
"@id": "#197"
|
|
42072
42072
|
},
|
|
42073
42073
|
{
|
|
42074
|
-
"@id": "#
|
|
42074
|
+
"@id": "#203"
|
|
42075
42075
|
},
|
|
42076
42076
|
{
|
|
42077
|
-
"@id": "#
|
|
42077
|
+
"@id": "#200"
|
|
42078
42078
|
},
|
|
42079
42079
|
{
|
|
42080
42080
|
"@id": "#206"
|
|
@@ -42810,10 +42810,10 @@
|
|
|
42810
42810
|
"@id": "#219"
|
|
42811
42811
|
},
|
|
42812
42812
|
{
|
|
42813
|
-
"@id": "#
|
|
42813
|
+
"@id": "#213"
|
|
42814
42814
|
},
|
|
42815
42815
|
{
|
|
42816
|
-
"@id": "#
|
|
42816
|
+
"@id": "#210"
|
|
42817
42817
|
},
|
|
42818
42818
|
{
|
|
42819
42819
|
"@id": "#216"
|
|
@@ -43499,7 +43499,7 @@
|
|
|
43499
43499
|
"doc:ExternalDomainElement",
|
|
43500
43500
|
"doc:DomainElement"
|
|
43501
43501
|
],
|
|
43502
|
-
"doc:raw": "
|
|
43502
|
+
"doc:raw": "code: 'J'\ndescription: 'Information and communication'\n",
|
|
43503
43503
|
"core:mediaType": "application/yaml",
|
|
43504
43504
|
"sourcemaps:sources": [
|
|
43505
43505
|
{
|
|
@@ -43520,7 +43520,7 @@
|
|
|
43520
43520
|
"doc:ExternalDomainElement",
|
|
43521
43521
|
"doc:DomainElement"
|
|
43522
43522
|
],
|
|
43523
|
-
"doc:raw": "
|
|
43523
|
+
"doc:raw": "class: '3'\ndescription: '150 - 300'\nnumberOfFte: 5500\nnumberOfEmployees: 5232\n",
|
|
43524
43524
|
"core:mediaType": "application/yaml",
|
|
43525
43525
|
"sourcemaps:sources": [
|
|
43526
43526
|
{
|
|
@@ -44232,7 +44232,7 @@
|
|
|
44232
44232
|
"doc:ExternalDomainElement",
|
|
44233
44233
|
"doc:DomainElement"
|
|
44234
44234
|
],
|
|
44235
|
-
"doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '
|
|
44235
|
+
"doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '21'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)21 302099'\n",
|
|
44236
44236
|
"core:mediaType": "application/yaml",
|
|
44237
44237
|
"sourcemaps:sources": [
|
|
44238
44238
|
{
|
|
@@ -44253,7 +44253,7 @@
|
|
|
44253
44253
|
"doc:ExternalDomainElement",
|
|
44254
44254
|
"doc:DomainElement"
|
|
44255
44255
|
],
|
|
44256
|
-
"doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '
|
|
44256
|
+
"doc:raw": "type: 'GENERAL'\ncountryDialCode : '+32'\nareaCode : '22'\nsubscriberNumber: '12.87.00'\nformatted: '+32-(0)22 000000'\n",
|
|
44257
44257
|
"core:mediaType": "application/yaml",
|
|
44258
44258
|
"sourcemaps:sources": [
|
|
44259
44259
|
{
|
|
@@ -44771,12 +44771,12 @@
|
|
|
44771
44771
|
{
|
|
44772
44772
|
"@id": "#202/source-map/lexical/element_0",
|
|
44773
44773
|
"sourcemaps:element": "amf://id#202",
|
|
44774
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44774
|
+
"sourcemaps:value": "[(1,0)-(3,0)]"
|
|
44775
44775
|
},
|
|
44776
44776
|
{
|
|
44777
44777
|
"@id": "#205/source-map/lexical/element_0",
|
|
44778
44778
|
"sourcemaps:element": "amf://id#205",
|
|
44779
|
-
"sourcemaps:value": "[(1,0)-(
|
|
44779
|
+
"sourcemaps:value": "[(1,0)-(5,0)]"
|
|
44780
44780
|
},
|
|
44781
44781
|
{
|
|
44782
44782
|
"@id": "#208/source-map/lexical/element_0",
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { JSONSchema7 } from 'json-schema'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for schema filtering during import.
|
|
5
|
+
*/
|
|
6
|
+
export interface SchemaFilteringOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Whether to exclude schemas that are empty objects with no properties.
|
|
9
|
+
* Default: false
|
|
10
|
+
*/
|
|
11
|
+
excludeEmptyObjects?: boolean
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to exclude schemas that only serve as conceptual markers.
|
|
15
|
+
* These are typically schemas with only type: "object" and description
|
|
16
|
+
* but no properties, constraints, or meaningful structure.
|
|
17
|
+
* Default: false
|
|
18
|
+
*/
|
|
19
|
+
excludeConceptualMarkers?: boolean
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Custom predicate function to determine if a schema should be excluded.
|
|
23
|
+
* Return true to exclude the schema from import.
|
|
24
|
+
*/
|
|
25
|
+
customExclusionFilter?: (schema: JSONSchema7, schemaId?: string) => boolean
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List of schema IDs to explicitly exclude.
|
|
29
|
+
*/
|
|
30
|
+
excludeSchemaIds?: string[]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Patterns to match against schema titles or IDs for exclusion.
|
|
34
|
+
*/
|
|
35
|
+
excludePatterns?: RegExp[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Strategy class for determining which schemas should be filtered out during import.
|
|
40
|
+
*/
|
|
41
|
+
export class SchemaFilteringStrategy {
|
|
42
|
+
constructor(private options: SchemaFilteringOptions = {}) {}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Determines whether a schema should be excluded from import.
|
|
46
|
+
*/
|
|
47
|
+
shouldExcludeSchema(schema: JSONSchema7, schemaId?: string): boolean {
|
|
48
|
+
// Check explicit exclusion list
|
|
49
|
+
if (schemaId && this.options.excludeSchemaIds?.includes(schemaId)) {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check pattern exclusions
|
|
54
|
+
if (schemaId && this.options.excludePatterns) {
|
|
55
|
+
for (const pattern of this.options.excludePatterns) {
|
|
56
|
+
if (pattern.test(schemaId) || (schema.title && pattern.test(schema.title))) {
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for empty objects
|
|
63
|
+
if (this.options.excludeEmptyObjects && this.isEmptyObjectSchema(schema)) {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for conceptual markers
|
|
68
|
+
if (this.options.excludeConceptualMarkers && this.isConceptualMarkerSchema(schema)) {
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Apply custom filter
|
|
73
|
+
if (this.options.customExclusionFilter?.(schema, schemaId)) {
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks if a schema represents an empty object with no meaningful structure.
|
|
82
|
+
*/
|
|
83
|
+
private isEmptyObjectSchema(schema: JSONSchema7): boolean {
|
|
84
|
+
return (
|
|
85
|
+
schema.type === 'object' &&
|
|
86
|
+
(!schema.properties || Object.keys(schema.properties).length === 0) &&
|
|
87
|
+
!schema.additionalProperties &&
|
|
88
|
+
!schema.patternProperties &&
|
|
89
|
+
!schema.allOf &&
|
|
90
|
+
!schema.oneOf &&
|
|
91
|
+
!schema.anyOf &&
|
|
92
|
+
!schema.if &&
|
|
93
|
+
!schema.then &&
|
|
94
|
+
!schema.else &&
|
|
95
|
+
!schema.required?.length &&
|
|
96
|
+
!schema.minProperties &&
|
|
97
|
+
!schema.maxProperties
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Checks if a schema is likely a conceptual marker with no structural value.
|
|
103
|
+
*/
|
|
104
|
+
private isConceptualMarkerSchema(schema: JSONSchema7): boolean {
|
|
105
|
+
// Check for schemas that are just empty objects or only have allOf with references
|
|
106
|
+
if (this.isEmptyObjectSchema(schema)) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Note: We removed the check for single allOf with $ref as this represents
|
|
111
|
+
// valid schema inheritance/extension patterns (e.g., BlogPosting extending SocialMediaPosting)
|
|
112
|
+
// Such schemas should be included as they define meaningful type hierarchies
|
|
113
|
+
|
|
114
|
+
// Check for schemas that only have description and type but no structure
|
|
115
|
+
if (
|
|
116
|
+
schema.type === 'object' &&
|
|
117
|
+
schema.description &&
|
|
118
|
+
!schema.properties &&
|
|
119
|
+
!schema.additionalProperties &&
|
|
120
|
+
!schema.allOf &&
|
|
121
|
+
!schema.oneOf &&
|
|
122
|
+
!schema.anyOf &&
|
|
123
|
+
Object.keys(schema).filter((key) => !['$schema', '$id', 'title', 'description', 'type'].includes(key)).length ===
|
|
124
|
+
0
|
|
125
|
+
) {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Gets a human-readable reason why a schema was excluded.
|
|
134
|
+
*/
|
|
135
|
+
getExclusionReason(schema: JSONSchema7, schemaId?: string): string {
|
|
136
|
+
if (schemaId && this.options.excludeSchemaIds?.includes(schemaId)) {
|
|
137
|
+
return `Schema ID '${schemaId}' is in the exclusion list`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.isEmptyObjectSchema(schema)) {
|
|
141
|
+
return 'Schema is an empty object with no structural definition'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.isConceptualMarkerSchema(schema)) {
|
|
145
|
+
return 'Schema appears to be a conceptual marker with no concrete structure'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 'Schema excluded by custom filter'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Predefined filtering strategies for common use cases.
|
|
154
|
+
*/
|
|
155
|
+
export const FILTERING_STRATEGIES = {
|
|
156
|
+
/**
|
|
157
|
+
* Strategy for API modeling - excludes schemas that don't contribute to API structure.
|
|
158
|
+
*/
|
|
159
|
+
API_MODELING: new SchemaFilteringStrategy({
|
|
160
|
+
excludeEmptyObjects: true,
|
|
161
|
+
excludeConceptualMarkers: true,
|
|
162
|
+
excludePatterns: [
|
|
163
|
+
/.*Enumeration$/, // Exclude *Enumeration schemas
|
|
164
|
+
/.*Type$/, // Exclude conceptual *Type schemas that are just markers
|
|
165
|
+
],
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Strategy for database modeling - very strict, only includes schemas with concrete properties.
|
|
170
|
+
*/
|
|
171
|
+
DATABASE_MODELING: new SchemaFilteringStrategy({
|
|
172
|
+
excludeEmptyObjects: true,
|
|
173
|
+
excludeConceptualMarkers: true,
|
|
174
|
+
customExclusionFilter: (schema: JSONSchema7) => {
|
|
175
|
+
// For database modeling, exclude anything that doesn't have properties or clear structure
|
|
176
|
+
return (
|
|
177
|
+
schema.type === 'object' &&
|
|
178
|
+
!schema.properties &&
|
|
179
|
+
!schema.additionalProperties &&
|
|
180
|
+
!schema.allOf?.some(
|
|
181
|
+
(subSchema) => typeof subSchema === 'object' && 'properties' in subSchema && subSchema.properties
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
},
|
|
185
|
+
}),
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Conservative strategy - only excludes obviously empty schemas.
|
|
189
|
+
*/
|
|
190
|
+
CONSERVATIVE: new SchemaFilteringStrategy({
|
|
191
|
+
excludeEmptyObjects: true,
|
|
192
|
+
}),
|
|
193
|
+
} as const
|