Entity Repository Architecture¶
The entity repository provides a unified API for entity CRUD operations, enabling consistent data access across UI, MCP, and external integrations.
Overview¶
graph TB
subgraph Consumers
UI[Web UI]
MCP[MCP Server]
Hub[metaseed-hub]
end
subgraph Service["Entity Service"]
ES[EntityService]
end
subgraph Repositories["Repository Implementations"]
MER[MemoryEntityRepository]
FER[FileEntityRepository]
DBR[DatabaseRepository - Future]
end
subgraph Storage["Storage Backends"]
MEM[In-Memory - AppState]
FILE[JSON Files]
DB[Database - Future]
end
UI --> ES
MCP --> ES
Hub --> ES
ES --> MER
ES --> FER
ES --> DBR
MER --> MEM
FER --> FILE
DBR --> DB
Components¶
| Component | Location | Responsibility |
|---|---|---|
| EntityRepository | metaseed.repositories.base |
Abstract interface for entity storage |
| EntityData | metaseed.repositories.base |
Transfer object with hierarchy info |
| FileEntityRepository | metaseed.repositories.file |
JSON file-based persistence |
| MemoryEntityRepository | metaseed.repositories.memory |
Wraps in-memory AppState as a repository |
| EntityService | metaseed.ui.services.entities |
Business logic layer |
EntityRepository Interface¶
The EntityRepository ABC defines the contract for entity persistence:
from metaseed.repositories import EntityRepository, EntityData
class EntityRepository(ABC):
def list_entities(self, entity_type: str | None = None) -> list[EntityData]: ...
def get_entity(self, entity_id: str) -> EntityData | None: ...
def create_entity(self, entity_type: str, data: dict, parent_id: str | None = None) -> EntityData: ...
def update_entity(self, entity_id: str, data: dict) -> EntityData: ...
def delete_entity(self, entity_id: str) -> bool: ...
def get_tree(self) -> list[EntityData]: ...
def get_profile(self) -> str: ...
def get_version(self) -> str | None: ...
def set_profile(self, profile: str, version: str | None = None) -> None: ...
EntityData¶
The EntityData dataclass is the transfer object between repository and consumers:
@dataclass
class EntityData:
id: str
entity_type: str
label: str
data: dict[str, Any]
parent_id: str | None = None
children: list[EntityData] = field(default_factory=list)
Usage Patterns¶
UI with AppState¶
The UI uses MemoryEntityRepository to wrap the in-memory AppState:
from metaseed.ui.state import AppState
from metaseed.repositories import MemoryEntityRepository
from metaseed.ui.services.entities import EntityService
state = AppState(profile="miappe")
repo = MemoryEntityRepository(state)
service = EntityService(repo)
# Create entity
result = service.create_entity("Investigation", {"title": "My Study"})
MCP with File Repository¶
The MCP server can use FileEntityRepository for file-based state sharing:
from metaseed.repositories import FileEntityRepository
from metaseed.ui.services.entities import EntityService
repo = FileEntityRepository.from_dataset_name("my-dataset")
service = EntityService(repo)
# Both MCP and UI can read/write to the same file
entity = service.get_entity("abc123")
External Integration¶
For metaseed-hub or other integrations:
from metaseed.repositories import FileEntityRepository
# Point to shared dataset location
repo = FileEntityRepository(
dataset_path=Path("/shared/data/project.json"),
profile="miappe",
version="1.2",
)
# Direct repository access
entities = repo.list_entities("Investigation")
tree = repo.get_tree()
File Format¶
FileEntityRepository uses JSON with hierarchy metadata:
{
"profile": "miappe",
"version": "1.2",
"modified": "2024-01-15T10:30:00",
"entities": [
{
"id": "abc123",
"entity_type": "Investigation",
"label": "My Investigation",
"parent_id": null,
"unique_id": "INV-001",
"title": "My Investigation"
},
{
"id": "def456",
"entity_type": "Study",
"label": "Study One",
"parent_id": "abc123",
"unique_id": "STU-001",
"title": "Study One",
"investigation_id": "INV-001"
}
]
}
Tree Hierarchy¶
Entities form a tree structure managed by TreeNode parent-child relationships:
Investigation (root)
├── Study (parent_id -> Investigation)
│ ├── ObservationUnit (parent_id -> Study)
│ └── BiologicalMaterial (parent_id -> Study)
└── Person (parent_id -> Investigation)
The hierarchy is separate from Pydantic model nested fields, which contain string references to related entities.
State Synchronization¶
For multi-process scenarios (UI + MCP):
- Both processes use
FileEntityRepositorypointing to the same dataset file - Call
repo.reload()to sync with external changes - WebSocket notifications signal when changes occur
# In MCP after making changes
repo._save() # Write to file
# In UI to pick up changes
repo.reload() # Read from file
When the file cannot be read¶
FileEntityRepository reads the dataset file when it is constructed. A read that fails — a truncated file, a permission error, a half-written save — leaves the repository with no entities, which is indistinguishable from a dataset that is genuinely empty. Writing that state back would replace the user's records with nothing, so the repository does not:
- A failed load is recorded. Every subsequent save refuses and raises
DatasetLoadFailedError, naming the file and the original error. reload()clears the condition once a read succeeds, so a transient failure (a concurrent writer, a busy filesystem) is recoverable without constructing a new repository.- Reads still work and return what was loaded, which is nothing. The refusal is on the write path, where the damage would be permanent.
An unreadable file is therefore reported, never silently treated as an empty dataset.
Saves are atomic: the repository writes a temporary file beside the target and renames it into place. An interrupted save leaves the previous file intact rather than a truncated one — which matters here in particular, because a truncated file is exactly what makes the next load fail.
Service Operations¶
All entity operations are methods on EntityService; there are no module-level
functions. Construct a service with a repository and call its methods:
from metaseed.ui.state import AppState
from metaseed.repositories import MemoryEntityRepository
from metaseed.ui.services.entities import EntityService
service = EntityService(MemoryEntityRepository(AppState(profile="miappe")))
result = service.create_entity("Investigation", {"title": "Test"})
entities = service.list_entities() # all entities, grouped by type
tree = service.get_tree() # nested tree structure
service.update_entity(result["id"], {"title": "Renamed"})
service.delete_entity(result["id"])
Design Principles¶
The repository layer, shared by both the entity and dataset repositories, follows these principles:
- Dependency Injection: Repository passed to the service/manager, not global state
- Interface Segregation: Small, focused repository interfaces
- Single Responsibility: Service handles business logic, repository handles persistence
- Open/Closed: New backends added without modifying existing code