Data Access Layer
The Data Access Layer in etchblok-test-api provides a centralized abstraction for persisting and retrieving bookmarks, tags, and collections. While the current implementation is entirely in-memory, it follows the Repository pattern to decouple business logic from the underlying storage mechanism.
The Bookmark Repository
The BookmarkRepository class in app.db.repository serves as the primary data access object. It maintains internal dictionaries for each entity type and provides methods for CRUD operations, pagination, and filtering.
Managing Entities
You interact with the repository by passing model instances (like Bookmark, Tag, or Collection) to its save methods. Because the storage is in-memory, these operations persist immediately to the internal state.
from app.db.repository import BookmarkRepository
from app.models.bookmark import Bookmark
repo = BookmarkRepository()
# Create and save a bookmark
new_bookmark = Bookmark(url="https://example.com", title="Example")
repo.save_bookmark(new_bookmark)
# Retrieve by ID
bookmark = repo.get_bookmark(new_bookmark.id)
Internally, BookmarkRepository uses standard Python dictionaries to store these objects:
def __init__(self) -> None:
self._bookmarks: Dict[str, Bookmark] = {}
self._tags: Dict[str, Tag] = {}
self._collections: Dict[str, Collection] = {}
Pagination and Filtering
When you need to display lists of bookmarks, the repository provides a list_bookmarks method that handles both pagination and status-based filtering (e.g., "active", "archived", or "trashed").
# Get the first page of active bookmarks, 10 per page
bookmarks, total_count = repo.list_bookmarks(page=1, per_page=10, status="active")
The implementation performs an in-memory sort by created_at in descending order before slicing the results:
def list_bookmarks(self, page: int = 1, per_page: int = 25, status: Optional[str] = None) -> Tuple[List[Bookmark], int]:
items = list(self._bookmarks.values())
if status:
# Filters by BookmarkStatus enum
target = BookmarkStatus(status)
items = [b for b in items if b.status == target]
items.sort(key=lambda b: b.created_at, reverse=True)
total = len(items)
start = (page - 1) * per_page
return items[start : start + per_page], total
Integration with Services
The repository is rarely called directly by route handlers. Instead, it is injected into services that coordinate higher-level business logic.
BookmarkService Facade
The BookmarkService (found in app/services/bookmark_service.py) acts as a facade over the repository. It ensures that when a bookmark is saved, it is also added to the search index and the cache is invalidated.
def create_bookmark(self, data: Dict[str, Any]) -> Tuple[Optional[Bookmark], Optional[str]]:
bookmark = Bookmark.from_dict(data)
self._repo.save_bookmark(bookmark) # Persist to repository
self._search.index_bookmark(bookmark) # Update search index
self._cache.invalidate(bookmark.id) # Clear cache
return bookmark, None
Search Index Hydration
The SearchIndex in app/services/search_service.py depends on the repository to hydrate search results. When a search is performed, the index identifies matching IDs, and the repository is used to fetch the full Bookmark objects.
def search(self, query: str, limit: int = 20) -> List[Bookmark]:
# ... tokenization and ID lookup ...
results = []
for bid in candidate_ids:
bookmark = self._repo.get_bookmark(bid)
if bookmark:
results.append(bookmark)
return self._rank_results(results, tokens)[:limit]
Data Integrity and Limitations
Because etchblok-test-api uses an in-memory repository, there are several architectural behaviors to keep in mind:
- Volatility: All data is lost when the application process restarts. The repository does not currently write to disk.
- No Transactions: Mutation methods like
save_bookmarkordelete_tagdo not support atomic transactions. If a multi-step operation fails halfway through, the repository may be left in a partially updated state. - Manual Referential Integrity: The repository does not handle cascading deletes. For example, when a
Tagis deleted, theBookmarkServicemust manually iterate through all bookmarks to remove the tag reference:
# From BookmarkService.delete_tag in app/services/bookmark_service.py
for bookmark in self._repo.get_bookmarks_with_tag(tag_id):
bookmark.remove_tag(tag_id)
self._repo.save_bookmark(bookmark)
self._repo.delete_tag(tag_id)
- Performance: The
list_bookmarksmethod sorts the entire collection of bookmarks on every request. While efficient for small datasets, this will become a bottleneck as the number of bookmarks grows.