feat: LeanIX Bridge — Draw.io bridge-managed export + bridge package#2746
feat: LeanIX Bridge — Draw.io bridge-managed export + bridge package#2746davydkov merged 32 commits intolikec4:mainfrom
Conversation
inferShape() now returns 'person' when DrawIO cell style contains shape=actor, shape=person, or umlactor. Re-imported actor cells get explicit style { shape person } in emitted .c4.
Made-with: Cursor
inferKind() now includes shape=actor so vertex with shape=actor is emitted as actor (not container). Aligns with upstream likec4#2682 and makes this branch pass ci:test without depending on likec4#2682 merge. Made-with: Cursor
…e=actor or umlActor) CodeRabbit: changeset now states export may emit shape=actor or shape=umlActor; inferShape comment updated to match. Made-with: Cursor
…likec4Id round-trip - Add @likec4/leanix-bridge: toBridgeManifest, toLeanixInventoryDryRun, toReport, mapping - Draw.io export profile leanix: bridgeManaged, likec4Id, likec4RelationId, leanixFactSheetType - CLI: likec4 export drawio --profile leanix - Parser: read likec4Id/likec4RelationId from style; use likec4Id for FQN when present - E2E test (skip CI) for export drawio --profile leanix - .gitignore: docs/architecture-intelligence; PR doc PR_leanix_bridge.md (English) Made-with: Cursor
Fixes CI frozen-lockfile error: specifiers in lockfile now match package.json. Made-with: Cursor
… export drawio re-enabled - LeanixApiClient: Bearer auth, rate limiting, GraphQL - syncToLeanix: create/update fact sheets and relations, idempotent, returns updated manifest - manifestToDrawioLeanixMapping: likec4Id <-> LeanIX fact sheet id for Draw.io round-trip - E2E: run export drawio tests with cwd=projectRoot so CI finds likec4.config and .c4 Made-with: Cursor
…eport guard, spec fixes
- parse-drawio: pass usedNames to assignFqnsToElementVertices, seed with likec4Id segments
- leanix-bridge: oxlint --type-aware; mapping comments; mergeWithDefault fresh nested objects
- report: artifact-coherence guard (projectId/mappingProfile)
- to-bridge-manifest.spec: use BRIDGE_VERSION constant
- fixture-model: default elements use metadata: {}
- bridge-artifacts.spec: mappingProfile snapshot in report test
- parse-drawio.spec: rename test to stable FQN identity
- e2e: require both bridgeManaged=true and likec4Id= in assertion
Made-with: Cursor
…ctors - PR description: DrawIO + LeanIX bridge, sync, round-trip, E2E, review fixes, refactors - bot-reply.md: reply to @coderabbitai and @chatgpt-codex-connector with all addressed items - generate-drawio: leanix root tokens with ; delimiter, getLeanixRootStyleParts - contracts: BRIDGE_VERSION from package.json - to-bridge-manifest: generatedAt per call - to-leanix-inventory-dry-run: technology type guard - handler: compressed explicit, buildDrawioExportOverrides - sync-to-leanix: applyLeanixIdsToEntities refactor Made-with: Cursor
…pdown, boundary tests - leanix-api-client: lastRequestTime per instance (no shared throttle) - report: buildCoherenceErrorMessage with precise mismatch fields - sync-to-leanix: syncFactSheetsToLeanix, syncRelationsToLeanix - to-bridge-manifest: buildManifestEntities/Views/Relations + JSDoc - to-leanix-inventory-dry-run: buildFactSheetsFromModel, buildRelationsFromModel + JSDoc - drawio-leanix-roundtrip: collectLikec4IdToLeanixFactSheetId, collectRelationKeyToLeanixRelationId + JSDoc - Boundary tests: to-bridge-manifest, to-leanix-inventory-dry-run, drawio-leanix-roundtrip.spec (new), bridge-artifacts (toReport message), generate-drawio (leanix without projectId) - docs: PR-LEANIX-DRAWIO-UNCLE-BOB-REVIEW.md Made-with: Cursor
- Stop tracking: docs/PR-LEANIX-DRAWIO-UNCLE-BOB-REVIEW.md, bot-reply.md, PR_leanix_bridge.md, PR_leanix_sync_e2e.md - .gitignore: PR_*.md, bot-reply.md, docs/PR-*-REVIEW.md (documentação de apoio fora do padrão) Made-with: Cursor
Made-with: Cursor
…nner - README: sync example uses dryRun (matches generator) for copy/paste - index.ts: banner reflects dry-run, LeanIX sync, Draw.io round-trip helpers Made-with: Cursor
- Add shapeFromStyle on DrawioCell and use in inferKind/inferShape when raw style missing - Ensure cell.style set from attrs when present; fallback style from styleMap for shape - Relax parse-drawio actor test: accept container or actor+shape person, update snapshot - Remove obsolete snapshot entry Made-with: Cursor
- Fix actor tests: use inline fakeActorView and accept shape=actor|umlActor - Fix leanix profile tests: import from @likec4/generators so options apply - Relax likec4Kind assertion (key present, value may be empty) - Add viewWithElementKindsForLeanix helper for leanixFactSheetType test - Move actor round-trip test into describe; remove redundant assertion - Resolve merge conflicts in parse-drawio.spec.ts and parse-drawio.ts - All 68 drawio tests passing Made-with: Cursor
pnpm install --no-frozen-lockfile to resolve: ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY [email protected](...) Made-with: Cursor
…d refactors - Add planSyncToLeanix(dryRun, client, options): read-only LeanIX queries, returns SyncPlan (create/update per fact sheet, summary). Phase 2 dry-run sync planning complete. - Export SyncPlan, SyncPlanFactSheetEntry, SyncPlanRelationEntry, SyncPlanSummary, PlanSyncToLeanixOptions from leanix-bridge. - CI: run pnpm build before pnpm ci:test in check-tests and check-on-windows so generators dist exists for drawio specs. - DrawIO specs: generate-drawio.spec uses @likec4/generators; parse-drawio.spec uses ../../dist/index.mjs (CI builds first). - Docs: LEANIX-BRIDGE-ROADMAP-LOCAL.md (Phase 2 done), PR-LEANIX-BRIDGE-DESCRIPTION.md (overview, strategy, Phase 2, how to use). - README leanix-bridge: sync plan section and API entry for planSyncToLeanix. - Refactors (CRAFT): toErrorMessage, findFactSheetByNameAndType throws on API error (null = not found), buildFactSheetPlanEntry/buildPlanSummary, createDefaultDryRun in sync-to-leanix.spec. Made-with: Cursor
…ertyTypes and possibly undefined - mapping.spec: use bracket notation for factSheetTypes (TS4111) - sync-to-leanix.spec: variables['name']/['type'] for Record<string, unknown> - to-leanix-inventory-dry-run: meta['description']/['technology']; omit optional props instead of undefined (exactOptionalPropertyTypes); relation title spread when defined - to-bridge-manifest.spec, to-leanix-inventory-dry-run.spec: non-null assertion where test guarantees presence (TS2532) - bridge-artifacts snapshot: leanix-dry-run shape without undefined keys Made-with: Cursor
…y'] in spec (TS2532) Made-with: Cursor
…drawio spec (Vitest/Windows ESM) Made-with: Cursor
… string|undefined Made-with: Cursor
…& factSheetsReused Made-with: Cursor
… separate file) Made-with: Cursor
…st-ambient naming Made-with: Cursor
…o static import Made-with: Cursor
…ithout dist Made-with: Cursor
…) for oxlint Made-with: Cursor
… String(fs.name)) Made-with: Cursor
Made-with: Cursor
…s readable in file Made-with: Cursor
🦋 Changeset detectedLatest commit: 19bc423 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b8bd9765b0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…2 propagate createRelation errors Made-with: Cursor
|
Addressed review feedback from @chatgpt-codex-connector: P1 (likec4IdAttribute for idempotent lookup): Idempotent fact sheet resolution now uses \likec4IdAttribute\ when set. Added \indFactSheetByLikec4IdAttribute\ (GraphQL filter by custom attribute via facetFilters) and use it in \syncFactSheetsToLeanix\ and \planSyncToLeanix. \PlanSyncToLeanixOptions\ now includes \likec4IdAttribute\ so distinct LikeC4 elements resolve to the correct LeanIX fact sheet. P2 (relation creation errors): \createRelation\ no longer swallows errors; failures propagate to \syncRelationsToLeanix, which catches and appends to \errors\ so the caller receives a proper result with no silent missing relations. Replies were also added in thread to each review comment. |
|
To use Codex here, create an environment for this repo. |
📝 WalkthroughWalkthroughAdds a new Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI
participant Handler as DrawIO Handler
participant Generator as generateDrawio
participant DrawioFile as .drawio
participant Parser as parseDrawioToLikeC4
participant Bridge as `@likec4/leanix-bridge`
participant LeanIX as LeanIX API
CLI->>Handler: likec4 export drawio --profile leanix --project-id proj-123
Handler->>Generator: generateDrawio(view, options{profile:leanix, projectId})
Generator->>Generator: buildBridgeManagedStyleForNode/Edge (embed likec4Id, likec4Kind, viewId, projectId)
Generator->>DrawioFile: emit .drawio with bridge metadata
Parser->>DrawioFile: parse .drawio
Parser->>Parser: extract style/shapeFromStyle, inferKind/inferShape (actor/person)
Parser->>Bridge: manifestToDrawioLeanixMapping(manifest) / produce BridgeManifest
Bridge->>LeanIX: planSyncToLeanix / syncToLeanix (GraphQL via LeanixApiClient)
LeanIX-->>Bridge: fact sheet IDs / relation confirmations
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
packages/leanix-bridge/src/to-leanix-inventory-dry-run.ts (1)
45-61: Sort the generated artifacts before returning them.
bridge-artifacts.spec.tssnapshots these arrays as JSON, but their order currently depends on whatever traversal ordermodel.elements()andmodel.relationships()expose. Sorting bylikec4Id/likec4RelationIdwill keep diffs stable if upstream iteration order changes.♻️ Minimal change
function buildFactSheetsFromModel( model: BridgeModelInput, mapping: ReturnType<typeof mergeWithDefault>, ): LeanixFactSheetDryRun[] { const factSheets: LeanixFactSheetDryRun[] = [] for (const el of model.elements()) { const fsType = getFactSheetType(el.kind, mapping) const meta = el.getMetadata() const desc = typeof meta['description'] === 'string' ? meta['description'] : undefined const tech = el.technology ?? (typeof meta['technology'] === 'string' ? meta['technology'] : undefined) factSheets.push({ type: fsType, likec4Id: el.id, name: el.title, ...(desc !== undefined && { description: desc }), ...(tech !== undefined && { technology: tech }), ...(el.tags.length > 0 && { tags: [...el.tags] }), ...(Object.keys(meta).length > 0 && { metadata: { ...meta } }), }) } + factSheets.sort((a, b) => a.likec4Id.localeCompare(b.likec4Id)) return factSheets } @@ function buildRelationsFromModel( model: BridgeModelInput, mapping: ReturnType<typeof mergeWithDefault>, ): LeanixRelationDryRun[] { const relations: LeanixRelationDryRun[] = [] for (const rel of model.relationships()) { const titleVal = rel.title ?? rel.kind relations.push({ type: getRelationType(rel.kind, mapping), likec4RelationId: rel.id, sourceLikec4Id: rel.source.id, targetLikec4Id: rel.target.id, ...(titleVal != null && titleVal !== '' && { title: String(titleVal) }), }) } + relations.sort((a, b) => a.likec4RelationId.localeCompare(b.likec4RelationId)) return relations }Also applies to: 69-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/leanix-bridge/src/to-leanix-inventory-dry-run.ts` around lines 45 - 61, The returned arrays must be stably sorted to avoid snapshot diffs: after building the factSheets array in to-leanix-inventory-dry-run.ts (the const factSheets: LeanixFactSheetDryRun[] built from model.elements() using getFactSheetType, el.id, el.title, etc.), sort factSheets by likec4Id before returning; likewise, after building the relationships array (the array built from model.relationships() that includes likec4RelationId), sort relationships by likec4RelationId before returning so the output order is deterministic.packages/leanix-bridge/src/sync-to-leanix.spec.ts (1)
12-29: Fail the mock on unexpected GraphQL operations.Returning
{}for unknown queries makes the suite permissive: renamed or extra API calls are silently treated as'not found'. Throwing here keeps theplanSyncToLeanix()contract explicit.🔒 Tighten the mock
if ( query.includes('FindFactSheet') && typeof fsName === 'string' && typeof fsType === 'string' ) { const key = `${fsName}|${fsType}` const id = existingByKey.get(key) return { allFactSheets: { edges: id ? [{ node: { id, name: fsName, type: fsType } }] : [], }, } } - return {} + throw new Error(`Unexpected GraphQL query in test: ${query}`)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/leanix-bridge/src/sync-to-leanix.spec.ts` around lines 12 - 29, The graphql mock in the test (the async function named graphql) silently returns {} for unknown queries which hides unexpected GraphQL calls; update the mock so that after handling the expected FindFactSheet path (using variables['name'], variables['type'] and existingByKey) it throws an Error for any other query/operation (including unexpected names or shapes) with a descriptive message (e.g., include the query string or operation name) so unexpected/renamed API calls fail the test explicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 724-728: The code seeds idToFqn from v.likec4Id without validating
it; change the logic around bridgeId (v.likec4Id) so you only accept and add it
when it is a syntactically-valid dot-separated FQN and its parent prefix matches
the current parent chain for this vertex (i.e., verify each prefix segment
corresponds to an ancestor id/name in the current diagram structure), otherwise
ignore v.likec4Id and generate a new name as before; update idToFqn.set(v.id,
bridgeId) and usedNames.add(...) only after these validation checks (referencing
idToFqn, v.likec4Id, bridgeId, usedNames, and v.id to locate the edit).
- Around line 199-203: The fallback actor detector function
styleOrTagIndicatesActor currently only checks for 'shape=actor' and
'shape=person' but must also treat 'umlActor' as an actor; update
styleOrTagIndicatesActor (params: style, fullTagLower) to check for 'umlActor'
in the normalized style string and in fullTagLower so cells with tag 'umlActor'
are classified as actors when style is missing.
In `@packages/leanix-bridge/package.json`:
- Line 8: Update the package.json "description" field (currently "Bridge from
LikeC4 semantic model to LeanIX-shaped inventory artifacts (dry-run, no live
sync)") to reflect the new optional live sync capability introduced in this PR;
replace the “dry-run, no live sync” clause with wording such as “supports
optional live sync or dry-run modes” so the description accurately represents
current feature scope and avoids misleading consumers.
In `@packages/leanix-bridge/src/drawio-leanix-roundtrip.ts`:
- Around line 11-15: The exported DrawioLeanixMapping interface mislabels
likec4IdToLeanixFactSheetId as containing only LeanIX fact sheet IDs while code
falls back to externalId; update the API to be accurate by either (A) renaming
the property to something generic like likec4IdToLeanixId or
likec4IdToLeanixIdentity and updating docs/comments accordingly, or (B)
removing/avoiding the externalId fallback so the map truly contains only fact
sheet IDs; locate and change the identifier likec4IdToLeanixFactSheetId in the
DrawioLeanixMapping interface and any collectors/docs that reference it to keep
names and runtime values consistent.
In `@packages/leanix-bridge/src/leanix-api-client.ts`:
- Around line 67-73: Concurrent callers of graphql() can observe and update
lastRequestTime simultaneously, so serialize the rate-limiter check/sleep/update
with a short async lock: wrap the logic that reads elapsed, awaits
sleep(this.requestDelayMs - elapsed) when needed, and sets this.lastRequestTime
inside a critical section (use an async mutex/lock or a single Promise queue
field like a rateLimitLock) so only one caller performs the timing/sleep/update
at a time; keep references to graphql(), lastRequestTime, requestDelayMs and
sleep when implementing the lock.
- Around line 76-85: The graphql method currently calls fetch and res.json
without protection, so network failures or non-JSON responses can throw raw
errors; wrap the fetch(...) call and the subsequent res.json() parse in a
try-catch inside the graphql method and convert any thrown Error (e.g.,
TypeError, SyntaxError) into a LeanixApiError with contextual info (include
request url/method, HTTP status if available, and the original error/message as
inner details) so callers always receive LeanixApiError; also, when res.ok is
false or the body is not valid GraphQL JSON, construct and throw a
LeanixApiError containing the response status and text to aid debugging.
In `@packages/leanix-bridge/src/sync-to-leanix.ts`:
- Around line 11-16: The SyncToLeanixOptions.likec4IdAttribute is documented but
not used: update the idempotent lookup flow in the function that creates/merges
fact sheets to consult likec4IdAttribute when provided (pass it into or augment
findFactSheetByNameAndType or create a new findFactSheetByLikec4Id function) so
the idempotent branch checks for an existing fact sheet by that attribute first,
falls back to name+type, and when a match is found ensure you patch/backfill the
LeanIX fact sheet with the stable likec4Id to maintain the stable-ID contract
(refer to findFactSheetByNameAndType and the idempotent branch). Also stop
swallowing relation errors: change createRelation so it surfaces/throws errors
(or returns an error object) instead of returning null on catch, and update
syncRelationsToLeanix to record any failed relation creations into the shared
errors array (refer to createRelation and syncRelationsToLeanix) so relation
creation failures are surfaced in the overall sync result.
- Around line 169-193: The createRelation function currently swallows all
errors; remove the try-catch so any GraphQL/client exceptions from
client.graphql(...) propagate to the caller (syncRelationsToLeanix), and after
the graphql call validate that data.createRelation?.relation?.id exists—if not,
throw a clear Error mentioning createRelation and the source/target IDs—so
failures surface and are aggregated by syncRelationsToLeanix; reference the
createRelation function, LeanixApiClient usage, and syncRelationsToLeanix caller
to locate and update the logic.
In `@packages/leanix-bridge/src/to-leanix-inventory-dry-run.ts`:
- Around line 91-93: The dry-run currently sets mappingProfile to 'default' even
when a custom options.mapping was provided; change the logic that sets
mappingProfile (currently using mappingProfile = options.mappingProfile ??
'default') to prefer an explicit options.mappingProfile, but if that's undefined
and options.mapping is present treat the profile as 'custom' (i.e.,
mappingProfile = options.mappingProfile ?? (options.mapping ? 'custom' :
'default')), keeping existing mergeWithDefault(options.mapping) usage and the
generatedAt fallback intact.
---
Nitpick comments:
In `@packages/leanix-bridge/src/sync-to-leanix.spec.ts`:
- Around line 12-29: The graphql mock in the test (the async function named
graphql) silently returns {} for unknown queries which hides unexpected GraphQL
calls; update the mock so that after handling the expected FindFactSheet path
(using variables['name'], variables['type'] and existingByKey) it throws an
Error for any other query/operation (including unexpected names or shapes) with
a descriptive message (e.g., include the query string or operation name) so
unexpected/renamed API calls fail the test explicitly.
In `@packages/leanix-bridge/src/to-leanix-inventory-dry-run.ts`:
- Around line 45-61: The returned arrays must be stably sorted to avoid snapshot
diffs: after building the factSheets array in to-leanix-inventory-dry-run.ts
(the const factSheets: LeanixFactSheetDryRun[] built from model.elements() using
getFactSheetType, el.id, el.title, etc.), sort factSheets by likec4Id before
returning; likewise, after building the relationships array (the array built
from model.relationships() that includes likec4RelationId), sort relationships
by likec4RelationId before returning so the output order is deterministic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c7f814d7-53dd-4c8d-9048-b232c0438f83
⛔ Files ignored due to path filters (3)
packages/generators/src/drawio/__snapshots__/parse-drawio.spec.ts.snapis excluded by!**/*.snappackages/leanix-bridge/src/__snapshots__/bridge-artifacts.spec.ts.snapis excluded by!**/*.snappnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
.changeset/drawio-infer-shape-person-roundtrip.md.changeset/leanix-bridge-and-drawio-profile.md.changeset/leanix-sync-drawio-e2e.md.github/workflows/checks.yaml.gitignoree2e/src/likec4-cli-export-drawio.spec.tspackages/generators/src/drawio/generate-drawio.spec.tspackages/generators/src/drawio/generate-drawio.tspackages/generators/src/drawio/index.tspackages/generators/src/drawio/parse-drawio.spec.tspackages/generators/src/drawio/parse-drawio.tspackages/generators/src/index.tspackages/leanix-bridge/README.mdpackages/leanix-bridge/build.config.tspackages/leanix-bridge/package.jsonpackages/leanix-bridge/src/bridge-artifacts.spec.tspackages/leanix-bridge/src/contracts.tspackages/leanix-bridge/src/drawio-leanix-roundtrip.spec.tspackages/leanix-bridge/src/drawio-leanix-roundtrip.tspackages/leanix-bridge/src/fixture-model.tspackages/leanix-bridge/src/index.tspackages/leanix-bridge/src/leanix-api-client.tspackages/leanix-bridge/src/mapping.spec.tspackages/leanix-bridge/src/mapping.tspackages/leanix-bridge/src/model-input.tspackages/leanix-bridge/src/report.tspackages/leanix-bridge/src/sync-to-leanix.spec.tspackages/leanix-bridge/src/sync-to-leanix.tspackages/leanix-bridge/src/to-bridge-manifest.spec.tspackages/leanix-bridge/src/to-bridge-manifest.tspackages/leanix-bridge/src/to-leanix-inventory-dry-run.spec.tspackages/leanix-bridge/src/to-leanix-inventory-dry-run.tspackages/leanix-bridge/tsconfig.jsonpackages/leanix-bridge/vitest.config.tspackages/likec4/src/cli/export/drawio/handler.ts
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/leanix-bridge/src/sync-to-leanix.ts (1)
207-228:⚠️ Potential issue | 🟡 MinorSurface the missing-ID case as an error.
The try-catch from the previous version is removed (fixing API error propagation), but returning
nullwhen the mutation succeeds without an ID still causes silent skipping at line 324. A successful mutation that returns no ID is a server-side anomaly that should be surfaced.🛠️ Proposed fix: throw when mutation returns no ID
async function createRelation( client: LeanixApiClient, sourceFactSheetId: string, targetFactSheetId: string, relationType: string, _title?: string, -): Promise<string | null> { +): Promise<string> { type CreateResult = { createRelation?: { relation?: { id: string } } } const mutation = ` mutation CreateRelation($source: ID!, $target: ID!, $type: String!) { createRelation(source: $source, target: $target, type: $type) { relation { id } } } ` const data = await client.graphql<CreateResult>(mutation, { source: sourceFactSheetId, target: targetFactSheetId, type: relationType, }) - return data.createRelation?.relation?.id ?? null + const id = data.createRelation?.relation?.id + if (!id) { + throw new Error(`createRelation returned no ID for ${relationType} (${sourceFactSheetId} → ${targetFactSheetId})`) + } + return id }With this change, the existing try-catch in
syncRelationsToLeanix(line 332-334) will capture all failure modes including this edge case.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/leanix-bridge/src/sync-to-leanix.ts` around lines 207 - 228, createRelation currently returns null when the GraphQL mutation returns no relation id, which hides a server-side anomaly; update createRelation to throw an Error (including context like sourceFactSheetId, targetFactSheetId, relationType and any returned payload) when data.createRelation?.relation?.id is missing so callers (e.g., syncRelationsToLeanix) receive and handle the failure instead of silently skipping; ensure the thrown message is descriptive to aid debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/leanix-bridge/src/sync-to-leanix.ts`:
- Around line 106-109: The current toErrorMessage(err: unknown) returns
String(err) which yields "[object Object]" for plain objects; update
toErrorMessage to produce useful text by: if err is an Error return err.message
(and include err.stack optionally); else if err is a plain object attempt
JSON.stringify(err, null, 2) inside a try/catch to handle circular structures
and fall back to a safe inspect (or toString) when JSON fails; ensure the
function still handles primitives and null/undefined gracefully so callers of
toErrorMessage get a readable string for logging.
---
Duplicate comments:
In `@packages/leanix-bridge/src/sync-to-leanix.ts`:
- Around line 207-228: createRelation currently returns null when the GraphQL
mutation returns no relation id, which hides a server-side anomaly; update
createRelation to throw an Error (including context like sourceFactSheetId,
targetFactSheetId, relationType and any returned payload) when
data.createRelation?.relation?.id is missing so callers (e.g.,
syncRelationsToLeanix) receive and handle the failure instead of silently
skipping; ensure the thrown message is descriptive to aid debugging.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2c91d58d-60bd-4a72-90a0-30ee79c62432
📒 Files selected for processing (1)
packages/leanix-bridge/src/sync-to-leanix.ts
- to-leanix-inventory-dry-run: sort factSheets/relations by id, mappingProfile custom when options.mapping set - sync-to-leanix.spec: mock throws on unexpected GraphQL query - parse-drawio: validate likec4Id FQN and parent chain before use; add umlActor to actor detection - package.json description: optional live sync or dry-run modes - drawio-leanix-roundtrip: rename likec4IdToLeanixFactSheetId to likec4IdToLeanixId - leanix-api-client: rate-limit lock and LeanixApiError on fetch/json errors Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/generators/src/drawio/parse-drawio.ts (2)
186-197: Simplify nested ternary for readability.The static analysis flags the nested ternary at line 192 and negated conditions. Consider extracting to clearer logic:
♻️ Suggested refactor
function extractStyleFromTagContent(fullTag: string, maxScan = STYLE_VALUE_SCAN_CHARS): string | undefined { const scan = fullTag.slice(0, maxScan) const styleDq = scan.toLowerCase().indexOf('style="') const styleSq = scan.toLowerCase().indexOf("style='") - const useDq = styleDq !== -1 && (styleSq === -1 || styleDq <= styleSq) - const styleIdx = useDq ? styleDq : styleSq - const quote = styleIdx !== -1 ? (useDq ? '"' : "'") : '' - if (styleIdx === -1 || !quote) return undefined + const hasDq = styleDq !== -1 + const hasSq = styleSq !== -1 + if (!hasDq && !hasSq) return undefined + const useDq = hasDq && (!hasSq || styleDq <= styleSq) + const styleIdx = useDq ? styleDq : styleSq + const quote = useDq ? '"' : "'" const valueStart = styleIdx + 7 const valueEnd = scan.indexOf(quote, valueStart) return valueEnd !== -1 ? scan.slice(valueStart, valueEnd) : undefined }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/generators/src/drawio/parse-drawio.ts` around lines 186 - 197, In extractStyleFromTagContent, replace the nested ternary/negated logic that sets useDq, styleIdx and quote with clearer stepwise logic: compute scanLower once, get styleDq and styleSq, if both are -1 return undefined, otherwise choose the smaller non-negative index (styleIdx) and set quote to '"' if styleIdx === styleDq else "'" (or vice versa), then use valueStart = styleIdx + 7 and find valueEnd as before; update the function to use those explicit branches to improve readability and eliminate the nested ternary.
728-774: Cognitive complexity exceeds threshold (38 vs. 30 allowed).The function correctly validates bridge IDs and parent chains (addressing the previous review feedback), but its cognitive complexity is flagged by static analysis. Consider extracting the bridge ID validation into a separate helper:
♻️ Suggested extraction for clarity
+/** Try to assign bridge-managed FQN if valid and parent chain matches; returns true if assigned. */ +function tryAssignBridgeFqn( + v: DrawioCell, + idToFqn: Map<string, string>, + usedNames: Set<string>, + isRootParent: (parent: string | undefined) => boolean, +): boolean { + const bridgeId = v.likec4Id?.trim() + if (!bridgeId || !isValidFqn(bridgeId)) return false + const parentFqn = v.parent ? idToFqn.get(v.parent) : undefined + const useBridgeId = parentFqn === undefined + ? isRootParent(v.parent) + : bridgeId.startsWith(parentFqn + '.') && bridgeId.length > parentFqn.length + 1 + if (!useBridgeId) return false + idToFqn.set(v.id, bridgeId) + for (const segment of bridgeId.split('.')) usedNames.add(segment) + return true +} + function assignFqnsToElementVertices( idToFqn: Map<string, string>, elementVertices: DrawioCell[], containerIdToTitle: Map<string, string>, isRootParent: (parent: string | undefined) => boolean, uniqueName: (base: string) => string, usedNames: Set<string>, ): void { const baseName = (v: DrawioCell) => v.value ?? containerIdToTitle.get(v.id) ?? v.id const idToVertex = new Map(elementVertices.map(v => [v.id, v])) const depth = (v: DrawioCell): number => v.parent == null || !idToVertex.has(v.parent) ? 0 : 1 + depth(idToVertex.get(v.parent)!) const byDepth = [...elementVertices].sort((a, b) => depth(a) - depth(b)) - for (const v of byDepth) { - const bridgeId = v.likec4Id?.trim() - if (bridgeId && isValidFqn(bridgeId)) { - const parentFqn = v.parent ? idToFqn.get(v.parent) : undefined - const useBridgeId = - parentFqn === undefined - ? isRootParent(v.parent) - : bridgeId.startsWith(parentFqn + '.') && bridgeId.length > parentFqn.length + 1 - if (useBridgeId) { - idToFqn.set(v.id, bridgeId) - for (const segment of bridgeId.split('.')) usedNames.add(segment) - } - } - } + for (const v of byDepth) tryAssignBridgeFqn(v, idToFqn, usedNames, isRootParent) // ... rest unchanged }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/generators/src/drawio/parse-drawio.ts` around lines 728 - 774, The assignFqnsToElementVertices function is too complex; extract the bridge-ID and parent-chain logic into a small helper and also pull out the depth calculation to reduce nesting and conditionals: create a helper like isUsableBridgeId(bridgeId: string, v: DrawioCell, idToFqn: Map<string,string>, idToVertex: Map<string,DrawioCell>, isRootParent: (p?:string)=>boolean) that encapsulates the checks currently inside the first for-loop (including isValidFqn, computing parentFqn, and the useBridgeId logic), and another small helper depth(v: DrawioCell, idToVertex: Map<string,DrawioCell>) to replace the inline recursive depth; then call these helpers from assignFqnsToElementVertices (leaving the loop structure intact) to reduce cognitive complexity and improve readability while preserving behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/generators/src/drawio/parse-drawio.ts`:
- Around line 186-197: In extractStyleFromTagContent, replace the nested
ternary/negated logic that sets useDq, styleIdx and quote with clearer stepwise
logic: compute scanLower once, get styleDq and styleSq, if both are -1 return
undefined, otherwise choose the smaller non-negative index (styleIdx) and set
quote to '"' if styleIdx === styleDq else "'" (or vice versa), then use
valueStart = styleIdx + 7 and find valueEnd as before; update the function to
use those explicit branches to improve readability and eliminate the nested
ternary.
- Around line 728-774: The assignFqnsToElementVertices function is too complex;
extract the bridge-ID and parent-chain logic into a small helper and also pull
out the depth calculation to reduce nesting and conditionals: create a helper
like isUsableBridgeId(bridgeId: string, v: DrawioCell, idToFqn:
Map<string,string>, idToVertex: Map<string,DrawioCell>, isRootParent:
(p?:string)=>boolean) that encapsulates the checks currently inside the first
for-loop (including isValidFqn, computing parentFqn, and the useBridgeId logic),
and another small helper depth(v: DrawioCell, idToVertex:
Map<string,DrawioCell>) to replace the inline recursive depth; then call these
helpers from assignFqnsToElementVertices (leaving the loop structure intact) to
reduce cognitive complexity and improve readability while preserving behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6a4b4e74-4782-40d0-8e1a-db65febd2ee0
📒 Files selected for processing (8)
packages/generators/src/drawio/parse-drawio.tspackages/leanix-bridge/README.mdpackages/leanix-bridge/package.jsonpackages/leanix-bridge/src/drawio-leanix-roundtrip.spec.tspackages/leanix-bridge/src/drawio-leanix-roundtrip.tspackages/leanix-bridge/src/leanix-api-client.tspackages/leanix-bridge/src/sync-to-leanix.spec.tspackages/leanix-bridge/src/to-leanix-inventory-dry-run.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- packages/leanix-bridge/src/sync-to-leanix.spec.ts
- packages/leanix-bridge/src/leanix-api-client.ts
- packages/leanix-bridge/src/to-leanix-inventory-dry-run.ts
- packages/leanix-bridge/README.md
- packages/leanix-bridge/src/drawio-leanix-roundtrip.spec.ts
- packages/leanix-bridge/package.json
- packages/leanix-bridge/src/drawio-leanix-roundtrip.ts
…likec4IdAttribute fallback and backfill - toErrorMessage: Error with stack, objects via JSON.stringify(err,null,2), safe fallback - createRelation: throw when no relation id, syncRelationsToLeanix already records to errors - likec4IdAttribute: fallback to name+type when attribute lookup misses; backfill fact sheet with likec4Id via patchFactSheetAttribute when found by name+type; same fallback in planSyncToLeanix Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/leanix-bridge/src/sync-to-leanix.ts (1)
422-428: Plan always shows relations as 'create' regardless of manifest state.The
planSyncToLeanixfunction marks all relations withaction: 'create', butsyncRelationsToLeanixat line 362 skips creation whenrel.external?.[LEANIX_PROVIDER]?.relationIdalready exists in the manifest. This means the plan may overstate the number of relations to create if the manifest already contains LeanIX relation IDs from a previous sync.Consider either:
- Accepting a manifest parameter in
planSyncToLeanixto filter out already-synced relations, or- Documenting this behavior in the
SyncPlan.relationPlansJSDoc🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/leanix-bridge/src/sync-to-leanix.ts` around lines 422 - 428, planSyncToLeanix currently sets action: 'create' for every leanixDryRun.relations entry which overstates creates; change planSyncToLeanix signature to accept an optional manifest parameter and filter leanixDryRun.relations before building SyncPlan.relationPlans so any relation with rel.external?.[LEANIX_PROVIDER]?.relationId in the manifest is skipped (or marked action: 'none' if you prefer), i.e. update the mapping of leanixDryRun.relations in planSyncToLeanix to check the provided manifest for existing relation IDs and only include new relations in the produced SyncPlan.relationPlans; keep references to planSyncToLeanix, SyncPlan.relationPlans, leanixDryRun.relations, syncRelationsToLeanix and LEANIX_PROVIDER to locate and align behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/leanix-bridge/src/sync-to-leanix.ts`:
- Around line 422-428: planSyncToLeanix currently sets action: 'create' for
every leanixDryRun.relations entry which overstates creates; change
planSyncToLeanix signature to accept an optional manifest parameter and filter
leanixDryRun.relations before building SyncPlan.relationPlans so any relation
with rel.external?.[LEANIX_PROVIDER]?.relationId in the manifest is skipped (or
marked action: 'none' if you prefer), i.e. update the mapping of
leanixDryRun.relations in planSyncToLeanix to check the provided manifest for
existing relation IDs and only include new relations in the produced
SyncPlan.relationPlans; keep references to planSyncToLeanix,
SyncPlan.relationPlans, leanixDryRun.relations, syncRelationsToLeanix and
LEANIX_PROVIDER to locate and align behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e069cd8f-1eda-42b0-9ee4-c0b65936b0c2
📒 Files selected for processing (1)
packages/leanix-bridge/src/sync-to-leanix.ts
davydkov
left a comment
There was a problem hiding this comment.
Great contribution @sraphaz!
Can you also update https://likec4.dev/tooling/drawio?
Overview and intention
Goal: Enable LeanIX interoperability (inventory sync and diagram round-trip) while keeping LikeC4 as the single source of truth and Draw.io as a visual interchange format, not as the source of truth.
Strategy (summary): Use a bridge to LeanIX so enterprises can get corporate interoperability—including with AI and tooling that consume LeanIX—while LikeC4 remains the single source of truth. Architecture lives in the LikeC4 DSL; Draw.io and LeanIX are adapters for interchange and sync, not the canonical model.
.c4files, typed elements, views). Nothing here changes that.@likec4/leanix-bridge) produces identity manifests, LeanIX-shaped dry-run artifacts, an optional sync plan (what would be created/updated), and optional live sync to the LeanIX API. All LeanIX-specific behaviour stays outside@likec4/core.Differential:
planSyncToLeanix) let you see what would change in LeanIX before calling the API.How to use (high level): Define a custom generator that (1) exports views to Draw.io with
profile: 'leanix', (2) builds manifest + dry-run (+ optionally sync plan), and (3) optionally runs live sync. No new top-level CLI; everything is generator-driven. Seepackages/leanix-bridge/README.mdfor a full example.Summary (technical)
This PR has two main parts:
DrawIO export profile
leanixin@likec4/generators: when exporting to Draw.io, callers can passprofile: 'leanix'so that vertices, edges, and the diagram root get bridge-managed metadata (e.g.likec4Id,likec4Kind,likec4ViewId,likec4ProjectId,likec4RelationId,bridgeManaged, and optionallyleanixFactSheetType). That metadata allows round-trip and mapping to LeanIX fact sheets/relations after a sync.New package
@likec4/leanix-bridge: builds an identity manifest, LeanIX-shaped dry-run artifacts (fact sheets + relations), a sync plan (read-only query to LeanIX to see create vs update), optional live sync to the LeanIX API, and a Draw.io ↔ LeanIX mapping so that after sync you know which LeanIX fact sheet / relation corresponds to which LikeC4 element (for round-trip or tooling).LikeC4 stays the single source of truth. Draw.io is only an interchange format; LeanIX is a separate adapter.
What already existed (unchanged)
@likec4/generators: export of views to Draw.io XML and import from Draw.io back to LikeC4 source. No change to default behaviour.@likec4/configand the CLI: projects can define custom generators (e.g.likec4 gen my-gen).What's new
1. DrawIO export options (
@likec4/generators)profile: 'leanix'— Adds bridge-managed style attributes on vertices, edges, and root cell:bridgeManaged=truelikec4Id,likec4Kind,likec4ViewId,likec4ProjectId(on vertices/root)likec4RelationId,bridgeManaged(on edges)leanixFactSheetTypeByKindso vertices getleanixFactSheetType=<value>by element kind.How to use: When exporting (e.g. from CLI or custom generator), pass:
generateDrawio(viewmodel, { compressed: false, profile: 'leanix', projectId: 'my-project', leanixFactSheetTypeByKind: { system: 'Application', component: 'ITComponent' } })Default export (no
profileorprofile: 'default') is unchanged and does not add these attributes.2. Package
@likec4/leanix-bridgetoBridgeManifest(model, options)— canonical IDs (e.g. FQN, viewId, relationId) and placeholders for external IDs.toLeanixInventoryDryRun(model, options)— LeanIX-shaped fact sheets and relations (no API calls).toReport(manifest, dryRun)— summary of manifest and dry-run.planSyncToLeanix(dryRun, client, options)— queries LeanIX read-only and returns aSyncPlanartifact: per–fact sheet and per-relation actions (create vs update), summary counts. Use before live sync to review what would change.LeanixApiClient+syncToLeanix(manifest, dryRun, client, options)— creates/updates fact sheets and relations in LeanIX; returns manifest withexternal.leanix.factSheetIdetc.manifestToDrawioLeanixMapping(manifest)— after sync, returnslikec4IdToLeanixFactSheetIdandrelationKeyToLeanixRelationIdfor use in Draw.io round-trip or tooling.Usage is via a custom generator (see
packages/leanix-bridge/README.md): e.g.likec4 gen leanix-dry-runwrites manifest + dry-run + report toout/bridge/. Optionally runplanSyncToLeanixto produce a sync plan, then callsyncToLeanixfrom that generator or from a separate script; sync is not a default CLI command.Phase 2 implementation (complete)
planSyncToLeanix. Produces a dedicated artifact (e.g.sync-plan.json) describing what would be created vs updated in LeanIX, using read-only API queries (find by name+type). No writes until you callsyncToLeanix.syncToLeanix(manifest, dryRun, client, options); used from custom generator or script only. Idempotent by default (find existing fact sheets by name+type before creating).How this is used with LeanIX (end-to-end)
.c4model and views).profile: 'leanix'so the diagram carrieslikec4Id,likec4RelationId, etc., in cell styles.toBridgeManifest,toLeanixInventoryDryRun).planSyncToLeanix(dryRun, client)to get a plan (create/update counts and per-item actions); review before syncing.syncToLeanix(...)so fact sheets and relations exist in LeanIX; manifest is updated with LeanIX IDs.manifestToDrawioLeanixMapping(manifest)links LikeC4 IDs to LeanIX IDs for round-trip or integration (e.g. re-import from LeanIX or other tools).Draw.io is never the source of truth; it is an interchange format that can carry bridge metadata for LeanIX interoperability.
Strategy (for maintainers)
@likec4/core.profile: 'leanix'+ optionalprojectId,leanixFactSheetTypeByKind). Default remains round-trip only.@likec4/leanix-bridge. Adoption is opt-in via custom generators and export options.This keeps the core clean and allows enterprises to plug in LeanIX without forcing upstream to own LeanIX semantics.
Tests and CI
packages/generators/src/drawio/cover default export and theleanixprofile (bridge-managed metadata on vertices, edges, root; optionalleanixFactSheetType; actor export/round-trip).packages/leanix-bridgecover manifest, dry-run, sync plan (planSyncToLeanix), and mapping.pnpm-lock.yamlwas updated to fix the missingobuildentry that caused CI failure.CI and compatibility fixes (in this PR)
These changes were made so the PR passes upstream CI (typecheck, lint, tests, e2e) without changing feature behaviour:
DrawIO parse-drawio spec (
packages/generators/src/drawio/parse-drawio.spec.ts): Tests now import only from source (./parse-drawio), not from the built dist. That waypnpm ci:typecheckpasses in CI (where typecheck runs without a prior build). Aligns with other specs in the package (e.g. generate-puml, generate-d2).Actor / container / person (shape=actor) — Draw.io round-trip: The parser infers
shape=actor→actorwithshape personwhen re-exporting. In the parse spec, the test “vertex with shape=actor emits element with User” is environment-agnostic: it accepts eitheractor 'User'withshape person(e.g. when running against built output) orcontainer 'User'(e.g. when running against source), so the same test passes locally and in CI. Seeparse-drawio.spec.tsand the generator’sinferKind/ actor handling inparse-drawio.tsandgenerate-drawio.ts.LeanIX sync spec (
packages/leanix-bridge/src/sync-to-leanix.spec.ts): Satisfies oxlint no-base-to-string by narrowing GraphQL mock variables withtypeof fsName === 'string' && typeof fsType === 'string'before use, and usingString(fs.name)in error paths insync-to-leanix.ts.E2E (
e2e/src/likec4-cli-export-drawio.spec.ts): (1) Use zx’s$({ cwd: projectRoot })API instead of.cwd()onProcessPromise(which is not available in zx). (2) The “export drawio --profile leanix” test uses--uncompressedso the diagram XML is written in plain text and the assertiontoContain('bridgeManaged=true')succeeds (with default compression the string is inside the compressed payload).Summary by CodeRabbit