@likec4/core 1.26.1 → 1.27.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 (54) hide show
  1. package/dist/builder/index.d.mts +3 -1
  2. package/dist/builder/index.mjs +6 -4
  3. package/dist/compute-view/index.d.mts +1 -2
  4. package/dist/compute-view/index.mjs +19 -7
  5. package/dist/compute-view/relationships-view/index.d.mts +42 -0
  6. package/dist/compute-view/relationships-view/index.mjs +181 -0
  7. package/dist/index.d.mts +3 -5
  8. package/dist/index.mjs +5 -5
  9. package/dist/model/index.d.mts +3 -3
  10. package/dist/model/index.mjs +1 -1
  11. package/dist/shared/{core.yND74qFI.mjs → core.BMep80lJ.mjs} +2 -1
  12. package/dist/shared/{core.DRxv3HIl.mjs → core.C-YXI-43.mjs} +1 -5
  13. package/dist/shared/core.CQXU9DF7.mjs +477 -0
  14. package/dist/shared/{core.Brmu7VBL.d.mts → core.CtloVwbo.d.mts} +3 -1
  15. package/dist/shared/{core.BqezHsjR.mjs → core.Cu-UdkSl.mjs} +16 -477
  16. package/dist/shared/core.DbRvwARP.mjs +5 -0
  17. package/dist/shared/{core.oI7caxVx.d.mts → core.Uz54YXx0.d.mts} +1 -1
  18. package/dist/shared/{core.CcjDE4j7.mjs → core.zj2huD5x.mjs} +17 -30
  19. package/dist/types/index.mjs +1 -1
  20. package/dist/utils/index.d.mts +3 -2
  21. package/dist/utils/index.mjs +5 -3
  22. package/package.json +19 -9
  23. package/src/builder/Builder.model.ts +74 -0
  24. package/src/builder/Builder.with.ts +20 -339
  25. package/src/compute-view/deployment-view/_types.ts +1 -1
  26. package/src/compute-view/deployment-view/predicates/relation-direct.ts +6 -7
  27. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +2 -1
  28. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +1 -1
  29. package/src/compute-view/dynamic-view/compute.ts +22 -43
  30. package/src/compute-view/index.ts +0 -1
  31. package/src/compute-view/relationships-view/_types.ts +7 -6
  32. package/src/compute-view/relationships-view/compute.ts +140 -52
  33. package/src/compute-view/relationships-view/index.ts +3 -0
  34. package/src/compute-view/relationships-view/utils.ts +44 -11
  35. package/src/compute-view/utils/resolve-extended-views.ts +20 -5
  36. package/src/compute-view/utils/view-hash.ts +1 -4
  37. package/src/index.ts +1 -1
  38. package/src/model/ElementModel.ts +2 -0
  39. package/src/model/view/NodeModel.ts +5 -0
  40. package/src/utils/object-hash.ts +13 -2
  41. package/dist/builder/index.d.ts +0 -509
  42. package/dist/compute-view/index.d.ts +0 -23
  43. package/dist/index.d.ts +0 -147
  44. package/dist/model/index.d.ts +0 -118
  45. package/dist/shared/core.-qjOvsEL.d.ts +0 -146
  46. package/dist/shared/core.30yYKXcZ.d.ts +0 -186
  47. package/dist/shared/core.BBJd-FqJ.mjs +0 -37
  48. package/dist/shared/core.BH59D8Ji.d.ts +0 -858
  49. package/dist/shared/core.CIbWBWW3.d.ts +0 -127
  50. package/dist/shared/core.Cpd5mZgF.d.ts +0 -732
  51. package/dist/shared/core.Dd1nCiNx.d.mts +0 -12
  52. package/dist/shared/core.mD0TIdz_.d.ts +0 -12
  53. package/dist/types/index.d.ts +0 -72
  54. package/dist/utils/index.d.ts +0 -458
@@ -1,14 +1,13 @@
1
1
  import { filter, flatMap, isNonNullish, map, pick, pipe } from 'remeda'
2
2
  import { invariant } from '../../../errors'
3
- import type {
4
- ConnectionModel,
5
- DeploymentElementModel,
6
- DeploymentRelationModel,
7
- LikeC4DeploymentModel,
8
- } from '../../../model'
9
3
  import type { DeploymentConnectionModel } from '../../../model/connection/deployment'
10
4
  import { findConnection, findConnectionsBetween, findConnectionsWithin } from '../../../model/connection/deployment'
11
- import { findConnectionsBetween as findModelConnectionsBetween } from '../../../model/connection/model'
5
+ import {
6
+ type ConnectionModel,
7
+ findConnectionsBetween as findModelConnectionsBetween,
8
+ } from '../../../model/connection/model'
9
+ import type { DeploymentElementModel, DeploymentRelationModel } from '../../../model/DeploymentElementModel'
10
+ import type { LikeC4DeploymentModel } from '../../../model/DeploymentModel'
12
11
  import type { RelationshipModel } from '../../../model/RelationModel'
13
12
  import type { AnyAux } from '../../../model/types'
14
13
  import { type Filterable, type OperatorPredicate, type RelationExpr, FqnExpr } from '../../../types'
@@ -1,8 +1,9 @@
1
1
  import { anyPass, filter, pipe } from 'remeda'
2
2
  import { invariant } from '../../../errors'
3
- import type { AnyAux, RelationshipModel } from '../../../model'
4
3
  import { findConnectionsBetween } from '../../../model/connection/deployment'
5
4
  import type { LikeC4DeploymentModel } from '../../../model/DeploymentModel'
5
+ import type { RelationshipModel } from '../../../model/RelationModel'
6
+ import type { AnyAux } from '../../../model/types'
6
7
  import { type RelationExpr, FqnExpr } from '../../../types'
7
8
  import { isAncestor } from '../../../utils'
8
9
  import type { Connection, Elem, PredicateExecutor } from '../_types'
@@ -1,7 +1,7 @@
1
1
  import { anyPass, filter, pipe } from 'remeda'
2
2
  import { invariant } from '../../../errors'
3
- import type { LikeC4DeploymentModel } from '../../../model'
4
3
  import { findConnection, findConnectionsBetween } from '../../../model/connection/deployment'
4
+ import type { LikeC4DeploymentModel } from '../../../model/DeploymentModel'
5
5
  import type { RelationshipModel } from '../../../model/RelationModel'
6
6
  import { type RelationExpr, FqnExpr } from '../../../types'
7
7
  import { isAncestor } from '../../../utils'
@@ -8,22 +8,22 @@ import {
8
8
  type Color,
9
9
  type ComputedDynamicView,
10
10
  type ComputedEdge,
11
- DefaultArrowType,
12
- DefaultLineStyle,
13
- DefaultRelationshipColor,
14
11
  type DynamicView,
15
12
  type DynamicViewStep,
16
- isDynamicViewParallelSteps,
17
- isViewRuleAutoLayout,
18
- isViewRulePredicate,
19
13
  type NonEmptyArray,
20
14
  type RelationId,
21
15
  type RelationshipArrowType,
22
16
  type RelationshipLineType,
23
17
  type StepEdgeId,
24
- stepEdgeId,
25
18
  type Tag,
26
- type ViewId
19
+ type ViewId,
20
+ DefaultArrowType,
21
+ DefaultLineStyle,
22
+ DefaultRelationshipColor,
23
+ isDynamicViewParallelSteps,
24
+ isViewRuleAutoLayout,
25
+ isViewRulePredicate,
26
+ stepEdgeId,
27
27
  } from '../../types'
28
28
  import { ancestorsFqn, commonAncestor, parentFqn } from '../../utils/fqn'
29
29
  import { applyCustomElementProperties } from '../utils/applyCustomElementProperties'
@@ -62,7 +62,7 @@ class DynamicViewCompute {
62
62
 
63
63
  constructor(
64
64
  protected model: LikeC4Model,
65
- protected view: DynamicView
65
+ protected view: DynamicView,
66
66
  ) {}
67
67
 
68
68
  private addStep(
@@ -75,7 +75,7 @@ class DynamicViewCompute {
75
75
  ...step
76
76
  }: DynamicViewStep,
77
77
  index: number,
78
- parent?: number
78
+ parent?: number,
79
79
  ) {
80
80
  const id = parent ? stepEdgeId(parent, index) : stepEdgeId(index)
81
81
  const source = this.model.element(stepSource)
@@ -89,11 +89,8 @@ class DynamicViewCompute {
89
89
  relations,
90
90
  tags,
91
91
  navigateTo: derivedNavigateTo,
92
- head,
93
- tail,
94
92
  color,
95
93
  line,
96
- notation
97
94
  } = this.findRelations(source, target)
98
95
 
99
96
  const navigateTo = isTruthy(stepNavigateTo) && stepNavigateTo !== this.view.id ? stepNavigateTo : derivedNavigateTo
@@ -108,11 +105,8 @@ class DynamicViewCompute {
108
105
  isBackward: isBackward ?? false,
109
106
  ...(navigateTo ? { navigateTo } : {}),
110
107
  ...(tags ? { tags } : {}),
111
- ...(head ? { head } : {}),
112
- ...(tail ? { tail } : {}),
113
108
  ...(color ? { color } : {}),
114
109
  ...(line ? { line } : {}),
115
- ...(notation ? { notation } : {})
116
110
  })
117
111
  }
118
112
 
@@ -171,7 +165,7 @@ class DynamicViewCompute {
171
165
  color: DefaultRelationshipColor,
172
166
  line: DefaultLineStyle,
173
167
  head: DefaultArrowType,
174
- ...step
168
+ ...step,
175
169
  }
176
170
  if (isBackward) {
177
171
  edge.dir = 'back'
@@ -204,8 +198,8 @@ class DynamicViewCompute {
204
198
  applyViewRuleStyles(
205
199
  rules,
206
200
  // Keep order of elements
207
- elements.map(e => nonNullable(nodesMap.get(e.id)))
208
- )
201
+ elements.map(e => nonNullable(nodesMap.get(e.id))),
202
+ ),
209
203
  )
210
204
 
211
205
  const autoLayoutRule = findLast(rules, isViewRuleAutoLayout)
@@ -217,7 +211,7 @@ class DynamicViewCompute {
217
211
  autoLayout: {
218
212
  direction: autoLayoutRule?.direction ?? 'LR',
219
213
  ...(autoLayoutRule?.nodeSep && { nodeSep: autoLayoutRule.nodeSep }),
220
- ...(autoLayoutRule?.rankSep && { rankSep: autoLayoutRule.rankSep })
214
+ ...(autoLayoutRule?.rankSep && { rankSep: autoLayoutRule.rankSep }),
221
215
  },
222
216
  nodes: map(nodes, n => {
223
217
  // omit notation
@@ -230,9 +224,9 @@ class DynamicViewCompute {
230
224
  edges,
231
225
  ...(elementNotations.length > 0 && {
232
226
  notation: {
233
- elements: elementNotations
234
- }
235
- })
227
+ elements: elementNotations,
228
+ },
229
+ }),
236
230
  })
237
231
  }
238
232
 
@@ -241,11 +235,8 @@ class DynamicViewCompute {
241
235
  tags: NonEmptyArray<Tag> | null
242
236
  relations: NonEmptyArray<RelationId> | null
243
237
  navigateTo: ViewId | null
244
- tail: RelationshipArrowType | null
245
- head: RelationshipArrowType | null
246
238
  color: Color | null
247
239
  line: RelationshipLineType | null
248
- notation: string | null
249
240
  } {
250
241
  const relationships = findConnection(source, target, 'directed').flatMap(r => [...r.relations])
251
242
  if (relationships.length === 0) {
@@ -254,18 +245,15 @@ class DynamicViewCompute {
254
245
  tags: null,
255
246
  relations: null,
256
247
  navigateTo: null,
257
- tail: null,
258
- head: null,
259
248
  color: null,
260
249
  line: null,
261
- notation: null
262
250
  }
263
251
  }
264
252
  const alltags = pipe(
265
253
  relationships,
266
254
  flatMap(r => r.tags),
267
255
  filter(isTruthy),
268
- unique()
256
+ unique(),
269
257
  )
270
258
  const tags = hasAtLeast(alltags, 1) ? alltags : null
271
259
  const relations = hasAtLeast(relationships, 1) ? map(relationships, r => r.id) : null
@@ -280,27 +268,21 @@ class DynamicViewCompute {
280
268
  filter(isTruthy),
281
269
  filter(v => v !== this.view.id),
282
270
  unique(),
283
- only()
271
+ only(),
284
272
  )
285
273
 
286
274
  const commonProperties = pipe(
287
275
  relationships,
288
276
  reduce((acc, { title, $relationship: r }) => {
289
277
  isTruthy(title) && acc.title.add(title)
290
- isTruthy(r.tail) && acc.tail.add(r.tail)
291
- isTruthy(r.head) && acc.head.add(r.head)
292
278
  isTruthy(r.color) && acc.color.add(r.color)
293
279
  isTruthy(r.line) && acc.line.add(r.line)
294
-
295
280
  return acc
296
281
  }, {
297
- tail: new Set<RelationshipArrowType>(),
298
- head: new Set<RelationshipArrowType>(),
299
282
  color: new Set<Color>(),
300
283
  line: new Set<RelationshipLineType>(),
301
- notation: new Set<string>(),
302
- title: new Set<string>()
303
- })
284
+ title: new Set<string>(),
285
+ }),
304
286
  )
305
287
 
306
288
  return {
@@ -308,17 +290,14 @@ class DynamicViewCompute {
308
290
  relations,
309
291
  navigateTo: navigateTo ?? null,
310
292
  title: only([...commonProperties.title]) ?? null,
311
- tail: only([...commonProperties.tail]) ?? null,
312
- head: only([...commonProperties.head]) ?? null,
313
293
  color: only([...commonProperties.color]) ?? null,
314
294
  line: only([...commonProperties.line]) ?? null,
315
- notation: only([...commonProperties.notation]) ?? null
316
295
  }
317
296
  }
318
297
  }
319
298
  export function computeDynamicView<M extends AnyAux>(
320
299
  model: LikeC4Model<M>,
321
- view: DynamicView
300
+ view: DynamicView,
322
301
  ): ComputedDynamicView {
323
302
  return new DynamicViewCompute(model, view).compute()
324
303
  }
@@ -1,5 +1,4 @@
1
1
  export { computeView, computeViews, unsafeComputeView } from './compute-view'
2
2
  export type { ComputeViewResult } from './compute-view'
3
- export { computeRelationshipsView } from './relationships-view/compute'
4
3
  export { resolveRulesExtendedViews } from './utils/resolve-extended-views'
5
4
  export { viewsWithReadableEdges, withReadableEdges } from './utils/with-readable-edges'
@@ -1,5 +1,6 @@
1
1
  import { type ElementModel } from '../../model/ElementModel'
2
2
  import type { RelationshipModel } from '../../model/RelationModel'
3
+ import type { AnyAux } from '../../model/types'
3
4
 
4
5
  // type Pair = {
5
6
  // left: ReadonlySet<ElementModel>
@@ -7,10 +8,10 @@ import type { RelationshipModel } from '../../model/RelationModel'
7
8
  // right: ReadonlySet<ElementModel>
8
9
  // }
9
10
 
10
- export type RelationshipsViewData = {
11
- incomers: ReadonlySet<ElementModel>
12
- incoming: ReadonlySet<RelationshipModel>
13
- subjects: ReadonlySet<ElementModel>
14
- outgoing: ReadonlySet<RelationshipModel>
15
- outgoers: ReadonlySet<ElementModel>
11
+ export type RelationshipsViewData<M extends AnyAux = AnyAux> = {
12
+ incomers: ReadonlySet<ElementModel<M>>
13
+ incoming: ReadonlySet<RelationshipModel<M>>
14
+ subjects: ReadonlySet<ElementModel<M>>
15
+ outgoing: ReadonlySet<RelationshipModel<M>>
16
+ outgoers: ReadonlySet<ElementModel<M>>
16
17
  }
@@ -1,74 +1,162 @@
1
+ import { pipe } from 'remeda'
2
+ import { invariant } from '../../errors'
1
3
  import type { ElementModel } from '../../model/ElementModel'
2
4
  import type { LikeC4Model } from '../../model/LikeC4Model'
3
5
  import type { RelationshipModel } from '../../model/RelationModel'
4
- import type { Fqn } from '../../types'
5
- import { ifind, toSet } from '../../utils/iterable'
6
+ import type { AnyAux } from '../../model/types'
7
+ import type { LikeC4ViewModel } from '../../model/view/LikeC4ViewModel'
8
+ import { isDescendantOf, sortParentsFirst } from '../../utils/fqn'
9
+ import { ifilter, imap, toArray, toSet } from '../../utils/iterable'
10
+ import type { RelationshipsViewData } from './_types'
11
+ import { treeFromElements } from './utils'
6
12
 
7
- export function computeRelationshipsView(
8
- subjectId: Fqn,
9
- likec4model: LikeC4Model,
10
- ): {
11
- incomers: Set<ElementModel<LikeC4Model.Any>>
12
- incoming: Set<RelationshipModel<LikeC4Model.Any>>
13
- subjects: Set<ElementModel<LikeC4Model.Any>>
14
- outgoing: Set<RelationshipModel<LikeC4Model.Any>>
15
- outgoers: Set<ElementModel<LikeC4Model.Any>>
16
- } {
17
- const subject = likec4model.element(subjectId)
18
- let relationships = {
19
- incoming: [...subject.incoming()],
20
- outgoing: [...subject.outgoing()],
13
+ const finalize = <M extends AnyAux>(
14
+ elements: Set<ElementModel<M>>,
15
+ explicits: Set<ElementModel<M>>,
16
+ ): Set<ElementModel<M>> => {
17
+ if (elements.size > 2 && explicits.size !== elements.size) {
18
+ return new Set(sortParentsFirst([
19
+ ...treeFromElements(elements).flatten(),
20
+ ...explicits,
21
+ ]))
21
22
  }
23
+ if (elements.size > 1) {
24
+ return new Set(sortParentsFirst([...elements]))
25
+ }
26
+ return elements
27
+ }
22
28
 
23
- const subjectSiblings = toSet(subject.ascendingSiblings())
24
- const isSubjectSibling = (e: ElementModel) => subjectSiblings.has(e)
25
- const subjects = new Set([subject])
29
+ function computeRelationships<const M extends AnyAux>(
30
+ subject: ElementModel<M>,
31
+ others: Set<ElementModel<M>>,
32
+ relationships: {
33
+ incoming: ReadonlyArray<RelationshipModel<M>>
34
+ outgoing: ReadonlyArray<RelationshipModel<M>>
35
+ },
36
+ ): Omit<RelationshipsViewData<M>, 'subjectExistsInScope'> {
37
+ const isOther = (e: ElementModel<M>) => others.has(e)
38
+ let subjects = new Set([subject])
39
+ const explicits = {
40
+ incomers: new Set<ElementModel<M>>(),
41
+ subjects: new Set([subject]),
42
+ outgoers: new Set<ElementModel<M>>(),
43
+ }
26
44
 
27
45
  // Iterate over incoming and outgoing relationships
28
46
  // and add the source and target elements to appropriate sets.
29
- // If element is deeper in the hierarchy - find the closest ancestor that is a sibling of the subject.
30
- const incomers = new Set(relationships.incoming.flatMap((r) => {
47
+ // If element is deeper in the hierarchy - add all ancestors until that is a sibling of the subject
48
+ let incomers = new Set(relationships.incoming.flatMap((r) => {
49
+ explicits.subjects.add(r.target)
50
+ explicits.incomers.add(r.source)
51
+
31
52
  subjects.add(r.target)
32
- if (subjectSiblings.has(r.source)) {
33
- return [r.source]
53
+ if (r.target !== subject) {
54
+ let target = r.target.parent
55
+ while (target && target !== subject) {
56
+ subjects.add(target)
57
+ target = target.parent
58
+ }
34
59
  }
35
- const parent = ifind(r.source.ancestors(), isSubjectSibling)
36
- return parent ? [r.source, parent] : [r.source]
60
+
61
+ let source = r.source
62
+ const incomerBranch = [] as ElementModel<M>[]
63
+ while (true) {
64
+ incomerBranch.push(source)
65
+ if (isOther(source) || !source.parent) {
66
+ break
67
+ }
68
+ source = source.parent
69
+ }
70
+ return incomerBranch
37
71
  }))
38
- const outgoers = new Set(relationships.outgoing.flatMap((r) => {
72
+
73
+ let outgoers = new Set(relationships.outgoing.flatMap((r) => {
74
+ explicits.subjects.add(r.source)
75
+ explicits.outgoers.add(r.target)
76
+
39
77
  subjects.add(r.source)
40
- if (subjectSiblings.has(r.target)) {
41
- return [r.target]
78
+ if (r.source !== subject) {
79
+ let source = r.source.parent
80
+ while (source && source !== subject) {
81
+ subjects.add(source)
82
+ source = source.parent
83
+ }
84
+ }
85
+
86
+ let target = r.target
87
+ const outgoerBranch = [] as ElementModel<M>[]
88
+ while (true) {
89
+ outgoerBranch.push(target)
90
+ if (isOther(target) || !target.parent) {
91
+ break
92
+ }
93
+ target = target.parent
42
94
  }
43
- const parent = ifind(r.target.ancestors(), isSubjectSibling)
44
- return parent ? [r.target, parent] : [r.target]
95
+ return outgoerBranch
45
96
  }))
46
97
 
47
98
  return {
48
- incomers,
99
+ incomers: finalize(incomers, explicits.incomers),
49
100
  incoming: new Set(relationships.incoming),
50
- subjects,
101
+ subjects: finalize(subjects, explicits.subjects),
51
102
  outgoing: new Set(relationships.outgoing),
52
- outgoers,
103
+ outgoers: finalize(outgoers, explicits.outgoers),
53
104
  }
105
+ }
106
+
107
+ export function computeRelationshipsView<const M extends AnyAux>(
108
+ subjectId: NoInfer<M['Element']>,
109
+ likec4model: LikeC4Model<M>,
110
+ scopeViewId: NoInfer<M['View']> | null,
111
+ scope: 'global' | 'view' = 'global',
112
+ ): RelationshipsViewData<M> {
113
+ let subjectExistsInScope = true
114
+ const view = scopeViewId ? likec4model.findView(scopeViewId) : null
115
+ if (scope === 'view') {
116
+ invariant(
117
+ view,
118
+ 'Scope view id is required when scope is "view"',
119
+ )
120
+ return computeScopedRelationshipsView(subjectId, view, likec4model)
121
+ }
122
+
123
+ const subject = likec4model.element(subjectId)
124
+ const subjectSiblings = toSet(subject.ascendingSiblings())
125
+ return computeRelationships(
126
+ subject,
127
+ subjectSiblings,
128
+ {
129
+ incoming: [...subject.incoming()],
130
+ outgoing: [...subject.outgoing()],
131
+ },
132
+ )
133
+ }
134
+
135
+ function computeScopedRelationshipsView<const M extends AnyAux>(
136
+ subjectId: NoInfer<M['Element']>,
137
+ view: LikeC4ViewModel<M>,
138
+ likec4model: LikeC4Model<M>,
139
+ ): RelationshipsViewData<M> {
140
+ const subject = likec4model.element(subjectId)
141
+ let relationships = {
142
+ incoming: toArray(ifilter(subject.incoming(), (r) => view.includesRelation(r.id))),
143
+ outgoing: toArray(ifilter(subject.outgoing(), (r) => view.includesRelation(r.id))),
144
+ }
145
+
146
+ const isDescendant = isDescendantOf(subject)
147
+
148
+ const others = new Set([
149
+ ...subject.ascendingSiblings(),
150
+ ...pipe(
151
+ view.elements(),
152
+ imap((e) => e.element),
153
+ ifilter((e): e is ElementModel<M> => e !== subject && isDescendant(e)),
154
+ ),
155
+ ])
54
156
 
55
- // return {
56
- // id: `relationships-${subjectId}` as ViewId,
57
- // title: `Relationships of ${subject.title}`,
58
- // description: null,
59
- // autoLayout: {
60
- // direction: 'LR',
61
- // },
62
- // tags: null,
63
- // links: null,
64
- // hash: 'empty',
65
- // customColorDefinitions: {},
66
- // ...layoutRelationshipsView({
67
- // incomers,
68
- // incoming: new Set(relationships.incoming),
69
- // subjects,
70
- // outgoing: new Set(relationships.outgoing),
71
- // outgoers,
72
- // }),
73
- // }
157
+ return computeRelationships(
158
+ subject,
159
+ others,
160
+ relationships,
161
+ )
74
162
  }
@@ -0,0 +1,3 @@
1
+ export type { RelationshipsViewData } from './_types'
2
+ export { computeRelationshipsView } from './compute'
3
+ export { treeFromElements } from './utils'
@@ -1,14 +1,14 @@
1
1
  import { nonNullable } from '../../errors'
2
2
  import type { ElementModel } from '../../model/ElementModel'
3
- import type { Fqn } from '../../types'
3
+ import type { AnyAux } from '../../model/types'
4
4
  import { isAncestor, isDescendantOf, sortParentsFirst } from '../../utils/fqn'
5
5
  import { DefaultMap } from '../../utils/mnemonist'
6
6
 
7
- export function treeFromElements(elements: Iterable<ElementModel>) {
8
- const sorted = sortParentsFirst([...elements]) as ReadonlyArray<ElementModel>
7
+ export function treeFromElements<const M extends AnyAux>(elements: Iterable<ElementModel<M>>) {
8
+ const sorted = sortParentsFirst([...elements]) as ReadonlyArray<ElementModel<M>>
9
9
  const root = new Set(sorted)
10
- const map = new Map(sorted.map(e => [e.id, e]))
11
- const parents = new DefaultMap<ElementModel, ElementModel | null>(() => null)
10
+ const map = new Map(sorted.map(e => [e._literalId, e]))
11
+ const parents = new DefaultMap<ElementModel<M>, ElementModel<M> | null>(() => null)
12
12
  const children = sorted.reduce((acc, parent, index, all) => {
13
13
  acc.set(
14
14
  parent,
@@ -25,16 +25,49 @@ export function treeFromElements(elements: Iterable<ElementModel>) {
25
25
  parents.set(el, parent)
26
26
  }
27
27
  return acc
28
- }, [] as ElementModel[]),
28
+ }, [] as ElementModel<M>[]),
29
29
  )
30
30
  return acc
31
- }, new DefaultMap<ElementModel, ElementModel[]>(() => []))
31
+ }, new DefaultMap<ElementModel<M>, ElementModel<M>[]>(() => []))
32
32
 
33
33
  return {
34
34
  sorted,
35
- byId: (id: string) => nonNullable(map.get(id as Fqn), `Element not found by id: ${id}`),
36
- root: root as ReadonlySet<ElementModel>,
37
- parent: (el: ElementModel) => parents.get(el),
38
- children: (el: ElementModel): ReadonlyArray<ElementModel> => children.get(el),
35
+ byId: (id: M['Element']): ElementModel<M> => nonNullable(map.get(id), `Element not found by id: ${id}`),
36
+ root: root as ReadonlySet<ElementModel<M>>,
37
+ parent: (el: ElementModel<M>): ElementModel<M> | null => parents.get(el),
38
+ children: (el: ElementModel<M>): ReadonlyArray<ElementModel<M>> => children.get(el),
39
+ /**
40
+ * Flattens the tree structure by removing redundant hierarchy levels.
41
+ * @example
42
+ * A
43
+ * └── B
44
+ * ├── C
45
+ * │ └── D
46
+ * │ └── E
47
+ * └── F
48
+ * └── G
49
+ * becomes
50
+ * A
51
+ * ├── C
52
+ * │ └── E
53
+ * └── F
54
+ * └── G
55
+ */
56
+ flatten: (): Set<ElementModel<M>> => {
57
+ return new Set([
58
+ ...root,
59
+ ...sorted.reduce((acc, el) => {
60
+ const _children = children.get(el)
61
+ if (_children.length === 0) {
62
+ acc.push(el)
63
+ return acc
64
+ }
65
+ if (_children.length > 1) {
66
+ acc.push(..._children)
67
+ }
68
+ return acc
69
+ }, [] as ElementModel<M>[]),
70
+ ])
71
+ },
39
72
  }
40
73
  }
@@ -1,18 +1,19 @@
1
- import { values } from 'remeda'
2
- import { type ExtendsElementView, isElementView, isExtendsElementView, type LikeC4View } from '../../types/view'
1
+ import { unique, values } from 'remeda'
2
+ import { type ExtendsElementView, type LikeC4View, isElementView, isExtendsElementView } from '../../types/view'
3
3
 
4
4
  import Graph from 'graphology'
5
5
  import { topologicalSort } from 'graphology-dag/topological-sort'
6
6
  import willCreateCycle from 'graphology-dag/will-create-cycle'
7
+ import { isNonEmptyArray } from '../../utils'
7
8
  /**
8
9
  * Resolve rules of extended views
9
10
  * (Removes invalid views)
10
11
  */
11
12
  export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
12
- unresolvedViews: V
13
+ unresolvedViews: V,
13
14
  ): V {
14
15
  const g = new Graph<{ view: LikeC4View }>({
15
- type: 'directed'
16
+ type: 'directed',
16
17
  })
17
18
  const extendedViews = [] as ExtendsElementView<any, any>[]
18
19
  for (const view of values(unresolvedViews)) {
@@ -53,10 +54,24 @@ export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
53
54
  return acc
54
55
  }
55
56
 
57
+ const tags = unique([
58
+ ...(extendsFrom.tags ?? []),
59
+ ...(view.tags ?? []),
60
+ ])
61
+
62
+ const links = [
63
+ ...(extendsFrom.links ?? []),
64
+ ...(view.links ?? []),
65
+ ]
66
+
56
67
  acc[view.id] = {
57
68
  ...extendsFrom,
58
69
  ...view,
59
- rules: [...extendsFrom.rules, ...view.rules]
70
+ title: view.title ?? extendsFrom.title ?? null,
71
+ description: view.description ?? extendsFrom.description ?? null,
72
+ tags: isNonEmptyArray(tags) ? tags : null,
73
+ links: isNonEmptyArray(links) ? links : null,
74
+ rules: [...extendsFrom.rules, ...view.rules],
60
75
  }
61
76
  return acc
62
77
  }, {} as Record<string, LikeC4View>) as V
@@ -19,9 +19,6 @@ export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V,
19
19
  mapToObj(({ source, target, ...edge }) => [`${source}:${target}`, edge]),
20
20
  ),
21
21
  }
22
- view.hash = objectHash(tohash, {
23
- ignoreUnknown: true,
24
- respectType: false,
25
- })
22
+ view.hash = objectHash(tohash)
26
23
  return view as unknown as V
27
24
  }
package/src/index.ts CHANGED
@@ -36,9 +36,9 @@ export * from './utils'
36
36
  export * as Expr from './types/expression'
37
37
 
38
38
  export {
39
- computeRelationshipsView,
40
39
  computeView,
41
40
  computeViews,
42
41
  unsafeComputeView,
43
42
  } from './compute-view'
43
+
44
44
  export type { ComputeViewResult } from './compute-view'
@@ -25,6 +25,7 @@ export type ElementsIterator<M extends AnyAux> = IteratorLike<ElementModel<M>>
25
25
 
26
26
  export class ElementModel<M extends AnyAux = AnyAux> {
27
27
  readonly id: M['Fqn']
28
+ readonly _literalId: M['Element']
28
29
  readonly hierarchyLevel: number
29
30
 
30
31
  constructor(
@@ -32,6 +33,7 @@ export class ElementModel<M extends AnyAux = AnyAux> {
32
33
  public readonly $element: C4Element,
33
34
  ) {
34
35
  this.id = this.$element.id
36
+ this._literalId = this.$element.id
35
37
  this.hierarchyLevel = hierarchyLevel(this.id)
36
38
  }
37
39
 
@@ -4,6 +4,7 @@ import {
4
4
  type DeploymentNodeKind,
5
5
  type DiagramView,
6
6
  type ElementShape as C4ElementShape,
7
+ type ElementStyle,
7
8
  type IconUrl,
8
9
  type IteratorLike,
9
10
  type Link,
@@ -114,6 +115,10 @@ export class NodeModel<M extends AnyAux, V extends ComputedView | DiagramView =
114
115
  return this.$node.navigateTo ? this.$view.$model.view(this.$node.navigateTo) : null
115
116
  }
116
117
 
118
+ get style(): ElementStyle {
119
+ return this.$node.style
120
+ }
121
+
117
122
  /**
118
123
  * Get all ancestor elements (i.e. parent, parent’s parent, etc.)
119
124
  * (from closest to root)