package model import ( "context" "fmt" "net/url" "strconv" ) // ArchiveFileResponse represents an archive file in API responses type ArchiveFileResponse struct { ID string `json:"id"` ArchiveID string `json:"archive_id"` ArchiverKey string `json:"extractor_key"` // JSON tag kept for backward compatibility Filename string `json:"filename"` MimeType string `json:"mime_type"` FileSize int64 `json:"file_size"` HashSha256 string `json:"hash_sha256"` CreatedAt string `json:"created_at"` DownloadURL string `json:"download_url"` Content string `json:"content,omitempty"` ContentMimeType string `json:"content_mime_type,omitempty"` } // CategoryResponse represents a category in API responses type CategoryResponse struct { ID string `json:"id"` Name string `json:"name"` Icon string `json:"icon"` } // LinkListItem represents a link in the domain layer for API responses // This separates API concerns from database persistence models type LinkListItem struct { Link // Computed/aggregated fields LatestArchiveTitle string `json:"latest_archive_title,omitempty"` LatestArchiveStatus string `json:"latest_archive_status,omitempty"` Thumbnail *ArchiveFileResponse `json:"thumbnail,omitempty"` Categories []CategoryResponse `json:"categories,omitempty"` // Internal fields (not serialized) ThumbnailURL string `json:"-"` // Only populated if IncludeThumbnail is true HasThumbnail bool `json:"-"` // Indicates if thumbnail exists } // LinkDomain defines the interface for link operations type LinkDomain interface { CreateLink(ctx context.Context, url, userID string) (*Link, error) GetLink(ctx context.Context, id string) (*Link, error) ListLinks(ctx context.Context, req LinkListRequest) ([]LinkListItem, int, error) DeleteLink(ctx context.Context, id string) error } // ArchiveDomain defines the interface for archive operations type ArchiveDomain interface { GetArchive(ctx context.Context, archiveID string) (*Archive, error) GetArchiveHistory(ctx context.Context, linkID string) ([]*Archive, error) ReArchiveLink(ctx context.Context, linkID string) error GetArchiveFiles(ctx context.Context, archiveID string) ([]*ArchiveFile, error) GetArchiveFile(ctx context.Context, fileID string) (*ArchiveFile, error) DeleteArchive(ctx context.Context, archiveID string) error ProcessArchive(ctx context.Context, archive *Archive, link *Link, archiverKeys []string) error } // CategoryDomain defines the interface for category operations type CategoryDomain interface { DetermineCategoriesFromMimeType(ctx context.Context, mimeType string) ([]string, error) UpdateLinkCategories(ctx context.Context, linkID string, mimeTypes []string) error ListCategories(ctx context.Context) ([]*Category, error) } // AuthDomain defines the interface for authentication operations type AuthDomain interface { Login(email, password string) (*User, *Token, error) CreateUser(email, password, role string) (*User, error) VerifyPassword(user *User, password string) bool ListUsers() ([]*User, error) GetUser(id string) (*User, error) UpdateUser(id, email, role string) (*User, error) UpdateUserPassword(id, password string) error DeleteUser(id string) error } // LinkListRequest contains request parameters for listing links type LinkListRequest struct { UserID string Limit int Offset int CategoryID string IncludeThumbnail bool SearchQuery string } // Defaults sets sensible defaults for LinkListRequest func (r *LinkListRequest) Defaults() { if r.Limit == 0 { r.Limit = 50 // Default limit } if r.Offset < 0 { r.Offset = 0 } } // IsValid validates LinkListRequest func (r *LinkListRequest) IsValid() error { if r.UserID == "" { return ErrInvalidRequest } if r.Limit < 1 || r.Limit > 100 { return ErrInvalidRequest } if r.Offset < 0 { return ErrInvalidRequest } return nil } // FromURLValues parses query parameters from url.Values into the request struct func (r *LinkListRequest) FromURLValues(values url.Values) error { // Parse limit if limitStr := values.Get("limit"); limitStr != "" { limit, err := strconv.Atoi(limitStr) if err != nil { return fmt.Errorf("invalid limit parameter: %w", err) } r.Limit = limit } // Parse offset if offsetStr := values.Get("offset"); offsetStr != "" { offset, err := strconv.Atoi(offsetStr) if err != nil { return fmt.Errorf("invalid offset parameter: %w", err) } r.Offset = offset } // Parse category_id if categoryID := values.Get("category_id"); categoryID != "" { r.CategoryID = categoryID } // Parse include_thumbnail if includeThumbnailStr := values.Get("include_thumbnail"); includeThumbnailStr != "" { includeThumbnail, err := strconv.ParseBool(includeThumbnailStr) if err != nil { return fmt.Errorf("invalid include_thumbnail parameter: %w", err) } r.IncludeThumbnail = includeThumbnail } // Parse search query (supports both 'q' and 'search' parameters) if searchQuery := values.Get("q"); searchQuery != "" { r.SearchQuery = searchQuery } else if searchQuery := values.Get("search"); searchQuery != "" { r.SearchQuery = searchQuery } return nil }