Skip to main content

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:

  1. Manual Collections: Users explicitly add or remove bookmark IDs using add_bookmark and remove_bookmark. These collections also support custom ordering via the reorder method.
  2. 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_url ensures that bookmarked URLs follow a standard http:// or https:// 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.