package repo import ( "context" "encoding/json" "errors" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/dbiz/cdp/data-layer/api/internal/apperr" "github.com/dbiz/cdp/data-layer/api/internal/model" ) type SavedQueryRepo struct { pg *pgxpool.Pool } func NewSavedQueryRepo(pg *pgxpool.Pool) *SavedQueryRepo { return &SavedQueryRepo{pg: pg} } const ( insertSavedQuery = ` INSERT INTO saved_queries (workspace_id, owner_id, name, kind, spec) VALUES ($1, NULLIF($2, '')::uuid, $3, $4, $5) RETURNING id, workspace_id, COALESCE(owner_id::text, '') AS owner_id, name, kind, spec, created_at, updated_at ` selectSavedQueries = ` SELECT id, workspace_id, COALESCE(owner_id::text, '') AS owner_id, name, kind, spec, created_at, updated_at FROM saved_queries WHERE workspace_id = $1 ORDER BY updated_at DESC LIMIT $2 OFFSET $3 ` selectSavedQuery = ` SELECT id, workspace_id, COALESCE(owner_id::text, '') AS owner_id, name, kind, spec, created_at, updated_at FROM saved_queries WHERE workspace_id = $1 AND id = $2 ` updateSavedQuery = ` UPDATE saved_queries SET name = $3, spec = $4, updated_at = now() WHERE workspace_id = $1 AND id = $2 RETURNING id, workspace_id, COALESCE(owner_id::text, '') AS owner_id, name, kind, spec, created_at, updated_at ` deleteSavedQuery = `DELETE FROM saved_queries WHERE workspace_id = $1 AND id = $2` ) func (r *SavedQueryRepo) Create(ctx context.Context, q model.SavedQuery) (*model.SavedQuery, error) { spec, err := json.Marshal(q.Spec) if err != nil { return nil, apperr.BadRequest("spec must be valid json", "spec", err) } row := r.pg.QueryRow(ctx, insertSavedQuery, q.WorkspaceID, q.OwnerID, q.Name, q.Kind, spec) return scanSavedQuery(row) } func (r *SavedQueryRepo) List(ctx context.Context, workspaceID string, limit, offset int) ([]model.SavedQuery, error) { rows, err := r.pg.Query(ctx, selectSavedQueries, workspaceID, limit, offset) if err != nil { return nil, apperr.Internal(err) } defer rows.Close() out := []model.SavedQuery{} for rows.Next() { q, err := scanSavedQuery(rows) if err != nil { return nil, err } out = append(out, *q) } return out, rows.Err() } func (r *SavedQueryRepo) Get(ctx context.Context, workspaceID, id string) (*model.SavedQuery, error) { row := r.pg.QueryRow(ctx, selectSavedQuery, workspaceID, id) return scanSavedQuery(row) } func (r *SavedQueryRepo) Update(ctx context.Context, workspaceID, id, name string, spec map[string]any) (*model.SavedQuery, error) { specJSON, err := json.Marshal(spec) if err != nil { return nil, apperr.BadRequest("spec must be valid json", "spec", err) } row := r.pg.QueryRow(ctx, updateSavedQuery, workspaceID, id, name, specJSON) return scanSavedQuery(row) } func (r *SavedQueryRepo) Delete(ctx context.Context, workspaceID, id string) error { ct, err := r.pg.Exec(ctx, deleteSavedQuery, workspaceID, id) if err != nil { return apperr.Internal(err) } if ct.RowsAffected() == 0 { return apperr.NotFound("saved query not found") } return nil } // scanSavedQuery accepts both pgx.Row and pgx.Rows (they share Scan). type scanner interface { Scan(dest ...any) error } func scanSavedQuery(s scanner) (*model.SavedQuery, error) { var q model.SavedQuery var specRaw []byte if err := s.Scan(&q.ID, &q.WorkspaceID, &q.OwnerID, &q.Name, &q.Kind, &specRaw, &q.CreatedAt, &q.UpdatedAt); err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, apperr.NotFound("saved query not found") } return nil, apperr.Internal(err) } if len(specRaw) > 0 { if err := json.Unmarshal(specRaw, &q.Spec); err != nil { return nil, apperr.Internal(err) } } return &q, nil }