@likec4/core 1.26.2 → 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 (51) 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 +6 -6
  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.DvRnF4Sc.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.NNojPYtn.mjs → core.zj2huD5x.mjs} +12 -8
  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/compute-view/deployment-view/_types.ts +1 -1
  25. package/src/compute-view/deployment-view/predicates/relation-direct.ts +6 -7
  26. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +2 -1
  27. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +1 -1
  28. package/src/compute-view/index.ts +0 -1
  29. package/src/compute-view/relationships-view/_types.ts +7 -6
  30. package/src/compute-view/relationships-view/compute.ts +140 -52
  31. package/src/compute-view/relationships-view/index.ts +3 -0
  32. package/src/compute-view/relationships-view/utils.ts +44 -11
  33. package/src/compute-view/utils/view-hash.ts +1 -4
  34. package/src/index.ts +1 -1
  35. package/src/model/ElementModel.ts +2 -0
  36. package/src/model/view/NodeModel.ts +5 -0
  37. package/src/utils/object-hash.ts +13 -2
  38. package/dist/builder/index.d.ts +0 -509
  39. package/dist/compute-view/index.d.ts +0 -23
  40. package/dist/index.d.ts +0 -147
  41. package/dist/model/index.d.ts +0 -118
  42. package/dist/shared/core.-qjOvsEL.d.ts +0 -146
  43. package/dist/shared/core.30yYKXcZ.d.ts +0 -186
  44. package/dist/shared/core.BH59D8Ji.d.ts +0 -858
  45. package/dist/shared/core.C8xqv5sq.mjs +0 -37
  46. package/dist/shared/core.CIbWBWW3.d.ts +0 -127
  47. package/dist/shared/core.Cpd5mZgF.d.ts +0 -732
  48. package/dist/shared/core.Dd1nCiNx.d.mts +0 -12
  49. package/dist/shared/core.mD0TIdz_.d.ts +0 -12
  50. package/dist/types/index.d.ts +0 -72
  51. package/dist/utils/index.d.ts +0 -458
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likec4/core",
3
- "version": "1.26.2",
3
+ "version": "1.27.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://likec4.dev",
6
6
  "author": "Denis Davydkov <denis@davydkov.com>",
@@ -37,6 +37,14 @@
37
37
  "default": "./dist/builder/index.mjs"
38
38
  }
39
39
  },
40
+ "./compute-view/relationships": {
41
+ "sources": "./src/compute-view/relationships-view/index.ts",
42
+ "default": {
43
+ "types": "./dist/compute-view/relationships-view/index.d.mts",
44
+ "import": "./dist/compute-view/relationships-view/index.mjs",
45
+ "default": "./dist/compute-view/relationships-view/index.mjs"
46
+ }
47
+ },
40
48
  "./compute-view": {
41
49
  "sources": "./src/compute-view/index.ts",
42
50
  "default": {
@@ -68,22 +76,24 @@
68
76
  "import": "./dist/types/index.mjs",
69
77
  "default": "./dist/types/index.mjs"
70
78
  }
71
- }
79
+ },
80
+ "./src": "./src/index.ts"
72
81
  },
73
82
  "publishConfig": {
74
83
  "registry": "https://registry.npmjs.org",
75
84
  "access": "public"
76
85
  },
77
86
  "dependencies": {
78
- "type-fest": "4.34.1"
87
+ "type-fest": "^4.38.0"
79
88
  },
80
89
  "devDependencies": {
81
90
  "@dagrejs/dagre": "1.1.4",
82
- "@mantine/colors-generator": "7.17.1",
91
+ "@mantine/colors-generator": "7.17.2",
83
92
  "@types/natural-compare-lite": "^1.4.2",
84
93
  "@types/object-hash": "^3.0.6",
85
94
  "chroma-js": "^3.1.2",
86
95
  "defu": "^6.1.4",
96
+ "fdir": "^6.4.3",
87
97
  "graphology": "^0.25.4",
88
98
  "graphology-dag": "^0.4.1",
89
99
  "graphology-types": "^0.24.8",
@@ -91,12 +101,12 @@
91
101
  "natural-compare-lite": "^1.4.0",
92
102
  "object-hash": "^3.0.0",
93
103
  "obliterator": "^2.0.5",
94
- "remeda": "^2.21.0",
104
+ "remeda": "^2.21.2",
95
105
  "turbo": "^2.4.4",
96
- "typescript": "5.8.2",
97
- "unbuild": "^3.3.1",
98
- "vitest": "^3.0.8",
99
- "@likec4/tsconfig": "1.26.2"
106
+ "typescript": "^5.8.2",
107
+ "unbuild": "^3.5.0",
108
+ "vitest": "^3.0.9",
109
+ "@likec4/tsconfig": "1.27.0"
100
110
  },
101
111
  "scripts": {
102
112
  "typecheck": "tsc --noEmit",
@@ -318,6 +318,80 @@ export function model<
318
318
  op14: (input: ModelBuilder<N>) => ModelBuilder<O>,
319
319
  op15: (input: ModelBuilder<O>) => ModelBuilder<P>,
320
320
  ): (input: Builder<A>) => Builder<P>
321
+ export function model<
322
+ A extends AnyTypes,
323
+ B extends AnyTypes,
324
+ C extends AnyTypes,
325
+ D extends AnyTypes,
326
+ E extends AnyTypes,
327
+ F extends AnyTypes,
328
+ G extends AnyTypes,
329
+ H extends AnyTypes,
330
+ I extends AnyTypes,
331
+ J extends AnyTypes,
332
+ K extends AnyTypes,
333
+ L extends AnyTypes,
334
+ M extends AnyTypes,
335
+ N extends AnyTypes,
336
+ O extends AnyTypes,
337
+ P extends AnyTypes,
338
+ Q extends AnyTypes,
339
+ >(
340
+ op1: (input: ModelBuilder<A>) => ModelBuilder<B>,
341
+ op2: (input: ModelBuilder<B>) => ModelBuilder<C>,
342
+ op3: (input: ModelBuilder<C>) => ModelBuilder<D>,
343
+ op4: (input: ModelBuilder<D>) => ModelBuilder<E>,
344
+ op5: (input: ModelBuilder<E>) => ModelBuilder<F>,
345
+ op6: (input: ModelBuilder<F>) => ModelBuilder<G>,
346
+ op7: (input: ModelBuilder<G>) => ModelBuilder<H>,
347
+ op8: (input: ModelBuilder<H>) => ModelBuilder<I>,
348
+ op9: (input: ModelBuilder<I>) => ModelBuilder<J>,
349
+ op10: (input: ModelBuilder<J>) => ModelBuilder<K>,
350
+ op11: (input: ModelBuilder<K>) => ModelBuilder<L>,
351
+ op12: (input: ModelBuilder<L>) => ModelBuilder<M>,
352
+ op13: (input: ModelBuilder<M>) => ModelBuilder<N>,
353
+ op14: (input: ModelBuilder<N>) => ModelBuilder<O>,
354
+ op15: (input: ModelBuilder<O>) => ModelBuilder<P>,
355
+ op16: (input: ModelBuilder<P>) => ModelBuilder<Q>,
356
+ ): (input: Builder<A>) => Builder<Q>
357
+ export function model<
358
+ A extends AnyTypes,
359
+ B extends AnyTypes,
360
+ C extends AnyTypes,
361
+ D extends AnyTypes,
362
+ E extends AnyTypes,
363
+ F extends AnyTypes,
364
+ G extends AnyTypes,
365
+ H extends AnyTypes,
366
+ I extends AnyTypes,
367
+ J extends AnyTypes,
368
+ K extends AnyTypes,
369
+ L extends AnyTypes,
370
+ M extends AnyTypes,
371
+ N extends AnyTypes,
372
+ O extends AnyTypes,
373
+ P extends AnyTypes,
374
+ Q extends AnyTypes,
375
+ R extends AnyTypes,
376
+ >(
377
+ op1: (input: ModelBuilder<A>) => ModelBuilder<B>,
378
+ op2: (input: ModelBuilder<B>) => ModelBuilder<C>,
379
+ op3: (input: ModelBuilder<C>) => ModelBuilder<D>,
380
+ op4: (input: ModelBuilder<D>) => ModelBuilder<E>,
381
+ op5: (input: ModelBuilder<E>) => ModelBuilder<F>,
382
+ op6: (input: ModelBuilder<F>) => ModelBuilder<G>,
383
+ op7: (input: ModelBuilder<G>) => ModelBuilder<H>,
384
+ op8: (input: ModelBuilder<H>) => ModelBuilder<I>,
385
+ op9: (input: ModelBuilder<I>) => ModelBuilder<J>,
386
+ op10: (input: ModelBuilder<J>) => ModelBuilder<K>,
387
+ op11: (input: ModelBuilder<K>) => ModelBuilder<L>,
388
+ op12: (input: ModelBuilder<L>) => ModelBuilder<M>,
389
+ op13: (input: ModelBuilder<M>) => ModelBuilder<N>,
390
+ op14: (input: ModelBuilder<N>) => ModelBuilder<O>,
391
+ op15: (input: ModelBuilder<O>) => ModelBuilder<P>,
392
+ op16: (input: ModelBuilder<P>) => ModelBuilder<Q>,
393
+ op17: (input: ModelBuilder<Q>) => ModelBuilder<R>,
394
+ ): (input: Builder<A>) => Builder<R>
321
395
 
322
396
  export function model(...ops: any[]) {
323
397
  return (input: Builder<any>) => {
@@ -1,4 +1,4 @@
1
- import { LikeC4DeploymentModel } from '../../model'
1
+ import { LikeC4DeploymentModel } from '../../model/DeploymentModel'
2
2
  import type { RelationshipModel } from '../../model/RelationModel'
3
3
  import type { ExpressionV2, Filterable, OperatorPredicate } from '../../types'
4
4
  import type { Ctx, Memory, Stage, StageExclude, StageInclude } from './memory'
@@ -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'
@@ -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
  }
@@ -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)
@@ -1,3 +1,14 @@
1
- import objectHash from 'object-hash'
1
+ import objecthash from 'object-hash'
2
+ import { isNonNullish } from 'remeda'
3
+ import { invariant } from '../errors'
2
4
 
3
- export { objectHash }
5
+ export function objectHash(value: any): string {
6
+ invariant(
7
+ typeof value === 'object' && isNonNullish(value),
8
+ 'objectHash: value must be an object',
9
+ )
10
+ return objecthash(value, {
11
+ ignoreUnknown: true,
12
+ respectType: false,
13
+ })
14
+ }