package storage import ( "context" "database/sql" _ "embed" "fmt" "time" "gitlab.global.bsf.tools/esv/bsf/bsf-integration/orchard/orchard-mvp/internal/config" "gitlab.global.bsf.tools/esv/bsf/bsf-integration/orchard/orchard-mvp/internal/models" "github.com/google/uuid" _ "github.com/lib/pq" ) //go:embed migrations/001_initial.sql var migrationSQL string // Database handles all database operations type Database struct { db *sql.DB } // NewDatabase creates a new database connection and runs migrations func NewDatabase(cfg *config.DatabaseConfig) (*Database, error) { dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode) db, err := sql.Open("postgres", dsn) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } if err := db.Ping(); err != nil { return nil, fmt.Errorf("failed to ping database: %w", err) } d := &Database{db: db} // Run migrations if err := d.runMigrations(); err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } return d, nil } // runMigrations executes the embedded SQL migrations func (d *Database) runMigrations() error { _, err := d.db.Exec(migrationSQL) return err } // Close closes the database connection func (d *Database) Close() error { return d.db.Close() } // Grove operations func (d *Database) CreateGrove(ctx context.Context, grove *models.Grove) error { grove.ID = uuid.New().String() grove.CreatedAt = time.Now() grove.UpdatedAt = grove.CreatedAt _, err := d.db.ExecContext(ctx, ` INSERT INTO groves (id, name, description, is_public, created_at, updated_at, created_by) VALUES ($1, $2, $3, $4, $5, $6, $7) `, grove.ID, grove.Name, grove.Description, grove.IsPublic, grove.CreatedAt, grove.UpdatedAt, grove.CreatedBy) return err } func (d *Database) GetGrove(ctx context.Context, name string) (*models.Grove, error) { var grove models.Grove err := d.db.QueryRowContext(ctx, ` SELECT id, name, description, is_public, created_at, updated_at, created_by FROM groves WHERE name = $1 `, name).Scan(&grove.ID, &grove.Name, &grove.Description, &grove.IsPublic, &grove.CreatedAt, &grove.UpdatedAt, &grove.CreatedBy) if err == sql.ErrNoRows { return nil, nil } return &grove, err } func (d *Database) ListGroves(ctx context.Context, userID string) ([]*models.Grove, error) { rows, err := d.db.QueryContext(ctx, ` SELECT g.id, g.name, g.description, g.is_public, g.created_at, g.updated_at, g.created_by FROM groves g LEFT JOIN access_permissions ap ON g.id = ap.grove_id AND ap.user_id = $1 WHERE g.is_public = true OR ap.user_id IS NOT NULL ORDER BY g.name `, userID) if err != nil { return nil, err } defer rows.Close() var groves []*models.Grove for rows.Next() { var grove models.Grove if err := rows.Scan(&grove.ID, &grove.Name, &grove.Description, &grove.IsPublic, &grove.CreatedAt, &grove.UpdatedAt, &grove.CreatedBy); err != nil { return nil, err } groves = append(groves, &grove) } return groves, nil } // Tree operations func (d *Database) CreateTree(ctx context.Context, tree *models.Tree) error { tree.ID = uuid.New().String() tree.CreatedAt = time.Now() tree.UpdatedAt = tree.CreatedAt _, err := d.db.ExecContext(ctx, ` INSERT INTO trees (id, grove_id, name, description, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6) `, tree.ID, tree.GroveID, tree.Name, tree.Description, tree.CreatedAt, tree.UpdatedAt) return err } func (d *Database) GetTree(ctx context.Context, groveID, name string) (*models.Tree, error) { var tree models.Tree err := d.db.QueryRowContext(ctx, ` SELECT id, grove_id, name, description, created_at, updated_at FROM trees WHERE grove_id = $1 AND name = $2 `, groveID, name).Scan(&tree.ID, &tree.GroveID, &tree.Name, &tree.Description, &tree.CreatedAt, &tree.UpdatedAt) if err == sql.ErrNoRows { return nil, nil } return &tree, err } func (d *Database) ListTrees(ctx context.Context, groveID string) ([]*models.Tree, error) { rows, err := d.db.QueryContext(ctx, ` SELECT id, grove_id, name, description, created_at, updated_at FROM trees WHERE grove_id = $1 ORDER BY name `, groveID) if err != nil { return nil, err } defer rows.Close() var trees []*models.Tree for rows.Next() { var tree models.Tree if err := rows.Scan(&tree.ID, &tree.GroveID, &tree.Name, &tree.Description, &tree.CreatedAt, &tree.UpdatedAt); err != nil { return nil, err } trees = append(trees, &tree) } return trees, nil } // Fruit operations func (d *Database) CreateFruit(ctx context.Context, fruit *models.Fruit) error { fruit.CreatedAt = time.Now() fruit.RefCount = 1 _, err := d.db.ExecContext(ctx, ` INSERT INTO fruits (id, size, content_type, original_name, created_at, created_by, ref_count, s3_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO UPDATE SET ref_count = fruits.ref_count + 1 `, fruit.ID, fruit.Size, fruit.ContentType, fruit.OriginalName, fruit.CreatedAt, fruit.CreatedBy, fruit.RefCount, fruit.S3Key) return err } func (d *Database) GetFruit(ctx context.Context, id string) (*models.Fruit, error) { var fruit models.Fruit err := d.db.QueryRowContext(ctx, ` SELECT id, size, content_type, original_name, created_at, created_by, ref_count, s3_key FROM fruits WHERE id = $1 `, id).Scan(&fruit.ID, &fruit.Size, &fruit.ContentType, &fruit.OriginalName, &fruit.CreatedAt, &fruit.CreatedBy, &fruit.RefCount, &fruit.S3Key) if err == sql.ErrNoRows { return nil, nil } return &fruit, err } // Graft operations func (d *Database) CreateGraft(ctx context.Context, graft *models.Graft) error { graft.ID = uuid.New().String() graft.CreatedAt = time.Now() _, err := d.db.ExecContext(ctx, ` INSERT INTO grafts (id, tree_id, name, fruit_id, created_at, created_by) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (tree_id, name) DO UPDATE SET fruit_id = $4, created_at = $5, created_by = $6 `, graft.ID, graft.TreeID, graft.Name, graft.FruitID, graft.CreatedAt, graft.CreatedBy) return err } func (d *Database) GetGraft(ctx context.Context, treeID, name string) (*models.Graft, error) { var graft models.Graft err := d.db.QueryRowContext(ctx, ` SELECT id, tree_id, name, fruit_id, created_at, created_by FROM grafts WHERE tree_id = $1 AND name = $2 `, treeID, name).Scan(&graft.ID, &graft.TreeID, &graft.Name, &graft.FruitID, &graft.CreatedAt, &graft.CreatedBy) if err == sql.ErrNoRows { return nil, nil } return &graft, err } func (d *Database) ListGrafts(ctx context.Context, treeID string) ([]*models.Graft, error) { rows, err := d.db.QueryContext(ctx, ` SELECT id, tree_id, name, fruit_id, created_at, created_by FROM grafts WHERE tree_id = $1 ORDER BY name `, treeID) if err != nil { return nil, err } defer rows.Close() var grafts []*models.Graft for rows.Next() { var graft models.Graft if err := rows.Scan(&graft.ID, &graft.TreeID, &graft.Name, &graft.FruitID, &graft.CreatedAt, &graft.CreatedBy); err != nil { return nil, err } grafts = append(grafts, &graft) } return grafts, nil } // Harvest operations func (d *Database) CreateHarvest(ctx context.Context, harvest *models.Harvest) error { harvest.ID = uuid.New().String() harvest.HarvestedAt = time.Now() _, err := d.db.ExecContext(ctx, ` INSERT INTO harvests (id, fruit_id, tree_id, original_name, harvested_at, harvested_by, source_ip) VALUES ($1, $2, $3, $4, $5, $6, $7) `, harvest.ID, harvest.FruitID, harvest.TreeID, harvest.OriginalName, harvest.HarvestedAt, harvest.HarvestedBy, harvest.SourceIP) return err } // Audit operations func (d *Database) CreateAuditLog(ctx context.Context, log *models.AuditLog) error { log.ID = uuid.New().String() log.Timestamp = time.Now() _, err := d.db.ExecContext(ctx, ` INSERT INTO audit_logs (id, action, resource, user_id, details, timestamp, source_ip) VALUES ($1, $2, $3, $4, $5, $6, $7) `, log.ID, log.Action, log.Resource, log.UserID, log.Details, log.Timestamp, log.SourceIP) return err } // Access control operations func (d *Database) CheckAccess(ctx context.Context, groveID, userID, requiredLevel string) (bool, error) { // Check if grove is public (read access for everyone) var isPublic bool err := d.db.QueryRowContext(ctx, `SELECT is_public FROM groves WHERE id = $1`, groveID).Scan(&isPublic) if err != nil { return false, err } if isPublic && requiredLevel == "read" { return true, nil } // Check user-specific permissions var level string err = d.db.QueryRowContext(ctx, ` SELECT level FROM access_permissions WHERE grove_id = $1 AND user_id = $2 AND (expires_at IS NULL OR expires_at > NOW()) `, groveID, userID).Scan(&level) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } // Check permission hierarchy: admin > write > read switch requiredLevel { case "read": return true, nil case "write": return level == "write" || level == "admin", nil case "admin": return level == "admin", nil } return false, nil } func (d *Database) GrantAccess(ctx context.Context, perm *models.AccessPermission) error { perm.ID = uuid.New().String() perm.CreatedAt = time.Now() _, err := d.db.ExecContext(ctx, ` INSERT INTO access_permissions (id, grove_id, user_id, level, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (grove_id, user_id) DO UPDATE SET level = $4, expires_at = $6 `, perm.ID, perm.GroveID, perm.UserID, perm.Level, perm.CreatedAt, perm.ExpiresAt) return err } // Consumer tracking func (d *Database) TrackConsumer(ctx context.Context, treeID, projectURL string) error { _, err := d.db.ExecContext(ctx, ` INSERT INTO consumers (id, tree_id, project_url, last_access, created_at) VALUES ($1, $2, $3, NOW(), NOW()) ON CONFLICT (tree_id, project_url) DO UPDATE SET last_access = NOW() `, uuid.New().String(), treeID, projectURL) return err } func (d *Database) GetConsumers(ctx context.Context, treeID string) ([]*models.Consumer, error) { rows, err := d.db.QueryContext(ctx, ` SELECT id, tree_id, project_url, last_access, created_at FROM consumers WHERE tree_id = $1 ORDER BY last_access DESC `, treeID) if err != nil { return nil, err } defer rows.Close() var consumers []*models.Consumer for rows.Next() { var consumer models.Consumer if err := rows.Scan(&consumer.ID, &consumer.TreeID, &consumer.ProjectURL, &consumer.LastAccess, &consumer.CreatedAt); err != nil { return nil, err } consumers = append(consumers, &consumer) } return consumers, nil }