monorepo/cloud/maplepress-backend/internal/repo/page_repo.go

279 lines
8 KiB
Go

// 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
}