@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.
Files changed (112) hide show
  1. package/dist/compute-view/index.d.mts +91 -0
  2. package/dist/compute-view/index.d.ts +91 -0
  3. package/dist/compute-view/index.mjs +79 -0
  4. package/dist/index.d.mts +246 -434
  5. package/dist/index.d.ts +246 -434
  6. package/dist/index.mjs +418 -1322
  7. package/dist/model/index.d.mts +642 -0
  8. package/dist/model/index.d.ts +642 -0
  9. package/dist/model/index.mjs +2 -0
  10. package/dist/shared/core.BBgiAAv1.mjs +144 -0
  11. package/dist/shared/core.BPyJvuMU.mjs +2420 -0
  12. package/dist/shared/core.BQ4AbfvB.mjs +9228 -0
  13. package/dist/shared/{core.CoLQkcYL.d.mts → core.DMYY7C8s.d.mts} +398 -249
  14. package/dist/shared/{core.CoLQkcYL.d.ts → core.DMYY7C8s.d.ts} +398 -249
  15. package/dist/shared/core.k12s_keD.mjs +203 -0
  16. package/dist/types/index.d.mts +69 -1
  17. package/dist/types/index.d.ts +69 -1
  18. package/dist/types/index.mjs +21 -1
  19. package/package.json +38 -40
  20. package/src/builder/Builder.deployment.ts +219 -0
  21. package/src/builder/Builder.deploymentModel.ts +360 -0
  22. package/src/builder/Builder.element.ts +17 -19
  23. package/src/builder/Builder.model.ts +13 -2
  24. package/src/builder/Builder.test-d.ts +59 -4
  25. package/src/builder/Builder.ts +238 -53
  26. package/src/builder/Builder.view.ts +238 -53
  27. package/src/builder/_types.ts +87 -15
  28. package/src/compute-view/LikeC4ModelGraph.ts +330 -0
  29. package/src/compute-view/compute-view.ts +92 -0
  30. package/src/compute-view/deployment-view/Memory.ts +108 -0
  31. package/src/compute-view/deployment-view/Stage.ts +145 -0
  32. package/src/compute-view/deployment-view/__test__/fixture.ts +210 -0
  33. package/src/compute-view/deployment-view/_types.ts +29 -0
  34. package/src/compute-view/deployment-view/clean-connections.ts +229 -0
  35. package/src/compute-view/deployment-view/compute.ts +111 -0
  36. package/src/compute-view/deployment-view/predicates/deployment-ref.ts +146 -0
  37. package/src/compute-view/deployment-view/predicates/relation-direct.ts +130 -0
  38. package/src/compute-view/deployment-view/predicates/relation-in-out.ts +43 -0
  39. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +46 -0
  40. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +49 -0
  41. package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
  42. package/src/compute-view/deployment-view/utils.ts +212 -0
  43. package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
  44. package/src/compute-view/dynamic-view/compute.ts +310 -0
  45. package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +456 -0
  46. package/src/compute-view/element-view/__test__/fixture.ts +645 -0
  47. package/src/compute-view/element-view/compute.ts +734 -0
  48. package/src/compute-view/element-view/predicates.ts +517 -0
  49. package/src/compute-view/index.ts +5 -0
  50. package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
  51. package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
  52. package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
  53. package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
  54. package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
  55. package/src/compute-view/utils/buildComputedNodes.ts +148 -0
  56. package/src/compute-view/utils/buildElementNotations.ts +63 -0
  57. package/src/compute-view/utils/derive-edge-props-from-relationships.ts +97 -0
  58. package/src/compute-view/utils/elementExpressionToPredicate.ts +52 -0
  59. package/src/compute-view/utils/link-nodes-with-edges.ts +58 -0
  60. package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
  61. package/src/compute-view/utils/resolve-extended-views.ts +65 -0
  62. package/src/compute-view/utils/resolve-global-rules.ts +83 -0
  63. package/src/compute-view/utils/sortNodes.ts +101 -0
  64. package/src/compute-view/utils/topological-sort.ts +160 -0
  65. package/src/compute-view/utils/uniqueTags.test.ts +42 -0
  66. package/src/compute-view/utils/uniqueTags.ts +21 -0
  67. package/src/compute-view/utils/view-hash.ts +27 -0
  68. package/src/compute-view/utils/with-readable-edges.ts +31 -0
  69. package/src/index.ts +5 -1
  70. package/src/model/DeploymentElementModel.ts +534 -0
  71. package/src/model/DeploymentModel.ts +364 -0
  72. package/src/model/ElementModel.ts +195 -0
  73. package/src/model/LikeC4Model.test-d.ts +151 -0
  74. package/src/model/LikeC4Model.ts +280 -457
  75. package/src/model/RelationModel.ts +84 -0
  76. package/src/model/__test__/fixture.ts +153 -338
  77. package/src/model/connection/ConnectionModel.ts +94 -0
  78. package/src/model/connection/DeploymentConnectionModel.ts +99 -0
  79. package/src/model/connection/deployment.ts +124 -0
  80. package/src/model/connection/model.ts +123 -0
  81. package/src/model/index.ts +4 -2
  82. package/src/model/types.ts +95 -15
  83. package/src/model/view/EdgeModel.ts +103 -0
  84. package/src/model/view/LikeC4ViewModel.ts +220 -0
  85. package/src/model/view/NodeModel.ts +228 -0
  86. package/src/types/_common.ts +2 -0
  87. package/src/types/deployments.ts +237 -0
  88. package/src/types/element.ts +7 -3
  89. package/src/types/expression.ts +8 -6
  90. package/src/types/index.ts +1 -0
  91. package/src/types/model.ts +68 -9
  92. package/src/types/overview-graph.ts +2 -2
  93. package/src/types/relation.ts +4 -4
  94. package/src/types/view-notation.ts +2 -2
  95. package/src/types/view.ts +104 -30
  96. package/src/utils/{commonHead.ts → common-head.ts} +2 -0
  97. package/src/utils/fqn.ts +83 -27
  98. package/src/utils/getOrCreate.ts +42 -0
  99. package/src/utils/graphlib.ts +10 -0
  100. package/src/utils/index.ts +4 -1
  101. package/src/utils/object-hash.ts +3 -0
  102. package/src/utils/set.ts +76 -0
  103. package/src/utils/string-hash.ts +16 -0
  104. package/dist/index.cjs +0 -5452
  105. package/dist/index.d.cts +0 -1053
  106. package/dist/shared/core.BXTs-8n7.mjs +0 -293
  107. package/dist/shared/core.CoLQkcYL.d.cts +0 -734
  108. package/dist/shared/core.DIYZvpVg.cjs +0 -353
  109. package/dist/types/index.cjs +0 -70
  110. package/dist/types/index.d.cts +0 -2
  111. package/src/model/LikeC4DiagramModel.ts +0 -338
  112. 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
+ }