feat: expose external_id in EntityResponse and link resolver (#569)

Signed-off-by: Drew Cain <groksrc@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Drew Cain
2026-02-15 20:33:37 -06:00
committed by GitHub
parent 113d1b6f1b
commit 6afe4fd0cc
3 changed files with 57 additions and 0 deletions
+1
View File
@@ -195,6 +195,7 @@ class EntityResponse(SQLAlchemyModel):
entity_metadata: Optional[Dict] = None
checksum: Optional[str] = None
content_type: ContentType
external_id: Optional[str] = None
observations: List[ObservationResponse] = []
relations: List[RelationResponse] = []
created_at: datetime
@@ -1,5 +1,6 @@
"""Service for resolving markdown links to permalinks."""
import uuid as uuid_mod
from typing import Optional, Tuple, Dict
from loguru import logger
@@ -63,6 +64,19 @@ class LinkResolver:
explicit_project_reference = "::" in clean_text
clean_text = normalize_project_reference(clean_text)
# --- External ID Resolution ---
# Try external_id first if identifier looks like a UUID.
# Canonicalize to lowercase-hyphen form so uppercase or unhyphenated
# UUIDs also match the stored external_id values.
try:
canonical_id = str(uuid_mod.UUID(clean_text))
entity = await self.entity_repository.get_by_external_id(canonical_id)
if entity:
logger.debug(f"Found entity by external_id: {entity.permalink}")
return entity
except ValueError:
pass
# Trigger: link uses project namespace syntax (project::note)
# Why: treat it as an explicit cross-project reference
# Outcome: resolve only within the referenced project scope
+42
View File
@@ -1,5 +1,6 @@
"""Tests for link resolution service."""
import uuid
from datetime import datetime, timezone
import pytest
@@ -857,3 +858,44 @@ async def test_simple_link_no_slash_skips_relative_resolution(relative_path_reso
# testing/nested/ is not the same folder as testing/, but it's closer than nested/
# The context-aware resolution will pick the closest match
assert result.file_path == "testing/nested/deep-note.md"
# ============================================================================
# External ID (UUID) resolution tests
# ============================================================================
@pytest.mark.asyncio
async def test_resolve_link_by_external_id(link_resolver, test_entities):
"""Test resolving a link using a valid external_id (UUID)."""
entity = test_entities[0]
result = await link_resolver.resolve_link(entity.external_id)
assert result is not None
assert result.id == entity.id
assert result.external_id == entity.external_id
@pytest.mark.asyncio
async def test_resolve_link_by_external_id_uppercase(link_resolver, test_entities):
"""Test that uppercase UUID is canonicalized and resolves correctly."""
entity = test_entities[0]
upper_id = entity.external_id.upper()
result = await link_resolver.resolve_link(upper_id)
assert result is not None
assert result.id == entity.id
@pytest.mark.asyncio
async def test_resolve_link_by_external_id_nonexistent(link_resolver):
"""Test that a valid UUID format that doesn't match any entity returns None."""
fake_id = str(uuid.uuid4())
result = await link_resolver.resolve_link(fake_id)
assert result is None
@pytest.mark.asyncio
async def test_resolve_link_non_uuid_falls_through(link_resolver, test_entities, project_prefix):
"""Test that non-UUID strings skip UUID resolution and use normal lookup."""
result = await link_resolver.resolve_link("Core Service")
assert result is not None
assert result.permalink == f"{project_prefix}/components/core-service"