@likec4/core 1.17.1 → 1.18.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/compute-view/index.d.mts +91 -0
- package/dist/compute-view/index.d.ts +91 -0
- package/dist/compute-view/index.mjs +79 -0
- package/dist/index.d.mts +246 -434
- package/dist/index.d.ts +246 -434
- package/dist/index.mjs +418 -1322
- package/dist/model/index.d.mts +642 -0
- package/dist/model/index.d.ts +642 -0
- package/dist/model/index.mjs +2 -0
- package/dist/shared/core.BBgiAAv1.mjs +144 -0
- package/dist/shared/core.BPyJvuMU.mjs +2420 -0
- package/dist/shared/core.BQ4AbfvB.mjs +9228 -0
- package/dist/shared/{core.CoLQkcYL.d.mts → core.DMYY7C8s.d.mts} +398 -249
- package/dist/shared/{core.CoLQkcYL.d.ts → core.DMYY7C8s.d.ts} +398 -249
- package/dist/shared/core.k12s_keD.mjs +203 -0
- package/dist/types/index.d.mts +69 -1
- package/dist/types/index.d.ts +69 -1
- package/dist/types/index.mjs +21 -1
- package/package.json +38 -40
- package/src/builder/Builder.deployment.ts +219 -0
- package/src/builder/Builder.deploymentModel.ts +360 -0
- package/src/builder/Builder.element.ts +17 -19
- package/src/builder/Builder.model.ts +13 -2
- package/src/builder/Builder.test-d.ts +59 -4
- package/src/builder/Builder.ts +238 -53
- package/src/builder/Builder.view.ts +238 -53
- package/src/builder/_types.ts +87 -15
- package/src/compute-view/LikeC4ModelGraph.ts +330 -0
- package/src/compute-view/compute-view.ts +92 -0
- package/src/compute-view/deployment-view/Memory.ts +108 -0
- package/src/compute-view/deployment-view/Stage.ts +145 -0
- package/src/compute-view/deployment-view/__test__/fixture.ts +210 -0
- package/src/compute-view/deployment-view/_types.ts +29 -0
- package/src/compute-view/deployment-view/clean-connections.ts +229 -0
- package/src/compute-view/deployment-view/compute.ts +111 -0
- package/src/compute-view/deployment-view/predicates/deployment-ref.ts +146 -0
- package/src/compute-view/deployment-view/predicates/relation-direct.ts +130 -0
- package/src/compute-view/deployment-view/predicates/relation-in-out.ts +43 -0
- package/src/compute-view/deployment-view/predicates/relation-incoming.ts +46 -0
- package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +49 -0
- package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
- package/src/compute-view/deployment-view/utils.ts +212 -0
- package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
- package/src/compute-view/dynamic-view/compute.ts +310 -0
- package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +456 -0
- package/src/compute-view/element-view/__test__/fixture.ts +645 -0
- package/src/compute-view/element-view/compute.ts +734 -0
- package/src/compute-view/element-view/predicates.ts +517 -0
- package/src/compute-view/index.ts +5 -0
- package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
- package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
- package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
- package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
- package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
- package/src/compute-view/utils/buildComputedNodes.ts +148 -0
- package/src/compute-view/utils/buildElementNotations.ts +63 -0
- package/src/compute-view/utils/derive-edge-props-from-relationships.ts +97 -0
- package/src/compute-view/utils/elementExpressionToPredicate.ts +52 -0
- package/src/compute-view/utils/link-nodes-with-edges.ts +58 -0
- package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
- package/src/compute-view/utils/resolve-extended-views.ts +65 -0
- package/src/compute-view/utils/resolve-global-rules.ts +83 -0
- package/src/compute-view/utils/sortNodes.ts +101 -0
- package/src/compute-view/utils/topological-sort.ts +160 -0
- package/src/compute-view/utils/uniqueTags.test.ts +42 -0
- package/src/compute-view/utils/uniqueTags.ts +21 -0
- package/src/compute-view/utils/view-hash.ts +27 -0
- package/src/compute-view/utils/with-readable-edges.ts +31 -0
- package/src/index.ts +5 -1
- package/src/model/DeploymentElementModel.ts +534 -0
- package/src/model/DeploymentModel.ts +364 -0
- package/src/model/ElementModel.ts +195 -0
- package/src/model/LikeC4Model.test-d.ts +151 -0
- package/src/model/LikeC4Model.ts +280 -457
- package/src/model/RelationModel.ts +84 -0
- package/src/model/__test__/fixture.ts +153 -338
- package/src/model/connection/ConnectionModel.ts +94 -0
- package/src/model/connection/DeploymentConnectionModel.ts +99 -0
- package/src/model/connection/deployment.ts +124 -0
- package/src/model/connection/model.ts +123 -0
- package/src/model/index.ts +4 -2
- package/src/model/types.ts +95 -15
- package/src/model/view/EdgeModel.ts +103 -0
- package/src/model/view/LikeC4ViewModel.ts +220 -0
- package/src/model/view/NodeModel.ts +228 -0
- package/src/types/_common.ts +2 -0
- package/src/types/deployments.ts +237 -0
- package/src/types/element.ts +7 -3
- package/src/types/expression.ts +8 -6
- package/src/types/index.ts +1 -0
- package/src/types/model.ts +68 -9
- package/src/types/overview-graph.ts +2 -2
- package/src/types/relation.ts +4 -4
- package/src/types/view-notation.ts +2 -2
- package/src/types/view.ts +104 -30
- package/src/utils/{commonHead.ts → common-head.ts} +2 -0
- package/src/utils/fqn.ts +83 -27
- package/src/utils/getOrCreate.ts +42 -0
- package/src/utils/graphlib.ts +10 -0
- package/src/utils/index.ts +4 -1
- package/src/utils/object-hash.ts +3 -0
- package/src/utils/set.ts +76 -0
- package/src/utils/string-hash.ts +16 -0
- package/dist/index.cjs +0 -5452
- package/dist/index.d.cts +0 -1053
- package/dist/shared/core.BXTs-8n7.mjs +0 -293
- package/dist/shared/core.CoLQkcYL.d.cts +0 -734
- package/dist/shared/core.DIYZvpVg.cjs +0 -353
- package/dist/types/index.cjs +0 -70
- package/dist/types/index.d.cts +0 -2
- package/src/model/LikeC4DiagramModel.ts +0 -338
- package/src/model/LikeC4ViewModel.ts +0 -320
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { map, prop } from 'remeda'
|
|
2
|
+
import { Builder } from '../../../builder'
|
|
3
|
+
import type { DeploymentViewRuleBuilderOp } from '../../../builder/Builder.view'
|
|
4
|
+
import { LikeC4Model } from '../../../model'
|
|
5
|
+
import type { DeploymentView } from '../../../types'
|
|
6
|
+
import { withReadableEdges } from '../../utils/with-readable-edges'
|
|
7
|
+
import { computeDeploymentView } from '../compute'
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
builder: b,
|
|
11
|
+
model: {
|
|
12
|
+
model,
|
|
13
|
+
person,
|
|
14
|
+
system,
|
|
15
|
+
component,
|
|
16
|
+
webapp,
|
|
17
|
+
mobile,
|
|
18
|
+
...$m
|
|
19
|
+
},
|
|
20
|
+
deployment: {
|
|
21
|
+
deployment,
|
|
22
|
+
env,
|
|
23
|
+
node,
|
|
24
|
+
zone,
|
|
25
|
+
instanceOf,
|
|
26
|
+
...$d
|
|
27
|
+
},
|
|
28
|
+
views: {
|
|
29
|
+
view,
|
|
30
|
+
views,
|
|
31
|
+
viewOf,
|
|
32
|
+
deploymentView,
|
|
33
|
+
$rules,
|
|
34
|
+
$include,
|
|
35
|
+
$exclude,
|
|
36
|
+
$style
|
|
37
|
+
}
|
|
38
|
+
} = Builder.forSpecification({
|
|
39
|
+
elements: {
|
|
40
|
+
person: {
|
|
41
|
+
style: {
|
|
42
|
+
shape: 'person'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
system: {},
|
|
46
|
+
component: {},
|
|
47
|
+
webapp: {
|
|
48
|
+
style: {
|
|
49
|
+
shape: 'browser'
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
mobile: {
|
|
53
|
+
style: {
|
|
54
|
+
shape: 'mobile'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
deployments: {
|
|
59
|
+
env: {},
|
|
60
|
+
zone: {},
|
|
61
|
+
node: {}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export const builder = b
|
|
66
|
+
.with(
|
|
67
|
+
model(
|
|
68
|
+
person('customer', {
|
|
69
|
+
title: 'Happy Customer'
|
|
70
|
+
}),
|
|
71
|
+
system('cloud').with(
|
|
72
|
+
component('frontend').with(
|
|
73
|
+
webapp('dashboard'),
|
|
74
|
+
mobile('mobile')
|
|
75
|
+
),
|
|
76
|
+
component('auth'),
|
|
77
|
+
component('backend').with(
|
|
78
|
+
component('api')
|
|
79
|
+
),
|
|
80
|
+
component('media', {
|
|
81
|
+
shape: 'storage'
|
|
82
|
+
})
|
|
83
|
+
),
|
|
84
|
+
system('aws').with(
|
|
85
|
+
component('rds', {
|
|
86
|
+
shape: 'storage'
|
|
87
|
+
}),
|
|
88
|
+
component('s3', {
|
|
89
|
+
shape: 'storage'
|
|
90
|
+
})
|
|
91
|
+
),
|
|
92
|
+
system('email')
|
|
93
|
+
),
|
|
94
|
+
model(
|
|
95
|
+
$m.rel('customer', 'cloud', 'uses services'),
|
|
96
|
+
$m.rel('customer', 'cloud.frontend.mobile', 'opens mobile app'),
|
|
97
|
+
$m.rel('customer', 'cloud.frontend.dashboard', 'opens in browser'),
|
|
98
|
+
$m.rel('cloud.frontend.dashboard', 'cloud.auth', {
|
|
99
|
+
title: 'authenticates',
|
|
100
|
+
color: 'green'
|
|
101
|
+
}),
|
|
102
|
+
$m.rel('cloud.frontend.dashboard', 'cloud.backend.api', 'fetches data'),
|
|
103
|
+
$m.rel('cloud.frontend.dashboard', 'cloud.media', 'fetches media'),
|
|
104
|
+
$m.rel('cloud.frontend.mobile', 'cloud.auth', {
|
|
105
|
+
title: 'authenticates',
|
|
106
|
+
color: 'amber'
|
|
107
|
+
}),
|
|
108
|
+
$m.rel('cloud.frontend.mobile', 'cloud.backend.api', 'fetches data'),
|
|
109
|
+
$m.rel('cloud.frontend.mobile', 'cloud.media', 'fetches media'),
|
|
110
|
+
$m.rel('cloud.backend.api', 'cloud.auth', 'authorizes'),
|
|
111
|
+
$m.rel('cloud.backend.api', 'cloud.media', 'uploads media'),
|
|
112
|
+
$m.rel('cloud.backend.api', 'aws.rds', 'reads/writes'),
|
|
113
|
+
$m.rel('cloud.backend.api', 'email', 'sends emails'),
|
|
114
|
+
$m.rel('cloud.media', 'aws.s3', 'uploads'),
|
|
115
|
+
$m.rel('email', 'customer', 'sends emails')
|
|
116
|
+
),
|
|
117
|
+
deployment(
|
|
118
|
+
node('customer').with(
|
|
119
|
+
instanceOf('instance', 'customer')
|
|
120
|
+
),
|
|
121
|
+
env('prod').with(
|
|
122
|
+
zone('eu').with(
|
|
123
|
+
zone('zone1').with(
|
|
124
|
+
instanceOf('api', 'cloud.backend.api'),
|
|
125
|
+
instanceOf('ui', 'cloud.frontend.dashboard')
|
|
126
|
+
),
|
|
127
|
+
zone('zone2').with(
|
|
128
|
+
instanceOf('api', 'cloud.backend.api'),
|
|
129
|
+
instanceOf('ui', 'cloud.frontend.dashboard')
|
|
130
|
+
),
|
|
131
|
+
instanceOf('auth', 'cloud.auth'),
|
|
132
|
+
instanceOf('media', 'cloud.media'),
|
|
133
|
+
instanceOf('db', 'aws.rds')
|
|
134
|
+
),
|
|
135
|
+
zone('us').with(
|
|
136
|
+
zone('zone1').with(
|
|
137
|
+
instanceOf('api', 'cloud.backend.api'),
|
|
138
|
+
instanceOf('ui', 'cloud.frontend.dashboard')
|
|
139
|
+
),
|
|
140
|
+
instanceOf('db', 'aws.rds')
|
|
141
|
+
)
|
|
142
|
+
),
|
|
143
|
+
env('acc').with(
|
|
144
|
+
node('testCustomer').with(
|
|
145
|
+
instanceOf('instance', 'customer')
|
|
146
|
+
),
|
|
147
|
+
zone('eu').with(
|
|
148
|
+
instanceOf('api', 'cloud.backend.api'),
|
|
149
|
+
instanceOf('ui', 'cloud.frontend.dashboard'),
|
|
150
|
+
instanceOf('auth', 'cloud.auth'),
|
|
151
|
+
instanceOf('db', 'aws.rds')
|
|
152
|
+
)
|
|
153
|
+
),
|
|
154
|
+
env('global').with(
|
|
155
|
+
instanceOf('email', 'email')
|
|
156
|
+
),
|
|
157
|
+
$d.rel('prod.eu.db', 'prod.us.db', {
|
|
158
|
+
title: 'replicates',
|
|
159
|
+
color: 'green'
|
|
160
|
+
}),
|
|
161
|
+
$d.rel('prod.us.db', 'prod.eu.db', {
|
|
162
|
+
title: 'replicates',
|
|
163
|
+
color: 'amber'
|
|
164
|
+
})
|
|
165
|
+
)
|
|
166
|
+
// views(
|
|
167
|
+
// view('index', $include('*')),
|
|
168
|
+
// viewOf('cloud', 'cloud', $rules(
|
|
169
|
+
// $include('*'),
|
|
170
|
+
// )),
|
|
171
|
+
// deploymentView('prod', $rules(
|
|
172
|
+
// $include('customer.c'),
|
|
173
|
+
// $include('prod._'),
|
|
174
|
+
// $exclude('prod.eu.zone1 <-> prod.eu.zone2')
|
|
175
|
+
// ))
|
|
176
|
+
// )
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
export type Types = typeof builder['Types']
|
|
180
|
+
|
|
181
|
+
export { $exclude, $include }
|
|
182
|
+
|
|
183
|
+
export function computeView(...rules: DeploymentViewRuleBuilderOp<Types>[]) {
|
|
184
|
+
const modelsource = builder.with(
|
|
185
|
+
views(
|
|
186
|
+
deploymentView('index', $rules(...rules))
|
|
187
|
+
)
|
|
188
|
+
).build()
|
|
189
|
+
|
|
190
|
+
const model = LikeC4Model.create({
|
|
191
|
+
...modelsource,
|
|
192
|
+
views: {}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const view = withReadableEdges(computeDeploymentView(model, modelsource.views.index as DeploymentView))
|
|
196
|
+
|
|
197
|
+
return Object.assign(view, {
|
|
198
|
+
nodeIds: map(view.nodes, prop('id')) as string[],
|
|
199
|
+
edgeIds: map(view.edges, prop('id')) as string[]
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function computeNodesAndEdges(...rules: DeploymentViewRuleBuilderOp<Types>[]) {
|
|
204
|
+
const { nodeIds, edgeIds } = computeView(...rules)
|
|
205
|
+
return {
|
|
206
|
+
// Starts with capital letter to be first in snapshot
|
|
207
|
+
Nodes: nodeIds,
|
|
208
|
+
edges: edgeIds
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { LikeC4DeploymentModel } from '../../model'
|
|
2
|
+
import type { DeploymentConnectionModel } from '../../model/connection/deployment'
|
|
3
|
+
import type { DeploymentElementModel } from '../../model/DeploymentElementModel'
|
|
4
|
+
import type { AnyAux } from '../../model/types'
|
|
5
|
+
import type { DeploymentExpression } from '../../types'
|
|
6
|
+
import type { Memory, Patch } from './Memory'
|
|
7
|
+
import type { Stage } from './Stage'
|
|
8
|
+
|
|
9
|
+
export type Elem = DeploymentElementModel<AnyAux>
|
|
10
|
+
|
|
11
|
+
export type Connections<M extends AnyAux = AnyAux> = ReadonlyArray<DeploymentConnectionModel<M>>
|
|
12
|
+
|
|
13
|
+
export type PredicateParams<Expr extends DeploymentExpression = any> = {
|
|
14
|
+
expr: Expr
|
|
15
|
+
model: LikeC4DeploymentModel
|
|
16
|
+
stage: Stage
|
|
17
|
+
memory: Memory
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PredicateCtx {
|
|
21
|
+
model: LikeC4DeploymentModel
|
|
22
|
+
stage: Stage
|
|
23
|
+
memory: Memory
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PredicateExecutor<Expr extends DeploymentExpression> {
|
|
27
|
+
include(expr: Expr, ctx: PredicateCtx): Patch
|
|
28
|
+
exclude(expr: Expr, ctx: PredicateCtx): Patch
|
|
29
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import DefaultMap from 'mnemonist/default-map'
|
|
2
|
+
import { dropWhile, filter, flatMap, forEach, forEachObj, groupBy, hasAtLeast, map, pipe, prop, sort } from 'remeda'
|
|
3
|
+
import { mergeConnections } from '../../model/connection/deployment'
|
|
4
|
+
import { DeploymentConnectionModel } from '../../model/connection/DeploymentConnectionModel'
|
|
5
|
+
import { RelationshipsAccum } from '../../model/DeploymentElementModel'
|
|
6
|
+
import type { RelationshipModel } from '../../model/RelationModel'
|
|
7
|
+
import type { AnyAux } from '../../model/types'
|
|
8
|
+
import { compareFqnHierarchically, isAncestor } from '../../utils'
|
|
9
|
+
import { difference } from '../../utils/set'
|
|
10
|
+
import type { Connections, Elem } from './_types'
|
|
11
|
+
import { type Patch } from './Memory'
|
|
12
|
+
|
|
13
|
+
const filterEmptyConnection = filter((c: DeploymentConnectionModel<any>) => c.nonEmpty())
|
|
14
|
+
|
|
15
|
+
// We add '.' to distinguish between connections with boundary and without
|
|
16
|
+
// This way we can sort them hierarchically
|
|
17
|
+
const connectionBoundary = (conn: DeploymentConnectionModel<any>) => conn.boundary?.id ? `.${conn.boundary.id}` : ''
|
|
18
|
+
|
|
19
|
+
function cleanCrossBoundary<M extends AnyAux>(connections: Connections<M>): Connections<M> {
|
|
20
|
+
// Keep only connections between leafs
|
|
21
|
+
// Also find connections based on same relation
|
|
22
|
+
const groupedByRelation = new DefaultMap<RelationshipModel<M>, Array<DeploymentConnectionModel<M>>>(() => [])
|
|
23
|
+
for (const conn of connections) {
|
|
24
|
+
for (const relation of conn.relations.model) {
|
|
25
|
+
groupedByRelation.get(relation).push(conn)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// DeploymentConnectionModel is immutables
|
|
30
|
+
// So we create new instances without excluded relations
|
|
31
|
+
const excludedRelations = new DefaultMap<
|
|
32
|
+
DeploymentConnectionModel<M>,
|
|
33
|
+
Set<RelationshipModel<M>>
|
|
34
|
+
>(() => new Set())
|
|
35
|
+
|
|
36
|
+
// In each group, find connected to same leaf
|
|
37
|
+
for (const [relation, sameRelationGroup] of groupedByRelation) {
|
|
38
|
+
if (!hasAtLeast(sameRelationGroup, 2)) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Outgoing from same source
|
|
43
|
+
pipe(
|
|
44
|
+
sameRelationGroup,
|
|
45
|
+
flatMap(conn => [
|
|
46
|
+
{ group: `$source-${conn.source.id}`, conn },
|
|
47
|
+
{ group: `$target-${conn.target.id}`, conn }
|
|
48
|
+
]),
|
|
49
|
+
groupBy(prop('group')),
|
|
50
|
+
forEachObj((connections) => {
|
|
51
|
+
if (connections.length < 2) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
pipe(
|
|
55
|
+
connections,
|
|
56
|
+
map(prop('conn')),
|
|
57
|
+
// Sort by hierarchy, first are deepest
|
|
58
|
+
sort((a, b) => compareFqnHierarchically(connectionBoundary(a), connectionBoundary(b)) * -1),
|
|
59
|
+
// Skip connections until we reach another boundary
|
|
60
|
+
dropWhile((conn, i, all) => i === 0 || conn.boundary === all[i - 1]!.boundary),
|
|
61
|
+
// Clean relations from the rest
|
|
62
|
+
forEach((conn) => {
|
|
63
|
+
excludedRelations.get(conn).add(relation)
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return pipe(
|
|
71
|
+
connections,
|
|
72
|
+
map(conn => {
|
|
73
|
+
const excluded = excludedRelations.get(conn)
|
|
74
|
+
if (excluded && excluded.size > 0) {
|
|
75
|
+
const updated = new DeploymentConnectionModel(
|
|
76
|
+
conn.source,
|
|
77
|
+
conn.target,
|
|
78
|
+
new RelationshipsAccum(
|
|
79
|
+
difference(conn.relations.model, excluded),
|
|
80
|
+
conn.relations.deployment
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return updated
|
|
84
|
+
}
|
|
85
|
+
return conn
|
|
86
|
+
}),
|
|
87
|
+
filterEmptyConnection
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Remove relationships from connection model, that are already included in the connections between descendants.
|
|
93
|
+
* In other words - if there is same connection down the hierarchy.
|
|
94
|
+
*
|
|
95
|
+
* @returns New connection without redundant relationships
|
|
96
|
+
* Connection may be empty if all relationships are redundant, in this case it should be removed
|
|
97
|
+
*/
|
|
98
|
+
function excludeRedundantRelationships<M extends AnyAux>(
|
|
99
|
+
connections: Connections<M>
|
|
100
|
+
): Array<DeploymentConnectionModel<M>> {
|
|
101
|
+
return pipe(
|
|
102
|
+
connections,
|
|
103
|
+
map(connection => {
|
|
104
|
+
const { source, target } = connection
|
|
105
|
+
|
|
106
|
+
// Connection between instances is always explicit
|
|
107
|
+
if (source.isInstance() && target.isInstance()) {
|
|
108
|
+
return connection
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let modelRelations = connection.relations.model
|
|
112
|
+
let deploymentRelations = connection.relations.deployment
|
|
113
|
+
|
|
114
|
+
for (const c of connections) {
|
|
115
|
+
if (
|
|
116
|
+
isAncestor(source.id, c.source.id) && isAncestor(target.id, c.target.id)
|
|
117
|
+
|| isAncestor(source.id, c.source.id) && c.target.id === target.id
|
|
118
|
+
|| isAncestor(target.id, c.target.id) && c.source.id === source.id
|
|
119
|
+
) {
|
|
120
|
+
modelRelations = difference(modelRelations, c.relations.model)
|
|
121
|
+
deploymentRelations = difference(deploymentRelations, c.relations.deployment)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// if any set changed, return new connection
|
|
126
|
+
if (modelRelations !== connection.relations.model || deploymentRelations !== connection.relations.deployment) {
|
|
127
|
+
return new DeploymentConnectionModel(
|
|
128
|
+
source,
|
|
129
|
+
target,
|
|
130
|
+
new RelationshipsAccum(
|
|
131
|
+
modelRelations,
|
|
132
|
+
deploymentRelations
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return connection
|
|
138
|
+
}),
|
|
139
|
+
filterEmptyConnection
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* This patch:
|
|
144
|
+
* 1. Keeps connections between leafs or having direct deployment relations
|
|
145
|
+
* 2. Removes cross-boundary model relations, that already exist inside boundaries
|
|
146
|
+
* (e.g. prefer relations inside same deployment node over relations between nodes)
|
|
147
|
+
* 3. Removes implicit connections between elements, if their descendants have same connection
|
|
148
|
+
*/
|
|
149
|
+
export const cleanConnections: Patch = (memory) => {
|
|
150
|
+
if (memory.connections.length < 2) {
|
|
151
|
+
return memory
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const leafs = new Set<Elem>()
|
|
155
|
+
|
|
156
|
+
for (const element of memory.elements) {
|
|
157
|
+
// Instance is always leaf
|
|
158
|
+
if (element.isInstance()) {
|
|
159
|
+
leafs.add(element)
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
// Check if element is ancestor of any element in elements
|
|
163
|
+
let isLeaf = true
|
|
164
|
+
for (const el of memory.elements) {
|
|
165
|
+
if (isAncestor(element.id, el.id)) {
|
|
166
|
+
isLeaf = false
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (isLeaf) {
|
|
171
|
+
leafs.add(element)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const connectedElements = new Set<Elem>()
|
|
176
|
+
|
|
177
|
+
const connections = pipe(
|
|
178
|
+
memory.connections,
|
|
179
|
+
mergeConnections,
|
|
180
|
+
// Keep connections
|
|
181
|
+
// - between leafs
|
|
182
|
+
// - has direct deployment relation
|
|
183
|
+
filter(c => {
|
|
184
|
+
return (leafs.has(c.source) && leafs.has(c.target)) || c.hasDirectDeploymentRelation()
|
|
185
|
+
}),
|
|
186
|
+
cleanCrossBoundary,
|
|
187
|
+
excludeRedundantRelationships,
|
|
188
|
+
forEach((c) => {
|
|
189
|
+
connectedElements.add(c.source)
|
|
190
|
+
connectedElements.add(c.target)
|
|
191
|
+
if (c.boundary) {
|
|
192
|
+
connectedElements.add(c.boundary)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
const isAncestorOfConnected = (el: Elem) => {
|
|
198
|
+
if (el.isInstance()) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
for (const connected of connectedElements) {
|
|
202
|
+
if (isAncestor(el.id, connected.id)) {
|
|
203
|
+
return true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Update memory
|
|
210
|
+
const newMemory = memory.clone()
|
|
211
|
+
newMemory.connections = connections
|
|
212
|
+
|
|
213
|
+
// Iterate over elements (preserving order) and pick:
|
|
214
|
+
// - explicitly included
|
|
215
|
+
// - connected elements
|
|
216
|
+
// - ancestors of connected elements
|
|
217
|
+
const finalElements = new Set<Elem>()
|
|
218
|
+
for (const el of newMemory.finalElements) {
|
|
219
|
+
if (
|
|
220
|
+
newMemory.isExplicit(el)
|
|
221
|
+
|| connectedElements.has(el)
|
|
222
|
+
|| isAncestorOfConnected(el)
|
|
223
|
+
) {
|
|
224
|
+
finalElements.add(el)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
newMemory.finalElements = finalElements
|
|
228
|
+
return newMemory
|
|
229
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { findLast } from 'remeda'
|
|
2
|
+
import { nonexhaustive } from '../../errors'
|
|
3
|
+
import { type LikeC4DeploymentModel, LikeC4Model } from '../../model'
|
|
4
|
+
import type { AnyAux } from '../../model/types'
|
|
5
|
+
import type { DeploymentViewRule } from '../../types'
|
|
6
|
+
import {
|
|
7
|
+
type ComputedDeploymentView,
|
|
8
|
+
DeploymentElementExpression,
|
|
9
|
+
DeploymentRelationExpression,
|
|
10
|
+
type DeploymentView,
|
|
11
|
+
isViewRuleAutoLayout,
|
|
12
|
+
isViewRulePredicate
|
|
13
|
+
} from '../../types'
|
|
14
|
+
import { linkNodesWithEdges } from '../utils/link-nodes-with-edges'
|
|
15
|
+
import { topologicalSort } from '../utils/topological-sort'
|
|
16
|
+
import { calcViewLayoutHash } from '../utils/view-hash'
|
|
17
|
+
import { cleanConnections } from './clean-connections'
|
|
18
|
+
import { MutableMemory, type Patch } from './Memory'
|
|
19
|
+
import { DeploymentRefPredicate } from './predicates/deployment-ref'
|
|
20
|
+
import { DirectRelationPredicate } from './predicates/relation-direct'
|
|
21
|
+
import { InOutRelationPredicate } from './predicates/relation-in-out'
|
|
22
|
+
import { IncomingRelationPredicate } from './predicates/relation-incoming'
|
|
23
|
+
import { OutgoingRelationPredicate } from './predicates/relation-outgoing'
|
|
24
|
+
import { WildcardPredicate } from './predicates/wildcard'
|
|
25
|
+
import { Stage } from './Stage'
|
|
26
|
+
import { applyDeploymentViewRuleStyles, buildNodes, toComputedEdges } from './utils'
|
|
27
|
+
|
|
28
|
+
function processPredicates<M extends AnyAux>(
|
|
29
|
+
model: LikeC4DeploymentModel<M>,
|
|
30
|
+
rules: DeploymentViewRule[]
|
|
31
|
+
) {
|
|
32
|
+
let memory = MutableMemory.empty()
|
|
33
|
+
let stage: Stage | null = null
|
|
34
|
+
|
|
35
|
+
for (const rule of rules) {
|
|
36
|
+
if (isViewRulePredicate(rule)) {
|
|
37
|
+
const op = 'include' in rule ? 'include' : 'exclude'
|
|
38
|
+
const exprs = rule.include ?? rule.exclude
|
|
39
|
+
for (const expr of exprs) {
|
|
40
|
+
stage = new Stage(stage)
|
|
41
|
+
const ctx = { model, stage, memory }
|
|
42
|
+
let patch: Patch | undefined
|
|
43
|
+
switch (true) {
|
|
44
|
+
case DeploymentElementExpression.isRef(expr):
|
|
45
|
+
patch = DeploymentRefPredicate[op](expr, ctx)
|
|
46
|
+
break
|
|
47
|
+
case DeploymentElementExpression.isWildcard(expr):
|
|
48
|
+
patch = WildcardPredicate[op](expr, ctx)
|
|
49
|
+
break
|
|
50
|
+
case DeploymentRelationExpression.isDirect(expr):
|
|
51
|
+
patch = DirectRelationPredicate[op](expr, ctx)
|
|
52
|
+
break
|
|
53
|
+
case DeploymentRelationExpression.isInOut(expr):
|
|
54
|
+
patch = InOutRelationPredicate[op](expr, ctx)
|
|
55
|
+
break
|
|
56
|
+
case DeploymentRelationExpression.isOutgoing(expr):
|
|
57
|
+
patch = OutgoingRelationPredicate[op](expr, ctx)
|
|
58
|
+
break
|
|
59
|
+
case DeploymentRelationExpression.isIncoming(expr):
|
|
60
|
+
patch = IncomingRelationPredicate[op](expr, ctx)
|
|
61
|
+
break
|
|
62
|
+
default:
|
|
63
|
+
nonexhaustive(expr)
|
|
64
|
+
}
|
|
65
|
+
patch ??= stage.patch()
|
|
66
|
+
memory = patch(memory)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return cleanConnections(memory)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function computeDeploymentView<M extends AnyAux>(
|
|
74
|
+
likec4model: LikeC4Model<M>,
|
|
75
|
+
{
|
|
76
|
+
docUri: _docUri, // exclude docUri
|
|
77
|
+
rules, // exclude rules
|
|
78
|
+
...view
|
|
79
|
+
}: DeploymentView
|
|
80
|
+
): ComputedDeploymentView<M['ViewId']> {
|
|
81
|
+
const memory = processPredicates<M>(likec4model.deployment, rules)
|
|
82
|
+
|
|
83
|
+
const nodesMap = buildNodes(memory)
|
|
84
|
+
|
|
85
|
+
const computedEdges = toComputedEdges(memory.connections)
|
|
86
|
+
|
|
87
|
+
linkNodesWithEdges(nodesMap, computedEdges)
|
|
88
|
+
|
|
89
|
+
const sorted = topologicalSort({
|
|
90
|
+
nodes: [...nodesMap.values()],
|
|
91
|
+
edges: computedEdges
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const nodes = applyDeploymentViewRuleStyles(
|
|
95
|
+
rules,
|
|
96
|
+
sorted.nodes
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
const autoLayoutRule = findLast(rules, isViewRuleAutoLayout)
|
|
100
|
+
|
|
101
|
+
return calcViewLayoutHash({
|
|
102
|
+
...view,
|
|
103
|
+
autoLayout: {
|
|
104
|
+
direction: autoLayoutRule?.direction ?? 'TB',
|
|
105
|
+
...(autoLayoutRule?.nodeSep && { nodeSep: autoLayoutRule.nodeSep }),
|
|
106
|
+
...(autoLayoutRule?.rankSep && { rankSep: autoLayoutRule.rankSep })
|
|
107
|
+
},
|
|
108
|
+
nodes,
|
|
109
|
+
edges: sorted.edges
|
|
110
|
+
})
|
|
111
|
+
}
|