Skip to main content

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_bookmark or delete_tag do 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 Tag is deleted, the BookmarkService must 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_bookmarks method 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.