@likec4/core 1.17.1 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/builder/index.d.mts +502 -0
- package/dist/builder/index.d.ts +502 -0
- package/dist/builder/index.js +871 -0
- package/dist/compute-view/index.d.mts +22 -0
- package/dist/compute-view/index.d.ts +22 -0
- package/dist/compute-view/index.js +78 -0
- package/dist/index.d.mts +10 -919
- package/dist/index.d.ts +10 -919
- package/dist/{index.mjs → index.js} +7 -1688
- package/dist/model/index.d.mts +254 -0
- package/dist/model/index.d.ts +254 -0
- package/dist/model/index.js +1 -0
- package/dist/shared/chunk-RAAYCPUM.M-JWF7SS.js +21 -0
- package/dist/shared/core.BIfgabEw.d.ts +719 -0
- package/dist/shared/core.BZ9Msq7w.d.mts +719 -0
- package/dist/shared/{core.CoLQkcYL.d.cts → core.C05RDLhi.d.ts} +460 -362
- package/dist/shared/{core.CoLQkcYL.d.mts → core.CmYMqgha.d.mts} +460 -362
- package/dist/shared/core.D6-fWbM3.d.ts +127 -0
- package/dist/shared/core.D74xkKkG.d.mts +148 -0
- package/dist/shared/core.D74xkKkG.d.ts +148 -0
- package/dist/shared/core.sLaWHBjR.d.mts +127 -0
- package/dist/shared/index.-BdzdI2C.js +7443 -0
- package/dist/shared/object_hash.CE_oF3I3.js +1222 -0
- package/dist/shared/{core.BXTs-8n7.mjs → view.CyZrG8BJ.js} +108 -47
- package/dist/types/index.d.mts +71 -1
- package/dist/types/index.d.ts +71 -1
- package/dist/types/index.js +20 -0
- package/dist/utils/index.d.mts +404 -0
- package/dist/utils/index.d.ts +404 -0
- package/dist/utils/index.js +330 -0
- package/package.json +59 -47
- package/src/builder/Builder-style1.test-d.ts +190 -0
- package/src/builder/Builder-style2.test-d.ts +175 -0
- package/src/builder/Builder.deployment.ts +224 -0
- package/src/builder/Builder.deploymentModel.ts +385 -0
- package/src/builder/Builder.element.ts +19 -19
- package/src/builder/Builder.model.ts +67 -42
- package/src/builder/Builder.ts +533 -367
- package/src/builder/Builder.view-common.ts +234 -0
- package/src/builder/Builder.view-deployment.ts +135 -0
- package/src/builder/Builder.view-element.ts +189 -0
- package/src/builder/Builder.views.ts +109 -33
- package/src/builder/Builder.with.ts +475 -0
- package/src/builder/__snapshots__/Builder-style1.spec.ts.snap +374 -0
- package/src/builder/__snapshots__/Builder-style2.spec.ts.snap +183 -0
- package/src/builder/__snapshots__/Builder.spec.ts.snap +374 -0
- package/src/builder/_types.ts +113 -18
- package/src/builder/index.ts +10 -4
- package/src/colors/index.ts +2 -2
- package/src/compute-view/compute-view.ts +83 -0
- package/src/compute-view/deployment-view/__test__/TestHelper.ts +236 -0
- package/src/compute-view/deployment-view/__test__/fixture.ts +198 -0
- package/src/compute-view/deployment-view/_types.ts +37 -0
- package/src/compute-view/deployment-view/clean-connections.ts +238 -0
- package/src/compute-view/deployment-view/compute.ts +125 -0
- package/src/compute-view/deployment-view/memory/index.ts +9 -0
- package/src/compute-view/deployment-view/memory/memory.ts +97 -0
- package/src/compute-view/deployment-view/predicates/elements.ts +144 -0
- package/src/compute-view/deployment-view/predicates/relation-direct.ts +237 -0
- package/src/compute-view/deployment-view/predicates/relation-in-out.ts +61 -0
- package/src/compute-view/deployment-view/predicates/relation-incoming.ts +95 -0
- package/src/compute-view/deployment-view/predicates/relation-outgoing.ts +88 -0
- package/src/compute-view/deployment-view/predicates/wildcard.ts +29 -0
- package/src/compute-view/deployment-view/stages/stage-exclude.ts +5 -0
- package/src/compute-view/deployment-view/stages/stage-final.ts +245 -0
- package/src/compute-view/deployment-view/stages/stage-include.ts +95 -0
- package/src/compute-view/deployment-view/utils.ts +276 -0
- package/src/compute-view/dynamic-view/__test__/fixture.ts +58 -0
- package/src/compute-view/dynamic-view/compute.ts +324 -0
- package/src/compute-view/element-view/__test__/TestHelper.ts +258 -0
- package/src/compute-view/element-view/__test__/__snapshots__/legacy.spec.ts.snap +458 -0
- package/src/compute-view/element-view/__test__/fixture.ts +675 -0
- package/src/compute-view/element-view/_types.ts +48 -0
- package/src/compute-view/element-view/clean-connections.ts +113 -0
- package/src/compute-view/element-view/compute.ts +315 -0
- package/src/compute-view/element-view/memory/NodeGroup.ts +31 -0
- package/src/compute-view/element-view/memory/index.ts +10 -0
- package/src/compute-view/element-view/memory/memory.ts +177 -0
- package/src/compute-view/element-view/memory/stage-exclude.ts +59 -0
- package/src/compute-view/element-view/memory/stage-final.ts +129 -0
- package/src/compute-view/element-view/memory/stage-include.ts +128 -0
- package/src/compute-view/element-view/predicates/_utils.ts +86 -0
- package/src/compute-view/element-view/predicates/element-expand.ts +33 -0
- package/src/compute-view/element-view/predicates/element-kind-tag.ts +26 -0
- package/src/compute-view/element-view/predicates/element-ref.ts +37 -0
- package/src/compute-view/element-view/predicates/relation-direct.ts +309 -0
- package/src/compute-view/element-view/predicates/relation-in-out.ts +66 -0
- package/src/compute-view/element-view/predicates/relation-in.ts +118 -0
- package/src/compute-view/element-view/predicates/relation-out.ts +116 -0
- package/src/compute-view/element-view/predicates/wildcard.ts +88 -0
- package/src/compute-view/element-view/utils.ts +59 -0
- package/src/compute-view/index.ts +4 -0
- package/src/compute-view/memory/AbstractMemory.ts +45 -0
- package/src/compute-view/memory/AbstractStageExclude.ts +216 -0
- package/src/compute-view/memory/AbstractStageInclude.ts +200 -0
- package/src/compute-view/memory/_types.ts +147 -0
- package/src/compute-view/memory/index.ts +5 -0
- package/src/compute-view/memory/ops.ts +43 -0
- package/src/compute-view/utils/__snapshots__/sortNodes.spec.ts.snap +113 -0
- package/src/compute-view/utils/ancestorsOfNode.ts +19 -0
- package/src/compute-view/utils/applyCustomElementProperties.ts +66 -0
- package/src/compute-view/utils/applyCustomRelationProperties.ts +41 -0
- package/src/compute-view/utils/applyViewRuleStyles.ts +53 -0
- package/src/compute-view/utils/buildComputedNodes.ts +148 -0
- package/src/compute-view/utils/buildElementNotations.ts +63 -0
- package/src/compute-view/utils/elementExpressionToPredicate.ts +50 -0
- package/src/compute-view/utils/link-nodes-with-edges.ts +54 -0
- package/src/compute-view/utils/merge-props-from-relationships.ts +128 -0
- package/src/compute-view/utils/relationExpressionToPredicates.ts +55 -0
- package/src/compute-view/utils/resolve-extended-views.ts +69 -0
- package/src/compute-view/utils/resolve-global-rules.ts +83 -0
- package/src/compute-view/utils/topological-sort.ts +256 -0
- package/src/compute-view/utils/uniqueTags.test.ts +42 -0
- package/src/compute-view/utils/uniqueTags.ts +21 -0
- package/src/compute-view/utils/view-hash.ts +27 -0
- package/src/compute-view/utils/with-readable-edges.ts +36 -0
- package/src/index.ts +22 -12
- package/src/model/DeploymentElementModel.ts +599 -0
- package/src/model/DeploymentModel.ts +370 -0
- package/src/model/ElementModel.ts +221 -0
- package/src/model/LikeC4Model.test-d.ts +152 -0
- package/src/model/LikeC4Model.ts +336 -457
- package/src/model/RelationModel.ts +85 -0
- package/src/model/__test__/fixture.ts +149 -338
- package/src/model/connection/Connection.ts +72 -0
- package/src/model/connection/deployment/DeploymentConnectionModel.ts +178 -0
- package/src/model/connection/deployment/find.ts +120 -0
- package/src/model/connection/deployment/index.ts +8 -0
- package/src/model/connection/index.ts +22 -0
- package/src/model/connection/model/ConnectionModel.ts +157 -0
- package/src/model/connection/model/find.ts +117 -0
- package/src/model/connection/model/index.ts +8 -0
- package/src/model/connection/ops.ts +274 -0
- package/src/model/guards.ts +9 -0
- package/src/model/index.ts +46 -2
- package/src/model/types.ts +112 -15
- package/src/model/view/EdgeModel.ts +103 -0
- package/src/model/view/LikeC4ViewModel.ts +226 -0
- package/src/model/view/NodeModel.ts +229 -0
- package/src/types/_common.ts +55 -1
- package/src/types/deployments.ts +169 -0
- package/src/types/element.ts +9 -5
- package/src/types/expression-v2.ts +172 -0
- package/src/types/expression.ts +17 -11
- package/src/types/global.ts +3 -3
- package/src/types/index.ts +220 -12
- package/src/types/model.ts +75 -12
- package/src/types/operators.ts +1 -1
- package/src/types/overview-graph.ts +2 -2
- package/src/types/relation.ts +18 -14
- package/src/types/theme.ts +0 -1
- package/src/types/view-notation.ts +2 -2
- package/src/types/view.ts +117 -41
- package/src/utils/{commonHead.ts → common-head.ts} +2 -0
- package/src/utils/const.ts +1 -0
- package/src/utils/fqn.ts +203 -56
- package/src/utils/getOrCreate.ts +42 -0
- package/src/utils/graphlib.ts +10 -0
- package/src/utils/index.ts +74 -6
- package/src/utils/iterable/_types.ts +5 -0
- package/src/utils/iterable/filter.ts +55 -0
- package/src/utils/iterable/flat.ts +17 -0
- package/src/utils/iterable/index.ts +12 -0
- package/src/utils/iterable/map.ts +40 -0
- package/src/utils/iterable/reduce.ts +24 -0
- package/src/utils/iterable/some.ts +36 -0
- package/src/utils/iterable/to.ts +19 -0
- package/src/utils/iterable/unique.ts +29 -0
- package/src/utils/object-hash.ts +3 -0
- package/src/utils/relations.ts +3 -64
- package/src/utils/set.ts +78 -0
- package/src/utils/string-hash.ts +16 -0
- package/dist/index.cjs +0 -5452
- package/dist/index.d.cts +0 -1053
- package/dist/shared/core.CoLQkcYL.d.ts +0 -734
- package/dist/shared/core.DIYZvpVg.cjs +0 -353
- package/dist/types/index.cjs +0 -70
- package/dist/types/index.d.cts +0 -2
- package/dist/types/index.mjs +0 -1
- package/src/builder/Builder.test-d.ts +0 -69
- package/src/builder/Builder.view.ts +0 -358
- package/src/model/LikeC4DiagramModel.ts +0 -338
- package/src/model/LikeC4ViewModel.ts +0 -320
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { isTruthy, only, pickBy, pipe, reduce, unique } from 'remeda'
|
|
2
|
+
import type { Color, DeploymentRelation, Link, Tag, ViewId } from '../../types'
|
|
3
|
+
import type { ModelRelation, RelationshipArrowType, RelationshipKind, RelationshipLineType } from '../../types/relation'
|
|
4
|
+
import { isNonEmptyArray } from '../../utils'
|
|
5
|
+
|
|
6
|
+
function pickRelationshipProps(relation: ModelRelation | DeploymentRelation) {
|
|
7
|
+
const {
|
|
8
|
+
title,
|
|
9
|
+
description = null,
|
|
10
|
+
technology = null,
|
|
11
|
+
kind = null,
|
|
12
|
+
color = null,
|
|
13
|
+
line = null,
|
|
14
|
+
head = null,
|
|
15
|
+
tail = null,
|
|
16
|
+
navigateTo = null
|
|
17
|
+
} = relation
|
|
18
|
+
return {
|
|
19
|
+
// Pick description only if title is present
|
|
20
|
+
...(title && {
|
|
21
|
+
title,
|
|
22
|
+
description
|
|
23
|
+
}),
|
|
24
|
+
technology,
|
|
25
|
+
kind,
|
|
26
|
+
color,
|
|
27
|
+
line,
|
|
28
|
+
head,
|
|
29
|
+
tail,
|
|
30
|
+
navigateTo
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Merges properties from multiple relationships into a single object.
|
|
36
|
+
* @param relations - The relationships to merge.
|
|
37
|
+
* @param prefer - The relationship to prefer when merging.
|
|
38
|
+
*/
|
|
39
|
+
export function mergePropsFromRelationships(
|
|
40
|
+
relations: Array<ModelRelation | DeploymentRelation>,
|
|
41
|
+
prefer?: ModelRelation | DeploymentRelation
|
|
42
|
+
) {
|
|
43
|
+
const allprops = pipe(
|
|
44
|
+
relations,
|
|
45
|
+
reduce(
|
|
46
|
+
(acc, r) => {
|
|
47
|
+
if (isTruthy(r.title) && !acc.title.includes(r.title)) {
|
|
48
|
+
acc.title.push(r.title)
|
|
49
|
+
}
|
|
50
|
+
if (isTruthy(r.description) && !acc.description.includes(r.description)) {
|
|
51
|
+
acc.description.push(r.description)
|
|
52
|
+
}
|
|
53
|
+
if (isTruthy(r.technology) && !acc.technology.includes(r.technology)) {
|
|
54
|
+
acc.technology.push(r.technology)
|
|
55
|
+
}
|
|
56
|
+
if (isTruthy(r.kind) && !acc.kind.includes(r.kind)) {
|
|
57
|
+
acc.kind.push(r.kind)
|
|
58
|
+
}
|
|
59
|
+
if (isTruthy(r.color) && !acc.color.includes(r.color)) {
|
|
60
|
+
acc.color.push(r.color)
|
|
61
|
+
}
|
|
62
|
+
if (isTruthy(r.line) && !acc.line.includes(r.line)) {
|
|
63
|
+
acc.line.push(r.line)
|
|
64
|
+
}
|
|
65
|
+
if (isTruthy(r.head) && !acc.head.includes(r.head)) {
|
|
66
|
+
acc.head.push(r.head)
|
|
67
|
+
}
|
|
68
|
+
if (isTruthy(r.tail) && !acc.tail.includes(r.tail)) {
|
|
69
|
+
acc.tail.push(r.tail)
|
|
70
|
+
}
|
|
71
|
+
if (isTruthy(r.navigateTo) && !acc.navigateTo.includes(r.navigateTo)) {
|
|
72
|
+
acc.navigateTo.push(r.navigateTo)
|
|
73
|
+
}
|
|
74
|
+
if (r.tags) {
|
|
75
|
+
acc.tags.push(...r.tags)
|
|
76
|
+
}
|
|
77
|
+
if (r.links) {
|
|
78
|
+
acc.links.push(...r.links)
|
|
79
|
+
}
|
|
80
|
+
return acc
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
title: [] as string[],
|
|
84
|
+
description: [] as string[],
|
|
85
|
+
technology: [] as string[],
|
|
86
|
+
kind: [] as RelationshipKind[],
|
|
87
|
+
head: [] as RelationshipArrowType[],
|
|
88
|
+
tail: [] as RelationshipArrowType[],
|
|
89
|
+
color: [] as Color[],
|
|
90
|
+
tags: [] as Tag[],
|
|
91
|
+
links: [] as Link[],
|
|
92
|
+
line: [] as RelationshipLineType[],
|
|
93
|
+
navigateTo: [] as ViewId[]
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
let technology = only(allprops.technology)
|
|
99
|
+
let title = only(allprops.title) ?? (allprops.title.length > 1 ? '[...]' : null)
|
|
100
|
+
|
|
101
|
+
const tags = unique(allprops.tags)
|
|
102
|
+
let merged = pickBy(
|
|
103
|
+
{
|
|
104
|
+
// If there is no title, but there is technology, use technology as title
|
|
105
|
+
title: title ?? (technology ? `[${technology}]` : null),
|
|
106
|
+
description: only(allprops.description),
|
|
107
|
+
technology,
|
|
108
|
+
kind: only(allprops.kind),
|
|
109
|
+
head: only(allprops.head),
|
|
110
|
+
tail: only(allprops.tail),
|
|
111
|
+
color: only(allprops.color),
|
|
112
|
+
line: only(allprops.line),
|
|
113
|
+
navigateTo: only(allprops.navigateTo),
|
|
114
|
+
...isNonEmptyArray(allprops.links) && { links: allprops.links },
|
|
115
|
+
...isNonEmptyArray(tags) && { tags }
|
|
116
|
+
},
|
|
117
|
+
isTruthy
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (prefer) {
|
|
121
|
+
return {
|
|
122
|
+
...merged,
|
|
123
|
+
...pickBy(pickRelationshipProps(prefer), isTruthy)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return merged
|
|
127
|
+
}
|
|
128
|
+
export type MergedRelationshipProps = ReturnType<typeof mergePropsFromRelationships>
|
|
@@ -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 { ModelRelation } 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<ModelRelation, '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
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { values } from 'remeda'
|
|
2
|
+
import { type ExtendsElementView, isElementView, isExtendsElementView, type LikeC4View } from '../../types/view'
|
|
3
|
+
|
|
4
|
+
import Graph from 'graphology'
|
|
5
|
+
import { topologicalSort } from 'graphology-dag/topological-sort'
|
|
6
|
+
import willCreateCycle from 'graphology-dag/will-create-cycle'
|
|
7
|
+
/**
|
|
8
|
+
* Resolve rules of extended views
|
|
9
|
+
* (Removes invalid views)
|
|
10
|
+
*/
|
|
11
|
+
export function resolveRulesExtendedViews<V extends Record<any, LikeC4View>>(
|
|
12
|
+
unresolvedViews: V
|
|
13
|
+
): V {
|
|
14
|
+
const g = new Graph<{ view: LikeC4View }>({
|
|
15
|
+
type: 'directed'
|
|
16
|
+
})
|
|
17
|
+
const extendedViews = [] as ExtendsElementView<any, any>[]
|
|
18
|
+
for (const view of values(unresolvedViews)) {
|
|
19
|
+
g.addNode(view.id, { view })
|
|
20
|
+
if (isExtendsElementView(view)) {
|
|
21
|
+
extendedViews.push(view)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (extendedViews.length === 0) {
|
|
26
|
+
return unresolvedViews
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const view of extendedViews) {
|
|
30
|
+
if (!g.hasNode(view.extends)) {
|
|
31
|
+
console.warn(`View "${view.id}" extends from "${view.extends}" which does not exist`)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
if (willCreateCycle(g, view.id, view.extends)) {
|
|
35
|
+
console.warn(`View "${view.id}" extends from "${view.extends}" which creates a cycle`)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// view -> parent
|
|
40
|
+
g.addDirectedEdge(view.id, view.extends)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sorted = topologicalSort(g).reverse()
|
|
44
|
+
return sorted.reduce((acc, id) => {
|
|
45
|
+
const view = g.getNodeAttribute(id, 'view')
|
|
46
|
+
if (!isExtendsElementView(view)) {
|
|
47
|
+
acc[view.id] = view
|
|
48
|
+
return acc
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const extendsFrom = acc[view.extends]
|
|
52
|
+
if (!extendsFrom || !isElementView(extendsFrom)) {
|
|
53
|
+
return acc
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
acc[view.id] = {
|
|
57
|
+
...extendsFrom,
|
|
58
|
+
...view,
|
|
59
|
+
rules: [...extendsFrom.rules, ...view.rules]
|
|
60
|
+
}
|
|
61
|
+
return acc
|
|
62
|
+
}, {} as Record<string, LikeC4View>) as V
|
|
63
|
+
|
|
64
|
+
// forEachNodeInTopologicalOrder(g, (_id, { view }) => {
|
|
65
|
+
|
|
66
|
+
// })
|
|
67
|
+
|
|
68
|
+
// return ordered as V
|
|
69
|
+
}
|
|
@@ -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.rules, globals)
|
|
25
|
+
}
|
|
26
|
+
} else if (isDynamicView(view)) {
|
|
27
|
+
return {
|
|
28
|
+
...view,
|
|
29
|
+
rules: resolveGlobalRulesInDynamicView(view.rules, globals)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
nonexhaustive(view)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ViewRuleGlobal = ViewRuleGlobalPredicateRef | ViewRuleGlobalStyle
|
|
36
|
+
|
|
37
|
+
export function resolveGlobalRulesInElementView(
|
|
38
|
+
rules: ViewRule[],
|
|
39
|
+
globals: ModelGlobals
|
|
40
|
+
): Array<Exclude<ViewRule, ViewRuleGlobal>> {
|
|
41
|
+
return 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
|
+
rules: DynamicViewRule[],
|
|
63
|
+
globals: ModelGlobals
|
|
64
|
+
): Array<Exclude<DynamicViewRule, ViewRuleGlobal>> {
|
|
65
|
+
return 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,256 @@
|
|
|
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, partition, pipe, takeWhile } from 'remeda'
|
|
5
|
+
import { invariant, nonNullable } from '../../errors'
|
|
6
|
+
import type { ComputedEdge, ComputedNode, Fqn, NodeId } from '../../types'
|
|
7
|
+
import { ancestorsOfNode } from './ancestorsOfNode'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Keeps initial order of the elements, but ensures parents are placed before children
|
|
11
|
+
*/
|
|
12
|
+
function ensureParentsFirst<T extends { id: string; parent: string | null }>(
|
|
13
|
+
array: ReadonlyArray<T>,
|
|
14
|
+
): Array<T> {
|
|
15
|
+
const result = [] as T[]
|
|
16
|
+
const items = [...array]
|
|
17
|
+
let item
|
|
18
|
+
while ((item = items.shift())) {
|
|
19
|
+
let parent = item.parent
|
|
20
|
+
if (parent) {
|
|
21
|
+
const ancestors = [] as T[]
|
|
22
|
+
while (parent) {
|
|
23
|
+
const parentIndx = items.findIndex(i => i.id === parent)
|
|
24
|
+
if (parentIndx < 0) {
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
const [parentItem] = items.splice(parentIndx, 1)
|
|
28
|
+
if (!parentItem) {
|
|
29
|
+
throw new Error('Invalid state, should not happen')
|
|
30
|
+
}
|
|
31
|
+
ancestors.unshift(parentItem)
|
|
32
|
+
parent = parentItem.parent
|
|
33
|
+
}
|
|
34
|
+
result.push(...ancestors)
|
|
35
|
+
}
|
|
36
|
+
result.push(item)
|
|
37
|
+
}
|
|
38
|
+
return result as Array<T>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Side effect, mutates node.children field to preserve same order as in the input
|
|
43
|
+
*/
|
|
44
|
+
function updateChildren(nodes: ComputedNode[]) {
|
|
45
|
+
nodes.forEach(parent => {
|
|
46
|
+
if (parent.children.length > 0) {
|
|
47
|
+
parent.children = nodes.reduce((acc, n) => {
|
|
48
|
+
if (n.parent === parent.id) {
|
|
49
|
+
acc.push(n.id)
|
|
50
|
+
}
|
|
51
|
+
return acc
|
|
52
|
+
}, [] as NodeId[])
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return nodes
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type TopologicalSortParam = {
|
|
59
|
+
nodes: ReadonlyMap<Fqn, ComputedNode>
|
|
60
|
+
edges: Iterable<ComputedEdge>
|
|
61
|
+
}
|
|
62
|
+
export function topologicalSort(
|
|
63
|
+
param: TopologicalSortParam,
|
|
64
|
+
): {
|
|
65
|
+
nodes: ComputedNode[]
|
|
66
|
+
edges: ComputedEdge[]
|
|
67
|
+
} {
|
|
68
|
+
let nodes = ensureParentsFirst([...param.nodes.values()])
|
|
69
|
+
let edges = [...param.edges]
|
|
70
|
+
if (nodes.length < 2 || edges.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
nodes,
|
|
73
|
+
edges,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getNode = (id: Fqn) => nonNullable(param.nodes.get(id))
|
|
78
|
+
// const nodeLevel = (id: string) => getNode(id).level + 1
|
|
79
|
+
//
|
|
80
|
+
|
|
81
|
+
const g = new Graph({
|
|
82
|
+
multi: true,
|
|
83
|
+
allowSelfLoops: true,
|
|
84
|
+
type: 'directed',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const enrichedEdges = pipe(
|
|
88
|
+
edges,
|
|
89
|
+
map((edge, __dirname) => {
|
|
90
|
+
const source = getNode(edge.source),
|
|
91
|
+
target = getNode(edge.target),
|
|
92
|
+
parent = edge.parent ? getNode(edge.parent) : null
|
|
93
|
+
return ({
|
|
94
|
+
id: edge.id,
|
|
95
|
+
edge,
|
|
96
|
+
parent,
|
|
97
|
+
source,
|
|
98
|
+
target,
|
|
99
|
+
})
|
|
100
|
+
}),
|
|
101
|
+
// sortBy(
|
|
102
|
+
// [prop('sourceIndex'), 'asc'],
|
|
103
|
+
// // [prop('sourceInCount'), 'asc'],
|
|
104
|
+
// // [prop('sourceLevel'), 'asc'],
|
|
105
|
+
// ),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const [edgesBetweenLeafs, edgesWithCompounds] = partition(
|
|
109
|
+
enrichedEdges,
|
|
110
|
+
({ source, target }) => source.children.length === 0 && target.children.length === 0,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// edgesBetweenLeafs.sort((a, b) => {
|
|
114
|
+
// if (a.source === b.source) {
|
|
115
|
+
// return 0
|
|
116
|
+
// }
|
|
117
|
+
// if (a.source.level === 0 && b.source.level === 0) {
|
|
118
|
+
// return 0
|
|
119
|
+
// }
|
|
120
|
+
// if (isSameHierarchy(a.source, b.target) || isSameHierarchy(a.target, b.source)) {
|
|
121
|
+
// return edges.indexOf(a.edge) - edges.indexOf(b.edge)
|
|
122
|
+
// }
|
|
123
|
+
// const aLevel = a.parent ? a.parent.level + 1 - a.source.level : a.source.level
|
|
124
|
+
// const bLevel = b.parent ? b.parent.level + 1 - b.source.level : b.source.level
|
|
125
|
+
// return aLevel - bLevel
|
|
126
|
+
// // if (a.source.level === 0 && b.source.level === 0) {
|
|
127
|
+
|
|
128
|
+
// // // if (a.source.depth)
|
|
129
|
+
// // return nodes.indexOf(a.target) - nodes.indexOf(b.target)
|
|
130
|
+
// // }
|
|
131
|
+
// // if (a.source.level === 0 && b.source.level > 0) {
|
|
132
|
+
// // return -1
|
|
133
|
+
// // }
|
|
134
|
+
// // if (a.parent && b.parent && a.parent !== b.parent) {
|
|
135
|
+
// // if (a.parent.level !== b.parent.level) {
|
|
136
|
+
// // return b.parent.level - a.parent.level
|
|
137
|
+
// // } else {
|
|
138
|
+
// // return nodes.indexOf(a.parent) - nodes.indexOf(b.parent)
|
|
139
|
+
// // }
|
|
140
|
+
// // }
|
|
141
|
+
// // return Math.min(nodes.indexOf(a.source), nodes.indexOf(a.target)) - Math.min(nodes.indexOf(b.source), nodes.indexOf(b.target))
|
|
142
|
+
// })
|
|
143
|
+
const sortedEdges = [] as ComputedEdge[]
|
|
144
|
+
|
|
145
|
+
const addEdgeToGraph = (edge: ComputedEdge) => {
|
|
146
|
+
g.mergeNode(edge.source)
|
|
147
|
+
g.mergeNode(edge.target)
|
|
148
|
+
sortedEdges.push(edge)
|
|
149
|
+
if (!willCreateCycle(g, edge.source, edge.target)) {
|
|
150
|
+
g.mergeDirectedEdge(edge.source, edge.target)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const { edge, source, target } of edgesBetweenLeafs) {
|
|
155
|
+
addEdgeToGraph(edge)
|
|
156
|
+
// Strengthen the graph by adding edges to parents
|
|
157
|
+
if (target.parent && target.parent !== edge.parent) {
|
|
158
|
+
pipe(
|
|
159
|
+
ancestorsOfNode(target, param.nodes),
|
|
160
|
+
takeWhile(ancestor => ancestor.inEdges.includes(edge.id)),
|
|
161
|
+
forEach(ancestor => {
|
|
162
|
+
g.mergeNode(ancestor.id)
|
|
163
|
+
if (!willCreateCycle(g, edge.source, ancestor.id)) {
|
|
164
|
+
g.mergeDirectedEdge(edge.source, ancestor.id)
|
|
165
|
+
}
|
|
166
|
+
if (!willCreateCycle(g, ancestor.id, edge.target)) {
|
|
167
|
+
g.mergeDirectedEdge(ancestor.id, edge.target)
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
if (source.parent) {
|
|
173
|
+
const sourceParent = getNode(source.parent)
|
|
174
|
+
g.mergeNode(sourceParent.id)
|
|
175
|
+
if (!willCreateCycle(g, sourceParent.id, source.id)) {
|
|
176
|
+
g.mergeDirectedEdge(sourceParent.id, source.id)
|
|
177
|
+
}
|
|
178
|
+
if (target.parent && target.parent !== source.parent) {
|
|
179
|
+
if (!willCreateCycle(g, sourceParent.id, target.parent)) {
|
|
180
|
+
g.mergeDirectedEdge(sourceParent.id, target.parent)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// addEdgeToGraph(edge)
|
|
186
|
+
}
|
|
187
|
+
for (const { edge } of edgesWithCompounds) {
|
|
188
|
+
addEdgeToGraph(edge)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
invariant(sortedEdges.length === edges.length, 'Not all edges were added to the graph')
|
|
192
|
+
|
|
193
|
+
// for (const compound of nodes) {
|
|
194
|
+
// if (compound.children.length === 0 || compound.inEdges.length === 0) {
|
|
195
|
+
// continue
|
|
196
|
+
// }
|
|
197
|
+
// // g.mergeNode(compound.id)
|
|
198
|
+
// for (const inEdge of compound.inEdges) {
|
|
199
|
+
// const { edge, source } = getEdge(inEdge)
|
|
200
|
+
// // ignore if this edge is coming from compound node
|
|
201
|
+
// if (source.children.length > 0) {
|
|
202
|
+
// continue
|
|
203
|
+
// }
|
|
204
|
+
// // if this edge is coming to the compound node directly
|
|
205
|
+
// if (edge.target === compound.id) {
|
|
206
|
+
// // for (const child of compound.children) {
|
|
207
|
+
// // // g.mergeNode(child)
|
|
208
|
+
// // if (!willCreateCycle(g, edge.source, child)) {
|
|
209
|
+
// // g.mergeDirectedEdge(edge.source, child)
|
|
210
|
+
// // }
|
|
211
|
+
// // }
|
|
212
|
+
// } else {
|
|
213
|
+
// if (!willCreateCycle(g, edge.source, compound.id)) {
|
|
214
|
+
// g.mergeDirectedEdge(edge.source, compound.id)
|
|
215
|
+
// }
|
|
216
|
+
// if (!willCreateCycle(g, compound.id, edge.target)) {
|
|
217
|
+
// g.mergeDirectedEdge(compound.id, edge.target)
|
|
218
|
+
// }
|
|
219
|
+
// }
|
|
220
|
+
// }
|
|
221
|
+
// }
|
|
222
|
+
|
|
223
|
+
const sortedIds = topsort(g)
|
|
224
|
+
let sorted = [] as ComputedNode[]
|
|
225
|
+
let unsorted = nodes.slice()
|
|
226
|
+
for (const sortedId of sortedIds) {
|
|
227
|
+
const indx = unsorted.findIndex(n => n.id === sortedId)
|
|
228
|
+
invariant(indx >= 0, `Node "${sortedId}" not found`)
|
|
229
|
+
sorted.push(...unsorted.splice(indx, 1))
|
|
230
|
+
}
|
|
231
|
+
// Merge unsorted nodes, keeping their initial order
|
|
232
|
+
if (unsorted.length > 0 && sorted.length > 0) {
|
|
233
|
+
sorted = sorted.flatMap(node => {
|
|
234
|
+
if (unsorted.length === 0) {
|
|
235
|
+
return node
|
|
236
|
+
}
|
|
237
|
+
const wereBefore = nodes
|
|
238
|
+
.slice(0, nodes.indexOf(node))
|
|
239
|
+
.filter(n => unsorted.includes(n))
|
|
240
|
+
if (wereBefore.length > 0) {
|
|
241
|
+
unsorted = unsorted.filter(n => !wereBefore.includes(n))
|
|
242
|
+
return [...wereBefore, node]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return node
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
// Add remaining unsorted nodes
|
|
249
|
+
sorted.push(...unsorted)
|
|
250
|
+
return {
|
|
251
|
+
nodes: updateChildren(
|
|
252
|
+
ensureParentsFirst(sorted),
|
|
253
|
+
),
|
|
254
|
+
edges: sortedEdges,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -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
|
+
}
|