@api-client/core 0.18.14 → 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/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/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +9 -9
- package/package.json +1 -1
- package/src/modeling/importers/FilteringJsonSchemaImporter.ts +324 -0
- package/src/modeling/importers/SchemaFilteringStrategy.ts +201 -0
- package/tests/unit/modeling/importers/schema_filtering.spec.ts +764 -0
|
@@ -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,
|
|
@@ -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
|
{
|
|
@@ -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
|
@@ -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,201 @@
|
|
|
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
|
+
// Check for schemas that only contain a single allOf with a $ref (like ActionStatusType)
|
|
111
|
+
if (
|
|
112
|
+
schema.type === 'object' &&
|
|
113
|
+
schema.allOf?.length === 1 &&
|
|
114
|
+
typeof schema.allOf[0] === 'object' &&
|
|
115
|
+
'$ref' in schema.allOf[0] &&
|
|
116
|
+
!schema.properties &&
|
|
117
|
+
!schema.additionalProperties
|
|
118
|
+
) {
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for schemas that only have description and type but no structure
|
|
123
|
+
if (
|
|
124
|
+
schema.type === 'object' &&
|
|
125
|
+
schema.description &&
|
|
126
|
+
!schema.properties &&
|
|
127
|
+
!schema.additionalProperties &&
|
|
128
|
+
!schema.allOf &&
|
|
129
|
+
!schema.oneOf &&
|
|
130
|
+
!schema.anyOf &&
|
|
131
|
+
Object.keys(schema).filter((key) => !['$schema', '$id', 'title', 'description', 'type'].includes(key)).length ===
|
|
132
|
+
0
|
|
133
|
+
) {
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Gets a human-readable reason why a schema was excluded.
|
|
142
|
+
*/
|
|
143
|
+
getExclusionReason(schema: JSONSchema7, schemaId?: string): string {
|
|
144
|
+
if (schemaId && this.options.excludeSchemaIds?.includes(schemaId)) {
|
|
145
|
+
return `Schema ID '${schemaId}' is in the exclusion list`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.isEmptyObjectSchema(schema)) {
|
|
149
|
+
return 'Schema is an empty object with no structural definition'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this.isConceptualMarkerSchema(schema)) {
|
|
153
|
+
return 'Schema appears to be a conceptual marker with no concrete structure'
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return 'Schema excluded by custom filter'
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Predefined filtering strategies for common use cases.
|
|
162
|
+
*/
|
|
163
|
+
export const FILTERING_STRATEGIES = {
|
|
164
|
+
/**
|
|
165
|
+
* Strategy for API modeling - excludes schemas that don't contribute to API structure.
|
|
166
|
+
*/
|
|
167
|
+
API_MODELING: new SchemaFilteringStrategy({
|
|
168
|
+
excludeEmptyObjects: true,
|
|
169
|
+
excludeConceptualMarkers: true,
|
|
170
|
+
excludePatterns: [
|
|
171
|
+
/.*Enumeration$/, // Exclude *Enumeration schemas
|
|
172
|
+
/.*Type$/, // Exclude conceptual *Type schemas that are just markers
|
|
173
|
+
],
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Strategy for database modeling - very strict, only includes schemas with concrete properties.
|
|
178
|
+
*/
|
|
179
|
+
DATABASE_MODELING: new SchemaFilteringStrategy({
|
|
180
|
+
excludeEmptyObjects: true,
|
|
181
|
+
excludeConceptualMarkers: true,
|
|
182
|
+
customExclusionFilter: (schema: JSONSchema7) => {
|
|
183
|
+
// For database modeling, exclude anything that doesn't have properties or clear structure
|
|
184
|
+
return (
|
|
185
|
+
schema.type === 'object' &&
|
|
186
|
+
!schema.properties &&
|
|
187
|
+
!schema.additionalProperties &&
|
|
188
|
+
!schema.allOf?.some(
|
|
189
|
+
(subSchema) => typeof subSchema === 'object' && 'properties' in subSchema && subSchema.properties
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Conservative strategy - only excludes obviously empty schemas.
|
|
197
|
+
*/
|
|
198
|
+
CONSERVATIVE: new SchemaFilteringStrategy({
|
|
199
|
+
excludeEmptyObjects: true,
|
|
200
|
+
}),
|
|
201
|
+
} as const
|