Domain Models
Domain models in etchblok-test-api serve as the core data structures and business logic containers for the application. By using Python dataclasses, the project ensures that entities like bookmarks, tags, and collections have a consistent structure while providing built-in methods for state transitions and serialization.
Bookmarks and Lifecycle Management
The Bookmark class in app.models.bookmark is the central entity of the system. It represents a saved URL along with its metadata, such as title, description, and associated tags.
State Transitions
Bookmarks follow a specific lifecycle managed by the BookmarkStatus enum. Instead of hard-deleting records, etchblok-test-api uses a soft-delete pattern where bookmarks are moved between statuses:
- ACTIVE: The default state for new bookmarks.
- ARCHIVED: Bookmarks moved out of the main view but preserved for reference.
- TRASHED: Bookmarks marked for deletion.
These transitions are handled by methods that also update the updated_at timestamp via an internal _touch() helper:
def archive(self) -> None:
"""Move the bookmark to the archive."""
self.status = BookmarkStatus.ARCHIVED
self._touch()
def trash(self) -> None:
"""Soft-delete the bookmark by moving it to the trash."""
self.status = BookmarkStatus.TRASHED
self._touch()
Tag Association
Bookmarks maintain a list of tag IDs. The add_tag and remove_tag methods ensure that duplicate tags are not added and that the modification timestamp is updated whenever the association changes.
Tags and Metadata
The Tag class in app.models.tag provides a way to categorize bookmarks. Beyond a simple name, tags include a TagColor enum for UI rendering and a usage_count to track how many bookmarks are currently associated with the tag.
Usage Tracking
The usage_count is not automatically calculated by a database query; instead, it is incremented or decremented by the service layer when bookmarks are tagged or untagged:
def increment_usage(self) -> int:
"""Record that a bookmark now uses this tag. Returns new count."""
self.usage_count += 1
return self.usage_count
Reserved Names and Validation
To prevent conflicts with system-generated views, etchblok-test-api enforces a set of reserved tag names defined in app/models/_validators.py. Users cannot create tags named all, untagged, archived, or trash. Additionally, tag names are limited to 50 characters and cannot be empty.
Collections: Manual vs. Smart
The Collection class in app.models.collection allows users to group bookmarks. The system supports two distinct types of collections defined by CollectionType:
- Manual Collections: Users explicitly add or remove bookmark IDs using
add_bookmarkandremove_bookmark. These collections also support custom ordering via thereordermethod. - Smart Collections: These are dynamic groups defined by a
filter_rule.
Smart Collection Filtering
Smart collections use a naive keyword search implemented in the _apply_filter method. This method checks if the filter_rule (a string) exists within the bookmark's title or description:
def _apply_filter(self, bookmarks: list) -> List[str]:
if not self.filter_rule:
return []
keyword = self.filter_rule.lower()
return [b.id for b in bookmarks if keyword in b.title.lower() or keyword in b.description.lower()]
Data Integrity and Serialization
Every domain model in etchblok-test-api implements a standard interface for serialization to ensure compatibility with the API layer and persistence mechanisms.
Serialization Patterns
Models include to_dict() for converting instances into JSON-safe dictionaries and a from_dict() class method for instantiation from raw data.
- ID Generation: Each model generates a unique identifier upon instantiation using truncated UUIDs. Bookmarks use 12 characters, Tags use 8, and Collections use 10.
- Validation: While models handle basic state logic, complex validation (like URL regex matching or length checks) is delegated to internal helpers in
app/models/_validators.py. For example,_validate_urlensures that bookmarked URLs follow a standardhttp://orhttps://format.
Integration with Services
The BookmarkService in app/services/bookmark_service.py orchestrates these models. When a new bookmark is created, the service uses Bookmark.from_dict(data) to create the instance before passing it to the repository for storage. This separation ensures that the domain models remain focused on data structure and internal state, while the service layer handles external dependencies like caching and search indexing.