@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.
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 +246 -434
  5. package/dist/index.d.ts +246 -434
  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.mts → core.DMYY7C8s.d.mts} +398 -249
  14. package/dist/shared/{core.CoLQkcYL.d.ts → 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 +38 -40
  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 -1053
  106. package/dist/shared/core.BXTs-8n7.mjs +0 -293
  107. package/dist/shared/core.CoLQkcYL.d.cts +0 -734
  108. package/dist/shared/core.DIYZvpVg.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,66 @@
1
+ import { isEmpty, isNullish, omitBy } from 'remeda'
2
+ import { ComputedNode, isCustomElement, isViewRuleGroup, isViewRulePredicate, type ViewRule } from '../../types'
3
+ import type { Expression } from '../../types/expression'
4
+ import { elementExprToPredicate } from './elementExpressionToPredicate'
5
+
6
+ export function flattenGroupRules<T extends Expression>(guard: (expr: Expression) => expr is T) {
7
+ return (rule: ViewRule): Array<T> => {
8
+ if (isViewRuleGroup(rule)) {
9
+ return rule.groupRules.flatMap(flattenGroupRules(guard))
10
+ }
11
+ if (isViewRulePredicate(rule)) {
12
+ return 'include' in rule ? rule.include.filter(guard) : []
13
+ }
14
+
15
+ return []
16
+ }
17
+ }
18
+
19
+ export function applyCustomElementProperties(_rules: ViewRule[], _nodes: ComputedNode[]) {
20
+ const rules = _rules.flatMap(flattenGroupRules(isCustomElement))
21
+ if (rules.length === 0) {
22
+ return _nodes
23
+ }
24
+ const nodes = [..._nodes]
25
+ for (
26
+ const {
27
+ custom: { expr, ...props }
28
+ } of rules
29
+ ) {
30
+ const { border, opacity, ...rest } = omitBy(props, isNullish)
31
+ const notEmpty = !isEmpty(rest)
32
+ const satisfies = elementExprToPredicate(expr)
33
+ nodes.forEach((node, i) => {
34
+ if (ComputedNode.isNodesGroup(node) || !satisfies(node)) {
35
+ return
36
+ }
37
+ if (notEmpty) {
38
+ node = {
39
+ ...node,
40
+ isCustomized: true,
41
+ ...rest
42
+ }
43
+ }
44
+
45
+ let styleOverride: ComputedNode['style'] | undefined
46
+ if (border !== undefined) {
47
+ styleOverride = { border }
48
+ }
49
+ if (opacity !== undefined) {
50
+ styleOverride = { ...styleOverride, opacity }
51
+ }
52
+ if (styleOverride) {
53
+ node = {
54
+ ...node,
55
+ isCustomized: true,
56
+ style: {
57
+ ...node.style,
58
+ ...styleOverride
59
+ }
60
+ }
61
+ }
62
+ nodes[i] = node
63
+ })
64
+ }
65
+ return nodes
66
+ }
@@ -0,0 +1,41 @@
1
+ import { isNullish, omitBy, pick } from 'remeda'
2
+ import type { ComputedEdge, ComputedNode, ViewRule } from '../../types'
3
+ import * as Expr from '../../types/expression'
4
+ import { flattenGroupRules } from './applyCustomElementProperties'
5
+ import { relationExpressionToPredicates } from './relationExpressionToPredicates'
6
+
7
+ export function applyCustomRelationProperties(
8
+ _rules: ViewRule[],
9
+ nodes: ComputedNode[],
10
+ _edges: Iterable<ComputedEdge>
11
+ ): ComputedEdge[] {
12
+ const rules = _rules.flatMap(flattenGroupRules(Expr.isCustomRelationExpr))
13
+ const edges = Array.from(_edges)
14
+ if (rules.length === 0 || edges.length === 0) {
15
+ return edges
16
+ }
17
+ for (
18
+ const {
19
+ customRelation: { relation, title, ...customprops }
20
+ } of rules
21
+ ) {
22
+ const props = omitBy(customprops, isNullish)
23
+ const satisfies = relationExpressionToPredicates(relation)
24
+ edges.forEach((edge, i) => {
25
+ const source = nodes.find(n => n.id === edge.source)
26
+ const target = nodes.find(n => n.id === edge.target)
27
+ if (!source || !target) {
28
+ return
29
+ }
30
+ if (satisfies({ source, target, ...pick(edge, ['kind', 'tags']) })) {
31
+ edges[i] = {
32
+ ...edge,
33
+ ...props,
34
+ label: title ?? edge.label,
35
+ isCustomized: true
36
+ }
37
+ }
38
+ })
39
+ }
40
+ return edges
41
+ }
@@ -0,0 +1,53 @@
1
+ import { anyPass, filter, forEach, isDefined, isNot, pipe } from 'remeda'
2
+ import { ComputedNode, isViewRuleStyle, type ViewRule, type ViewRuleStyle } from '../../types'
3
+ import { elementExprToPredicate } from './elementExpressionToPredicate'
4
+
5
+ type Predicate<T> = (x: T) => boolean
6
+
7
+ type CommonViewRuleStyle = Pick<ViewRuleStyle, 'style' | 'notation'>
8
+
9
+ export function applyViewRuleStyle<Rule extends CommonViewRuleStyle>(
10
+ rule: Rule,
11
+ predicates: Predicate<ComputedNode>[],
12
+ nodes: ComputedNode[]
13
+ ) {
14
+ pipe(
15
+ nodes,
16
+ filter(isNot(ComputedNode.isNodesGroup)),
17
+ filter(anyPass(predicates)),
18
+ forEach(n => {
19
+ n.shape = rule.style.shape ?? n.shape
20
+ n.color = rule.style.color ?? n.color
21
+ if (isDefined(rule.style.icon)) {
22
+ n.icon = rule.style.icon
23
+ }
24
+ if (isDefined(rule.notation)) {
25
+ n.notation = rule.notation
26
+ }
27
+ let styleOverride: ComputedNode['style'] | undefined
28
+ if (isDefined(rule.style.border)) {
29
+ styleOverride = { border: rule.style.border }
30
+ }
31
+ if (isDefined(rule.style.opacity)) {
32
+ styleOverride = { ...styleOverride, opacity: rule.style.opacity }
33
+ }
34
+ if (styleOverride) {
35
+ n.style = {
36
+ ...n.style,
37
+ ...styleOverride
38
+ }
39
+ }
40
+ })
41
+ )
42
+ }
43
+
44
+ export function applyViewRuleStyles(rules: ViewRule[], nodes: ComputedNode[]) {
45
+ for (const rule of rules) {
46
+ if (!isViewRuleStyle(rule) || rule.targets.length === 0) {
47
+ continue
48
+ }
49
+ const predicates = rule.targets.map(elementExprToPredicate)
50
+ applyViewRuleStyle(rule, predicates, nodes)
51
+ }
52
+ return nodes
53
+ }
@@ -0,0 +1,148 @@
1
+ import type { Simplify } from 'type-fest'
2
+ import { nonNullable } from '../../errors'
3
+ import {
4
+ type ComputedNode,
5
+ DefaultElementShape,
6
+ DefaultThemeColor,
7
+ type Element,
8
+ ElementKind,
9
+ type Fqn,
10
+ type NodeId
11
+ } from '../../types'
12
+ import { compareByFqnHierarchically, parentFqn } from '../../utils/fqn'
13
+ import { NodesGroup } from '../element-view/compute'
14
+
15
+ function updateDepthOfAncestors(node: ComputedNode, nodes: ReadonlyMap<Fqn, ComputedNode>) {
16
+ let parentNd
17
+ while (!!node.parent && (parentNd = nodes.get(node.parent))) {
18
+ const depth = parentNd.depth ?? 1
19
+ parentNd.depth = Math.max(depth, (node.depth ?? 0) + 1)
20
+ if (parentNd.depth === depth) {
21
+ // stop if we didn't change depth
22
+ break
23
+ }
24
+ node = parentNd
25
+ }
26
+ }
27
+
28
+ const modelElementAsNodeSource = (element: Element): ComputedNodeSource => {
29
+ return {
30
+ ...element,
31
+ modelRef: 1
32
+ }
33
+ }
34
+
35
+ // type ComputedNodeSource = Simplify<SetRequired<Partial<Omit<ComputedNode, 'parent' | 'children' | 'inEdges' | 'outEdges' | 'level' | 'depth'>>, 'id' | 'title' | 'kind'>>
36
+ export type ComputedNodeSource = Simplify<
37
+ & Pick<ComputedNode, 'id' | 'title' | 'kind' | 'deploymentRef' | 'modelRef'>
38
+ & Partial<Omit<Element, 'id' | 'title' | 'kind'>>
39
+ >
40
+
41
+ export function buildComputedNodesFromElements(elements: ReadonlyArray<Element>, groups?: NodesGroup[]) {
42
+ return buildComputedNodes(elements.map(modelElementAsNodeSource), groups)
43
+ }
44
+
45
+ export function buildComputedNodes(
46
+ elements: ReadonlyArray<ComputedNodeSource>,
47
+ groups?: NodesGroup[]
48
+ ): ReadonlyMap<Fqn, ComputedNode> {
49
+ const nodesMap = new Map<Fqn, ComputedNode>()
50
+
51
+ const elementToGroup = new Map<Fqn, NodeId>()
52
+
53
+ groups?.forEach(({ id, parent, viewRule, explicits }) => {
54
+ if (parent) {
55
+ nonNullable(nodesMap.get(parent), `Parent group node ${parent} not found`).children.push(id)
56
+ }
57
+ nodesMap.set(id, {
58
+ id,
59
+ parent,
60
+ kind: ElementKind.Group,
61
+ title: viewRule.title ?? '',
62
+ color: viewRule.color ?? 'muted',
63
+ shape: 'rectangle',
64
+ children: [],
65
+ inEdges: [],
66
+ outEdges: [],
67
+ level: 0,
68
+ depth: 0,
69
+ description: null,
70
+ technology: null,
71
+ tags: null,
72
+ links: null,
73
+ style: {
74
+ border: viewRule.border ?? 'dashed',
75
+ opacity: viewRule.opacity ?? 0
76
+ }
77
+ })
78
+ for (const e of explicits) {
79
+ elementToGroup.set(e.id, id)
80
+ }
81
+ })
82
+
83
+ // Ensure that parent nodes are created before child nodes
84
+ Array.from(elements)
85
+ .sort(compareByFqnHierarchically)
86
+ .forEach(({ id, style, kind, title, color, shape, ...el }) => {
87
+ let parent = parentFqn(id)
88
+ let level = 0
89
+ let parentNd: ComputedNode | undefined
90
+ // Find the first ancestor that is already in the map
91
+ while (parent) {
92
+ parentNd = nodesMap.get(parent)
93
+ if (parentNd) {
94
+ break
95
+ }
96
+ parent = parentFqn(parent)
97
+ }
98
+ // If parent is not found in the map, check if it is in a group
99
+ if (!parentNd && elementToGroup.has(id)) {
100
+ parent = elementToGroup.get(id)!
101
+ parentNd = nodesMap.get(parent)!
102
+ }
103
+ if (parentNd) {
104
+ // if parent has no children and we are about to add first one
105
+ // we need to set its depth to 1
106
+ if (parentNd.children.length == 0) {
107
+ parentNd.depth = 1
108
+ // go up the tree and update depth of all parents
109
+ updateDepthOfAncestors(parentNd, nodesMap)
110
+ }
111
+ parentNd.children.push(id)
112
+ level = parentNd.level + 1
113
+ }
114
+ const node: ComputedNode = {
115
+ id,
116
+ parent,
117
+ kind,
118
+ title,
119
+ level,
120
+ color: color ?? DefaultThemeColor,
121
+ shape: shape ?? DefaultElementShape,
122
+ description: null,
123
+ technology: null,
124
+ tags: null,
125
+ links: null,
126
+ children: [],
127
+ inEdges: [],
128
+ outEdges: [],
129
+ ...el,
130
+ style: {
131
+ ...style
132
+ }
133
+ }
134
+ nodesMap.set(id, node)
135
+ })
136
+
137
+ // Create new map and add elements in the same order as they were in the input
138
+ const orderedMap = new Map<Fqn, ComputedNode>()
139
+
140
+ groups?.forEach(({ id }) => {
141
+ orderedMap.set(id, nonNullable(nodesMap.get(id)))
142
+ })
143
+ elements.forEach(({ id }) => {
144
+ orderedMap.set(id, nonNullable(nodesMap.get(id)))
145
+ })
146
+
147
+ return orderedMap
148
+ }
@@ -0,0 +1,63 @@
1
+ import { entries, flatMap, groupBy, map, mapValues, pipe, piped, prop, sortBy, unique } from 'remeda'
2
+ import type { ComputedNode, ElementNotation } from '../../types'
3
+
4
+ /**
5
+ * Build element notations from computed nodes:
6
+ * 1. Group by notation
7
+ * 2. Group by shape
8
+ * 3. Group by color
9
+ * 4. For each group get unique kinds
10
+ * 5. Unwind the groups
11
+ */
12
+ export function buildElementNotations(nodes: ComputedNode[]): ElementNotation[] {
13
+ return pipe(
14
+ nodes,
15
+ groupBy(prop('notation')),
16
+ mapValues(
17
+ piped(
18
+ groupBy(prop('shape')),
19
+ mapValues(
20
+ piped(
21
+ groupBy(prop('color')),
22
+ mapValues(
23
+ piped(
24
+ map(prop('kind')),
25
+ unique()
26
+ )
27
+ ),
28
+ entries(),
29
+ map(([color, kinds]) => ({
30
+ kinds,
31
+ color
32
+ }))
33
+ )
34
+ ),
35
+ entries(),
36
+ flatMap(([shape, colors]) =>
37
+ colors.map(({ color, kinds }) => ({
38
+ shape,
39
+ color,
40
+ kinds
41
+ }))
42
+ )
43
+ )
44
+ ),
45
+ entries(),
46
+ flatMap(([title, shapes]) =>
47
+ shapes.map(({ shape, color, kinds }) => ({
48
+ title,
49
+ shape,
50
+ color,
51
+ kinds
52
+ }))
53
+ ),
54
+ sortBy(
55
+ prop('shape'),
56
+ prop('title'),
57
+ [
58
+ n => n.kinds.length,
59
+ 'desc'
60
+ ]
61
+ )
62
+ )
63
+ }
@@ -0,0 +1,97 @@
1
+ import { isTruthy, only, pick, pickBy, pipe, reduce, unique } from 'remeda'
2
+ import type { Color, DeploymentRelation, Link, Tag, ViewId } from '../../types'
3
+ import type { Relation, RelationshipArrowType, RelationshipKind, RelationshipLineType } from '../../types/relation'
4
+ import { isNonEmptyArray } from '../../utils'
5
+
6
+ export function pickRelationshipProps(relation: Relation | DeploymentRelation) {
7
+ return pick(relation, [
8
+ 'title',
9
+ 'description',
10
+ 'technology',
11
+ 'tags',
12
+ 'links',
13
+ 'kind',
14
+ 'color',
15
+ 'line',
16
+ 'head',
17
+ 'tail',
18
+ 'navigateTo'
19
+ ])
20
+ }
21
+ type RelationshipProps = ReturnType<typeof pickRelationshipProps>
22
+
23
+ export function deriveEdgePropsFromRelationships(relations: Array<Relation | DeploymentRelation>) {
24
+ // TODO:
25
+ const allprops = pipe(
26
+ relations,
27
+ reduce(
28
+ (acc, r) => {
29
+ if (isTruthy(r.title) && !acc.title.includes(r.title)) {
30
+ acc.title.push(r.title)
31
+ }
32
+ if (isTruthy(r.description) && !acc.description.includes(r.description)) {
33
+ acc.description.push(r.description)
34
+ }
35
+ if (isTruthy(r.technology) && !acc.technology.includes(r.technology)) {
36
+ acc.technology.push(r.technology)
37
+ }
38
+ if (isTruthy(r.kind) && !acc.kind.includes(r.kind)) {
39
+ acc.kind.push(r.kind)
40
+ }
41
+ if (isTruthy(r.color) && !acc.color.includes(r.color)) {
42
+ acc.color.push(r.color)
43
+ }
44
+ if (isTruthy(r.line) && !acc.line.includes(r.line)) {
45
+ acc.line.push(r.line)
46
+ }
47
+ if (isTruthy(r.head) && !acc.head.includes(r.head)) {
48
+ acc.head.push(r.head)
49
+ }
50
+ if (isTruthy(r.tail) && !acc.tail.includes(r.tail)) {
51
+ acc.tail.push(r.tail)
52
+ }
53
+ if (isTruthy(r.navigateTo) && !acc.navigateTo.includes(r.navigateTo)) {
54
+ acc.navigateTo.push(r.navigateTo)
55
+ }
56
+ if (r.tags) {
57
+ acc.tags.push(...r.tags)
58
+ }
59
+ if (r.links) {
60
+ acc.links.push(...r.links)
61
+ }
62
+ return acc
63
+ },
64
+ {
65
+ title: [] as string[],
66
+ description: [] as string[],
67
+ technology: [] as string[],
68
+ kind: [] as RelationshipKind[],
69
+ head: [] as RelationshipArrowType[],
70
+ tail: [] as RelationshipArrowType[],
71
+ color: [] as Color[],
72
+ tags: [] as Tag[],
73
+ links: [] as Link[],
74
+ line: [] as RelationshipLineType[],
75
+ navigateTo: [] as ViewId[]
76
+ }
77
+ )
78
+ )
79
+ const tags = unique(allprops.tags)
80
+ return pickBy(
81
+ {
82
+ title: only(allprops.title) ?? (allprops.title.length > 1 ? '[...]' : null),
83
+ description: only(allprops.description),
84
+ technology: only(allprops.technology),
85
+ kind: only(allprops.kind),
86
+ head: only(allprops.head),
87
+ tail: only(allprops.tail),
88
+ color: only(allprops.color),
89
+ line: only(allprops.line),
90
+ navigateTo: only(allprops.navigateTo),
91
+ ...isNonEmptyArray(allprops.links) && { links: allprops.links },
92
+ ...isNonEmptyArray(tags) && { tags }
93
+ },
94
+ isTruthy
95
+ )
96
+ }
97
+ export type MergedRelationshipProps = ReturnType<typeof deriveEdgePropsFromRelationships>
@@ -0,0 +1,52 @@
1
+ import { isNullish } from 'remeda'
2
+ import { nonexhaustive } from '../../errors'
3
+ import {
4
+ type ComputedNode,
5
+ type Element,
6
+ type ElementPredicateExpression,
7
+ isCustomElement,
8
+ isElementKindExpr,
9
+ isElementRef,
10
+ isElementTagExpr,
11
+ isElementWhere,
12
+ isExpandedElementExpr,
13
+ isWildcard,
14
+ whereOperatorAsPredicate
15
+ } from '../../types'
16
+ import { parentFqn } from '../../utils'
17
+
18
+ type Predicate<T> = (x: T) => boolean
19
+
20
+ export function elementExprToPredicate<T extends Pick<ComputedNode, 'id' | 'kind' | 'tags'>>(
21
+ target: ElementPredicateExpression
22
+ ): Predicate<T> {
23
+ if (isElementWhere(target)) {
24
+ const predicate = elementExprToPredicate(target.where.expr)
25
+ const where = whereOperatorAsPredicate(target.where.condition)
26
+ return n => predicate(n) && where(n)
27
+ }
28
+ if (isWildcard(target)) {
29
+ return () => true
30
+ }
31
+ if (isElementKindExpr(target)) {
32
+ return target.isEqual ? n => n.kind === target.elementKind : n => n.kind !== target.elementKind
33
+ }
34
+ if (isElementTagExpr(target)) {
35
+ return target.isEqual
36
+ ? ({ tags }) => !!tags && tags.includes(target.elementTag)
37
+ : ({ tags }) => isNullish(tags) || !tags.includes(target.elementTag)
38
+ }
39
+ if (isExpandedElementExpr(target)) {
40
+ return n => n.id === target.expanded || parentFqn(n.id) === target.expanded
41
+ }
42
+ if (isElementRef(target)) {
43
+ const { element, isChildren, isDescendants } = target
44
+ return isChildren || isDescendants
45
+ ? n => n.id.startsWith(element + '.')
46
+ : n => (n.id as string) === element
47
+ }
48
+ if (isCustomElement(target)) {
49
+ return elementExprToPredicate(target.custom.expr)
50
+ }
51
+ nonexhaustive(target)
52
+ }
@@ -0,0 +1,58 @@
1
+ import { last, reverse } from 'remeda'
2
+ import { invariant } from '../../errors'
3
+ import type { Fqn } from '../../types/element'
4
+ import type { ComputedEdge, ComputedNode } from '../../types/view'
5
+ import { commonHead } from '../../utils/common-head'
6
+ import { ancestorsOfNode } from './ancestorsOfNode'
7
+
8
+ /**
9
+ * Update `inEdges` and `outEdges` props of nodes based on the edges
10
+ * Mutates nodes and updates their in/out edges
11
+ */
12
+ export function linkNodesWithEdges(nodesMap: ReadonlyMap<Fqn, ComputedNode>, edges: ComputedEdge[]) {
13
+ for (const edge of edges) {
14
+ const source = nodesMap.get(edge.source)
15
+ const target = nodesMap.get(edge.target)
16
+ invariant(source, `Source node ${edge.source} not found`)
17
+ invariant(target, `Target node ${edge.target} not found`)
18
+
19
+ source.outEdges.push(edge.id)
20
+ target.inEdges.push(edge.id)
21
+
22
+ if (edge.dir === 'both') {
23
+ source.inEdges.push(edge.id)
24
+ target.outEdges.push(edge.id)
25
+ }
26
+
27
+ // These ancestors are reversed: from bottom to top
28
+ const sourceAncestors = ancestorsOfNode(source, nodesMap)
29
+ const targetAncestors = ancestorsOfNode(target, nodesMap)
30
+
31
+ const hasAncestors = sourceAncestors.length > 0 && targetAncestors.length > 0
32
+
33
+ const edgeParent = hasAncestors
34
+ ? last(
35
+ commonHead(
36
+ reverse(sourceAncestors),
37
+ reverse(targetAncestors)
38
+ )
39
+ )
40
+ : null
41
+ edge.parent = edgeParent ? edgeParent.id : null
42
+
43
+ // Process edge source ancestors
44
+ for (const sourceAncestor of sourceAncestors) {
45
+ if (sourceAncestor === edgeParent) {
46
+ break
47
+ }
48
+ sourceAncestor.outEdges.push(edge.id)
49
+ }
50
+ // Process target hierarchy
51
+ for (const targetAncestor of targetAncestors) {
52
+ if (targetAncestor === edgeParent) {
53
+ break
54
+ }
55
+ targetAncestor.inEdges.push(edge.id)
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,55 @@
1
+ import { nonexhaustive } from '../../errors'
2
+ import type { Element } from '../../types/element'
3
+ import {
4
+ isIncoming,
5
+ isInOut,
6
+ isOutgoing,
7
+ isRelation,
8
+ isRelationWhere,
9
+ type RelationExpression,
10
+ type RelationWhereExpr
11
+ } from '../../types/expression'
12
+ import { whereOperatorAsPredicate } from '../../types/operators'
13
+ import type { Relation } from '../../types/relation'
14
+ import type { ComputedNode } from '../../types/view'
15
+ import { elementExprToPredicate } from './elementExpressionToPredicate'
16
+
17
+ type Predicate<T> = (x: T) => boolean
18
+ export type FilterableEdge = Pick<Relation, 'kind' | 'tags'> & {
19
+ source: ComputedNode
20
+ target: ComputedNode
21
+ }
22
+
23
+ export function relationExpressionToPredicates<T extends FilterableEdge>(
24
+ expr: RelationExpression | RelationWhereExpr
25
+ ): Predicate<T> {
26
+ switch (true) {
27
+ case isRelationWhere(expr): {
28
+ const predicate = relationExpressionToPredicates(expr.where.expr)
29
+ const where = whereOperatorAsPredicate(expr.where.condition)
30
+ return e => predicate(e) && where(e)
31
+ }
32
+ case isRelation(expr): {
33
+ const isSource = elementExprToPredicate(expr.source)
34
+ const isTarget = elementExprToPredicate(expr.target)
35
+ return edge => {
36
+ return (isSource(edge.source) && isTarget(edge.target))
37
+ || (!!expr.isBidirectional && isSource(edge.target) && isTarget(edge.source))
38
+ }
39
+ }
40
+ case isInOut(expr): {
41
+ const isInOut = elementExprToPredicate(expr.inout)
42
+ return edge => isInOut(edge.source) || isInOut(edge.target)
43
+ }
44
+ case isIncoming(expr): {
45
+ const isTarget = elementExprToPredicate(expr.incoming)
46
+ return edge => isTarget(edge.target)
47
+ }
48
+ case isOutgoing(expr): {
49
+ const isSource = elementExprToPredicate(expr.outgoing)
50
+ return edge => isSource(edge.source)
51
+ }
52
+ default:
53
+ nonexhaustive(expr)
54
+ }
55
+ }