mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix: fix permalink uniqueness violations on create/update/sync
This commit is contained in:
@@ -181,6 +181,9 @@ class Entity(BaseModel):
|
||||
- Optional relations to other entities
|
||||
- Optional description for high-level overview
|
||||
"""
|
||||
|
||||
# private field to override permalink
|
||||
_permalink: Optional[str] = None
|
||||
|
||||
title: str
|
||||
content: Optional[str] = None
|
||||
@@ -199,8 +202,8 @@ class Entity(BaseModel):
|
||||
|
||||
@property
|
||||
def permalink(self) -> PathId:
|
||||
"""Get the path ID in format {snake_case_title}."""
|
||||
return generate_permalink(self.file_path)
|
||||
"""Get a url friendly path}."""
|
||||
return self._permalink or generate_permalink(self.file_path)
|
||||
|
||||
@model_validator(mode="after")
|
||||
@classmethod
|
||||
|
||||
@@ -19,6 +19,7 @@ from basic_memory.services import FileService
|
||||
from basic_memory.services import BaseService
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
class EntityService(BaseService[EntityModel]):
|
||||
@@ -40,6 +41,40 @@ class EntityService(BaseService[EntityModel]):
|
||||
self.file_service = file_service
|
||||
self.link_resolver = link_resolver
|
||||
|
||||
async def resolve_permalink(
|
||||
self,
|
||||
file_path: Path,
|
||||
markdown: Optional[EntityMarkdown] = None
|
||||
) -> str:
|
||||
"""Get or generate unique permalink for an entity.
|
||||
|
||||
Priority:
|
||||
1. Use explicit permalink from markdown frontmatter if present
|
||||
2. For existing files, keep current permalink
|
||||
3. Generate new unique permalink for new files
|
||||
"""
|
||||
# If markdown has explicit permalink, try to use it
|
||||
if markdown and markdown.frontmatter.permalink:
|
||||
desired_permalink = markdown.frontmatter.permalink
|
||||
else:
|
||||
# For existing files, try to find current permalink
|
||||
existing = await self.repository.get_by_file_path(str(file_path))
|
||||
if existing:
|
||||
return existing.permalink
|
||||
|
||||
# New file - generate permalink
|
||||
desired_permalink = generate_permalink(file_path)
|
||||
|
||||
# Make unique if needed
|
||||
permalink = desired_permalink
|
||||
suffix = 1
|
||||
while await self.repository.get_by_permalink(permalink):
|
||||
permalink = f"{desired_permalink}-{suffix}"
|
||||
suffix += 1
|
||||
logger.debug(f"creating unique permalink: {permalink}")
|
||||
|
||||
return permalink
|
||||
|
||||
async def create_or_update_entity(self, schema: EntitySchema) -> (EntityModel, bool):
|
||||
"""Create new entity or update existing one.
|
||||
if a new entity is created, the return value is (entity, True)
|
||||
@@ -66,9 +101,13 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
if await self.file_service.exists(file_path):
|
||||
raise EntityCreationError(
|
||||
f"file_path {file_path} for entity {schema.permalink} already exists: {file_path}"
|
||||
f"file for entity {schema.folder}/{schema.title} already exists: {file_path}"
|
||||
)
|
||||
|
||||
# Get unique permalink
|
||||
permalink = await self.resolve_permalink(schema.permalink or file_path)
|
||||
schema._permalink = permalink
|
||||
|
||||
post = await schema_to_markdown(schema)
|
||||
|
||||
# write file
|
||||
@@ -184,7 +223,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
Creates the entity with null checksum to indicate sync not complete.
|
||||
Relations will be added in second pass.
|
||||
"""
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title}")
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title}")
|
||||
model = entity_model_from_markdown(file_path, markdown)
|
||||
|
||||
# Mark as incomplete sync
|
||||
|
||||
@@ -35,7 +35,6 @@ class FileService:
|
||||
"""Generate absolute filesystem path for entity."""
|
||||
return self.base_path / f"{entity.file_path}"
|
||||
|
||||
# TODO move to tests
|
||||
async def write_entity_file(
|
||||
self,
|
||||
entity: EntityModel,
|
||||
|
||||
@@ -92,8 +92,28 @@ class SyncService:
|
||||
# First pass: Create/update entities
|
||||
# entities will have a null checksum to indicate they are not complete
|
||||
for file_path, entity_markdown in parsed_entities.items():
|
||||
|
||||
# Get unique permalink and update markdown if needed
|
||||
permalink = await self.entity_service.resolve_permalink(
|
||||
file_path,
|
||||
markdown=entity_markdown
|
||||
)
|
||||
|
||||
if permalink != entity_markdown.frontmatter.permalink:
|
||||
# Permalink changed - update markdown and rewrite file
|
||||
entity_markdown.frontmatter.metadata["permalink"] = permalink
|
||||
|
||||
# update file
|
||||
logger.info(f"Adding permalink '{permalink}' to file: {file_path}")
|
||||
updated_checksum = await self.entity_service.file_service.markdown_processor.write_file(
|
||||
directory / file_path, entity_markdown)
|
||||
|
||||
# Update checksum in changes report since file was modified
|
||||
changes.checksums[file_path] = updated_checksum
|
||||
|
||||
# if the file is new, create an entity
|
||||
if file_path in changes.new:
|
||||
# Create entity with final permalink
|
||||
logger.debug(f"Creating new entity_markdown: {file_path}")
|
||||
await self.entity_service.create_entity_from_markdown(
|
||||
file_path, entity_markdown
|
||||
|
||||
Reference in New Issue
Block a user