@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,330 @@
|
|
|
1
|
+
import { isArray, isString, reverse } from 'remeda'
|
|
2
|
+
import { invariant } from '../errors'
|
|
3
|
+
import { type Element, type Fqn, type ModelGlobals, type Relation, type RelationId } from '../types'
|
|
4
|
+
import { ancestorsFqn, commonAncestor, getOrCreate, isSameHierarchy, parentFqn } from '../utils'
|
|
5
|
+
import { intersection } from '../utils/set'
|
|
6
|
+
|
|
7
|
+
export type Params = {
|
|
8
|
+
elements: Record<Fqn, Element>
|
|
9
|
+
relations: Record<RelationId, Relation>
|
|
10
|
+
// Optional for tests
|
|
11
|
+
globals?: ModelGlobals
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type RelationEdge = {
|
|
15
|
+
source: Element
|
|
16
|
+
target: Element
|
|
17
|
+
relations: Relation[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type FqnOrElement = Fqn | Element
|
|
21
|
+
type FqnsOrElements = ReadonlyArray<Fqn> | ReadonlyArray<Element>
|
|
22
|
+
|
|
23
|
+
const RelationsSet = Set<Relation>
|
|
24
|
+
const MapRelations = Map<Fqn, Set<Relation>>
|
|
25
|
+
/**
|
|
26
|
+
* Used only for views calculations.
|
|
27
|
+
* Subject to change.
|
|
28
|
+
*/
|
|
29
|
+
export class LikeC4ModelGraph {
|
|
30
|
+
readonly #elements = new Map<Fqn, Element>()
|
|
31
|
+
// Parent element for given FQN
|
|
32
|
+
readonly #parents = new Map<Fqn, Element>()
|
|
33
|
+
// Children elements for given FQN
|
|
34
|
+
readonly #children = new Map<Fqn, Element[]>()
|
|
35
|
+
readonly #rootElements = new Set<Element>()
|
|
36
|
+
|
|
37
|
+
readonly #relations = new Map<RelationId, Relation>()
|
|
38
|
+
// Incoming to an element or its descendants
|
|
39
|
+
readonly #incoming = new MapRelations()
|
|
40
|
+
// Outgoing from an element or its descendants
|
|
41
|
+
readonly #outgoing = new MapRelations()
|
|
42
|
+
// Relationships inside the element, among descendants
|
|
43
|
+
readonly #internal = new MapRelations()
|
|
44
|
+
|
|
45
|
+
readonly #cacheAscendingSiblings = new Map<Fqn, Element[]>()
|
|
46
|
+
|
|
47
|
+
public readonly globals: ModelGlobals
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
{ elements, relations, globals }: Params
|
|
51
|
+
) {
|
|
52
|
+
this.globals = globals ?? {
|
|
53
|
+
predicates: {},
|
|
54
|
+
dynamicPredicates: {},
|
|
55
|
+
styles: {}
|
|
56
|
+
}
|
|
57
|
+
for (const el of Object.values(elements)) {
|
|
58
|
+
this.addElement(el)
|
|
59
|
+
}
|
|
60
|
+
for (const rel of Object.values(relations)) {
|
|
61
|
+
this.addRelation(rel)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get rootElements() {
|
|
66
|
+
return [...this.#rootElements]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get elements() {
|
|
70
|
+
return [...this.#elements.values()]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public element(id: Fqn) {
|
|
74
|
+
const el = this.#elements.get(id)
|
|
75
|
+
invariant(el, `Element ${id} not found`)
|
|
76
|
+
return el
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public connectedRelations(id: Fqn) {
|
|
80
|
+
return [...this._incomingTo(id), ...this._outgoingFrom(id), ...this._internalOf(id)]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public children(id: Fqn) {
|
|
84
|
+
return this._childrenOf(id).slice()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public descendants(id: Fqn) {
|
|
88
|
+
const result: Element[] = []
|
|
89
|
+
this.descendantsDfs(id, result)
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private descendantsDfs(id: Fqn, result: Element[]) {
|
|
94
|
+
result.push(this.element(id))
|
|
95
|
+
this.children(id).forEach(c => this.descendantsDfs(c.id, result))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get all sibling (i.e. same parent)
|
|
99
|
+
public siblings(element: Fqn | Element) {
|
|
100
|
+
const id = isString(element) ? element : element.id
|
|
101
|
+
const parent = parentFqn(id)
|
|
102
|
+
const siblings = parent ? this._childrenOf(parent) : this.rootElements
|
|
103
|
+
return siblings.filter(e => e.id !== id)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get all ancestor elements (i.e. parent, parent’s parent, etc.)
|
|
108
|
+
* (from closest to root)
|
|
109
|
+
*/
|
|
110
|
+
public ancestors(element: Fqn | Element): Array<Element> {
|
|
111
|
+
let id = isString(element) ? element : element.id
|
|
112
|
+
const result = [] as Element[]
|
|
113
|
+
let parent
|
|
114
|
+
while (parent = this.#parents.get(id)) {
|
|
115
|
+
result.push(parent)
|
|
116
|
+
id = parent.id
|
|
117
|
+
}
|
|
118
|
+
return result as Array<Element>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve siblings of the element and its ancestors
|
|
123
|
+
* (from closest to root)
|
|
124
|
+
*/
|
|
125
|
+
public ascendingSiblings(element: Fqn | Element): ReadonlyArray<Element> {
|
|
126
|
+
const id = isString(element) ? element : element.id
|
|
127
|
+
return getOrCreate(this.#cacheAscendingSiblings, id, () => [
|
|
128
|
+
...this.siblings(id),
|
|
129
|
+
...this.ancestors(id).flatMap(a => this.siblings(a.id))
|
|
130
|
+
])
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public incoming(element: Fqn | Element): ReadonlySet<Relation> {
|
|
134
|
+
return this._incomingTo(isString(element) ? element : element.id)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public outgoing(element: Fqn | Element): ReadonlySet<Relation> {
|
|
138
|
+
return this._outgoingFrom(isString(element) ? element : element.id)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolve all RelationEdges between element and others (any direction)
|
|
143
|
+
*/
|
|
144
|
+
public anyEdgesBetween(
|
|
145
|
+
_element: Fqn | Element,
|
|
146
|
+
others: ReadonlyArray<Fqn> | ReadonlyArray<Element>
|
|
147
|
+
): Array<RelationEdge> {
|
|
148
|
+
if (others.length === 0) {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
const element = isString(_element) ? this.element(_element) : _element
|
|
152
|
+
const in_element = this._incomingTo(element.id)
|
|
153
|
+
const element_out = this._outgoingFrom(element.id)
|
|
154
|
+
if (in_element.size === 0 && element_out.size === 0) {
|
|
155
|
+
return []
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const result = [] as Array<RelationEdge>
|
|
159
|
+
for (const _other of others) {
|
|
160
|
+
const other = isString(_other) ? this.element(_other) : _other
|
|
161
|
+
if (isSameHierarchy(element, other)) {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (element_out.size > 0) {
|
|
166
|
+
const outcoming = intersection(this._incomingTo(other.id), element_out)
|
|
167
|
+
// const outcoming = filter(in_other, isOutgoing)
|
|
168
|
+
if (outcoming.size > 0) {
|
|
169
|
+
result.push({
|
|
170
|
+
source: element,
|
|
171
|
+
target: other,
|
|
172
|
+
relations: [...outcoming]
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (in_element.size > 0) {
|
|
178
|
+
const incoming = intersection(this._outgoingFrom(other.id), in_element)
|
|
179
|
+
// const incoming = filter(other_out, isIncoming)
|
|
180
|
+
if (incoming.size > 0) {
|
|
181
|
+
result.push({
|
|
182
|
+
source: other,
|
|
183
|
+
target: element,
|
|
184
|
+
relations: [...incoming]
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return result
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolve all RelationEdges between elements (any direction)
|
|
194
|
+
*/
|
|
195
|
+
public edgesWithin<T extends Fqn[] | Element[]>(elements: T): Array<RelationEdge> {
|
|
196
|
+
if (elements.length < 2) {
|
|
197
|
+
return []
|
|
198
|
+
}
|
|
199
|
+
return elements.reduce((acc, el, index, array) => {
|
|
200
|
+
// return acc if last element
|
|
201
|
+
if (index === array.length - 1) {
|
|
202
|
+
return acc
|
|
203
|
+
}
|
|
204
|
+
acc.push(...this.anyEdgesBetween(el, array.slice(index + 1) as T))
|
|
205
|
+
return acc
|
|
206
|
+
}, [] as RelationEdge[])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get directed RelationEdge between source and target if exists
|
|
211
|
+
*/
|
|
212
|
+
public edgesBetween(
|
|
213
|
+
_sources: FqnOrElement | FqnsOrElements,
|
|
214
|
+
_targets: FqnOrElement | FqnsOrElements
|
|
215
|
+
) {
|
|
216
|
+
const sources = isArray(_sources) ? _sources : [_sources]
|
|
217
|
+
const targets = isArray(_targets) ? _targets : [_targets]
|
|
218
|
+
if (sources.length === 0 || targets.length === 0) {
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const result = [] as Array<RelationEdge>
|
|
223
|
+
for (const _source of sources) {
|
|
224
|
+
const source = isString(_source) ? this.element(_source) : _source
|
|
225
|
+
const outcoming = this._outgoingFrom(source.id)
|
|
226
|
+
if (outcoming.size === 0) {
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
// const isSameAsOut = isIncludedIn(outcoming)
|
|
230
|
+
|
|
231
|
+
for (const _target of targets) {
|
|
232
|
+
const target = isString(_target) ? this.element(_target) : _target
|
|
233
|
+
if (isSameHierarchy(source, target)) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
const incoming = this._incomingTo(target.id)
|
|
237
|
+
const relations = intersection(outcoming, incoming)
|
|
238
|
+
if (relations.size > 0) {
|
|
239
|
+
result.push({
|
|
240
|
+
source,
|
|
241
|
+
target,
|
|
242
|
+
relations: [...relations]
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return result
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private addElement(el: Element) {
|
|
251
|
+
if (this.#elements.has(el.id)) {
|
|
252
|
+
throw new Error(`Element ${el.id} already exists`)
|
|
253
|
+
}
|
|
254
|
+
this.#elements.set(el.id, el)
|
|
255
|
+
const parentId = parentFqn(el.id)
|
|
256
|
+
if (parentId) {
|
|
257
|
+
this.#parents.set(el.id, this.element(parentId))
|
|
258
|
+
this._childrenOf(parentId).push(el)
|
|
259
|
+
} else {
|
|
260
|
+
this.#rootElements.add(el)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private addRelation(rel: Relation) {
|
|
265
|
+
if (this.#relations.has(rel.id)) {
|
|
266
|
+
throw new Error(`Relation ${rel.id} already exists`)
|
|
267
|
+
}
|
|
268
|
+
this.#relations.set(rel.id, rel)
|
|
269
|
+
this._incomingTo(rel.target).add(rel)
|
|
270
|
+
this._outgoingFrom(rel.source).add(rel)
|
|
271
|
+
|
|
272
|
+
const relParent = commonAncestor(rel.source, rel.target)
|
|
273
|
+
// Process internal relationships
|
|
274
|
+
if (relParent) {
|
|
275
|
+
for (const ancestor of [relParent, ...ancestorsFqn(relParent)]) {
|
|
276
|
+
this._internalOf(ancestor).add(rel)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Process source hierarchy
|
|
280
|
+
for (const sourceAncestor of ancestorsFqn(rel.source)) {
|
|
281
|
+
if (sourceAncestor === relParent) {
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
this._outgoingFrom(sourceAncestor).add(rel)
|
|
285
|
+
}
|
|
286
|
+
// Process target hierarchy
|
|
287
|
+
for (const targetAncestor of ancestorsFqn(rel.target)) {
|
|
288
|
+
if (targetAncestor === relParent) {
|
|
289
|
+
break
|
|
290
|
+
}
|
|
291
|
+
this._incomingTo(targetAncestor).add(rel)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private _childrenOf(id: Fqn) {
|
|
296
|
+
let children = this.#children.get(id)
|
|
297
|
+
if (!children) {
|
|
298
|
+
children = []
|
|
299
|
+
this.#children.set(id, children)
|
|
300
|
+
}
|
|
301
|
+
return children
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private _incomingTo(id: Fqn) {
|
|
305
|
+
let incoming = this.#incoming.get(id)
|
|
306
|
+
if (!incoming) {
|
|
307
|
+
incoming = new RelationsSet()
|
|
308
|
+
this.#incoming.set(id, incoming)
|
|
309
|
+
}
|
|
310
|
+
return incoming
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private _outgoingFrom(id: Fqn) {
|
|
314
|
+
let outgoing = this.#outgoing.get(id)
|
|
315
|
+
if (!outgoing) {
|
|
316
|
+
outgoing = new RelationsSet()
|
|
317
|
+
this.#outgoing.set(id, outgoing)
|
|
318
|
+
}
|
|
319
|
+
return outgoing
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private _internalOf(id: Fqn) {
|
|
323
|
+
let internal = this.#internal.get(id)
|
|
324
|
+
if (!internal) {
|
|
325
|
+
internal = new RelationsSet()
|
|
326
|
+
this.#internal.set(id, internal)
|
|
327
|
+
}
|
|
328
|
+
return internal
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mapValues } from 'remeda'
|
|
2
|
+
import type { SetOptional } from 'type-fest'
|
|
3
|
+
import { nonexhaustive } from '../errors'
|
|
4
|
+
import { LikeC4Model } from '../model'
|
|
5
|
+
import {
|
|
6
|
+
type ComputedLikeC4ModelFromParsed,
|
|
7
|
+
type ComputedView,
|
|
8
|
+
isDeploymentView,
|
|
9
|
+
isDynamicView,
|
|
10
|
+
isElementView,
|
|
11
|
+
type LikeC4View,
|
|
12
|
+
type ParsedLikeC4Model
|
|
13
|
+
} from '../types'
|
|
14
|
+
import { computeDeploymentView } from './deployment-view/compute'
|
|
15
|
+
import { DynamicViewComputeCtx } from './dynamic-view/compute'
|
|
16
|
+
import { ComputeCtx } from './element-view/compute'
|
|
17
|
+
import { LikeC4ModelGraph } from './LikeC4ModelGraph'
|
|
18
|
+
|
|
19
|
+
type ComputeViewResult<V extends ComputedView = ComputedView> =
|
|
20
|
+
| {
|
|
21
|
+
isSuccess: true
|
|
22
|
+
error?: undefined
|
|
23
|
+
view: V
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
isSuccess: false
|
|
27
|
+
error: Error
|
|
28
|
+
view: undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ComputeViewFn = <V extends LikeC4View>(viewsource: V) => ComputeViewResult<ComputedView>
|
|
32
|
+
|
|
33
|
+
type Params = SetOptional<ParsedLikeC4Model, 'views'>
|
|
34
|
+
export function mkComputeView(model: Params): ComputeViewFn {
|
|
35
|
+
const index = new LikeC4ModelGraph(model)
|
|
36
|
+
let likec4model: LikeC4Model
|
|
37
|
+
|
|
38
|
+
return function computeView(viewsource) {
|
|
39
|
+
try {
|
|
40
|
+
switch (true) {
|
|
41
|
+
case isDeploymentView(viewsource): {
|
|
42
|
+
likec4model ??= LikeC4Model.create({
|
|
43
|
+
...model,
|
|
44
|
+
views: {}
|
|
45
|
+
})
|
|
46
|
+
return {
|
|
47
|
+
isSuccess: true,
|
|
48
|
+
view: computeDeploymentView(likec4model, viewsource)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
case isDynamicView(viewsource):
|
|
52
|
+
return {
|
|
53
|
+
isSuccess: true,
|
|
54
|
+
view: DynamicViewComputeCtx.compute(viewsource, index)
|
|
55
|
+
}
|
|
56
|
+
case isElementView(viewsource):
|
|
57
|
+
return {
|
|
58
|
+
isSuccess: true,
|
|
59
|
+
view: ComputeCtx.elementView(viewsource, index)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
nonexhaustive(viewsource)
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return {
|
|
67
|
+
isSuccess: false,
|
|
68
|
+
error: e instanceof Error ? e : new Error(`Unknown error: ${e}`),
|
|
69
|
+
view: undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function computeViews<const P extends ParsedLikeC4Model<any, any, any, any, any, any>>(
|
|
76
|
+
parsed: P
|
|
77
|
+
): ComputedLikeC4ModelFromParsed<P> {
|
|
78
|
+
const _computeView = mkComputeView(parsed)
|
|
79
|
+
const computeView = (source: LikeC4View): ComputedView => {
|
|
80
|
+
const result = _computeView(source)
|
|
81
|
+
if (result.isSuccess) {
|
|
82
|
+
return result.view
|
|
83
|
+
} else {
|
|
84
|
+
throw result.error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
__: 'computed',
|
|
89
|
+
...parsed,
|
|
90
|
+
views: mapValues(parsed.views, computeView)
|
|
91
|
+
} as any // TODO: fix this
|
|
92
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { DeploymentConnectionModel } from '../../model/connection/deployment'
|
|
2
|
+
import { difference } from '../../utils/set'
|
|
3
|
+
import type { Connections, Elem } from './_types'
|
|
4
|
+
|
|
5
|
+
export interface Memory {
|
|
6
|
+
/**
|
|
7
|
+
* All resolved elements, includes:
|
|
8
|
+
* - explicit elements (added directly, always appear in the view unless excluded)
|
|
9
|
+
* - elements from resolved connections (may be excluded, if connection is redundant @see excludeRedundantRelationships
|
|
10
|
+
* - implicit elements (not added directly, not included in the view, used for resolving connections)
|
|
11
|
+
*/
|
|
12
|
+
readonly elements: ReadonlySet<Elem>
|
|
13
|
+
/**
|
|
14
|
+
* Resolved connections
|
|
15
|
+
* May contains duplicates, need to be merged before further processing
|
|
16
|
+
*/
|
|
17
|
+
readonly connections: Connections
|
|
18
|
+
|
|
19
|
+
has(el: Elem): boolean
|
|
20
|
+
|
|
21
|
+
isExplicit(el: Elem): boolean
|
|
22
|
+
|
|
23
|
+
clone(): MutableMemory
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Patch = (memory: Memory) => Memory
|
|
27
|
+
|
|
28
|
+
export class MutableMemory implements Memory {
|
|
29
|
+
static empty(): Memory {
|
|
30
|
+
return new MutableMemory(new Set(), new Set(), [], new Set())
|
|
31
|
+
}
|
|
32
|
+
constructor(
|
|
33
|
+
public elements: Set<Elem>,
|
|
34
|
+
public explicits: Set<Elem>,
|
|
35
|
+
public connections: DeploymentConnectionModel[],
|
|
36
|
+
/**
|
|
37
|
+
* Final set of elements to be included in the view
|
|
38
|
+
* (`elements` excluding implicits)
|
|
39
|
+
* Keeps order in which elements were added
|
|
40
|
+
*/
|
|
41
|
+
public finalElements: Set<Elem>
|
|
42
|
+
) {
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public has(el: Elem): boolean {
|
|
46
|
+
return this.elements.has(el)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public isExplicit(el: Elem): boolean {
|
|
50
|
+
return this.explicits.has(el)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public exclude(excluded: Set<Elem>): MutableMemory {
|
|
54
|
+
const newMemory = this.clone()
|
|
55
|
+
newMemory.elements = difference(this.elements, excluded)
|
|
56
|
+
newMemory.explicits = difference(this.explicits, excluded)
|
|
57
|
+
newMemory.finalElements = difference(this.finalElements, excluded)
|
|
58
|
+
newMemory.connections = this.connections.filter(c => !excluded.has(c.source) && !excluded.has(c.target))
|
|
59
|
+
return newMemory
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public excludeConnections(excluded: Connections): MutableMemory {
|
|
63
|
+
if (excluded.length === 0) {
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
const newMemory = this.clone()
|
|
67
|
+
|
|
68
|
+
const excludedMap = new Map(excluded.map(c => [c.id, c]))
|
|
69
|
+
let minusElements = new Set<Elem>()
|
|
70
|
+
|
|
71
|
+
newMemory.connections = newMemory.connections.reduce((acc, c) => {
|
|
72
|
+
const excluded = excludedMap.get(c.id)
|
|
73
|
+
if (excluded) {
|
|
74
|
+
minusElements.add(c.source)
|
|
75
|
+
minusElements.add(c.target)
|
|
76
|
+
const diff = c.difference(excluded)
|
|
77
|
+
if (diff.nonEmpty()) {
|
|
78
|
+
acc.push(diff)
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
acc.push(c)
|
|
82
|
+
}
|
|
83
|
+
return acc
|
|
84
|
+
}, [] as DeploymentConnectionModel[])
|
|
85
|
+
|
|
86
|
+
minusElements = difference(minusElements, newMemory.explicits)
|
|
87
|
+
if (minusElements.size > 0) {
|
|
88
|
+
for (const stillExists of newMemory.connections) {
|
|
89
|
+
minusElements.delete(stillExists.source)
|
|
90
|
+
minusElements.delete(stillExists.target)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (minusElements.size > 0) {
|
|
94
|
+
newMemory.elements = difference(newMemory.elements, minusElements)
|
|
95
|
+
newMemory.finalElements = difference(newMemory.finalElements, minusElements)
|
|
96
|
+
}
|
|
97
|
+
return newMemory
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public clone(): MutableMemory {
|
|
101
|
+
return new MutableMemory(
|
|
102
|
+
new Set(this.elements),
|
|
103
|
+
new Set(this.explicits),
|
|
104
|
+
[...this.connections],
|
|
105
|
+
new Set(this.finalElements)
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { isArray, partition } from 'remeda'
|
|
2
|
+
import { mergeConnections } from '../../model/connection/deployment'
|
|
3
|
+
import type { DeploymentConnectionModel } from '../../model/connection/DeploymentConnectionModel'
|
|
4
|
+
import { union } from '../../utils/set'
|
|
5
|
+
import type { Connections, Elem } from './_types'
|
|
6
|
+
import { type Patch } from './Memory'
|
|
7
|
+
|
|
8
|
+
const asArray = <T>(value: T | ReadonlyArray<T>): ReadonlyArray<T> => isArray(value) ? value : [value]
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Stage is a single step in the computation of the view
|
|
12
|
+
*/
|
|
13
|
+
export class Stage {
|
|
14
|
+
// New elements
|
|
15
|
+
readonly #explicits = new Set<Elem>()
|
|
16
|
+
readonly #implicits = new Set<Elem>()
|
|
17
|
+
|
|
18
|
+
// Removed elements
|
|
19
|
+
readonly #excluded = new Set<Elem>()
|
|
20
|
+
readonly #excludedConnections = [] as DeploymentConnectionModel[]
|
|
21
|
+
|
|
22
|
+
#connections = [] as DeploymentConnectionModel[]
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
public readonly previous: Stage | null = null
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
get connections(): ReadonlyArray<DeploymentConnectionModel> {
|
|
29
|
+
return this.#connections
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public addExplicit(elements: Elem | ReadonlyArray<Elem>): void {
|
|
33
|
+
for (const el of asArray(elements)) {
|
|
34
|
+
this.#explicits.add(el)
|
|
35
|
+
this.#implicits.delete(el)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public addImplicit(elements: Elem | ReadonlyArray<Elem>): void {
|
|
40
|
+
for (const el of asArray(elements)) {
|
|
41
|
+
if (this.#explicits.has(el)) {
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
this.#implicits.add(el)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public addConnections(connections: Connections): Connections {
|
|
49
|
+
for (const c of connections) {
|
|
50
|
+
this.#connections.push(c)
|
|
51
|
+
this.addImplicit([c.source, c.target])
|
|
52
|
+
}
|
|
53
|
+
return connections
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public exclude(elements: Elem | ReadonlyArray<Elem>): void {
|
|
57
|
+
if (this.#explicits.size + this.#implicits.size > 0) {
|
|
58
|
+
console.warn('Excluding elements from the stage with existing elements')
|
|
59
|
+
}
|
|
60
|
+
for (const el of asArray(elements)) {
|
|
61
|
+
this.#excluded.add(el)
|
|
62
|
+
this.#explicits.delete(el)
|
|
63
|
+
this.#implicits.delete(el)
|
|
64
|
+
}
|
|
65
|
+
if (this.#connections.length > 0) {
|
|
66
|
+
console.warn('Excluding elements from the stage with existing connections')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public excludeConnections(connections: Connections): void {
|
|
71
|
+
if (this.#connections.length > 0) {
|
|
72
|
+
console.warn('Excluding connections from the stage with existing connections')
|
|
73
|
+
}
|
|
74
|
+
for (const c of connections) {
|
|
75
|
+
this.#excludedConnections.push(c)
|
|
76
|
+
this.#implicits.delete(c.source)
|
|
77
|
+
this.#implicits.delete(c.target)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public hasConnections(): boolean {
|
|
82
|
+
return this.#connections.length > 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns patch to apply to the memory
|
|
87
|
+
*/
|
|
88
|
+
public patch(): Patch {
|
|
89
|
+
return (memory) => {
|
|
90
|
+
let newMemory = memory.clone()
|
|
91
|
+
|
|
92
|
+
let fromConnections = new Set<Elem>()
|
|
93
|
+
|
|
94
|
+
if (this.#connections.length > 0) {
|
|
95
|
+
// To preserve order, we split new connections into two sets
|
|
96
|
+
// First are outgoing from known elements (in memory.elements)
|
|
97
|
+
const [fromKnown, rest] = partition(
|
|
98
|
+
this.#connections,
|
|
99
|
+
c => memory.elements.has(c.source)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
newMemory.connections = mergeConnections([
|
|
103
|
+
...memory.connections,
|
|
104
|
+
...fromKnown,
|
|
105
|
+
...rest
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
fromConnections = new Set([
|
|
109
|
+
// we have source in memory.elements
|
|
110
|
+
...fromKnown.map(c => c.target),
|
|
111
|
+
...rest.flatMap(c => [c.source, c.target])
|
|
112
|
+
])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.#explicits.size > 0 || fromConnections.size > 0 || this.#implicits.size > 0) {
|
|
116
|
+
newMemory.explicits = union(
|
|
117
|
+
newMemory.explicits,
|
|
118
|
+
this.#explicits
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
newMemory.finalElements = union(
|
|
122
|
+
newMemory.finalElements,
|
|
123
|
+
this.#explicits,
|
|
124
|
+
fromConnections
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
newMemory.elements = union(
|
|
128
|
+
newMemory.elements,
|
|
129
|
+
this.#explicits,
|
|
130
|
+
fromConnections,
|
|
131
|
+
this.#implicits
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.#excluded.size > 0) {
|
|
136
|
+
newMemory = newMemory.exclude(this.#excluded)
|
|
137
|
+
}
|
|
138
|
+
if (this.#excludedConnections.length > 0) {
|
|
139
|
+
newMemory = newMemory.excludeConnections(this.#excludedConnections)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return newMemory
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|