Skip to main content

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 metadata field is a dictionary used for storing arbitrary key/value pairs, allowing the model to be extended without schema changes.
  • Tags: The tags attribute stores a list of tag IDs. The model provides add_tag and remove_tag methods 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 color property defined by the TagColor enum (e.g., RED, BLUE, GREEN).
  • Usage Tracking: The model tracks how many bookmarks are currently using it via usage_count. This is updated using increment_usage() and decrement_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

  1. Manual Collections: These function as traditional folders. Users explicitly add or remove bookmark IDs using add_bookmark() and remove_bookmark().
  2. 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, or trash.
  • 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