@likec4/core 1.1.1 → 1.2.1

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 (56) hide show
  1. package/dist/colors/element.d.ts +1 -0
  2. package/dist/colors/index.d.ts +1 -0
  3. package/dist/colors/relationships.d.ts +1 -0
  4. package/dist/errors/errors.spec.d.ts +2 -0
  5. package/dist/errors/errors.spec.js +69 -0
  6. package/dist/errors/index.d.ts +1 -0
  7. package/dist/errors/index.js +7 -1
  8. package/dist/index.d.ts +1 -0
  9. package/dist/types/_common.d.ts +1 -0
  10. package/dist/types/element.d.ts +1 -0
  11. package/dist/types/expression.d.ts +1 -0
  12. package/dist/types/index.d.ts +1 -2
  13. package/dist/types/index.js +2 -3
  14. package/dist/types/model.d.ts +3 -3
  15. package/dist/types/opaque.d.ts +1 -0
  16. package/dist/types/relation.d.ts +1 -0
  17. package/dist/types/theme.d.ts +1 -0
  18. package/dist/types/view.d.ts +133 -8
  19. package/dist/types/view.js +31 -3
  20. package/dist/utils/fqn.d.ts +1 -0
  21. package/dist/utils/fqn.spec.d.ts +2 -0
  22. package/dist/utils/fqn.spec.js +82 -0
  23. package/dist/utils/guards.d.ts +1 -0
  24. package/dist/utils/index.d.ts +1 -0
  25. package/dist/utils/promises.d.ts +1 -0
  26. package/dist/utils/relations.d.ts +1 -0
  27. package/dist/utils/relations.spec.d.ts +2 -0
  28. package/dist/utils/relations.spec.js +210 -0
  29. package/package.json +6 -6
  30. package/src/colors/element.ts +80 -0
  31. package/src/colors/index.ts +12 -0
  32. package/src/colors/relationships.ts +54 -0
  33. package/src/errors/errors.spec.ts +88 -0
  34. package/src/errors/index.ts +120 -0
  35. package/src/index.ts +9 -0
  36. package/src/reset.d.ts +2 -0
  37. package/src/types/_common.ts +5 -0
  38. package/src/types/element.ts +56 -0
  39. package/src/types/expression.ts +140 -0
  40. package/src/types/index.ts +10 -0
  41. package/src/types/model.ts +15 -0
  42. package/src/types/opaque.ts +108 -0
  43. package/src/types/relation.ts +38 -0
  44. package/src/types/theme.ts +46 -0
  45. package/src/types/view.ts +263 -0
  46. package/src/utils/fqn.spec.ts +105 -0
  47. package/src/utils/fqn.ts +112 -0
  48. package/src/utils/guards.ts +10 -0
  49. package/src/utils/index.ts +4 -0
  50. package/src/utils/promises.ts +9 -0
  51. package/src/utils/relations.spec.ts +268 -0
  52. package/src/utils/relations.ts +104 -0
  53. package/dist/types/computed-view.d.ts +0 -52
  54. package/dist/types/computed-view.js +0 -1
  55. package/dist/types/diagram.d.ts +0 -40
  56. package/dist/types/diagram.js +0 -1
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Element, Fqn } from '../types'
3
+ import {
4
+ ancestorsFqn,
5
+ commonAncestor,
6
+ compareFqnHierarchically,
7
+ isAncestor,
8
+ isDescendantOf,
9
+ notDescendantOf,
10
+ parentFqn
11
+ } from './fqn'
12
+
13
+ const el = (id: string): Element => ({ id }) as unknown as Element
14
+
15
+ describe('parentFqn', () => {
16
+ it('should return null if no parent', () => {
17
+ expect(parentFqn('a' as Fqn)).toBeNull()
18
+ })
19
+ it('should return parent', () => {
20
+ expect(parentFqn('a.b' as Fqn)).toBe('a')
21
+ expect(parentFqn('a.b.c' as Fqn)).toBe('a.b')
22
+ })
23
+ })
24
+
25
+ describe('ancestorsFqn', () => {
26
+ it('should return empty array if no parent', () => {
27
+ expect(ancestorsFqn('a' as Fqn)).toEqual([])
28
+ })
29
+ it('should return ancestors', () => {
30
+ expect(ancestorsFqn('a.b.c.d.e' as Fqn)).toEqual(['a.b.c.d', 'a.b.c', 'a.b', 'a'])
31
+ })
32
+ })
33
+
34
+ describe('commonAncestor', () => {
35
+ it('should return null if no common ancestor', () => {
36
+ expect(commonAncestor('a' as Fqn, 'b' as Fqn)).toBeNull()
37
+ expect(commonAncestor('a.b' as Fqn, 'c.d' as Fqn)).toBeNull()
38
+ })
39
+
40
+ it('should return common ancestor', () => {
41
+ expect(commonAncestor('a.b' as Fqn, 'a.c' as Fqn)).toBe('a')
42
+ expect(commonAncestor('a.b.c' as Fqn, 'a.b.e' as Fqn)).toBe('a.b')
43
+ expect(commonAncestor('a.b.c.d.e' as Fqn, 'a.b.c.d' as Fqn)).toBe('a.b.c')
44
+ })
45
+ })
46
+
47
+ describe('isAncestor', () => {
48
+ it('should return true if ancestor', () => {
49
+ expect(isAncestor('a' as Fqn, 'a.b' as Fqn)).toBe(true)
50
+ expect(isAncestor('a.b' as Fqn, 'a.b.c' as Fqn)).toBe(true)
51
+ })
52
+ it('should return false if not ancestor', () => {
53
+ expect(isAncestor('a' as Fqn, 'b' as Fqn)).toBe(false)
54
+ expect(isAncestor('a.b' as Fqn, 'a' as Fqn)).toBe(false)
55
+ expect(isAncestor('a.b' as Fqn, 'b.a' as Fqn)).toBe(false)
56
+ })
57
+ })
58
+
59
+ describe('isDescendantOf', () => {
60
+ const predicate = isDescendantOf(['a', 'b', 'a.b', 'a.b.c'].map(el))
61
+
62
+ it('should return true if isDescendantOf', () => {
63
+ expect(predicate(el('a'))).toBe(true)
64
+ expect(predicate(el('b.c'))).toBe(true)
65
+ expect(predicate(el('a.b.c.d.e'))).toBe(true)
66
+ })
67
+ it('should return false if not descendantOf', () => {
68
+ expect(predicate(el('c'))).toBe(false)
69
+ expect(predicate(el('ac'))).toBe(false)
70
+ expect(predicate(el('d.a.c'))).toBe(false)
71
+ })
72
+ })
73
+
74
+ describe('notDescendantOf', () => {
75
+ const predicate = notDescendantOf(['a', 'b', 'a.b', 'a.b.c'].map(el))
76
+
77
+ it('should return true if notDescendantOf', () => {
78
+ expect(predicate(el('c'))).toBe(true)
79
+ expect(predicate(el('ac'))).toBe(true)
80
+ expect(predicate(el('d.a.c'))).toBe(true)
81
+ })
82
+ it('should return false if descendantOf', () => {
83
+ expect(predicate(el('a'))).toBe(false)
84
+ expect(predicate(el('b.c'))).toBe(false)
85
+ expect(predicate(el('a.b.c.d.e'))).toBe(false)
86
+ })
87
+ })
88
+
89
+ describe('compareFqnHierarchically', () => {
90
+ it('should compare hierarchically', () => {
91
+ expect(['a', 'b', 'a.b', 'a.b.c', 'a.c.c'].sort(compareFqnHierarchically)).toEqual([
92
+ 'a',
93
+ 'b',
94
+ 'a.b',
95
+ 'a.b.c',
96
+ 'a.c.c'
97
+ ])
98
+ })
99
+
100
+ it('should preserve initial order', () => {
101
+ expect(
102
+ ['aaa', 'aa', 'a', 'aaa.c', 'aa.b', 'a.c', 'a.b'].sort(compareFqnHierarchically)
103
+ ).toEqual(['aaa', 'aa', 'a', 'aaa.c', 'aa.b', 'a.c', 'a.b'])
104
+ })
105
+ })
@@ -0,0 +1,112 @@
1
+ import type { Element, Fqn } from '../types'
2
+ import { isString } from './guards'
3
+
4
+ export function nameFromFqn(fqn: Fqn) {
5
+ const lastDot = fqn.lastIndexOf('.')
6
+ if (lastDot > 0) {
7
+ return fqn.slice(lastDot + 1)
8
+ } else {
9
+ return fqn
10
+ }
11
+ }
12
+
13
+ export function isAncestor<E extends { id: Fqn }>(
14
+ ...args: [ancestor: string, another: string] | [ancestor: E, another: E]
15
+ ) {
16
+ const ancestor = isString(args[0]) ? args[0] : args[0].id
17
+ const another = isString(args[1]) ? args[1] : args[1].id
18
+ return another.startsWith(ancestor + '.')
19
+ }
20
+
21
+ export function isSameHierarchy<E extends { id: Fqn }>(one: E | Fqn, another: E | Fqn) {
22
+ const first = isString(one) ? one : one.id
23
+ const second = isString(another) ? another : another.id
24
+ return first === second || second.startsWith(first + '.') || first.startsWith(second + '.')
25
+ }
26
+
27
+ export function isDescendantOf<E extends { id: Fqn }>(ancestors: E[]): (e: E) => boolean {
28
+ const predicates = ancestors.flatMap(a => [(e: E) => e.id === a.id, (e: E) => isAncestor(a, e)])
29
+ return (e: E) => predicates.some(p => p(e))
30
+ }
31
+
32
+ export function notDescendantOf(ancestors: Element[]): (e: Element) => boolean {
33
+ const isDescendant = isDescendantOf(ancestors)
34
+ return (e: Element) => !isDescendant(e)
35
+ }
36
+
37
+ export function commonAncestor(first: Fqn, second: Fqn) {
38
+ const parentA = parentFqn(first)
39
+ const parentB = parentFqn(second)
40
+ if (parentA === parentB) {
41
+ return parentA
42
+ }
43
+ if (!parentA || !parentB) {
44
+ return null
45
+ }
46
+
47
+ const a = first.split('.')
48
+ const b = second.split('.')
49
+ let ancestor: Fqn | null = null
50
+
51
+ while (a.length > 1 && b.length > 1 && !!a[0] && a[0] === b[0]) {
52
+ ancestor = (ancestor ? `${ancestor}.${a[0]}` : a[0]) as Fqn
53
+ a.shift()
54
+ b.shift()
55
+ }
56
+ return ancestor
57
+ }
58
+
59
+ export function parentFqn(fqn: Fqn): Fqn | null {
60
+ const lastDot = fqn.lastIndexOf('.')
61
+ if (lastDot > 0) {
62
+ return fqn.substring(0, lastDot) as Fqn
63
+ }
64
+ return null
65
+ }
66
+
67
+ /**
68
+ * Get all ancestor elements (i.e. parent, parent’s parent, etc.)
69
+ * (from closest to root)
70
+ */
71
+ export function ancestorsFqn(fqn: Fqn): Fqn[] {
72
+ const path = fqn.split('.')
73
+ path.pop()
74
+ if (path.length === 0) {
75
+ return []
76
+ }
77
+ return path.reduce((acc, _, idx) => {
78
+ const ancestor = path.slice(0, idx + 1).join('.')
79
+ acc.unshift(ancestor as Fqn)
80
+ return acc
81
+ }, [] as Fqn[])
82
+ }
83
+
84
+ /**
85
+ * Compares two fully qualified names (fqns) hierarchically based on their depth.
86
+ * From parent nodes to leaves
87
+ *
88
+ * @param {string} a - The first fqn to compare.
89
+ * @param {string} b - The second fqn to compare.
90
+ * @returns {number} - 0 if the fqns have the same depth.
91
+ * - Positive number if a is deeper than b.
92
+ * - Negative number if b is deeper than a.
93
+ */
94
+ export function compareFqnHierarchically<T extends string = string>(a: T, b: T): number {
95
+ const depthA = a.split('.').length
96
+ const depthB = b.split('.').length
97
+ switch (true) {
98
+ case depthA > depthB: {
99
+ return 1
100
+ }
101
+ case depthA < depthB: {
102
+ return -1
103
+ }
104
+ default: {
105
+ return 0
106
+ }
107
+ }
108
+ }
109
+
110
+ export function compareByFqnHierarchically<T extends { id: string }>(a: T, b: T) {
111
+ return compareFqnHierarchically(a.id, b.id)
112
+ }
@@ -0,0 +1,10 @@
1
+ import type { NonEmptyArray } from '../types'
2
+ export { hasAtLeast } from 'remeda'
3
+
4
+ export function isString(value: unknown): value is string {
5
+ return value != null && typeof value === 'string'
6
+ }
7
+
8
+ export function isNonEmptyArray<A>(arr: ArrayLike<A> | undefined): arr is NonEmptyArray<A> {
9
+ return !!arr && Array.isArray(arr) && arr.length > 0
10
+ }
@@ -0,0 +1,4 @@
1
+ export * from './fqn'
2
+ export * from './guards'
3
+ export * from './promises'
4
+ export * from './relations'
@@ -0,0 +1,9 @@
1
+ const DELAY = 'LIKEC4_DELAY'
2
+
3
+ export function delay(ms: number = 100) {
4
+ return new Promise(resolve => {
5
+ setTimeout(() => {
6
+ resolve(DELAY)
7
+ }, ms)
8
+ })
9
+ }
@@ -0,0 +1,268 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Fqn, Relation, RelationID } from '../types'
3
+ import { compareRelations, Relations } from './relations'
4
+
5
+ const { isAnyBetween, isAnyInOut, isBetween, isIncoming, isInside, isOutgoing } = Relations
6
+
7
+ const relations = [
8
+ {
9
+ id: 'customer:cloud.frontend.dashboard' as RelationID,
10
+ source: 'customer' as Fqn,
11
+ target: 'cloud.frontend.dashboard' as Fqn,
12
+ title: ''
13
+ },
14
+ {
15
+ id: 'support:cloud.frontend.adminPanel' as RelationID,
16
+ source: 'support' as Fqn,
17
+ target: 'cloud.frontend.adminPanel' as Fqn,
18
+ title: ''
19
+ },
20
+ {
21
+ id: 'cloud.backend.storage:amazon.s3' as RelationID,
22
+ source: 'cloud.backend.storage' as Fqn,
23
+ target: 'amazon.s3' as Fqn,
24
+ title: ''
25
+ },
26
+ {
27
+ id: 'amazon.api:cloud.backend.graphql' as RelationID,
28
+ source: 'amazon.api' as Fqn,
29
+ target: 'cloud.backend.graphql' as Fqn,
30
+ title: ''
31
+ },
32
+ {
33
+ id: 'cloud.backend.graphql:cloud.backend.storage' as RelationID,
34
+ source: 'cloud.backend.graphql' as Fqn,
35
+ target: 'cloud.backend.storage' as Fqn,
36
+ title: ''
37
+ },
38
+ {
39
+ id: 'cloud.frontend.dashboard:cloud.backend.graphql' as RelationID,
40
+ source: 'cloud.frontend.dashboard' as Fqn,
41
+ target: 'cloud.backend.graphql' as Fqn,
42
+ title: ''
43
+ },
44
+ {
45
+ id: 'cloud.frontend.adminPanel:cloud.backend.graphql' as RelationID,
46
+ source: 'cloud.frontend.adminPanel' as Fqn,
47
+ target: 'cloud.backend.graphql' as Fqn,
48
+ title: ''
49
+ }
50
+ ] satisfies Relation[]
51
+
52
+ describe('relation predicates', () => {
53
+ const expectRelations = (predicate: (relation: Relation) => boolean) =>
54
+ expect(relations.filter(predicate).map(r => r.id))
55
+
56
+ const customer = 'customer' as Fqn
57
+ const cloud = 'cloud' as Fqn
58
+ const frontend = 'cloud.frontend' as Fqn
59
+ const backend = 'cloud.backend' as Fqn
60
+
61
+ it('isBetween: cloud.frontend -> cloud.backend', () => {
62
+ expectRelations(isBetween(frontend, backend)).toEqual([
63
+ 'cloud.frontend.dashboard:cloud.backend.graphql',
64
+ 'cloud.frontend.adminPanel:cloud.backend.graphql'
65
+ ])
66
+ })
67
+
68
+ it('isBetween: cloud.frontend.dashboard -> cloud.backend', () => {
69
+ expectRelations(isBetween('cloud.frontend.dashboard' as Fqn, backend)).toEqual([
70
+ 'cloud.frontend.dashboard:cloud.backend.graphql'
71
+ ])
72
+ })
73
+
74
+ it('isBetween: cloud.backend -> cloud.frontend', () => {
75
+ expectRelations(isBetween(backend, frontend)).toEqual([])
76
+ })
77
+
78
+ it('isBetween: cloud.backend -> cloud.backend', () => {
79
+ expectRelations(isBetween(backend, backend)).toEqual([
80
+ 'cloud.backend.graphql:cloud.backend.storage'
81
+ ])
82
+ })
83
+
84
+ it('isBetween: customer -> cloud', () => {
85
+ expectRelations(isBetween(customer, cloud)).toEqual(['customer:cloud.frontend.dashboard'])
86
+ })
87
+
88
+ it('isIncoming: customer', () => {
89
+ expectRelations(isIncoming(customer)).toEqual([])
90
+ })
91
+
92
+ it('isIncoming: cloud', () => {
93
+ expectRelations(isIncoming(cloud)).toEqual([
94
+ 'customer:cloud.frontend.dashboard',
95
+ 'support:cloud.frontend.adminPanel',
96
+ 'amazon.api:cloud.backend.graphql'
97
+ ])
98
+ })
99
+
100
+ it('isIncoming: cloud.frontend', () => {
101
+ expectRelations(isIncoming(frontend)).toEqual([
102
+ 'customer:cloud.frontend.dashboard',
103
+ 'support:cloud.frontend.adminPanel'
104
+ ])
105
+ })
106
+
107
+ it('isIncoming: cloud.frontend.dashboard', () => {
108
+ expectRelations(isIncoming('cloud.frontend.dashboard' as Fqn)).toEqual([
109
+ 'customer:cloud.frontend.dashboard'
110
+ ])
111
+ })
112
+
113
+ it('isOutgoing: cloud', () => {
114
+ expectRelations(isOutgoing(cloud)).toEqual(['cloud.backend.storage:amazon.s3'])
115
+ })
116
+
117
+ it('isOutgoing: cloud.frontend', () => {
118
+ expectRelations(isOutgoing(frontend)).toEqual([
119
+ 'cloud.frontend.dashboard:cloud.backend.graphql',
120
+ 'cloud.frontend.adminPanel:cloud.backend.graphql'
121
+ ])
122
+ })
123
+
124
+ it('isOutgoing: cloud.backend.storage', () => {
125
+ expectRelations(isOutgoing('cloud.backend.storage' as Fqn)).toEqual([
126
+ 'cloud.backend.storage:amazon.s3'
127
+ ])
128
+ })
129
+
130
+ it('isInside: cloud', () => {
131
+ expectRelations(isInside(cloud)).toEqual([
132
+ 'cloud.backend.graphql:cloud.backend.storage',
133
+ 'cloud.frontend.dashboard:cloud.backend.graphql',
134
+ 'cloud.frontend.adminPanel:cloud.backend.graphql'
135
+ ])
136
+ })
137
+
138
+ it('isInside: cloud.frontend', () => {
139
+ expectRelations(isInside(frontend)).toEqual([])
140
+ })
141
+
142
+ it('isInside: cloud.backend', () => {
143
+ expectRelations(isInside(backend)).toEqual(['cloud.backend.graphql:cloud.backend.storage'])
144
+ })
145
+
146
+ it('isAnyInOut: customer', () => {
147
+ expectRelations(isAnyInOut(customer)).toEqual(['customer:cloud.frontend.dashboard'])
148
+ })
149
+
150
+ it('isAnyInOut: cloud', () => {
151
+ expectRelations(isAnyInOut(cloud)).toEqual([
152
+ 'customer:cloud.frontend.dashboard',
153
+ 'support:cloud.frontend.adminPanel',
154
+ 'cloud.backend.storage:amazon.s3',
155
+ 'amazon.api:cloud.backend.graphql'
156
+ ])
157
+ })
158
+
159
+ it('isAnyBetween: cloud and amazon', () => {
160
+ expectRelations(isAnyBetween(cloud, 'amazon' as Fqn)).toEqual([
161
+ 'cloud.backend.storage:amazon.s3',
162
+ 'amazon.api:cloud.backend.graphql'
163
+ ])
164
+ })
165
+ })
166
+
167
+ describe('compareRelations', () => {
168
+ function rel(source: string, target: string) {
169
+ return {
170
+ source,
171
+ target
172
+ }
173
+ }
174
+ function sorted(...relations: Array<{ source: string; target: string }>) {
175
+ return relations.sort(compareRelations).map(r => r.source + ' -> ' + r.target)
176
+ }
177
+
178
+ it('should sort by source and target', () => {
179
+ expect(sorted(rel('customer', 'cloud.ui'), rel('customer', 'cloud'))).toEqual([
180
+ 'customer -> cloud',
181
+ 'customer -> cloud.ui'
182
+ ])
183
+
184
+ expect(
185
+ sorted(
186
+ rel('1', '2.1'),
187
+ rel('1', '2'),
188
+ rel('1.1.1', '2'),
189
+ rel('1.1.1', '2.1'),
190
+ rel('1.1', '2')
191
+ )
192
+ ).toEqual(['1 -> 2', '1 -> 2.1', '1.1 -> 2', '1.1.1 -> 2', '1.1.1 -> 2.1'])
193
+ })
194
+
195
+ it('should sort by parent', () => {
196
+ expect(
197
+ sorted(
198
+ // same parent
199
+ rel('cloud.1.1', 'cloud.2.1'),
200
+ rel('cloud.1.1', 'cloud.2'),
201
+ rel('cloud.1', 'cloud.2.1'),
202
+ rel('cloud.1', 'cloud.2'),
203
+ // no same parent
204
+ rel('cloud.1.1', 'aws.1'),
205
+ rel('cloud.1', 'aws.1'),
206
+ rel('cloud', 'aws')
207
+ )
208
+ ).toEqual([
209
+ 'cloud -> aws',
210
+ 'cloud.1 -> aws.1',
211
+ 'cloud.1.1 -> aws.1',
212
+ 'cloud.1 -> cloud.2',
213
+ 'cloud.1 -> cloud.2.1',
214
+ 'cloud.1.1 -> cloud.2',
215
+ 'cloud.1.1 -> cloud.2.1'
216
+ ])
217
+ })
218
+
219
+ it('should sort by ancestors', () => {
220
+ expect(
221
+ sorted(
222
+ // same parent 2nd level
223
+ rel('cloud.api.1', 'cloud.api.2'),
224
+ rel('cloud.ui.1', 'cloud.ui.2'),
225
+ rel('cloud.ui.2', 'cloud.ui.1'),
226
+ // same ancestor cloud
227
+ rel('cloud.ui.1', 'cloud.api.1'),
228
+ rel('cloud.ui.2', 'cloud.api.2'),
229
+ rel('cloud.ui', 'cloud.api.1'),
230
+ // same parent 1st level
231
+ rel('cloud.ui', 'cloud.api'),
232
+ // no same parent
233
+ rel('cloud.api.1', 'aws.1'),
234
+ rel('cloud.api', 'aws.1'),
235
+ rel('cloud.api', 'aws'),
236
+ rel('cloud.ui.1', 'aws.1'),
237
+ rel('cloud.ui', 'aws'),
238
+ rel('cloud', 'aws')
239
+ )
240
+ ).toEqual([
241
+ 'cloud -> aws',
242
+ 'cloud.api -> aws',
243
+ 'cloud.ui -> aws',
244
+ 'cloud.api -> aws.1',
245
+ 'cloud.api.1 -> aws.1',
246
+ 'cloud.ui.1 -> aws.1',
247
+ 'cloud.ui -> cloud.api',
248
+ 'cloud.ui -> cloud.api.1',
249
+ 'cloud.ui.1 -> cloud.api.1',
250
+ 'cloud.ui.2 -> cloud.api.2',
251
+ 'cloud.api.1 -> cloud.api.2',
252
+ 'cloud.ui.1 -> cloud.ui.2',
253
+ 'cloud.ui.2 -> cloud.ui.1'
254
+ ])
255
+ })
256
+
257
+ it('should sort relations from example', () => {
258
+ expect(sorted(...relations)).toEqual([
259
+ 'customer -> cloud.frontend.dashboard',
260
+ 'support -> cloud.frontend.adminPanel',
261
+ 'amazon.api -> cloud.backend.graphql',
262
+ 'cloud.backend.storage -> amazon.s3',
263
+ 'cloud.frontend.dashboard -> cloud.backend.graphql',
264
+ 'cloud.frontend.adminPanel -> cloud.backend.graphql',
265
+ 'cloud.backend.graphql -> cloud.backend.storage'
266
+ ])
267
+ })
268
+ })
@@ -0,0 +1,104 @@
1
+ import type { Fqn } from '../types'
2
+ import { commonAncestor, compareFqnHierarchically, isAncestor } from './fqn'
3
+
4
+ type Relation = {
5
+ source: string
6
+ target: string
7
+ }
8
+
9
+ type RelationPredicate = (rel: Relation) => boolean
10
+
11
+ /**
12
+ * Compares two relations hierarchically.
13
+ * From the most general (implicit) to the most specific.
14
+ */
15
+ export const compareRelations = <T extends { source: string; target: string }>(a: T, b: T) => {
16
+ const parentA = commonAncestor(a.source as Fqn, a.target as Fqn)
17
+ const parentB = commonAncestor(b.source as Fqn, b.target as Fqn)
18
+ if (parentA && !parentB) {
19
+ return 1
20
+ }
21
+ if (!parentA && parentB) {
22
+ return -1
23
+ }
24
+ const compareParents = parentA && parentB ? compareFqnHierarchically(parentA, parentB) : 0
25
+ if (compareParents === 0) {
26
+ const compareSource = compareFqnHierarchically(a.source, b.source)
27
+ if (compareSource !== 0) {
28
+ return compareSource
29
+ }
30
+ return compareFqnHierarchically(a.target, b.target)
31
+ }
32
+ return compareParents
33
+ }
34
+
35
+ const isInside = (parent: Fqn): RelationPredicate => {
36
+ const prefix = parent + '.'
37
+ return (rel: Relation) => {
38
+ return rel.source.startsWith(prefix) && rel.target.startsWith(prefix)
39
+ }
40
+ }
41
+
42
+ const isBetween = (source: Fqn, target: Fqn): RelationPredicate => {
43
+ const sourcePrefix = source + '.'
44
+ const targetPrefix = target + '.'
45
+ return (rel: Relation) => {
46
+ return (
47
+ (rel.source === source || (rel.source + '.').startsWith(sourcePrefix))
48
+ && (rel.target === target || (rel.target + '.').startsWith(targetPrefix))
49
+ )
50
+ }
51
+ }
52
+
53
+ const isAnyBetween = (source: Fqn, target: Fqn): RelationPredicate => {
54
+ const predicates = [isBetween(source, target), isBetween(target, source)]
55
+ return (rel) => predicates.some(p => p(rel))
56
+ }
57
+
58
+ const isIncoming = (target: Fqn): RelationPredicate => {
59
+ const targetPrefix = target + '.'
60
+ return (rel: Relation) => {
61
+ return (
62
+ !(rel.source + '.').startsWith(targetPrefix)
63
+ && (rel.target === target || (rel.target + '.').startsWith(targetPrefix))
64
+ )
65
+ }
66
+ }
67
+
68
+ const isOutgoing = (source: Fqn): RelationPredicate => {
69
+ const sourcePrefix = source + '.'
70
+ return (rel: Relation) => {
71
+ return (
72
+ (rel.source === source || (rel.source + '.').startsWith(sourcePrefix))
73
+ && !(rel.target + '.').startsWith(sourcePrefix)
74
+ )
75
+ }
76
+ }
77
+
78
+ const isAnyInOut = (source: Fqn): RelationPredicate => {
79
+ const predicates = [isIncoming(source), isOutgoing(source)]
80
+ return (rel: Relation) => {
81
+ return predicates.some(p => p(rel))
82
+ }
83
+ }
84
+
85
+ const hasRelation = <R extends { source: Fqn; target: Fqn }>(rel: R) => {
86
+ return <E extends { id: Fqn }>(element: E) => {
87
+ return (
88
+ rel.source === element.id
89
+ || rel.target === element.id
90
+ || isAncestor(element.id, rel.source)
91
+ || isAncestor(element.id, rel.target)
92
+ )
93
+ }
94
+ }
95
+
96
+ export const Relations = {
97
+ isInside,
98
+ isBetween,
99
+ isAnyBetween,
100
+ isIncoming,
101
+ isOutgoing,
102
+ isAnyInOut,
103
+ hasRelation
104
+ } as const
@@ -1,52 +0,0 @@
1
- import type { IconUrl, NonEmptyArray } from './_common';
2
- import type { ElementKind, ElementShape, ElementStyle, Fqn, Tag } from './element';
3
- import type { Opaque } from './opaque';
4
- import type { RelationID, RelationshipArrowType, RelationshipLineType } from './relation';
5
- import type { ThemeColor } from './theme';
6
- import type { BasicElementView, ViewID, ViewRuleAutoLayout } from './view';
7
- export type NodeId = Fqn;
8
- export type EdgeId = Opaque<string, 'EdgeId'>;
9
- export interface ComputedNode {
10
- id: NodeId;
11
- kind: ElementKind;
12
- parent: NodeId | null;
13
- title: string;
14
- description: string | null;
15
- technology: string | null;
16
- tags: NonEmptyArray<Tag> | null;
17
- links: NonEmptyArray<string> | null;
18
- children: NodeId[];
19
- inEdges: EdgeId[];
20
- outEdges: EdgeId[];
21
- shape: ElementShape;
22
- /**
23
- * @deprecated Use `style` instead
24
- */
25
- color: ThemeColor;
26
- /**
27
- * @deprecated Use `style` instead
28
- */
29
- icon?: IconUrl;
30
- style: ElementStyle;
31
- navigateTo?: ViewID;
32
- level: number;
33
- depth?: number;
34
- }
35
- export interface ComputedEdge {
36
- id: EdgeId;
37
- parent: NodeId | null;
38
- source: NodeId;
39
- target: NodeId;
40
- label: string | null;
41
- relations: RelationID[];
42
- color?: ThemeColor;
43
- line?: RelationshipLineType;
44
- head?: RelationshipArrowType;
45
- tail?: RelationshipArrowType;
46
- }
47
- export interface ComputedView extends Omit<BasicElementView, 'rules'> {
48
- readonly extends?: ViewID;
49
- readonly autoLayout: ViewRuleAutoLayout['autoLayout'];
50
- readonly nodes: ComputedNode[];
51
- readonly edges: ComputedEdge[];
52
- }
@@ -1 +0,0 @@
1
- export {};