@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.
@@ -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
+ })