@likec4/core 1.8.1 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/index.cjs +4467 -0
  2. package/dist/index.d.cts +793 -0
  3. package/dist/index.d.mts +793 -0
  4. package/dist/index.d.ts +793 -7
  5. package/dist/index.mjs +4390 -0
  6. package/dist/shared/core.CpR2fq5B.d.cts +833 -0
  7. package/dist/shared/core.CpR2fq5B.d.mts +833 -0
  8. package/dist/shared/core.CpR2fq5B.d.ts +833 -0
  9. package/dist/shared/core.Db3ntVII.cjs +318 -0
  10. package/dist/shared/core.DeifT5lC.mjs +263 -0
  11. package/dist/types/index.cjs +55 -0
  12. package/dist/types/index.d.cts +1 -0
  13. package/dist/types/index.d.mts +1 -0
  14. package/dist/types/index.d.ts +1 -14
  15. package/dist/types/index.mjs +1 -0
  16. package/package.json +43 -33
  17. package/src/colors/index.ts +27 -1
  18. package/src/errors/index.ts +3 -95
  19. package/src/index.ts +6 -6
  20. package/src/model/LikeC4Model.ts +480 -0
  21. package/src/model/LikeC4ViewModel.ts +318 -0
  22. package/src/model/__test__/LikeC4Model.spec.ts +79 -0
  23. package/src/model/__test__/fixture.ts +333 -0
  24. package/src/model/index.ts +2 -0
  25. package/src/model/types.ts +10 -0
  26. package/src/types/_common.ts +4 -2
  27. package/src/types/element.ts +11 -8
  28. package/src/types/expression.ts +7 -3
  29. package/src/types/index.ts +0 -1
  30. package/src/types/model.ts +16 -6
  31. package/src/types/relation.ts +18 -5
  32. package/src/types/theme.ts +28 -12
  33. package/src/types/view-notation.ts +3 -4
  34. package/src/types/view.ts +51 -16
  35. package/src/utils/compare-natural.spec.ts +37 -0
  36. package/src/utils/compare-natural.ts +14 -0
  37. package/src/utils/fqn.spec.ts +99 -2
  38. package/src/utils/fqn.ts +45 -5
  39. package/src/utils/index.ts +1 -0
  40. package/src/utils/relations.spec.ts +2 -4
  41. package/src/utils/relations.ts +14 -24
  42. package/dist/colors/element.d.ts +0 -69
  43. package/dist/colors/element.js +0 -75
  44. package/dist/colors/index.d.ts +0 -133
  45. package/dist/colors/index.js +0 -9
  46. package/dist/colors/relationships.d.ts +0 -58
  47. package/dist/colors/relationships.js +0 -49
  48. package/dist/errors/errors.spec.d.ts +0 -2
  49. package/dist/errors/errors.spec.js +0 -69
  50. package/dist/errors/index.d.ts +0 -39
  51. package/dist/errors/index.js +0 -104
  52. package/dist/index.js +0 -4
  53. package/dist/types/_common.d.ts +0 -9
  54. package/dist/types/_common.js +0 -1
  55. package/dist/types/element.d.ts +0 -55
  56. package/dist/types/element.js +0 -15
  57. package/dist/types/expression.d.ts +0 -119
  58. package/dist/types/expression.js +0 -67
  59. package/dist/types/index.js +0 -14
  60. package/dist/types/model.d.ts +0 -14
  61. package/dist/types/model.js +0 -1
  62. package/dist/types/opaque.d.ts +0 -103
  63. package/dist/types/opaque.js +0 -1
  64. package/dist/types/operators.d.ts +0 -44
  65. package/dist/types/operators.js +0 -59
  66. package/dist/types/overview-graph.d.ts +0 -41
  67. package/dist/types/overview-graph.js +0 -1
  68. package/dist/types/relation.d.ts +0 -30
  69. package/dist/types/relation.js +0 -3
  70. package/dist/types/theme.d.ts +0 -27
  71. package/dist/types/theme.js +0 -1
  72. package/dist/types/view-changes.d.ts +0 -26
  73. package/dist/types/view-changes.js +0 -1
  74. package/dist/types/view-notation.d.ts +0 -9
  75. package/dist/types/view-notation.js +0 -1
  76. package/dist/types/view.d.ts +0 -243
  77. package/dist/types/view.js +0 -50
  78. package/dist/utils/fqn.d.ts +0 -39
  79. package/dist/utils/fqn.js +0 -102
  80. package/dist/utils/fqn.spec.d.ts +0 -2
  81. package/dist/utils/fqn.spec.js +0 -82
  82. package/dist/utils/guards.d.ts +0 -5
  83. package/dist/utils/guards.js +0 -7
  84. package/dist/utils/index.d.ts +0 -5
  85. package/dist/utils/index.js +0 -4
  86. package/dist/utils/promises.d.ts +0 -2
  87. package/dist/utils/promises.js +0 -8
  88. package/dist/utils/relations.d.ts +0 -30
  89. package/dist/utils/relations.js +0 -79
  90. package/dist/utils/relations.spec.d.ts +0 -2
  91. package/dist/utils/relations.spec.js +0 -210
  92. package/src/errors/errors.spec.ts +0 -88
  93. package/src/reset.d.ts +0 -2
  94. package/src/types/opaque.ts +0 -108
@@ -0,0 +1,318 @@
1
+ import { isNullish, isString } from 'remeda'
2
+ import { nonNullable } from '../errors'
3
+ import type { ElementShape, Tag } from '../types/element'
4
+ import type { Color } from '../types/theme'
5
+ import type { ComputedEdge, ComputedNode, ComputedView } from '../types/view'
6
+ import type { LikeC4Model } from './LikeC4Model'
7
+ import type { EdgeId, Fqn } from './types'
8
+
9
+ type ElementOrFqn = Fqn | LikeC4ViewModel.Element
10
+
11
+ /**
12
+ * All methods are view-scoped, i.e. only return elements and connections in the view.
13
+ */
14
+ export class LikeC4ViewModel {
15
+ #rootElements = new Set<LikeC4ViewModel.Element>()
16
+
17
+ #elements = new Map<Fqn, LikeC4ViewModel.Element>()
18
+
19
+ #connections: Map<EdgeId, LikeC4ViewModel.Connection>
20
+
21
+ constructor(
22
+ public readonly view: ComputedView,
23
+ public readonly model: LikeC4Model
24
+ ) {
25
+ for (const node of view.nodes) {
26
+ const el = new LikeC4ViewModel.Element(node, this)
27
+ this.#elements.set(node.id, el)
28
+ if (isNullish(node.parent)) {
29
+ this.#rootElements.add(el)
30
+ }
31
+ }
32
+ this.#connections = new Map(view.edges.map(e => [e.id, new LikeC4ViewModel.Connection(e, this)]))
33
+ }
34
+
35
+ get id() {
36
+ return this.view.id
37
+ }
38
+
39
+ get title() {
40
+ return this.view.title
41
+ }
42
+
43
+ get viewOf() {
44
+ if (this.view.__ !== 'dynamic') {
45
+ return this.view.viewOf ? this.model.element(this.view.viewOf) : null
46
+ }
47
+ return null
48
+ }
49
+
50
+ get tags(): Tag[] {
51
+ return this.view.tags ?? []
52
+ }
53
+
54
+ public roots(): ReadonlyArray<LikeC4ViewModel.Element> {
55
+ return [...this.#rootElements]
56
+ }
57
+
58
+ public elements(): ReadonlyArray<LikeC4ViewModel.Element> {
59
+ return [...this.#elements.values()]
60
+ }
61
+
62
+ public element(id: Fqn): LikeC4ViewModel.Element {
63
+ return nonNullable(this.#elements.get(id), `LikeC4ViewModel.Element ${id} in view ${this.view.id} not found`)
64
+ }
65
+
66
+ public hasElement(id: Fqn): boolean {
67
+ return this.#elements.has(id)
68
+ }
69
+
70
+ public connections(): ReadonlyArray<LikeC4ViewModel.Connection> {
71
+ return [...this.#connections.values()]
72
+ }
73
+
74
+ public connection(id: EdgeId): LikeC4ViewModel.Connection {
75
+ return nonNullable(this.#connections.get(id), `Connection ${id} in view ${this.view.id} not found`)
76
+ }
77
+
78
+ public findConnections(
79
+ source: ElementOrFqn,
80
+ target: ElementOrFqn,
81
+ direction: 'both' | 'direct' = 'direct'
82
+ ): ReadonlyArray<LikeC4ViewModel.Connection> {
83
+ const sourceId = isString(source) ? source : source.id
84
+ const targetId = isString(target) ? target : target.id
85
+ return this.connections().filter(c =>
86
+ (c.source.id === sourceId && c.target.id === targetId)
87
+ || (direction === 'both' && c.source.id === targetId && c.target.id === sourceId)
88
+ )
89
+ }
90
+
91
+ public parent(element: ElementOrFqn): LikeC4ViewModel.Element | null {
92
+ const el = this.element(isString(element) ? element : element.id)
93
+ return el.node.parent ? this.element(el.node.parent) : null
94
+ }
95
+
96
+ public children(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
97
+ const el = this.element(isString(element) ? element : element.id)
98
+ return el.node.children.map(c => this.element(c))
99
+ }
100
+
101
+ // Get all sibling (i.e. same parent)
102
+ public siblings(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
103
+ const id = isString(element) ? element : element.id
104
+ const parent = this.parent(id)
105
+ const siblings = parent ? this.children(parent) : this.roots()
106
+ return siblings.filter(e => e.id !== id)
107
+ }
108
+
109
+ /**
110
+ * Get all ancestor elements (i.e. parent, parent’s parent, etc.)
111
+ * (from closest to root)
112
+ */
113
+ public ancestors(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
114
+ let id = isString(element) ? element : element.id
115
+ const result = [] as LikeC4ViewModel.Element[]
116
+ let parent
117
+ while (parent = this.parent(id)) {
118
+ result.push(parent)
119
+ id = parent.id
120
+ }
121
+ return result
122
+ }
123
+
124
+ public descendants(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
125
+ const children = this.children(element)
126
+ return children.flatMap(c => [c, ...this.descendants(c.id)])
127
+ }
128
+
129
+ public incoming(
130
+ element: ElementOrFqn,
131
+ filter: 'all' | 'direct' | 'to-descendants' = 'all'
132
+ ): ReadonlyArray<LikeC4ViewModel.Connection> {
133
+ const el = this.element(isString(element) ? element : element.id)
134
+ const edges = el.node.inEdges.map(e => nonNullable(this.#connections.get(e), `Edge ${e} not found`))
135
+
136
+ if (filter === 'all' || edges.length === 0) {
137
+ return edges
138
+ }
139
+
140
+ return edges.filter(rel => {
141
+ if (filter === 'direct') {
142
+ return rel.target.id === el.id
143
+ }
144
+ return rel.target.id !== el.id
145
+ })
146
+ }
147
+
148
+ public incomers(
149
+ element: ElementOrFqn,
150
+ filter: 'all' | 'direct' | 'to-descendants' = 'all'
151
+ ): ReadonlyArray<LikeC4ViewModel.Element> {
152
+ return this.incoming(element, filter).map(r => r.source)
153
+ }
154
+
155
+ /**
156
+ * Outgoing relationships from the element and its descendants
157
+ */
158
+ public outgoing(
159
+ element: ElementOrFqn,
160
+ filter: 'all' | 'direct' | 'from-descendants' = 'all'
161
+ ): ReadonlyArray<LikeC4ViewModel.Connection> {
162
+ const el = this.element(isString(element) ? element : element.id)
163
+ const edges = el.node.outEdges.map(e => nonNullable(this.#connections.get(e), `Edge ${e} not found`))
164
+
165
+ if (filter === 'all' || edges.length === 0) {
166
+ return edges
167
+ }
168
+
169
+ return edges.filter(rel => {
170
+ if (filter === 'direct') {
171
+ return rel.source.id === el.id
172
+ }
173
+ return rel.source.id !== el.id
174
+ })
175
+ }
176
+
177
+ public outgoers(
178
+ element: ElementOrFqn,
179
+ filter: 'all' | 'direct' | 'from-descendants' = 'all'
180
+ ): ReadonlyArray<LikeC4ViewModel.Element> {
181
+ return this.outgoing(element, filter).map(r => r.target)
182
+ }
183
+ }
184
+
185
+ export namespace LikeC4ViewModel {
186
+ /**
187
+ * Represents an element in the view. (Diagram node)
188
+ * All methods are view-scoped, i.e. `children` returns only children of the element in the view.
189
+ */
190
+ export class Element {
191
+ constructor(
192
+ public readonly node: ComputedNode,
193
+ private viewmodel: LikeC4ViewModel
194
+ ) {
195
+ }
196
+
197
+ get id() {
198
+ return this.node.id
199
+ }
200
+
201
+ get title() {
202
+ return this.node.title
203
+ }
204
+
205
+ get kind() {
206
+ return this.node.kind
207
+ }
208
+
209
+ get isRoot(): boolean {
210
+ return isNullish(this.node.parent)
211
+ }
212
+
213
+ get hasNested(): boolean {
214
+ return this.node.children.length > 0
215
+ }
216
+
217
+ get shape(): ElementShape {
218
+ return this.node.shape
219
+ }
220
+
221
+ get color(): Color {
222
+ return this.node.color
223
+ }
224
+
225
+ get tags(): Tag[] {
226
+ return this.node.tags ?? []
227
+ }
228
+
229
+ public model(): LikeC4Model.Element {
230
+ return this.viewmodel.model.element(this.id)
231
+ }
232
+
233
+ public parent(): LikeC4ViewModel.Element | null {
234
+ return this.node.parent ? this.viewmodel.element(this.node.parent) : null
235
+ }
236
+
237
+ public metadata(key: string): string | undefined
238
+ public metadata(key: string, defaultValue: string): string
239
+ public metadata(key: string, defaultValue?: string): string | undefined {
240
+ return this.model().metadata(key) ?? defaultValue
241
+ }
242
+
243
+ public hasMetadata(key: string): boolean {
244
+ return this.model().hasMetadata(key)
245
+ }
246
+
247
+ public ancestors(): ReadonlyArray<LikeC4ViewModel.Element> {
248
+ return this.viewmodel.ancestors(this)
249
+ }
250
+
251
+ public siblings(): ReadonlyArray<LikeC4ViewModel.Element> {
252
+ return this.viewmodel.siblings(this)
253
+ }
254
+
255
+ public descendants(): ReadonlyArray<LikeC4ViewModel.Element> {
256
+ return this.viewmodel.descendants(this)
257
+ }
258
+
259
+ public children(): ReadonlyArray<LikeC4ViewModel.Element> {
260
+ return this.viewmodel.children(this)
261
+ }
262
+
263
+ public incoming(filter: 'all' | 'direct' | 'to-descendants' = 'all'): ReadonlyArray<LikeC4ViewModel.Connection> {
264
+ return this.viewmodel.incoming(this, filter)
265
+ }
266
+
267
+ public incomers(filter: 'all' | 'direct' | 'to-descendants' = 'all'): ReadonlyArray<LikeC4ViewModel.Element> {
268
+ return this.viewmodel.incomers(this, filter)
269
+ }
270
+
271
+ public outgoing(filter: 'all' | 'direct' | 'from-descendants' = 'all'): ReadonlyArray<LikeC4ViewModel.Connection> {
272
+ return this.viewmodel.outgoing(this, filter)
273
+ }
274
+
275
+ public outgoers(filter: 'all' | 'direct' | 'from-descendants' = 'all'): ReadonlyArray<LikeC4ViewModel.Element> {
276
+ return this.viewmodel.outgoers(this, filter)
277
+ }
278
+
279
+ public connectionsTo(target: Fqn | LikeC4ViewModel.Element): ReadonlyArray<LikeC4ViewModel.Connection> {
280
+ return this.viewmodel.findConnections(this, target)
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Represents a connection between two elements.
286
+ * May be source from multiple model relationships.
287
+ */
288
+ export class Connection {
289
+ constructor(
290
+ public readonly edge: ComputedEdge,
291
+ private viewmodel: LikeC4ViewModel
292
+ ) {
293
+ }
294
+
295
+ get id() {
296
+ return this.edge.id
297
+ }
298
+
299
+ get source(): LikeC4ViewModel.Element {
300
+ return this.viewmodel.element(this.edge.source)
301
+ }
302
+
303
+ get target(): LikeC4ViewModel.Element {
304
+ return this.viewmodel.element(this.edge.target)
305
+ }
306
+
307
+ get tags(): Tag[] {
308
+ return this.edge.tags ?? []
309
+ }
310
+
311
+ /**
312
+ * Model relationships
313
+ */
314
+ relationships(): ReadonlyArray<LikeC4Model.Relationship> {
315
+ return this.edge.relations.map(r => nonNullable(this.viewmodel.model.relationship(r)))
316
+ }
317
+ }
318
+ }
@@ -0,0 +1,79 @@
1
+ import { prop } from 'remeda'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { LikeC4Model } from '../LikeC4Model'
4
+ import { fakeComputedModel, fakeElements } from './fixture'
5
+
6
+ describe('LikeC4Model', () => {
7
+ const model = LikeC4Model.from(fakeComputedModel)
8
+
9
+ it('roots', () => {
10
+ expect(model.roots().map(prop('id'))).toEqual([
11
+ 'customer',
12
+ 'support',
13
+ 'cloud',
14
+ 'email',
15
+ 'amazon'
16
+ ])
17
+ })
18
+
19
+ it('parent and chilren', () => {
20
+ const parent = model.parent('cloud.backend.graphql')!
21
+ expect(parent.id).toEqual('cloud.backend')
22
+ expect(parent.element).toStrictEqual(fakeElements['cloud.backend'])
23
+
24
+ const children = parent.children()
25
+
26
+ expect(children.map(prop('id'))).toEqual(['cloud.backend.graphql', 'cloud.backend.storage'])
27
+ })
28
+
29
+ it('ancestors in right order', () => {
30
+ const ancestors = model.element('cloud.frontend.dashboard').ancestors()
31
+ expect(ancestors).toHaveLength(2)
32
+ expect(ancestors[0]).toMatchObject({
33
+ id: 'cloud.frontend',
34
+ element: fakeElements['cloud.frontend']
35
+ })
36
+ expect(ancestors[1]).toMatchObject({
37
+ id: 'cloud',
38
+ element: fakeElements['cloud']
39
+ })
40
+ })
41
+
42
+ it('siblings of root', () => {
43
+ const siblings = model.element('cloud').siblings()
44
+ expect(siblings.map(prop('id'))).toEqual([
45
+ 'customer',
46
+ 'support',
47
+ 'email',
48
+ 'amazon'
49
+ ])
50
+ })
51
+
52
+ it('siblings', () => {
53
+ const backend = model.element('cloud.backend')
54
+
55
+ const siblings = backend.siblings()
56
+
57
+ expect(siblings.map(prop('id'))).toEqual(['cloud.frontend'])
58
+ })
59
+
60
+ it('descendants in right order', () => {
61
+ const descendants = model.element('cloud').descendants().map(prop('id'))
62
+ expect(descendants).toEqual([
63
+ 'cloud.backend',
64
+ 'cloud.backend.graphql',
65
+ 'cloud.backend.storage',
66
+ 'cloud.frontend',
67
+ 'cloud.frontend.adminPanel',
68
+ 'cloud.frontend.dashboard'
69
+ ])
70
+ })
71
+
72
+ it('internal relations', () => {
73
+ expect(
74
+ model.internal('cloud.backend').map(prop('id'))
75
+ ).toEqual([
76
+ 'cloud.backend.graphql:cloud.backend.storage'
77
+ ])
78
+ })
79
+ })