@api-client/core 0.18.3 → 0.18.5

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,642 @@
1
+ import type { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from 'json-schema'
2
+ import { DataDomain } from '../DataDomain.js'
3
+ import { DomainModel } from '../DomainModel.js'
4
+ import { DomainEntity } from '../DomainEntity.js'
5
+ import { DomainProperty, type DomainPropertySchema } from '../DomainProperty.js'
6
+ import type { PropertySchema } from '../types.js'
7
+ import type { PropertyWebBindings } from '../Bindings.js'
8
+ import type { DomainPropertyFormat, DomainPropertyType } from '../DataFormat.js'
9
+ import { sanitizeInput, toDatabaseColumnName, toDatabaseTableName } from '../helpers/database.js'
10
+
11
+ /**
12
+ * Represents a single message (info, warning, or error) generated during the import process.
13
+ */
14
+ export interface ImportMessage {
15
+ level: 'warning' | 'info'
16
+ message: string
17
+ location?: string
18
+ }
19
+
20
+ /**
21
+ * A report containing the result of the import process, including the created model
22
+ * and any non-blocking messages.
23
+ */
24
+ export interface JsonImportReport {
25
+ /**
26
+ * An array of messages generated during the import process.
27
+ * These can include warnings, info messages, or other non-blocking notifications.
28
+ */
29
+ messages: ImportMessage[]
30
+ /**
31
+ * The model that was created during the import.
32
+ */
33
+ model: DomainModel
34
+ }
35
+
36
+ /**
37
+ * A structure to hold in-memory JSON schema files for browser-based import.
38
+ */
39
+ export interface InMemorySchema {
40
+ /** A virtual path or identifier for the schema, used in `$ref` values. */
41
+ path: string
42
+ /** The parsed content of the JSON schema file. */
43
+ contents: JSONSchema7
44
+ }
45
+
46
+ /**
47
+ * Imports JSON Schema definitions into a DataDomain.
48
+ *
49
+ * This class parses a root JSON Schema file, bundles all its dependencies,
50
+ * and translates the object definitions into DomainEntity instances within a specified DomainModel.
51
+ */
52
+ export class JsonSchemaImporter {
53
+ private domain: DataDomain
54
+ private model!: DomainModel
55
+ private refMap = new Map<string, string>()
56
+ private messages: ImportMessage[] = []
57
+ private enums = new Map<string, JSONSchema7>()
58
+
59
+ constructor(domain: DataDomain) {
60
+ this.domain = domain
61
+ }
62
+
63
+ /**
64
+ * Resets the internal state for a new import operation.
65
+ */
66
+ private resetState(modelName: string) {
67
+ this.model = this.domain.addModel({ info: { name: modelName } })
68
+ this.refMap.clear()
69
+ this.enums.clear()
70
+ this.messages = []
71
+ }
72
+
73
+ /**
74
+ * Centralized reporting for warnings and info messages.
75
+ */
76
+ private report(level: 'warning' | 'info', message: string, location?: string): void {
77
+ this.messages.push({ level, message, location })
78
+ }
79
+
80
+ /**
81
+ * Main import method: orchestrates schema bundling, entity creation, and population.
82
+ */
83
+ public async import(schemas: InMemorySchema[], modelName: string): Promise<JsonImportReport> {
84
+ this.resetState(modelName)
85
+
86
+ // Build a flat map of all definitions and $id-indexed schemas
87
+ const definitionMap = new Map<string, JSONSchema7>()
88
+ const idMap = new Map<string, JSONSchema7>()
89
+ for (const schema of schemas) {
90
+ // Add $id mapping if present
91
+ if (schema.contents.$id) {
92
+ idMap.set(schema.contents.$id, schema.contents)
93
+ }
94
+ // Add the schema itself by its path
95
+ definitionMap.set(schema.path, schema.contents)
96
+ // Add all definitions inside this schema
97
+ const defs = (schema.contents.definitions ||
98
+ (schema.contents as unknown as { $defs: Record<string, JSONSchema7Definition> }).$defs) as
99
+ | Record<string, JSONSchema7Definition>
100
+ | undefined
101
+ if (defs) {
102
+ for (const [defName, defSchema] of Object.entries(defs)) {
103
+ if (typeof defSchema === 'object') {
104
+ // Use a JSON pointer for the key
105
+ definitionMap.set(`${schema.path}#/definitions/${defName}`, defSchema as JSONSchema7)
106
+ // Also allow lookup by just #/definitions/defName for single-file schemas
107
+ definitionMap.set(`#/definitions/${defName}`, defSchema as JSONSchema7)
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // == Pass 1: Create all entities ==
114
+ for (const [key, schema] of definitionMap.entries()) {
115
+ if (typeof schema === 'object' && schema.type === 'object') {
116
+ this.createEntity(key, schema, key)
117
+ } else if (this.isEnum(schema)) {
118
+ this.enums.set(key, schema)
119
+ } else {
120
+ this.report('warning', `Skipping non-object schema at ${key}`, key)
121
+ }
122
+ }
123
+ // Also create entities for all $id-indexed schemas if not already present
124
+ for (const [id, schema] of idMap.entries()) {
125
+ if (!this.refMap.has(id) && typeof schema === 'object' && schema.type === 'object') {
126
+ this.createEntity(id, schema, id)
127
+ } else if (this.isEnum(schema)) {
128
+ this.enums.set(id, schema)
129
+ } else {
130
+ this.report('warning', `Skipping non-object schema at ${id}`, id)
131
+ }
132
+ }
133
+
134
+ // == Pass 2: Populate entities ==
135
+ for (const [refPath, entityKey] of this.refMap.entries()) {
136
+ const entity = this.domain.findEntity(entityKey)
137
+ if (!entity) {
138
+ this.report('warning', `Entity not found: ${entityKey}`, refPath)
139
+ continue
140
+ }
141
+ const schema = this.resolveSchemaRef(refPath, definitionMap, idMap)
142
+ if (!schema) {
143
+ this.report('warning', `Schema not found for ref: ${refPath}`, refPath)
144
+ continue
145
+ }
146
+ this.populateEntity(entity, schema)
147
+ }
148
+
149
+ return { model: this.model, messages: this.messages }
150
+ }
151
+
152
+ private isEnum(schema: JSONSchema7): boolean {
153
+ if (schema.type !== 'string') {
154
+ // I might be wrong about it, but I think enums are only for strings
155
+ return false
156
+ }
157
+ if (!Array.isArray(schema.oneOf)) {
158
+ return false
159
+ }
160
+ const notConst = schema.oneOf.find((s) => typeof s !== 'object' || !('const' in s))
161
+ return !notConst // If all schemas in oneOf have 'const', it's an enum
162
+ }
163
+
164
+ /**
165
+ * Resolves a schema reference from the flat definition/id maps.
166
+ * Supports #/definitions/..., $id, or direct path keys.
167
+ */
168
+ private resolveSchemaRef(
169
+ refPath: string,
170
+ definitionMap: Map<string, JSONSchema7>,
171
+ idMap: Map<string, JSONSchema7>
172
+ ): JSONSchema7 | undefined {
173
+ // Try direct match
174
+ if (definitionMap.has(refPath)) return definitionMap.get(refPath)
175
+ if (idMap.has(refPath)) return idMap.get(refPath)
176
+ // Try to resolve #/definitions/Name from any schema
177
+ if (refPath.startsWith('#/definitions/')) {
178
+ for (const [key, schema] of definitionMap.entries()) {
179
+ if (key.endsWith(refPath)) return schema
180
+ }
181
+ }
182
+ // Try to resolve by $id fragment
183
+ if (refPath.startsWith('#')) {
184
+ for (const [id, schema] of idMap.entries()) {
185
+ if (id.endsWith(refPath)) return schema
186
+ }
187
+ }
188
+ return undefined
189
+ }
190
+
191
+ /**
192
+ * Helper for entity creation, can be extended for custom logic.
193
+ */
194
+ private createEntity(name: string, schema: JSONSchema7, refPath: string): DomainEntity {
195
+ const fixedName = schema.title || name
196
+ const cleanName = toDatabaseTableName(fixedName, 'untitled_entity')
197
+ const entity = this.model.addEntity({
198
+ info: {
199
+ name: cleanName,
200
+ displayName: fixedName,
201
+ },
202
+ })
203
+ if (fixedName !== cleanName) {
204
+ entity.info.displayName = fixedName // Keep original title for display
205
+ }
206
+ if (schema.description) {
207
+ entity.info.description = schema.description
208
+ }
209
+ this.refMap.set(refPath, entity.key)
210
+ return entity
211
+ }
212
+
213
+ /**
214
+ * Populates a DomainEntity with its properties, associations, and parent relationships.
215
+ */
216
+ private populateEntity(entity: DomainEntity, schema: JSONSchema7): void {
217
+ const properties = schema.properties || {}
218
+ const required = new Set(schema.required || [])
219
+
220
+ for (const [propName, propSchema] of Object.entries(properties)) {
221
+ if (typeof propSchema === 'object') {
222
+ this.createPropertyOrAssociation(entity, propName, propSchema, required.has(propName))
223
+ }
224
+ }
225
+
226
+ // Handle 'allOf' for inheritance and composition.
227
+ if (schema.allOf) {
228
+ this.processAllOf(entity, schema.allOf)
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Processes the `allOf` keyword, which can contain references (for inheritance)
234
+ * or inline schemas (for composition/mixins).
235
+ */
236
+ private processAllOf(entity: DomainEntity, allOfSchemas: JSONSchema7Definition[]): void {
237
+ allOfSchemas.forEach((subSchema, index) => {
238
+ if (typeof subSchema !== 'object') {
239
+ return
240
+ }
241
+ // Case 1: It's a reference to another entity (standard inheritance).
242
+ if (subSchema.$ref) {
243
+ // Try to resolve the referenced entity by $ref (could be #/definitions/..., $id, or path)
244
+ const parentKey = this.refMap.get(subSchema.$ref)
245
+ if (parentKey) {
246
+ try {
247
+ entity.addParent(parentKey)
248
+ } catch (e) {
249
+ this.report(
250
+ 'warning',
251
+ `Could not add parent ${parentKey} to ${entity.key}: ${(e as Error).message}`,
252
+ entity.key
253
+ )
254
+ }
255
+ } else {
256
+ this.report('warning', `Could not resolve parent reference: ${subSchema.$ref}`, entity.key)
257
+ }
258
+ return
259
+ }
260
+ // Case 2: It's an inline schema to be mixed in.
261
+ // const mixinKey = `${entity.key}_mixin_${index}`
262
+ const mixinName = subSchema.title || `${entity.info.name} Mixin ${index + 1}`
263
+ const mixinEntity = this.model.addEntity({
264
+ // key: mixinKey,
265
+ info: {
266
+ name: mixinName,
267
+ description:
268
+ subSchema.description ||
269
+ `Auto-generated mixin entity from an 'allOf' clause in the schema for '${entity.info.name}'. This entity is not intended for direct use.`,
270
+ },
271
+ })
272
+ this.populateEntity(mixinEntity, subSchema)
273
+ try {
274
+ entity.addParent(mixinEntity.key)
275
+ } catch (e) {
276
+ this.report(
277
+ 'warning',
278
+ `Could not add mixin parent ${mixinEntity.key} to ${entity.key}: ${(e as Error).message}`,
279
+ entity.key
280
+ )
281
+ }
282
+ })
283
+ }
284
+ /**
285
+ * Recursively analyzes a schema definition to find all contained primitive types and `$ref`s.
286
+ * It's used to understand the nature of a property (is it a primitive, an association, or a mix).
287
+ */
288
+ private collectTypesAndRefs(schema: JSONSchema7Definition): {
289
+ refs: Set<string>
290
+ types: Set<JSONSchema7TypeName>
291
+ isArray: boolean
292
+ } {
293
+ const collected = { refs: new Set<string>(), types: new Set<JSONSchema7TypeName>(), isArray: false }
294
+
295
+ if (typeof schema !== 'object') {
296
+ return collected
297
+ }
298
+
299
+ if (schema.$ref) {
300
+ collected.refs.add(schema.$ref)
301
+ return collected
302
+ }
303
+
304
+ if (schema.type) {
305
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type]
306
+ for (const t of types) {
307
+ if (t !== 'null') collected.types.add(t)
308
+ }
309
+ }
310
+
311
+ if (schema.type === 'array') {
312
+ collected.isArray = true
313
+ if (typeof schema.items === 'object') {
314
+ // Recurse into items
315
+ const itemInfo = this.collectTypesAndRefs(Array.isArray(schema.items) ? schema.items[0] : schema.items)
316
+ itemInfo.refs.forEach((r) => collected.refs.add(r))
317
+ itemInfo.types.forEach((t) => collected.types.add(t))
318
+ }
319
+ return collected
320
+ }
321
+
322
+ const choices = schema.anyOf || schema.oneOf
323
+ if (choices) {
324
+ for (const choice of choices) {
325
+ const choiceInfo = this.collectTypesAndRefs(choice)
326
+ choiceInfo.refs.forEach((r) => collected.refs.add(r))
327
+ choiceInfo.types.forEach((t) => collected.types.add(t))
328
+ if (choiceInfo.isArray) {
329
+ collected.isArray = true
330
+ }
331
+ }
332
+ }
333
+
334
+ return collected
335
+ }
336
+
337
+ /**
338
+ * Determines whether to create a DomainProperty or a DomainAssociation for a given schema property.
339
+ */
340
+ private createPropertyOrAssociation(
341
+ entity: DomainEntity,
342
+ propName: string,
343
+ propSchema: JSONSchema7,
344
+ isRequired: boolean
345
+ ): void {
346
+ // Analyze the property schema to understand its composition (primitives, refs, arrays).
347
+ const analysis = this.collectTypesAndRefs(propSchema)
348
+
349
+ // Case 1: If the property has `$ref` to an enum, we treat it as a DomainProperty.
350
+ if (analysis.refs.size === 1 && this.enums.has([...analysis.refs][0])) {
351
+ const prop = this.createEnumProperty(
352
+ propName,
353
+ propSchema,
354
+ this.enums.get([...analysis.refs][0]) as JSONSchema7,
355
+ entity.info.name as string
356
+ )
357
+ if (prop) {
358
+ prop.multiple = analysis.isArray
359
+ // console.log(`Adding enum property '${propName}' to entity '${entity.info.name}'`, prop.toJSON())
360
+ entity.addProperty(prop.toJSON())
361
+ }
362
+ return
363
+ }
364
+
365
+ // Case 2: If any `$ref` was found, we treat it as an Association.
366
+ if (analysis.refs.size > 0) {
367
+ const sanitizedName = toDatabaseColumnName(propName, 'untitled_association')
368
+ const assoc = entity.addAssociation({}, { info: { name: sanitizedName } })
369
+ if (sanitizedName !== propName) {
370
+ assoc.info.displayName = propName // Keep original name for display
371
+ }
372
+ if (propSchema.description) {
373
+ assoc.info.description = propSchema.description
374
+ }
375
+ assoc.required = isRequired
376
+ assoc.multiple = analysis.isArray
377
+ for (const ref of analysis.refs) {
378
+ const targetKey = this.refMap.get(ref)
379
+ if (targetKey) {
380
+ assoc.addTarget(targetKey)
381
+ }
382
+ }
383
+ if (analysis.types.size > 0) {
384
+ this.report(
385
+ 'warning',
386
+ `Property '${propName}' has a mix of reference and primitive types. Only the reference(s) will be used to create an association. The primitive types (${[
387
+ ...analysis.types,
388
+ ].join(', ')}) are ignored.`,
389
+ entity.key
390
+ )
391
+ }
392
+ return
393
+ }
394
+ // Case 3: No `$ref`s found, only primitive types. We treat it as a DomainProperty.
395
+ if (analysis.types.size > 0) {
396
+ // We need to create a temporary schema object to pass to createDomainProperty,
397
+ // as it expects a single schema. We'll use the first detected type.
398
+ const tempSchema: JSONSchema7 = {
399
+ ...propSchema,
400
+ // Pass the collected types to be resolved by mapJsonTypeToDomainType
401
+ type: [...analysis.types],
402
+ }
403
+ const prop = this.createDomainProperty(entity.key, propName, tempSchema, isRequired)
404
+ if (prop) {
405
+ prop.multiple = analysis.isArray
406
+ entity.addProperty(prop.toJSON())
407
+ }
408
+ return
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Creates and configures a DomainProperty based on a JSON schema property definition.
414
+ */
415
+ private createDomainProperty(
416
+ parentEntityKey: string,
417
+ propName: string,
418
+ propSchema: JSONSchema7,
419
+ isRequired: boolean
420
+ ): DomainProperty | null {
421
+ const typeInfo = this.mapJsonTypeToDomainType(propSchema.type, propSchema, `${parentEntityKey}.${propName}`)
422
+ if (!typeInfo) return null
423
+ const sanitizedName = toDatabaseColumnName(propName, 'untitled_property')
424
+ const propertyInit: Partial<DomainPropertySchema> = {
425
+ info: { name: sanitizedName, description: propSchema.description },
426
+ type: typeInfo.type,
427
+ required: isRequired,
428
+ }
429
+
430
+ // Use the constructor that doesn't add to the graph immediately.
431
+ const prop = new DomainProperty(this.domain, parentEntityKey, propertyInit)
432
+ if (sanitizedName !== propName) {
433
+ prop.info.displayName = propName // Keep original name for display
434
+ }
435
+
436
+ const schema: PropertySchema = {}
437
+ const webBindings: PropertyWebBindings = {}
438
+
439
+ if (propSchema.pattern) schema.pattern = propSchema.pattern
440
+ if (propSchema.enum) schema.enum = propSchema.enum.map(String)
441
+ if (propSchema.default !== undefined) schema.defaultValue = { type: 'literal', value: String(propSchema.default) }
442
+
443
+ if (typeInfo.type === 'string') {
444
+ if (typeof propSchema.minLength === 'number') schema.minimum = propSchema.minLength
445
+ if (typeof propSchema.maxLength === 'number') schema.maximum = propSchema.maxLength
446
+ } else if (typeInfo.type === 'number') {
447
+ if (typeof propSchema.minimum === 'number') schema.minimum = propSchema.minimum
448
+ if (typeof propSchema.maximum === 'number') schema.maximum = propSchema.maximum
449
+ if (typeof propSchema.exclusiveMinimum === 'number') {
450
+ schema.minimum = propSchema.exclusiveMinimum
451
+ schema.exclusiveMinimum = true
452
+ }
453
+ if (typeof propSchema.exclusiveMaximum === 'number') {
454
+ schema.maximum = propSchema.exclusiveMaximum
455
+ schema.exclusiveMaximum = true
456
+ }
457
+ if (typeof propSchema.multipleOf === 'number') schema.multipleOf = propSchema.multipleOf
458
+ if (typeInfo.format) {
459
+ webBindings.format = typeInfo.format
460
+ }
461
+ }
462
+
463
+ if (Object.keys(schema).length > 0) {
464
+ prop.schema = schema
465
+ }
466
+ if (Object.keys(webBindings).length > 0) {
467
+ prop.bindings = [{ type: 'web', schema: webBindings }]
468
+ }
469
+
470
+ return prop
471
+ }
472
+
473
+ /**
474
+ * Maps a JSON Schema type to a DomainPropertyType.
475
+ */
476
+ private mapJsonTypeToDomainType(
477
+ jsonType: JSONSchema7TypeName | JSONSchema7TypeName[] | undefined,
478
+ schema: JSONSchema7,
479
+ location?: string
480
+ ): { type: DomainPropertyType; format?: DomainPropertyFormat } | null {
481
+ if (!jsonType) {
482
+ return null
483
+ }
484
+
485
+ if (Array.isArray(jsonType)) {
486
+ const nonNullTypes = jsonType.filter((t) => t !== 'null')
487
+ if (nonNullTypes.length === 0) {
488
+ return null
489
+ }
490
+ if (nonNullTypes.length === 2 && nonNullTypes.includes('array')) {
491
+ const nonArrayType = nonNullTypes.filter((t) => t !== 'array')[0]
492
+ return this.mapJsonTypeToDomainType(nonArrayType, schema, location)
493
+ }
494
+ if (nonNullTypes.length > 1) {
495
+ this.report(
496
+ 'warning',
497
+ `Property has a union of primitive types: [${nonNullTypes.join(', ')}]. The importer will use the first type ('${nonNullTypes[0]}').`,
498
+ location
499
+ )
500
+ }
501
+ // Pick the first non-null type from the union.
502
+ return this.mapJsonTypeToDomainType(nonNullTypes[0], schema, location)
503
+ }
504
+
505
+ if (jsonType === 'string') {
506
+ // JSON Schema has the `format` property which can specify more about the string type.
507
+ // We need to decide how to handle these formats.
508
+ const choices = schema.anyOf || schema.oneOf
509
+ if (Array.isArray(choices)) {
510
+ // If we have a list of choices, we need to pick one.
511
+ const formats: string[] = []
512
+ for (const choice of choices) {
513
+ const typed = choice as JSONSchema7
514
+ if (typed.format) {
515
+ formats.push(typed.format)
516
+ }
517
+ }
518
+ // we will list dates from the most specific to the least specific
519
+ if (formats.includes('date-time')) {
520
+ return { type: 'datetime' }
521
+ }
522
+ if (formats.includes('date')) {
523
+ return { type: 'date' }
524
+ }
525
+ if (formats.includes('time')) {
526
+ return { type: 'time' }
527
+ }
528
+ }
529
+ // If no specific format is found, we default to string.
530
+ return { type: 'string' }
531
+ }
532
+
533
+ switch (jsonType) {
534
+ case 'number':
535
+ return { type: 'number', format: 'double' }
536
+ case 'integer':
537
+ return { type: 'number', format: 'int64' }
538
+ case 'boolean':
539
+ return { type: 'boolean' }
540
+ // JSON Schema's 'object' and 'array' are handled as associations or multi-valued properties.
541
+ // Other types like 'null' are handled by making the property not required.
542
+ default:
543
+ return null
544
+ }
545
+ }
546
+
547
+ private createEnumProperty(
548
+ propName: string,
549
+ propSchema: JSONSchema7,
550
+ enumSchema: JSONSchema7,
551
+ parentEntityKey: string
552
+ ): DomainProperty | null {
553
+ const description = propSchema.description ?? enumSchema.description
554
+ const typeInfo = this.mapJsonTypeToDomainType(enumSchema.type, enumSchema, `${parentEntityKey}.${propName}`)
555
+ if (!typeInfo) return null
556
+
557
+ const sanitizedName = toDatabaseColumnName(propName)
558
+ const propertyInit: Partial<DomainPropertySchema> = {
559
+ info: { name: sanitizedName },
560
+ type: typeInfo.type,
561
+ // required: isRequired,
562
+ required: false,
563
+ }
564
+
565
+ // Use the constructor that doesn't add to the graph immediately.
566
+ const prop = new DomainProperty(this.domain, parentEntityKey, propertyInit)
567
+ if (sanitizedName !== propName) {
568
+ prop.info.displayName = propName // Keep original name for display
569
+ }
570
+ if (description) {
571
+ prop.info.description = sanitizeInput(description)
572
+ }
573
+
574
+ const schema: PropertySchema = {}
575
+
576
+ const pattern = propSchema.pattern ?? enumSchema.pattern
577
+ if (pattern) {
578
+ schema.pattern = pattern
579
+ }
580
+ const defaultValue = propSchema.default ?? enumSchema.default
581
+ if (defaultValue !== undefined) {
582
+ schema.defaultValue = { type: 'literal', value: String(defaultValue) }
583
+ }
584
+
585
+ if (typeInfo.type === 'string') {
586
+ const minLength = propSchema.minLength ?? enumSchema.minLength
587
+ const maxLength = propSchema.maxLength ?? enumSchema.maxLength
588
+ if (minLength !== undefined) schema.minimum = minLength
589
+ if (maxLength !== undefined) schema.maximum = maxLength
590
+ } else if (typeInfo.type === 'number') {
591
+ const minimum = propSchema.minimum ?? enumSchema.minimum
592
+ const maximum = propSchema.maximum ?? enumSchema.maximum
593
+ if (minimum !== undefined) {
594
+ schema.minimum = minimum
595
+ }
596
+ if (maximum !== undefined) {
597
+ schema.maximum = maximum
598
+ }
599
+ const exclusiveMinimum = propSchema.exclusiveMinimum ?? enumSchema.exclusiveMinimum
600
+ const exclusiveMaximum = propSchema.exclusiveMaximum ?? enumSchema.exclusiveMaximum
601
+ if (typeof exclusiveMinimum === 'number') {
602
+ schema.minimum = exclusiveMinimum
603
+ schema.exclusiveMinimum = true
604
+ }
605
+ if (typeof exclusiveMaximum === 'number') {
606
+ schema.maximum = exclusiveMaximum
607
+ schema.exclusiveMaximum = true
608
+ }
609
+ const multipleOf = propSchema.multipleOf ?? enumSchema.multipleOf
610
+ if (typeof multipleOf === 'number') {
611
+ schema.multipleOf = multipleOf
612
+ }
613
+ }
614
+ if (enumSchema.oneOf && Array.isArray(enumSchema.oneOf)) {
615
+ schema.enum = []
616
+ for (const item of enumSchema.oneOf) {
617
+ if (typeof item !== 'object' || !('const' in item)) {
618
+ this.report(
619
+ 'warning',
620
+ `Invalid enum item in oneOf: ${JSON.stringify(item)}. Expected an object with 'const' property.`,
621
+ `${parentEntityKey}.${propName}`
622
+ )
623
+ continue
624
+ }
625
+ if (typeof (item as JSONSchema7).const !== 'string') {
626
+ this.report(
627
+ 'warning',
628
+ `Invalid enum const value: ${JSON.stringify((item as JSONSchema7).const)}. Expected a string.`,
629
+ `${parentEntityKey}.${propName}`
630
+ )
631
+ continue
632
+ }
633
+ schema.enum.push((item as JSONSchema7).const as string)
634
+ }
635
+ }
636
+
637
+ if (Object.keys(schema).length > 0) {
638
+ prop.schema = schema
639
+ }
640
+ return prop
641
+ }
642
+ }