@api-client/core 0.18.15 → 0.18.17
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/DataDomain.d.ts +7 -2
- package/build/src/modeling/DataDomain.d.ts.map +1 -1
- package/build/src/modeling/DataDomain.js +15 -2
- package/build/src/modeling/DataDomain.js.map +1 -1
- package/build/src/modeling/DomainSerialization.d.ts +6 -3
- package/build/src/modeling/DomainSerialization.d.ts.map +1 -1
- package/build/src/modeling/DomainSerialization.js +374 -52
- package/build/src/modeling/DomainSerialization.js.map +1 -1
- package/build/src/modeling/importers/SchemaFilteringStrategy.d.ts.map +1 -1
- package/build/src/modeling/importers/SchemaFilteringStrategy.js +3 -9
- package/build/src/modeling/importers/SchemaFilteringStrategy.js.map +1 -1
- package/build/src/modeling/types.d.ts +69 -2
- package/build/src/modeling/types.d.ts.map +1 -1
- package/build/src/modeling/types.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/data/models/example-generator-api.json +15 -15
- package/package.json +1 -1
- package/src/modeling/DataDomain.ts +24 -3
- package/src/modeling/DomainSerialization.ts +442 -56
- package/src/modeling/importers/SchemaFilteringStrategy.ts +3 -11
- package/src/modeling/types.ts +73 -2
- package/tests/unit/modeling/data_domain_serialization.spec.ts +504 -0
- package/tests/unit/modeling/importers/schema_filtering.spec.ts +47 -3
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { JsonEdge, Node } from '@api-client/graph/graph/types.js'
|
|
2
2
|
import { Graph } from '@api-client/graph/graph/Graph.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
DataDomainGraph,
|
|
5
|
+
DomainGraphEdge,
|
|
6
|
+
DomainGraphNodeType,
|
|
7
|
+
SerializedGraph,
|
|
8
|
+
DeserializationMode,
|
|
9
|
+
DeserializationResult,
|
|
10
|
+
DeserializationIssue,
|
|
11
|
+
DeserializeOptions,
|
|
12
|
+
} from './types.js'
|
|
4
13
|
import {
|
|
5
14
|
DomainAssociationKind,
|
|
6
15
|
DomainEntityKind,
|
|
@@ -78,7 +87,154 @@ function writeEdges(g: DataDomainGraph, domainKey: string): JsonEdge<DomainGraph
|
|
|
78
87
|
return result
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Validates the graph consistency before serialization.
|
|
92
|
+
* Ensures that all required edges exist for properties and associations.
|
|
93
|
+
* @param g The graph to validate
|
|
94
|
+
* @param domainKey The domain key to validate
|
|
95
|
+
* @throws Error if the graph is inconsistent
|
|
96
|
+
*/
|
|
97
|
+
function validateGraphConsistency(g: DataDomainGraph, domainKey: string): void {
|
|
98
|
+
const validationErrors: string[] = []
|
|
99
|
+
|
|
100
|
+
// Get all nodes that belong to this domain
|
|
101
|
+
const domainNodes = new Set<string>()
|
|
102
|
+
for (const nodeId of g.nodes()) {
|
|
103
|
+
const node = g.node(nodeId)
|
|
104
|
+
if (node && node.domain.key === domainKey) {
|
|
105
|
+
domainNodes.add(nodeId)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate that properties and associations have parent edges
|
|
110
|
+
for (const nodeId of domainNodes) {
|
|
111
|
+
const node = g.node(nodeId)
|
|
112
|
+
if (!node) continue
|
|
113
|
+
|
|
114
|
+
if (node.kind === DomainPropertyKind || node.kind === DomainAssociationKind) {
|
|
115
|
+
// These nodes must have exactly one incoming edge from their parent entity
|
|
116
|
+
const incomingEdges = [...g.inEdges(nodeId)]
|
|
117
|
+
const parentEdges = incomingEdges.filter((edge) => {
|
|
118
|
+
const sourceNode = g.node(edge.v)
|
|
119
|
+
return sourceNode && sourceNode.domain.key === domainKey
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
if (parentEdges.length === 0) {
|
|
123
|
+
const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association'
|
|
124
|
+
validationErrors.push(
|
|
125
|
+
`${nodeType} "${node.info.name}" (${nodeId}) has no parent entity edge. ` +
|
|
126
|
+
`This will cause deserialization to fail.`
|
|
127
|
+
)
|
|
128
|
+
} else if (parentEdges.length > 1) {
|
|
129
|
+
const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association'
|
|
130
|
+
const parentIds = parentEdges.map((e) => e.v).join(', ')
|
|
131
|
+
validationErrors.push(
|
|
132
|
+
`${nodeType} "${node.info.name}" (${nodeId}) has multiple parent edges: ${parentIds}. ` +
|
|
133
|
+
`This may cause deserialization issues.`
|
|
134
|
+
)
|
|
135
|
+
} else {
|
|
136
|
+
// Validate that the parent is actually an entity
|
|
137
|
+
const parentEdge = parentEdges[0]
|
|
138
|
+
const parentNode = g.node(parentEdge.v)
|
|
139
|
+
if (!parentNode || parentNode.kind !== DomainEntityKind) {
|
|
140
|
+
const nodeType = node.kind === DomainPropertyKind ? 'Property' : 'Association'
|
|
141
|
+
validationErrors.push(
|
|
142
|
+
`${nodeType} "${node.info.name}" (${nodeId}) has a parent that is not an entity. ` +
|
|
143
|
+
`Parent: ${parentNode?.kind || 'unknown'} (${parentEdge.v})`
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Validate that entities have model parents
|
|
150
|
+
if (node.kind === DomainEntityKind) {
|
|
151
|
+
const parents = [...g.parents(nodeId)]
|
|
152
|
+
const hasModelParent = parents.some((parentId) => {
|
|
153
|
+
const parent = g.node(parentId)
|
|
154
|
+
return parent && parent.kind === DomainModelKind
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
if (!hasModelParent) {
|
|
158
|
+
validationErrors.push(
|
|
159
|
+
`Entity "${node.info.name}" (${nodeId}) has no model parent. ` + `Entities must belong to a model.`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate that models have either namespace parents or are at root level
|
|
165
|
+
if (node.kind === DomainModelKind) {
|
|
166
|
+
const parents = [...g.parents(nodeId)]
|
|
167
|
+
const hasNamespaceParent = parents.some((parentId) => {
|
|
168
|
+
const parent = g.node(parentId)
|
|
169
|
+
return parent && parent.kind === DomainNamespaceKind
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// Models can either be at root level (no parents) or have a namespace parent
|
|
173
|
+
// They should not have other types of parents
|
|
174
|
+
if (parents.length > 0 && !hasNamespaceParent) {
|
|
175
|
+
const invalidParents = parents
|
|
176
|
+
.map((parentId) => {
|
|
177
|
+
const parent = g.node(parentId)
|
|
178
|
+
return `${parent?.kind || 'unknown'} (${parentId})`
|
|
179
|
+
})
|
|
180
|
+
.join(', ')
|
|
181
|
+
validationErrors.push(
|
|
182
|
+
`Model "${node.info.name}" (${nodeId}) has invalid parent types: ${invalidParents}. ` +
|
|
183
|
+
`Models can only belong to namespaces or be at root level.`
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validate association targets exist
|
|
190
|
+
for (const nodeId of domainNodes) {
|
|
191
|
+
const node = g.node(nodeId)
|
|
192
|
+
if (node && node.kind === DomainAssociationKind) {
|
|
193
|
+
const association = node as DomainAssociation
|
|
194
|
+
const outgoingEdges = [...g.outEdges(nodeId)]
|
|
195
|
+
|
|
196
|
+
// Check that association has target edges
|
|
197
|
+
const targetEdges = outgoingEdges.filter((edge) => {
|
|
198
|
+
const edgeData = g.edge(edge)
|
|
199
|
+
return edgeData && edgeData.type === 'association'
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
if (targetEdges.length === 0) {
|
|
203
|
+
validationErrors.push(
|
|
204
|
+
`Association "${association.info.name}" (${nodeId}) has no target entities. ` +
|
|
205
|
+
`Associations must reference at least one target entity.`
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Validate that all target entities exist
|
|
210
|
+
for (const targetEdge of targetEdges) {
|
|
211
|
+
const targetNode = g.node(targetEdge.w)
|
|
212
|
+
if (!targetNode) {
|
|
213
|
+
validationErrors.push(
|
|
214
|
+
`Association "${association.info.name}" (${nodeId}) references non-existent target: ${targetEdge.w}`
|
|
215
|
+
)
|
|
216
|
+
} else if (targetNode.kind !== DomainEntityKind) {
|
|
217
|
+
validationErrors.push(
|
|
218
|
+
`Association "${association.info.name}" (${nodeId}) references non-entity target: ` +
|
|
219
|
+
`${targetNode.kind} (${targetEdge.w})`
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (validationErrors.length > 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Graph consistency validation failed for domain "${domainKey}":\n` +
|
|
229
|
+
validationErrors.map((error) => ` - ${error}`).join('\n')
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
81
234
|
export function serialize(g: DataDomainGraph, domainKey: string): SerializedGraph {
|
|
235
|
+
// Validate graph consistency before serialization
|
|
236
|
+
validateGraphConsistency(g, domainKey)
|
|
237
|
+
|
|
82
238
|
const json: SerializedGraph = {
|
|
83
239
|
options: {
|
|
84
240
|
directed: g.isDirected(),
|
|
@@ -198,43 +354,124 @@ function findEdgeParent(child: string, edges: JsonEdge<DomainGraphEdge>[]): stri
|
|
|
198
354
|
* @param root The DataDomain instance to use as the root for the graph
|
|
199
355
|
* @param entry The node entry to restore
|
|
200
356
|
* @param edges The list of serialized graph edges
|
|
201
|
-
* @
|
|
202
|
-
* @
|
|
357
|
+
* @param mode The deserialization mode
|
|
358
|
+
* @param issues Array to collect issues in lenient mode
|
|
359
|
+
* @returns The restored node instance or undefined if failed in lenient mode
|
|
360
|
+
* @throws Error if the entry is malformed or the kind is unknown in strict mode
|
|
203
361
|
*/
|
|
204
|
-
function prepareNode(
|
|
362
|
+
function prepareNode(
|
|
363
|
+
root: DataDomain,
|
|
364
|
+
entry: unknown,
|
|
365
|
+
edges: JsonEdge<DomainGraphEdge>[],
|
|
366
|
+
mode: DeserializationMode,
|
|
367
|
+
issues: DeserializationIssue[]
|
|
368
|
+
): DomainGraphNodeType | undefined {
|
|
205
369
|
if (!entry) {
|
|
206
|
-
|
|
370
|
+
const issue: DeserializationIssue = {
|
|
371
|
+
type: 'malformed_entry',
|
|
372
|
+
severity: 'error',
|
|
373
|
+
message: 'Unable to restore data domain graph. Malformed node entry',
|
|
374
|
+
}
|
|
375
|
+
issues.push(issue)
|
|
376
|
+
|
|
377
|
+
if (mode === 'strict') {
|
|
378
|
+
throw new Error(issue.message)
|
|
379
|
+
}
|
|
380
|
+
return undefined
|
|
207
381
|
}
|
|
208
|
-
|
|
382
|
+
|
|
383
|
+
const domainElement = entry as { kind?: string; key?: string }
|
|
209
384
|
if (!domainElement.kind) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (domainElement.kind === DomainModelKind) {
|
|
216
|
-
return new DomainModel(root, domainElement as DomainModelSchema)
|
|
217
|
-
}
|
|
218
|
-
if (domainElement.kind === DomainEntityKind) {
|
|
219
|
-
return new DomainEntity(root, domainElement as DomainEntitySchema)
|
|
220
|
-
}
|
|
221
|
-
if (domainElement.kind === DomainPropertyKind) {
|
|
222
|
-
const typed = domainElement as DomainPropertySchema
|
|
223
|
-
const parent = findEdgeParent(typed.key, edges)
|
|
224
|
-
if (!parent) {
|
|
225
|
-
throw new Error(`Unable to restore data domain graph. Malformed node entry`)
|
|
385
|
+
const issue: DeserializationIssue = {
|
|
386
|
+
type: 'malformed_entry',
|
|
387
|
+
severity: 'error',
|
|
388
|
+
message: 'Unable to restore data domain graph. Node entry missing kind property',
|
|
389
|
+
affectedKey: domainElement.key,
|
|
226
390
|
}
|
|
227
|
-
|
|
391
|
+
issues.push(issue)
|
|
392
|
+
|
|
393
|
+
if (mode === 'strict') {
|
|
394
|
+
throw new Error(issue.message)
|
|
395
|
+
}
|
|
396
|
+
return undefined
|
|
228
397
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
if (domainElement.kind === DomainNamespaceKind) {
|
|
401
|
+
return new DomainNamespace(root, domainElement as DomainNamespaceSchema)
|
|
402
|
+
}
|
|
403
|
+
if (domainElement.kind === DomainModelKind) {
|
|
404
|
+
return new DomainModel(root, domainElement as DomainModelSchema)
|
|
405
|
+
}
|
|
406
|
+
if (domainElement.kind === DomainEntityKind) {
|
|
407
|
+
return new DomainEntity(root, domainElement as DomainEntitySchema)
|
|
408
|
+
}
|
|
409
|
+
if (domainElement.kind === DomainPropertyKind) {
|
|
410
|
+
const typed = domainElement as DomainPropertySchema
|
|
411
|
+
const parent = findEdgeParent(typed.key, edges)
|
|
412
|
+
if (!parent) {
|
|
413
|
+
const issue: DeserializationIssue = {
|
|
414
|
+
type: 'missing_edge',
|
|
415
|
+
severity: 'error',
|
|
416
|
+
message: `Property "${typed.info?.name || typed.key}" has no parent entity edge`,
|
|
417
|
+
affectedKey: typed.key,
|
|
418
|
+
resolution: mode === 'lenient' ? 'Property will be skipped' : undefined,
|
|
419
|
+
}
|
|
420
|
+
issues.push(issue)
|
|
421
|
+
|
|
422
|
+
if (mode === 'strict') {
|
|
423
|
+
throw new Error(`Unable to restore data domain graph. Malformed node entry`)
|
|
424
|
+
}
|
|
425
|
+
return undefined
|
|
426
|
+
}
|
|
427
|
+
return new DomainProperty(root, parent, typed)
|
|
428
|
+
}
|
|
429
|
+
if (domainElement.kind === DomainAssociationKind) {
|
|
430
|
+
const typed = domainElement as DomainAssociationSchema
|
|
431
|
+
const parent = findEdgeParent(typed.key, edges)
|
|
432
|
+
if (!parent) {
|
|
433
|
+
const issue: DeserializationIssue = {
|
|
434
|
+
type: 'missing_edge',
|
|
435
|
+
severity: 'error',
|
|
436
|
+
message: `Association "${typed.info?.name || typed.key}" has no parent entity edge`,
|
|
437
|
+
affectedKey: typed.key,
|
|
438
|
+
resolution: mode === 'lenient' ? 'Association will be skipped' : undefined,
|
|
439
|
+
}
|
|
440
|
+
issues.push(issue)
|
|
441
|
+
if (mode === 'strict') {
|
|
442
|
+
throw new Error(`Unable to restore data domain graph. Malformed node entry`)
|
|
443
|
+
}
|
|
444
|
+
return undefined
|
|
445
|
+
}
|
|
446
|
+
return new DomainAssociation(root, parent, typed)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const issue: DeserializationIssue = {
|
|
450
|
+
type: 'unknown_kind',
|
|
451
|
+
severity: 'error',
|
|
452
|
+
message: `Unknown node kind: ${domainElement.kind}`,
|
|
453
|
+
affectedKey: domainElement.key,
|
|
234
454
|
}
|
|
235
|
-
|
|
455
|
+
issues.push(issue)
|
|
456
|
+
|
|
457
|
+
if (mode === 'strict') {
|
|
458
|
+
throw new Error(`Unable to restore data domain graph. Unknown node kind ${domainElement.kind}`)
|
|
459
|
+
}
|
|
460
|
+
return undefined
|
|
461
|
+
} catch (error) {
|
|
462
|
+
const issue: DeserializationIssue = {
|
|
463
|
+
type: 'malformed_entry',
|
|
464
|
+
severity: 'error',
|
|
465
|
+
message: `Failed to create node: ${error instanceof Error ? error.message : String(error)}`,
|
|
466
|
+
affectedKey: domainElement.key,
|
|
467
|
+
}
|
|
468
|
+
issues.push(issue)
|
|
469
|
+
|
|
470
|
+
if (mode === 'strict') {
|
|
471
|
+
throw error
|
|
472
|
+
}
|
|
473
|
+
return undefined
|
|
236
474
|
}
|
|
237
|
-
throw new Error(`Unable to restore data domain graph. Unknown node kind ${domainElement.kind}`)
|
|
238
475
|
}
|
|
239
476
|
|
|
240
477
|
/**
|
|
@@ -246,74 +483,223 @@ function prepareNode(root: DataDomain, entry: unknown, edges: JsonEdge<DomainGra
|
|
|
246
483
|
*
|
|
247
484
|
* @param root The DataDomain instance to use as the root for the graph
|
|
248
485
|
* @param json The previously serialized graph
|
|
249
|
-
* @
|
|
486
|
+
* @param dependencies An array of foreign data domains to register with this domain
|
|
487
|
+
* @param mode The deserialization mode - 'strict' throws errors, 'lenient' collects issues
|
|
488
|
+
* @returns DeserializationResult with graph, issues, and success status
|
|
489
|
+
* @throws Error in strict mode when any issue is encountered
|
|
250
490
|
*/
|
|
251
|
-
export function deserialize(root: DataDomain,
|
|
491
|
+
export function deserialize(root: DataDomain, options: DeserializeOptions = {}): DeserializationResult {
|
|
492
|
+
const { json, dependencies, mode = 'strict' } = options
|
|
252
493
|
const g = new Graph<unknown, DomainGraphNodeType, DomainGraphEdge>({
|
|
253
494
|
compound: true,
|
|
254
495
|
multigraph: true,
|
|
255
496
|
directed: true,
|
|
256
497
|
})
|
|
498
|
+
const issues: DeserializationIssue[] = []
|
|
257
499
|
let foreignNodes = new Set<string>()
|
|
258
500
|
let foreignEdges = new Set<string>()
|
|
501
|
+
|
|
502
|
+
// Process dependencies
|
|
259
503
|
if (dependencies) {
|
|
260
504
|
for (const dependency of dependencies) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
505
|
+
try {
|
|
506
|
+
const result = mergeGraph(g, dependency.graph, dependency.key)
|
|
507
|
+
if (result.edges.size) {
|
|
508
|
+
foreignEdges = new Set([...foreignEdges, ...result.edges])
|
|
509
|
+
}
|
|
510
|
+
if (result.nodes.size) {
|
|
511
|
+
foreignNodes = new Set([...foreignNodes, ...result.nodes])
|
|
512
|
+
}
|
|
513
|
+
root.dependencies.set(dependency.key, dependency)
|
|
514
|
+
} catch (error) {
|
|
515
|
+
const message = `Failed to merge dependency "${dependency.key}": ${error instanceof Error ? error.message : String(error)}`
|
|
516
|
+
|
|
517
|
+
if (mode === 'strict') {
|
|
518
|
+
throw new Error(message)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const issue: DeserializationIssue = {
|
|
522
|
+
type: 'missing_dependency',
|
|
523
|
+
severity: 'error',
|
|
524
|
+
message,
|
|
525
|
+
affectedKey: dependency.key,
|
|
526
|
+
resolution: 'Dependency will be skipped',
|
|
527
|
+
}
|
|
528
|
+
issues.push(issue)
|
|
267
529
|
}
|
|
268
|
-
root.dependencies.set(dependency.key, dependency)
|
|
269
530
|
}
|
|
270
531
|
}
|
|
532
|
+
|
|
271
533
|
if (!json) {
|
|
272
|
-
return
|
|
534
|
+
return {
|
|
535
|
+
graph: g,
|
|
536
|
+
issues,
|
|
537
|
+
success: true,
|
|
538
|
+
}
|
|
273
539
|
}
|
|
540
|
+
|
|
541
|
+
// Process nodes
|
|
274
542
|
if (Array.isArray(json.nodes)) {
|
|
275
543
|
// 1st pass - set up nodes
|
|
276
544
|
const parentInfo = new Map<string, string[]>()
|
|
277
545
|
for (const entry of json.nodes) {
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
546
|
+
const node = prepareNode(root, entry.value, json.edges, mode, issues)
|
|
547
|
+
if (node) {
|
|
548
|
+
g.setNode(entry.v, node)
|
|
549
|
+
if (entry.parents) {
|
|
550
|
+
parentInfo.set(entry.v, entry.parents)
|
|
551
|
+
}
|
|
281
552
|
}
|
|
553
|
+
// Note: prepareNode throws in strict mode, so we won't reach here if there's an error
|
|
282
554
|
}
|
|
555
|
+
|
|
283
556
|
// 2nd pass - set up parents
|
|
284
557
|
for (const [key, parents] of parentInfo) {
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
558
|
+
// Verify the node still exists (might have been skipped in lenient mode)
|
|
559
|
+
if (!g.hasNode(key)) {
|
|
560
|
+
const issue: DeserializationIssue = {
|
|
561
|
+
type: 'missing_node',
|
|
562
|
+
severity: 'warning',
|
|
563
|
+
message: `Cannot set parents for missing node: ${key}`,
|
|
564
|
+
affectedKey: key,
|
|
565
|
+
resolution: 'Parent relationship will be skipped',
|
|
566
|
+
}
|
|
567
|
+
issues.push(issue)
|
|
568
|
+
continue
|
|
569
|
+
}
|
|
570
|
+
|
|
292
571
|
for (const parent of parents) {
|
|
293
|
-
|
|
572
|
+
// Verify parent exists
|
|
573
|
+
if (!g.hasNode(parent)) {
|
|
574
|
+
const issue: DeserializationIssue = {
|
|
575
|
+
type: 'missing_node',
|
|
576
|
+
severity: 'warning',
|
|
577
|
+
message: `Parent node "${parent}" not found for child "${key}"`,
|
|
578
|
+
affectedKey: key,
|
|
579
|
+
context: { parentKey: parent },
|
|
580
|
+
resolution: 'Parent relationship will be skipped',
|
|
581
|
+
}
|
|
582
|
+
issues.push(issue)
|
|
583
|
+
continue
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
g.setParent(key, parent)
|
|
588
|
+
} catch (error) {
|
|
589
|
+
const message = `Failed to set parent "${parent}" for child "${key}": ${error instanceof Error ? error.message : String(error)}`
|
|
590
|
+
const issue: DeserializationIssue = {
|
|
591
|
+
type: 'invalid_parent',
|
|
592
|
+
severity: 'warning',
|
|
593
|
+
message,
|
|
594
|
+
affectedKey: key,
|
|
595
|
+
context: { parentKey: parent },
|
|
596
|
+
resolution: 'Parent relationship will be skipped',
|
|
597
|
+
}
|
|
598
|
+
issues.push(issue)
|
|
599
|
+
}
|
|
294
600
|
}
|
|
295
601
|
}
|
|
296
602
|
}
|
|
603
|
+
|
|
604
|
+
// Process edges
|
|
297
605
|
if (Array.isArray(json.edges)) {
|
|
298
606
|
for (const entry of json.edges) {
|
|
299
607
|
if (!entry.value) {
|
|
300
|
-
|
|
608
|
+
const message = 'Edge entry missing value'
|
|
609
|
+
|
|
610
|
+
if (mode === 'strict') {
|
|
611
|
+
throw new Error(message)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const issue: DeserializationIssue = {
|
|
615
|
+
type: 'malformed_entry',
|
|
616
|
+
severity: 'error',
|
|
617
|
+
message,
|
|
618
|
+
context: { edge: entry },
|
|
619
|
+
}
|
|
620
|
+
issues.push(issue)
|
|
621
|
+
continue
|
|
301
622
|
}
|
|
623
|
+
|
|
624
|
+
// Handle foreign edges
|
|
302
625
|
if (entry.value.foreign) {
|
|
303
626
|
// The `v` has to be local to the graph. The `w` must be foreign.
|
|
304
627
|
if (!foreignNodes.has(entry.w)) {
|
|
305
|
-
//
|
|
628
|
+
// Note, we don't consider this an error, just a warning.
|
|
629
|
+
// Because of that, we don't throw an error here.
|
|
630
|
+
const issue: DeserializationIssue = {
|
|
631
|
+
type: 'missing_node',
|
|
632
|
+
severity: 'warning',
|
|
633
|
+
message: `Missing foreign node: ${entry.w}`,
|
|
634
|
+
affectedKey: entry.w,
|
|
635
|
+
resolution: 'Edge will be skipped',
|
|
636
|
+
}
|
|
637
|
+
issues.push(issue)
|
|
306
638
|
continue
|
|
307
639
|
}
|
|
308
640
|
}
|
|
641
|
+
|
|
642
|
+
// Check if nodes exist for the edge
|
|
309
643
|
if (foreignNodes.has(entry.v) || foreignNodes.has(entry.w)) {
|
|
310
644
|
if (!g.hasNode(entry.v) || !g.hasNode(entry.w)) {
|
|
311
|
-
|
|
645
|
+
const issue: DeserializationIssue = {
|
|
646
|
+
type: 'missing_node',
|
|
647
|
+
severity: 'warning',
|
|
648
|
+
message: `Missing foreign node for edge: ${entry.v} -> ${entry.w}`,
|
|
649
|
+
context: { sourceNode: entry.v, targetNode: entry.w },
|
|
650
|
+
resolution: 'Edge will be skipped',
|
|
651
|
+
}
|
|
652
|
+
issues.push(issue)
|
|
653
|
+
continue
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
// Both nodes should be local - verify they exist
|
|
657
|
+
if (!g.hasNode(entry.v)) {
|
|
658
|
+
const issue: DeserializationIssue = {
|
|
659
|
+
type: 'missing_node',
|
|
660
|
+
severity: 'warning',
|
|
661
|
+
message: `Source node not found for edge: ${entry.v}`,
|
|
662
|
+
affectedKey: entry.v,
|
|
663
|
+
resolution: 'Edge will be skipped',
|
|
664
|
+
}
|
|
665
|
+
issues.push(issue)
|
|
666
|
+
continue
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (!g.hasNode(entry.w)) {
|
|
670
|
+
const issue: DeserializationIssue = {
|
|
671
|
+
type: 'missing_node',
|
|
672
|
+
severity: 'warning',
|
|
673
|
+
message: `Target node not found for edge: ${entry.w}`,
|
|
674
|
+
affectedKey: entry.w,
|
|
675
|
+
resolution: 'Edge will be skipped',
|
|
676
|
+
}
|
|
677
|
+
issues.push(issue)
|
|
312
678
|
continue
|
|
313
679
|
}
|
|
314
680
|
}
|
|
315
|
-
|
|
681
|
+
|
|
682
|
+
try {
|
|
683
|
+
g.setEdge({ v: entry.v, w: entry.w, name: entry.name }, entry.value)
|
|
684
|
+
} catch (error) {
|
|
685
|
+
const message = `Failed to create edge ${entry.v} -> ${entry.w}: ${error instanceof Error ? error.message : String(error)}`
|
|
686
|
+
const issue: DeserializationIssue = {
|
|
687
|
+
type: 'malformed_entry',
|
|
688
|
+
severity: 'warning',
|
|
689
|
+
message,
|
|
690
|
+
context: { sourceNode: entry.v, targetNode: entry.w },
|
|
691
|
+
resolution: 'Edge will be skipped',
|
|
692
|
+
}
|
|
693
|
+
issues.push(issue)
|
|
694
|
+
}
|
|
316
695
|
}
|
|
317
696
|
}
|
|
318
|
-
|
|
697
|
+
|
|
698
|
+
const hasErrors = issues.some((issue) => issue.severity === 'error')
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
graph: g,
|
|
702
|
+
issues,
|
|
703
|
+
success: !hasErrors,
|
|
704
|
+
}
|
|
319
705
|
}
|
|
@@ -107,17 +107,9 @@ export class SchemaFilteringStrategy {
|
|
|
107
107
|
return true
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
schema.allOf?.length === 1 &&
|
|
114
|
-
typeof schema.allOf[0] === 'object' &&
|
|
115
|
-
'$ref' in schema.allOf[0] &&
|
|
116
|
-
!schema.properties &&
|
|
117
|
-
!schema.additionalProperties
|
|
118
|
-
) {
|
|
119
|
-
return true
|
|
120
|
-
}
|
|
110
|
+
// Note: We removed the check for single allOf with $ref as this represents
|
|
111
|
+
// valid schema inheritance/extension patterns (e.g., BlogPosting extending SocialMediaPosting)
|
|
112
|
+
// Such schemas should be included as they define meaningful type hierarchies
|
|
121
113
|
|
|
122
114
|
// Check for schemas that only have description and type but no structure
|
|
123
115
|
if (
|