@likec4/core 1.2.0 → 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 (51) 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 -0
  13. package/dist/types/index.js +2 -1
  14. package/dist/types/model.d.ts +1 -0
  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 +1 -0
  19. package/dist/utils/fqn.d.ts +1 -0
  20. package/dist/utils/fqn.spec.d.ts +2 -0
  21. package/dist/utils/fqn.spec.js +82 -0
  22. package/dist/utils/guards.d.ts +1 -0
  23. package/dist/utils/index.d.ts +1 -0
  24. package/dist/utils/promises.d.ts +1 -0
  25. package/dist/utils/relations.d.ts +1 -0
  26. package/dist/utils/relations.spec.d.ts +2 -0
  27. package/dist/utils/relations.spec.js +210 -0
  28. package/package.json +6 -6
  29. package/src/colors/element.ts +80 -0
  30. package/src/colors/index.ts +12 -0
  31. package/src/colors/relationships.ts +54 -0
  32. package/src/errors/errors.spec.ts +88 -0
  33. package/src/errors/index.ts +120 -0
  34. package/src/index.ts +9 -0
  35. package/src/reset.d.ts +2 -0
  36. package/src/types/_common.ts +5 -0
  37. package/src/types/element.ts +56 -0
  38. package/src/types/expression.ts +140 -0
  39. package/src/types/index.ts +10 -0
  40. package/src/types/model.ts +15 -0
  41. package/src/types/opaque.ts +108 -0
  42. package/src/types/relation.ts +38 -0
  43. package/src/types/theme.ts +46 -0
  44. package/src/types/view.ts +263 -0
  45. package/src/utils/fqn.spec.ts +105 -0
  46. package/src/utils/fqn.ts +112 -0
  47. package/src/utils/guards.ts +10 -0
  48. package/src/utils/index.ts +4 -0
  49. package/src/utils/promises.ts +9 -0
  50. package/src/utils/relations.spec.ts +268 -0
  51. package/src/utils/relations.ts +104 -0
@@ -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