@likec4/core 1.8.0 → 1.9.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 (89) hide show
  1. package/dist/index.cjs +4368 -0
  2. package/dist/index.d.cts +781 -0
  3. package/dist/index.d.mts +781 -0
  4. package/dist/index.mjs +4296 -0
  5. package/dist/shared/core.9l7eW3pn.d.cts +817 -0
  6. package/dist/shared/core.9l7eW3pn.d.mts +817 -0
  7. package/dist/shared/core.DcPLm41i.cjs +304 -0
  8. package/dist/shared/core.DfUTsojZ.mjs +251 -0
  9. package/dist/types/index.cjs +53 -0
  10. package/dist/types/index.d.cts +1 -0
  11. package/dist/types/index.d.mts +1 -0
  12. package/dist/types/index.mjs +1 -0
  13. package/package.json +41 -33
  14. package/src/colors/index.ts +27 -1
  15. package/src/errors/index.ts +3 -95
  16. package/src/index.ts +5 -6
  17. package/src/model/LikeC4Model.ts +480 -0
  18. package/src/model/LikeC4ViewModel.ts +318 -0
  19. package/src/model/__test__/LikeC4Model.spec.ts +79 -0
  20. package/src/model/__test__/fixture.ts +333 -0
  21. package/src/model/index.ts +2 -0
  22. package/src/model/types.ts +10 -0
  23. package/src/types/_common.ts +4 -2
  24. package/src/types/element.ts +8 -8
  25. package/src/types/expression.ts +3 -3
  26. package/src/types/index.ts +0 -1
  27. package/src/types/model.ts +16 -6
  28. package/src/types/relation.ts +15 -5
  29. package/src/types/theme.ts +28 -12
  30. package/src/types/view-notation.ts +3 -4
  31. package/src/types/view.ts +14 -10
  32. package/src/utils/fqn.ts +7 -4
  33. package/src/utils/relations.spec.ts +1 -3
  34. package/src/utils/relations.ts +7 -17
  35. package/dist/colors/element.d.ts +0 -69
  36. package/dist/colors/element.js +0 -75
  37. package/dist/colors/index.d.ts +0 -133
  38. package/dist/colors/index.js +0 -9
  39. package/dist/colors/relationships.d.ts +0 -58
  40. package/dist/colors/relationships.js +0 -49
  41. package/dist/errors/errors.spec.d.ts +0 -2
  42. package/dist/errors/errors.spec.js +0 -69
  43. package/dist/errors/index.d.ts +0 -39
  44. package/dist/errors/index.js +0 -104
  45. package/dist/index.d.ts +0 -7
  46. package/dist/index.js +0 -4
  47. package/dist/types/_common.d.ts +0 -9
  48. package/dist/types/_common.js +0 -1
  49. package/dist/types/element.d.ts +0 -55
  50. package/dist/types/element.js +0 -15
  51. package/dist/types/expression.d.ts +0 -119
  52. package/dist/types/expression.js +0 -67
  53. package/dist/types/index.d.ts +0 -14
  54. package/dist/types/index.js +0 -14
  55. package/dist/types/model.d.ts +0 -14
  56. package/dist/types/model.js +0 -1
  57. package/dist/types/opaque.d.ts +0 -103
  58. package/dist/types/opaque.js +0 -1
  59. package/dist/types/operators.d.ts +0 -44
  60. package/dist/types/operators.js +0 -59
  61. package/dist/types/overview-graph.d.ts +0 -41
  62. package/dist/types/overview-graph.js +0 -1
  63. package/dist/types/relation.d.ts +0 -30
  64. package/dist/types/relation.js +0 -3
  65. package/dist/types/theme.d.ts +0 -27
  66. package/dist/types/theme.js +0 -1
  67. package/dist/types/view-changes.d.ts +0 -26
  68. package/dist/types/view-changes.js +0 -1
  69. package/dist/types/view-notation.d.ts +0 -9
  70. package/dist/types/view-notation.js +0 -1
  71. package/dist/types/view.d.ts +0 -243
  72. package/dist/types/view.js +0 -50
  73. package/dist/utils/fqn.d.ts +0 -39
  74. package/dist/utils/fqn.js +0 -102
  75. package/dist/utils/fqn.spec.d.ts +0 -2
  76. package/dist/utils/fqn.spec.js +0 -82
  77. package/dist/utils/guards.d.ts +0 -5
  78. package/dist/utils/guards.js +0 -7
  79. package/dist/utils/index.d.ts +0 -5
  80. package/dist/utils/index.js +0 -4
  81. package/dist/utils/promises.d.ts +0 -2
  82. package/dist/utils/promises.js +0 -8
  83. package/dist/utils/relations.d.ts +0 -30
  84. package/dist/utils/relations.js +0 -79
  85. package/dist/utils/relations.spec.d.ts +0 -2
  86. package/dist/utils/relations.spec.js +0 -210
  87. package/src/errors/errors.spec.ts +0 -88
  88. package/src/reset.d.ts +0 -2
  89. package/src/types/opaque.ts +0 -108
@@ -0,0 +1,480 @@
1
+ import { isString, isTruthy, values } from 'remeda'
2
+ import { nonNullable } from '../errors'
3
+ import type * as c4 from '../types'
4
+ import { DefaultElementShape, DefaultThemeColor } from '../types/element'
5
+ import { ancestorsFqn, commonAncestor, parentFqn } from '../utils/fqn'
6
+ import { LikeC4ViewModel } from './LikeC4ViewModel'
7
+ import type { Fqn, RelationID, ViewID } from './types'
8
+
9
+ const RelationsSet = Set<LikeC4Model.Relationship>
10
+ const MapRelations = Map<Fqn, Set<LikeC4Model.Relationship>>
11
+
12
+ type ElementOrFqn = Fqn | LikeC4Model.Element
13
+
14
+ export class LikeC4Model {
15
+ #elements = new Map<Fqn, LikeC4Model.Element>()
16
+
17
+ // Parent element for given FQN
18
+ #parents = new Map<Fqn, LikeC4Model.Element>()
19
+
20
+ // Children elements for given FQN
21
+ #children = new Map<Fqn, LikeC4Model.Element[]>()
22
+
23
+ #rootElements = new Set<LikeC4Model.Element>()
24
+
25
+ #relations = new Map<RelationID, LikeC4Model.Relationship>()
26
+
27
+ // Incoming to an element or its descendants
28
+ #incoming = new MapRelations()
29
+
30
+ // Outgoing from an element or its descendants
31
+ #outgoing = new MapRelations()
32
+
33
+ // Relationships inside the element, among descendants
34
+ #internal = new MapRelations()
35
+
36
+ #cacheAscendingSiblings = new Map<Fqn, LikeC4Model.Element[]>()
37
+
38
+ #views: Map<ViewID, LikeC4ViewModel>
39
+
40
+ static from(computed: c4.ComputedLikeC4Model): LikeC4Model {
41
+ return new LikeC4Model(computed)
42
+ }
43
+
44
+ protected constructor(protected computed: c4.ComputedLikeC4Model) {
45
+ for (const el of values(computed.elements)) {
46
+ this.addElement(el)
47
+ }
48
+ for (const rel of values(computed.relations)) {
49
+ this.addRelation(rel)
50
+ }
51
+ this.#views = new Map(
52
+ values(computed.views).map(v => [v.id, new LikeC4ViewModel(v, this)])
53
+ )
54
+ }
55
+
56
+ /**
57
+ * Returns the root elements of the model.
58
+ */
59
+ public roots(): ReadonlyArray<LikeC4Model.Element> {
60
+ return [...this.#rootElements]
61
+ }
62
+
63
+ /**
64
+ * Returns all elements in the model.
65
+ */
66
+ public elements(): ReadonlyArray<LikeC4Model.Element> {
67
+ return [...this.#elements.values()]
68
+ }
69
+
70
+ /**
71
+ * Returns a specific element by its FQN.
72
+ */
73
+ public element(id: Fqn): LikeC4Model.Element {
74
+ return nonNullable(this.#elements.get(id), `Element ${id} not found`)
75
+ }
76
+
77
+ /**
78
+ * Returns all relationships in the model.
79
+ */
80
+ public relationships(): ReadonlyArray<LikeC4Model.Relationship> {
81
+ return [...this.#relations.values()]
82
+ }
83
+
84
+ /**
85
+ * Returns a specific relationship by its ID.
86
+ */
87
+ public relationship(id: RelationID): LikeC4Model.Relationship {
88
+ return nonNullable(this.#relations.get(id), `Relation ${id} not found`)
89
+ }
90
+
91
+ /**
92
+ * Returns all views in the model.
93
+ */
94
+ public views(): ReadonlyArray<LikeC4ViewModel> {
95
+ return [...this.#views.values()]
96
+ }
97
+
98
+ /**
99
+ * Returns a specific view by its ID.
100
+ */
101
+ public view(viewId: ViewID): LikeC4ViewModel {
102
+ return nonNullable(this.#views.get(viewId), `View ${viewId} not found`)
103
+ }
104
+
105
+ /**
106
+ * Returns the parent element of given element.
107
+ * @see ancestors
108
+ */
109
+ public parent(element: ElementOrFqn): LikeC4Model.Element | null {
110
+ const id = isString(element) ? element : element.id
111
+ return this.#parents.get(id) || null
112
+ }
113
+
114
+ /**
115
+ * Get all children of the element (only direct children),
116
+ * @see descendants
117
+ */
118
+ public children(element: ElementOrFqn): ReadonlyArray<LikeC4Model.Element> {
119
+ const id = isString(element) ? element : element.id
120
+ return this._childrenOf(id)
121
+ }
122
+
123
+ /**
124
+ * Get all sibling (i.e. same parent)
125
+ */
126
+ public siblings(element: ElementOrFqn): ReadonlyArray<LikeC4Model.Element> {
127
+ const id = isString(element) ? element : element.id
128
+ const parent = this.#parents.get(id)
129
+ const siblings = parent ? this._childrenOf(parent.id) : this.roots()
130
+ return siblings.filter(e => e.id !== id)
131
+ }
132
+
133
+ /**
134
+ * Get all ancestor elements (i.e. parent, parent’s parent, etc.)
135
+ * (from closest to root)
136
+ */
137
+ public ancestors(element: ElementOrFqn): ReadonlyArray<LikeC4Model.Element> {
138
+ let id = isString(element) ? element : element.id
139
+ const result = [] as LikeC4Model.Element[]
140
+ let parent
141
+ while (parent = this.#parents.get(id)) {
142
+ result.push(parent)
143
+ id = parent.id
144
+ }
145
+ return result
146
+ }
147
+
148
+ /**
149
+ * Get all descendant elements (i.e. children, children’s children, etc.)
150
+ */
151
+ public descendants(element: ElementOrFqn): ReadonlyArray<LikeC4Model.Element> {
152
+ const id = isString(element) ? element : element.id
153
+ const children = this._childrenOf(id)
154
+ return children.flatMap(c => [c, ...this.descendants(c.id)])
155
+ }
156
+
157
+ /**
158
+ * Incoming relationships to the element and its descendants
159
+ * @see incomers
160
+ */
161
+ public incoming(
162
+ element: ElementOrFqn,
163
+ filter: 'all' | 'direct' | 'to-descendants' = 'all'
164
+ ): ReadonlyArray<LikeC4Model.Relationship> {
165
+ const id = isString(element) ? element : element.id
166
+ const incoming = Array.from(this._incomingTo(id))
167
+ if (filter === 'all' || incoming.length === 0) {
168
+ return incoming
169
+ }
170
+
171
+ return incoming.filter(rel => {
172
+ if (filter === 'direct') {
173
+ return rel.target.id === id
174
+ }
175
+ return rel.target.id !== id
176
+ })
177
+ }
178
+
179
+ /**
180
+ * Source elements of incoming relationships
181
+ */
182
+ public incomers(
183
+ element: ElementOrFqn,
184
+ filter: 'all' | 'direct' | 'to-descendants' = 'all'
185
+ ): ReadonlyArray<LikeC4Model.Element> {
186
+ return this.incoming(element, filter).map(r => r.source)
187
+ }
188
+
189
+ /**
190
+ * Outgoing relationships from the element and its descendants
191
+ * @see outgoers
192
+ */
193
+ public outgoing(
194
+ element: ElementOrFqn,
195
+ filter: 'all' | 'direct' | 'from-descendants' = 'all'
196
+ ): ReadonlyArray<LikeC4Model.Relationship> {
197
+ const id = isString(element) ? element : element.id
198
+ const outgoing = Array.from(this._outgoingFrom(id))
199
+ if (filter === 'all' || outgoing.length === 0) {
200
+ return outgoing
201
+ }
202
+ return outgoing.filter(rel => {
203
+ if (filter === 'direct') {
204
+ return rel.source.id === id
205
+ }
206
+ return rel.source.id !== id
207
+ })
208
+ }
209
+
210
+ /**
211
+ * Target elements of outgoing relationships
212
+ */
213
+ public outgoers(
214
+ element: ElementOrFqn,
215
+ filter: 'all' | 'direct' | 'from-descendants' = 'all'
216
+ ): ReadonlyArray<LikeC4Model.Element> {
217
+ return this.outgoing(element, filter).map(r => r.target)
218
+ }
219
+
220
+ /**
221
+ * Relationships inside the element, among descendants
222
+ */
223
+ public internal(element: ElementOrFqn): ReadonlyArray<LikeC4Model.Relationship> {
224
+ const id = isString(element) ? element : element.id
225
+ return Array.from(this._internalOf(id))
226
+ }
227
+
228
+ /**
229
+ * Resolve siblings of the element and siblings of ancestors
230
+ * (from closest to root)
231
+ */
232
+ public ascendingSiblings(element: ElementOrFqn): ReadonlyArray<LikeC4Model.Element> {
233
+ const id = isString(element) ? element : element.id
234
+ let siblings = this.#cacheAscendingSiblings.get(id)
235
+ if (!siblings) {
236
+ siblings = [
237
+ ...this.siblings(id),
238
+ ...this.ancestors(id).flatMap(a => this.siblings(a.id))
239
+ ]
240
+ this.#cacheAscendingSiblings.set(id, siblings)
241
+ }
242
+ return siblings.slice()
243
+ }
244
+
245
+ /**
246
+ * Resolve all views that contain the element
247
+ */
248
+ public viewsWithElement(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel> {
249
+ const id = isString(element) ? element : element.id
250
+ return [...this.#views.values()].filter(v => v.hasElement(id))
251
+ }
252
+
253
+ private addElement(parsed: c4.Element) {
254
+ if (this.#elements.has(parsed.id)) {
255
+ throw new Error(`Element ${parsed.id} already exists`)
256
+ }
257
+ const el = new LikeC4Model.Element(parsed, this)
258
+ this.#elements.set(el.id, el)
259
+ const parentId = parentFqn(el.id)
260
+ if (parentId) {
261
+ this.#parents.set(el.id, this.element(parentId))
262
+ this._childrenOf(parentId).push(el)
263
+ } else {
264
+ this.#rootElements.add(el)
265
+ }
266
+ }
267
+
268
+ private addRelation(relation: c4.Relation) {
269
+ if (this.#relations.has(relation.id)) {
270
+ throw new Error(`Relation ${relation.id} already exists`)
271
+ }
272
+ const rel = new LikeC4Model.Relationship(relation, this)
273
+ this.#relations.set(rel.id, rel)
274
+ this._incomingTo(relation.target).add(rel)
275
+ this._outgoingFrom(relation.source).add(rel)
276
+
277
+ const relParent = commonAncestor(relation.source, relation.target)
278
+ // Process internal relationships
279
+ if (relParent) {
280
+ for (const ancestor of [relParent, ...ancestorsFqn(relParent)]) {
281
+ this._internalOf(ancestor).add(rel)
282
+ }
283
+ }
284
+ // Process source hierarchy
285
+ for (const sourceAncestor of ancestorsFqn(relation.source)) {
286
+ if (sourceAncestor === relParent) {
287
+ break
288
+ }
289
+ this._outgoingFrom(sourceAncestor).add(rel)
290
+ }
291
+ // Process target hierarchy
292
+ for (const targetAncestor of ancestorsFqn(relation.target)) {
293
+ if (targetAncestor === relParent) {
294
+ break
295
+ }
296
+ this._incomingTo(targetAncestor).add(rel)
297
+ }
298
+ }
299
+
300
+ private _childrenOf(id: Fqn) {
301
+ let children = this.#children.get(id)
302
+ if (!children) {
303
+ children = []
304
+ this.#children.set(id, children)
305
+ }
306
+ return children
307
+ }
308
+
309
+ private _incomingTo(id: Fqn) {
310
+ let incoming = this.#incoming.get(id)
311
+ if (!incoming) {
312
+ incoming = new RelationsSet()
313
+ this.#incoming.set(id, incoming)
314
+ }
315
+ return incoming
316
+ }
317
+
318
+ private _outgoingFrom(id: Fqn) {
319
+ let outgoing = this.#outgoing.get(id)
320
+ if (!outgoing) {
321
+ outgoing = new RelationsSet()
322
+ this.#outgoing.set(id, outgoing)
323
+ }
324
+ return outgoing
325
+ }
326
+
327
+ private _internalOf(id: Fqn) {
328
+ let internal = this.#internal.get(id)
329
+ if (!internal) {
330
+ internal = new RelationsSet()
331
+ this.#internal.set(id, internal)
332
+ }
333
+ return internal
334
+ }
335
+ }
336
+
337
+ export namespace LikeC4Model {
338
+ export class Element {
339
+ constructor(
340
+ public readonly element: c4.Element,
341
+ private model: LikeC4Model
342
+ ) {
343
+ }
344
+
345
+ get id() {
346
+ return this.element.id
347
+ }
348
+
349
+ get title() {
350
+ return this.element.title
351
+ }
352
+
353
+ get kind() {
354
+ return this.element.kind
355
+ }
356
+
357
+ get isRoot(): boolean {
358
+ return parentFqn(this.element.id) === null
359
+ }
360
+
361
+ get hasNested(): boolean {
362
+ return this.model.children(this).length > 0
363
+ }
364
+
365
+ get shape(): c4.ElementShape {
366
+ return this.element.shape ?? DefaultElementShape
367
+ }
368
+
369
+ get color(): c4.Color {
370
+ return this.element.color ?? DefaultThemeColor
371
+ }
372
+
373
+ get tags(): c4.Tag[] {
374
+ return this.element.tags ?? []
375
+ }
376
+
377
+ public parent(): LikeC4Model.Element | null {
378
+ return this.model.parent(this)
379
+ }
380
+
381
+ public metadata(key: string): string | undefined
382
+ public metadata(key: string, defaultValue: string): string
383
+ public metadata(key: string, defaultValue?: string): string | undefined {
384
+ return this.element.metadata?.[key] ?? defaultValue
385
+ }
386
+
387
+ public hasMetadata(key: string): boolean {
388
+ return isTruthy(this.element.metadata?.[key])
389
+ }
390
+
391
+ public ancestors() {
392
+ return this.model.ancestors(this)
393
+ }
394
+
395
+ public siblings() {
396
+ return this.model.siblings(this)
397
+ }
398
+
399
+ public descendants() {
400
+ return this.model.descendants(this)
401
+ }
402
+
403
+ public children() {
404
+ return this.model.children(this)
405
+ }
406
+
407
+ /**
408
+ * Views that contain this element
409
+ */
410
+ public views() {
411
+ return this.model.viewsWithElement(this)
412
+ }
413
+
414
+ public incoming(filter?: 'all' | 'direct' | 'to-descendants') {
415
+ return this.model.incoming(this, filter)
416
+ }
417
+
418
+ public incomers(filter?: 'all' | 'direct' | 'to-descendants') {
419
+ return this.model.incomers(this, filter)
420
+ }
421
+
422
+ public outgoing(filter?: 'all' | 'direct' | 'from-descendants') {
423
+ return this.model.outgoing(this, filter)
424
+ }
425
+
426
+ public outgoers(filter?: 'all' | 'direct' | 'from-descendants') {
427
+ return this.model.outgoers(this, filter)
428
+ }
429
+
430
+ public internal() {
431
+ return this.model.internal(this)
432
+ }
433
+
434
+ // public *descendants(): IterableIterator<LikeC4Element> {
435
+ // return
436
+ // }
437
+ }
438
+
439
+ export class Relationship {
440
+ constructor(
441
+ public readonly relationship: c4.Relation,
442
+ private model: LikeC4Model
443
+ ) {
444
+ }
445
+
446
+ get id() {
447
+ return this.relationship.id
448
+ }
449
+
450
+ get title() {
451
+ return this.relationship.title
452
+ }
453
+
454
+ get kind() {
455
+ return this.relationship.kind ?? null
456
+ }
457
+
458
+ get tags(): c4.Tag[] {
459
+ return this.relationship.tags ?? []
460
+ }
461
+
462
+ get source(): LikeC4Model.Element {
463
+ return this.model.element(this.relationship.source)
464
+ }
465
+
466
+ get target(): LikeC4Model.Element {
467
+ return this.model.element(this.relationship.target)
468
+ }
469
+
470
+ public metadata(key: string): string | undefined
471
+ public metadata(key: string, defaultValue: string): string
472
+ public metadata(key: string, defaultValue?: string): string | undefined {
473
+ return this.relationship.metadata?.[key] ?? defaultValue
474
+ }
475
+
476
+ public hasMetadata(key: string): boolean {
477
+ return isTruthy(this.relationship.metadata?.[key])
478
+ }
479
+ }
480
+ }