@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
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { test } from '@japa/runner'
|
|
2
|
+
import type { JSONSchema7 } from 'json-schema'
|
|
3
|
+
import { DataDomain } from '../../../../src/modeling/DataDomain.js'
|
|
4
|
+
import {
|
|
5
|
+
FilteringJsonSchemaImporter,
|
|
6
|
+
SchemaFilteringUtils,
|
|
7
|
+
} from '../../../../src/modeling/importers/FilteringJsonSchemaImporter.js'
|
|
8
|
+
import {
|
|
9
|
+
SchemaFilteringStrategy,
|
|
10
|
+
FILTERING_STRATEGIES,
|
|
11
|
+
} from '../../../../src/modeling/importers/SchemaFilteringStrategy.js'
|
|
12
|
+
import type { InMemorySchema } from '../../../../src/modeling/importers/JsonSchemaImporter.js'
|
|
13
|
+
|
|
14
|
+
test.group('SchemaFilteringStrategy', () => {
|
|
15
|
+
test('should identify empty object schemas', ({ assert }) => {
|
|
16
|
+
const strategy = new SchemaFilteringStrategy({ excludeEmptyObjects: true })
|
|
17
|
+
|
|
18
|
+
const emptySchema: JSONSchema7 = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
title: 'EmptyObject',
|
|
21
|
+
description: 'An empty object',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const nonEmptySchema: JSONSchema7 = {
|
|
25
|
+
type: 'object',
|
|
26
|
+
title: 'NonEmptyObject',
|
|
27
|
+
properties: {
|
|
28
|
+
name: { type: 'string' },
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
assert.isTrue(strategy.shouldExcludeSchema(emptySchema))
|
|
33
|
+
assert.isFalse(strategy.shouldExcludeSchema(nonEmptySchema))
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('should identify conceptual marker schemas', ({ assert }) => {
|
|
37
|
+
const strategy = new SchemaFilteringStrategy({ excludeConceptualMarkers: true })
|
|
38
|
+
|
|
39
|
+
// StatusEnumeration-like schema (empty object, should be excluded)
|
|
40
|
+
const statusEnum: JSONSchema7 = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
title: 'StatusEnumeration',
|
|
43
|
+
description: 'Lists or enumerations dealing with status types.',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ActionStatusType-like schema (allOf with reference only - represents inheritance, should NOT be excluded)
|
|
47
|
+
const actionStatus: JSONSchema7 = {
|
|
48
|
+
type: 'object',
|
|
49
|
+
title: 'ActionStatusType',
|
|
50
|
+
description: 'The status of an Action.',
|
|
51
|
+
allOf: [{ $ref: 'schema:StatusEnumeration' }],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Real schema with properties
|
|
55
|
+
const person: JSONSchema7 = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
title: 'Person',
|
|
58
|
+
properties: {
|
|
59
|
+
name: { type: 'string' },
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
assert.isTrue(strategy.shouldExcludeSchema(statusEnum))
|
|
64
|
+
assert.isFalse(strategy.shouldExcludeSchema(actionStatus)) // Changed: inheritance should not be excluded
|
|
65
|
+
assert.isFalse(strategy.shouldExcludeSchema(person))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('should preserve schema inheritance patterns', ({ assert }) => {
|
|
69
|
+
const strategy = new SchemaFilteringStrategy({ excludeConceptualMarkers: true })
|
|
70
|
+
|
|
71
|
+
// BlogPosting schema - extends SocialMediaPosting via allOf
|
|
72
|
+
const blogPosting: JSONSchema7 = {
|
|
73
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
74
|
+
$id: 'schema:BlogPosting',
|
|
75
|
+
title: 'BlogPosting',
|
|
76
|
+
description: 'A blog post.',
|
|
77
|
+
type: 'object',
|
|
78
|
+
allOf: [
|
|
79
|
+
{
|
|
80
|
+
description: 'A post to a social media platform, including blog posts, tweets, Facebook posts, etc.',
|
|
81
|
+
$ref: 'schema:SocialMediaPosting',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// SocialMediaPosting schema - has properties and extends Article
|
|
87
|
+
const socialMediaPosting: JSONSchema7 = {
|
|
88
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
89
|
+
$id: 'schema:SocialMediaPosting',
|
|
90
|
+
title: 'SocialMediaPosting',
|
|
91
|
+
description: 'A post to a social media platform, including blog posts, tweets, Facebook posts, etc.',
|
|
92
|
+
type: 'object',
|
|
93
|
+
allOf: [
|
|
94
|
+
{
|
|
95
|
+
description: 'An article, such as a news article or piece of investigative report.',
|
|
96
|
+
$ref: 'schema:Article',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
properties: {
|
|
100
|
+
sharedContent: {
|
|
101
|
+
description: 'A CreativeWork such as an image, video, or audio clip shared as part of this posting.',
|
|
102
|
+
oneOf: [{ $ref: 'schema:CreativeWork' }, { type: 'array', items: { $ref: 'schema:CreativeWork' } }],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Both schemas represent valid inheritance and should be included
|
|
108
|
+
assert.isFalse(strategy.shouldExcludeSchema(blogPosting, 'schema:BlogPosting'))
|
|
109
|
+
assert.isFalse(strategy.shouldExcludeSchema(socialMediaPosting, 'schema:SocialMediaPosting'))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('should filter by patterns', ({ assert }) => {
|
|
113
|
+
const strategy = new SchemaFilteringStrategy({
|
|
114
|
+
excludePatterns: [/.*Enumeration$/, /.*Type$/],
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
assert.isTrue(strategy.shouldExcludeSchema({} as JSONSchema7, 'StatusEnumeration'))
|
|
118
|
+
assert.isTrue(strategy.shouldExcludeSchema({} as JSONSchema7, 'BusinessEntityType'))
|
|
119
|
+
assert.isFalse(strategy.shouldExcludeSchema({} as JSONSchema7, 'Person'))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('should use custom exclusion filter', ({ assert }) => {
|
|
123
|
+
const strategy = new SchemaFilteringStrategy({
|
|
124
|
+
customExclusionFilter: (schema) => schema.title?.includes('Custom') ?? false,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const customSchema: JSONSchema7 = { title: 'CustomSchema' }
|
|
128
|
+
const normalSchema: JSONSchema7 = { title: 'NormalSchema' }
|
|
129
|
+
|
|
130
|
+
assert.isTrue(strategy.shouldExcludeSchema(customSchema))
|
|
131
|
+
assert.isFalse(strategy.shouldExcludeSchema(normalSchema))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('should handle schemas with additionalProperties', ({ assert }) => {
|
|
135
|
+
const strategy = new SchemaFilteringStrategy({ excludeEmptyObjects: true })
|
|
136
|
+
|
|
137
|
+
const schemaWithAdditional: JSONSchema7 = {
|
|
138
|
+
type: 'object',
|
|
139
|
+
title: 'FlexibleObject',
|
|
140
|
+
additionalProperties: true,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const schemaWithAdditionalSchema: JSONSchema7 = {
|
|
144
|
+
type: 'object',
|
|
145
|
+
title: 'ConstrainedObject',
|
|
146
|
+
additionalProperties: { type: 'string' },
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// These should NOT be filtered because additionalProperties provides structure
|
|
150
|
+
assert.isFalse(strategy.shouldExcludeSchema(schemaWithAdditional))
|
|
151
|
+
assert.isFalse(strategy.shouldExcludeSchema(schemaWithAdditionalSchema))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('should handle schemas with constraints but no properties', ({ assert }) => {
|
|
155
|
+
const strategy = new SchemaFilteringStrategy({ excludeEmptyObjects: true })
|
|
156
|
+
|
|
157
|
+
const constrainedSchema: JSONSchema7 = {
|
|
158
|
+
type: 'object',
|
|
159
|
+
title: 'ConstrainedObject',
|
|
160
|
+
minProperties: 1,
|
|
161
|
+
maxProperties: 5,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Should NOT be filtered because it has structural constraints
|
|
165
|
+
assert.isFalse(strategy.shouldExcludeSchema(constrainedSchema))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('should provide meaningful exclusion reasons', ({ assert }) => {
|
|
169
|
+
const strategy = new SchemaFilteringStrategy({
|
|
170
|
+
excludeEmptyObjects: true,
|
|
171
|
+
excludeConceptualMarkers: true,
|
|
172
|
+
excludeSchemaIds: ['exclude:this'],
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const emptySchema: JSONSchema7 = { type: 'object' }
|
|
176
|
+
const markerSchema: JSONSchema7 = {
|
|
177
|
+
type: 'object',
|
|
178
|
+
title: 'Marker',
|
|
179
|
+
description: 'Just a marker',
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
assert.include(strategy.getExclusionReason(emptySchema), 'empty object')
|
|
183
|
+
// The markerSchema is actually identified as empty object, not conceptual marker
|
|
184
|
+
assert.include(strategy.getExclusionReason(markerSchema), 'empty object')
|
|
185
|
+
assert.include(strategy.getExclusionReason({} as JSONSchema7, 'exclude:this'), 'exclusion list')
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test.group('Predefined Filtering Strategies', () => {
|
|
190
|
+
const testSchemas = [
|
|
191
|
+
{
|
|
192
|
+
id: 'schema:StatusEnumeration',
|
|
193
|
+
schema: { type: 'object', title: 'StatusEnumeration' } as JSONSchema7,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'schema:BusinessEntityType',
|
|
197
|
+
schema: { type: 'object', title: 'BusinessEntityType' } as JSONSchema7,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'schema:Person',
|
|
201
|
+
schema: { type: 'object', title: 'Person', properties: { name: { type: 'string' } } } as JSONSchema7,
|
|
202
|
+
},
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
test('API_MODELING strategy should filter enumeration and type schemas', ({ assert }) => {
|
|
206
|
+
const stats = SchemaFilteringUtils.getFilteringStats(testSchemas, FILTERING_STRATEGIES.API_MODELING)
|
|
207
|
+
|
|
208
|
+
assert.equal(stats.total, 3)
|
|
209
|
+
assert.equal(stats.filtered, 2) // StatusEnumeration and BusinessEntityType
|
|
210
|
+
assert.equal(stats.remaining, 1) // Person
|
|
211
|
+
assert.deepEqual(
|
|
212
|
+
stats.filteredSchemas.map((f) => f.id),
|
|
213
|
+
['schema:StatusEnumeration', 'schema:BusinessEntityType']
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
test('DATABASE_MODELING strategy should be very strict', ({ assert }) => {
|
|
218
|
+
const stats = SchemaFilteringUtils.getFilteringStats(testSchemas, FILTERING_STRATEGIES.DATABASE_MODELING)
|
|
219
|
+
|
|
220
|
+
assert.equal(stats.filtered, 2) // Should filter empty objects
|
|
221
|
+
assert.equal(stats.remaining, 1) // Only Person with properties
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('CONSERVATIVE strategy should only filter obviously empty schemas', ({ assert }) => {
|
|
225
|
+
const stats = SchemaFilteringUtils.getFilteringStats(testSchemas, FILTERING_STRATEGIES.CONSERVATIVE)
|
|
226
|
+
|
|
227
|
+
assert.equal(stats.filtered, 2) // Only truly empty objects
|
|
228
|
+
assert.equal(stats.remaining, 1)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test.group('Schema References and Dependencies', () => {
|
|
233
|
+
const createTestSchemas = (): InMemorySchema[] => [
|
|
234
|
+
// Empty marker schema
|
|
235
|
+
{
|
|
236
|
+
path: 'schema:StatusEnumeration',
|
|
237
|
+
contents: {
|
|
238
|
+
$id: 'schema:StatusEnumeration',
|
|
239
|
+
type: 'object',
|
|
240
|
+
title: 'StatusEnumeration',
|
|
241
|
+
description: 'Lists or enumerations dealing with status types.',
|
|
242
|
+
} as JSONSchema7,
|
|
243
|
+
},
|
|
244
|
+
// Schema that references the empty marker
|
|
245
|
+
{
|
|
246
|
+
path: 'schema:ActionStatusType',
|
|
247
|
+
contents: {
|
|
248
|
+
$id: 'schema:ActionStatusType',
|
|
249
|
+
type: 'object',
|
|
250
|
+
title: 'ActionStatusType',
|
|
251
|
+
allOf: [{ $ref: 'schema:StatusEnumeration' }],
|
|
252
|
+
} as JSONSchema7,
|
|
253
|
+
},
|
|
254
|
+
// Schema with property referencing filtered schema
|
|
255
|
+
{
|
|
256
|
+
path: 'schema:Task',
|
|
257
|
+
contents: {
|
|
258
|
+
$id: 'schema:Task',
|
|
259
|
+
type: 'object',
|
|
260
|
+
title: 'Task',
|
|
261
|
+
properties: {
|
|
262
|
+
name: { type: 'string' },
|
|
263
|
+
status: { $ref: 'schema:ActionStatusType' },
|
|
264
|
+
},
|
|
265
|
+
required: ['name'],
|
|
266
|
+
} as JSONSchema7,
|
|
267
|
+
},
|
|
268
|
+
// Schema inheriting from filtered schema
|
|
269
|
+
{
|
|
270
|
+
path: 'schema:UrgentTask',
|
|
271
|
+
contents: {
|
|
272
|
+
$id: 'schema:UrgentTask',
|
|
273
|
+
type: 'object',
|
|
274
|
+
title: 'UrgentTask',
|
|
275
|
+
allOf: [
|
|
276
|
+
{ $ref: 'schema:Task' },
|
|
277
|
+
{
|
|
278
|
+
properties: {
|
|
279
|
+
priority: { type: 'string', enum: ['high', 'urgent'] },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
} as JSONSchema7,
|
|
284
|
+
},
|
|
285
|
+
// Concrete schema that should be kept
|
|
286
|
+
{
|
|
287
|
+
path: 'schema:Person',
|
|
288
|
+
contents: {
|
|
289
|
+
$id: 'schema:Person',
|
|
290
|
+
type: 'object',
|
|
291
|
+
title: 'Person',
|
|
292
|
+
properties: {
|
|
293
|
+
name: { type: 'string' },
|
|
294
|
+
email: { type: 'string' },
|
|
295
|
+
},
|
|
296
|
+
required: ['name'],
|
|
297
|
+
} as JSONSchema7,
|
|
298
|
+
},
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
test('should handle schemas with filtered dependencies - no filtering', async ({ assert }) => {
|
|
302
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
303
|
+
const schemas = createTestSchemas()
|
|
304
|
+
const importer = new FilteringJsonSchemaImporter(domain)
|
|
305
|
+
|
|
306
|
+
const result = await importer.import(schemas, 'TestModel')
|
|
307
|
+
const entities = Array.from(result.model.listEntities())
|
|
308
|
+
|
|
309
|
+
// Without filtering, all schemas should create entities
|
|
310
|
+
assert.isAtLeast(entities.length, 5)
|
|
311
|
+
|
|
312
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
313
|
+
assert.include(entityNames, 'action_status_type')
|
|
314
|
+
assert.include(entityNames, 'person')
|
|
315
|
+
assert.include(entityNames, 'status_enumeration')
|
|
316
|
+
assert.include(entityNames, 'task')
|
|
317
|
+
assert.include(entityNames, 'urgent_task')
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('should handle schemas with filtered dependencies - API modeling', async ({ assert }) => {
|
|
321
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
322
|
+
const schemas = createTestSchemas()
|
|
323
|
+
const importer = FilteringJsonSchemaImporter.forApiModeling(domain)
|
|
324
|
+
|
|
325
|
+
const result = await importer.import(schemas, 'ApiModel')
|
|
326
|
+
const entities = Array.from(result.model.listEntities())
|
|
327
|
+
|
|
328
|
+
// Should filter StatusEnumeration and ActionStatusType (ends with Type)
|
|
329
|
+
// Task and UrgentTask should remain (they have concrete properties)
|
|
330
|
+
// Person should remain
|
|
331
|
+
assert.isAtMost(entities.length, 3)
|
|
332
|
+
|
|
333
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
334
|
+
assert.include(entityNames, 'person')
|
|
335
|
+
assert.include(entityNames, 'task')
|
|
336
|
+
// Note: UrgentTask uses allOf so it may be created as a mixin entity
|
|
337
|
+
assert.isAtLeast(entityNames.length, 2) // At least person and task
|
|
338
|
+
|
|
339
|
+
// Check filtering messages
|
|
340
|
+
const filterMessages = result.messages.filter((m) => m.message.includes('filtered'))
|
|
341
|
+
assert.isAtLeast(filterMessages.length, 1)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('should handle schemas with filtered dependencies - Database modeling', async ({ assert }) => {
|
|
345
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
346
|
+
const schemas = createTestSchemas()
|
|
347
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
348
|
+
|
|
349
|
+
const result = await importer.import(schemas, 'DatabaseModel')
|
|
350
|
+
const entities = Array.from(result.model.listEntities())
|
|
351
|
+
|
|
352
|
+
// Database modeling should be very strict - only keep schemas with concrete properties
|
|
353
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
354
|
+
assert.include(entityNames, 'person') // Has concrete properties
|
|
355
|
+
assert.include(entityNames, 'task') // Has concrete properties
|
|
356
|
+
// Note: UrgentTask uses allOf so it may be created as a mixin entity
|
|
357
|
+
assert.isAtLeast(entityNames.length, 2) // At least person and task
|
|
358
|
+
|
|
359
|
+
// Should filter empty marker schemas
|
|
360
|
+
assert.notInclude(entityNames, 'status_enumeration')
|
|
361
|
+
|
|
362
|
+
// Check that we have filtering messages
|
|
363
|
+
const filterMessages = result.messages.filter((m) => m.message.includes('filtered'))
|
|
364
|
+
assert.isAtLeast(filterMessages.length, 1)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('should clean up orphaned associations after filtering', async ({ assert }) => {
|
|
368
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
369
|
+
const schemas: InMemorySchema[] = [
|
|
370
|
+
{
|
|
371
|
+
path: 'schema:EmptyMarker',
|
|
372
|
+
contents: {
|
|
373
|
+
$id: 'schema:EmptyMarker',
|
|
374
|
+
type: 'object',
|
|
375
|
+
title: 'EmptyMarker',
|
|
376
|
+
} as JSONSchema7,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
path: 'schema:User',
|
|
380
|
+
contents: {
|
|
381
|
+
$id: 'schema:User',
|
|
382
|
+
type: 'object',
|
|
383
|
+
title: 'User',
|
|
384
|
+
properties: {
|
|
385
|
+
name: { type: 'string' },
|
|
386
|
+
marker: { $ref: 'schema:EmptyMarker' },
|
|
387
|
+
},
|
|
388
|
+
} as JSONSchema7,
|
|
389
|
+
},
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
393
|
+
const result = await importer.import(schemas, 'CleanupModel')
|
|
394
|
+
|
|
395
|
+
// EmptyMarker should be filtered out
|
|
396
|
+
const entities = Array.from(result.model.listEntities())
|
|
397
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
398
|
+
assert.include(entityNames, 'user')
|
|
399
|
+
assert.notInclude(entityNames, 'empty_marker')
|
|
400
|
+
|
|
401
|
+
// Check for cleanup messages
|
|
402
|
+
const cleanupMessages = result.messages.filter(
|
|
403
|
+
(m) => m.message.includes('association') || m.message.includes('orphaned')
|
|
404
|
+
)
|
|
405
|
+
assert.isAtLeast(cleanupMessages.length, 0) // May or may not have associations to clean up
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test.group('Complex Inheritance Scenarios', () => {
|
|
410
|
+
test('should handle inheritance chains with filtered base classes', async ({ assert }) => {
|
|
411
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
412
|
+
const schemas: InMemorySchema[] = [
|
|
413
|
+
{
|
|
414
|
+
path: 'schema:BaseType',
|
|
415
|
+
contents: {
|
|
416
|
+
$id: 'schema:BaseType',
|
|
417
|
+
type: 'object',
|
|
418
|
+
title: 'BaseType',
|
|
419
|
+
description: 'A base conceptual type',
|
|
420
|
+
} as JSONSchema7,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
path: 'schema:MiddleType',
|
|
424
|
+
contents: {
|
|
425
|
+
$id: 'schema:MiddleType',
|
|
426
|
+
type: 'object',
|
|
427
|
+
title: 'MiddleType',
|
|
428
|
+
allOf: [{ $ref: 'schema:BaseType' }],
|
|
429
|
+
} as JSONSchema7,
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
path: 'schema:ConcreteEntity',
|
|
433
|
+
contents: {
|
|
434
|
+
$id: 'schema:ConcreteEntity',
|
|
435
|
+
type: 'object',
|
|
436
|
+
title: 'ConcreteEntity',
|
|
437
|
+
allOf: [
|
|
438
|
+
{ $ref: 'schema:MiddleType' },
|
|
439
|
+
{
|
|
440
|
+
properties: {
|
|
441
|
+
id: { type: 'string' },
|
|
442
|
+
name: { type: 'string' },
|
|
443
|
+
},
|
|
444
|
+
required: ['id'],
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
} as JSONSchema7,
|
|
448
|
+
},
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
452
|
+
const result = await importer.import(schemas, 'InheritanceModel')
|
|
453
|
+
|
|
454
|
+
const entities = Array.from(result.model.listEntities())
|
|
455
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
456
|
+
|
|
457
|
+
// Check what entities were actually created
|
|
458
|
+
assert.isAtLeast(entities.length, 1)
|
|
459
|
+
|
|
460
|
+
// Base types should be filtered
|
|
461
|
+
assert.notInclude(entityNames, 'base_type')
|
|
462
|
+
|
|
463
|
+
// Check that some entity with ConcreteEntity properties was created
|
|
464
|
+
const concreteEntity = entities.find((e) => e.info.name?.includes('concrete_entity'))
|
|
465
|
+
assert.isNotNull(concreteEntity)
|
|
466
|
+
|
|
467
|
+
const properties = Array.from(concreteEntity!.listProperties())
|
|
468
|
+
assert.isAtLeast(properties.length, 1) // Should have some properties
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
test('should handle circular references with filtered schemas', async ({ assert }) => {
|
|
472
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
473
|
+
const schemas: InMemorySchema[] = [
|
|
474
|
+
{
|
|
475
|
+
path: 'schema:TypeA',
|
|
476
|
+
contents: {
|
|
477
|
+
$id: 'schema:TypeA',
|
|
478
|
+
type: 'object',
|
|
479
|
+
title: 'TypeA',
|
|
480
|
+
properties: {
|
|
481
|
+
refToB: { $ref: 'schema:TypeB' },
|
|
482
|
+
},
|
|
483
|
+
} as JSONSchema7,
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
path: 'schema:TypeB',
|
|
487
|
+
contents: {
|
|
488
|
+
$id: 'schema:TypeB',
|
|
489
|
+
type: 'object',
|
|
490
|
+
title: 'TypeB',
|
|
491
|
+
description: 'Empty marker type',
|
|
492
|
+
} as JSONSchema7,
|
|
493
|
+
},
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
const importer = FilteringJsonSchemaImporter.forApiModeling(domain)
|
|
497
|
+
const result = await importer.import(schemas, 'CircularModel')
|
|
498
|
+
|
|
499
|
+
// Should handle this gracefully without infinite loops
|
|
500
|
+
assert.isArray(result.messages)
|
|
501
|
+
|
|
502
|
+
const entities = Array.from(result.model.listEntities())
|
|
503
|
+
assert.isAtLeast(entities.length, 0) // Should not crash
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
test.group('Property and Parent Reference Filtering', () => {
|
|
508
|
+
test('should handle schemas where filtered types are used as property types', async ({ assert }) => {
|
|
509
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
510
|
+
const schemas: InMemorySchema[] = [
|
|
511
|
+
// Empty enumeration that should be filtered
|
|
512
|
+
{
|
|
513
|
+
path: 'schema:StatusEnumeration',
|
|
514
|
+
contents: {
|
|
515
|
+
$id: 'schema:StatusEnumeration',
|
|
516
|
+
type: 'object',
|
|
517
|
+
title: 'StatusEnumeration',
|
|
518
|
+
description: 'Status enumeration values',
|
|
519
|
+
} as JSONSchema7,
|
|
520
|
+
},
|
|
521
|
+
// Entity that has a property referencing the filtered schema
|
|
522
|
+
{
|
|
523
|
+
path: 'schema:Order',
|
|
524
|
+
contents: {
|
|
525
|
+
$id: 'schema:Order',
|
|
526
|
+
type: 'object',
|
|
527
|
+
title: 'Order',
|
|
528
|
+
properties: {
|
|
529
|
+
id: { type: 'string' },
|
|
530
|
+
amount: { type: 'number' },
|
|
531
|
+
status: { $ref: 'schema:StatusEnumeration' }, // References filtered schema
|
|
532
|
+
},
|
|
533
|
+
required: ['id', 'amount'],
|
|
534
|
+
} as JSONSchema7,
|
|
535
|
+
},
|
|
536
|
+
// Entity with array property referencing filtered schema
|
|
537
|
+
{
|
|
538
|
+
path: 'schema:StatusHistory',
|
|
539
|
+
contents: {
|
|
540
|
+
$id: 'schema:StatusHistory',
|
|
541
|
+
type: 'object',
|
|
542
|
+
title: 'StatusHistory',
|
|
543
|
+
properties: {
|
|
544
|
+
orderId: { type: 'string' },
|
|
545
|
+
statusChanges: {
|
|
546
|
+
type: 'array',
|
|
547
|
+
items: { $ref: 'schema:StatusEnumeration' }, // Array of filtered schema
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
required: ['orderId'],
|
|
551
|
+
} as JSONSchema7,
|
|
552
|
+
},
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
556
|
+
const result = await importer.import(schemas, 'PropertyRefModel')
|
|
557
|
+
|
|
558
|
+
const entities = Array.from(result.model.listEntities())
|
|
559
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
560
|
+
|
|
561
|
+
// StatusEnumeration should be filtered out
|
|
562
|
+
assert.notInclude(entityNames, 'status_enumeration')
|
|
563
|
+
|
|
564
|
+
// Order and StatusHistory should remain (they have concrete properties)
|
|
565
|
+
assert.include(entityNames, 'order')
|
|
566
|
+
assert.include(entityNames, 'status_history')
|
|
567
|
+
|
|
568
|
+
// Check that the entities have the expected properties
|
|
569
|
+
const orderEntity = entities.find((e) => e.info.name === 'order')
|
|
570
|
+
assert.isNotNull(orderEntity)
|
|
571
|
+
|
|
572
|
+
const orderProperties = Array.from(orderEntity!.listProperties())
|
|
573
|
+
assert.isAtLeast(orderProperties.length, 2) // id and amount at minimum
|
|
574
|
+
|
|
575
|
+
// Check for filtering messages about the StatusEnumeration
|
|
576
|
+
const filterMessages = result.messages.filter(
|
|
577
|
+
(m) => m.message.includes('filtered') && m.message.includes('status_enumeration')
|
|
578
|
+
)
|
|
579
|
+
assert.isAtLeast(filterMessages.length, 0) // May or may not have specific messages
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
test('should handle schemas where filtered types are used as parent classes', async ({ assert }) => {
|
|
583
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
584
|
+
const schemas: InMemorySchema[] = [
|
|
585
|
+
// Abstract base that should be filtered
|
|
586
|
+
{
|
|
587
|
+
path: 'schema:BaseEntity',
|
|
588
|
+
contents: {
|
|
589
|
+
$id: 'schema:BaseEntity',
|
|
590
|
+
type: 'object',
|
|
591
|
+
title: 'BaseEntity',
|
|
592
|
+
description: 'Base entity without concrete properties',
|
|
593
|
+
} as JSONSchema7,
|
|
594
|
+
},
|
|
595
|
+
// Another abstract type that extends the base
|
|
596
|
+
{
|
|
597
|
+
path: 'schema:TimestampedEntity',
|
|
598
|
+
contents: {
|
|
599
|
+
$id: 'schema:TimestampedEntity',
|
|
600
|
+
type: 'object',
|
|
601
|
+
title: 'TimestampedEntity',
|
|
602
|
+
allOf: [
|
|
603
|
+
{ $ref: 'schema:BaseEntity' },
|
|
604
|
+
{
|
|
605
|
+
properties: {
|
|
606
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
607
|
+
updatedAt: { type: 'string', format: 'date-time' },
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
} as JSONSchema7,
|
|
612
|
+
},
|
|
613
|
+
// Concrete entity that inherits from timestamped entity
|
|
614
|
+
{
|
|
615
|
+
path: 'schema:Product',
|
|
616
|
+
contents: {
|
|
617
|
+
$id: 'schema:Product',
|
|
618
|
+
type: 'object',
|
|
619
|
+
title: 'Product',
|
|
620
|
+
allOf: [
|
|
621
|
+
{ $ref: 'schema:TimestampedEntity' },
|
|
622
|
+
{
|
|
623
|
+
properties: {
|
|
624
|
+
id: { type: 'string' },
|
|
625
|
+
name: { type: 'string' },
|
|
626
|
+
price: { type: 'number' },
|
|
627
|
+
},
|
|
628
|
+
required: ['id', 'name', 'price'],
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
} as JSONSchema7,
|
|
632
|
+
},
|
|
633
|
+
]
|
|
634
|
+
|
|
635
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
636
|
+
const result = await importer.import(schemas, 'InheritanceModel')
|
|
637
|
+
|
|
638
|
+
const entities = Array.from(result.model.listEntities())
|
|
639
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
640
|
+
|
|
641
|
+
// BaseEntity should be filtered (no concrete properties)
|
|
642
|
+
assert.notInclude(entityNames, 'base_entity')
|
|
643
|
+
|
|
644
|
+
// Some entities should remain (they have properties)
|
|
645
|
+
assert.isAtLeast(entities.length, 1)
|
|
646
|
+
|
|
647
|
+
// Check that Product has all the expected properties
|
|
648
|
+
const productEntity = entities.find((e) => e.info.name?.includes('product'))
|
|
649
|
+
assert.isNotNull(productEntity)
|
|
650
|
+
|
|
651
|
+
const productProperties = Array.from(productEntity!.listProperties())
|
|
652
|
+
assert.isAtLeast(productProperties.length, 3) // At least id, name, price
|
|
653
|
+
|
|
654
|
+
// Check for inheritance relationships (associations)
|
|
655
|
+
const productAssociations = Array.from(productEntity!.listAssociations())
|
|
656
|
+
// May have associations to parent entities
|
|
657
|
+
assert.isAtLeast(productAssociations.length, 0)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
test('should handle nested object properties with filtered schemas', async ({ assert }) => {
|
|
661
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
662
|
+
const schemas: InMemorySchema[] = [
|
|
663
|
+
// Empty address type that should be filtered
|
|
664
|
+
{
|
|
665
|
+
path: 'schema:AddressType',
|
|
666
|
+
contents: {
|
|
667
|
+
$id: 'schema:AddressType',
|
|
668
|
+
type: 'object',
|
|
669
|
+
title: 'AddressType',
|
|
670
|
+
description: 'Address type enumeration',
|
|
671
|
+
} as JSONSchema7,
|
|
672
|
+
},
|
|
673
|
+
// Concrete address with properties
|
|
674
|
+
{
|
|
675
|
+
path: 'schema:Address',
|
|
676
|
+
contents: {
|
|
677
|
+
$id: 'schema:Address',
|
|
678
|
+
type: 'object',
|
|
679
|
+
title: 'Address',
|
|
680
|
+
properties: {
|
|
681
|
+
street: { type: 'string' },
|
|
682
|
+
city: { type: 'string' },
|
|
683
|
+
country: { type: 'string' },
|
|
684
|
+
type: { $ref: 'schema:AddressType' }, // References filtered schema
|
|
685
|
+
},
|
|
686
|
+
required: ['street', 'city'],
|
|
687
|
+
} as JSONSchema7,
|
|
688
|
+
},
|
|
689
|
+
// User with nested address
|
|
690
|
+
{
|
|
691
|
+
path: 'schema:User',
|
|
692
|
+
contents: {
|
|
693
|
+
$id: 'schema:User',
|
|
694
|
+
type: 'object',
|
|
695
|
+
title: 'User',
|
|
696
|
+
properties: {
|
|
697
|
+
id: { type: 'string' },
|
|
698
|
+
name: { type: 'string' },
|
|
699
|
+
address: { $ref: 'schema:Address' }, // Nested object reference
|
|
700
|
+
addresses: {
|
|
701
|
+
type: 'array',
|
|
702
|
+
items: { $ref: 'schema:Address' }, // Array of nested objects
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
required: ['id', 'name'],
|
|
706
|
+
} as JSONSchema7,
|
|
707
|
+
},
|
|
708
|
+
]
|
|
709
|
+
|
|
710
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
711
|
+
const result = await importer.import(schemas, 'NestedModel')
|
|
712
|
+
|
|
713
|
+
const entities = Array.from(result.model.listEntities())
|
|
714
|
+
const entityNames = entities.map((e) => e.info.name)
|
|
715
|
+
|
|
716
|
+
// AddressType should be filtered
|
|
717
|
+
assert.notInclude(entityNames, 'address_type')
|
|
718
|
+
|
|
719
|
+
// Address and User should remain
|
|
720
|
+
assert.include(entityNames, 'address')
|
|
721
|
+
assert.include(entityNames, 'user')
|
|
722
|
+
|
|
723
|
+
// Check that Address entity has properties
|
|
724
|
+
const addressEntity = entities.find((e) => e.info.name === 'address')
|
|
725
|
+
assert.isNotNull(addressEntity)
|
|
726
|
+
const addressProperties = Array.from(addressEntity!.listProperties())
|
|
727
|
+
assert.isAtLeast(addressProperties.length, 3) // street, city, country at minimum
|
|
728
|
+
|
|
729
|
+
// Check that User entity has properties and associations
|
|
730
|
+
const userEntity = entities.find((e) => e.info.name === 'user')
|
|
731
|
+
assert.isNotNull(userEntity)
|
|
732
|
+
const userProperties = Array.from(userEntity!.listProperties())
|
|
733
|
+
const userAssociations = Array.from(userEntity!.listAssociations())
|
|
734
|
+
|
|
735
|
+
assert.isAtLeast(userProperties.length, 2) // id, name at minimum
|
|
736
|
+
assert.isAtLeast(userAssociations.length, 0) // May have associations to Address
|
|
737
|
+
})
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
test.group('Integration Tests', () => {
|
|
741
|
+
test('should maintain original functionality when no filtering is applied', async ({ assert }) => {
|
|
742
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
743
|
+
const schemas: InMemorySchema[] = [
|
|
744
|
+
{
|
|
745
|
+
path: 'schema:Person',
|
|
746
|
+
contents: {
|
|
747
|
+
$id: 'schema:Person',
|
|
748
|
+
type: 'object',
|
|
749
|
+
title: 'Person',
|
|
750
|
+
properties: {
|
|
751
|
+
name: { type: 'string' },
|
|
752
|
+
age: { type: 'integer' },
|
|
753
|
+
},
|
|
754
|
+
required: ['name'],
|
|
755
|
+
} as JSONSchema7,
|
|
756
|
+
},
|
|
757
|
+
]
|
|
758
|
+
|
|
759
|
+
const importer = new FilteringJsonSchemaImporter(domain) // No filtering options
|
|
760
|
+
const result = await importer.import(schemas, 'StandardModel')
|
|
761
|
+
|
|
762
|
+
const entities = Array.from(result.model.listEntities())
|
|
763
|
+
assert.equal(entities.length, 1)
|
|
764
|
+
assert.equal(entities[0].info.name, 'person')
|
|
765
|
+
|
|
766
|
+
const properties = Array.from(entities[0].listProperties())
|
|
767
|
+
assert.equal(properties.length, 2) // name and age
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
test('should combine pre-filtering and post-filtering correctly', async ({ assert }) => {
|
|
771
|
+
const domain = new DataDomain({ key: 'test-domain', info: { name: 'Test Domain' } })
|
|
772
|
+
const schemas: InMemorySchema[] = [
|
|
773
|
+
{
|
|
774
|
+
path: 'schema:EmptyType',
|
|
775
|
+
contents: {
|
|
776
|
+
$id: 'schema:EmptyType',
|
|
777
|
+
type: 'object',
|
|
778
|
+
title: 'EmptyType',
|
|
779
|
+
} as JSONSchema7,
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
path: 'schema:ValidEntity',
|
|
783
|
+
contents: {
|
|
784
|
+
$id: 'schema:ValidEntity',
|
|
785
|
+
type: 'object',
|
|
786
|
+
title: 'ValidEntity',
|
|
787
|
+
properties: {
|
|
788
|
+
data: { type: 'string' },
|
|
789
|
+
},
|
|
790
|
+
} as JSONSchema7,
|
|
791
|
+
},
|
|
792
|
+
]
|
|
793
|
+
|
|
794
|
+
const importer = FilteringJsonSchemaImporter.forDatabaseModeling(domain)
|
|
795
|
+
const result = await importer.import(schemas, 'CombinedModel')
|
|
796
|
+
|
|
797
|
+
// Should have both pre-filter and post-filter messages
|
|
798
|
+
const preFilterMessages = result.messages.filter((m) => m.message.includes('Pre-filtered'))
|
|
799
|
+
const postFilterMessages = result.messages.filter((m) => m.message.includes('Post-filtered'))
|
|
800
|
+
|
|
801
|
+
assert.isAtLeast(preFilterMessages.length + postFilterMessages.length, 1)
|
|
802
|
+
|
|
803
|
+
// Only ValidEntity should remain
|
|
804
|
+
const entities = Array.from(result.model.listEntities())
|
|
805
|
+
assert.equal(entities.length, 1)
|
|
806
|
+
assert.equal(entities[0].info.name, 'valid_entity')
|
|
807
|
+
})
|
|
808
|
+
})
|