Domain Models
Domain models in this application are implemented as Python dataclasses located in the app/models/ directory. These models serve as the core business entities, encapsulating both the state of the application and the logic for valid state transitions.
The system revolves around three primary entities: Bookmarks, Tags, and Collections.
The Bookmark Entity
The Bookmark class in app/models/bookmark.py is the central entity. It represents a saved URL along with its associated metadata and organizational state.
Lifecycle and State Transitions
A bookmark's visibility is managed through the BookmarkStatus enum, which defines three states: ACTIVE, ARCHIVED, and TRASHED. The entity provides explicit methods to transition between these states, ensuring that the updated_at timestamp is refreshed via the internal _touch() method.
# From app/models/bookmark.py
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()
def restore(self) -> None:
"""Restore a trashed or archived bookmark to active status."""
self.status = BookmarkStatus.ACTIVE
self._touch()
Identity and Metadata
- ID Generation: Bookmarks use a truncated UUID for identification, generated as
uuid.uuid4().hex[:12]. - Metadata: The
metadatafield is a dictionary used for storing arbitrary key/value pairs, allowing the model to be extended without schema changes. - Tags: The
tagsattribute stores a list of tag IDs. The model providesadd_tagandremove_tagmethods to manage these associations safely.
Tagging System
The Tag class in app/models/tag.py provides a labeling mechanism for bookmarks. Unlike simple strings, tags are full entities with their own identity and properties.
Properties and Usage
- Visuals: Each tag has a
colorproperty defined by theTagColorenum (e.g.,RED,BLUE,GREEN). - Usage Tracking: The model tracks how many bookmarks are currently using it via
usage_count. This is updated usingincrement_usage()anddecrement_usage()methods. - Validation: The
rename()method enforces domain rules, such as preventing empty names or names exceeding 50 characters.
# From app/models/tag.py
def rename(self, new_name: str) -> None:
new_name = new_name.strip()
if not new_name:
raise ValueError("Tag name cannot be empty")
if len(new_name) > 50:
raise ValueError("Tag name cannot exceed 50 characters")
self.name = new_name
Collections
The Collection class in app/models/collection.py allows for grouping bookmarks. It supports two distinct operational modes defined by CollectionType.
Manual vs. Smart Collections
- Manual Collections: These function as traditional folders. Users explicitly add or remove bookmark IDs using
add_bookmark()andremove_bookmark(). - Smart Collections: These are dynamic groups defined by a
filter_rule. They do not store a static list of IDs; instead, they use the_apply_filter()method to match bookmarks based on keywords in their title or description.
# From app/models/collection.py
def _apply_filter(self, bookmarks: list) -> List[str]:
"""Evaluate the filter_rule against a list of bookmarks."""
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()]
Domain Integrity
The application maintains domain integrity through a combination of internal model logic and service-layer orchestration.
Serialization and Instantiation
All models implement to_dict() for serialization to JSON-safe dictionaries and a from_dict() class method for instantiation from raw data. This pattern is consistently used in BookmarkService to bridge the gap between the API layer and the repository.
Validation Helpers
While models perform basic internal validation, complex business rules are enforced by BookmarkService using helpers from app/models/_validators.py. These include:
- URL Validation: Ensuring URLs match a standard pattern.
- Reserved Names: Preventing tags from using reserved names like
all,untagged, ortrash. - Length Constraints: Enforcing limits on titles (256 chars) and descriptions (2048 chars).
Cross-Entity Orchestration
The BookmarkService in app/services/bookmark_service.py handles operations that affect multiple entities. For example, when a Tag is deleted, the service ensures it is stripped from all associated Bookmark instances:
# From app/services/bookmark_service.py
def delete_tag(self, tag_id: str) -> bool:
"""Delete a tag and strip it from all bookmarks."""
tag = self._repo.get_tag(tag_id)
if not tag:
return False
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)
self._repo.delete_tag(tag_id)
return True