@likec4/core 1.19.2 → 1.20.1
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 +4 -3
- package/dist/builder/index.d.ts +4 -3
- 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 +3 -3
- package/dist/model/index.d.ts +3 -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.CqXs9Frh.d.ts → core.BdbFkE_p.d.cts} +10 -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.KOeaSlL8.js → core.CqCTxzMY.mjs} +787 -372
- package/dist/shared/core.D74xkKkG.d.cts +148 -0
- package/dist/shared/{core.DyJ41fBO.d.ts → core.D_Gc58Gt.d.ts} +12 -4
- package/dist/shared/core.Damrzla-.d.ts +12 -0
- package/dist/shared/{core.BL1oTk7-.d.mts → core.Dfzrh1VA.d.mts} +12 -4
- package/dist/shared/core.DsoCu540.cjs +26 -0
- package/dist/shared/{view.DrmaOfjr.js → core.DunIMHRf.mjs} +24 -1
- package/dist/shared/{core.2I-l5UtM.d.mts → core.ZWiGANIJ.d.ts} +10 -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 +51 -25
- package/src/builder/Builder.view-common.ts +20 -1
- package/src/compute-view/deployment-view/predicates/relation-direct.ts +26 -29
- package/src/compute-view/deployment-view/predicates/relation-incoming.ts +1 -1
- package/src/compute-view/deployment-view/predicates/utils.ts +47 -16
- 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 +4 -0
- package/src/model/ElementModel.ts +13 -0
- package/src/model/LikeC4Model.ts +2 -2
- package/src/model/RelationModel.ts +5 -0
- 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/operators.ts +42 -2
- 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'
|
|
@@ -543,6 +543,10 @@ export class DeploymentRelationModel<M extends AnyAux = AnyAux> {
|
|
|
543
543
|
}
|
|
544
544
|
return
|
|
545
545
|
}
|
|
546
|
+
|
|
547
|
+
public isDeploymentRelation(): this is DeploymentRelationModel<M> {
|
|
548
|
+
return true
|
|
549
|
+
}
|
|
546
550
|
}
|
|
547
551
|
|
|
548
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'
|
|
@@ -86,4 +87,8 @@ export class RelationshipModel<M extends AnyAux = AnyAux> {
|
|
|
86
87
|
}
|
|
87
88
|
return
|
|
88
89
|
}
|
|
90
|
+
|
|
91
|
+
public isDeploymentRelation(): this is DeploymentRelationModel<M> {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
89
94
|
}
|
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/operators.ts
CHANGED
|
@@ -16,6 +16,7 @@ type AllNever = {
|
|
|
16
16
|
or?: never
|
|
17
17
|
tag?: never
|
|
18
18
|
kind?: never
|
|
19
|
+
participant?: never
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export type TagEqual<Tag> = Omit<AllNever, 'tag'> & {
|
|
@@ -32,6 +33,17 @@ export const isKindEqual = <Kind>(operator: WhereOperator<any, Kind>): operator
|
|
|
32
33
|
return 'kind' in operator
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export type Participant = 'source' | 'target'
|
|
37
|
+
export type ParticipantOperator<Tag, Kind> = Omit<AllNever, 'participant'> & {
|
|
38
|
+
participant: Participant
|
|
39
|
+
operator: KindEqual<Kind> | TagEqual<Tag>
|
|
40
|
+
}
|
|
41
|
+
export const isParticipantOperator = <Tag, Kind>(
|
|
42
|
+
operator: WhereOperator<Tag, any>,
|
|
43
|
+
): operator is ParticipantOperator<Tag, Kind> => {
|
|
44
|
+
return 'participant' in operator
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
export type NotOperator<Tag, Kind> = Omit<AllNever, 'not'> & {
|
|
36
48
|
not: WhereOperator<Tag, Kind>
|
|
37
49
|
}
|
|
@@ -56,6 +68,7 @@ export const isOrOperator = <Tag, Kind>(operator: WhereOperator<Tag, Kind>): ope
|
|
|
56
68
|
export type WhereOperator<Tag, Kind> =
|
|
57
69
|
| TagEqual<Tag>
|
|
58
70
|
| KindEqual<Kind>
|
|
71
|
+
| ParticipantOperator<Tag, Kind>
|
|
59
72
|
| NotOperator<Tag, Kind>
|
|
60
73
|
| AndOperator<Tag, Kind>
|
|
61
74
|
| OrOperator<Tag, Kind>
|
|
@@ -66,15 +79,23 @@ export type Filterable<
|
|
|
66
79
|
> = {
|
|
67
80
|
tags?: readonly FTag[] | null
|
|
68
81
|
kind?: FKind
|
|
82
|
+
source?: Filterable<FTag, FKind>
|
|
83
|
+
target?: Filterable<FTag, FKind>
|
|
69
84
|
}
|
|
70
85
|
|
|
71
86
|
export type OperatorPredicate<V extends Filterable> = (value: V) => boolean
|
|
72
87
|
|
|
73
88
|
export function whereOperatorAsPredicate<
|
|
74
|
-
FTag extends string = string,
|
|
75
|
-
FKind extends string = string,
|
|
89
|
+
FTag extends string | null = string | null,
|
|
90
|
+
FKind extends string | null = string | null,
|
|
76
91
|
>(operator: WhereOperator<FTag, FKind>): OperatorPredicate<Filterable> {
|
|
77
92
|
switch (true) {
|
|
93
|
+
case isParticipantOperator(operator): {
|
|
94
|
+
const participant = operator.participant
|
|
95
|
+
const participantPredicate = whereOperatorAsPredicate(operator.operator)
|
|
96
|
+
|
|
97
|
+
return participantIs(participant, participantPredicate)
|
|
98
|
+
}
|
|
78
99
|
case isTagEqual(operator): {
|
|
79
100
|
if ('eq' in operator.tag) {
|
|
80
101
|
const tag = operator.tag.eq
|
|
@@ -115,3 +136,22 @@ export function whereOperatorAsPredicate<
|
|
|
115
136
|
nonexhaustive(operator)
|
|
116
137
|
}
|
|
117
138
|
}
|
|
139
|
+
|
|
140
|
+
function participantIs<FTag extends string | null, FKind extends string | null>(
|
|
141
|
+
participant: Participant,
|
|
142
|
+
predicate: OperatorPredicate<Filterable<FTag, FKind>>,
|
|
143
|
+
): OperatorPredicate<Filterable<FTag, FKind>> {
|
|
144
|
+
return (value) => {
|
|
145
|
+
if (!value.source || !value.target) {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
switch (participant) {
|
|
149
|
+
case 'source': {
|
|
150
|
+
return predicate(value.source)
|
|
151
|
+
}
|
|
152
|
+
case 'target': {
|
|
153
|
+
return predicate(value.target)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|