@likec4/core 1.10.1 → 1.12.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.
@@ -1,35 +1,37 @@
1
- import { isNullish, isString } from 'remeda'
1
+ import { isNullish } from 'remeda'
2
2
  import { nonNullable } from '../errors'
3
- import type { ElementShape, Tag } from '../types/element'
4
- import type { Color } from '../types/theme'
3
+ import {
4
+ type ElementKind as C4ElementKind,
5
+ type ElementShape as C4ElementShape,
6
+ type Tag as C4Tag
7
+ } from '../types/element'
8
+ import type { Color as C4Color } from '../types/theme'
5
9
  import type { ComputedEdge, ComputedNode, ComputedView } from '../types/view'
6
10
  import type { LikeC4Model } from './LikeC4Model'
7
- import type { EdgeId, Fqn } from './types'
8
-
9
- type ElementOrFqn = Fqn | LikeC4ViewModel.Element
11
+ import { type EdgeId, type ElementOrFqn, type Fqn, getId, type IncomingFilter, type OutgoingFilter } from './types'
10
12
 
11
13
  /**
12
14
  * All methods are view-scoped, i.e. only return elements and connections in the view.
13
15
  */
14
16
  export class LikeC4ViewModel {
15
- #rootElements = new Set<LikeC4ViewModel.Element>()
17
+ private _rootElements = new Set<LikeC4ViewModel.Element>()
16
18
 
17
- #elements = new Map<Fqn, LikeC4ViewModel.Element>()
19
+ private _elements = new Map<Fqn, LikeC4ViewModel.Element>()
18
20
 
19
- #connections: Map<EdgeId, LikeC4ViewModel.Connection>
21
+ private _connections: Map<EdgeId, LikeC4ViewModel.Connection>
20
22
 
21
23
  constructor(
22
24
  public readonly view: ComputedView,
23
- public readonly model: LikeC4Model
25
+ public readonly model: LikeC4Model<LikeC4ViewModel>
24
26
  ) {
25
27
  for (const node of view.nodes) {
26
28
  const el = new LikeC4ViewModel.Element(node, this)
27
- this.#elements.set(node.id, el)
29
+ this._elements.set(node.id, el)
28
30
  if (isNullish(node.parent)) {
29
- this.#rootElements.add(el)
31
+ this._rootElements.add(el)
30
32
  }
31
33
  }
32
- this.#connections = new Map(view.edges.map(e => [e.id, new LikeC4ViewModel.Connection(e, this)]))
34
+ this._connections = new Map(view.edges.map(e => [e.id, new LikeC4ViewModel.Connection(e, this)]))
33
35
  }
34
36
 
35
37
  get id() {
@@ -47,32 +49,32 @@ export class LikeC4ViewModel {
47
49
  return null
48
50
  }
49
51
 
50
- get tags(): Tag[] {
52
+ get tags(): C4Tag[] {
51
53
  return this.view.tags ?? []
52
54
  }
53
55
 
54
56
  public roots(): ReadonlyArray<LikeC4ViewModel.Element> {
55
- return [...this.#rootElements]
57
+ return [...this._rootElements]
56
58
  }
57
59
 
58
60
  public elements(): ReadonlyArray<LikeC4ViewModel.Element> {
59
- return [...this.#elements.values()]
61
+ return [...this._elements.values()]
60
62
  }
61
63
 
62
64
  public element(id: Fqn): LikeC4ViewModel.Element {
63
- return nonNullable(this.#elements.get(id), `LikeC4ViewModel.Element ${id} in view ${this.view.id} not found`)
65
+ return nonNullable(this._elements.get(id), `LikeC4ViewModel.Element ${id} in view ${this.id} not found`)
64
66
  }
65
67
 
66
68
  public hasElement(id: Fqn): boolean {
67
- return this.#elements.has(id)
69
+ return this._elements.has(id)
68
70
  }
69
71
 
70
72
  public connections(): ReadonlyArray<LikeC4ViewModel.Connection> {
71
- return [...this.#connections.values()]
73
+ return [...this._connections.values()]
72
74
  }
73
75
 
74
76
  public connection(id: EdgeId): LikeC4ViewModel.Connection {
75
- return nonNullable(this.#connections.get(id), `Connection ${id} in view ${this.view.id} not found`)
77
+ return nonNullable(this._connections.get(id), `Connection ${id} in view ${this.id} not found`)
76
78
  }
77
79
 
78
80
  public findConnections(
@@ -80,8 +82,8 @@ export class LikeC4ViewModel {
80
82
  target: ElementOrFqn,
81
83
  direction: 'both' | 'direct' = 'direct'
82
84
  ): ReadonlyArray<LikeC4ViewModel.Connection> {
83
- const sourceId = isString(source) ? source : source.id
84
- const targetId = isString(target) ? target : target.id
85
+ const sourceId = getId(source)
86
+ const targetId = getId(target)
85
87
  return this.connections().filter(c =>
86
88
  (c.source.id === sourceId && c.target.id === targetId)
87
89
  || (direction === 'both' && c.source.id === targetId && c.target.id === sourceId)
@@ -89,18 +91,18 @@ export class LikeC4ViewModel {
89
91
  }
90
92
 
91
93
  public parent(element: ElementOrFqn): LikeC4ViewModel.Element | null {
92
- const el = this.element(isString(element) ? element : element.id)
94
+ const el = this.element(getId(element))
93
95
  return el.node.parent ? this.element(el.node.parent) : null
94
96
  }
95
97
 
96
98
  public children(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
97
- const el = this.element(isString(element) ? element : element.id)
99
+ const el = this.element(getId(element))
98
100
  return el.node.children.map(c => this.element(c))
99
101
  }
100
102
 
101
103
  // Get all sibling (i.e. same parent)
102
104
  public siblings(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
103
- const id = isString(element) ? element : element.id
105
+ const id = getId(element)
104
106
  const parent = this.parent(id)
105
107
  const siblings = parent ? this.children(parent) : this.roots()
106
108
  return siblings.filter(e => e.id !== id)
@@ -111,7 +113,7 @@ export class LikeC4ViewModel {
111
113
  * (from closest to root)
112
114
  */
113
115
  public ancestors(element: ElementOrFqn): ReadonlyArray<LikeC4ViewModel.Element> {
114
- let id = isString(element) ? element : element.id
116
+ let id = getId(element)
115
117
  const result = [] as LikeC4ViewModel.Element[]
116
118
  let parent
117
119
  while (parent = this.parent(id)) {
@@ -128,10 +130,10 @@ export class LikeC4ViewModel {
128
130
 
129
131
  public incoming(
130
132
  element: ElementOrFqn,
131
- filter: 'all' | 'direct' | 'to-descendants' = 'all'
133
+ filter: IncomingFilter = 'all'
132
134
  ): 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
+ const el = this.element(getId(element))
136
+ const edges = el.node.inEdges.map(e => nonNullable(this._connections.get(e), `Edge ${e} not found`))
135
137
 
136
138
  if (filter === 'all' || edges.length === 0) {
137
139
  return edges
@@ -147,7 +149,7 @@ export class LikeC4ViewModel {
147
149
 
148
150
  public incomers(
149
151
  element: ElementOrFqn,
150
- filter: 'all' | 'direct' | 'to-descendants' = 'all'
152
+ filter: IncomingFilter = 'all'
151
153
  ): ReadonlyArray<LikeC4ViewModel.Element> {
152
154
  return this.incoming(element, filter).map(r => r.source)
153
155
  }
@@ -157,10 +159,10 @@ export class LikeC4ViewModel {
157
159
  */
158
160
  public outgoing(
159
161
  element: ElementOrFqn,
160
- filter: 'all' | 'direct' | 'from-descendants' = 'all'
162
+ filter: OutgoingFilter = 'all'
161
163
  ): 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
+ const el = this.element(getId(element))
165
+ const edges = el.node.outEdges.map(e => nonNullable(this._connections.get(e), `Edge ${e} not found`))
164
166
 
165
167
  if (filter === 'all' || edges.length === 0) {
166
168
  return edges
@@ -176,7 +178,7 @@ export class LikeC4ViewModel {
176
178
 
177
179
  public outgoers(
178
180
  element: ElementOrFqn,
179
- filter: 'all' | 'direct' | 'from-descendants' = 'all'
181
+ filter: OutgoingFilter = 'all'
180
182
  ): ReadonlyArray<LikeC4ViewModel.Element> {
181
183
  return this.outgoing(element, filter).map(r => r.target)
182
184
  }
@@ -202,7 +204,7 @@ export namespace LikeC4ViewModel {
202
204
  return this.node.title
203
205
  }
204
206
 
205
- get kind() {
207
+ get kind(): C4ElementKind {
206
208
  return this.node.kind
207
209
  }
208
210
 
@@ -214,19 +216,19 @@ export namespace LikeC4ViewModel {
214
216
  return this.node.children.length > 0
215
217
  }
216
218
 
217
- get shape(): ElementShape {
219
+ get shape(): C4ElementShape {
218
220
  return this.node.shape
219
221
  }
220
222
 
221
- get color(): Color {
223
+ get color(): C4Color {
222
224
  return this.node.color
223
225
  }
224
226
 
225
- get tags(): Tag[] {
227
+ get tags(): C4Tag[] {
226
228
  return this.node.tags ?? []
227
229
  }
228
230
 
229
- public model(): LikeC4Model.Element {
231
+ public model(): LikeC4Model.ElementModel<LikeC4ViewModel> {
230
232
  return this.viewmodel.model.element(this.id)
231
233
  }
232
234
 
@@ -268,11 +270,11 @@ export namespace LikeC4ViewModel {
268
270
  return this.viewmodel.incomers(this, filter)
269
271
  }
270
272
 
271
- public outgoing(filter: 'all' | 'direct' | 'from-descendants' = 'all'): ReadonlyArray<LikeC4ViewModel.Connection> {
273
+ public outgoing(filter: OutgoingFilter = 'all'): ReadonlyArray<LikeC4ViewModel.Connection> {
272
274
  return this.viewmodel.outgoing(this, filter)
273
275
  }
274
276
 
275
- public outgoers(filter: 'all' | 'direct' | 'from-descendants' = 'all'): ReadonlyArray<LikeC4ViewModel.Element> {
277
+ public outgoers(filter: OutgoingFilter = 'all'): ReadonlyArray<LikeC4ViewModel.Element> {
276
278
  return this.viewmodel.outgoers(this, filter)
277
279
  }
278
280
 
@@ -304,14 +306,14 @@ export namespace LikeC4ViewModel {
304
306
  return this.viewmodel.element(this.edge.target)
305
307
  }
306
308
 
307
- get tags(): Tag[] {
309
+ get tags(): C4Tag[] {
308
310
  return this.edge.tags ?? []
309
311
  }
310
312
 
311
313
  /**
312
314
  * Model relationships
313
315
  */
314
- relationships(): ReadonlyArray<LikeC4Model.Relationship> {
316
+ relationships(): ReadonlyArray<LikeC4Model.Relationship<LikeC4ViewModel>> {
315
317
  return this.edge.relations.map(r => nonNullable(this.viewmodel.model.relationship(r)))
316
318
  }
317
319
  }
@@ -1,2 +1,3 @@
1
- export * from './LikeC4Model'
2
- export * from './LikeC4ViewModel'
1
+ export { LikeC4DiagramModel } from './LikeC4DiagramModel'
2
+ export { LikeC4Model } from './LikeC4Model'
3
+ export { LikeC4ViewModel } from './LikeC4ViewModel'
@@ -2,9 +2,22 @@ import type { Fqn as CoreFqn } from '../types/element'
2
2
  import type { RelationID as CoreRelationID } from '../types/relation'
3
3
  import type { EdgeId as CoreEdgeId, ViewID as CoreViewID } from '../types/view'
4
4
 
5
+ import { isString } from 'remeda'
5
6
  import type { LiteralUnion } from 'type-fest'
6
7
 
7
8
  export type Fqn = LiteralUnion<CoreFqn, string>
8
9
  export type RelationID = LiteralUnion<CoreRelationID, string>
9
10
  export type ViewID = LiteralUnion<CoreViewID, string>
10
11
  export type EdgeId = LiteralUnion<CoreEdgeId, string>
12
+
13
+ export type IncomingFilter = 'all' | 'direct' | 'to-descendants'
14
+ export type OutgoingFilter = 'all' | 'direct' | 'from-descendants'
15
+
16
+ export type ElementOrFqn = Fqn | { id: Fqn }
17
+
18
+ /**
19
+ * Utility function to extract `id` from the given element.
20
+ */
21
+ export function getId(element: ElementOrFqn): Fqn {
22
+ return isString(element) ? element : element.id
23
+ }
@@ -9,5 +9,3 @@ export * from './theme'
9
9
  export * from './view'
10
10
  export * from './view-changes'
11
11
  export * from './view-notation'
12
-
13
- export * as Expr from './expression'
@@ -1,7 +1,7 @@
1
1
  import type { UnwrapTagged } from 'type-fest'
2
2
  import type { Element, ElementKind, ElementKindSpecification, Fqn, Tag } from './element'
3
3
  import type { Relation, RelationID, RelationshipKindSpecification } from './relation'
4
- import type { ComputedView, LikeC4View, ViewID } from './view'
4
+ import type { ComputedView, DiagramView, LikeC4View, ViewID } from './view'
5
5
 
6
6
  /**
7
7
  * Parsed elements, relations, and views.
@@ -21,5 +21,14 @@ export interface ParsedLikeC4Model {
21
21
  * Same as `ParsedLikeC4Model` but with computed views.
22
22
  */
23
23
  export interface ComputedLikeC4Model extends Omit<ParsedLikeC4Model, 'views'> {
24
+ __?: never
24
25
  views: Record<ViewID, ComputedView>
25
26
  }
27
+
28
+ /**
29
+ * Same as `ParsedLikeC4Model` but with layouted views (DiagramView)
30
+ */
31
+ export interface LayoutedLikeC4Model extends Omit<ParsedLikeC4Model, 'views'> {
32
+ __: 'layouted'
33
+ views: Record<ViewID, DiagramView>
34
+ }
package/src/types/view.ts CHANGED
@@ -40,11 +40,17 @@ export function isViewRuleStyle(rule: ViewRule): rule is ViewRuleStyle {
40
40
  }
41
41
 
42
42
  export type AutoLayoutDirection = 'TB' | 'BT' | 'LR' | 'RL'
43
+ export function isAutoLayoutDirection(autoLayout: unknown): autoLayout is AutoLayoutDirection {
44
+ return autoLayout === 'TB' || autoLayout === 'BT' || autoLayout === 'LR' || autoLayout === 'RL'
45
+ }
46
+
43
47
  export interface ViewRuleAutoLayout {
44
- autoLayout: AutoLayoutDirection
48
+ direction: AutoLayoutDirection
49
+ nodeSep?: number
50
+ rankSep?: number
45
51
  }
46
52
  export function isViewRuleAutoLayout(rule: ViewRule): rule is ViewRuleAutoLayout {
47
- return 'autoLayout' in rule
53
+ return 'direction' in rule
48
54
  }
49
55
 
50
56
  export type ViewRule = ViewRulePredicate | ViewRuleStyle | ViewRuleAutoLayout
@@ -265,10 +271,14 @@ export interface ViewWithNotation {
265
271
  elements: ElementNotation[]
266
272
  }
267
273
  }
268
-
274
+ export interface ViewAutoLayout {
275
+ direction: ViewRuleAutoLayout['direction']
276
+ rankSep?: number
277
+ nodeSep?: number
278
+ }
269
279
  export interface ComputedElementView extends Omit<ElementView, 'rules' | 'docUri'>, ViewWithHash, ViewWithNotation {
270
280
  readonly extends?: ViewID
271
- readonly autoLayout: ViewRuleAutoLayout['autoLayout']
281
+ readonly autoLayout: ViewAutoLayout
272
282
  readonly nodes: ComputedNode[]
273
283
  readonly edges: ComputedEdge[]
274
284
  rules?: never
@@ -277,7 +287,7 @@ export interface ComputedElementView extends Omit<ElementView, 'rules' | 'docUri
277
287
  export interface ComputedDynamicView
278
288
  extends Omit<DynamicView, 'rules' | 'steps' | 'docUri'>, ViewWithHash, ViewWithNotation
279
289
  {
280
- readonly autoLayout: ViewRuleAutoLayout['autoLayout']
290
+ readonly autoLayout: ViewAutoLayout
281
291
  readonly nodes: ComputedNode[]
282
292
  readonly edges: ComputedEdge[]
283
293
  steps?: never
@@ -354,7 +364,7 @@ export type ViewManualLayout = {
354
364
  readonly y: number
355
365
  readonly width: number
356
366
  readonly height: number
357
- readonly autoLayout: AutoLayoutDirection
367
+ readonly autoLayout: ViewAutoLayout
358
368
  readonly nodes: Record<string, {
359
369
  isCompound: boolean
360
370
  x: number
@@ -1,9 +1,20 @@
1
+ import { randomInteger } from 'remeda'
2
+
1
3
  const DELAY = 'LIKEC4_DELAY'
2
4
 
3
- export function delay(ms: number = 100) {
5
+ export function delay(): Promise<string>
6
+ export function delay(ms: number): Promise<string>
7
+ export function delay(randomFrom: number, randomTo: number): Promise<string>
8
+ export function delay(...args: number[]): Promise<string> {
9
+ let ms = 100
10
+ if (args.length === 2) {
11
+ ms = randomInteger(args[0]!, args[1]!)
12
+ } else if (args.length === 1) {
13
+ ms = args[0]!
14
+ }
4
15
  return new Promise(resolve => {
5
16
  setTimeout(() => {
6
17
  resolve(DELAY)
7
- }, ms)
18
+ }, ms ?? 100)
8
19
  })
9
20
  }
@@ -1,79 +0,0 @@
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
- })
@@ -1,37 +0,0 @@
1
- import { sort as remedaSort } from 'remeda'
2
- import { describe, expect, it } from 'vitest'
3
- import { compareNatural } from './compare-natural'
4
-
5
- describe('compareNatural', () => {
6
- const sort = (array: Array<string | undefined>) => remedaSort(array, compareNatural)
7
-
8
- it('should move undefined to the end', () => {
9
- expect(
10
- sort([
11
- undefined,
12
- 'apple',
13
- undefined,
14
- 'banana'
15
- ])
16
- ).toEqual([
17
- 'apple',
18
- 'banana',
19
- undefined,
20
- undefined
21
- ])
22
- })
23
-
24
- it('should sort array of strings', () => {
25
- expect(sort([
26
- 'apple1',
27
- 'apple11',
28
- 'apple2',
29
- 'apple10'
30
- ])).toEqual([
31
- 'apple1',
32
- 'apple2',
33
- 'apple10',
34
- 'apple11'
35
- ])
36
- })
37
- })