package db import ( "context" "database/sql" "encoding/json" "github.com/gooseek/backend/internal/pages" ) type PageRepository struct { db *PostgresDB } func NewPageRepository(db *PostgresDB) *PageRepository { return &PageRepository{db: db} } func (r *PageRepository) RunMigrations(ctx context.Context) error { migrations := []string{ `CREATE TABLE IF NOT EXISTS pages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, thread_id UUID REFERENCES threads(id) ON DELETE SET NULL, title VARCHAR(500) NOT NULL, subtitle TEXT, sections JSONB NOT NULL DEFAULT '[]', sources JSONB NOT NULL DEFAULT '[]', thumbnail TEXT, is_public BOOLEAN DEFAULT FALSE, share_id VARCHAR(100) UNIQUE, view_count INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() )`, `CREATE INDEX IF NOT EXISTS idx_pages_user ON pages(user_id)`, `CREATE INDEX IF NOT EXISTS idx_pages_share ON pages(share_id)`, } for _, m := range migrations { if _, err := r.db.db.ExecContext(ctx, m); err != nil { return err } } return nil } func (r *PageRepository) Create(ctx context.Context, p *pages.Page) error { sectionsJSON, _ := json.Marshal(p.Sections) sourcesJSON, _ := json.Marshal(p.Sources) query := ` INSERT INTO pages (user_id, thread_id, title, subtitle, sections, sources, thumbnail, is_public) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, updated_at ` var threadID *string if p.ThreadID != "" { threadID = &p.ThreadID } return r.db.db.QueryRowContext(ctx, query, p.UserID, threadID, p.Title, p.Subtitle, sectionsJSON, sourcesJSON, p.Thumbnail, p.IsPublic, ).Scan(&p.ID, &p.CreatedAt, &p.UpdatedAt) } func (r *PageRepository) GetByID(ctx context.Context, id string) (*pages.Page, error) { query := ` SELECT id, user_id, thread_id, title, subtitle, sections, sources, thumbnail, is_public, share_id, view_count, created_at, updated_at FROM pages WHERE id = $1 ` var p pages.Page var sectionsJSON, sourcesJSON []byte var threadID, shareID sql.NullString err := r.db.db.QueryRowContext(ctx, query, id).Scan( &p.ID, &p.UserID, &threadID, &p.Title, &p.Subtitle, §ionsJSON, &sourcesJSON, &p.Thumbnail, &p.IsPublic, &shareID, &p.ViewCount, &p.CreatedAt, &p.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } json.Unmarshal(sectionsJSON, &p.Sections) json.Unmarshal(sourcesJSON, &p.Sources) if threadID.Valid { p.ThreadID = threadID.String } if shareID.Valid { p.ShareID = shareID.String } return &p, nil } func (r *PageRepository) GetByShareID(ctx context.Context, shareID string) (*pages.Page, error) { query := ` SELECT id, user_id, thread_id, title, subtitle, sections, sources, thumbnail, is_public, share_id, view_count, created_at, updated_at FROM pages WHERE share_id = $1 AND is_public = true ` var p pages.Page var sectionsJSON, sourcesJSON []byte var threadID, shareIDVal sql.NullString err := r.db.db.QueryRowContext(ctx, query, shareID).Scan( &p.ID, &p.UserID, &threadID, &p.Title, &p.Subtitle, §ionsJSON, &sourcesJSON, &p.Thumbnail, &p.IsPublic, &shareIDVal, &p.ViewCount, &p.CreatedAt, &p.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } json.Unmarshal(sectionsJSON, &p.Sections) json.Unmarshal(sourcesJSON, &p.Sources) if threadID.Valid { p.ThreadID = threadID.String } if shareIDVal.Valid { p.ShareID = shareIDVal.String } return &p, nil } func (r *PageRepository) GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*pages.Page, error) { query := ` SELECT id, user_id, thread_id, title, subtitle, sections, sources, thumbnail, is_public, share_id, view_count, created_at, updated_at FROM pages WHERE user_id = $1 ORDER BY updated_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.db.db.QueryContext(ctx, query, userID, limit, offset) if err != nil { return nil, err } defer rows.Close() var pagesList []*pages.Page for rows.Next() { var p pages.Page var sectionsJSON, sourcesJSON []byte var threadID, shareID sql.NullString if err := rows.Scan( &p.ID, &p.UserID, &threadID, &p.Title, &p.Subtitle, §ionsJSON, &sourcesJSON, &p.Thumbnail, &p.IsPublic, &shareID, &p.ViewCount, &p.CreatedAt, &p.UpdatedAt, ); err != nil { return nil, err } json.Unmarshal(sectionsJSON, &p.Sections) json.Unmarshal(sourcesJSON, &p.Sources) if threadID.Valid { p.ThreadID = threadID.String } if shareID.Valid { p.ShareID = shareID.String } pagesList = append(pagesList, &p) } return pagesList, nil } func (r *PageRepository) Update(ctx context.Context, p *pages.Page) error { sectionsJSON, _ := json.Marshal(p.Sections) sourcesJSON, _ := json.Marshal(p.Sources) query := ` UPDATE pages SET title = $2, subtitle = $3, sections = $4, sources = $5, thumbnail = $6, is_public = $7, updated_at = NOW() WHERE id = $1 ` _, err := r.db.db.ExecContext(ctx, query, p.ID, p.Title, p.Subtitle, sectionsJSON, sourcesJSON, p.Thumbnail, p.IsPublic, ) return err } func (r *PageRepository) SetShareID(ctx context.Context, pageID, shareID string) error { _, err := r.db.db.ExecContext(ctx, "UPDATE pages SET share_id = $2, is_public = true WHERE id = $1", pageID, shareID, ) return err } func (r *PageRepository) IncrementViewCount(ctx context.Context, id string) error { _, err := r.db.db.ExecContext(ctx, "UPDATE pages SET view_count = view_count + 1 WHERE id = $1", id, ) return err } func (r *PageRepository) Delete(ctx context.Context, id string) error { _, err := r.db.db.ExecContext(ctx, "DELETE FROM pages WHERE id = $1", id) return err }