@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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/index.d.mts +559 -0
- package/dist/index.mjs +1110 -0
- package/package.json +62 -0
- package/src/adr-generation.ts +105 -0
- package/src/contracts.ts +96 -0
- package/src/drawio-leanix-roundtrip.ts +51 -0
- package/src/drift-report.ts +61 -0
- package/src/fixture-model.ts +52 -0
- package/src/governance-checks.ts +103 -0
- package/src/impact-report.ts +38 -0
- package/src/index.ts +109 -0
- package/src/leanix-api-client.ts +138 -0
- package/src/leanix-graphql-operations.ts +160 -0
- package/src/leanix-inventory-snapshot.ts +263 -0
- package/src/mapping.ts +88 -0
- package/src/model-input.ts +36 -0
- package/src/reconcile.ts +227 -0
- package/src/report.ts +73 -0
- package/src/sync-to-leanix.ts +352 -0
- package/src/to-bridge-manifest.ts +85 -0
- package/src/to-leanix-inventory-dry-run.ts +105 -0
- package/src/validate.ts +106 -0
|
@@ -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
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -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
|
+
}
|