@likec4/core 0.6.2 → 0.6.3
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/dist/compute-view/EdgeBuilder.d.ts +7 -0
- package/dist/compute-view/EdgeBuilder.js +34 -0
- package/dist/compute-view/compute-view.d.ts +11 -0
- package/dist/compute-view/compute-view.js +14 -0
- package/dist/compute-view/compute.element-view.d.ts +4 -0
- package/dist/compute-view/compute.element-view.js +168 -0
- package/dist/compute-view/index.d.ts +2 -0
- package/dist/compute-view/index.js +1 -0
- package/dist/compute-view/utils/anyPossibleRelations.d.ts +2 -0
- package/dist/compute-view/utils/anyPossibleRelations.js +12 -0
- package/dist/compute-view/utils/evaluate-expression.d.ts +10 -0
- package/dist/compute-view/utils/evaluate-expression.js +186 -0
- package/dist/compute-view/utils/sortNodes.d.ts +2 -0
- package/dist/compute-view/utils/sortNodes.js +42 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/model-index/index.d.ts +1 -0
- package/dist/model-index/index.js +1 -0
- package/dist/model-index/model-index.d.ts +30 -0
- package/dist/model-index/model-index.js +143 -0
- package/dist/types/computed-view.d.ts +30 -0
- package/dist/types/computed-view.js +1 -0
- package/dist/types/diagram.d.ts +27 -0
- package/dist/types/diagram.js +1 -0
- package/dist/types/element.d.ts +26 -0
- package/dist/types/element.js +5 -0
- package/dist/types/expression.d.ts +43 -0
- package/dist/types/expression.js +24 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +2 -0
- package/dist/types/model.d.ts +9 -0
- package/dist/types/model.js +1 -0
- package/dist/types/opaque.d.ts +102 -0
- package/dist/types/opaque.js +1 -0
- package/dist/types/relation.d.ts +10 -0
- package/dist/types/relation.js +1 -0
- package/dist/types/view.d.ts +29 -0
- package/dist/types/view.js +9 -0
- package/dist/utils/fqn.d.ts +10 -0
- package/dist/utils/fqn.js +51 -0
- package/dist/utils/guards.d.ts +3 -0
- package/dist/utils/guards.js +10 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/relations.d.ts +16 -0
- package/dist/utils/relations.js +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { equals, head, keys, pluck, reject } from 'rambdax';
|
|
2
|
+
import { commonAncestor, compareFqnHierarchically } from '../utils/fqn';
|
|
3
|
+
import { compareRelations } from '../utils/relations';
|
|
4
|
+
export class EdgeBuilder {
|
|
5
|
+
_relationsObj = {};
|
|
6
|
+
add(source, target, relation) {
|
|
7
|
+
const bySource = this._relationsObj[source] ?? {};
|
|
8
|
+
const relations = bySource[target] ?? [];
|
|
9
|
+
relations.push(relation);
|
|
10
|
+
bySource[target] = relations;
|
|
11
|
+
this._relationsObj[source] = bySource;
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
build() {
|
|
15
|
+
return keys(this._relationsObj)
|
|
16
|
+
.flatMap(source => {
|
|
17
|
+
const targets = this._relationsObj[source] ?? {};
|
|
18
|
+
return keys(targets).map(target => {
|
|
19
|
+
const relations = (targets[target] ?? []).sort(compareRelations);
|
|
20
|
+
const label = head(reject(equals(''), pluck('title', relations))) ?? null;
|
|
21
|
+
return {
|
|
22
|
+
id: `${source}:${target}`,
|
|
23
|
+
parent: commonAncestor(source, target),
|
|
24
|
+
source,
|
|
25
|
+
target,
|
|
26
|
+
label,
|
|
27
|
+
relations: pluck('id', relations)
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
})
|
|
31
|
+
.sort(compareRelations)
|
|
32
|
+
.reverse();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ModelIndex } from '../model-index';
|
|
2
|
+
import type { Element, ElementView, Fqn, LikeC4Model, Relation, RelationID, ViewID } from '../types';
|
|
3
|
+
import type { ComputedView } from '../types/computed-view';
|
|
4
|
+
export declare function computeView(view: ElementView, index: ModelIndex): ComputedView;
|
|
5
|
+
type InputModel = {
|
|
6
|
+
elements: Record<Fqn, Element>;
|
|
7
|
+
relations: Record<RelationID, Relation>;
|
|
8
|
+
views: Record<ViewID, ElementView>;
|
|
9
|
+
};
|
|
10
|
+
export declare function computeViews(model: InputModel): LikeC4Model;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { map } from 'rambdax';
|
|
2
|
+
import { ModelIndex } from '../model-index';
|
|
3
|
+
import { computeElementView } from './compute.element-view';
|
|
4
|
+
export function computeView(view, index) {
|
|
5
|
+
return computeElementView(view, index);
|
|
6
|
+
}
|
|
7
|
+
export function computeViews(model) {
|
|
8
|
+
const index = ModelIndex.from(model);
|
|
9
|
+
return {
|
|
10
|
+
elements: model.elements,
|
|
11
|
+
relations: model.relations,
|
|
12
|
+
views: map(v => computeElementView(v, index), model.views)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { anyPass, filter, head } from 'rambdax';
|
|
2
|
+
import { DefaultElementShape, DefaultThemeColor } from '../types';
|
|
3
|
+
import * as Expression from '../types/expression';
|
|
4
|
+
import { isViewRuleAutoLayout, isViewRuleExpression, isViewRuleStyle } from '../types/view';
|
|
5
|
+
import { Relations, compareByFqnHierarchically, failExpectedNever, isSameHierarchy, parentFqn } from '../utils';
|
|
6
|
+
import { EdgeBuilder } from './EdgeBuilder';
|
|
7
|
+
import { anyPossibleRelations } from './utils/anyPossibleRelations';
|
|
8
|
+
import { evaluateExpression } from './utils/evaluate-expression';
|
|
9
|
+
import { sortNodes } from './utils/sortNodes';
|
|
10
|
+
function updateSetWith(set, elements, addToSet = true) {
|
|
11
|
+
for (const e of elements) {
|
|
12
|
+
addToSet ? set.add(e) : set.delete(e);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function transformToNodes(elementsIterator, index, currentViewid) {
|
|
16
|
+
return [...elementsIterator]
|
|
17
|
+
.sort(compareByFqnHierarchically)
|
|
18
|
+
.reduce((map, { id, title, color, shape, description }) => {
|
|
19
|
+
let parent = parentFqn(id);
|
|
20
|
+
while (parent) {
|
|
21
|
+
if (map.has(parent)) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
parent = parentFqn(parent);
|
|
25
|
+
}
|
|
26
|
+
const navigateTo = head(index.defaultViewOf(id).filter(v => v !== currentViewid));
|
|
27
|
+
map.set(id, Object.assign({
|
|
28
|
+
id,
|
|
29
|
+
parent,
|
|
30
|
+
title,
|
|
31
|
+
color: color ?? DefaultThemeColor,
|
|
32
|
+
shape: shape ?? DefaultElementShape,
|
|
33
|
+
children: []
|
|
34
|
+
}, description ? { description } : {}, navigateTo ? { navigateTo } : {}));
|
|
35
|
+
if (parent) {
|
|
36
|
+
map.get(parent)?.children.push(id);
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}, new Map());
|
|
40
|
+
}
|
|
41
|
+
function applyViewRuleStyles(rules, nodes) {
|
|
42
|
+
for (const rule of rules) {
|
|
43
|
+
const predicates = [];
|
|
44
|
+
if (!rule.style.color && !rule.style.shape) {
|
|
45
|
+
// skip empty
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const target of rule.targets) {
|
|
49
|
+
if (Expression.isWildcard(target)) {
|
|
50
|
+
predicates.push(() => true);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
if (Expression.isElementRef(target)) {
|
|
54
|
+
const { element, isDescedants } = target;
|
|
55
|
+
predicates.push(isDescedants ? n => n.id.startsWith(element + '.') : n => n.id === element);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
failExpectedNever(target);
|
|
59
|
+
}
|
|
60
|
+
filter(anyPass(predicates), nodes).forEach(n => {
|
|
61
|
+
n.shape = rule.style.shape ?? n.shape;
|
|
62
|
+
n.color = rule.style.color ?? n.color;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return nodes;
|
|
66
|
+
}
|
|
67
|
+
export function computeElementView(view, index) {
|
|
68
|
+
const relations = new Set();
|
|
69
|
+
const elements = new Set();
|
|
70
|
+
const neighbours = new Set();
|
|
71
|
+
const rootElement = view.viewOf ?? null;
|
|
72
|
+
const rulesInclude = view.rules.filter(isViewRuleExpression);
|
|
73
|
+
if (rootElement && rulesInclude.length == 0) {
|
|
74
|
+
elements.add(index.find(rootElement));
|
|
75
|
+
}
|
|
76
|
+
for (const { isInclude, exprs } of rulesInclude) {
|
|
77
|
+
for (const expr of exprs) {
|
|
78
|
+
let { elements: newElements, neighbours: newNeighbours, relations: newRelations } = evaluateExpression(index, expr, rootElement);
|
|
79
|
+
if (isInclude) {
|
|
80
|
+
const filters = [];
|
|
81
|
+
// When include element(s) without newNeighbours, include in-out relations
|
|
82
|
+
if (elements.size > 0 &&
|
|
83
|
+
newElements.length > 0 &&
|
|
84
|
+
newNeighbours.length === 0 &&
|
|
85
|
+
Expression.isElement(expr)) {
|
|
86
|
+
for (const e of elements) {
|
|
87
|
+
for (const newE of newElements) {
|
|
88
|
+
if (!isSameHierarchy(e, newE)) {
|
|
89
|
+
filters.push(Relations.isAnyBetween(e.id, newE.id));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// // When include elements by tag or kind, include in-out relations
|
|
95
|
+
// if (elements.size > 0 && newElements.length > 0 && (Expression.isElementKind(expr) || Expression.isElementTag(expr))) {
|
|
96
|
+
// for (const e of elements) {
|
|
97
|
+
// for (const newE of newElements) {
|
|
98
|
+
// if (e.id !== newE.id && !isAncestor(e, newE) && !isAncestor(newE, e)) {
|
|
99
|
+
// filters.push(isAnyRelationBetween(e.id, newE.id))
|
|
100
|
+
// }
|
|
101
|
+
// }
|
|
102
|
+
// }
|
|
103
|
+
// }
|
|
104
|
+
if (filters.length > 0) {
|
|
105
|
+
newRelations = newRelations.concat(index.filterRelations(anyPass(filters)));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (Expression.isAnyRelation(expr)) {
|
|
110
|
+
// Exclude only relations, but not elements and neighbours
|
|
111
|
+
newNeighbours = [];
|
|
112
|
+
newElements = [];
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
newNeighbours = newNeighbours.concat(newElements);
|
|
116
|
+
// Also exclude nested elements from current neighbours
|
|
117
|
+
// for (const n of neighbours) {
|
|
118
|
+
// if (newNeighbours.some(e => n.id.startsWith(e.id + '.'))) {
|
|
119
|
+
// newNeighbours.push(n)
|
|
120
|
+
// }
|
|
121
|
+
// }
|
|
122
|
+
// Do not exclude relations
|
|
123
|
+
newRelations = [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
updateSetWith(elements, newElements, isInclude);
|
|
127
|
+
updateSetWith(relations, newRelations, isInclude);
|
|
128
|
+
updateSetWith(neighbours, newNeighbours, isInclude);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const elementsIds = new Set([...elements].map(e => e.id));
|
|
132
|
+
const edgeBuilder = new EdgeBuilder();
|
|
133
|
+
const resolvedRelations = [...relations];
|
|
134
|
+
for (const [source, target] of anyPossibleRelations([...elements, ...neighbours])) {
|
|
135
|
+
if (!elementsIds.has(source.id) && !elementsIds.has(target.id)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const findPredicate = Relations.isBetween(source.id, target.id);
|
|
139
|
+
let idx = resolvedRelations.findIndex(findPredicate);
|
|
140
|
+
while (idx >= 0) {
|
|
141
|
+
const [rel] = resolvedRelations.splice(idx, 1);
|
|
142
|
+
if (rel) {
|
|
143
|
+
elements.add(source);
|
|
144
|
+
elements.add(target);
|
|
145
|
+
edgeBuilder.add(source.id, target.id, rel);
|
|
146
|
+
}
|
|
147
|
+
idx = resolvedRelations.findIndex(findPredicate);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const nodesreg = transformToNodes(elements, index, view.id);
|
|
151
|
+
const edges = edgeBuilder.build().map(edge => {
|
|
152
|
+
while (edge.parent) {
|
|
153
|
+
if (nodesreg.has(edge.parent)) {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
edge.parent = parentFqn(edge.parent);
|
|
157
|
+
}
|
|
158
|
+
return edge;
|
|
159
|
+
});
|
|
160
|
+
const nodes = applyViewRuleStyles(view.rules.filter(isViewRuleStyle), sortNodes(nodesreg, edges));
|
|
161
|
+
const autoLayoutRule = view.rules.find(isViewRuleAutoLayout);
|
|
162
|
+
return {
|
|
163
|
+
...view,
|
|
164
|
+
autoLayout: autoLayoutRule?.autoLayout ?? 'TB',
|
|
165
|
+
nodes,
|
|
166
|
+
edges
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { computeView, computeViews } from './compute-view';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { prop, uniqBy } from 'rambdax';
|
|
2
|
+
import { compareByFqnHierarchically, isAncestor } from '../../utils/fqn';
|
|
3
|
+
export function* anyPossibleRelations(elements) {
|
|
4
|
+
const uniqElements = uniqBy(prop('id'), elements).sort(compareByFqnHierarchically).reverse();
|
|
5
|
+
for (const source of uniqElements) {
|
|
6
|
+
for (const target of uniqElements) {
|
|
7
|
+
if (source !== target && !isAncestor(source, target) && !isAncestor(target, source)) {
|
|
8
|
+
yield [source, target];
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ModelIndex } from '../../model-index';
|
|
2
|
+
import type { Element, Fqn, Relation, Expression } from '../../types';
|
|
3
|
+
export declare const keepLeafs: (elements: Element[]) => Element[];
|
|
4
|
+
interface EvaluateViewExpressionResult {
|
|
5
|
+
elements: Element[];
|
|
6
|
+
neighbours: Element[];
|
|
7
|
+
relations: Relation[];
|
|
8
|
+
}
|
|
9
|
+
export declare function evaluateExpression(index: ModelIndex, expr: Expression, rootElement: Fqn | null): EvaluateViewExpressionResult;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { anyPass, map, pluck, uniq } from 'rambdax';
|
|
2
|
+
import * as Expr from '../../types/expression';
|
|
3
|
+
import { failExpectedNever, isAncestor } from '../../utils';
|
|
4
|
+
import { isBetween, isIncoming, isInside, isOutgoing } from '../../utils/relations';
|
|
5
|
+
import { anyPossibleRelations } from './anyPossibleRelations';
|
|
6
|
+
const dropNested = (elements) => {
|
|
7
|
+
return elements.reduce((acc, current) => {
|
|
8
|
+
if (acc.length === 0)
|
|
9
|
+
return [current];
|
|
10
|
+
// current is a child of some in acc
|
|
11
|
+
if (acc.some(p => p.id === current.id || isAncestor(p, current))) {
|
|
12
|
+
return acc;
|
|
13
|
+
}
|
|
14
|
+
return [
|
|
15
|
+
// drop children of current
|
|
16
|
+
...acc.filter(e => !isAncestor(current, e)),
|
|
17
|
+
current
|
|
18
|
+
];
|
|
19
|
+
}, []);
|
|
20
|
+
};
|
|
21
|
+
export const keepLeafs = (elements) => {
|
|
22
|
+
return elements.reduce((acc, current) => {
|
|
23
|
+
if (acc.length === 0)
|
|
24
|
+
return [current];
|
|
25
|
+
// current is an ancestor of some in acc
|
|
26
|
+
if (acc.some(p => p.id === current.id || isAncestor(current, p))) {
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
return [
|
|
30
|
+
// drop ancestors of current
|
|
31
|
+
...acc.filter(e => !isAncestor(e, current)),
|
|
32
|
+
current
|
|
33
|
+
];
|
|
34
|
+
}, []);
|
|
35
|
+
};
|
|
36
|
+
const evaluateElementExpression = (index, expr, rootElement = null) => {
|
|
37
|
+
let elements = [];
|
|
38
|
+
let neighbours = [];
|
|
39
|
+
let relations = [];
|
|
40
|
+
// const inOutRelations = (elements: Element[]) => {
|
|
41
|
+
// if (elements.length == 0) return []
|
|
42
|
+
// const filters = dropNested(elements).map(e => isAnyInOut(e.id))
|
|
43
|
+
// return index.filterRelations(anyPass(filters))
|
|
44
|
+
// }
|
|
45
|
+
const allRelationsBetween = (elements) => {
|
|
46
|
+
if (elements.length <= 1)
|
|
47
|
+
return [];
|
|
48
|
+
const filters = [];
|
|
49
|
+
for (const [source, target] of anyPossibleRelations(elements)) {
|
|
50
|
+
filters.push(isBetween(source.id, target.id));
|
|
51
|
+
}
|
|
52
|
+
return filters.length ? index.filterRelations(anyPass(filters)) : [];
|
|
53
|
+
};
|
|
54
|
+
// WildcardExpression
|
|
55
|
+
if (Expr.isWildcard(expr)) {
|
|
56
|
+
if (rootElement) {
|
|
57
|
+
elements = [index.find(rootElement), ...index.children(rootElement)];
|
|
58
|
+
neighbours = [
|
|
59
|
+
...index.siblings(rootElement),
|
|
60
|
+
...index.ancestors(rootElement).flatMap(a => [...index.siblings(a.id)])
|
|
61
|
+
];
|
|
62
|
+
relations = index.filterRelations(anyPass([isInside(rootElement), isIncoming(rootElement), isOutgoing(rootElement)]));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
elements = index.rootElements();
|
|
66
|
+
neighbours = elements;
|
|
67
|
+
relations = allRelationsBetween(elements);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
elements,
|
|
71
|
+
neighbours,
|
|
72
|
+
relations
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Identifier
|
|
76
|
+
if (Expr.isElementRef(expr)) {
|
|
77
|
+
elements = expr.isDescedants ? index.children(expr.element) : [index.find(expr.element)];
|
|
78
|
+
if (expr.isDescedants) {
|
|
79
|
+
relations = index.filterRelations(isInside(expr.element));
|
|
80
|
+
// relations = index.filterRelations(anyPass([
|
|
81
|
+
// isBetween(expr.element),
|
|
82
|
+
// isIncoming(expr.element),
|
|
83
|
+
// isOutgoing(expr.element),
|
|
84
|
+
// ]))
|
|
85
|
+
// } else {
|
|
86
|
+
// relations = index.filterRelations(anyPass([
|
|
87
|
+
// isIncoming(expr.element),
|
|
88
|
+
// isOutgoing(expr.element)
|
|
89
|
+
// ]))
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
elements,
|
|
93
|
+
neighbours,
|
|
94
|
+
relations
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
failExpectedNever(expr);
|
|
98
|
+
};
|
|
99
|
+
export function evaluateExpression(index, expr, rootElement) {
|
|
100
|
+
const elements = [];
|
|
101
|
+
let neighbours = [];
|
|
102
|
+
let relations = [];
|
|
103
|
+
if (Expr.isInOut(expr)) {
|
|
104
|
+
const targets = evaluateElementExpression(index, expr.inout).elements;
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
const incoming = index.filterRelations(isIncoming(target.id));
|
|
107
|
+
const outgoing = index.filterRelations(isOutgoing(target.id));
|
|
108
|
+
if (incoming.length + outgoing.length > 0) {
|
|
109
|
+
elements.push(target);
|
|
110
|
+
neighbours = neighbours.concat(map(index.find, [...pluck('source', incoming), ...pluck('target', outgoing)]));
|
|
111
|
+
relations = relations.concat([...incoming, ...outgoing]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
elements: uniq(elements),
|
|
116
|
+
neighbours: dropNested(neighbours),
|
|
117
|
+
relations: uniq(relations)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
if (Expr.isIncoming(expr)) {
|
|
121
|
+
const targets = evaluateElementExpression(index, expr.incoming).elements;
|
|
122
|
+
for (const target of targets) {
|
|
123
|
+
const incoming = index.filterRelations(isIncoming(target.id));
|
|
124
|
+
if (incoming.length > 0) {
|
|
125
|
+
elements.push(target);
|
|
126
|
+
neighbours = neighbours.concat(map(index.find, pluck('source', incoming)));
|
|
127
|
+
relations = relations.concat(incoming);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
elements: uniq(elements),
|
|
132
|
+
neighbours: dropNested(neighbours),
|
|
133
|
+
relations: uniq(relations)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (Expr.isOutgoing(expr)) {
|
|
137
|
+
const sources = evaluateElementExpression(index, expr.outgoing).elements;
|
|
138
|
+
for (const source of sources) {
|
|
139
|
+
const outgoing = index.filterRelations(isOutgoing(source.id));
|
|
140
|
+
if (outgoing.length > 0) {
|
|
141
|
+
elements.push(source);
|
|
142
|
+
neighbours = neighbours.concat(map(index.find, pluck('target', outgoing)));
|
|
143
|
+
relations = relations.concat(outgoing);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
elements: uniq(elements),
|
|
148
|
+
neighbours: dropNested(neighbours),
|
|
149
|
+
relations: uniq(relations)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (Expr.isRelation(expr)) {
|
|
153
|
+
const isSourceWildcard = Expr.isWildcard(expr.source);
|
|
154
|
+
const isTargetWildcard = Expr.isWildcard(expr.target);
|
|
155
|
+
if (isSourceWildcard && !isTargetWildcard) {
|
|
156
|
+
return evaluateExpression(index, {
|
|
157
|
+
incoming: expr.target
|
|
158
|
+
}, rootElement);
|
|
159
|
+
}
|
|
160
|
+
if (!isSourceWildcard && isTargetWildcard) {
|
|
161
|
+
return evaluateExpression(index, {
|
|
162
|
+
outgoing: expr.source
|
|
163
|
+
}, rootElement);
|
|
164
|
+
}
|
|
165
|
+
const sources = evaluateElementExpression(index, expr.source).elements;
|
|
166
|
+
const targets = evaluateElementExpression(index, expr.target).elements;
|
|
167
|
+
for (const source of sources) {
|
|
168
|
+
for (const target of targets) {
|
|
169
|
+
if (isAncestor(source.id, target.id) || isAncestor(target.id, source.id)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const foundRelations = index.filterRelations(isBetween(source.id, target.id));
|
|
173
|
+
if (foundRelations.length > 0) {
|
|
174
|
+
relations = relations.concat(foundRelations);
|
|
175
|
+
neighbours.push(source, target);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
elements,
|
|
181
|
+
neighbours: uniq(neighbours),
|
|
182
|
+
relations: uniq(relations)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return evaluateElementExpression(index, expr, rootElement);
|
|
186
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Graph, alg } from '@dagrejs/graphlib';
|
|
2
|
+
export function sortNodes(_nodes, edges) {
|
|
3
|
+
const g = new Graph({
|
|
4
|
+
compound: true,
|
|
5
|
+
directed: true,
|
|
6
|
+
multigraph: false
|
|
7
|
+
});
|
|
8
|
+
for (const nd of _nodes.values()) {
|
|
9
|
+
g.setNode(nd.id);
|
|
10
|
+
// console.log(`add ${nd.id}`)
|
|
11
|
+
if (nd.parent) {
|
|
12
|
+
g.setEdge(nd.parent, nd.id);
|
|
13
|
+
// console.log(`${nd.parent} -> ${nd.id}`)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (const edge of edges) {
|
|
17
|
+
const source = _nodes.get(edge.source);
|
|
18
|
+
let target = _nodes.get(edge.target);
|
|
19
|
+
while (source && target) {
|
|
20
|
+
if (!g.hasEdge(source.id, target.id)) {
|
|
21
|
+
g.setEdge(source.id, target.id);
|
|
22
|
+
// console.log(`${source.id} -> ${target.id}`)
|
|
23
|
+
if (!alg.isAcyclic(g)) {
|
|
24
|
+
g.removeEdge(source.id, target.id);
|
|
25
|
+
// console.log(`remove ${source.id} -> ${target.id}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (target.parent && target.parent !== edge.parent) {
|
|
29
|
+
target = _nodes.get(target.parent);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
target = undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const sorted = alg.topsort(g);
|
|
37
|
+
const nodes = sorted.map(id => _nodes.get(id));
|
|
38
|
+
for (const node of nodes) {
|
|
39
|
+
node.children = nodes.flatMap(n => (n.parent === node.id ? n.id : []));
|
|
40
|
+
}
|
|
41
|
+
return nodes;
|
|
42
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './model-index';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './model-index';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Predicate } from 'rambdax';
|
|
2
|
+
import type { Element, ElementView, Fqn, Relation, RelationID, ViewID } from '../types';
|
|
3
|
+
type ModelInput = {
|
|
4
|
+
elements: Record<Fqn, Element>;
|
|
5
|
+
relations: Record<RelationID, Relation>;
|
|
6
|
+
views: Record<ViewID, ElementView>;
|
|
7
|
+
};
|
|
8
|
+
export declare class ModelIndex {
|
|
9
|
+
private root;
|
|
10
|
+
private _elements;
|
|
11
|
+
private _relations;
|
|
12
|
+
private _defaultElementView;
|
|
13
|
+
get relations(): Relation[];
|
|
14
|
+
filterRelations: (predicate: Predicate<Relation>) => Relation[];
|
|
15
|
+
static from({ elements, relations, views }: ModelInput): ModelIndex;
|
|
16
|
+
addElement(el: Element): void;
|
|
17
|
+
private locateTrie;
|
|
18
|
+
find: (id: Fqn) => Element;
|
|
19
|
+
children: (id: Fqn) => Element[];
|
|
20
|
+
siblings: (id: Fqn) => Element[];
|
|
21
|
+
/**
|
|
22
|
+
* Ancestors from closest parent to root
|
|
23
|
+
*/
|
|
24
|
+
ancestors: (id: Fqn) => Element[];
|
|
25
|
+
rootElements(): Element[];
|
|
26
|
+
get elements(): Element[];
|
|
27
|
+
addRelation(rel: Relation): void;
|
|
28
|
+
defaultViewOf: (id: Fqn) => ViewID[];
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { values } from 'rambdax';
|
|
2
|
+
import invariant from 'tiny-invariant';
|
|
3
|
+
import { parentFqn } from '../utils/fqn';
|
|
4
|
+
function childrenOf(trie) {
|
|
5
|
+
const children = [];
|
|
6
|
+
for (const { el } of values(trie.children)) {
|
|
7
|
+
if (el) {
|
|
8
|
+
children.push(el);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return children;
|
|
12
|
+
}
|
|
13
|
+
function asPath(id) {
|
|
14
|
+
return id.split('.');
|
|
15
|
+
}
|
|
16
|
+
export class ModelIndex {
|
|
17
|
+
root = {
|
|
18
|
+
children: {}
|
|
19
|
+
};
|
|
20
|
+
_elements = new Set();
|
|
21
|
+
_relations = new Map();
|
|
22
|
+
_defaultElementView = new Map();
|
|
23
|
+
// private _taggedElements = new Map<Tag, Set<Element>>()
|
|
24
|
+
// private _taggedRelations = new Map<Tag, Set<Relation>>()
|
|
25
|
+
get relations() {
|
|
26
|
+
return [...this._relations.values()];
|
|
27
|
+
}
|
|
28
|
+
filterRelations = (predicate) => {
|
|
29
|
+
return [...this._relations.values()].filter(predicate);
|
|
30
|
+
};
|
|
31
|
+
static from({ elements, relations, views }) {
|
|
32
|
+
const index = new ModelIndex();
|
|
33
|
+
for (const el of Object.values(elements)) {
|
|
34
|
+
index.addElement(el);
|
|
35
|
+
}
|
|
36
|
+
for (const rel of Object.values(relations)) {
|
|
37
|
+
index.addRelation(rel);
|
|
38
|
+
}
|
|
39
|
+
for (const { id, viewOf } of Object.values(views)) {
|
|
40
|
+
if (viewOf) {
|
|
41
|
+
const views = index._defaultElementView.get(viewOf) ?? [];
|
|
42
|
+
views.push(id);
|
|
43
|
+
index._defaultElementView.set(viewOf, views);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
addElement(el) {
|
|
49
|
+
const path = asPath(el.id);
|
|
50
|
+
let scope = this.root;
|
|
51
|
+
for (const name of path) {
|
|
52
|
+
const next = scope.children[name] ?? {
|
|
53
|
+
children: {}
|
|
54
|
+
};
|
|
55
|
+
scope.children[name] = next;
|
|
56
|
+
scope = next;
|
|
57
|
+
}
|
|
58
|
+
scope.el = el;
|
|
59
|
+
this._elements.add(el);
|
|
60
|
+
}
|
|
61
|
+
locateTrie = (id) => {
|
|
62
|
+
let scope = this.root;
|
|
63
|
+
for (const name of asPath(id)) {
|
|
64
|
+
const next = scope.children[name];
|
|
65
|
+
invariant(next, `Invalid index, Element not found at path ${name} of ${id}`);
|
|
66
|
+
scope = next;
|
|
67
|
+
}
|
|
68
|
+
return scope;
|
|
69
|
+
};
|
|
70
|
+
find = (id) => {
|
|
71
|
+
const trie = this.locateTrie(id);
|
|
72
|
+
if (!trie.el) {
|
|
73
|
+
throw new Error(`Invalid index, element not found at path ${id}`);
|
|
74
|
+
}
|
|
75
|
+
return trie.el;
|
|
76
|
+
};
|
|
77
|
+
children = (id) => {
|
|
78
|
+
return childrenOf(this.locateTrie(id));
|
|
79
|
+
};
|
|
80
|
+
siblings = (id) => {
|
|
81
|
+
const parent = parentFqn(id);
|
|
82
|
+
const trie = parent ? this.locateTrie(parent) : this.root;
|
|
83
|
+
return childrenOf(trie).filter(e => e.id !== id);
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Ancestors from closest parent to root
|
|
87
|
+
*/
|
|
88
|
+
ancestors = (id) => {
|
|
89
|
+
const path = asPath(id);
|
|
90
|
+
const ancestors = [];
|
|
91
|
+
// The root
|
|
92
|
+
if (path.length === 1) {
|
|
93
|
+
return ancestors;
|
|
94
|
+
}
|
|
95
|
+
// Remove the element itself
|
|
96
|
+
path.pop();
|
|
97
|
+
let name = path.shift();
|
|
98
|
+
let trie = this.root;
|
|
99
|
+
while (name) {
|
|
100
|
+
const next = trie.children[name];
|
|
101
|
+
invariant(next, `Invalid index, Element not found at path ${name} of ${id}`);
|
|
102
|
+
trie = next;
|
|
103
|
+
if (!trie.el) {
|
|
104
|
+
throw new Error(`invalid index, no element ${name} found in ${id}`);
|
|
105
|
+
}
|
|
106
|
+
ancestors.unshift(trie.el);
|
|
107
|
+
name = path.shift();
|
|
108
|
+
}
|
|
109
|
+
return ancestors;
|
|
110
|
+
};
|
|
111
|
+
// tagged = (tag?: Tag): TaggedResult => {
|
|
112
|
+
// return tag ? {
|
|
113
|
+
// elements: [...this._taggedElements.get(tag)?.values() ?? []],
|
|
114
|
+
// relations: [...this._taggedRelations.get(tag)?.values() ?? []],
|
|
115
|
+
// } : {
|
|
116
|
+
// elements: uniq([...this._taggedElements.values()].flatMap(s => [...s.values()])),
|
|
117
|
+
// relations: uniq([...this._taggedRelations.values()].flatMap(s => [...s.values()]))
|
|
118
|
+
// }
|
|
119
|
+
// }
|
|
120
|
+
rootElements() {
|
|
121
|
+
return childrenOf(this.root);
|
|
122
|
+
}
|
|
123
|
+
get elements() {
|
|
124
|
+
return [...this._elements];
|
|
125
|
+
}
|
|
126
|
+
// hasElement(fqn: Fqn): boolean {
|
|
127
|
+
// return fqn in this._elements
|
|
128
|
+
// }
|
|
129
|
+
addRelation(rel) {
|
|
130
|
+
// Validate source and target
|
|
131
|
+
this.locateTrie(rel.source);
|
|
132
|
+
this.locateTrie(rel.target);
|
|
133
|
+
this._relations.set(rel.id, rel);
|
|
134
|
+
// for (const tag of rel.tags) {
|
|
135
|
+
// const tagged = this._taggedRelations.get(tag) ?? new Set()
|
|
136
|
+
// tagged.add(rel)
|
|
137
|
+
// this._taggedRelations.set(tag, tagged)
|
|
138
|
+
// }
|
|
139
|
+
}
|
|
140
|
+
defaultViewOf = (id) => {
|
|
141
|
+
return this._defaultElementView.get(id) ?? [];
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Opaque } from './opaque';
|
|
2
|
+
import type { ElementShape, Fqn, ThemeColor } from './element';
|
|
3
|
+
import type { RelationID } from './relation';
|
|
4
|
+
import type { ElementView, ViewID, ViewRuleAutoLayout } from './view';
|
|
5
|
+
export type NodeId = Fqn;
|
|
6
|
+
export type EdgeId = Opaque<string, 'EdgeId'>;
|
|
7
|
+
export interface ComputedNode {
|
|
8
|
+
id: NodeId;
|
|
9
|
+
parent: NodeId | null;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
technology?: string;
|
|
13
|
+
children: NodeId[];
|
|
14
|
+
shape: ElementShape;
|
|
15
|
+
color: ThemeColor;
|
|
16
|
+
navigateTo?: ViewID;
|
|
17
|
+
}
|
|
18
|
+
export interface ComputedEdge {
|
|
19
|
+
id: EdgeId;
|
|
20
|
+
parent: NodeId | null;
|
|
21
|
+
source: NodeId;
|
|
22
|
+
target: NodeId;
|
|
23
|
+
label: string | null;
|
|
24
|
+
relations: RelationID[];
|
|
25
|
+
}
|
|
26
|
+
export interface ComputedView<Node extends ComputedNode = ComputedNode, Edge extends ComputedEdge = ComputedEdge> extends ElementView {
|
|
27
|
+
autoLayout: ViewRuleAutoLayout['autoLayout'];
|
|
28
|
+
nodes: Node[];
|
|
29
|
+
edges: Edge[];
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComputedEdge, ComputedNode, ComputedView } from './computed-view';
|
|
2
|
+
export type Point = [x: number, y: number];
|
|
3
|
+
export interface DiagramLabel {
|
|
4
|
+
align: 'left' | 'right' | 'center';
|
|
5
|
+
fontStyle?: 'bold' | 'normal';
|
|
6
|
+
fontSize: number;
|
|
7
|
+
pt: Point;
|
|
8
|
+
width: number;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DiagramNode extends ComputedNode {
|
|
12
|
+
size: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
labels: DiagramLabel[];
|
|
17
|
+
position: Point;
|
|
18
|
+
}
|
|
19
|
+
export interface DiagramEdge extends ComputedEdge {
|
|
20
|
+
points: Point[];
|
|
21
|
+
headArrow?: Point[];
|
|
22
|
+
labels: DiagramLabel[];
|
|
23
|
+
}
|
|
24
|
+
export interface DiagramView extends ComputedView<DiagramNode, DiagramEdge> {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Opaque } from './opaque';
|
|
2
|
+
export type Fqn = Opaque<string, 'Fqn'>;
|
|
3
|
+
export declare function Fqn(name: string, parent?: Fqn | null): Fqn;
|
|
4
|
+
export type ElementKind = Opaque<string, 'ElementKind'>;
|
|
5
|
+
export type ThemeColor = 'primary' | 'secondary' | 'muted';
|
|
6
|
+
export type ElementShape = 'rectangle' | 'person' | 'browser' | 'cylinder' | 'storage' | 'queue';
|
|
7
|
+
export declare const DefaultThemeColor: ThemeColor;
|
|
8
|
+
export declare const DefaultElementShape: ElementShape;
|
|
9
|
+
export interface ElementStyle {
|
|
10
|
+
shape?: ElementShape;
|
|
11
|
+
}
|
|
12
|
+
export type Tag = Opaque<string, 'Tag'>;
|
|
13
|
+
export interface TagSpec {
|
|
14
|
+
readonly id: Tag;
|
|
15
|
+
readonly style: ElementStyle;
|
|
16
|
+
}
|
|
17
|
+
export interface Element {
|
|
18
|
+
readonly id: Fqn;
|
|
19
|
+
readonly kind: ElementKind;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly description?: string;
|
|
22
|
+
readonly technology?: string;
|
|
23
|
+
readonly tags?: Tag[];
|
|
24
|
+
readonly shape?: ElementShape;
|
|
25
|
+
readonly color?: ThemeColor;
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Fqn } from './element';
|
|
2
|
+
interface BaseExr {
|
|
3
|
+
element?: never;
|
|
4
|
+
isDescedants?: never;
|
|
5
|
+
wildcard?: never;
|
|
6
|
+
source?: never;
|
|
7
|
+
target?: never;
|
|
8
|
+
inout?: never;
|
|
9
|
+
incoming?: never;
|
|
10
|
+
outgoing?: never;
|
|
11
|
+
}
|
|
12
|
+
export interface ElementRefExpr extends Omit<BaseExr, 'element' | 'isDescedants'> {
|
|
13
|
+
element: Fqn;
|
|
14
|
+
isDescedants: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function isElementRef(expr: Expression): expr is ElementRefExpr;
|
|
17
|
+
export interface WildcardExpr extends Omit<BaseExr, 'wildcard'> {
|
|
18
|
+
wildcard: true;
|
|
19
|
+
}
|
|
20
|
+
export declare function isWildcard(expr: Expression): expr is WildcardExpr;
|
|
21
|
+
export type ElementExpression = ElementRefExpr | WildcardExpr;
|
|
22
|
+
export declare function isElement(expr: Expression): expr is ElementExpression;
|
|
23
|
+
export interface RelationExpr extends Omit<BaseExr, 'source' | 'target'> {
|
|
24
|
+
source: ElementExpression;
|
|
25
|
+
target: ElementExpression;
|
|
26
|
+
}
|
|
27
|
+
export declare function isRelation(expr: Expression): expr is RelationExpr;
|
|
28
|
+
export interface InOutExpr extends Omit<BaseExr, 'inout'> {
|
|
29
|
+
inout: ElementExpression;
|
|
30
|
+
}
|
|
31
|
+
export declare function isInOut(expr: Expression): expr is InOutExpr;
|
|
32
|
+
export interface IncomingExpr extends Omit<BaseExr, 'incoming'> {
|
|
33
|
+
incoming: ElementExpression;
|
|
34
|
+
}
|
|
35
|
+
export declare function isIncoming(expr: Expression): expr is IncomingExpr;
|
|
36
|
+
export interface OutgoingExpr extends Omit<BaseExr, 'outgoing'> {
|
|
37
|
+
outgoing: ElementExpression;
|
|
38
|
+
}
|
|
39
|
+
export declare function isOutgoing(expr: Expression): expr is OutgoingExpr;
|
|
40
|
+
export type AnyRelationExpression = RelationExpr | InOutExpr | IncomingExpr | OutgoingExpr;
|
|
41
|
+
export declare function isAnyRelation(expr: Expression): expr is AnyRelationExpression;
|
|
42
|
+
export type Expression = ElementExpression | AnyRelationExpression;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function isElementRef(expr) {
|
|
2
|
+
return 'element' in expr && 'isDescedants' in expr;
|
|
3
|
+
}
|
|
4
|
+
export function isWildcard(expr) {
|
|
5
|
+
return 'wildcard' in expr;
|
|
6
|
+
}
|
|
7
|
+
export function isElement(expr) {
|
|
8
|
+
return isElementRef(expr) || isWildcard(expr);
|
|
9
|
+
}
|
|
10
|
+
export function isRelation(expr) {
|
|
11
|
+
return 'source' in expr && 'target' in expr;
|
|
12
|
+
}
|
|
13
|
+
export function isInOut(expr) {
|
|
14
|
+
return 'inout' in expr;
|
|
15
|
+
}
|
|
16
|
+
export function isIncoming(expr) {
|
|
17
|
+
return 'incoming' in expr;
|
|
18
|
+
}
|
|
19
|
+
export function isOutgoing(expr) {
|
|
20
|
+
return 'outgoing' in expr;
|
|
21
|
+
}
|
|
22
|
+
export function isAnyRelation(expr) {
|
|
23
|
+
return isRelation(expr) || isInOut(expr) || isIncoming(expr) || isOutgoing(expr);
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Fqn, DefaultThemeColor, DefaultElementShape } from './element';
|
|
2
|
+
export { isViewRuleExpression, isViewRuleStyle } from './view';
|
|
3
|
+
export type * from './opaque';
|
|
4
|
+
export type * from './element';
|
|
5
|
+
export type * from './relation';
|
|
6
|
+
export type * from './model';
|
|
7
|
+
export type * from './view';
|
|
8
|
+
export type * from './expression';
|
|
9
|
+
export type * from './computed-view';
|
|
10
|
+
export type * from './diagram';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Fqn, Element } from './element';
|
|
2
|
+
import type { RelationID, Relation } from './relation';
|
|
3
|
+
import type { ViewID } from './view';
|
|
4
|
+
import type { ComputedView } from '../compute-view';
|
|
5
|
+
export interface LikeC4Model {
|
|
6
|
+
elements: Record<Fqn, Element>;
|
|
7
|
+
relations: Record<RelationID, Relation>;
|
|
8
|
+
views: Record<ViewID, ComputedView>;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
declare const tag: unique symbol;
|
|
2
|
+
declare type Tagged<Token> = {
|
|
3
|
+
readonly [tag]: Token;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
Create an opaque type, which hides its internal details from the public, and can only be created by being used explicitly.
|
|
7
|
+
|
|
8
|
+
The generic type parameter can be anything. It doesn't have to be an object.
|
|
9
|
+
|
|
10
|
+
[Read more about opaque types.](https://codemix.com/opaque-types-in-javascript/)
|
|
11
|
+
|
|
12
|
+
There have been several discussions about adding this feature to TypeScript via the `opaque type` operator, similar to how Flow does it. Unfortunately, nothing has (yet) moved forward:
|
|
13
|
+
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
|
|
14
|
+
- [Microsoft/TypeScript#15408](https://github.com/Microsoft/TypeScript/issues/15408)
|
|
15
|
+
- [Microsoft/TypeScript#15807](https://github.com/Microsoft/TypeScript/issues/15807)
|
|
16
|
+
|
|
17
|
+
@example
|
|
18
|
+
```
|
|
19
|
+
import type {Opaque} from 'type-fest';
|
|
20
|
+
|
|
21
|
+
type AccountNumber = Opaque<number, 'AccountNumber'>;
|
|
22
|
+
type AccountBalance = Opaque<number, 'AccountBalance'>;
|
|
23
|
+
|
|
24
|
+
// The `Token` parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures:
|
|
25
|
+
type ThingOne = Opaque<string>;
|
|
26
|
+
type ThingTwo = Opaque<string>;
|
|
27
|
+
|
|
28
|
+
// To the compiler, these types are allowed to be cast to each other as they have the same underlying type. They are both `string & { __opaque__: unknown }`.
|
|
29
|
+
// To avoid this behaviour, you would instead pass the "Token" parameter, like so.
|
|
30
|
+
type NewThingOne = Opaque<string, 'ThingOne'>;
|
|
31
|
+
type NewThingTwo = Opaque<string, 'ThingTwo'>;
|
|
32
|
+
|
|
33
|
+
// Now they're completely separate types, so the following will fail to compile.
|
|
34
|
+
function createNewThingOne (): NewThingOne {
|
|
35
|
+
// As you can see, casting from a string is still allowed. However, you may not cast NewThingOne to NewThingTwo, and vice versa.
|
|
36
|
+
return 'new thing one' as NewThingOne;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// This will fail to compile, as they are fundamentally different types.
|
|
40
|
+
const thingTwo = createNewThingOne() as NewThingTwo;
|
|
41
|
+
|
|
42
|
+
// Here's another example of opaque typing.
|
|
43
|
+
function createAccountNumber(): AccountNumber {
|
|
44
|
+
return 2 as AccountNumber;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
|
|
48
|
+
return 4 as AccountBalance;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// This will compile successfully.
|
|
52
|
+
getMoneyForAccount(createAccountNumber());
|
|
53
|
+
|
|
54
|
+
// But this won't, because it has to be explicitly passed as an `AccountNumber` type.
|
|
55
|
+
getMoneyForAccount(2);
|
|
56
|
+
|
|
57
|
+
// You can use opaque values like they aren't opaque too.
|
|
58
|
+
const accountNumber = createAccountNumber();
|
|
59
|
+
|
|
60
|
+
// This will not compile successfully.
|
|
61
|
+
const newAccountNumber = accountNumber + 2;
|
|
62
|
+
|
|
63
|
+
// As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type.
|
|
64
|
+
type Person = {
|
|
65
|
+
id: Opaque<number, Person>;
|
|
66
|
+
name: string;
|
|
67
|
+
};
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
@category Type
|
|
71
|
+
*/
|
|
72
|
+
export type Opaque<Type, Token = unknown> = Type & Tagged<Token>;
|
|
73
|
+
/**
|
|
74
|
+
Revert an opaque type back to its original type by removing the readonly `[tag]`.
|
|
75
|
+
|
|
76
|
+
Why is this necessary?
|
|
77
|
+
|
|
78
|
+
1. Use an `Opaque` type as object keys
|
|
79
|
+
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
|
|
80
|
+
|
|
81
|
+
@example
|
|
82
|
+
```
|
|
83
|
+
import type {Opaque, UnwrapOpaque} from 'type-fest';
|
|
84
|
+
|
|
85
|
+
type AccountType = Opaque<'SAVINGS' | 'CHECKING', 'AccountType'>;
|
|
86
|
+
|
|
87
|
+
const moneyByAccountType: Record<UnwrapOpaque<AccountType>, number> = {
|
|
88
|
+
SAVINGS: 99,
|
|
89
|
+
CHECKING: 0.1
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Without UnwrapOpaque, the following expression would throw a type error.
|
|
93
|
+
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
|
|
94
|
+
|
|
95
|
+
// Attempting to pass an non-Opaque type to UnwrapOpaque will raise a type error.
|
|
96
|
+
type WontWork = UnwrapOpaque<string>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
@category Type
|
|
100
|
+
*/
|
|
101
|
+
export type UnwrapOpaque<OpaqueType extends Tagged<unknown>> = OpaqueType extends Opaque<infer Type, OpaqueType[typeof tag]> ? Type : OpaqueType;
|
|
102
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Opaque } from './opaque';
|
|
2
|
+
import type { Fqn, Tag } from './element';
|
|
3
|
+
export type RelationID = Opaque<string, 'RelationID'>;
|
|
4
|
+
export interface Relation {
|
|
5
|
+
readonly id: RelationID;
|
|
6
|
+
readonly source: Fqn;
|
|
7
|
+
readonly target: Fqn;
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly tags?: Tag[];
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Opaque } from './opaque';
|
|
2
|
+
import type { ElementShape, Fqn, ThemeColor } from './element';
|
|
3
|
+
import type { ElementExpression, Expression } from './expression';
|
|
4
|
+
export type ViewID = Opaque<string, 'ViewID'>;
|
|
5
|
+
export interface ViewRuleExpression {
|
|
6
|
+
isInclude: boolean;
|
|
7
|
+
exprs: Expression[];
|
|
8
|
+
}
|
|
9
|
+
export declare function isViewRuleExpression(rule: ViewRule): rule is ViewRuleExpression;
|
|
10
|
+
export interface ViewRuleStyle {
|
|
11
|
+
targets: ElementExpression[];
|
|
12
|
+
style: {
|
|
13
|
+
color?: ThemeColor;
|
|
14
|
+
shape?: ElementShape;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function isViewRuleStyle(rule: ViewRule): rule is ViewRuleStyle;
|
|
18
|
+
export interface ViewRuleAutoLayout {
|
|
19
|
+
autoLayout: 'TB' | 'BT' | 'LR' | 'RL';
|
|
20
|
+
}
|
|
21
|
+
export declare function isViewRuleAutoLayout(rule: ViewRule): rule is ViewRuleAutoLayout;
|
|
22
|
+
export type ViewRule = ViewRuleExpression | ViewRuleStyle | ViewRuleAutoLayout;
|
|
23
|
+
export interface ElementView {
|
|
24
|
+
readonly id: ViewID;
|
|
25
|
+
readonly viewOf?: Fqn;
|
|
26
|
+
readonly title?: string;
|
|
27
|
+
readonly description?: string;
|
|
28
|
+
readonly rules: ViewRule[];
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function isViewRuleExpression(rule) {
|
|
2
|
+
return 'exprs' in rule && 'isInclude' in rule;
|
|
3
|
+
}
|
|
4
|
+
export function isViewRuleStyle(rule) {
|
|
5
|
+
return 'style' in rule && 'targets' in rule;
|
|
6
|
+
}
|
|
7
|
+
export function isViewRuleAutoLayout(rule) {
|
|
8
|
+
return 'autoLayout' in rule;
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Element, Fqn } from '../types';
|
|
2
|
+
export declare function nameFromFqn(fqn: Fqn): string;
|
|
3
|
+
export declare function isAncestor(...args: [ancestor: Fqn, another: Fqn] | [ancestor: Element, another: Element]): boolean;
|
|
4
|
+
export declare function isSameHierarchy(...args: [one: Fqn, another: Fqn] | [one: Element, another: Element]): boolean;
|
|
5
|
+
export declare function commonAncestor(first: Fqn, second: Fqn): Fqn | null;
|
|
6
|
+
export declare function parentFqn(fqn: Fqn): Fqn | null;
|
|
7
|
+
export declare const compareFqnHierarchically: (a: string, b: string) => number;
|
|
8
|
+
export declare const compareByFqnHierarchically: <T extends {
|
|
9
|
+
id: Fqn;
|
|
10
|
+
}>(a: T, b: T) => number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isString } from './guards';
|
|
2
|
+
export function nameFromFqn(fqn) {
|
|
3
|
+
const lastDot = fqn.lastIndexOf('.');
|
|
4
|
+
if (lastDot > 0) {
|
|
5
|
+
return fqn.slice(lastDot + 1);
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
return fqn;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isAncestor(...args) {
|
|
12
|
+
const ancestor = isString(args[0]) ? args[0] : args[0].id;
|
|
13
|
+
const another = isString(args[1]) ? args[1] : args[1].id;
|
|
14
|
+
return another.startsWith(ancestor + '.');
|
|
15
|
+
}
|
|
16
|
+
export function isSameHierarchy(...args) {
|
|
17
|
+
const one = isString(args[0]) ? args[0] : args[0].id;
|
|
18
|
+
const another = isString(args[1]) ? args[1] : args[1].id;
|
|
19
|
+
return one === another || another.startsWith(one + '.') || one.startsWith(another + '.');
|
|
20
|
+
}
|
|
21
|
+
export function commonAncestor(first, second) {
|
|
22
|
+
const a = first.split('.');
|
|
23
|
+
const b = second.split('.');
|
|
24
|
+
let ancestor = null;
|
|
25
|
+
while (a.length > 1 && b.length > 1 && !!a[0] && a[0] === b[0]) {
|
|
26
|
+
ancestor = (ancestor ? `${ancestor}.${a[0]}` : a[0]);
|
|
27
|
+
a.shift();
|
|
28
|
+
b.shift();
|
|
29
|
+
}
|
|
30
|
+
return ancestor;
|
|
31
|
+
}
|
|
32
|
+
export function parentFqn(fqn) {
|
|
33
|
+
const lastDot = fqn.lastIndexOf('.');
|
|
34
|
+
if (lastDot > 0) {
|
|
35
|
+
return fqn.substring(0, lastDot);
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
export const compareFqnHierarchically = (a, b) => {
|
|
40
|
+
const depthA = a.split('.').length;
|
|
41
|
+
const depthB = b.split('.').length;
|
|
42
|
+
if (depthA === depthB) {
|
|
43
|
+
return a.localeCompare(b);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return depthA - depthB;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export const compareByFqnHierarchically = (a, b) => {
|
|
50
|
+
return compareFqnHierarchically(a.id, b.id);
|
|
51
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function isString(value) {
|
|
2
|
+
return typeof value === 'string';
|
|
3
|
+
}
|
|
4
|
+
export function failExpectedNever(arg) {
|
|
5
|
+
throw new Error(`Unexpected value: ${JSON.stringify(arg)}`);
|
|
6
|
+
}
|
|
7
|
+
export function ignoreNeverInRuntime(arg) {
|
|
8
|
+
console.warn(`Unexpected and ignored value: ${JSON.stringify(arg)}`);
|
|
9
|
+
// throw new Error(`Unexpected value: ${arg}`);
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Fqn } from '../types';
|
|
2
|
+
type Relation = {
|
|
3
|
+
source: Fqn;
|
|
4
|
+
target: Fqn;
|
|
5
|
+
};
|
|
6
|
+
export declare const compareRelations: <T extends {
|
|
7
|
+
source: Fqn;
|
|
8
|
+
target: Fqn;
|
|
9
|
+
}>(a: T, b: T) => number;
|
|
10
|
+
export declare const isInside: (parent: Fqn) => (rel: Relation) => boolean;
|
|
11
|
+
export declare const isBetween: (source: Fqn, target: Fqn) => (rel: Relation) => boolean;
|
|
12
|
+
export declare const isAnyBetween: (source: Fqn, target: Fqn) => import("rambdax").Predicate<Relation>;
|
|
13
|
+
export declare const isIncoming: (target: Fqn) => (rel: Relation) => boolean;
|
|
14
|
+
export declare const isOutgoing: (source: Fqn) => (rel: Relation) => boolean;
|
|
15
|
+
export declare const isAnyInOut: (source: Fqn) => import("rambdax").Predicate<Relation>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { either } from 'rambdax';
|
|
2
|
+
import { compareFqnHierarchically } from './fqn';
|
|
3
|
+
export const compareRelations = (a, b) => {
|
|
4
|
+
return (compareFqnHierarchically(a.source, b.source) || compareFqnHierarchically(a.target, b.target));
|
|
5
|
+
};
|
|
6
|
+
export const isInside = (parent) => {
|
|
7
|
+
const prefix = parent + '.';
|
|
8
|
+
return (rel) => {
|
|
9
|
+
return rel.source.startsWith(prefix) && rel.target.startsWith(prefix);
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export const isBetween = (source, target) => {
|
|
13
|
+
const sourcePrefix = source + '.';
|
|
14
|
+
const targetPrefix = target + '.';
|
|
15
|
+
return (rel) => {
|
|
16
|
+
return ((rel.source + '.').startsWith(sourcePrefix) && (rel.target + '.').startsWith(targetPrefix));
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
export const isAnyBetween = (source, target) => {
|
|
20
|
+
return either(isBetween(source, target), isBetween(target, source));
|
|
21
|
+
};
|
|
22
|
+
export const isIncoming = (target) => {
|
|
23
|
+
const targetPrefix = target + '.';
|
|
24
|
+
return (rel) => {
|
|
25
|
+
return (!(rel.source + '.').startsWith(targetPrefix) && (rel.target + '.').startsWith(targetPrefix));
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export const isOutgoing = (source) => {
|
|
29
|
+
const sourcePrefix = source + '.';
|
|
30
|
+
return (rel) => {
|
|
31
|
+
return ((rel.source + '.').startsWith(sourcePrefix) && !(rel.target + '.').startsWith(sourcePrefix));
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export const isAnyInOut = (source) => {
|
|
35
|
+
return either(isIncoming(source), isOutgoing(source));
|
|
36
|
+
};
|