@likec4/core 1.19.1 → 1.20.0
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/dist/builder/index.cjs +881 -0
- package/dist/builder/index.d.cts +503 -0
- package/dist/builder/index.d.mts +5 -4
- package/dist/builder/index.d.ts +5 -4
- package/dist/builder/{index.js → index.mjs} +14 -4
- package/dist/compute-view/index.cjs +86 -0
- package/dist/compute-view/index.d.cts +23 -0
- package/dist/compute-view/index.d.mts +3 -2
- package/dist/compute-view/index.d.ts +3 -2
- package/dist/compute-view/{index.js → index.mjs} +6 -5
- package/dist/index.cjs +4320 -0
- package/dist/index.d.cts +270 -0
- package/dist/index.d.mts +130 -4
- package/dist/index.d.ts +130 -4
- package/dist/{index.js → index.mjs} +512 -7
- package/dist/model/index.cjs +40 -0
- package/dist/model/index.d.cts +259 -0
- package/dist/model/index.d.mts +8 -3
- package/dist/model/index.d.ts +8 -3
- package/dist/model/index.mjs +1 -0
- package/dist/shared/core.B2QYWlYe.cjs +435 -0
- package/dist/shared/core.B72vUaNB.d.cts +850 -0
- package/dist/shared/core.BGus30EM.cjs +8027 -0
- package/dist/shared/core.BRuJ3Wzf.d.mts +12 -0
- package/dist/shared/{core.BZ9Msq7w.d.mts → core.BdbFkE_p.d.cts} +14 -4
- package/dist/shared/core.BvdNMbcA.d.cts +12 -0
- package/dist/shared/{object_hash.CE_oF3I3.js → core.BxvDITEI.mjs} +26 -389
- package/dist/shared/core.ByNgh-mC.d.cts +127 -0
- package/dist/shared/core.C9DCnJh8.cjs +908 -0
- package/dist/shared/{index.DafVOuVd.js → core.CqCTxzMY.mjs} +1111 -572
- package/dist/shared/core.D74xkKkG.d.cts +148 -0
- package/dist/shared/{core.C05RDLhi.d.ts → core.D_Gc58Gt.d.ts} +23 -5
- package/dist/shared/core.Damrzla-.d.ts +12 -0
- package/dist/shared/{core.CmYMqgha.d.mts → core.Dfzrh1VA.d.mts} +23 -5
- package/dist/shared/core.DsoCu540.cjs +26 -0
- package/dist/shared/{view.CyZrG8BJ.js → core.DunIMHRf.mjs} +27 -1
- package/dist/shared/{core.BIfgabEw.d.ts → core.ZWiGANIJ.d.ts} +14 -4
- package/dist/shared/core.gv1bMwCC.cjs +39 -0
- package/dist/shared/core.jy6z2iRn.d.mts +729 -0
- package/dist/shared/core.m3rCQAv6.mjs +37 -0
- package/dist/types/index.cjs +107 -0
- package/dist/types/index.d.cts +72 -0
- package/dist/types/index.d.mts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/{index.js → index.mjs} +1 -1
- package/dist/utils/index.cjs +95 -0
- package/dist/utils/index.d.cts +356 -0
- package/dist/utils/index.d.mts +1 -64
- package/dist/utils/index.d.ts +1 -64
- package/dist/utils/index.mjs +56 -0
- package/package.json +52 -26
- package/src/builder/Builder.view-common.ts +21 -1
- package/src/compute-view/deployment-view/__test__/fixture.ts +31 -3
- package/src/compute-view/deployment-view/_types.ts +2 -1
- package/src/compute-view/deployment-view/compute.ts +4 -36
- package/src/compute-view/deployment-view/predicates/index.ts +8 -0
- package/src/compute-view/deployment-view/predicates/relation-direct.ts +125 -99
- package/src/compute-view/deployment-view/predicates/relation-in-out.ts +16 -8
- package/src/compute-view/deployment-view/predicates/relation-incoming.ts +18 -12
- package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +16 -9
- package/src/compute-view/deployment-view/predicates/relation-where.ts +18 -0
- package/src/compute-view/deployment-view/predicates/utils.ts +214 -0
- package/src/compute-view/element-view/__test__/fixture.ts +17 -4
- package/src/compute-view/element-view/compute.ts +3 -2
- package/src/compute-view/element-view/predicates.ts +520 -0
- package/src/compute-view/element-view/utils.ts +11 -7
- package/src/compute-view/index.ts +1 -0
- package/src/compute-view/relationships-view/_types.ts +16 -0
- package/src/compute-view/relationships-view/compute.ts +74 -0
- package/src/compute-view/relationships-view/layout.ts +357 -0
- package/src/compute-view/relationships-view/utils.ts +40 -0
- package/src/compute-view/utils/relationExpressionToPredicates.ts +11 -4
- package/src/index.ts +16 -0
- package/src/model/DeploymentElementModel.ts +18 -1
- package/src/model/ElementModel.ts +13 -0
- package/src/model/LikeC4Model.ts +2 -2
- package/src/model/RelationModel.ts +9 -0
- package/src/model/connection/deployment/DeploymentConnectionModel.ts +11 -1
- package/src/model/guards.ts +17 -1
- package/src/model/view/LikeC4ViewModel.ts +7 -1
- package/src/model/view/NodeModel.ts +5 -0
- package/src/types/deployments.ts +1 -0
- package/src/types/expression-v2.ts +12 -0
- package/src/types/index.ts +1 -0
- package/src/types/operators.ts +46 -6
- package/src/types/view.ts +2 -2
- package/src/utils/index.ts +0 -5
- package/src/utils/iterable/unique.ts +1 -7
- package/dist/model/index.js +0 -1
- package/dist/utils/index.js +0 -347
- /package/dist/shared/{chunk-RAAYCPUM.M-JWF7SS.js → core.M-JWF7SS.mjs} +0 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import dagre, { type EdgeConfig, type GraphLabel } from '@dagrejs/dagre'
|
|
2
|
+
import DefaultMap from 'mnemonist/default-map'
|
|
3
|
+
import { concat, filter, forEachObj, groupBy, map, mapToObj, pipe, prop, reduce, tap } from 'remeda'
|
|
4
|
+
import { invariant } from '../../errors'
|
|
5
|
+
import type { ElementModel } from '../../model/ElementModel'
|
|
6
|
+
import type { RelationshipModel } from '../../model/RelationModel'
|
|
7
|
+
import type { DiagramNode, DiagramView, Fqn, NodeId } from '../../types'
|
|
8
|
+
import { sortParentsFirst } from '../../utils'
|
|
9
|
+
import { toArray } from '../../utils/iterable'
|
|
10
|
+
import type { RelationshipsViewData } from './_types'
|
|
11
|
+
import { treeFromElements } from './utils'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* All constants related to the layout
|
|
15
|
+
*/
|
|
16
|
+
const Sizes = {
|
|
17
|
+
dagre: {
|
|
18
|
+
ranksep: 60,
|
|
19
|
+
nodesep: 35,
|
|
20
|
+
edgesep: 25,
|
|
21
|
+
} satisfies GraphLabel,
|
|
22
|
+
edgeLabel: {
|
|
23
|
+
width: 120,
|
|
24
|
+
height: 10,
|
|
25
|
+
minlen: 1,
|
|
26
|
+
weight: 1,
|
|
27
|
+
} satisfies EdgeConfig,
|
|
28
|
+
|
|
29
|
+
emptyNodeOffset: 120,
|
|
30
|
+
|
|
31
|
+
nodeWidth: 320,
|
|
32
|
+
nodeHeight: 180,
|
|
33
|
+
|
|
34
|
+
// Spacer between elements in a compound node
|
|
35
|
+
// 0 means no spacer
|
|
36
|
+
spacerHeight: 0,
|
|
37
|
+
|
|
38
|
+
compound: {
|
|
39
|
+
labelHeight: 1,
|
|
40
|
+
paddingTop: 60,
|
|
41
|
+
paddingBottom: 32,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
type NodeData = {
|
|
45
|
+
portId: string
|
|
46
|
+
element: ElementModel
|
|
47
|
+
isCompound: boolean
|
|
48
|
+
}
|
|
49
|
+
function createGraph() {
|
|
50
|
+
const g = new dagre.graphlib.Graph<NodeData>({
|
|
51
|
+
directed: true,
|
|
52
|
+
compound: true,
|
|
53
|
+
multigraph: true,
|
|
54
|
+
})
|
|
55
|
+
g.setGraph({
|
|
56
|
+
...Sizes.dagre,
|
|
57
|
+
rankdir: 'LR',
|
|
58
|
+
})
|
|
59
|
+
g.setDefaultEdgeLabel(() => ({ ...Sizes.edgeLabel }))
|
|
60
|
+
g.setDefaultNodeLabel(() => ({}))
|
|
61
|
+
|
|
62
|
+
// columns.reduce((prev, column) => {
|
|
63
|
+
// const c = graphColumn(column)
|
|
64
|
+
// g.setNode(c.id, {})
|
|
65
|
+
// g.setNode(c.anchor, { width: 40, height: 2 })
|
|
66
|
+
// g.setParent(c.anchor, c.id)
|
|
67
|
+
// if (prev) {
|
|
68
|
+
// g.setEdge(prev, c.anchor, {
|
|
69
|
+
// width: 0
|
|
70
|
+
// })
|
|
71
|
+
// }
|
|
72
|
+
// return c.anchor
|
|
73
|
+
// }, null as string | null)
|
|
74
|
+
|
|
75
|
+
return g
|
|
76
|
+
}
|
|
77
|
+
type G = ReturnType<typeof createGraph>
|
|
78
|
+
|
|
79
|
+
const PortSuffix = '-port'
|
|
80
|
+
|
|
81
|
+
function createNodes(
|
|
82
|
+
prefix: string,
|
|
83
|
+
elements: ReadonlySet<ElementModel>,
|
|
84
|
+
g: G,
|
|
85
|
+
) {
|
|
86
|
+
const graphNodes = new DefaultMap<Fqn, { id: string; portId: string }>(key => ({
|
|
87
|
+
id: `${prefix}-${key}`,
|
|
88
|
+
portId: `${prefix}-${key}`,
|
|
89
|
+
}))
|
|
90
|
+
const tree = treeFromElements(elements)
|
|
91
|
+
for (const element of tree.sorted) {
|
|
92
|
+
const isCompound = tree.children(element).length > 0
|
|
93
|
+
const fqn = element.id
|
|
94
|
+
const id = `${prefix}-${fqn}`
|
|
95
|
+
const portId = isCompound ? `${id}${PortSuffix}` : id
|
|
96
|
+
graphNodes.set(fqn, {
|
|
97
|
+
id,
|
|
98
|
+
portId,
|
|
99
|
+
})
|
|
100
|
+
g.setNode(id, {
|
|
101
|
+
element,
|
|
102
|
+
isCompound,
|
|
103
|
+
portId,
|
|
104
|
+
width: Sizes.nodeWidth,
|
|
105
|
+
height: Sizes.nodeHeight,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (isCompound) {
|
|
109
|
+
g.setNode(portId, {
|
|
110
|
+
element,
|
|
111
|
+
portId,
|
|
112
|
+
isCompound,
|
|
113
|
+
width: Sizes.nodeWidth - Sizes.dagre.ranksep,
|
|
114
|
+
height: Sizes.compound.labelHeight,
|
|
115
|
+
})
|
|
116
|
+
g.setParent(portId, id)
|
|
117
|
+
// g.node(id).padding = 60
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const parent = tree.parent(element)
|
|
121
|
+
if (parent) {
|
|
122
|
+
g.setParent(id, `${prefix}-${parent.id}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...tree,
|
|
127
|
+
byId: (id: string) => {
|
|
128
|
+
const element = tree.byId(id)
|
|
129
|
+
const graph = graphNodes.get(element.id)
|
|
130
|
+
return {
|
|
131
|
+
element,
|
|
132
|
+
graph,
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
graphNodes,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply dagre layout to the graph
|
|
141
|
+
* And return a function to get node bounds for xyflow
|
|
142
|
+
*/
|
|
143
|
+
function applyDagreLayout(g: G) {
|
|
144
|
+
dagre.layout(g, {
|
|
145
|
+
// disableOptimalOrderHeuristic: true,
|
|
146
|
+
})
|
|
147
|
+
return (nodeId: string) => {
|
|
148
|
+
const node = g.node(nodeId)
|
|
149
|
+
const { x, y, width, height } = node
|
|
150
|
+
return {
|
|
151
|
+
position: {
|
|
152
|
+
x: x - Math.round(width / 2),
|
|
153
|
+
y: y - Math.round(height / 2),
|
|
154
|
+
},
|
|
155
|
+
width,
|
|
156
|
+
height,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function layoutRelationshipsView(data: RelationshipsViewData): Pick<DiagramView, 'nodes' | 'edges' | 'bounds'> {
|
|
162
|
+
const g = createGraph()
|
|
163
|
+
|
|
164
|
+
const incomers = createNodes('in', data.incomers, g),
|
|
165
|
+
subjects = createNodes('subject', data.subjects, g),
|
|
166
|
+
outgoers = createNodes('out', data.outgoers, g)
|
|
167
|
+
|
|
168
|
+
const edges = [] as Array<{
|
|
169
|
+
name: string
|
|
170
|
+
source: string // node id
|
|
171
|
+
target: string // node id
|
|
172
|
+
relations: RelationshipModel[]
|
|
173
|
+
}>
|
|
174
|
+
|
|
175
|
+
pipe(
|
|
176
|
+
concat(
|
|
177
|
+
pipe(
|
|
178
|
+
toArray(data.incoming),
|
|
179
|
+
map(r => ({
|
|
180
|
+
id: r.source.id,
|
|
181
|
+
source: incomers.byId(r.source.id).graph,
|
|
182
|
+
target: subjects.byId(r.target.id).graph,
|
|
183
|
+
relation: r,
|
|
184
|
+
})),
|
|
185
|
+
// Sort by source
|
|
186
|
+
sortParentsFirst,
|
|
187
|
+
),
|
|
188
|
+
pipe(
|
|
189
|
+
toArray(data.outgoing),
|
|
190
|
+
map(r => ({
|
|
191
|
+
id: r.target.id,
|
|
192
|
+
source: subjects.byId(r.source.id).graph,
|
|
193
|
+
target: outgoers.byId(r.target.id).graph,
|
|
194
|
+
relation: r,
|
|
195
|
+
})),
|
|
196
|
+
// Sort by target
|
|
197
|
+
sortParentsFirst,
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
map(r => ({
|
|
201
|
+
...r,
|
|
202
|
+
expr: `${r.source.id}->${r.target.id}`,
|
|
203
|
+
})),
|
|
204
|
+
// Group if same source and target
|
|
205
|
+
groupBy(prop('expr')),
|
|
206
|
+
forEachObj((grouped) => {
|
|
207
|
+
const source = grouped[0].source
|
|
208
|
+
const target = grouped[0].target
|
|
209
|
+
const name = grouped[0].expr
|
|
210
|
+
|
|
211
|
+
g.setEdge(source.portId, target.portId, {
|
|
212
|
+
...Sizes.edgeLabel,
|
|
213
|
+
}, name)
|
|
214
|
+
edges.push({
|
|
215
|
+
name,
|
|
216
|
+
source: source.id,
|
|
217
|
+
target: target.id,
|
|
218
|
+
relations: map(grouped, prop('relation')),
|
|
219
|
+
})
|
|
220
|
+
}),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// Grow nodes with more than 2 ports
|
|
224
|
+
for (const subjectNode of subjects.graphNodes.values()) {
|
|
225
|
+
const nodeId = subjectNode.id
|
|
226
|
+
const node = g.node(nodeId)
|
|
227
|
+
if (node.isCompound) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
const edgeCount = Math.max(g.inEdges(nodeId)?.length ?? 0, g.outEdges(nodeId)?.length ?? 0)
|
|
231
|
+
if (edgeCount > 2) {
|
|
232
|
+
node.height = node.height + (edgeCount - 3) * 14
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const dagreBounds = applyDagreLayout(g)
|
|
237
|
+
|
|
238
|
+
const nodeIds = [
|
|
239
|
+
...incomers.graphNodes.values(),
|
|
240
|
+
...subjects.graphNodes.values(),
|
|
241
|
+
...outgoers.graphNodes.values(),
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
// Calculate bounds for all nodes except compounds
|
|
245
|
+
// We shrink compounds to fit their children
|
|
246
|
+
const _calculatedNodeBounds = pipe(
|
|
247
|
+
nodeIds,
|
|
248
|
+
// Compound nodes have different portId
|
|
249
|
+
filter(n => n.id === n.portId),
|
|
250
|
+
mapToObj(n => [n.id, dagreBounds(n.id)]),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
function nodeBounds(nodeId: string): ReturnType<typeof dagreBounds> {
|
|
254
|
+
return _calculatedNodeBounds[nodeId] ??= pipe(
|
|
255
|
+
g.children(nodeId) as string[] | undefined ?? [],
|
|
256
|
+
filter(id => !id.endsWith(PortSuffix)),
|
|
257
|
+
map(id => nodeBounds(id)),
|
|
258
|
+
tap(bounds => {
|
|
259
|
+
invariant(bounds.length > 0, `Node ${nodeId} has no nested nodes`)
|
|
260
|
+
}),
|
|
261
|
+
reduce((acc, bounds) => {
|
|
262
|
+
return {
|
|
263
|
+
minY: Math.min(acc.minY, bounds.position.y),
|
|
264
|
+
maxY: Math.max(acc.maxY, bounds.position.y + bounds.height),
|
|
265
|
+
}
|
|
266
|
+
}, { minY: Infinity, maxY: -Infinity }),
|
|
267
|
+
({ minY, maxY }) => {
|
|
268
|
+
const {
|
|
269
|
+
position: { x },
|
|
270
|
+
width,
|
|
271
|
+
} = dagreBounds(nodeId)
|
|
272
|
+
minY = minY - Sizes.compound.paddingTop
|
|
273
|
+
maxY = maxY + Sizes.compound.paddingBottom
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
position: {
|
|
277
|
+
x,
|
|
278
|
+
y: minY,
|
|
279
|
+
},
|
|
280
|
+
width,
|
|
281
|
+
height: maxY - minY,
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function nodeLevel(nodeId: string): number {
|
|
288
|
+
const parent = g.parent(nodeId)
|
|
289
|
+
if (parent) {
|
|
290
|
+
return nodeLevel(parent) + 1
|
|
291
|
+
}
|
|
292
|
+
return 0
|
|
293
|
+
}
|
|
294
|
+
function nodeDepth(nodeId: string): number {
|
|
295
|
+
const children = g.children(nodeId) as string[] | undefined ?? []
|
|
296
|
+
if (children.length === 0) {
|
|
297
|
+
return 0
|
|
298
|
+
}
|
|
299
|
+
return 1 + Math.max(...children.map(nodeDepth))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const nodes = nodeIds.map(({ id }): DiagramNode => {
|
|
303
|
+
const { element } = g.node(id)
|
|
304
|
+
let { position, width, height } = nodeBounds(id)
|
|
305
|
+
const parentId = g.parent(id)
|
|
306
|
+
// if (parentId) {
|
|
307
|
+
// const parentPos = nodeBounds(parentId).position
|
|
308
|
+
// position = {
|
|
309
|
+
// x: position.x - parentPos.x,
|
|
310
|
+
// y: position.y - parentPos.y,
|
|
311
|
+
// }
|
|
312
|
+
// }
|
|
313
|
+
const children = (g.children(id) as NodeId[] | undefined ?? []).filter(c => !c.endsWith(PortSuffix))
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
id: id as NodeId,
|
|
317
|
+
parent: parentId as NodeId ?? null,
|
|
318
|
+
position: [position.x, position.y],
|
|
319
|
+
title: element.title,
|
|
320
|
+
description: element.description,
|
|
321
|
+
technology: element.technology,
|
|
322
|
+
tags: null,
|
|
323
|
+
links: null,
|
|
324
|
+
color: element.color,
|
|
325
|
+
shape: element.shape,
|
|
326
|
+
modelRef: element.id,
|
|
327
|
+
kind: element.kind,
|
|
328
|
+
level: nodeLevel(id),
|
|
329
|
+
labelBBox: {
|
|
330
|
+
x: position.x,
|
|
331
|
+
y: position.y,
|
|
332
|
+
width: width,
|
|
333
|
+
height: height,
|
|
334
|
+
},
|
|
335
|
+
style: {
|
|
336
|
+
...element.$element.style,
|
|
337
|
+
},
|
|
338
|
+
inEdges: [],
|
|
339
|
+
outEdges: [],
|
|
340
|
+
...(children.length > 0 && { depth: nodeDepth(id) }),
|
|
341
|
+
children,
|
|
342
|
+
width,
|
|
343
|
+
height,
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
bounds: {
|
|
349
|
+
x: 0,
|
|
350
|
+
y: 0,
|
|
351
|
+
width: g.graph().width ?? 100,
|
|
352
|
+
height: g.graph().height ?? 100,
|
|
353
|
+
},
|
|
354
|
+
nodes,
|
|
355
|
+
edges: [],
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import DefaultMap from 'mnemonist/default-map'
|
|
2
|
+
import { nonNullable } from '../../errors'
|
|
3
|
+
import type { ElementModel } from '../../model/ElementModel'
|
|
4
|
+
import type { Fqn } from '../../types'
|
|
5
|
+
import { isAncestor, isDescendantOf, sortParentsFirst } from '../../utils/fqn'
|
|
6
|
+
|
|
7
|
+
export function treeFromElements(elements: Iterable<ElementModel>) {
|
|
8
|
+
const sorted = sortParentsFirst([...elements]) as ReadonlyArray<ElementModel>
|
|
9
|
+
const root = new Set(sorted)
|
|
10
|
+
const map = new Map(sorted.map(e => [e.id, e]))
|
|
11
|
+
const parents = new DefaultMap<ElementModel, ElementModel | null>(() => null)
|
|
12
|
+
const children = sorted.reduce((acc, parent, index, all) => {
|
|
13
|
+
acc.set(
|
|
14
|
+
parent,
|
|
15
|
+
all
|
|
16
|
+
.slice(index + 1)
|
|
17
|
+
.filter(isDescendantOf(parent))
|
|
18
|
+
.map(e => {
|
|
19
|
+
root.delete(e)
|
|
20
|
+
return e
|
|
21
|
+
})
|
|
22
|
+
.reduce((acc, el) => {
|
|
23
|
+
if (!acc.some(isAncestor(el))) {
|
|
24
|
+
acc.push(el)
|
|
25
|
+
parents.set(el, parent)
|
|
26
|
+
}
|
|
27
|
+
return acc
|
|
28
|
+
}, [] as ElementModel[]),
|
|
29
|
+
)
|
|
30
|
+
return acc
|
|
31
|
+
}, new DefaultMap<ElementModel, ElementModel[]>(() => []))
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
sorted,
|
|
35
|
+
byId: (id: string) => nonNullable(map.get(id as Fqn), `Element not found by id: ${id}`),
|
|
36
|
+
root: root as ReadonlySet<ElementModel>,
|
|
37
|
+
parent: (el: ElementModel) => parents.get(el),
|
|
38
|
+
children: (el: ElementModel): ReadonlyArray<ElementModel> => children.get(el),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { pick } from 'remeda'
|
|
1
2
|
import { nonexhaustive } from '../../errors'
|
|
2
3
|
import type { Element } from '../../types/element'
|
|
3
4
|
import {
|
|
5
|
+
type RelationExpression,
|
|
6
|
+
type RelationWhereExpr,
|
|
4
7
|
isIncoming,
|
|
5
8
|
isInOut,
|
|
6
9
|
isOutgoing,
|
|
7
10
|
isRelation,
|
|
8
11
|
isRelationWhere,
|
|
9
|
-
type RelationExpression,
|
|
10
|
-
type RelationWhereExpr
|
|
11
12
|
} from '../../types/expression'
|
|
12
13
|
import { whereOperatorAsPredicate } from '../../types/operators'
|
|
13
14
|
import type { ModelRelation } from '../../types/relation'
|
|
@@ -21,13 +22,19 @@ export type FilterableEdge = Pick<ModelRelation, 'kind' | 'tags'> & {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function relationExpressionToPredicates<T extends FilterableEdge>(
|
|
24
|
-
expr: RelationExpression | RelationWhereExpr
|
|
25
|
+
expr: RelationExpression | RelationWhereExpr,
|
|
25
26
|
): Predicate<T> {
|
|
26
27
|
switch (true) {
|
|
27
28
|
case isRelationWhere(expr): {
|
|
28
29
|
const predicate = relationExpressionToPredicates(expr.where.expr)
|
|
29
30
|
const where = whereOperatorAsPredicate(expr.where.condition)
|
|
30
|
-
return e =>
|
|
31
|
+
return e =>
|
|
32
|
+
predicate(e) && where({
|
|
33
|
+
source: { tags: e.source.tags, kind: e.source.kind },
|
|
34
|
+
target: { tags: e.target.tags, kind: e.target.kind },
|
|
35
|
+
...(e.tags && { tags: e.tags }),
|
|
36
|
+
...(e.kind && { kind: e.kind }),
|
|
37
|
+
})
|
|
31
38
|
}
|
|
32
39
|
case isRelation(expr): {
|
|
33
40
|
const isSource = elementExprToPredicate(expr.source)
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import DefaultMap from 'mnemonist/default-map'
|
|
2
|
+
import LinkedList from 'mnemonist/linked-list'
|
|
3
|
+
import Queue from 'mnemonist/queue'
|
|
4
|
+
import Stack from 'mnemonist/stack'
|
|
5
|
+
// import dagre from '@dagrejs/dagre'
|
|
6
|
+
|
|
7
|
+
export { DefaultMap, LinkedList, Queue, Stack }
|
|
8
|
+
|
|
1
9
|
export {
|
|
2
10
|
computeColorValues,
|
|
3
11
|
defaultTheme,
|
|
@@ -24,3 +32,11 @@ export * from './types'
|
|
|
24
32
|
export * from './utils'
|
|
25
33
|
|
|
26
34
|
export * as Expr from './types/expression'
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
computeRelationshipsView,
|
|
38
|
+
computeView,
|
|
39
|
+
computeViews,
|
|
40
|
+
unsafeComputeView,
|
|
41
|
+
} from './compute-view'
|
|
42
|
+
export type { ComputeViewResult } from './compute-view'
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ElementShape as C4ElementShape,
|
|
12
12
|
type IteratorLike,
|
|
13
13
|
type Link,
|
|
14
|
+
type RelationshipKind,
|
|
14
15
|
type Tag,
|
|
15
16
|
type Tag as C4Tag,
|
|
16
17
|
type ThemeColor,
|
|
@@ -62,6 +63,10 @@ abstract class AbstractDeploymentElementModel<M extends AnyAux = AnyAux> {
|
|
|
62
63
|
return this.$node.tags ?? []
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
get kind(): DeploymentNodeKind | string | null {
|
|
67
|
+
return this.$node.kind ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
get description(): string | null {
|
|
66
71
|
return this.$node.description ?? null
|
|
67
72
|
}
|
|
@@ -223,7 +228,7 @@ export class DeploymentNodeModel<M extends AnyAux = AnyAux> extends AbstractDepl
|
|
|
223
228
|
return this.$model.parent(this)
|
|
224
229
|
}
|
|
225
230
|
|
|
226
|
-
get kind(): DeploymentNodeKind {
|
|
231
|
+
override get kind(): DeploymentNodeKind {
|
|
227
232
|
return this.$node.kind
|
|
228
233
|
}
|
|
229
234
|
|
|
@@ -367,6 +372,10 @@ export class DeployedInstanceModel<M extends AnyAux = AnyAux> extends AbstractDe
|
|
|
367
372
|
return this.$instance.tags ?? []
|
|
368
373
|
}
|
|
369
374
|
|
|
375
|
+
override get kind(): string | null {
|
|
376
|
+
return this.$instance.kind ?? null
|
|
377
|
+
}
|
|
378
|
+
|
|
370
379
|
override get description(): string | null {
|
|
371
380
|
return this.$instance.description ?? this.element.description
|
|
372
381
|
}
|
|
@@ -514,6 +523,10 @@ export class DeploymentRelationModel<M extends AnyAux = AnyAux> {
|
|
|
514
523
|
return this.$relationship.tags ?? []
|
|
515
524
|
}
|
|
516
525
|
|
|
526
|
+
get kind(): RelationshipKind | null {
|
|
527
|
+
return this.$relationship.kind ?? null
|
|
528
|
+
}
|
|
529
|
+
|
|
517
530
|
get navigateTo(): LikeC4ViewModel<M> | null {
|
|
518
531
|
return this.$relationship.navigateTo ? this.$model.$model.view(this.$relationship.navigateTo) : null
|
|
519
532
|
}
|
|
@@ -530,6 +543,10 @@ export class DeploymentRelationModel<M extends AnyAux = AnyAux> {
|
|
|
530
543
|
}
|
|
531
544
|
return
|
|
532
545
|
}
|
|
546
|
+
|
|
547
|
+
public isDeploymentRelation(): this is DeploymentRelationModel<M> {
|
|
548
|
+
return true
|
|
549
|
+
}
|
|
533
550
|
}
|
|
534
551
|
|
|
535
552
|
export class RelationshipsAccum<M extends AnyAux> {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type Element as C4Element,
|
|
4
4
|
type ElementKind as C4ElementKind,
|
|
5
5
|
type ElementShape as C4ElementShape,
|
|
6
|
+
type IconUrl,
|
|
6
7
|
type IteratorLike,
|
|
7
8
|
type Link,
|
|
8
9
|
type Tag as C4Tag,
|
|
@@ -47,6 +48,10 @@ export class ElementModel<M extends AnyAux = AnyAux> {
|
|
|
47
48
|
return this.$element.color as ThemeColor ?? DefaultThemeColor
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
get icon(): IconUrl | null {
|
|
52
|
+
return this.$element.icon ?? null
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
get tags(): ReadonlyArray<C4Tag> {
|
|
51
56
|
return this.$element.tags ?? []
|
|
52
57
|
}
|
|
@@ -71,6 +76,10 @@ export class ElementModel<M extends AnyAux = AnyAux> {
|
|
|
71
76
|
return this.scopedViews().next().value ?? null
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
get isRoot(): boolean {
|
|
80
|
+
return this.parent === null
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
public isAncestorOf(another: ElementModel<M>): boolean {
|
|
75
84
|
return isAncestor(this, another)
|
|
76
85
|
}
|
|
@@ -219,3 +228,7 @@ export class ElementModel<M extends AnyAux = AnyAux> {
|
|
|
219
228
|
return this.$model.deployment.instancesOf(this)
|
|
220
229
|
}
|
|
221
230
|
}
|
|
231
|
+
|
|
232
|
+
export function isElementModel<M extends AnyAux = AnyAux>(element: any): element is ElementModel<M> {
|
|
233
|
+
return element instanceof ElementModel
|
|
234
|
+
}
|
package/src/model/LikeC4Model.ts
CHANGED
|
@@ -239,8 +239,8 @@ export class LikeC4Model<M extends AnyAux = LikeC4Model.Any> {
|
|
|
239
239
|
public view(viewId: M['View']): LikeC4ViewModel<M> {
|
|
240
240
|
return nonNullable(this.#views.get(viewId as any), `View ${viewId} not found`)
|
|
241
241
|
}
|
|
242
|
-
public findView(viewId: M['View']): LikeC4ViewModel<M> {
|
|
243
|
-
return
|
|
242
|
+
public findView(viewId: M['View']): LikeC4ViewModel<M> | null {
|
|
243
|
+
return this.#views.get(viewId as M['ViewId']) ?? null
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
/**
|
|
@@ -3,6 +3,7 @@ import type { IteratorLike } from '../types'
|
|
|
3
3
|
import type { Link, Tag } from '../types/element'
|
|
4
4
|
import type { ModelRelation } from '../types/relation'
|
|
5
5
|
import { commonAncestor } from '../utils/fqn'
|
|
6
|
+
import type { DeploymentRelationModel } from './DeploymentElementModel'
|
|
6
7
|
import type { ElementModel } from './ElementModel'
|
|
7
8
|
import type { LikeC4Model } from './LikeC4Model'
|
|
8
9
|
import type { AnyAux } from './types'
|
|
@@ -67,6 +68,10 @@ export class RelationshipModel<M extends AnyAux = AnyAux> {
|
|
|
67
68
|
return this.$relationship.tags ?? []
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
get kind(): string | null {
|
|
72
|
+
return this.$relationship.kind ?? null
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
get links(): ReadonlyArray<Link> {
|
|
71
76
|
return this.$relationship.links ?? []
|
|
72
77
|
}
|
|
@@ -82,4 +87,8 @@ export class RelationshipModel<M extends AnyAux = AnyAux> {
|
|
|
82
87
|
}
|
|
83
88
|
return
|
|
84
89
|
}
|
|
90
|
+
|
|
91
|
+
public isDeploymentRelation(): this is DeploymentRelationModel<M> {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
85
94
|
}
|
|
@@ -110,11 +110,21 @@ export class DeploymentConnectionModel<M extends AnyAux = AnyAux>
|
|
|
110
110
|
yield* this.relations.deployment
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Merge with another connections, if it has the same source and target.
|
|
115
|
+
* Returns new connection with union of relationships.
|
|
116
|
+
*/
|
|
117
|
+
public mergeWith(others: DeploymentConnectionModel<M>[]): DeploymentConnectionModel<M>
|
|
113
118
|
/**
|
|
114
119
|
* Merge with another connection, if it has the same source and target.
|
|
115
120
|
* Returns new connection with union of relationships.
|
|
116
121
|
*/
|
|
117
|
-
public mergeWith(other: DeploymentConnectionModel<M>)
|
|
122
|
+
public mergeWith(other: DeploymentConnectionModel<M>): DeploymentConnectionModel<M>
|
|
123
|
+
public mergeWith(other: DeploymentConnectionModel<M> | DeploymentConnectionModel<M>[]): DeploymentConnectionModel<M> {
|
|
124
|
+
if (Array.isArray(other)) {
|
|
125
|
+
return other.reduce((acc, o) => acc.mergeWith(o), this)
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
invariant(this.source.id === other.source.id, 'Cannot merge connections with different sources')
|
|
119
129
|
invariant(this.target.id === other.target.id, 'Cannot merge connections with different targets')
|
|
120
130
|
return new DeploymentConnectionModel(
|
package/src/model/guards.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DeploymentElementModel,
|
|
3
|
+
DeploymentRelationEndpoint,
|
|
4
|
+
NestedElementOfDeployedInstanceModel,
|
|
5
|
+
} from './DeploymentElementModel'
|
|
6
|
+
import { DeployedInstanceModel, DeploymentNodeModel } from './DeploymentElementModel'
|
|
7
|
+
import type { AnyAux } from './types'
|
|
2
8
|
|
|
3
9
|
export function isDeploymentNode(model: DeploymentElementModel): model is DeploymentNodeModel {
|
|
4
10
|
return model.isDeploymentNode()
|
|
@@ -7,3 +13,13 @@ export function isDeploymentNode(model: DeploymentElementModel): model is Deploy
|
|
|
7
13
|
export function isDeployedInstance(model: DeploymentElementModel): model is DeployedInstanceModel {
|
|
8
14
|
return model.isInstance()
|
|
9
15
|
}
|
|
16
|
+
|
|
17
|
+
export function isNestedElementOfDeployedInstanceModel<M extends AnyAux = AnyAux>(
|
|
18
|
+
model: DeploymentRelationEndpoint<M>,
|
|
19
|
+
): model is NestedElementOfDeployedInstanceModel<M> {
|
|
20
|
+
return !model.isInstance() && !model.isDeploymentNode()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isDeploymentElementModel(x: any): x is DeploymentElementModel {
|
|
24
|
+
return x instanceof DeploymentNodeModel || x instanceof DeployedInstanceModel
|
|
25
|
+
}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
isElementView,
|
|
17
17
|
isScopedElementView,
|
|
18
18
|
} from '../../types/view'
|
|
19
|
-
import { compareByFqnHierarchically, getOrCreate } from '../../utils'
|
|
19
|
+
import { compareByFqnHierarchically, getOrCreate, ifind } from '../../utils'
|
|
20
20
|
import type { ElementModel } from '../ElementModel'
|
|
21
21
|
import type { LikeC4Model } from '../LikeC4Model'
|
|
22
22
|
import { type AnyAux, getId } from '../types'
|
|
@@ -138,6 +138,12 @@ export class LikeC4ViewModel<M extends AnyAux, V extends ComputedView | DiagramV
|
|
|
138
138
|
public findNode(node: M['NodeOrId']): NodeModel<M, V> | null {
|
|
139
139
|
return this.#nodes.get(getId(node) as C4NodeId) ?? null
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
public findNodeWithElement(fqn: M['Element']): NodeModel.WithElement<M, V> | null {
|
|
143
|
+
const nd = ifind(this.#nodes.values(), node => node.element?.id === fqn) ?? null
|
|
144
|
+
return nd && nd.hasElement() ? nd : null
|
|
145
|
+
}
|
|
146
|
+
|
|
141
147
|
/**
|
|
142
148
|
* Iterate over all nodes.
|
|
143
149
|
*/
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type DeploymentNodeKind,
|
|
5
5
|
type DiagramView,
|
|
6
6
|
type ElementShape as C4ElementShape,
|
|
7
|
+
type IconUrl,
|
|
7
8
|
type IteratorLike,
|
|
8
9
|
type Link,
|
|
9
10
|
type NodeId,
|
|
@@ -97,6 +98,10 @@ export class NodeModel<M extends AnyAux, V extends ComputedView | DiagramView =
|
|
|
97
98
|
return this.$node.color
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
get icon(): IconUrl | null {
|
|
102
|
+
return this.$node.icon ?? null
|
|
103
|
+
}
|
|
104
|
+
|
|
100
105
|
get tags(): ReadonlyArray<C4Tag> {
|
|
101
106
|
return this.$node.tags ?? []
|
|
102
107
|
}
|
package/src/types/deployments.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface DeployedInstance {
|
|
|
42
42
|
readonly title?: string
|
|
43
43
|
readonly description?: string | null
|
|
44
44
|
readonly technology?: string | null
|
|
45
|
+
readonly kind?: string | null // TODO: ensure the property is set
|
|
45
46
|
readonly tags?: NonEmptyArray<Tag> | null
|
|
46
47
|
readonly links?: NonEmptyArray<Link> | null
|
|
47
48
|
readonly style?: DeploymentElementStyle
|