feat(analytics): cards to use db (#2886)
This commit is contained in:
parent
129ab734f3
commit
21895677c3
6 changed files with 747 additions and 161 deletions
|
|
@ -3,7 +3,6 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"openreplay/backend/pkg/analytics/api/models"
|
||||
"openreplay/backend/pkg/server/api"
|
||||
|
|
@ -14,22 +13,6 @@ import (
|
|||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// getCardId returns the ID from the request
|
||||
func getCardId(r *http.Request) (int64, error) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
if idStr == "" {
|
||||
return 0, fmt.Errorf("invalid Card ID")
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid Card ID")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (e *handlersImpl) createCard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
|
@ -54,30 +37,18 @@ func (e *handlersImpl) createCard(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO save card to DB
|
||||
|
||||
resp := &models.CardGetResponse{
|
||||
Card: models.Card{
|
||||
CardID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: models.CardBase{
|
||||
Name: req.Name,
|
||||
IsPublic: req.IsPublic,
|
||||
Thumbnail: req.Thumbnail,
|
||||
MetricType: req.MetricType,
|
||||
MetricOf: req.MetricOf,
|
||||
Series: req.Series,
|
||||
},
|
||||
},
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := r.Context().Value("userData").(*user.User)
|
||||
e.log.Info(r.Context(), "User ID: ", currentUser.ID)
|
||||
resp, err := e.service.CreateCard(projectID, currentUser.ID, req)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
|
@ -87,68 +58,128 @@ func (e *handlersImpl) getCard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
id, err := getCardId(r)
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
thumbnail := "https://example.com/image.png"
|
||||
id, err := getIDFromRequest(r, "id")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO get card from DB
|
||||
|
||||
resp := &models.CardGetResponse{
|
||||
Card: models.Card{
|
||||
CardID: id,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: models.CardBase{
|
||||
Name: "My Card",
|
||||
IsPublic: true,
|
||||
Thumbnail: &thumbnail,
|
||||
MetricType: "timeseries",
|
||||
MetricOf: "session_count",
|
||||
},
|
||||
},
|
||||
resp, err := e.service.GetCardWithSeries(projectID, id)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
// get cards paginated
|
||||
func (e *handlersImpl) getCards(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
// TODO get cards from DB
|
||||
thumbnail := "https://example.com/image.png"
|
||||
|
||||
resp := &models.GetCardsResponse{
|
||||
Cards: []models.Card{
|
||||
{
|
||||
CardID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: models.CardBase{
|
||||
Name: "My Card",
|
||||
IsPublic: true,
|
||||
Thumbnail: &thumbnail,
|
||||
MetricType: "timeseries",
|
||||
MetricOf: "session_count",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 10,
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
//currentUser := r.Context().Value("userData").(*user.User)
|
||||
resp, err := e.service.GetCards(projectID)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
func (e *handlersImpl) getCardsPaginated(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
// Extract projectID from request
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
query := r.URL.Query()
|
||||
|
||||
// Filters
|
||||
filters := models.CardListFilter{
|
||||
Filters: make(map[string]interface{}),
|
||||
}
|
||||
|
||||
if name := query.Get("name"); name != "" {
|
||||
filters.Filters["name"] = name
|
||||
}
|
||||
if metricType := query.Get("metric_type"); metricType != "" {
|
||||
filters.Filters["metric_type"] = metricType
|
||||
}
|
||||
if dashboardIDs := query["dashboard_ids"]; len(dashboardIDs) > 0 {
|
||||
// Parse dashboard_ids into []int
|
||||
var ids []int
|
||||
for _, id := range dashboardIDs {
|
||||
if val, err := strconv.Atoi(id); err == nil {
|
||||
ids = append(ids, val)
|
||||
}
|
||||
}
|
||||
filters.Filters["dashboard_ids"] = ids
|
||||
}
|
||||
|
||||
// Sorting
|
||||
sort := models.CardListSort{
|
||||
Field: query.Get("sort_field"),
|
||||
Order: query.Get("sort_order"),
|
||||
}
|
||||
if sort.Field == "" {
|
||||
sort.Field = "created_at" // Default sort field
|
||||
}
|
||||
if sort.Order == "" {
|
||||
sort.Order = "desc" // Default sort order
|
||||
}
|
||||
|
||||
// Pagination
|
||||
limit := 10 // Default limit
|
||||
page := 1 // Default page number
|
||||
if val := query.Get("limit"); val != "" {
|
||||
if l, err := strconv.Atoi(val); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
if val := query.Get("page"); val != "" {
|
||||
if p, err := strconv.Atoi(val); err == nil && p > 0 {
|
||||
page = p
|
||||
}
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Validate inputs
|
||||
if err := models.ValidateStruct(filters); err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, fmt.Errorf("invalid filters: %w", err), startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
if err := models.ValidateStruct(sort); err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, fmt.Errorf("invalid sort: %w", err), startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the service
|
||||
resp, err := e.service.GetCardsPaginated(projectID, filters, sort, limit, offset)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with JSON
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
|
|
@ -156,7 +187,13 @@ func (e *handlersImpl) updateCard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
id, err := getCardId(r)
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
cardId, err := getIDFromRequest(r, "id")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
|
|
@ -182,25 +219,11 @@ func (e *handlersImpl) updateCard(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// TODO update card in DB
|
||||
|
||||
resp := &models.CardGetResponse{
|
||||
Card: models.Card{
|
||||
CardID: id,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: models.CardBase{
|
||||
Name: req.Name,
|
||||
IsPublic: req.IsPublic,
|
||||
Thumbnail: req.Thumbnail,
|
||||
MetricType: req.MetricType,
|
||||
MetricOf: req.MetricOf,
|
||||
},
|
||||
},
|
||||
currentUser := r.Context().Value("userData").(*user.User)
|
||||
resp, err := e.service.UpdateCard(projectID, int64(cardId), currentUser.ID, req)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
|
|
@ -210,13 +233,24 @@ func (e *handlersImpl) deleteCard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
_, err := getCardId(r)
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO delete card from DB
|
||||
cardId, err := getIDFromRequest(r, "id")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := r.Context().Value("userData").(*user.User)
|
||||
err = e.service.DeleteCard(projectID, int64(cardId), currentUser.ID)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, nil, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
|
@ -225,6 +259,12 @@ func (e *handlersImpl) getCardChartData(w http.ResponseWriter, r *http.Request)
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
projectID, err := getIDFromRequest(r, "projectId")
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := api.ReadBody(e.log, w, r, e.jsonSizeLimit)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize)
|
||||
|
|
@ -240,26 +280,13 @@ func (e *handlersImpl) getCardChartData(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
validate := validator.New()
|
||||
err = validate.Struct(req)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO get card chart data from ClickHouse
|
||||
jsonInput := `
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"timestamp": 1733934939000,
|
||||
"Series A": 100,
|
||||
"Series B": 200
|
||||
},
|
||||
{
|
||||
"timestamp": 1733935939000,
|
||||
"Series A": 150,
|
||||
"Series B": 250
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
var resp models.GetCardChartDataResponse
|
||||
err = json.Unmarshal([]byte(jsonInput), &resp)
|
||||
currentUser := r.Context().Value("userData").(*user.User)
|
||||
resp, err := e.service.GetCardChartData(projectID, currentUser.ID, req)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,32 +2,14 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"openreplay/backend/pkg/analytics/api/models"
|
||||
"openreplay/backend/pkg/server/api"
|
||||
"openreplay/backend/pkg/server/user"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getIDFromRequest(r *http.Request, key string) (int, error) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars[key]
|
||||
if idStr == "" {
|
||||
return 0, fmt.Errorf("missing %s in request", key)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid %s format", key)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (e *handlersImpl) createDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
config "openreplay/backend/internal/config/analytics"
|
||||
"openreplay/backend/pkg/analytics/service"
|
||||
"openreplay/backend/pkg/logger"
|
||||
"openreplay/backend/pkg/objectstorage"
|
||||
"openreplay/backend/pkg/server/api"
|
||||
"openreplay/backend/pkg/server/keys"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type handlersImpl struct {
|
||||
|
|
@ -26,7 +30,7 @@ func (e *handlersImpl) GetAll() []*api.Description {
|
|||
{"/v1/analytics/{projectId}/dashboards/{id}", e.updateDashboard, "PUT"},
|
||||
{"/v1/analytics/{projectId}/dashboards/{id}", e.deleteDashboard, "DELETE"},
|
||||
{"/v1/analytics/{projectId}/cards", e.createCard, "POST"},
|
||||
{"/v1/analytics/{projectId}/cards", e.getCards, "GET"},
|
||||
{"/v1/analytics/{projectId}/cards", e.getCardsPaginated, "GET"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}", e.getCard, "GET"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}", e.updateCard, "PUT"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}", e.deleteCard, "DELETE"},
|
||||
|
|
@ -45,3 +49,18 @@ func NewHandlers(log logger.Logger, cfg *config.Config, responser *api.Responser
|
|||
service: service,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getIDFromRequest(r *http.Request, key string) (int, error) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars[key]
|
||||
if idStr == "" {
|
||||
return 0, fmt.Errorf("missing %s in request", key)
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid %s format", key)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,24 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CardBase Common fields for the Card entity
|
||||
type CardBase struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IsPublic bool `json:"isPublic" validate:"omitempty"`
|
||||
DefaultConfig map[string]any `json:"defaultConfig"`
|
||||
Thumbnail *string `json:"thumbnail" validate:"omitempty,url"`
|
||||
MetricType string `json:"metricType" validate:"required,oneof=timeseries table funnel"`
|
||||
MetricOf string `json:"metricOf" validate:"required,oneof=session_count user_count"`
|
||||
MetricFormat string `json:"metricFormat" validate:"required,oneof=default percentage"`
|
||||
ViewType string `json:"viewType" validate:"required,oneof=line_chart table_view"`
|
||||
MetricValue []string `json:"metricValue" validate:"omitempty"`
|
||||
SessionID *int64 `json:"sessionId" validate:"omitempty"`
|
||||
Series []CardSeries `json:"series" validate:"required,dive"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
IsPublic bool `json:"isPublic" validate:"omitempty"`
|
||||
DefaultConfig map[string]any `json:"defaultConfig"`
|
||||
Thumbnail *string `json:"thumbnail" validate:"omitempty,url"`
|
||||
MetricType string `json:"metricType" validate:"required,oneof=timeseries table funnel"`
|
||||
MetricOf string `json:"metricOf" validate:"required,oneof=session_count user_count"`
|
||||
MetricFormat string `json:"metricFormat" validate:"required,oneof=default percentage"`
|
||||
ViewType string `json:"viewType" validate:"required,oneof=line_chart table_view"`
|
||||
MetricValue []string `json:"metricValue" validate:"omitempty"`
|
||||
SessionID *int64 `json:"sessionId" validate:"omitempty"`
|
||||
Series []CardSeriesBase `json:"series" validate:"required,dive"`
|
||||
}
|
||||
|
||||
// Card Fields specific to database operations
|
||||
|
|
@ -31,9 +33,7 @@ type Card struct {
|
|||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||
}
|
||||
|
||||
type CardSeries struct {
|
||||
SeriesID int64 `json:"seriesId" validate:"omitempty"`
|
||||
MetricID int64 `json:"metricId" validate:"omitempty"`
|
||||
type CardSeriesBase struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedAt time.Time `json:"createdAt" validate:"omitempty"`
|
||||
DeletedAt *time.Time `json:"deletedAt" validate:"omitempty"`
|
||||
|
|
@ -41,6 +41,12 @@ type CardSeries struct {
|
|||
Filter SeriesFilter `json:"filter"`
|
||||
}
|
||||
|
||||
type CardSeries struct {
|
||||
SeriesID int64 `json:"seriesId" validate:"omitempty"`
|
||||
MetricID int64 `json:"metricId" validate:"omitempty"`
|
||||
CardSeriesBase
|
||||
}
|
||||
|
||||
type SeriesFilter struct {
|
||||
EventOrder string `json:"eventOrder" validate:"required,oneof=then or and"`
|
||||
Filters []FilterItem `json:"filters"`
|
||||
|
|
@ -62,6 +68,7 @@ type CardCreateRequest struct {
|
|||
|
||||
type CardGetResponse struct {
|
||||
Card
|
||||
Series []CardSeries `json:"series"`
|
||||
}
|
||||
|
||||
type CardUpdateRequest struct {
|
||||
|
|
@ -70,7 +77,11 @@ type CardUpdateRequest struct {
|
|||
|
||||
type GetCardsResponse struct {
|
||||
Cards []Card `json:"cards"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type GetCardsResponsePaginated struct {
|
||||
Cards []Card `json:"cards"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type DataPoint struct {
|
||||
|
|
@ -79,14 +90,122 @@ type DataPoint struct {
|
|||
}
|
||||
|
||||
type GetCardChartDataRequest struct {
|
||||
ProjectID int64 `json:"projectId" validate:"required"`
|
||||
MetricType string `json:"metricType" validate:"required,oneof=timeseries table funnel"`
|
||||
MetricOf string `json:"metricOf" validate:"required,oneof=session_count user_count"`
|
||||
ViewType string `json:"viewType" validate:"required,oneof=line_chart table_view"`
|
||||
MetricFormat string `json:"metricFormat" validate:"required,oneof=default percentage"`
|
||||
SessionID int64 `json:"sessionId" validate:"required"`
|
||||
Series []CardSeries `json:"series"`
|
||||
SessionID int64 `json:"sessionId"`
|
||||
Series []CardSeries `json:"series" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type GetCardChartDataResponse struct {
|
||||
Data []DataPoint `json:"data"`
|
||||
}
|
||||
|
||||
/************************************************************
|
||||
* CardListFilter and Sorter
|
||||
*/
|
||||
|
||||
// Supported filters, fields, and orders
|
||||
var (
|
||||
SupportedFilterKeys = map[string]bool{
|
||||
"name": true,
|
||||
"metric_type": true,
|
||||
"dashboard_ids": true,
|
||||
}
|
||||
SupportedSortFields = map[string]string{
|
||||
"name": "m.name",
|
||||
"created_at": "m.created_at",
|
||||
"metric_type": "m.metric_type",
|
||||
}
|
||||
SupportedSortOrders = map[string]bool{
|
||||
"asc": true,
|
||||
"desc": true,
|
||||
}
|
||||
)
|
||||
|
||||
// CardListFilter holds filtering criteria for listing cards.
|
||||
type CardListFilter struct {
|
||||
// Keys: "name" (string), "metric_type" (string), "dashboard_ids" ([]int)
|
||||
Filters map[string]interface{} `validate:"supportedFilters"`
|
||||
}
|
||||
|
||||
// CardListSort holds sorting criteria.
|
||||
type CardListSort struct {
|
||||
Field string `validate:"required,supportedSortField"`
|
||||
Order string `validate:"required,supportedSortOrder"`
|
||||
}
|
||||
|
||||
// Validator singleton
|
||||
var validate *validator.Validate
|
||||
|
||||
func GetValidator() *validator.Validate {
|
||||
if validate == nil {
|
||||
validate = validator.New()
|
||||
// Register custom validations
|
||||
_ = validate.RegisterValidation("supportedFilters", supportedFiltersValidator)
|
||||
_ = validate.RegisterValidation("supportedSortField", supportedSortFieldValidator)
|
||||
_ = validate.RegisterValidation("supportedSortOrder", supportedSortOrderValidator)
|
||||
}
|
||||
return validate
|
||||
}
|
||||
|
||||
func ValidateStruct(obj interface{}) error {
|
||||
return GetValidator().Struct(obj)
|
||||
}
|
||||
|
||||
// Custom validations
|
||||
func supportedFiltersValidator(fl validator.FieldLevel) bool {
|
||||
filters, ok := fl.Field().Interface().(map[string]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for k := range filters {
|
||||
if !SupportedFilterKeys[k] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func supportedSortFieldValidator(fl validator.FieldLevel) bool {
|
||||
field := strings.ToLower(fl.Field().String())
|
||||
_, ok := SupportedSortFields[field]
|
||||
return ok
|
||||
}
|
||||
|
||||
func supportedSortOrderValidator(fl validator.FieldLevel) bool {
|
||||
order := strings.ToLower(fl.Field().String())
|
||||
return SupportedSortOrders[order]
|
||||
}
|
||||
|
||||
// Filter helpers
|
||||
func (f *CardListFilter) GetNameFilter() *string {
|
||||
if val, ok := f.Filters["name"].(string); ok && val != "" {
|
||||
return &val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *CardListFilter) GetMetricTypeFilter() *string {
|
||||
if val, ok := f.Filters["metric_type"].(string); ok && val != "" {
|
||||
return &val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *CardListFilter) GetDashboardIDs() []int {
|
||||
if val, ok := f.Filters["dashboard_ids"].([]int); ok && len(val) > 0 {
|
||||
return val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort helpers
|
||||
func (s *CardListSort) GetSQLField() string {
|
||||
return SupportedSortFields[strings.ToLower(s.Field)]
|
||||
}
|
||||
|
||||
func (s *CardListSort) GetSQLOrder() string {
|
||||
return strings.ToUpper(s.Order)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"openreplay/backend/pkg/analytics/api/models"
|
||||
"openreplay/backend/pkg/db/postgres/pool"
|
||||
|
|
@ -15,12 +16,21 @@ type Service interface {
|
|||
CreateDashboard(projectId int, userId uint64, req *models.CreateDashboardRequest) (*models.GetDashboardResponse, error)
|
||||
UpdateDashboard(projectId int, dashboardId int, userId uint64, req *models.UpdateDashboardRequest) (*models.GetDashboardResponse, error)
|
||||
DeleteDashboard(projectId int, dashboardId int, userId uint64) error
|
||||
GetCard(projectId int, cardId int) (*models.CardGetResponse, error)
|
||||
GetCardWithSeries(projectId int, cardId int) (*models.CardGetResponse, error)
|
||||
GetCards(projectId int) (*models.GetCardsResponse, error)
|
||||
GetCardsPaginated(projectId int, filters models.CardListFilter, sort models.CardListSort, limit int, offset int) (*models.GetCardsResponsePaginated, error)
|
||||
CreateCard(projectId int, userId uint64, req *models.CardCreateRequest) (*models.CardGetResponse, error)
|
||||
UpdateCard(projectId int, cardId int64, userId uint64, req *models.CardUpdateRequest) (*models.CardGetResponse, error)
|
||||
DeleteCard(projectId int, cardId int64, userId uint64) error
|
||||
GetCardChartData(projectId int, userId uint64, req *models.GetCardChartDataRequest) ([]models.DataPoint, error)
|
||||
}
|
||||
|
||||
type serviceImpl struct {
|
||||
log logger.Logger
|
||||
pgconn pool.Pool
|
||||
storage objectstorage.ObjectStorage
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewService(log logger.Logger, conn pool.Pool, storage objectstorage.ObjectStorage) (Service, error) {
|
||||
|
|
@ -37,5 +47,6 @@ func NewService(log logger.Logger, conn pool.Pool, storage objectstorage.ObjectS
|
|||
log: log,
|
||||
pgconn: conn,
|
||||
storage: storage,
|
||||
ctx: context.Background(),
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
428
backend/pkg/analytics/service/card-service.go
Normal file
428
backend/pkg/analytics/service/card-service.go
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/lib/pq"
|
||||
"openreplay/backend/pkg/analytics/api/models"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s serviceImpl) CreateCard(projectId int, userID uint64, req *models.CardCreateRequest) (*models.CardGetResponse, error) {
|
||||
if req.MetricValue == nil {
|
||||
req.MetricValue = []string{}
|
||||
}
|
||||
|
||||
tx, err := s.pgconn.Begin() // Start transaction
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start transaction: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Insert the card
|
||||
sql := `
|
||||
INSERT INTO public.metrics (project_id, user_id, name, metric_type, view_type, metric_of, metric_value, metric_format, is_public)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING metric_id, project_id, user_id, name, metric_type, view_type, metric_of, metric_value, metric_format, is_public, created_at, edited_at`
|
||||
|
||||
card := &models.CardGetResponse{}
|
||||
err = tx.QueryRow(
|
||||
ctx, sql,
|
||||
projectId, userID, req.Name, req.MetricType, req.ViewType, req.MetricOf, req.MetricValue, req.MetricFormat, req.IsPublic,
|
||||
).Scan(
|
||||
&card.CardID,
|
||||
&card.ProjectID,
|
||||
&card.UserID,
|
||||
&card.Name,
|
||||
&card.MetricType,
|
||||
&card.ViewType,
|
||||
&card.MetricOf,
|
||||
&card.MetricValue,
|
||||
&card.MetricFormat,
|
||||
&card.IsPublic,
|
||||
&card.CreatedAt,
|
||||
&card.EditedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create card: %w", err)
|
||||
}
|
||||
|
||||
// Create series for the card
|
||||
seriesList := s.CreateSeries(ctx, tx, card.CardID, req.Series)
|
||||
if len(seriesList) != len(req.Series) {
|
||||
return nil, fmt.Errorf("not all series were created successfully")
|
||||
}
|
||||
|
||||
card.Series = seriesList
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) CreateSeries(ctx context.Context, tx pgx.Tx, metricId int64, series []models.CardSeriesBase) []models.CardSeries {
|
||||
if len(series) == 0 {
|
||||
return nil // No series to create
|
||||
}
|
||||
|
||||
// Batch insert for better performance
|
||||
sql := `
|
||||
INSERT INTO public.metric_series (metric_id, name, index, filter) VALUES %s
|
||||
RETURNING series_id, metric_id, name, index, filter`
|
||||
|
||||
// Generate the VALUES placeholders dynamically
|
||||
var values []string
|
||||
var args []interface{}
|
||||
for i, ser := range series {
|
||||
values = append(values, fmt.Sprintf("($%d, $%d, $%d, $%d)", i*4+1, i*4+2, i*4+3, i*4+4))
|
||||
|
||||
filterJSON, err := json.Marshal(ser.Filter) // Convert struct to JSON
|
||||
if err != nil {
|
||||
s.log.Error(ctx, "failed to serialize filter to JSON: %v", err)
|
||||
return nil
|
||||
}
|
||||
fmt.Println(string(filterJSON))
|
||||
args = append(args, metricId, ser.Name, i, string(filterJSON))
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(sql, strings.Join(values, ","))
|
||||
s.log.Info(ctx, "Executing query: %s with args: %v", query, args)
|
||||
|
||||
rows, err := tx.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
s.log.Error(ctx, "failed to execute batch insert for series: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if rows.Err() != nil {
|
||||
s.log.Error(ctx, "Query returned an error: %v", rows.Err())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect inserted series
|
||||
var seriesList []models.CardSeries
|
||||
for rows.Next() {
|
||||
cardSeries := models.CardSeries{}
|
||||
if err := rows.Scan(&cardSeries.SeriesID, &cardSeries.MetricID, &cardSeries.Name, &cardSeries.Index, &cardSeries.Filter); err != nil {
|
||||
s.log.Error(ctx, "failed to scan series: %v", err)
|
||||
continue
|
||||
}
|
||||
seriesList = append(seriesList, cardSeries)
|
||||
}
|
||||
|
||||
return seriesList
|
||||
}
|
||||
|
||||
func (s serviceImpl) GetCard(projectId int, cardID int) (*models.CardGetResponse, error) {
|
||||
sql :=
|
||||
`SELECT metric_id, project_id, user_id, name, metric_type, view_type, metric_of, metric_value, metric_format, is_public, created_at, edited_at
|
||||
FROM public.metrics
|
||||
WHERE metric_id = $1 AND project_id = $2 AND deleted_at IS NULL`
|
||||
|
||||
card := &models.CardGetResponse{}
|
||||
err := s.pgconn.QueryRow(sql, cardID, projectId).Scan(
|
||||
&card.CardID, &card.ProjectID, &card.UserID, &card.Name, &card.MetricType, &card.ViewType, &card.MetricOf, &card.MetricValue, &card.MetricFormat, &card.IsPublic, &card.CreatedAt, &card.EditedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get card: %w", err)
|
||||
}
|
||||
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) GetCardWithSeries(projectId int, cardID int) (*models.CardGetResponse, error) {
|
||||
sql := `
|
||||
SELECT m.metric_id, m.project_id, m.user_id, m.name, m.metric_type, m.view_type, m.metric_of,
|
||||
m.metric_value, m.metric_format, m.is_public, m.created_at, m.edited_at,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'seriesId', ms.series_id,
|
||||
'index', ms.index,
|
||||
'name', ms.name,
|
||||
'filter', ms.filter
|
||||
)
|
||||
) FILTER (WHERE ms.series_id IS NOT NULL), '[]'
|
||||
) AS series
|
||||
FROM public.metrics m
|
||||
LEFT JOIN public.metric_series ms ON m.metric_id = ms.metric_id
|
||||
WHERE m.metric_id = $1 AND m.project_id = $2 AND m.deleted_at IS NULL
|
||||
GROUP BY m.metric_id, m.project_id, m.user_id, m.name, m.metric_type, m.view_type,
|
||||
m.metric_of, m.metric_value, m.metric_format, m.is_public, m.created_at, m.edited_at
|
||||
`
|
||||
|
||||
card := &models.CardGetResponse{}
|
||||
var seriesJSON []byte
|
||||
err := s.pgconn.QueryRow(sql, cardID, projectId).Scan(
|
||||
&card.CardID, &card.ProjectID, &card.UserID, &card.Name, &card.MetricType, &card.ViewType, &card.MetricOf,
|
||||
&card.MetricValue, &card.MetricFormat, &card.IsPublic, &card.CreatedAt, &card.EditedAt, &seriesJSON,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get card: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(seriesJSON, &card.Series); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal series: %w", err)
|
||||
}
|
||||
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) GetCards(projectId int) (*models.GetCardsResponse, error) {
|
||||
sql := `
|
||||
SELECT metric_id, project_id, user_id, name, metric_type, view_type, metric_of, metric_value, metric_format, is_public, created_at, edited_at
|
||||
FROM public.metrics
|
||||
WHERE project_id = $1 AND deleted_at IS NULL`
|
||||
|
||||
rows, err := s.pgconn.Query(sql, projectId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cards: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
cards := make([]models.Card, 0)
|
||||
for rows.Next() {
|
||||
card := models.Card{}
|
||||
if err := rows.Scan(
|
||||
&card.CardID, &card.ProjectID, &card.UserID, &card.Name, &card.MetricType, &card.ViewType, &card.MetricOf,
|
||||
&card.MetricValue, &card.MetricFormat, &card.IsPublic, &card.CreatedAt, &card.EditedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan card: %w", err)
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
|
||||
return &models.GetCardsResponse{Cards: cards}, nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) GetCardsPaginated(
|
||||
projectId int,
|
||||
filters models.CardListFilter,
|
||||
sort models.CardListSort,
|
||||
limit,
|
||||
offset int,
|
||||
) (*models.GetCardsResponsePaginated, error) {
|
||||
// Validate inputs
|
||||
if err := models.ValidateStruct(filters); err != nil {
|
||||
return nil, fmt.Errorf("invalid filters: %w", err)
|
||||
}
|
||||
if err := models.ValidateStruct(sort); err != nil {
|
||||
return nil, fmt.Errorf("invalid sort: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
conditions []string
|
||||
params []interface{}
|
||||
paramIndex = 1
|
||||
)
|
||||
|
||||
// Project ID is mandatory
|
||||
conditions = append(conditions, fmt.Sprintf("m.project_id = $%d", paramIndex))
|
||||
params = append(params, projectId)
|
||||
paramIndex++
|
||||
|
||||
// Apply filters
|
||||
if nameFilter := filters.GetNameFilter(); nameFilter != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("m.name ILIKE $%d", paramIndex))
|
||||
params = append(params, "%"+*nameFilter+"%")
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
if typeFilter := filters.GetMetricTypeFilter(); typeFilter != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("m.metric_type = $%d", paramIndex))
|
||||
params = append(params, *typeFilter)
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
var joinClause string
|
||||
if dashboardIDs := filters.GetDashboardIDs(); len(dashboardIDs) > 0 {
|
||||
joinClause = "LEFT JOIN public.dashboard_widgets dw ON m.metric_id = dw.metric_id"
|
||||
conditions = append(conditions, fmt.Sprintf("dw.dashboard_id = ANY($%d)", paramIndex))
|
||||
params = append(params, pq.Array(dashboardIDs))
|
||||
paramIndex++
|
||||
}
|
||||
|
||||
// Exclude deleted
|
||||
conditions = append(conditions, "m.deleted_at IS NULL")
|
||||
|
||||
whereClause := "WHERE " + strings.Join(conditions, " AND ")
|
||||
|
||||
orderClause := fmt.Sprintf("ORDER BY %s %s", sort.GetSQLField(), sort.GetSQLOrder())
|
||||
limitClause := fmt.Sprintf("LIMIT $%d", paramIndex)
|
||||
params = append(params, limit)
|
||||
paramIndex++
|
||||
offsetClause := fmt.Sprintf("OFFSET $%d", paramIndex)
|
||||
params = append(params, offset)
|
||||
paramIndex++
|
||||
|
||||
// Main query
|
||||
query := fmt.Sprintf(`
|
||||
SELECT m.metric_id, m.project_id, m.user_id, m.name, m.metric_type, m.view_type, m.metric_of,
|
||||
m.metric_value, m.metric_format, m.is_public, m.created_at, m.edited_at
|
||||
FROM public.metrics m
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
`, joinClause, whereClause, orderClause, limitClause, offsetClause)
|
||||
|
||||
rows, err := s.pgconn.Query(query, params...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cards: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var cards []models.Card
|
||||
for rows.Next() {
|
||||
var card models.Card
|
||||
if err := rows.Scan(
|
||||
&card.CardID, &card.ProjectID, &card.UserID, &card.Name, &card.MetricType, &card.ViewType, &card.MetricOf,
|
||||
&card.MetricValue, &card.MetricFormat, &card.IsPublic, &card.CreatedAt, &card.EditedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan card: %w", err)
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
|
||||
// Count total (exclude limit, offset, order)
|
||||
countParams := params[0 : len(params)-2] // all filter params without limit/offset
|
||||
countQuery := fmt.Sprintf(`
|
||||
SELECT COUNT(*)
|
||||
FROM public.metrics m
|
||||
%s
|
||||
%s
|
||||
`, joinClause, whereClause)
|
||||
|
||||
var total int
|
||||
if err := s.pgconn.QueryRow(countQuery, countParams...).Scan(&total); err != nil {
|
||||
return nil, fmt.Errorf("failed to get total count: %w", err)
|
||||
}
|
||||
|
||||
return &models.GetCardsResponsePaginated{
|
||||
Cards: cards,
|
||||
Total: total,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) UpdateCard(projectId int, cardID int64, userID uint64, req *models.CardUpdateRequest) (*models.CardGetResponse, error) {
|
||||
if req.MetricValue == nil {
|
||||
req.MetricValue = []string{}
|
||||
}
|
||||
|
||||
tx, err := s.pgconn.Begin() // Start transaction
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to start transaction: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err := tx.Commit(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Update the card
|
||||
sql := `
|
||||
UPDATE public.metrics
|
||||
SET name = $1, metric_type = $2, view_type = $3, metric_of = $4, metric_value = $5, metric_format = $6, is_public = $7
|
||||
WHERE metric_id = $8 AND project_id = $9 AND deleted_at IS NULL
|
||||
RETURNING metric_id, project_id, user_id, name, metric_type, view_type, metric_of, metric_value, metric_format, is_public, created_at, edited_at`
|
||||
|
||||
card := &models.CardGetResponse{}
|
||||
err = tx.QueryRow(ctx, sql,
|
||||
req.Name, req.MetricType, req.ViewType, req.MetricOf, req.MetricValue, req.MetricFormat, req.IsPublic, cardID, projectId,
|
||||
).Scan(
|
||||
&card.CardID, &card.ProjectID, &card.UserID, &card.Name, &card.MetricType, &card.ViewType, &card.MetricOf,
|
||||
&card.MetricValue, &card.MetricFormat, &card.IsPublic, &card.CreatedAt, &card.EditedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update card: %w", err)
|
||||
}
|
||||
|
||||
// delete all series for the card and create new ones
|
||||
err = s.DeleteCardSeries(card.CardID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete series: %w", err)
|
||||
}
|
||||
|
||||
seriesList := s.CreateSeries(ctx, tx, card.CardID, req.Series)
|
||||
if len(seriesList) != len(req.Series) {
|
||||
return nil, fmt.Errorf("not all series were created successfully")
|
||||
}
|
||||
|
||||
card.Series = seriesList
|
||||
return card, nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) DeleteCardSeries(cardId int64) error {
|
||||
sql := `DELETE FROM public.metric_series WHERE metric_id = $1`
|
||||
err := s.pgconn.Exec(sql, cardId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete series: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) DeleteCard(projectId int, cardID int64, userID uint64) error {
|
||||
sql := `
|
||||
UPDATE public.metrics
|
||||
SET deleted_at = now()
|
||||
WHERE metric_id = $1 AND project_id = $2 AND user_id = $3 AND deleted_at IS NULL`
|
||||
|
||||
err := s.pgconn.Exec(sql, cardID, projectId, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete card: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s serviceImpl) GetCardChartData(projectId int, userID uint64, req *models.GetCardChartDataRequest) ([]models.DataPoint, error) {
|
||||
jsonInput := `
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"timestamp": 1733934939000,
|
||||
"Series A": 100,
|
||||
"Series B": 200
|
||||
},
|
||||
{
|
||||
"timestamp": 1733935939000,
|
||||
"Series A": 150,
|
||||
"Series B": 250
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
var resp models.GetCardChartDataResponse
|
||||
if err := json.Unmarshal([]byte(jsonInput), &resp); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return resp.Data, nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue