@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.
- package/build/src/modeling/helpers/database.d.ts +24 -0
- package/build/src/modeling/helpers/database.d.ts.map +1 -0
- package/build/src/modeling/helpers/database.js +42 -0
- package/build/src/modeling/helpers/database.js.map +1 -0
- package/build/src/modeling/importers/CsvImporter.d.ts +41 -0
- package/build/src/modeling/importers/CsvImporter.d.ts.map +1 -0
- package/build/src/modeling/importers/CsvImporter.js +82 -0
- package/build/src/modeling/importers/CsvImporter.js.map +1 -0
- package/build/src/modeling/importers/ImporterException.d.ts +10 -0
- package/build/src/modeling/importers/ImporterException.d.ts.map +1 -0
- package/build/src/modeling/importers/ImporterException.js +10 -0
- package/build/src/modeling/importers/ImporterException.js.map +1 -0
- package/build/src/modeling/importers/JsonSchemaImporter.d.ts +99 -0
- package/build/src/modeling/importers/JsonSchemaImporter.d.ts.map +1 -0
- package/build/src/modeling/importers/JsonSchemaImporter.js +525 -0
- package/build/src/modeling/importers/JsonSchemaImporter.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -2
- package/src/modeling/helpers/database.ts +48 -0
- package/src/modeling/importers/CsvImporter.ts +91 -0
- package/src/modeling/importers/ImporterException.ts +10 -0
- package/src/modeling/importers/JsonSchemaImporter.ts +642 -0
- package/tests/unit/modeling/importers/csv_importer.spec.ts +189 -0
- package/tests/unit/modeling/importers/json_schema_importer.spec.ts +451 -0
|
@@ -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
|
+
}
|