@api-client/core 0.18.16 → 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/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 +12 -12
- package/package.json +1 -1
- package/src/modeling/DataDomain.ts +24 -3
- package/src/modeling/DomainSerialization.ts +442 -56
- package/src/modeling/types.ts +73 -2
- package/tests/unit/modeling/data_domain_serialization.spec.ts +504 -0
|
@@ -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
|
}
|
package/src/modeling/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
DomainAssociationKind,
|
|
14
14
|
DataDomainKind,
|
|
15
15
|
} from '../models/kinds.js'
|
|
16
|
+
import type { DataDomain } from './DataDomain.js'
|
|
16
17
|
|
|
17
18
|
export interface DataDomainRemoveOptions {
|
|
18
19
|
/**
|
|
@@ -55,7 +56,7 @@ export interface DomainGraphEdge {
|
|
|
55
56
|
/**
|
|
56
57
|
* The type of the edge.
|
|
57
58
|
* - `association` The edge is to an association object.
|
|
58
|
-
* - When coming **from** an
|
|
59
|
+
* - When coming **from** an entity (the `v` property), that entity owns the association.
|
|
59
60
|
* - When coming **to** an entity (the `w` property), that entity is the target of the association.
|
|
60
61
|
* An association can have multiple targets.
|
|
61
62
|
* - `property` The edge is to a property object. Can only be created between an entity and a property.
|
|
@@ -684,7 +685,7 @@ export interface MatchUserPropertyAccessRule extends BaseAccessRule {
|
|
|
684
685
|
/**
|
|
685
686
|
* The action is allowed if the authenticated user's email domain matches a specific domain.
|
|
686
687
|
* This is used to restrict access based on the user's email address.
|
|
687
|
-
* For example, only users with an email address from "
|
|
688
|
+
* For example, only users with an email address from "my-company.com" can access certain resources.
|
|
688
689
|
*/
|
|
689
690
|
export interface MatchEmailDomainAccessRule extends BaseAccessRule {
|
|
690
691
|
type: 'matchEmailDomain'
|
|
@@ -822,3 +823,73 @@ export interface DomainImpactItem {
|
|
|
822
823
|
*/
|
|
823
824
|
parent?: string
|
|
824
825
|
}
|
|
826
|
+
|
|
827
|
+
export interface DeserializeOptions {
|
|
828
|
+
/**
|
|
829
|
+
* The mode to use for deserialization.
|
|
830
|
+
*/
|
|
831
|
+
mode?: DeserializationMode
|
|
832
|
+
/**
|
|
833
|
+
* The serialized graph to deserialize.
|
|
834
|
+
* This is the JSON representation of the graph.
|
|
835
|
+
*/
|
|
836
|
+
json?: SerializedGraph
|
|
837
|
+
/**
|
|
838
|
+
* The list of foreign domains that this domain depends on.
|
|
839
|
+
*/
|
|
840
|
+
dependencies?: DataDomain[]
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Describes the mode for deserializing a domain graph.
|
|
845
|
+
*/
|
|
846
|
+
export type DeserializationMode = 'strict' | 'lenient'
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Describes an issue found during deserialization.
|
|
850
|
+
*/
|
|
851
|
+
export interface DeserializationIssue {
|
|
852
|
+
/**
|
|
853
|
+
* The type of issue encountered.
|
|
854
|
+
*/
|
|
855
|
+
type: 'missing_node' | 'missing_edge' | 'invalid_parent' | 'missing_dependency' | 'malformed_entry' | 'unknown_kind'
|
|
856
|
+
/**
|
|
857
|
+
* The severity of the issue.
|
|
858
|
+
*/
|
|
859
|
+
severity: 'error' | 'warning' | 'info'
|
|
860
|
+
/**
|
|
861
|
+
* A human-readable description of the issue.
|
|
862
|
+
*/
|
|
863
|
+
message: string
|
|
864
|
+
/**
|
|
865
|
+
* The key of the affected node, edge, or entity if applicable.
|
|
866
|
+
*/
|
|
867
|
+
affectedKey?: string
|
|
868
|
+
/**
|
|
869
|
+
* Additional context about the issue.
|
|
870
|
+
*/
|
|
871
|
+
context?: Record<string, unknown>
|
|
872
|
+
/**
|
|
873
|
+
* The action taken to handle this issue in lenient mode.
|
|
874
|
+
*/
|
|
875
|
+
resolution?: string
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The result of a deserialization operation.
|
|
880
|
+
*/
|
|
881
|
+
export interface DeserializationResult {
|
|
882
|
+
/**
|
|
883
|
+
* The deserialized graph.
|
|
884
|
+
*/
|
|
885
|
+
graph: DataDomainGraph
|
|
886
|
+
/**
|
|
887
|
+
* Issues encountered during deserialization.
|
|
888
|
+
*/
|
|
889
|
+
issues: DeserializationIssue[]
|
|
890
|
+
/**
|
|
891
|
+
* Whether the deserialization was successful.
|
|
892
|
+
* This is set to true when a critical failures occurred.
|
|
893
|
+
*/
|
|
894
|
+
success: boolean
|
|
895
|
+
}
|