@likec4/leanix-bridge 1.53.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.
@@ -0,0 +1,85 @@
1
+ import type {
2
+ BridgeManifest,
3
+ CanonicalId,
4
+ ManifestEntity,
5
+ ManifestRelation,
6
+ ManifestView,
7
+ ViewId,
8
+ } from './contracts'
9
+ import {
10
+ BRIDGE_MANIFEST_VERSION,
11
+ BRIDGE_VERSION,
12
+ } from './contracts'
13
+ import type { BridgeModelInput } from './model-input'
14
+
15
+ export interface ToBridgeManifestOptions {
16
+ manifestVersion?: string
17
+ generatedAt?: string
18
+ bridgeVersion?: string
19
+ mappingProfile?: string
20
+ }
21
+
22
+ const defaultOptions: Omit<Required<ToBridgeManifestOptions>, 'generatedAt'> = {
23
+ manifestVersion: BRIDGE_MANIFEST_VERSION,
24
+ bridgeVersion: BRIDGE_VERSION,
25
+ mappingProfile: 'default',
26
+ }
27
+
28
+ /** Builds manifest entities map from model elements (canonicalId + empty external). */
29
+ function buildManifestEntities(model: BridgeModelInput): Record<CanonicalId, ManifestEntity> {
30
+ const entities: Record<CanonicalId, ManifestEntity> = {}
31
+ for (const el of model.elements()) {
32
+ entities[el.id] = { canonicalId: el.id, external: {} }
33
+ }
34
+ return entities
35
+ }
36
+
37
+ /** Builds manifest views map from model views (viewId + empty external). */
38
+ function buildManifestViews(model: BridgeModelInput): Record<ViewId, ManifestView> {
39
+ const views: Record<ViewId, ManifestView> = {}
40
+ for (const v of model.views()) {
41
+ views[v.id] = { viewId: v.id, external: {} }
42
+ }
43
+ return views
44
+ }
45
+
46
+ /** Builds manifest relations array from model relationships (compositeKey + empty external). */
47
+ function buildManifestRelations(model: BridgeModelInput): ManifestRelation[] {
48
+ const relations: ManifestRelation[] = []
49
+ for (const rel of model.relationships()) {
50
+ relations.push({
51
+ relationId: rel.id,
52
+ sourceFqn: rel.source.id,
53
+ targetFqn: rel.target.id,
54
+ compositeKey: `${rel.source.id}|${rel.target.id}|${rel.id}`,
55
+ external: {},
56
+ })
57
+ }
58
+ return relations
59
+ }
60
+
61
+ /**
62
+ * Produces the identity manifest from a LikeC4 model (canonical IDs + placeholders for external IDs).
63
+ * Pure function; no live API calls.
64
+ */
65
+ export function toBridgeManifest(
66
+ model: BridgeModelInput,
67
+ options: ToBridgeManifestOptions = {},
68
+ ): BridgeManifest {
69
+ const opts: Required<ToBridgeManifestOptions> = {
70
+ ...defaultOptions,
71
+ ...options,
72
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
73
+ }
74
+
75
+ return {
76
+ manifestVersion: opts.manifestVersion,
77
+ generatedAt: opts.generatedAt,
78
+ bridgeVersion: opts.bridgeVersion,
79
+ mappingProfile: opts.mappingProfile,
80
+ projectId: model.projectId,
81
+ entities: buildManifestEntities(model),
82
+ views: buildManifestViews(model),
83
+ relations: buildManifestRelations(model),
84
+ }
85
+ }
@@ -0,0 +1,105 @@
1
+ import { getFactSheetType, getRelationType, mergeWithDefault } from './mapping'
2
+ import type { LeanixMappingConfig } from './mapping'
3
+ import type { BridgeModelInput } from './model-input'
4
+
5
+ /** Single LeanIX fact sheet in dry-run shape (no IDs from live API) */
6
+ export interface LeanixFactSheetDryRun {
7
+ type: string
8
+ likec4Id: string
9
+ name: string
10
+ description?: string
11
+ technology?: string
12
+ tags?: string[]
13
+ metadata?: Record<string, unknown>
14
+ }
15
+
16
+ /** Single LeanIX relation in dry-run shape */
17
+ export interface LeanixRelationDryRun {
18
+ type: string
19
+ likec4RelationId: string
20
+ sourceLikec4Id: string
21
+ targetLikec4Id: string
22
+ title?: string
23
+ }
24
+
25
+ /** Dry-run inventory: fact sheets and relations as would be sent to LeanIX, without live IDs */
26
+ export interface LeanixInventoryDryRun {
27
+ generatedAt: string
28
+ projectId: string
29
+ mappingProfile: string
30
+ factSheets: LeanixFactSheetDryRun[]
31
+ relations: LeanixRelationDryRun[]
32
+ }
33
+
34
+ /** Options for toLeanixInventoryDryRun (mapping, mappingProfile, generatedAt). */
35
+ export interface ToLeanixInventoryDryRunOptions {
36
+ mapping?: LeanixMappingConfig | null
37
+ mappingProfile?: string
38
+ generatedAt?: string
39
+ }
40
+
41
+ /** Builds LeanIX fact sheet dry-run list from model elements and mapping. */
42
+ function buildFactSheetsFromModel(
43
+ model: BridgeModelInput,
44
+ mapping: ReturnType<typeof mergeWithDefault>,
45
+ ): LeanixFactSheetDryRun[] {
46
+ const factSheets: LeanixFactSheetDryRun[] = []
47
+ for (const el of model.elements()) {
48
+ const fsType = getFactSheetType(el.kind, mapping)
49
+ const meta = el.getMetadata()
50
+ const desc = typeof meta['description'] === 'string' ? meta['description'] : undefined
51
+ const tech = el.technology ?? (typeof meta['technology'] === 'string' ? meta['technology'] : undefined)
52
+ factSheets.push({
53
+ type: fsType,
54
+ likec4Id: el.id,
55
+ name: el.title,
56
+ ...(desc !== undefined && { description: desc }),
57
+ ...(tech !== undefined && { technology: tech }),
58
+ ...(el.tags.length > 0 && { tags: [...el.tags] }),
59
+ ...(Object.keys(meta).length > 0 && { metadata: { ...meta } }),
60
+ })
61
+ }
62
+ factSheets.sort((a, b) => a.likec4Id.localeCompare(b.likec4Id))
63
+ return factSheets
64
+ }
65
+
66
+ /** Builds LeanIX relation dry-run list from model relationships and mapping. */
67
+ function buildRelationsFromModel(
68
+ model: BridgeModelInput,
69
+ mapping: ReturnType<typeof mergeWithDefault>,
70
+ ): LeanixRelationDryRun[] {
71
+ const relations: LeanixRelationDryRun[] = []
72
+ for (const rel of model.relationships()) {
73
+ const titleVal = rel.title ?? rel.kind
74
+ relations.push({
75
+ type: getRelationType(rel.kind, mapping),
76
+ likec4RelationId: rel.id,
77
+ sourceLikec4Id: rel.source.id,
78
+ targetLikec4Id: rel.target.id,
79
+ ...(titleVal != null && titleVal !== '' && { title: String(titleVal) }),
80
+ })
81
+ }
82
+ relations.sort((a, b) => a.likec4RelationId.localeCompare(b.likec4RelationId))
83
+ return relations
84
+ }
85
+
86
+ /**
87
+ * Produces LeanIX-shaped inventory artifacts (fact sheets + relations) from a LikeC4 model.
88
+ * Pure function; no live API. Use for dry-run and planning.
89
+ */
90
+ export function toLeanixInventoryDryRun(
91
+ model: BridgeModelInput,
92
+ options: ToLeanixInventoryDryRunOptions = {},
93
+ ): LeanixInventoryDryRun {
94
+ const mapping = mergeWithDefault(options.mapping)
95
+ const generatedAt = options.generatedAt ?? new Date().toISOString()
96
+ const mappingProfile = options.mappingProfile ?? (options.mapping ? 'custom' : 'default')
97
+
98
+ return {
99
+ generatedAt,
100
+ projectId: model.projectId,
101
+ mappingProfile,
102
+ factSheets: buildFactSheetsFromModel(model, mapping),
103
+ relations: buildRelationsFromModel(model, mapping),
104
+ }
105
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Type guards for bridge artifacts (e.g. parsed JSON).
3
+ * Keeps validation next to the type definitions (SRP).
4
+ * Nested shapes are validated to avoid false positives (G2, G3).
5
+ */
6
+
7
+ import type { BridgeManifest, ManifestEntity, ManifestRelation, ManifestView } from './contracts'
8
+ import type {
9
+ LeanixFactSheetSnapshotItem,
10
+ LeanixInventorySnapshot,
11
+ LeanixRelationSnapshotItem,
12
+ } from './leanix-inventory-snapshot'
13
+
14
+ function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === 'object' && value !== null
16
+ }
17
+
18
+ function isManifestEntity(value: unknown): value is ManifestEntity {
19
+ if (!isRecord(value)) return false
20
+ return typeof value['canonicalId'] === 'string'
21
+ }
22
+
23
+ function isManifestView(value: unknown): value is ManifestView {
24
+ if (!isRecord(value)) return false
25
+ return typeof value['viewId'] === 'string'
26
+ }
27
+
28
+ function isManifestRelation(value: unknown): value is ManifestRelation {
29
+ if (!isRecord(value)) return false
30
+ return (
31
+ typeof value['relationId'] === 'string' &&
32
+ typeof value['sourceFqn'] === 'string' &&
33
+ typeof value['targetFqn'] === 'string' &&
34
+ typeof value['compositeKey'] === 'string'
35
+ )
36
+ }
37
+
38
+ function isLeanixFactSheetSnapshotItem(value: unknown): value is LeanixFactSheetSnapshotItem {
39
+ if (!isRecord(value)) return false
40
+ return (
41
+ typeof value['id'] === 'string' &&
42
+ typeof value['name'] === 'string' &&
43
+ typeof value['type'] === 'string'
44
+ )
45
+ }
46
+
47
+ function isLeanixRelationSnapshotItem(value: unknown): value is LeanixRelationSnapshotItem {
48
+ if (!isRecord(value)) return false
49
+ return (
50
+ typeof value['sourceFactSheetId'] === 'string' &&
51
+ typeof value['targetFactSheetId'] === 'string' &&
52
+ typeof value['type'] === 'string'
53
+ )
54
+ }
55
+
56
+ /**
57
+ * Returns true if the value is a valid BridgeManifest shape (manifestVersion, generatedAt, bridgeVersion, mappingProfile, projectId, entities, relations, views)
58
+ * and nested entities/views/relations match expected shapes.
59
+ */
60
+ export function isBridgeManifest(obj: unknown): obj is BridgeManifest {
61
+ if (!isRecord(obj)) return false
62
+ if (
63
+ typeof obj['manifestVersion'] !== 'string' ||
64
+ typeof obj['generatedAt'] !== 'string' ||
65
+ typeof obj['bridgeVersion'] !== 'string' ||
66
+ typeof obj['mappingProfile'] !== 'string' ||
67
+ typeof obj['projectId'] !== 'string'
68
+ ) {
69
+ return false
70
+ }
71
+ const entities = obj['entities']
72
+ if (typeof entities !== 'object' || entities === null || Array.isArray(entities)) return false
73
+ for (const v of Object.values(entities)) {
74
+ if (!isManifestEntity(v)) return false
75
+ }
76
+ const views = obj['views']
77
+ if (typeof views !== 'object' || views === null || Array.isArray(views)) return false
78
+ for (const v of Object.values(views)) {
79
+ if (!isManifestView(v)) return false
80
+ }
81
+ const relations = obj['relations']
82
+ if (!Array.isArray(relations)) return false
83
+ for (const r of relations) {
84
+ if (!isManifestRelation(r)) return false
85
+ }
86
+ return true
87
+ }
88
+
89
+ /**
90
+ * Returns true if the value is a valid LeanixInventorySnapshot shape (generatedAt, factSheets and relations arrays)
91
+ * and each fact sheet/relation item has required fields.
92
+ */
93
+ export function isLeanixInventorySnapshot(obj: unknown): obj is LeanixInventorySnapshot {
94
+ if (!isRecord(obj)) return false
95
+ if (typeof obj['generatedAt'] !== 'string') return false
96
+ if (obj['workspaceId'] !== undefined && typeof obj['workspaceId'] !== 'string') return false
97
+ if (!Array.isArray(obj['factSheets'])) return false
98
+ for (const fs of obj['factSheets']) {
99
+ if (!isLeanixFactSheetSnapshotItem(fs)) return false
100
+ }
101
+ if (!Array.isArray(obj['relations'])) return false
102
+ for (const rel of obj['relations']) {
103
+ if (!isLeanixRelationSnapshotItem(rel)) return false
104
+ }
105
+ return true
106
+ }