Bookmark Management
The BookmarkService in etchblok-test-api acts as the central orchestrator for all bookmark, tag, and collection operations. It provides a high-level facade that coordinates data persistence via the BookmarkRepository, full-text search through the SearchIndex, and performance optimization using an LRUCache.
Centralized Bookmark Operations
When you interact with bookmarks in etchblok-test-api, the BookmarkService ensures that business rules—such as URL validation and cache consistency—are applied uniformly. For example, in app/routes/bookmarks.py, the route handlers delegate directly to the service rather than manipulating models or the database themselves.
Creating and Updating Bookmarks
To create a bookmark, you pass a dictionary of data to create_bookmark. The service validates the input before persisting it.
# Example usage in app/routes/bookmarks.py
@bookmarks_bp.route("/", methods=["POST"])
def create_bookmark():
data = request.get_json(force=True)
bookmark, error = _service.create_bookmark(data)
if error:
return jsonify({"error": error}), 400
return jsonify(bookmark.to_dict()), 201
Internally, create_bookmark performs several steps:
- Validation: It calls
_validate_urland_validate_titleto ensure the data is well-formed. - Persistence: It saves the
Bookmarkobject viaself._repo.save_bookmark(bookmark). - Indexing: It immediately updates the search index using
self._search.index_bookmark(bookmark). - Cache Invalidation: It clears any existing cache entry for that ID via
self._cache.invalidate(bookmark.id).
Updates follow a similar pattern in update_bookmark, where the service performs partial updates, re-validates changed fields, and refreshes both the search index and the cache.
Lifecycle Management: Soft-Deletes and Archiving
etchblok-test-api distinguishes between "trashing" a bookmark and "archiving" it.
- Soft-Delete: The
delete_bookmarkmethod does not remove the record from the database. Instead, it callsbookmark.trash(), which marks it as trashed. - Archiving: The
archive_bookmarkmethod moves a bookmark to an archived state. - Restoration: The
restore_bookmarkmethod can bring a bookmark back to active status from either the trash or the archive.
In all these cases, the service ensures the LRUCache is invalidated so that subsequent fetches reflect the new status.
Tag Management and Cascading Updates
The BookmarkService handles the complex relationship between tags and bookmarks. While tags are independent entities, deleting one has side effects on the bookmarks that use it.
When you call delete_tag(tag_id), the service performs a cascading cleanup:
def delete_tag(self, tag_id: str) -> bool:
tag = self._repo.get_tag(tag_id)
if not tag:
return False
# Remove the tag reference from every bookmark that uses it
for bookmark in self._repo.get_bookmarks_with_tag(tag_id):
bookmark.remove_tag(tag_id)
self._repo.save_bookmark(bookmark)
self._cache.invalidate(bookmark.id)
# Finally, delete the tag itself
self._repo.delete_tag(tag_id)
return True
This logic ensures that no bookmark is left with a "dangling" reference to a non-existent tag.
Organizing with Collections
Collections allow you to group bookmarks. The BookmarkService provides methods to manage these groups and their memberships.
create_collection(data): Validates that a name is provided before saving.add_to_collection(collection_id, bookmark_id): Retrieves the collection and uses its internaladd_bookmarklogic before persisting the change.remove_from_collection(collection_id, bookmark_id): Similarly manages the removal of a bookmark from a collection.
Performance and Search
To maintain responsiveness, BookmarkService integrates caching and specialized search indexing.
LRU Caching
The service maintains an internal LRUCache with a default max_size of 256. When you call get_bookmark(bookmark_id), the service checks the cache first:
def get_bookmark(self, bookmark_id: str) -> Optional[Bookmark]:
cached = self._cache.get(bookmark_id)
if cached is not None:
return cached
bookmark = self._repo.get_bookmark(bookmark_id)
if bookmark:
self._cache.put(bookmark.id, bookmark)
return bookmark
This reduces database load for frequently accessed bookmarks. The service manually invalidates cache entries during any write operation (update, delete, archive, etc.) to prevent stale data.
Full-Text Search
The search(query, limit) method delegates to the SearchIndex. By default, search results are limited to 20 items. The search index is updated in real-time whenever a bookmark is created or updated through the service.
Internal Architecture
Singleton Pattern
BookmarkService is implemented as a singleton using the __new__ method. This ensures that the cache and search index are shared across all parts of the etchblok-test-api application, such as different Flask blueprints.
_instance: Optional["BookmarkService"] = None
def __new__(cls) -> "BookmarkService":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._init_services()
return cls._instance
Service Initialization
The _init_services method bootstraps the internal components. It instantiates the BookmarkRepository, the LRUCache, and the SearchIndex. For testing purposes, the _reset() method can be used to re-initialize these components and clear the state.