package db import ( "database/sql" "fmt" "time" "github.com/insertr/server/internal/models" _ "github.com/mattn/go-sqlite3" ) // SQLiteDB wraps a SQLite database connection type SQLiteDB struct { db *sql.DB } // NewSQLiteDB creates a new SQLite database connection func NewSQLiteDB(dbPath string) (*SQLiteDB, error) { db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("opening database: %w", err) } // Test connection if err := db.Ping(); err != nil { return nil, fmt.Errorf("connecting to database: %w", err) } sqliteDB := &SQLiteDB{db: db} // Initialize schema if err := sqliteDB.initSchema(); err != nil { return nil, fmt.Errorf("initializing schema: %w", err) } return sqliteDB, nil } // Close closes the database connection func (s *SQLiteDB) Close() error { return s.db.Close() } // initSchema creates the necessary tables func (s *SQLiteDB) initSchema() error { schema := ` CREATE TABLE IF NOT EXISTS content ( id TEXT NOT NULL, site_id TEXT NOT NULL, value TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('text', 'markdown', 'link')), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, site_id) ); CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id); CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at); -- Trigger to update updated_at timestamp CREATE TRIGGER IF NOT EXISTS update_content_updated_at AFTER UPDATE ON content FOR EACH ROW BEGIN UPDATE content SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id AND site_id = NEW.site_id; END; ` if _, err := s.db.Exec(schema); err != nil { return fmt.Errorf("creating schema: %w", err) } return nil } // GetContent fetches a single content item by ID and site ID func (s *SQLiteDB) GetContent(siteID, contentID string) (*models.ContentItem, error) { query := ` SELECT id, site_id, value, type, created_at, updated_at FROM content WHERE id = ? AND site_id = ? ` var item models.ContentItem err := s.db.QueryRow(query, contentID, siteID).Scan( &item.ID, &item.SiteID, &item.Value, &item.Type, &item.CreatedAt, &item.UpdatedAt, ) if err == sql.ErrNoRows { return nil, nil // Content not found } if err != nil { return nil, fmt.Errorf("querying content: %w", err) } return &item, nil } // GetAllContent fetches all content for a site func (s *SQLiteDB) GetAllContent(siteID string) ([]models.ContentItem, error) { query := ` SELECT id, site_id, value, type, created_at, updated_at FROM content WHERE site_id = ? ORDER BY updated_at DESC ` rows, err := s.db.Query(query, siteID) if err != nil { return nil, fmt.Errorf("querying all content: %w", err) } defer rows.Close() var items []models.ContentItem for rows.Next() { var item models.ContentItem err := rows.Scan(&item.ID, &item.SiteID, &item.Value, &item.Type, &item.CreatedAt, &item.UpdatedAt) if err != nil { return nil, fmt.Errorf("scanning content row: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterating content rows: %w", err) } return items, nil } // GetBulkContent fetches multiple content items by IDs func (s *SQLiteDB) GetBulkContent(siteID string, contentIDs []string) ([]models.ContentItem, error) { if len(contentIDs) == 0 { return []models.ContentItem{}, nil } // Build placeholders for IN clause placeholders := make([]interface{}, len(contentIDs)+1) placeholders[0] = siteID for i, id := range contentIDs { placeholders[i+1] = id } // Build query with proper number of placeholders query := fmt.Sprintf(` SELECT id, site_id, value, type, created_at, updated_at FROM content WHERE site_id = ? AND id IN (%s) ORDER BY updated_at DESC `, buildPlaceholders(len(contentIDs))) rows, err := s.db.Query(query, placeholders...) if err != nil { return nil, fmt.Errorf("querying bulk content: %w", err) } defer rows.Close() var items []models.ContentItem for rows.Next() { var item models.ContentItem err := rows.Scan(&item.ID, &item.SiteID, &item.Value, &item.Type, &item.CreatedAt, &item.UpdatedAt) if err != nil { return nil, fmt.Errorf("scanning bulk content row: %w", err) } items = append(items, item) } return items, nil } // CreateContent creates a new content item func (s *SQLiteDB) CreateContent(siteID, contentID, value, contentType string) (*models.ContentItem, error) { now := time.Now() query := ` INSERT INTO content (id, site_id, value, type, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) ` _, err := s.db.Exec(query, contentID, siteID, value, contentType, now, now) if err != nil { return nil, fmt.Errorf("creating content: %w", err) } return &models.ContentItem{ ID: contentID, SiteID: siteID, Value: value, Type: contentType, CreatedAt: now, UpdatedAt: now, }, nil } // UpdateContent updates an existing content item func (s *SQLiteDB) UpdateContent(siteID, contentID, value string) (*models.ContentItem, error) { // First check if content exists existing, err := s.GetContent(siteID, contentID) if err != nil { return nil, fmt.Errorf("checking existing content: %w", err) } if existing == nil { return nil, fmt.Errorf("content not found: %s", contentID) } query := ` UPDATE content SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND site_id = ? ` _, err = s.db.Exec(query, value, contentID, siteID) if err != nil { return nil, fmt.Errorf("updating content: %w", err) } // Fetch and return updated content return s.GetContent(siteID, contentID) } // buildPlaceholders creates a string of SQL placeholders like "?,?,?" func buildPlaceholders(count int) string { if count == 0 { return "" } result := "?" for i := 1; i < count; i++ { result += ",?" } return result }