@likec4/core 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/compute-view/index.d.mts +91 -0
  2. package/dist/compute-view/index.d.ts +91 -0
  3. package/dist/compute-view/index.mjs +79 -0
  4. package/dist/index.d.mts +248 -435
  5. package/dist/index.d.ts +248 -435
  6. package/dist/index.mjs +418 -1322
  7. package/dist/model/index.d.mts +642 -0
  8. package/dist/model/index.d.ts +642 -0
  9. package/dist/model/index.mjs +2 -0
  10. package/dist/shared/core.BBgiAAv1.mjs +144 -0
  11. package/dist/shared/core.BPyJvuMU.mjs +2420 -0
  12. package/dist/shared/core.BQ4AbfvB.mjs +9228 -0
  13. package/dist/shared/{core.CoLQkcYL.d.ts → core.DMYY7C8s.d.mts} +398 -249
  14. package/dist/shared/{core.CoLQkcYL.d.cts → core.DMYY7C8s.d.ts} +398 -249
  15. package/dist/shared/core.k12s_keD.mjs +203 -0
  16. package/dist/types/index.d.mts +69 -1
  17. package/dist/types/index.d.ts +69 -1
  18. package/dist/types/index.mjs +21 -1
  19. package/package.json +40 -42
  20. package/src/builder/Builder.deployment.ts +219 -0
  21. package/src/builder/Builder.deploymentModel.ts +360 -0
  22. package/src/builder/Builder.element.ts +17 -19
  23. package/src/builder/Builder.model.ts +13 -2
  24. package/src/builder/Builder.test-d.ts +59 -4
  25. package/src/builder/Builder.ts +238 -53
  26. package/src/builder/Builder.view.ts +238 -53
  27. package/src/builder/_types.ts +87 -15
  28. package/src/compute-view/LikeC4ModelGraph.ts +330 -0
  29. package/src/compute-view/compute-view.ts +92 -0
  30. package/src/compute-view/deployment-view/Memory.ts +108 -0
  31. package/src/compute-view/deployment-view/Stage.ts +145 -0
  32. package/src/compute-view/deployment-view/__test__/fixture.ts +210 -0
  33. package/src/compute-view/deployment-view/_types.ts +29 -0
  34. package/src/compute-view/deployment-view/clean-connections.ts +229 -0
  35. package/src/compute-view/deployment-view/compute.ts +111 -0
  36. package/src/compute-view/deployment-view/predicates/deployment-ref.ts +146 -0
  37. package/src/compute-view/deployment-view/predicates/relation-direct.ts +130 -0
  38. package/src/compute-view/deployment-view/predicates/relation-in-out.ts +43 -0
  39. package/src/compute-view/deployment-view/predicates/relation-incoming.ts +46 -0
  40. package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +49 -0
  41. package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
  42. package/src/compute-view/deployment-view/utils.ts +212 -0
  43. package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
  44. package/src/compute-view/dynamic-view/compute.ts +310 -0
  45. package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +456 -0
  46. package/src/compute-view/element-view/__test__/fixture.ts +645 -0
  47. package/src/compute-view/element-view/compute.ts +734 -0
  48. package/src/compute-view/element-view/predicates.ts +517 -0
  49. package/src/compute-view/index.ts +5 -0
  50. package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
  51. package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
  52. package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
  53. package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
  54. package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
  55. package/src/compute-view/utils/buildComputedNodes.ts +148 -0
  56. package/src/compute-view/utils/buildElementNotations.ts +63 -0
  57. package/src/compute-view/utils/derive-edge-props-from-relationships.ts +97 -0
  58. package/src/compute-view/utils/elementExpressionToPredicate.ts +52 -0
  59. package/src/compute-view/utils/link-nodes-with-edges.ts +58 -0
  60. package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
  61. package/src/compute-view/utils/resolve-extended-views.ts +65 -0
  62. package/src/compute-view/utils/resolve-global-rules.ts +83 -0
  63. package/src/compute-view/utils/sortNodes.ts +101 -0
  64. package/src/compute-view/utils/topological-sort.ts +160 -0
  65. package/src/compute-view/utils/uniqueTags.test.ts +42 -0
  66. package/src/compute-view/utils/uniqueTags.ts +21 -0
  67. package/src/compute-view/utils/view-hash.ts +27 -0
  68. package/src/compute-view/utils/with-readable-edges.ts +31 -0
  69. package/src/index.ts +5 -1
  70. package/src/model/DeploymentElementModel.ts +534 -0
  71. package/src/model/DeploymentModel.ts +364 -0
  72. package/src/model/ElementModel.ts +195 -0
  73. package/src/model/LikeC4Model.test-d.ts +151 -0
  74. package/src/model/LikeC4Model.ts +280 -457
  75. package/src/model/RelationModel.ts +84 -0
  76. package/src/model/__test__/fixture.ts +153 -338
  77. package/src/model/connection/ConnectionModel.ts +94 -0
  78. package/src/model/connection/DeploymentConnectionModel.ts +99 -0
  79. package/src/model/connection/deployment.ts +124 -0
  80. package/src/model/connection/model.ts +123 -0
  81. package/src/model/index.ts +4 -2
  82. package/src/model/types.ts +95 -15
  83. package/src/model/view/EdgeModel.ts +103 -0
  84. package/src/model/view/LikeC4ViewModel.ts +220 -0
  85. package/src/model/view/NodeModel.ts +228 -0
  86. package/src/types/_common.ts +2 -0
  87. package/src/types/deployments.ts +237 -0
  88. package/src/types/element.ts +7 -3
  89. package/src/types/expression.ts +8 -6
  90. package/src/types/index.ts +1 -0
  91. package/src/types/model.ts +68 -9
  92. package/src/types/overview-graph.ts +2 -2
  93. package/src/types/relation.ts +4 -4
  94. package/src/types/view-notation.ts +2 -2
  95. package/src/types/view.ts +104 -30
  96. package/src/utils/{commonHead.ts → common-head.ts} +2 -0
  97. package/src/utils/fqn.ts +83 -27
  98. package/src/utils/getOrCreate.ts +42 -0
  99. package/src/utils/graphlib.ts +10 -0
  100. package/src/utils/index.ts +4 -1
  101. package/src/utils/object-hash.ts +3 -0
  102. package/src/utils/set.ts +76 -0
  103. package/src/utils/string-hash.ts +16 -0
  104. package/dist/index.cjs +0 -5452
  105. package/dist/index.d.cts +0 -1052
  106. package/dist/shared/core.CoLQkcYL.d.mts +0 -734
  107. package/dist/shared/core.v5DxGcX6.mjs +0 -293
  108. package/dist/shared/core.zN6ZscFQ.cjs +0 -353
  109. package/dist/types/index.cjs +0 -70
  110. package/dist/types/index.d.cts +0 -2
  111. package/src/model/LikeC4DiagramModel.ts +0 -338
  112. package/src/model/LikeC4ViewModel.ts +0 -320
@@ -0,0 +1,65 @@
1
+ import { first, last, values } from 'remeda'
2
+ import { isExtendsElementView, type LikeC4View } from '../../types/view'
3
+ import { findCycles, Graph, isAcyclic, postorder } from '../../utils/graphlib'
4
+
5
+ /**
6
+ * Resolve rules of extended views
7
+ * (Removes invalid views)
8
+ */
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
11
+ unresolvedViews: V
12
+ ): V {
13
+ const g = new Graph({
14
+ directed: true,
15
+ multigraph: false,
16
+ compound: false
17
+ })
18
+ for (const view of values(unresolvedViews)) {
19
+ g.setNode(view.id, view.id)
20
+ if (isExtendsElementView(view)) {
21
+ // view -> parent
22
+ g.setEdge(view.id, view.extends)
23
+ }
24
+ }
25
+ if (g.edgeCount() === 0) {
26
+ return unresolvedViews
27
+ }
28
+
29
+ // Remove circular dependencies
30
+ while (!isAcyclic(g)) {
31
+ const firstCycle = first(findCycles(g))
32
+ if (!firstCycle) {
33
+ break
34
+ }
35
+ const cycledNode = last(firstCycle)
36
+ if (!cycledNode) {
37
+ break
38
+ }
39
+ g.removeNode(cycledNode)
40
+ }
41
+
42
+ const ordered = postorder(g, g.sources() as unknown as string[])
43
+
44
+ return ordered.reduce((acc, id) => {
45
+ const view = unresolvedViews[id]
46
+ if (!view) {
47
+ return acc
48
+ }
49
+ if (isExtendsElementView(view)) {
50
+ const extendsFrom = acc[view.extends]
51
+ if (!extendsFrom) {
52
+ console.warn(`View "${view.id}" extends from "${view.extends}" which does not exist`)
53
+ return acc
54
+ }
55
+ return Object.assign(acc, {
56
+ [view.id]: {
57
+ ...extendsFrom,
58
+ ...view,
59
+ rules: [...extendsFrom.rules, ...view.rules]
60
+ }
61
+ })
62
+ }
63
+ return Object.assign(acc, { [view.id]: view })
64
+ }, {} as V)
65
+ }
@@ -0,0 +1,83 @@
1
+ import { isNullish } from 'remeda'
2
+ import { nonexhaustive } from '../../errors'
3
+ import {
4
+ type DynamicView,
5
+ type DynamicViewRule,
6
+ type ElementView,
7
+ isDynamicView,
8
+ isElementView,
9
+ isViewRuleGlobalPredicateRef,
10
+ isViewRuleGlobalStyle,
11
+ type ModelGlobals,
12
+ type ViewRule,
13
+ type ViewRuleGlobalPredicateRef,
14
+ type ViewRuleGlobalStyle
15
+ } from '../../types'
16
+
17
+ export function resolveGlobalRules<V extends DynamicView | ElementView>(
18
+ view: V,
19
+ globals: ModelGlobals
20
+ ): V {
21
+ if (isElementView(view)) {
22
+ return {
23
+ ...view,
24
+ rules: resolveGlobalRulesInElementView(view, globals)
25
+ }
26
+ } else if (isDynamicView(view)) {
27
+ return {
28
+ ...view,
29
+ rules: resolveGlobalRulesInDynamicView(view, globals)
30
+ }
31
+ }
32
+ nonexhaustive(view)
33
+ }
34
+
35
+ type ViewRuleGlobal = ViewRuleGlobalPredicateRef | ViewRuleGlobalStyle
36
+
37
+ export function resolveGlobalRulesInElementView(
38
+ view: ElementView,
39
+ globals: ModelGlobals
40
+ ): Array<Exclude<ViewRule, ViewRuleGlobal>> {
41
+ return view.rules.reduce((acc, rule) => {
42
+ if (isViewRuleGlobalPredicateRef(rule)) {
43
+ const globalPredicates = globals.predicates[rule.predicateId]
44
+ if (isNullish(globalPredicates)) {
45
+ return acc
46
+ }
47
+ return acc.concat(globalPredicates)
48
+ }
49
+ if (isViewRuleGlobalStyle(rule)) {
50
+ const globalStyles = globals.styles[rule.styleId]
51
+ if (isNullish(globalStyles)) {
52
+ return acc
53
+ }
54
+ return acc.concat(globalStyles)
55
+ }
56
+ acc.push(rule)
57
+ return acc
58
+ }, [] as Array<Exclude<ViewRule, ViewRuleGlobal>>)
59
+ }
60
+
61
+ export function resolveGlobalRulesInDynamicView(
62
+ view: DynamicView,
63
+ globals: ModelGlobals
64
+ ): Array<Exclude<DynamicViewRule, ViewRuleGlobal>> {
65
+ return view.rules.reduce((acc, rule) => {
66
+ if (isViewRuleGlobalPredicateRef(rule)) {
67
+ const globalPredicates = globals.dynamicPredicates[rule.predicateId]
68
+ if (isNullish(globalPredicates)) {
69
+ return acc
70
+ }
71
+ return acc.concat(globalPredicates)
72
+ }
73
+ if (isViewRuleGlobalStyle(rule)) {
74
+ const globalStyles = globals.styles[rule.styleId]
75
+ if (isNullish(globalStyles)) {
76
+ return acc
77
+ }
78
+ return acc.concat(globalStyles)
79
+ }
80
+ acc.push(rule)
81
+ return acc
82
+ }, [] as Array<Exclude<DynamicViewRule, ViewRuleGlobal>>)
83
+ }
@@ -0,0 +1,101 @@
1
+ import { difference, filter, map, pipe, sort, tap } from 'remeda'
2
+ import { invariant, nonNullable } from '../../errors'
3
+ import type { Fqn } from '../../types/element'
4
+ import type { ComputedEdge, ComputedNode, EdgeId } from '../../types/view'
5
+ import { compareByFqnHierarchically } from '../../utils/fqn'
6
+ import { Graph, postorder } from '../../utils/graphlib'
7
+ import { compareRelations } from '../../utils/relations'
8
+
9
+ // side effect
10
+ function sortChildren(nodes: readonly ComputedNode[]) {
11
+ nodes.forEach(parent => {
12
+ if (parent.children.length > 0) {
13
+ parent.children = nodes.flatMap(n => (n.parent === parent.id ? n.id : []))
14
+ }
15
+ })
16
+ }
17
+
18
+ export function sortNodes({
19
+ nodes,
20
+ edges
21
+ }: {
22
+ nodes: ComputedNode[]
23
+ edges: ComputedEdge[]
24
+ }): ComputedNode[] {
25
+ if (nodes.length < 2) {
26
+ return nodes
27
+ }
28
+ if (edges.length === 0) {
29
+ return pipe(
30
+ nodes,
31
+ sort(compareByFqnHierarchically),
32
+ tap(sortChildren)
33
+ )
34
+ }
35
+
36
+ const g = new Graph({
37
+ compound: false,
38
+ directed: true,
39
+ multigraph: false
40
+ })
41
+
42
+ const getNode = (id: Fqn) =>
43
+ nonNullable(
44
+ nodes.find(n => n.id === id),
45
+ 'Edge not found'
46
+ )
47
+ const getEdge = (id: EdgeId) =>
48
+ nonNullable(
49
+ edges.find(edge => edge.id === id),
50
+ 'Edge not found'
51
+ )
52
+
53
+ for (const e of [...edges].sort(compareRelations)) {
54
+ g.setEdge(e.source, e.target)
55
+ }
56
+
57
+ for (const n of nodes) {
58
+ g.setNode(n.id, n.id)
59
+ if (n.children.length > 0) {
60
+ n.inEdges.forEach(e => {
61
+ const edge = getEdge(e)
62
+ // if this edge from leaf to the child of this node
63
+ if (edge.target !== n.id && getNode(edge.source).children.length === 0) {
64
+ // const id = `${edge.source}:${n.id}`
65
+ g.setEdge(edge.source, n.id)
66
+ }
67
+ })
68
+ // n.outEdges.forEach(e => {
69
+ // const edge = getEdge(e)
70
+ // if (edge.source !== n.id) {
71
+ // const id = `${n.id}:${edge.target}`
72
+ // g.setEdge(n.id, edge.target, undefined, id)
73
+ // }
74
+ // })
75
+ }
76
+ if (n.parent) {
77
+ g.setEdge(n.parent, n.id)
78
+ }
79
+ }
80
+
81
+ let sources = g.sources() as unknown as Fqn[]
82
+ if (sources.length === 0) {
83
+ sources = pipe(
84
+ nodes,
85
+ filter(n => n.inEdges.length === 0 || n.parent === null),
86
+ sort(compareByFqnHierarchically),
87
+ map(n => n.id)
88
+ )
89
+ }
90
+ // sources = sources.sort(compareFqnHierarchically)
91
+ const orderedIds = postorder(g, sources).reverse() as Fqn[]
92
+ const sorted = orderedIds.map(getNode)
93
+ if (sorted.length < nodes.length) {
94
+ const unsorted = difference(nodes, sorted).sort(compareByFqnHierarchically)
95
+ sorted.push(...unsorted)
96
+ }
97
+
98
+ invariant(sorted.length === nodes.length, 'Not all nodes were processed by graphlib')
99
+ sortChildren(sorted)
100
+ return sorted
101
+ }
@@ -0,0 +1,160 @@
1
+ import Graph from 'graphology'
2
+ import { topologicalSort as topsort } from 'graphology-dag/topological-sort'
3
+ import willCreateCycle from 'graphology-dag/will-create-cycle'
4
+ import { forEach, map, pipe, prop, sortBy } from 'remeda'
5
+ import { invariant, nonNullable } from '../../errors'
6
+ import type { ComputedEdge, ComputedNode, NodeId } from '../../types'
7
+
8
+ /**
9
+ * Keeps initial order of the elements, but ensures that parents are before children
10
+ */
11
+ function ensureParentsFirst<T extends { id: string; parent: string | null }>(
12
+ array: ReadonlyArray<T>
13
+ ): Array<T> {
14
+ const result = [] as T[]
15
+ const items = [...array]
16
+ let item
17
+ while ((item = items.shift())) {
18
+ let parent = item.parent
19
+ if (parent) {
20
+ const ancestors = [] as T[]
21
+ while (parent) {
22
+ const parentIndx = items.findIndex(i => i.id === parent)
23
+ if (parentIndx < 0) {
24
+ break
25
+ }
26
+ const [parentItem] = items.splice(parentIndx, 1)
27
+ if (!parentItem) {
28
+ throw new Error('Invalid state, should not happen')
29
+ }
30
+ ancestors.unshift(parentItem)
31
+ parent = parentItem.parent
32
+ }
33
+ result.push(...ancestors)
34
+ }
35
+ result.push(item)
36
+ }
37
+ return result as Array<T>
38
+ }
39
+
40
+ /**
41
+ * Side effect, mutates node.children field to preserve same order as in the input
42
+ */
43
+ function updateChildren(nodes: ComputedNode[]) {
44
+ nodes.forEach(parent => {
45
+ if (parent.children.length > 0) {
46
+ parent.children = nodes.reduce((acc, n) => {
47
+ if (n.parent === parent.id) {
48
+ acc.push(n.id)
49
+ }
50
+ return acc
51
+ }, [] as NodeId[])
52
+ }
53
+ })
54
+ return nodes
55
+ }
56
+
57
+ type TopologicalSortParam = {
58
+ nodes: Iterable<ComputedNode>
59
+ edges: Iterable<ComputedEdge>
60
+ }
61
+ export function topologicalSort(
62
+ param: TopologicalSortParam
63
+ ): {
64
+ nodes: ComputedNode[]
65
+ edges: ComputedEdge[]
66
+ } {
67
+ let nodes = [...param.nodes]
68
+ let edges = [...param.edges]
69
+ if (nodes.length < 2) {
70
+ return {
71
+ nodes,
72
+ edges
73
+ }
74
+ }
75
+ nodes = ensureParentsFirst(nodes)
76
+
77
+ if (edges.length === 0) {
78
+ return {
79
+ nodes,
80
+ edges
81
+ }
82
+ }
83
+
84
+ const getNode = (id: string) => nonNullable(nodes.find(n => n.id === id))
85
+ const nodeLevel = (id: string) => getNode(id).level + 1
86
+ const getEdge = (id: string) => nonNullable(edges.find(edge => edge.id === id))
87
+
88
+ const g = new Graph({
89
+ multi: true,
90
+ allowSelfLoops: true,
91
+ type: 'directed'
92
+ })
93
+
94
+ for (const n of nodes) {
95
+ g.addNode(n.id)
96
+ }
97
+ const sortedEdges = [] as ComputedEdge[]
98
+
99
+ pipe(
100
+ edges,
101
+ map(e => ({
102
+ e,
103
+ parentLevel: e.parent ? nodeLevel(e.parent) : 0,
104
+ sourceLevel: nodeLevel(e.source),
105
+ targetLevel: nodeLevel(e.target),
106
+ sourceIndex: nodes.findIndex(n => n.id === e.source),
107
+ targetIndex: nodes.findIndex(n => n.id === e.target)
108
+ })),
109
+ sortBy(
110
+ [prop('parentLevel'), 'desc'],
111
+ [prop('sourceLevel'), 'desc'],
112
+ [prop('targetLevel'), 'desc'],
113
+ [prop('sourceIndex'), 'asc'],
114
+ [prop('targetIndex'), 'asc']
115
+ ),
116
+ forEach(({ e }, idx, _all) => {
117
+ sortedEdges.push(e)
118
+ if (idx === 0 || !willCreateCycle(g, e.source, e.target)) {
119
+ g.mergeDirectedEdge(e.source, e.target)
120
+ }
121
+ })
122
+ )
123
+
124
+ // Strengthen the graph by adding edges to compound nodes
125
+ for (const n of nodes) {
126
+ if (n.children.length > 0) {
127
+ n.inEdges.forEach(e => {
128
+ const edge = getEdge(e)
129
+ // if this edge from leaf to the child of this node
130
+ if (edge.target !== n.id && getNode(edge.source).children.length === 0) {
131
+ if (!willCreateCycle(g, edge.source, n.id)) {
132
+ g.mergeDirectedEdge(edge.source, n.id)
133
+ }
134
+ }
135
+ })
136
+ }
137
+ if (n.parent && !willCreateCycle(g, n.parent, n.id)) {
138
+ g.mergeDirectedEdge(n.parent, n.id)
139
+ }
140
+ }
141
+
142
+ const sortedIds = topsort(g)
143
+ const sorted = [] as ComputedNode[]
144
+ for (const sortedId of sortedIds) {
145
+ const indx = nodes.findIndex(n => n.id === sortedId)
146
+ invariant(indx >= 0, `Node "${sortedId}" not found`)
147
+ sorted.push(...nodes.splice(indx, 1))
148
+ }
149
+ if (nodes.length > 0) {
150
+ sorted.push(...nodes)
151
+ }
152
+ return {
153
+ nodes: pipe(
154
+ sorted,
155
+ ensureParentsFirst,
156
+ updateChildren
157
+ ),
158
+ edges: sortedEdges
159
+ }
160
+ }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { uniqueTags } from './uniqueTags'
3
+
4
+ describe('uniqueTags function', () => {
5
+ it('returns unique tags from an array of elements', () => {
6
+ const input = [
7
+ { tags: ['tag1', 'tag2', 'tag3'] },
8
+ { tags: ['tag2', 'tag3', 'tag4'] },
9
+ { tags: ['tag3', 'tag4', 'tag5'] }
10
+ ] as const
11
+ const result = uniqueTags(input)
12
+ expect(result).toEqual(['tag1', 'tag2', 'tag3', 'tag4', 'tag5'])
13
+ })
14
+
15
+ it('should return unique tags naturally sorted', () => {
16
+ const input = [
17
+ { tags: ['tag1', 'tag20', 'tag30'] },
18
+ { tags: ['tag2', 'tag23', 'tag34'] },
19
+ { tags: ['tag3'] }
20
+ ] as const
21
+ const result = uniqueTags(input)
22
+ expect(result).toEqual([
23
+ 'tag1',
24
+ 'tag2',
25
+ 'tag3',
26
+ 'tag20',
27
+ 'tag23',
28
+ 'tag30',
29
+ 'tag34'
30
+ ])
31
+ // ].sort(compareNatural))
32
+ })
33
+
34
+ it('returns null if the tags array is null', () => {
35
+ const input = [
36
+ { tags: null },
37
+ {}
38
+ ]
39
+ const result = uniqueTags(input)
40
+ expect(result).toBeNull()
41
+ })
42
+ })
@@ -0,0 +1,21 @@
1
+ import { flatMap, hasAtLeast, pipe, sort, unique } from 'remeda'
2
+ import type { LiteralUnion } from 'type-fest'
3
+ import type { NonEmptyReadonlyArray } from '../../types/_common'
4
+ import type { Tag } from '../../types/element'
5
+ import { compareNatural } from '../../utils/compare-natural'
6
+
7
+ /**
8
+ * Extracts unique tags from an array of elements.
9
+ * and sort in natural order; returns null if no tags are present.
10
+ */
11
+ export function uniqueTags<T extends { tags?: NonEmptyReadonlyArray<LiteralUnion<Tag, string>> | null }>(
12
+ elements: ReadonlyArray<T>
13
+ ) {
14
+ const tags = pipe(
15
+ elements,
16
+ flatMap(e => e.tags ?? []),
17
+ unique(),
18
+ sort(compareNatural)
19
+ )
20
+ return hasAtLeast(tags, 1) ? tags as NonEmptyReadonlyArray<Tag> : null
21
+ }
@@ -0,0 +1,27 @@
1
+ import { isTruthy, map, mapToObj, pick, pipe } from 'remeda'
2
+ import type { SetOptional } from 'type-fest'
3
+ import type { ComputedView } from '../../types'
4
+ import { objectHash } from '../../utils/object-hash'
5
+
6
+ export function calcViewLayoutHash<V extends ComputedView>(view: SetOptional<V, 'hash'>): V {
7
+ const tohash = {
8
+ id: view.id,
9
+ __: view.__ ?? 'element',
10
+ autoLayout: view.autoLayout,
11
+ nodes: pipe(
12
+ view.nodes,
13
+ map(pick(['id', 'title', 'description', 'technology', 'shape', 'icon', 'children'])),
14
+ mapToObj(({ id, icon, ...node }) => [id, { ...node, icon: isTruthy(icon) ? 'Y' : 'N' }])
15
+ ),
16
+ edges: pipe(
17
+ view.edges,
18
+ map(pick(['source', 'target', 'label', 'description', 'technology', 'dir', 'head', 'tail', 'line'])),
19
+ mapToObj(({ source, target, ...edge }) => [`${source}:${target}`, edge])
20
+ )
21
+ }
22
+ view.hash = objectHash(tohash, {
23
+ ignoreUnknown: true,
24
+ respectType: false
25
+ })
26
+ return view as V
27
+ }
@@ -0,0 +1,31 @@
1
+ import { map, mapToObj, mapValues } from 'remeda'
2
+ import type { ComputedLikeC4Model } from '../../types'
3
+ import type { ComputedView, EdgeId } from '../../types/view'
4
+
5
+ /**
6
+ * Convert hashed edge ids to human-readable
7
+ * Mostly for testing purposes
8
+ */
9
+ export function withReadableEdges<T extends ComputedView>({ edges, nodes, ...view }: T): T {
10
+ const edgeids = mapToObj(edges, e => [e.id, `${e.source}:${e.target}` as EdgeId])
11
+
12
+ return {
13
+ ...view,
14
+ edges: edges.map(e => ({
15
+ ...e,
16
+ id: edgeids[e.id]!
17
+ })),
18
+ nodes: nodes.map(n => ({
19
+ ...n,
20
+ inEdges: map(n.inEdges, e => edgeids[e]!),
21
+ outEdges: map(n.outEdges, e => edgeids[e]!)
22
+ }))
23
+ } as T
24
+ }
25
+
26
+ export function viewsWithReadableEdges<M extends ComputedLikeC4Model>({ views, ...model }: M): M {
27
+ return {
28
+ ...model,
29
+ views: mapValues(views, withReadableEdges)
30
+ } as M
31
+ }
package/src/index.ts CHANGED
@@ -6,11 +6,15 @@ export * from './errors'
6
6
  export * from './model'
7
7
 
8
8
  export * from './types'
9
- export * from './utils/commonHead'
9
+ export * from './utils/common-head'
10
10
  export * from './utils/compare-natural'
11
11
  export * from './utils/fqn'
12
+ export * from './utils/getOrCreate'
12
13
  export * from './utils/guards'
14
+ export * from './utils/object-hash'
13
15
  export * from './utils/promises'
14
16
  export { compareRelations } from './utils/relations'
17
+ export * from './utils/set'
18
+ export * from './utils/string-hash'
15
19
 
16
20
  export * as Expr from './types/expression'