// File Path: monorepo/cloud/maplepress-backend/internal/repo/page_repo.go package repo import ( "context" "fmt" "github.com/gocql/gocql" "go.uber.org/zap" "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/page" ) type pageRepository struct { session *gocql.Session logger *zap.Logger } func NewPageRepository(session *gocql.Session, logger *zap.Logger) page.Repository { return &pageRepository{ session: session, logger: logger.Named("page-repo"), } } // Create inserts a new page func (r *pageRepository) Create(ctx context.Context, p *page.Page) error { query := ` INSERT INTO maplepress.pages_by_site ( site_id, page_id, tenant_id, title, content, excerpt, url, status, post_type, author, published_at, modified_at, indexed_at, meilisearch_doc_id, created_at, updated_at, created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` return r.session.Query(query, p.SiteID, p.PageID, p.TenantID, p.Title, p.Content, p.Excerpt, p.URL, p.Status, p.PostType, p.Author, p.PublishedAt, p.ModifiedAt, p.IndexedAt, p.MeilisearchDocID, p.CreatedAt, p.UpdatedAt, p.CreatedFromIPAddress, p.CreatedFromIPTimestamp, p.ModifiedFromIPAddress, p.ModifiedFromIPTimestamp, ).WithContext(ctx).Exec() } // Update updates an existing page func (r *pageRepository) Update(ctx context.Context, p *page.Page) error { query := ` UPDATE maplepress.pages_by_site SET title = ?, content = ?, excerpt = ?, url = ?, status = ?, post_type = ?, author = ?, published_at = ?, modified_at = ?, indexed_at = ?, meilisearch_doc_id = ?, updated_at = ?, modified_from_ip_address = ?, modified_from_ip_timestamp = ? WHERE site_id = ? AND page_id = ? ` return r.session.Query(query, p.Title, p.Content, p.Excerpt, p.URL, p.Status, p.PostType, p.Author, p.PublishedAt, p.ModifiedAt, p.IndexedAt, p.MeilisearchDocID, p.UpdatedAt, p.ModifiedFromIPAddress, p.ModifiedFromIPTimestamp, p.SiteID, p.PageID, ).WithContext(ctx).Exec() } // Upsert creates or updates a page func (r *pageRepository) Upsert(ctx context.Context, p *page.Page) error { // In Cassandra, INSERT acts as an upsert return r.Create(ctx, p) } // GetByID retrieves a page by site_id and page_id func (r *pageRepository) GetByID(ctx context.Context, siteID gocql.UUID, pageID string) (*page.Page, error) { query := ` SELECT site_id, page_id, tenant_id, title, content, excerpt, url, status, post_type, author, published_at, modified_at, indexed_at, meilisearch_doc_id, created_at, updated_at, created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp FROM maplepress.pages_by_site WHERE site_id = ? AND page_id = ? ` p := &page.Page{} err := r.session.Query(query, siteID, pageID). WithContext(ctx). Scan( &p.SiteID, &p.PageID, &p.TenantID, &p.Title, &p.Content, &p.Excerpt, &p.URL, &p.Status, &p.PostType, &p.Author, &p.PublishedAt, &p.ModifiedAt, &p.IndexedAt, &p.MeilisearchDocID, &p.CreatedAt, &p.UpdatedAt, &p.CreatedFromIPAddress, &p.CreatedFromIPTimestamp, &p.ModifiedFromIPAddress, &p.ModifiedFromIPTimestamp, ) if err == gocql.ErrNotFound { return nil, fmt.Errorf("page not found: site_id=%s, page_id=%s", siteID, pageID) } if err != nil { return nil, fmt.Errorf("failed to get page: %w", err) } return p, nil } // GetBySiteID retrieves all pages for a site func (r *pageRepository) GetBySiteID(ctx context.Context, siteID gocql.UUID) ([]*page.Page, error) { query := ` SELECT site_id, page_id, tenant_id, title, content, excerpt, url, status, post_type, author, published_at, modified_at, indexed_at, meilisearch_doc_id, created_at, updated_at, created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp FROM maplepress.pages_by_site WHERE site_id = ? ` iter := r.session.Query(query, siteID).WithContext(ctx).Iter() defer iter.Close() var pages []*page.Page p := &page.Page{} for iter.Scan( &p.SiteID, &p.PageID, &p.TenantID, &p.Title, &p.Content, &p.Excerpt, &p.URL, &p.Status, &p.PostType, &p.Author, &p.PublishedAt, &p.ModifiedAt, &p.IndexedAt, &p.MeilisearchDocID, &p.CreatedAt, &p.UpdatedAt, &p.CreatedFromIPAddress, &p.CreatedFromIPTimestamp, &p.ModifiedFromIPAddress, &p.ModifiedFromIPTimestamp, ) { pages = append(pages, p) p = &page.Page{} // Create new instance for next iteration } if err := iter.Close(); err != nil { return nil, fmt.Errorf("failed to iterate pages: %w", err) } return pages, nil } // GetBySiteIDPaginated retrieves pages for a site with pagination func (r *pageRepository) GetBySiteIDPaginated(ctx context.Context, siteID gocql.UUID, limit int, pageState []byte) ([]*page.Page, []byte, error) { query := ` SELECT site_id, page_id, tenant_id, title, content, excerpt, url, status, post_type, author, published_at, modified_at, indexed_at, meilisearch_doc_id, created_at, updated_at, created_from_ip_address, created_from_ip_timestamp, modified_from_ip_address, modified_from_ip_timestamp FROM maplepress.pages_by_site WHERE site_id = ? ` q := r.session.Query(query, siteID).WithContext(ctx).PageSize(limit) if len(pageState) > 0 { q = q.PageState(pageState) } iter := q.Iter() defer iter.Close() var pages []*page.Page p := &page.Page{} for iter.Scan( &p.SiteID, &p.PageID, &p.TenantID, &p.Title, &p.Content, &p.Excerpt, &p.URL, &p.Status, &p.PostType, &p.Author, &p.PublishedAt, &p.ModifiedAt, &p.IndexedAt, &p.MeilisearchDocID, &p.CreatedAt, &p.UpdatedAt, &p.CreatedFromIPAddress, &p.CreatedFromIPTimestamp, &p.ModifiedFromIPAddress, &p.ModifiedFromIPTimestamp, ) { pages = append(pages, p) p = &page.Page{} // Create new instance for next iteration } if err := iter.Close(); err != nil { return nil, nil, fmt.Errorf("failed to iterate pages: %w", err) } nextPageState := iter.PageState() return pages, nextPageState, nil } // Delete deletes a page func (r *pageRepository) Delete(ctx context.Context, siteID gocql.UUID, pageID string) error { query := `DELETE FROM maplepress.pages_by_site WHERE site_id = ? AND page_id = ?` return r.session.Query(query, siteID, pageID).WithContext(ctx).Exec() } // DeleteBySiteID deletes all pages for a site func (r *pageRepository) DeleteBySiteID(ctx context.Context, siteID gocql.UUID) error { // Note: This is an expensive operation in Cassandra // Better to delete partition by partition if possible query := `DELETE FROM maplepress.pages_by_site WHERE site_id = ?` return r.session.Query(query, siteID).WithContext(ctx).Exec() } // DeleteMultiple deletes multiple pages by their IDs func (r *pageRepository) DeleteMultiple(ctx context.Context, siteID gocql.UUID, pageIDs []string) error { // Use batch for efficiency batch := r.session.NewBatch(gocql.LoggedBatch).WithContext(ctx) query := `DELETE FROM maplepress.pages_by_site WHERE site_id = ? AND page_id = ?` for _, pageID := range pageIDs { batch.Query(query, siteID, pageID) } return r.session.ExecuteBatch(batch) } // CountBySiteID counts pages for a site func (r *pageRepository) CountBySiteID(ctx context.Context, siteID gocql.UUID) (int64, error) { query := `SELECT COUNT(*) FROM maplepress.pages_by_site WHERE site_id = ?` var count int64 err := r.session.Query(query, siteID).WithContext(ctx).Scan(&count) if err != nil { return 0, fmt.Errorf("failed to count pages: %w", err) } return count, nil } // Exists checks if a page exists func (r *pageRepository) Exists(ctx context.Context, siteID gocql.UUID, pageID string) (bool, error) { query := `SELECT page_id FROM maplepress.pages_by_site WHERE site_id = ? AND page_id = ?` var id string err := r.session.Query(query, siteID, pageID).WithContext(ctx).Scan(&id) if err == gocql.ErrNotFound { return false, nil } if err != nil { return false, fmt.Errorf("failed to check page existence: %w", err) } return true, nil }