@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,364 @@
|
|
|
1
|
+
import DefaultMap from 'mnemonist/default-map'
|
|
2
|
+
import { values } from 'remeda'
|
|
3
|
+
import type { LiteralUnion } from 'type-fest'
|
|
4
|
+
import { invariant, nonNullable } from '../errors'
|
|
5
|
+
import {
|
|
6
|
+
type AnyLikeC4Model,
|
|
7
|
+
type ComputedDeploymentView,
|
|
8
|
+
DeploymentElement,
|
|
9
|
+
type DeploymentRef,
|
|
10
|
+
type DeploymentRelation,
|
|
11
|
+
type Tag as C4Tag
|
|
12
|
+
} from '../types'
|
|
13
|
+
import { ancestorsFqn, parentFqn } from '../utils/fqn'
|
|
14
|
+
import { getOrCreate } from '../utils/getOrCreate'
|
|
15
|
+
import { isString } from '../utils/guards'
|
|
16
|
+
import {
|
|
17
|
+
DeployedInstanceModel,
|
|
18
|
+
type DeployedInstancesIterator,
|
|
19
|
+
DeploymentElementModel,
|
|
20
|
+
type DeploymentElementsIterator,
|
|
21
|
+
DeploymentNodeModel,
|
|
22
|
+
type DeploymentNodesIterator,
|
|
23
|
+
DeploymentRelationModel,
|
|
24
|
+
NestedElementOfDeployedInstanceModel
|
|
25
|
+
} from './DeploymentElementModel'
|
|
26
|
+
import type { LikeC4Model } from './LikeC4Model'
|
|
27
|
+
import { type AnyAux, getId, type IncomingFilter, type IteratorLike, type OutgoingFilter } from './types'
|
|
28
|
+
import type { LikeC4ViewModel } from './view/LikeC4ViewModel'
|
|
29
|
+
|
|
30
|
+
export class LikeC4DeploymentModel<M extends AnyAux = AnyAux> {
|
|
31
|
+
readonly #elements = new Map<M['Deployment'], DeploymentElementModel<M>>()
|
|
32
|
+
// Parent element for given FQN
|
|
33
|
+
readonly #parents = new Map<M['Deployment'], DeploymentNodeModel<M>>()
|
|
34
|
+
// Children elements for given FQN
|
|
35
|
+
readonly #children = new DefaultMap<M['Deployment'], Set<DeploymentElementModel<M>>>(() => new Set())
|
|
36
|
+
|
|
37
|
+
// Keep track of instances of the logical element
|
|
38
|
+
readonly #instancesOf = new DefaultMap<M['Element'], Set<DeployedInstanceModel<M>>>(() => new Set())
|
|
39
|
+
|
|
40
|
+
readonly #rootElements = new Set<DeploymentNodeModel<M>>()
|
|
41
|
+
|
|
42
|
+
readonly #relations = new Map<M['RelationId'], DeploymentRelationModel<M>>()
|
|
43
|
+
|
|
44
|
+
// Incoming to an element or its descendants
|
|
45
|
+
readonly #incoming = new DefaultMap<M['Deployment'], Set<DeploymentRelationModel<M>>>(() => new Set())
|
|
46
|
+
|
|
47
|
+
// Outgoing from an element or its descendants
|
|
48
|
+
readonly #outgoing = new DefaultMap<M['Deployment'], Set<DeploymentRelationModel<M>>>(() => new Set())
|
|
49
|
+
|
|
50
|
+
// Relationships inside the element, among descendants
|
|
51
|
+
readonly #internal = new DefaultMap<M['Deployment'], Set<DeploymentRelationModel<M>>>(() => new Set())
|
|
52
|
+
|
|
53
|
+
// readonly #views = new Map<ViewID, LikeC4ViewModel<M>>()
|
|
54
|
+
|
|
55
|
+
readonly #allTags = new DefaultMap<C4Tag, Set<DeploymentElementModel<M> | DeploymentRelationModel<M>>>(() =>
|
|
56
|
+
new Set()
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
readonly #nestedElementsOfDeployment = new Map<
|
|
60
|
+
`${M['Deployment']}@${M['Element']}`,
|
|
61
|
+
NestedElementOfDeployedInstanceModel<M>
|
|
62
|
+
>()
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
public readonly $model: LikeC4Model<M>,
|
|
66
|
+
public readonly $deployments: AnyLikeC4Model['deployments']
|
|
67
|
+
) {
|
|
68
|
+
for (const element of values($deployments.elements)) {
|
|
69
|
+
const el = this.addElement(element)
|
|
70
|
+
for (const tag of el.tags) {
|
|
71
|
+
this.#allTags.get(tag).add(el)
|
|
72
|
+
}
|
|
73
|
+
if (el.isInstance()) {
|
|
74
|
+
this.#instancesOf.get(el.$element.id).add(el)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const relation of values($deployments.relations)) {
|
|
78
|
+
const el = this.addRelation(relation)
|
|
79
|
+
for (const tag of el.tags) {
|
|
80
|
+
this.#allTags.get(tag).add(el)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public element(el: M['DeploymentOrFqn']): DeploymentElementModel<M> {
|
|
86
|
+
if (el instanceof DeploymentElementModel) {
|
|
87
|
+
return el
|
|
88
|
+
}
|
|
89
|
+
const id = getId(el)
|
|
90
|
+
return nonNullable(this.#elements.get(id), `Element ${id} not found`)
|
|
91
|
+
}
|
|
92
|
+
public findElement(el: M['Deployment']): DeploymentElementModel<M> | null {
|
|
93
|
+
return this.#elements.get(el) ?? null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public node(el: M['DeploymentOrFqn']): DeploymentNodeModel<M> {
|
|
97
|
+
const element = this.element(el)
|
|
98
|
+
invariant(element.isDeploymentNode(), `Element ${element.id} is not a deployment node`)
|
|
99
|
+
return element
|
|
100
|
+
}
|
|
101
|
+
public findNode(el: M['Deployment']): DeploymentNodeModel<M> | null {
|
|
102
|
+
const element = this.findElement(el)
|
|
103
|
+
if (!element) {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
invariant(element.isDeploymentNode(), `Element ${element?.id} is not a deployment node`)
|
|
107
|
+
return element
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public instance(el: M['DeploymentOrFqn']): DeployedInstanceModel<M> {
|
|
111
|
+
const element = this.element(el)
|
|
112
|
+
invariant(element.isInstance(), `Element ${element.id} is not a deployed instance`)
|
|
113
|
+
return element
|
|
114
|
+
}
|
|
115
|
+
public findInstance(el: M['Deployment']): DeployedInstanceModel<M> | null {
|
|
116
|
+
const element = this.findElement(el)
|
|
117
|
+
if (!element) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
invariant(element.isInstance(), `Element ${element?.id} is not a deployed instance`)
|
|
121
|
+
return element
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Returns the root elements of the model.
|
|
126
|
+
*/
|
|
127
|
+
public roots(): DeploymentNodesIterator<M> {
|
|
128
|
+
return this.#rootElements.values()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns all elements in the model.
|
|
133
|
+
*/
|
|
134
|
+
public elements(): DeploymentElementsIterator<M> {
|
|
135
|
+
return this.#elements.values()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns all elements in the model.
|
|
140
|
+
*/
|
|
141
|
+
public *nodes(): DeploymentNodesIterator<M> {
|
|
142
|
+
for (const element of this.#elements.values()) {
|
|
143
|
+
if (element.isDeploymentNode()) {
|
|
144
|
+
yield element
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public *instances(): DeployedInstancesIterator<M> {
|
|
151
|
+
for (const element of this.#elements.values()) {
|
|
152
|
+
if (element.isInstance()) {
|
|
153
|
+
yield element
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Iterate over all instances of the given logical element.
|
|
161
|
+
*/
|
|
162
|
+
public *instancesOf(element: M['ElementOrFqn']): DeployedInstancesIterator<M> {
|
|
163
|
+
const id = getId(element)
|
|
164
|
+
const instances = this.#instancesOf.get(id)
|
|
165
|
+
if (instances) {
|
|
166
|
+
yield* instances
|
|
167
|
+
}
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public deploymentRef(ref: DeploymentRef): DeploymentElementModel<M> | NestedElementOfDeployedInstanceModel<M> {
|
|
172
|
+
if ('element' in ref) {
|
|
173
|
+
const { id, element } = ref
|
|
174
|
+
return getOrCreate(this.#nestedElementsOfDeployment, `${id}@${element}`, () => {
|
|
175
|
+
return new NestedElementOfDeployedInstanceModel(this.instance(id), this.$model.element(element))
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
return this.element(ref)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Returns all relationships in the model.
|
|
183
|
+
*/
|
|
184
|
+
public relationships(): IteratorLike<DeploymentRelationModel<M>> {
|
|
185
|
+
return this.#relations.values()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns a specific relationship by its ID.
|
|
190
|
+
*/
|
|
191
|
+
public relationship(id: M['RelationId']): DeploymentRelationModel<M> {
|
|
192
|
+
return nonNullable(this.#relations.get(id), `DeploymentRelationModel ${id} not found`)
|
|
193
|
+
}
|
|
194
|
+
public findRelationship(id: LiteralUnion<M['RelationId'], string>): DeploymentRelationModel<M> | null {
|
|
195
|
+
return this.#relations.get(id as M['RelationId']) ?? null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns all deployment views in the model.
|
|
200
|
+
*/
|
|
201
|
+
public *views(): IteratorLike<LikeC4ViewModel<M, ComputedDeploymentView>> {
|
|
202
|
+
for (const view of this.$model.views()) {
|
|
203
|
+
if (view.isDeploymentView()) {
|
|
204
|
+
yield view
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns the parent element of given element.
|
|
212
|
+
* @see ancestors
|
|
213
|
+
*/
|
|
214
|
+
public parent(element: M['DeploymentOrFqn']): DeploymentNodeModel<M> | null {
|
|
215
|
+
const id = getId(element)
|
|
216
|
+
return this.#parents.get(id) || null
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Get all children of the element (only direct children),
|
|
221
|
+
* @see descendants
|
|
222
|
+
*/
|
|
223
|
+
public children(element: M['DeploymentOrFqn']): ReadonlySet<DeploymentElementModel<M>> {
|
|
224
|
+
const id = getId(element)
|
|
225
|
+
return this.#children.get(id)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get all sibling (i.e. same parent)
|
|
230
|
+
*/
|
|
231
|
+
public *siblings(element: M['DeploymentOrFqn']): DeploymentElementsIterator<M> {
|
|
232
|
+
const id = getId(element)
|
|
233
|
+
const siblings = this.parent(element)?.children() ?? this.roots()
|
|
234
|
+
for (const sibling of siblings) {
|
|
235
|
+
if (sibling.id !== id) {
|
|
236
|
+
yield sibling
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get all ancestor elements (i.e. parent, parent’s parent, etc.)
|
|
244
|
+
* (from closest to root)
|
|
245
|
+
*/
|
|
246
|
+
public *ancestors(element: M['DeploymentOrFqn']): DeploymentNodesIterator<M> {
|
|
247
|
+
let id = isString(element) ? element : element.id
|
|
248
|
+
let parent
|
|
249
|
+
while (parent = this.#parents.get(id)) {
|
|
250
|
+
yield parent
|
|
251
|
+
id = parent.id
|
|
252
|
+
}
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get all descendant elements (i.e. children, children’s children, etc.)
|
|
258
|
+
*/
|
|
259
|
+
public *descendants(element: M['DeploymentOrFqn']): DeploymentElementsIterator<M> {
|
|
260
|
+
for (const child of this.children(element)) {
|
|
261
|
+
yield child
|
|
262
|
+
yield* this.descendants(child.id)
|
|
263
|
+
}
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Incoming relationships to the element and its descendants
|
|
269
|
+
* @see incomers
|
|
270
|
+
*/
|
|
271
|
+
public *incoming(
|
|
272
|
+
element: M['DeploymentOrFqn'],
|
|
273
|
+
filter: IncomingFilter = 'all'
|
|
274
|
+
): IteratorLike<DeploymentRelationModel<M>> {
|
|
275
|
+
const id = getId(element)
|
|
276
|
+
for (const rel of this.#incoming.get(id)) {
|
|
277
|
+
switch (true) {
|
|
278
|
+
case filter === 'all':
|
|
279
|
+
case filter === 'direct' && rel.target.id === id:
|
|
280
|
+
case filter === 'to-descendants' && rel.target.id !== id:
|
|
281
|
+
yield rel
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Outgoing relationships from the element and its descendants
|
|
290
|
+
* @see outgoers
|
|
291
|
+
*/
|
|
292
|
+
public *outgoing(
|
|
293
|
+
element: M['DeploymentOrFqn'],
|
|
294
|
+
filter: OutgoingFilter = 'all'
|
|
295
|
+
): IteratorLike<DeploymentRelationModel<M>> {
|
|
296
|
+
const id = getId(element)
|
|
297
|
+
for (const rel of this.#outgoing.get(id)) {
|
|
298
|
+
switch (true) {
|
|
299
|
+
case filter === 'all':
|
|
300
|
+
case filter === 'direct' && rel.source.id === id:
|
|
301
|
+
case filter === 'from-descendants' && rel.source.id !== id:
|
|
302
|
+
yield rel
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private addElement(element: DeploymentElement) {
|
|
310
|
+
if (this.#elements.has(element.id)) {
|
|
311
|
+
throw new Error(`Element ${element.id} already exists`)
|
|
312
|
+
}
|
|
313
|
+
const el = DeploymentElement.isDeploymentNode(element)
|
|
314
|
+
? new DeploymentNodeModel(this, element)
|
|
315
|
+
: new DeployedInstanceModel(this, element, this.$model.element(element.element))
|
|
316
|
+
this.#elements.set(el.id, el)
|
|
317
|
+
const parentId = parentFqn(el.id)
|
|
318
|
+
if (parentId) {
|
|
319
|
+
invariant(this.#elements.has(parentId), `Parent ${parentId} of ${el.id} not found`)
|
|
320
|
+
this.#parents.set(el.id, this.node(parentId))
|
|
321
|
+
this.#children.get(parentId).add(el)
|
|
322
|
+
} else {
|
|
323
|
+
invariant(el.isDeploymentNode(), `Root element ${el.id} is not a deployment node`)
|
|
324
|
+
this.#rootElements.add(el)
|
|
325
|
+
}
|
|
326
|
+
return el
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private addRelation(relation: DeploymentRelation) {
|
|
330
|
+
if (this.#relations.has(relation.id)) {
|
|
331
|
+
throw new Error(`Relation ${relation.id} already exists`)
|
|
332
|
+
}
|
|
333
|
+
const rel = new DeploymentRelationModel(
|
|
334
|
+
this,
|
|
335
|
+
relation
|
|
336
|
+
)
|
|
337
|
+
this.#relations.set(rel.id, rel)
|
|
338
|
+
this.#incoming.get(rel.target.id).add(rel)
|
|
339
|
+
this.#outgoing.get(rel.source.id).add(rel)
|
|
340
|
+
|
|
341
|
+
const relParent = rel.boundary?.id ?? null
|
|
342
|
+
// Process internal relationships
|
|
343
|
+
if (relParent) {
|
|
344
|
+
for (const ancestor of [relParent, ...ancestorsFqn(relParent)]) {
|
|
345
|
+
this.#internal.get(ancestor).add(rel)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Process source hierarchy
|
|
349
|
+
for (const sourceAncestor of ancestorsFqn(rel.source.id)) {
|
|
350
|
+
if (sourceAncestor === relParent) {
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
this.#outgoing.get(sourceAncestor).add(rel)
|
|
354
|
+
}
|
|
355
|
+
// Process target hierarchy
|
|
356
|
+
for (const targetAncestor of ancestorsFqn(rel.target.id)) {
|
|
357
|
+
if (targetAncestor === relParent) {
|
|
358
|
+
break
|
|
359
|
+
}
|
|
360
|
+
this.#incoming.get(targetAncestor).add(rel)
|
|
361
|
+
}
|
|
362
|
+
return rel
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { isTruthy } from 'remeda'
|
|
2
|
+
import {
|
|
3
|
+
DefaultElementShape,
|
|
4
|
+
DefaultThemeColor,
|
|
5
|
+
type Element as C4Element,
|
|
6
|
+
type ElementKind as C4ElementKind,
|
|
7
|
+
type ElementShape as C4ElementShape,
|
|
8
|
+
type Link,
|
|
9
|
+
type Tag as C4Tag,
|
|
10
|
+
type ThemeColor
|
|
11
|
+
} from '../types'
|
|
12
|
+
import { commonAncestor } from '../utils'
|
|
13
|
+
import { type DeployedInstancesIterator } from './DeploymentElementModel'
|
|
14
|
+
import type { LikeC4Model } from './LikeC4Model'
|
|
15
|
+
import type { RelationshipModel, RelationshipsIterator } from './RelationModel'
|
|
16
|
+
import type { AnyAux, IncomingFilter, IteratorLike, OutgoingFilter } from './types'
|
|
17
|
+
import type { LikeC4ViewModel, ViewsIterator } from './view/LikeC4ViewModel'
|
|
18
|
+
|
|
19
|
+
export type ElementsIterator<M extends AnyAux> = IteratorLike<ElementModel<M>>
|
|
20
|
+
|
|
21
|
+
export class ElementModel<M extends AnyAux> {
|
|
22
|
+
constructor(
|
|
23
|
+
public readonly model: LikeC4Model<M>,
|
|
24
|
+
public readonly $element: C4Element
|
|
25
|
+
) {
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get id(): M['Fqn'] {
|
|
29
|
+
return this.$element.id
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get kind(): C4ElementKind {
|
|
33
|
+
return this.$element.kind
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get shape(): C4ElementShape {
|
|
37
|
+
return this.$element.shape ?? DefaultElementShape
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get color(): ThemeColor {
|
|
41
|
+
return this.$element.color as ThemeColor ?? DefaultThemeColor
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get tags(): ReadonlyArray<C4Tag> {
|
|
45
|
+
return this.$element.tags ?? []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get title(): string {
|
|
49
|
+
return this.$element.title
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get description(): string | null {
|
|
53
|
+
return this.$element.description
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get technology(): string | null {
|
|
57
|
+
return this.$element.technology
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get links(): ReadonlyArray<Link> {
|
|
61
|
+
return this.$element.links ?? []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get parent(): ElementModel<M> | null {
|
|
65
|
+
return this.model.parent(this)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get defaultView(): LikeC4ViewModel<M> | null {
|
|
69
|
+
return this.scopedViews().next().value ?? null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all ancestor elements (i.e. parent, parent’s parent, etc.)
|
|
74
|
+
* (from closest to root)
|
|
75
|
+
*/
|
|
76
|
+
public ancestors(): ElementsIterator<M> {
|
|
77
|
+
return this.model.ancestors(this)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the common ancestor of this element and another element.
|
|
82
|
+
*/
|
|
83
|
+
public commonAncestor(another: ElementModel<M>): ElementModel<M> | null {
|
|
84
|
+
const common = commonAncestor(this.id, another.id)
|
|
85
|
+
return common ? this.model.element(common) : null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public children(): ReadonlySet<ElementModel<M>> {
|
|
89
|
+
return this.model.children(this)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get all descendant elements (i.e. children, children’s children, etc.)
|
|
94
|
+
*/
|
|
95
|
+
public descendants(): ElementsIterator<M> {
|
|
96
|
+
return this.model.descendants(this)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get all sibling (i.e. same parent)
|
|
101
|
+
*/
|
|
102
|
+
public siblings(): ElementsIterator<M> {
|
|
103
|
+
return this.model.siblings(this)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve siblings of the element and its ancestors
|
|
108
|
+
* (from closest to root)
|
|
109
|
+
*/
|
|
110
|
+
public *ascendingSiblings(): ElementsIterator<M> {
|
|
111
|
+
yield* this.siblings()
|
|
112
|
+
for (const ancestor of this.ancestors()) {
|
|
113
|
+
yield* ancestor.siblings()
|
|
114
|
+
}
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public incoming(filter: IncomingFilter = 'all'): RelationshipsIterator<M> {
|
|
119
|
+
return this.model.incoming(this, filter)
|
|
120
|
+
}
|
|
121
|
+
public *incomers(filter: IncomingFilter = 'all'): ElementsIterator<M> {
|
|
122
|
+
const unique = new Set<M['Fqn']>()
|
|
123
|
+
for (const r of this.incoming(filter)) {
|
|
124
|
+
if (unique.has(r.source.id)) {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
unique.add(r.source.id)
|
|
128
|
+
yield r.source
|
|
129
|
+
}
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
public outgoing(filter: OutgoingFilter = 'all'): RelationshipsIterator<M> {
|
|
133
|
+
return this.model.outgoing(this, filter)
|
|
134
|
+
}
|
|
135
|
+
public *outgoers(filter: OutgoingFilter = 'all'): ElementsIterator<M> {
|
|
136
|
+
const unique = new Set<M['Fqn']>()
|
|
137
|
+
for (const r of this.outgoing(filter)) {
|
|
138
|
+
if (unique.has(r.target.id)) {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
unique.add(r.target.id)
|
|
142
|
+
yield r.target
|
|
143
|
+
}
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected cachedOutgoing: Set<RelationshipModel<M>> | null = null
|
|
148
|
+
protected cachedIncoming: Set<RelationshipModel<M>> | null = null
|
|
149
|
+
|
|
150
|
+
public get allOutgoing(): ReadonlySet<RelationshipModel<M>> {
|
|
151
|
+
this.cachedOutgoing ??= new Set(this.outgoing())
|
|
152
|
+
return this.cachedOutgoing
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public get allIncoming(): ReadonlySet<RelationshipModel<M>> {
|
|
156
|
+
this.cachedIncoming ??= new Set(this.incoming())
|
|
157
|
+
return this.cachedIncoming
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Iterate over all views that include this element.
|
|
162
|
+
*/
|
|
163
|
+
public *views(): ViewsIterator<M> {
|
|
164
|
+
for (const view of this.model.views()) {
|
|
165
|
+
if (view.includesElement(this.id)) {
|
|
166
|
+
yield view
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Iterate over all views that scope this element.
|
|
174
|
+
* It is possible that element is not included in the view.
|
|
175
|
+
*/
|
|
176
|
+
public *scopedViews(): ViewsIterator<M> {
|
|
177
|
+
for (const vm of this.model.views()) {
|
|
178
|
+
if (vm.isElementView() && vm.$view.viewOf === this.id) {
|
|
179
|
+
yield vm
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @returns true if the element is deployed
|
|
187
|
+
*/
|
|
188
|
+
public isDeployed(): boolean {
|
|
189
|
+
return isTruthy(this.deployments().next().value)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public deployments(): DeployedInstancesIterator<M> {
|
|
193
|
+
return this.model.deployment.instancesOf(this)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { expectTypeOf, test } from 'vitest'
|
|
2
|
+
import { Builder } from '../builder'
|
|
3
|
+
import { computeViews } from '../compute-view/compute-view'
|
|
4
|
+
import type { ComputedView, Fqn, ViewId } from '../types'
|
|
5
|
+
import { LikeC4Model } from './LikeC4Model'
|
|
6
|
+
|
|
7
|
+
test('LikeC4Model.create: should have types', () => {
|
|
8
|
+
const {
|
|
9
|
+
model: {
|
|
10
|
+
model,
|
|
11
|
+
actor,
|
|
12
|
+
system,
|
|
13
|
+
component,
|
|
14
|
+
relTo
|
|
15
|
+
},
|
|
16
|
+
deployment: {
|
|
17
|
+
deployment,
|
|
18
|
+
instanceOf,
|
|
19
|
+
env,
|
|
20
|
+
vm
|
|
21
|
+
},
|
|
22
|
+
views: {
|
|
23
|
+
deploymentView,
|
|
24
|
+
views,
|
|
25
|
+
view,
|
|
26
|
+
$include
|
|
27
|
+
},
|
|
28
|
+
builder
|
|
29
|
+
} = Builder.forSpecification({
|
|
30
|
+
elements: {
|
|
31
|
+
actor: {
|
|
32
|
+
style: {
|
|
33
|
+
shape: 'person'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
system: {},
|
|
37
|
+
component: {}
|
|
38
|
+
},
|
|
39
|
+
deployments: {
|
|
40
|
+
env: {},
|
|
41
|
+
vm: {}
|
|
42
|
+
},
|
|
43
|
+
relationships: {
|
|
44
|
+
like: {},
|
|
45
|
+
dislike: {}
|
|
46
|
+
},
|
|
47
|
+
tags: ['tag1', 'tag2', 'tag1']
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const source = builder
|
|
51
|
+
.with(
|
|
52
|
+
model(
|
|
53
|
+
actor('alice'),
|
|
54
|
+
actor('bob'),
|
|
55
|
+
system('cloud').with(
|
|
56
|
+
component('backend').with(
|
|
57
|
+
component('api'),
|
|
58
|
+
component('db')
|
|
59
|
+
),
|
|
60
|
+
component('frontend').with(
|
|
61
|
+
relTo('cloud.backend.api')
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
),
|
|
65
|
+
deployment(
|
|
66
|
+
env('prod').with(
|
|
67
|
+
vm('vm1'),
|
|
68
|
+
vm('vm2')
|
|
69
|
+
),
|
|
70
|
+
env('dev').with(
|
|
71
|
+
vm('vm1'),
|
|
72
|
+
instanceOf('api', 'cloud.backend.api'),
|
|
73
|
+
vm('vm2')
|
|
74
|
+
)
|
|
75
|
+
),
|
|
76
|
+
views(
|
|
77
|
+
view('index', $include('*')),
|
|
78
|
+
deploymentView('prodview', $include('*'))
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
.build()
|
|
82
|
+
|
|
83
|
+
const computed = computeViews(source)
|
|
84
|
+
const m = LikeC4Model.create(computed)
|
|
85
|
+
|
|
86
|
+
expectTypeOf(m.Aux.Element).toEqualTypeOf(
|
|
87
|
+
'' as 'alice' | 'bob' | 'cloud' | 'cloud.backend' | 'cloud.backend.api' | 'cloud.backend.db' | 'cloud.frontend'
|
|
88
|
+
)
|
|
89
|
+
expectTypeOf(m.Aux.Fqn).toEqualTypeOf(
|
|
90
|
+
'' as Fqn<'alice' | 'bob' | 'cloud' | 'cloud.backend' | 'cloud.backend.api' | 'cloud.backend.db' | 'cloud.frontend'>
|
|
91
|
+
)
|
|
92
|
+
expectTypeOf(m.Aux.ViewId).toEqualTypeOf(
|
|
93
|
+
'' as ViewId<'index' | 'prodview'>
|
|
94
|
+
)
|
|
95
|
+
expectTypeOf(m.Aux.ViewType).toEqualTypeOf(
|
|
96
|
+
{} as ComputedView<ViewId<'index' | 'prodview'>>
|
|
97
|
+
)
|
|
98
|
+
expectTypeOf(m.Aux.Deployment).toEqualTypeOf(
|
|
99
|
+
'' as 'prod' | 'dev' | 'prod.vm1' | 'prod.vm2' | 'dev.vm1' | 'dev.vm2' | 'dev.api'
|
|
100
|
+
)
|
|
101
|
+
expectTypeOf(m.Aux.DeploymentFqn).toEqualTypeOf(
|
|
102
|
+
'' as Fqn<'prod' | 'dev' | 'prod.vm1' | 'prod.vm2' | 'dev.vm1' | 'dev.vm2' | 'dev.api'>
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('LikeC4Model.fromDump: should have types', () => {
|
|
107
|
+
const m = LikeC4Model.fromDump({
|
|
108
|
+
specification: {
|
|
109
|
+
tags: [],
|
|
110
|
+
elements: {},
|
|
111
|
+
relationships: {},
|
|
112
|
+
deployments: {}
|
|
113
|
+
},
|
|
114
|
+
elements: {
|
|
115
|
+
el1: {},
|
|
116
|
+
el2: {}
|
|
117
|
+
},
|
|
118
|
+
relations: {},
|
|
119
|
+
globals: {
|
|
120
|
+
predicates: {},
|
|
121
|
+
dynamicPredicates: {},
|
|
122
|
+
styles: {}
|
|
123
|
+
},
|
|
124
|
+
views: {
|
|
125
|
+
v1: {},
|
|
126
|
+
v2: {}
|
|
127
|
+
},
|
|
128
|
+
deployments: {
|
|
129
|
+
elements: {
|
|
130
|
+
d1: {},
|
|
131
|
+
d2: {}
|
|
132
|
+
},
|
|
133
|
+
relations: {}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
expectTypeOf(m.Aux.Element).toEqualTypeOf(
|
|
137
|
+
'' as 'el1' | 'el2'
|
|
138
|
+
)
|
|
139
|
+
expectTypeOf(m.Aux.Fqn).toEqualTypeOf(
|
|
140
|
+
'' as Fqn<'el1' | 'el2'>
|
|
141
|
+
)
|
|
142
|
+
expectTypeOf(m.Aux.ViewId).toEqualTypeOf(
|
|
143
|
+
'' as ViewId<'v1' | 'v2'>
|
|
144
|
+
)
|
|
145
|
+
expectTypeOf(m.Aux.Deployment).toEqualTypeOf(
|
|
146
|
+
'' as 'd1' | 'd2'
|
|
147
|
+
)
|
|
148
|
+
expectTypeOf(m.Aux.DeploymentFqn).toEqualTypeOf(
|
|
149
|
+
'' as Fqn<'d1' | 'd2'>
|
|
150
|
+
)
|
|
151
|
+
})
|