diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index bfc48b0006..debb780425 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -11,6 +11,8 @@ import ( "github.com/SigNoz/signoz/ee/query-service/license" "github.com/SigNoz/signoz/ee/query-service/usage" "github.com/SigNoz/signoz/pkg/alertmanager" + "github.com/SigNoz/signoz/pkg/modules/preference" + preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core" baseapp "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations" "github.com/SigNoz/signoz/pkg/query-service/app/integrations" @@ -21,6 +23,7 @@ import ( rules "github.com/SigNoz/signoz/pkg/query-service/rules" "github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/preferencetypes" "github.com/SigNoz/signoz/pkg/version" "github.com/gorilla/mux" ) @@ -54,6 +57,7 @@ type APIHandler struct { // NewAPIHandler returns an APIHandler func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, error) { + preference := preference.NewAPI(preferencecore.NewPreference(preferencecore.NewStore(signoz.SQLStore), preferencetypes.NewDefaultPreferenceMap())) baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{ Reader: opts.DataConnector, @@ -71,6 +75,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, UseTraceNewSchema: opts.UseTraceNewSchema, AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager), Signoz: signoz, + Preference: preference, }) if err != nil { diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 872d1c2005..9c77f8abae 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -44,7 +44,6 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline" "github.com/SigNoz/signoz/pkg/query-service/app/opamp" opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model" - "github.com/SigNoz/signoz/pkg/query-service/app/preferences" "github.com/SigNoz/signoz/pkg/query-service/cache" baseconst "github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/query-service/healthcheck" @@ -116,10 +115,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - if err := preferences.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { - return nil, err - } - if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore); err != nil { return nil, err } diff --git a/ee/sqlstore/postgressqlstore/dialect.go b/ee/sqlstore/postgressqlstore/dialect.go index 62adf3ddb5..fead4e0fa7 100644 --- a/ee/sqlstore/postgressqlstore/dialect.go +++ b/ee/sqlstore/postgressqlstore/dialect.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/SigNoz/signoz/pkg/errors" "github.com/uptrace/bun" ) @@ -14,6 +15,16 @@ var ( Text = "text" ) +var ( + Org = "org" + User = "user" +) + +var ( + OrgReference = `("org_id") REFERENCES "organizations" ("id")` + UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE` +) + type dialect struct { } @@ -225,7 +236,10 @@ func (dialect *dialect) AddNotNullDefaultToColumn(ctx context.Context, bun bun.I return nil } -func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error { +func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error { + if reference == "" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference") + } oldTableName := bun.Dialect().Tables().Get(reflect.TypeOf(oldModel)).Name newTableName := bun.Dialect().Tables().Get(reflect.TypeOf(newModel)).Name @@ -237,11 +251,74 @@ func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldMo return nil } + fkReference := "" + if reference == Org { + fkReference = OrgReference + } else if reference == User { + fkReference = UserReference + } + _, err = bun. NewCreateTable(). IfNotExists(). Model(newModel). - ForeignKey(`("org_id") REFERENCES "organizations" ("id")`). + ForeignKey(fkReference). + Exec(ctx) + + if err != nil { + return err + } + + err = cb(ctx) + if err != nil { + return err + } + + _, err = bun. + NewDropTable(). + IfExists(). + Model(oldModel). + Exec(ctx) + if err != nil { + return err + } + + _, err = bun. + ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME TO %s", newTableName, oldTableName)) + if err != nil { + return err + } + + return nil +} + +func (dialect *dialect) AddPrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error { + if reference == "" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference") + } + oldTableName := bun.Dialect().Tables().Get(reflect.TypeOf(oldModel)).Name + newTableName := bun.Dialect().Tables().Get(reflect.TypeOf(newModel)).Name + + identityExists, err := dialect.ColumnExists(ctx, bun, oldTableName, Identity) + if err != nil { + return err + } + if identityExists { + return nil + } + + fkReference := "" + if reference == Org { + fkReference = OrgReference + } else if reference == User { + fkReference = UserReference + } + + _, err = bun. + NewCreateTable(). + IfNotExists(). + Model(newModel). + ForeignKey(fkReference). Exec(ctx) if err != nil { diff --git a/pkg/modules/preference/api.go b/pkg/modules/preference/api.go new file mode 100644 index 0000000000..973fe24030 --- /dev/null +++ b/pkg/modules/preference/api.go @@ -0,0 +1,149 @@ +package preference + +import ( + "encoding/json" + "net/http" + + errorsV2 "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/preferencetypes" + "github.com/gorilla/mux" +) + +type API interface { + GetOrgPreference(http.ResponseWriter, *http.Request) + UpdateOrgPreference(http.ResponseWriter, *http.Request) + GetAllOrgPreferences(http.ResponseWriter, *http.Request) + + GetUserPreference(http.ResponseWriter, *http.Request) + UpdateUserPreference(http.ResponseWriter, *http.Request) + GetAllUserPreferences(http.ResponseWriter, *http.Request) +} + +type preferenceAPI struct { + usecase Usecase +} + +func NewAPI(usecase Usecase) API { + return &preferenceAPI{usecase: usecase} +} + +func (p *preferenceAPI) GetOrgPreference(rw http.ResponseWriter, r *http.Request) { + preferenceId := mux.Vars(r)["preferenceId"] + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(rw, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + preference, err := p.usecase.GetOrgPreference( + r.Context(), preferenceId, claims.OrgID, + ) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, preference) +} + +func (p *preferenceAPI) UpdateOrgPreference(rw http.ResponseWriter, r *http.Request) { + preferenceId := mux.Vars(r)["preferenceId"] + req := preferencetypes.UpdatablePreference{} + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(rw, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + + err := json.NewDecoder(r.Body).Decode(&req) + + if err != nil { + render.Error(rw, err) + return + } + err = p.usecase.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, claims.OrgID) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusNoContent, nil) +} + +func (p *preferenceAPI) GetAllOrgPreferences(rw http.ResponseWriter, r *http.Request) { + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(rw, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + preferences, err := p.usecase.GetAllOrgPreferences( + r.Context(), claims.OrgID, + ) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, preferences) +} + +func (p *preferenceAPI) GetUserPreference(rw http.ResponseWriter, r *http.Request) { + preferenceId := mux.Vars(r)["preferenceId"] + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(rw, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + + preference, err := p.usecase.GetUserPreference( + r.Context(), preferenceId, claims.OrgID, claims.UserID, + ) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, preference) +} + +func (p *preferenceAPI) UpdateUserPreference(rw http.ResponseWriter, r *http.Request) { + preferenceId := mux.Vars(r)["preferenceId"] + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(rw, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + req := preferencetypes.UpdatablePreference{} + + err := json.NewDecoder(r.Body).Decode(&req) + + if err != nil { + render.Error(rw, err) + return + } + err = p.usecase.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, claims.UserID) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusNoContent, nil) +} + +func (p *preferenceAPI) GetAllUserPreferences(rw http.ResponseWriter, r *http.Request) { + claims, ok := authtypes.ClaimsFromContext(r.Context()) + if !ok { + render.Error(rw, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) + return + } + preferences, err := p.usecase.GetAllUserPreferences( + r.Context(), claims.OrgID, claims.UserID, + ) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusOK, preferences) +} diff --git a/pkg/modules/preference/core/preference.go b/pkg/modules/preference/core/preference.go new file mode 100644 index 0000000000..3a447e0a1d --- /dev/null +++ b/pkg/modules/preference/core/preference.go @@ -0,0 +1,278 @@ +package core + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/modules/preference" + "github.com/SigNoz/signoz/pkg/types/preferencetypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type usecase struct { + store preferencetypes.PreferenceStore + defaultMap map[string]preferencetypes.Preference +} + +func NewPreference(store preferencetypes.PreferenceStore, defaultMap map[string]preferencetypes.Preference) preference.Usecase { + return &usecase{store: store, defaultMap: defaultMap} +} + +func (usecase *usecase) GetOrgPreference(ctx context.Context, preferenceID string, orgID string) (*preferencetypes.GettablePreference, error) { + preference, seen := usecase.defaultMap[preferenceID] + if !seen { + return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("no such preferenceID exists: %s", preferenceID)) + } + + isPreferenceEnabled := preference.IsEnabledForScope(preferencetypes.OrgAllowedScope) + if !isPreferenceEnabled { + return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("preference is not enabled at org scope: %s", preferenceID)) + } + + orgPreference, err := usecase.store.GetOrgPreference(ctx, orgID, preferenceID) + if err != nil { + if err == sql.ErrNoRows { + return &preferencetypes.GettablePreference{ + PreferenceID: preferenceID, + PreferenceValue: preference.DefaultValue, + }, nil + } + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, fmt.Sprintf("error in fetching the org preference: %s", preferenceID)) + } + + return &preferencetypes.GettablePreference{ + PreferenceID: preferenceID, + PreferenceValue: preference.SanitizeValue(orgPreference.PreferenceValue), + }, nil +} + +func (usecase *usecase) UpdateOrgPreference(ctx context.Context, preferenceID string, preferenceValue interface{}, orgID string) error { + preference, seen := usecase.defaultMap[preferenceID] + if !seen { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("no such preferenceID exists: %s", preferenceID)) + } + + isPreferenceEnabled := preference.IsEnabledForScope(preferencetypes.OrgAllowedScope) + if !isPreferenceEnabled { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("preference is not enabled at org scope: %s", preferenceID)) + } + + err := preference.IsValidValue(preferenceValue) + if err != nil { + return err + } + + storablePreferenceValue, encodeErr := json.Marshal(preferenceValue) + if encodeErr != nil { + return errors.Wrapf(encodeErr, errors.TypeInvalidInput, errors.CodeInvalidInput, "error in encoding the preference value") + } + + orgPreference, dberr := usecase.store.GetOrgPreference(ctx, orgID, preferenceID) + if dberr != nil && dberr != sql.ErrNoRows { + return errors.Wrapf(dberr, errors.TypeInternal, errors.CodeInternal, "error in getting the preference value") + } + + if dberr != nil { + orgPreference.ID = valuer.GenerateUUID() + orgPreference.PreferenceID = preferenceID + orgPreference.PreferenceValue = string(storablePreferenceValue) + orgPreference.OrgID = orgID + } else { + orgPreference.PreferenceValue = string(storablePreferenceValue) + } + + dberr = usecase.store.UpsertOrgPreference(ctx, orgPreference) + if dberr != nil { + return errors.Wrapf(dberr, errors.TypeInternal, errors.CodeInternal, "error in setting the preference value") + } + + return nil +} + +func (usecase *usecase) GetAllOrgPreferences(ctx context.Context, orgID string) ([]*preferencetypes.PreferenceWithValue, error) { + allOrgPreferences := []*preferencetypes.PreferenceWithValue{} + orgPreferences, err := usecase.store.GetAllOrgPreferences(ctx, orgID) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error in setting all org preference values") + } + + preferenceValueMap := map[string]interface{}{} + for _, preferenceValue := range orgPreferences { + preferenceValueMap[preferenceValue.PreferenceID] = preferenceValue.PreferenceValue + } + + for _, preference := range usecase.defaultMap { + isEnabledForOrgScope := preference.IsEnabledForScope(preferencetypes.OrgAllowedScope) + if isEnabledForOrgScope { + preferenceWithValue := &preferencetypes.PreferenceWithValue{} + preferenceWithValue.Key = preference.Key + preferenceWithValue.Name = preference.Name + preferenceWithValue.Description = preference.Description + preferenceWithValue.AllowedScopes = preference.AllowedScopes + preferenceWithValue.AllowedValues = preference.AllowedValues + preferenceWithValue.DefaultValue = preference.DefaultValue + preferenceWithValue.Range = preference.Range + preferenceWithValue.ValueType = preference.ValueType + preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues + value, seen := preferenceValueMap[preference.Key] + + if seen { + preferenceWithValue.Value = value + } else { + preferenceWithValue.Value = preference.DefaultValue + } + + preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value) + allOrgPreferences = append(allOrgPreferences, preferenceWithValue) + } + } + return allOrgPreferences, nil +} + +func (usecase *usecase) GetUserPreference(ctx context.Context, preferenceID string, orgID string, userID string) (*preferencetypes.GettablePreference, error) { + preference, seen := usecase.defaultMap[preferenceID] + if !seen { + return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("no such preferenceID exists: %s", preferenceID)) + } + + preferenceValue := preferencetypes.GettablePreference{ + PreferenceID: preferenceID, + PreferenceValue: preference.DefaultValue, + } + + isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(preferencetypes.UserAllowedScope) + if !isPreferenceEnabledAtUserScope { + return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("preference is not enabled at user scope: %s", preferenceID)) + } + + isPreferenceEnabledAtOrgScope := preference.IsEnabledForScope(preferencetypes.OrgAllowedScope) + if isPreferenceEnabledAtOrgScope { + orgPreference, err := usecase.store.GetOrgPreference(ctx, orgID, preferenceID) + if err != nil && err != sql.ErrNoRows { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, fmt.Sprintf("error in fetching the org preference: %s", preferenceID)) + } + if err == nil { + preferenceValue.PreferenceValue = orgPreference.PreferenceValue + } + } + + userPreference, err := usecase.store.GetUserPreference(ctx, userID, preferenceID) + if err != nil && err != sql.ErrNoRows { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, fmt.Sprintf("error in fetching the user preference: %s", preferenceID)) + } + + if err == nil { + preferenceValue.PreferenceValue = userPreference.PreferenceValue + } + + return &preferencetypes.GettablePreference{ + PreferenceID: preferenceValue.PreferenceID, + PreferenceValue: preference.SanitizeValue(preferenceValue.PreferenceValue), + }, nil +} + +func (usecase *usecase) UpdateUserPreference(ctx context.Context, preferenceID string, preferenceValue interface{}, userID string) error { + preference, seen := usecase.defaultMap[preferenceID] + if !seen { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("no such preferenceID exists: %s", preferenceID)) + } + + isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(preferencetypes.UserAllowedScope) + if !isPreferenceEnabledAtUserScope { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("preference is not enabled at user scope: %s", preferenceID)) + } + + err := preference.IsValidValue(preferenceValue) + if err != nil { + return err + } + + storablePreferenceValue, encodeErr := json.Marshal(preferenceValue) + if encodeErr != nil { + return errors.Wrapf(encodeErr, errors.TypeInvalidInput, errors.CodeInvalidInput, "error in encoding the preference value") + } + + userPreference, dberr := usecase.store.GetUserPreference(ctx, userID, preferenceID) + if dberr != nil && dberr != sql.ErrNoRows { + return errors.Wrapf(dberr, errors.TypeInternal, errors.CodeInternal, "error in getting the preference value") + } + + if dberr != nil { + userPreference.ID = valuer.GenerateUUID() + userPreference.PreferenceID = preferenceID + userPreference.PreferenceValue = string(storablePreferenceValue) + userPreference.UserID = userID + } else { + userPreference.PreferenceValue = string(storablePreferenceValue) + } + + dberr = usecase.store.UpsertUserPreference(ctx, userPreference) + if dberr != nil { + return errors.Wrapf(dberr, errors.TypeInternal, errors.CodeInternal, "error in setting the preference value") + } + + return nil +} + +func (usecase *usecase) GetAllUserPreferences(ctx context.Context, orgID string, userID string) ([]*preferencetypes.PreferenceWithValue, error) { + allUserPreferences := []*preferencetypes.PreferenceWithValue{} + + orgPreferences, err := usecase.store.GetAllOrgPreferences(ctx, orgID) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error in setting all org preference values") + } + + preferenceOrgValueMap := map[string]interface{}{} + for _, preferenceValue := range orgPreferences { + preferenceOrgValueMap[preferenceValue.PreferenceID] = preferenceValue.PreferenceValue + } + + userPreferences, err := usecase.store.GetAllUserPreferences(ctx, userID) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error in setting all user preference values") + } + + preferenceUserValueMap := map[string]interface{}{} + for _, preferenceValue := range userPreferences { + preferenceUserValueMap[preferenceValue.PreferenceID] = preferenceValue.PreferenceValue + } + + for _, preference := range usecase.defaultMap { + isEnabledForUserScope := preference.IsEnabledForScope(preferencetypes.UserAllowedScope) + + if isEnabledForUserScope { + preferenceWithValue := &preferencetypes.PreferenceWithValue{} + preferenceWithValue.Key = preference.Key + preferenceWithValue.Name = preference.Name + preferenceWithValue.Description = preference.Description + preferenceWithValue.AllowedScopes = preference.AllowedScopes + preferenceWithValue.AllowedValues = preference.AllowedValues + preferenceWithValue.DefaultValue = preference.DefaultValue + preferenceWithValue.Range = preference.Range + preferenceWithValue.ValueType = preference.ValueType + preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues + preferenceWithValue.Value = preference.DefaultValue + + isEnabledForOrgScope := preference.IsEnabledForScope(preferencetypes.OrgAllowedScope) + if isEnabledForOrgScope { + value, seen := preferenceOrgValueMap[preference.Key] + if seen { + preferenceWithValue.Value = value + } + } + + value, seen := preferenceUserValueMap[preference.Key] + + if seen { + preferenceWithValue.Value = value + } + + preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value) + allUserPreferences = append(allUserPreferences, preferenceWithValue) + } + } + return allUserPreferences, nil +} diff --git a/pkg/modules/preference/core/store.go b/pkg/modules/preference/core/store.go new file mode 100644 index 0000000000..329a308c4b --- /dev/null +++ b/pkg/modules/preference/core/store.go @@ -0,0 +1,116 @@ +package core + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types/preferencetypes" +) + +type store struct { + store sqlstore.SQLStore +} + +func NewStore(db sqlstore.SQLStore) preferencetypes.PreferenceStore { + return &store{store: db} +} + +func (store *store) GetOrgPreference(ctx context.Context, orgID string, preferenceID string) (*preferencetypes.StorableOrgPreference, error) { + orgPreference := new(preferencetypes.StorableOrgPreference) + err := store. + store. + BunDB(). + NewSelect(). + Model(orgPreference). + Where("preference_id = ?", preferenceID). + Where("org_id = ?", orgID). + Scan(ctx) + + if err != nil { + return orgPreference, err + } + + return orgPreference, nil +} + +func (store *store) GetAllOrgPreferences(ctx context.Context, orgID string) ([]*preferencetypes.StorableOrgPreference, error) { + orgPreferences := make([]*preferencetypes.StorableOrgPreference, 0) + err := store. + store. + BunDB(). + NewSelect(). + Model(&orgPreferences). + Where("org_id = ?", orgID). + Scan(ctx) + + if err != nil { + return orgPreferences, err + } + + return orgPreferences, nil +} + +func (store *store) UpsertOrgPreference(ctx context.Context, orgPreference *preferencetypes.StorableOrgPreference) error { + _, err := store. + store. + BunDB(). + NewInsert(). + Model(orgPreference). + On("CONFLICT (id) DO UPDATE"). + Exec(ctx) + if err != nil { + return err + } + + return nil +} + +func (store *store) GetUserPreference(ctx context.Context, userID string, preferenceID string) (*preferencetypes.StorableUserPreference, error) { + userPreference := new(preferencetypes.StorableUserPreference) + err := store. + store. + BunDB(). + NewSelect(). + Model(userPreference). + Where("preference_id = ?", preferenceID). + Where("user_id = ?", userID). + Scan(ctx) + + if err != nil { + return userPreference, err + } + + return userPreference, nil +} + +func (store *store) GetAllUserPreferences(ctx context.Context, userID string) ([]*preferencetypes.StorableUserPreference, error) { + userPreferences := make([]*preferencetypes.StorableUserPreference, 0) + err := store. + store. + BunDB(). + NewSelect(). + Model(&userPreferences). + Where("user_id = ?", userID). + Scan(ctx) + + if err != nil { + return userPreferences, err + } + + return userPreferences, nil +} + +func (store *store) UpsertUserPreference(ctx context.Context, userPreference *preferencetypes.StorableUserPreference) error { + _, err := store. + store. + BunDB(). + NewInsert(). + Model(userPreference). + On("CONFLICT (id) DO UPDATE"). + Exec(ctx) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/modules/preference/usecase.go b/pkg/modules/preference/usecase.go new file mode 100644 index 0000000000..d4b2c934ee --- /dev/null +++ b/pkg/modules/preference/usecase.go @@ -0,0 +1,17 @@ +package preference + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/types/preferencetypes" +) + +type Usecase interface { + GetOrgPreference(ctx context.Context, preferenceId string, orgId string) (*preferencetypes.GettablePreference, error) + UpdateOrgPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, orgId string) error + GetAllOrgPreferences(ctx context.Context, orgId string) ([]*preferencetypes.PreferenceWithValue, error) + + GetUserPreference(ctx context.Context, preferenceId string, orgId string, userId string) (*preferencetypes.GettablePreference, error) + UpdateUserPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, userId string) error + GetAllUserPreferences(ctx context.Context, orgId string, userId string) ([]*preferencetypes.PreferenceWithValue, error) +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 05d4c52299..cae4367707 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -21,6 +21,7 @@ import ( "github.com/SigNoz/signoz/pkg/alertmanager" errorsV2 "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/modules/preference" "github.com/SigNoz/signoz/pkg/query-service/app/metricsexplorer" "github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/valuer" @@ -44,7 +45,6 @@ import ( logsv4 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v4" "github.com/SigNoz/signoz/pkg/query-service/app/metrics" metricsv3 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v3" - "github.com/SigNoz/signoz/pkg/query-service/app/preferences" "github.com/SigNoz/signoz/pkg/query-service/app/querier" querierV2 "github.com/SigNoz/signoz/pkg/query-service/app/querier/v2" "github.com/SigNoz/signoz/pkg/query-service/app/queryBuilder" @@ -142,6 +142,8 @@ type APIHandler struct { AlertmanagerAPI *alertmanager.API Signoz *signoz.SigNoz + + Preference preference.API } type APIHandlerOpts struct { @@ -187,6 +189,8 @@ type APIHandlerOpts struct { AlertmanagerAPI *alertmanager.API Signoz *signoz.SigNoz + + Preference preference.API } // NewAPIHandler returns an APIHandler @@ -257,6 +261,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { SummaryService: summaryService, AlertmanagerAPI: opts.AlertmanagerAPI, Signoz: opts.Signoz, + Preference: opts.Preference, } logsQueryBuilder := logsv3.PrepareLogsQuery @@ -3415,132 +3420,37 @@ func (aH *APIHandler) getProducerConsumerEval( func (aH *APIHandler) getUserPreference( w http.ResponseWriter, r *http.Request, ) { - preferenceId := mux.Vars(r)["preferenceId"] - claims, ok := authtypes.ClaimsFromContext(r.Context()) - if !ok { - render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) - return - } - - preference, apiErr := preferences.GetUserPreference( - r.Context(), preferenceId, claims.OrgID, claims.UserID, - ) - if apiErr != nil { - RespondError(w, apiErr, nil) - return - } - - aH.Respond(w, preference) + aH.Preference.GetUserPreference(w, r) } func (aH *APIHandler) updateUserPreference( w http.ResponseWriter, r *http.Request, ) { - preferenceId := mux.Vars(r)["preferenceId"] - claims, ok := authtypes.ClaimsFromContext(r.Context()) - if !ok { - render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) - return - } - req := preferences.UpdatePreference{} - - err := json.NewDecoder(r.Body).Decode(&req) - - if err != nil { - RespondError(w, model.BadRequest(err), nil) - return - } - preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, claims.UserID) - if apiErr != nil { - RespondError(w, apiErr, nil) - return - } - - aH.Respond(w, preference) + aH.Preference.UpdateUserPreference(w, r) } func (aH *APIHandler) getAllUserPreferences( w http.ResponseWriter, r *http.Request, ) { - claims, ok := authtypes.ClaimsFromContext(r.Context()) - if !ok { - render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) - return - } - preference, apiErr := preferences.GetAllUserPreferences( - r.Context(), claims.OrgID, claims.UserID, - ) - if apiErr != nil { - RespondError(w, apiErr, nil) - return - } - - aH.Respond(w, preference) + aH.Preference.GetAllUserPreferences(w, r) } func (aH *APIHandler) getOrgPreference( w http.ResponseWriter, r *http.Request, ) { - preferenceId := mux.Vars(r)["preferenceId"] - claims, ok := authtypes.ClaimsFromContext(r.Context()) - if !ok { - render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) - return - } - preference, apiErr := preferences.GetOrgPreference( - r.Context(), preferenceId, claims.OrgID, - ) - if apiErr != nil { - RespondError(w, apiErr, nil) - return - } - - aH.Respond(w, preference) + aH.Preference.GetOrgPreference(w, r) } func (aH *APIHandler) updateOrgPreference( w http.ResponseWriter, r *http.Request, ) { - preferenceId := mux.Vars(r)["preferenceId"] - req := preferences.UpdatePreference{} - claims, ok := authtypes.ClaimsFromContext(r.Context()) - if !ok { - render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) - return - } - - err := json.NewDecoder(r.Body).Decode(&req) - - if err != nil { - RespondError(w, model.BadRequest(err), nil) - return - } - preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, claims.OrgID) - if apiErr != nil { - RespondError(w, apiErr, nil) - return - } - - aH.Respond(w, preference) + aH.Preference.UpdateOrgPreference(w, r) } func (aH *APIHandler) getAllOrgPreferences( w http.ResponseWriter, r *http.Request, ) { - claims, ok := authtypes.ClaimsFromContext(r.Context()) - if !ok { - render.Error(w, errorsV2.Newf(errorsV2.TypeUnauthenticated, errorsV2.CodeUnauthenticated, "unauthenticated")) - return - } - preference, apiErr := preferences.GetAllOrgPreferences( - r.Context(), claims.OrgID, - ) - if apiErr != nil { - RespondError(w, apiErr, nil) - return - } - - aH.Respond(w, preference) + aH.Preference.GetAllOrgPreferences(w, r) } // RegisterIntegrationRoutes Registers all Integrations diff --git a/pkg/query-service/app/preferences/map.go b/pkg/query-service/app/preferences/map.go deleted file mode 100644 index e7ff9e7a80..0000000000 --- a/pkg/query-service/app/preferences/map.go +++ /dev/null @@ -1,84 +0,0 @@ -package preferences - -var preferenceMap = map[string]Preference{ - "ORG_ONBOARDING": { - Key: "ORG_ONBOARDING", - Name: "Organisation Onboarding", - Description: "Organisation Onboarding", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"org"}, - }, - "WELCOME_CHECKLIST_DO_LATER": { - Key: "WELCOME_CHECKLIST_DO_LATER", - Name: "Welcome Checklist Do Later", - Description: "Welcome Checklist Do Later", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, - "WELCOME_CHECKLIST_SEND_LOGS_SKIPPED": { - Key: "WELCOME_CHECKLIST_SEND_LOGS_SKIPPED", - Name: "Welcome Checklist Send Logs Skipped", - Description: "Welcome Checklist Send Logs Skipped", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, - "WELCOME_CHECKLIST_SEND_TRACES_SKIPPED": { - Key: "WELCOME_CHECKLIST_SEND_TRACES_SKIPPED", - Name: "Welcome Checklist Send Traces Skipped", - Description: "Welcome Checklist Send Traces Skipped", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, - "WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED": { - Key: "WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED", - Name: "Welcome Checklist Send Infra Metrics Skipped", - Description: "Welcome Checklist Send Infra Metrics Skipped", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, - "WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED": { - Key: "WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED", - Name: "Welcome Checklist Setup Dashboards Skipped", - Description: "Welcome Checklist Setup Dashboards Skipped", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, - "WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED": { - Key: "WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED", - Name: "Welcome Checklist Setup Alerts Skipped", - Description: "Welcome Checklist Setup Alerts Skipped", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, - "WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED": { - Key: "WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED", - Name: "Welcome Checklist Setup Saved View Skipped", - Description: "Welcome Checklist Setup Saved View Skipped", - ValueType: "boolean", - DefaultValue: false, - AllowedValues: []interface{}{true, false}, - IsDiscreteValues: true, - AllowedScopes: []string{"user"}, - }, -} diff --git a/pkg/query-service/app/preferences/model.go b/pkg/query-service/app/preferences/model.go deleted file mode 100644 index 043652591d..0000000000 --- a/pkg/query-service/app/preferences/model.go +++ /dev/null @@ -1,500 +0,0 @@ -package preferences - -import ( - "context" - "database/sql" - "fmt" - "strings" - - "github.com/SigNoz/signoz/pkg/query-service/model" - "github.com/jmoiron/sqlx" -) - -type Range struct { - Min int64 `json:"min"` - Max int64 `json:"max"` -} - -type Preference struct { - Key string `json:"key"` - Name string `json:"name"` - Description string `json:"description"` - ValueType string `json:"valueType"` - DefaultValue interface{} `json:"defaultValue"` - AllowedValues []interface{} `json:"allowedValues"` - IsDiscreteValues bool `json:"isDiscreteValues"` - Range Range `json:"range"` - AllowedScopes []string `json:"allowedScopes"` -} - -func (p *Preference) ErrorValueTypeMismatch() *model.ApiError { - return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not of expected type: %s", p.ValueType)} -} - -const ( - PreferenceValueTypeInteger string = "integer" - PreferenceValueTypeFloat string = "float" - PreferenceValueTypeString string = "string" - PreferenceValueTypeBoolean string = "boolean" -) - -const ( - OrgAllowedScope string = "org" - UserAllowedScope string = "user" -) - -func (p *Preference) checkIfInAllowedValues(preferenceValue interface{}) (bool, *model.ApiError) { - - switch p.ValueType { - case PreferenceValueTypeInteger: - _, ok := preferenceValue.(int64) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - case PreferenceValueTypeFloat: - _, ok := preferenceValue.(float64) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - case PreferenceValueTypeString: - _, ok := preferenceValue.(string) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - case PreferenceValueTypeBoolean: - _, ok := preferenceValue.(bool) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - } - isInAllowedValues := false - for _, value := range p.AllowedValues { - switch p.ValueType { - case PreferenceValueTypeInteger: - allowedValue, ok := value.(int64) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - - if allowedValue == preferenceValue { - isInAllowedValues = true - } - case PreferenceValueTypeFloat: - allowedValue, ok := value.(float64) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - - if allowedValue == preferenceValue { - isInAllowedValues = true - } - case PreferenceValueTypeString: - allowedValue, ok := value.(string) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - - if allowedValue == preferenceValue { - isInAllowedValues = true - } - case PreferenceValueTypeBoolean: - allowedValue, ok := value.(bool) - if !ok { - return false, p.ErrorValueTypeMismatch() - } - - if allowedValue == preferenceValue { - isInAllowedValues = true - } - } - } - return isInAllowedValues, nil -} - -func (p *Preference) IsValidValue(preferenceValue interface{}) *model.ApiError { - typeSafeValue := preferenceValue - switch p.ValueType { - case PreferenceValueTypeInteger: - val, ok := preferenceValue.(int64) - if !ok { - floatVal, ok := preferenceValue.(float64) - if !ok || floatVal != float64(int64(floatVal)) { - return p.ErrorValueTypeMismatch() - } - val = int64(floatVal) - typeSafeValue = val - } - if !p.IsDiscreteValues { - if val < p.Range.Min || val > p.Range.Max { - return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not in the range specified, min: %v , max:%v", p.Range.Min, p.Range.Max)} - } - } - case PreferenceValueTypeString: - _, ok := preferenceValue.(string) - if !ok { - return p.ErrorValueTypeMismatch() - } - case PreferenceValueTypeFloat: - _, ok := preferenceValue.(float64) - if !ok { - return p.ErrorValueTypeMismatch() - } - case PreferenceValueTypeBoolean: - _, ok := preferenceValue.(bool) - if !ok { - return p.ErrorValueTypeMismatch() - } - } - - // check the validity of the value being part of allowed values or the range specified if any - if p.IsDiscreteValues { - if p.AllowedValues != nil { - isInAllowedValues, valueMisMatchErr := p.checkIfInAllowedValues(typeSafeValue) - - if valueMisMatchErr != nil { - return valueMisMatchErr - } - if !isInAllowedValues { - return &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("the preference value is not in the list of allowedValues: %v", p.AllowedValues)} - } - } - } - return nil -} - -func (p *Preference) IsEnabledForScope(scope string) bool { - isPreferenceEnabledForGivenScope := false - if p.AllowedScopes != nil { - for _, allowedScope := range p.AllowedScopes { - if allowedScope == strings.ToLower(scope) { - isPreferenceEnabledForGivenScope = true - } - } - } - return isPreferenceEnabledForGivenScope -} - -func (p *Preference) SanitizeValue(preferenceValue interface{}) interface{} { - switch p.ValueType { - case PreferenceValueTypeBoolean: - if preferenceValue == "1" || preferenceValue == true { - return true - } else { - return false - } - default: - return preferenceValue - } -} - -type AllPreferences struct { - Preference - Value interface{} `json:"value"` -} - -type PreferenceKV struct { - PreferenceId string `json:"preference_id" db:"preference_id"` - PreferenceValue interface{} `json:"preference_value" db:"preference_value"` -} - -type UpdatePreference struct { - PreferenceValue interface{} `json:"preference_value"` -} - -var db *sqlx.DB - -func InitDB(inputDB *sqlx.DB) error { - db = inputDB - return nil -} - -// org preference functions -func GetOrgPreference(ctx context.Context, preferenceId string, orgId string) (*PreferenceKV, *model.ApiError) { - // check if the preference key exists or not - preference, seen := preferenceMap[preferenceId] - if !seen { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} - } - - // check if the preference is enabled for org scope or not - isPreferenceEnabled := preference.IsEnabledForScope(OrgAllowedScope) - if !isPreferenceEnabled { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at org scope: %s", preferenceId)} - } - - // fetch the value from the database - var orgPreference PreferenceKV - query := `SELECT preference_id , preference_value FROM org_preference WHERE preference_id=$1 AND org_id=$2;` - err := db.Get(&orgPreference, query, preferenceId, orgId) - - // if the value doesn't exist in db then return the default value - if err != nil { - if err == sql.ErrNoRows { - return &PreferenceKV{ - PreferenceId: preferenceId, - PreferenceValue: preference.DefaultValue, - }, nil - } - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in fetching the org preference: %s", err.Error())} - - } - - // else return the value fetched from the org_preference table - return &PreferenceKV{ - PreferenceId: preferenceId, - PreferenceValue: preference.SanitizeValue(orgPreference.PreferenceValue), - }, nil -} - -func UpdateOrgPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, orgId string) (*PreferenceKV, *model.ApiError) { - // check if the preference key exists or not - preference, seen := preferenceMap[preferenceId] - if !seen { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} - } - - // check if the preference is enabled at org scope or not - isPreferenceEnabled := preference.IsEnabledForScope(OrgAllowedScope) - if !isPreferenceEnabled { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at org scope: %s", preferenceId)} - } - - err := preference.IsValidValue(preferenceValue) - if err != nil { - return nil, err - } - - // update the values in the org_preference table and return the key and the value - query := `INSERT INTO org_preference(preference_id,preference_value,org_id) VALUES($1,$2,$3) - ON CONFLICT(preference_id,org_id) DO - UPDATE SET preference_value= $2 WHERE preference_id=$1 AND org_id=$3;` - - _, dberr := db.Exec(query, preferenceId, preferenceValue, orgId) - - if dberr != nil { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in setting the preference value: %s", dberr.Error())} - } - - return &PreferenceKV{ - PreferenceId: preferenceId, - PreferenceValue: preferenceValue, - }, nil -} - -func GetAllOrgPreferences(ctx context.Context, orgId string) (*[]AllPreferences, *model.ApiError) { - // filter out all the org enabled preferences from the preference variable - allOrgPreferences := []AllPreferences{} - - // fetch all the org preference values stored in org_preference table - orgPreferenceValues := []PreferenceKV{} - - query := `SELECT preference_id,preference_value FROM org_preference WHERE org_id=$1;` - err := db.Select(&orgPreferenceValues, query, orgId) - - if err != nil { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all org preference values: %s", err)} - } - - // create a map of key vs values from the above response - preferenceValueMap := map[string]interface{}{} - - for _, preferenceValue := range orgPreferenceValues { - preferenceValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue - } - - // update in the above filtered list wherver value present in the map - for _, preference := range preferenceMap { - isEnabledForOrgScope := preference.IsEnabledForScope(OrgAllowedScope) - if isEnabledForOrgScope { - preferenceWithValue := AllPreferences{} - preferenceWithValue.Key = preference.Key - preferenceWithValue.Name = preference.Name - preferenceWithValue.Description = preference.Description - preferenceWithValue.AllowedScopes = preference.AllowedScopes - preferenceWithValue.AllowedValues = preference.AllowedValues - preferenceWithValue.DefaultValue = preference.DefaultValue - preferenceWithValue.Range = preference.Range - preferenceWithValue.ValueType = preference.ValueType - preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues - value, seen := preferenceValueMap[preference.Key] - - if seen { - preferenceWithValue.Value = value - } else { - preferenceWithValue.Value = preference.DefaultValue - } - - preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value) - allOrgPreferences = append(allOrgPreferences, preferenceWithValue) - } - } - return &allOrgPreferences, nil -} - -// user preference functions -func GetUserPreference(ctx context.Context, preferenceId string, orgId string, userId string) (*PreferenceKV, *model.ApiError) { - // check if the preference key exists - preference, seen := preferenceMap[preferenceId] - if !seen { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} - } - - preferenceValue := PreferenceKV{ - PreferenceId: preferenceId, - PreferenceValue: preference.DefaultValue, - } - - // check if the preference is enabled at user scope - isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(UserAllowedScope) - if !isPreferenceEnabledAtUserScope { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at user scope: %s", preferenceId)} - } - - isPreferenceEnabledAtOrgScope := preference.IsEnabledForScope(OrgAllowedScope) - // get the value from the org scope if enabled at org scope - if isPreferenceEnabledAtOrgScope { - orgPreference := PreferenceKV{} - - query := `SELECT preference_id , preference_value FROM org_preference WHERE preference_id=$1 AND org_id=$2;` - - err := db.Get(&orgPreference, query, preferenceId, orgId) - - // if there is error in getting values and its not an empty rows error return from here - if err != nil && err != sql.ErrNoRows { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting org preference values: %s", err.Error())} - } - - // if there is no error update the preference value with value from org preference - if err == nil { - preferenceValue.PreferenceValue = orgPreference.PreferenceValue - } - } - - // get the value from the user_preference table, if exists return this value else the one calculated in the above step - userPreference := PreferenceKV{} - - query := `SELECT preference_id, preference_value FROM user_preference WHERE preference_id=$1 AND user_id=$2;` - err := db.Get(&userPreference, query, preferenceId, userId) - - if err != nil && err != sql.ErrNoRows { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting user preference values: %s", err.Error())} - } - - if err == nil { - preferenceValue.PreferenceValue = userPreference.PreferenceValue - } - - return &PreferenceKV{ - PreferenceId: preferenceValue.PreferenceId, - PreferenceValue: preference.SanitizeValue(preferenceValue.PreferenceValue), - }, nil -} - -func UpdateUserPreference(ctx context.Context, preferenceId string, preferenceValue interface{}, userId string) (*PreferenceKV, *model.ApiError) { - // check if the preference id is valid - preference, seen := preferenceMap[preferenceId] - if !seen { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no such preferenceId exists: %s", preferenceId)} - } - - // check if the preference is enabled at user scope - isPreferenceEnabledAtUserScope := preference.IsEnabledForScope(UserAllowedScope) - if !isPreferenceEnabledAtUserScope { - return nil, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("preference is not enabled at user scope: %s", preferenceId)} - } - - err := preference.IsValidValue(preferenceValue) - if err != nil { - return nil, err - } - // update the user preference values - query := `INSERT INTO user_preference(preference_id,preference_value,user_id) VALUES($1,$2,$3) - ON CONFLICT(preference_id,user_id) DO - UPDATE SET preference_value= $2 WHERE preference_id=$1 AND user_id=$3;` - - _, dberrr := db.Exec(query, preferenceId, preferenceValue, userId) - - if dberrr != nil { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in setting the preference value: %s", dberrr.Error())} - } - - return &PreferenceKV{ - PreferenceId: preferenceId, - PreferenceValue: preferenceValue, - }, nil -} - -func GetAllUserPreferences(ctx context.Context, orgId string, userId string) (*[]AllPreferences, *model.ApiError) { - allUserPreferences := []AllPreferences{} - - // fetch all the org preference values stored in org_preference table - orgPreferenceValues := []PreferenceKV{} - - query := `SELECT preference_id,preference_value FROM org_preference WHERE org_id=$1;` - err := db.Select(&orgPreferenceValues, query, orgId) - - if err != nil { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all org preference values: %s", err)} - } - - // create a map of key vs values from the above response - preferenceOrgValueMap := map[string]interface{}{} - - for _, preferenceValue := range orgPreferenceValues { - preferenceOrgValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue - } - - // fetch all the user preference values stored in user_preference table - userPreferenceValues := []PreferenceKV{} - - query = `SELECT preference_id,preference_value FROM user_preference WHERE user_id=$1;` - err = db.Select(&userPreferenceValues, query, userId) - - if err != nil { - return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in getting all user preference values: %s", err)} - } - - // create a map of key vs values from the above response - preferenceUserValueMap := map[string]interface{}{} - - for _, preferenceValue := range userPreferenceValues { - preferenceUserValueMap[preferenceValue.PreferenceId] = preferenceValue.PreferenceValue - } - - // update in the above filtered list wherver value present in the map - for _, preference := range preferenceMap { - isEnabledForUserScope := preference.IsEnabledForScope(UserAllowedScope) - - if isEnabledForUserScope { - preferenceWithValue := AllPreferences{} - preferenceWithValue.Key = preference.Key - preferenceWithValue.Name = preference.Name - preferenceWithValue.Description = preference.Description - preferenceWithValue.AllowedScopes = preference.AllowedScopes - preferenceWithValue.AllowedValues = preference.AllowedValues - preferenceWithValue.DefaultValue = preference.DefaultValue - preferenceWithValue.Range = preference.Range - preferenceWithValue.ValueType = preference.ValueType - preferenceWithValue.IsDiscreteValues = preference.IsDiscreteValues - preferenceWithValue.Value = preference.DefaultValue - - isEnabledForOrgScope := preference.IsEnabledForScope(OrgAllowedScope) - if isEnabledForOrgScope { - value, seen := preferenceOrgValueMap[preference.Key] - if seen { - preferenceWithValue.Value = value - } - } - - value, seen := preferenceUserValueMap[preference.Key] - - if seen { - preferenceWithValue.Value = value - } - - preferenceWithValue.Value = preference.SanitizeValue(preferenceWithValue.Value) - allUserPreferences = append(allUserPreferences, preferenceWithValue) - } - } - return &allUserPreferences, nil -} diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 001b18d5fa..ebf5c743c8 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -14,6 +14,8 @@ import ( "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/http/middleware" + "github.com/SigNoz/signoz/pkg/modules/preference" + preferencecore "github.com/SigNoz/signoz/pkg/modules/preference/core" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/query-service/agentConf" "github.com/SigNoz/signoz/pkg/query-service/app/clickhouseReader" @@ -23,12 +25,12 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline" "github.com/SigNoz/signoz/pkg/query-service/app/opamp" opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model" - "github.com/SigNoz/signoz/pkg/query-service/app/preferences" "github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/telemetrystore" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/preferencetypes" "github.com/SigNoz/signoz/pkg/web" "github.com/rs/cors" "github.com/soheilhy/cmux" @@ -98,10 +100,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - if err := preferences.InitDB(serverOptions.SigNoz.SQLStore.SQLxDB()); err != nil { - return nil, err - } - if err := dashboards.InitDB(serverOptions.SigNoz.SQLStore); err != nil { return nil, err } @@ -188,6 +186,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } telemetry.GetInstance().SetReader(reader) + preferenceModule := preference.NewAPI(preferencecore.NewPreference(preferencecore.NewStore(serverOptions.SigNoz.SQLStore), preferencetypes.NewDefaultPreferenceMap())) apiHandler, err := NewAPIHandler(APIHandlerOpts{ Reader: reader, SkipConfig: skipConfig, @@ -205,6 +204,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { JWT: serverOptions.Jwt, AlertmanagerAPI: alertmanager.NewAPI(serverOptions.SigNoz.Alertmanager), Signoz: serverOptions.SigNoz, + Preference: preferenceModule, }) if err != nil { return nil, err diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 15f888e325..1d69e81f5e 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -66,6 +66,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM sqlmigration.NewUpdateInvitesFactory(sqlstore), sqlmigration.NewUpdatePatFactory(sqlstore), sqlmigration.NewUpdateAlertmanagerFactory(sqlstore), + sqlmigration.NewUpdatePreferencesFactory(sqlstore), ) } diff --git a/pkg/sqlmigration/021_update_alertmanager.go b/pkg/sqlmigration/021_update_alertmanager.go index 8e362a3b0f..51561534e0 100644 --- a/pkg/sqlmigration/021_update_alertmanager.go +++ b/pkg/sqlmigration/021_update_alertmanager.go @@ -142,7 +142,7 @@ func (migration *updateAlertmanager) Up(ctx context.Context, db *bun.DB) error { err = migration. store. Dialect(). - UpdatePrimaryKey(ctx, tx, new(existingAlertmanagerConfig), new(newAlertmanagerConfig), func(ctx context.Context) error { + UpdatePrimaryKey(ctx, tx, new(existingAlertmanagerConfig), new(newAlertmanagerConfig), OrgReference, func(ctx context.Context) error { existingAlertmanagerConfigs := make([]*existingAlertmanagerConfig, 0) err = tx. NewSelect(). @@ -174,7 +174,7 @@ func (migration *updateAlertmanager) Up(ctx context.Context, db *bun.DB) error { err = migration. store. Dialect(). - UpdatePrimaryKey(ctx, tx, new(existingAlertmanagerState), new(newAlertmanagerState), func(ctx context.Context) error { + UpdatePrimaryKey(ctx, tx, new(existingAlertmanagerState), new(newAlertmanagerState), OrgReference, func(ctx context.Context) error { existingAlertmanagerStates := make([]*existingAlertmanagerState, 0) err = tx. NewSelect(). diff --git a/pkg/sqlmigration/022_update_preferences.go b/pkg/sqlmigration/022_update_preferences.go new file mode 100644 index 0000000000..93ead90744 --- /dev/null +++ b/pkg/sqlmigration/022_update_preferences.go @@ -0,0 +1,202 @@ +package sqlmigration + +import ( + "context" + "database/sql" + "fmt" + "reflect" + + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type updatePreferences struct { + store sqlstore.SQLStore +} + +type existingOrgPreference struct { + bun.BaseModel `bun:"table:org_preference"` + PreferenceID string `bun:"preference_id,pk,type:text,notnull"` + PreferenceValue string `bun:"preference_value,type:text,notnull"` + OrgID string `bun:"org_id,pk,type:text,notnull"` +} + +type newOrgPreference struct { + bun.BaseModel `bun:"table:org_preference_new"` + types.Identifiable + PreferenceID string `bun:"preference_id,type:text,notnull"` + PreferenceValue string `bun:"preference_value,type:text,notnull"` + OrgID string `bun:"org_id,type:text,notnull"` +} + +type existingUserPreference struct { + bun.BaseModel `bun:"table:user_preference"` + PreferenceID string `bun:"preference_id,type:text,pk"` + PreferenceValue string `bun:"preference_value,type:text"` + UserID string `bun:"user_id,type:text,pk"` +} + +type newUserPreference struct { + bun.BaseModel `bun:"table:user_preference_new"` + types.Identifiable + PreferenceID string `bun:"preference_id,type:text,notnull"` + PreferenceValue string `bun:"preference_value,type:text,notnull"` + UserID string `bun:"user_id,type:text,notnull"` +} + +func NewUpdatePreferencesFactory(sqlstore sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] { + return factory. + NewProviderFactory( + factory.MustNewName("update_preferences"), + func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return newUpdatePreferences(ctx, ps, c, sqlstore) + }) +} + +func newUpdatePreferences(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) { + return &updatePreferences{store: store}, nil +} + +func (migration *updatePreferences) Register(migrations *migrate.Migrations) error { + if err := migrations. + Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *updatePreferences) Up(ctx context.Context, db *bun.DB) error { + tx, err := db. + BeginTx(ctx, nil) + if err != nil { + return err + } + + defer tx.Rollback() + + err = migration. + store. + Dialect(). + AddPrimaryKey(ctx, tx, new(existingOrgPreference), new(newOrgPreference), OrgReference, func(ctx context.Context) error { + existingOrgPreferences := make([]*existingOrgPreference, 0) + err = tx. + NewSelect(). + Model(&existingOrgPreferences). + Scan(ctx) + if err != nil { + if err != sql.ErrNoRows { + return err + } + } + + if err == nil && len(existingOrgPreferences) > 0 { + newOrgPreferences := migration. + CopyOldOrgPreferencesToNewOrgPreferences(existingOrgPreferences) + _, err = tx. + NewInsert(). + Model(&newOrgPreferences). + Exec(ctx) + if err != nil { + return err + } + } + + tableName := tx.Dialect().Tables().Get(reflect.TypeOf(new(existingOrgPreference))).Name + _, err = tx. + ExecContext(ctx, fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s_unique_idx ON %s (preference_id, org_id)", tableName, fmt.Sprintf("%s_new", tableName))) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + err = migration. + store. + Dialect(). + AddPrimaryKey(ctx, tx, new(existingUserPreference), new(newUserPreference), UserReference, func(ctx context.Context) error { + existingUserPreferences := make([]*existingUserPreference, 0) + err = tx. + NewSelect(). + Model(&existingUserPreferences). + Scan(ctx) + if err != nil { + if err != sql.ErrNoRows { + return err + } + } + + if err == nil && len(existingUserPreferences) > 0 { + newUserPreferences := migration. + CopyOldUserPreferencesToNewUserPreferences(existingUserPreferences) + _, err = tx. + NewInsert(). + Model(&newUserPreferences). + Exec(ctx) + if err != nil { + return err + } + } + + tableName := tx.Dialect().Tables().Get(reflect.TypeOf(new(existingUserPreference))).Name + _, err = tx. + ExecContext(ctx, fmt.Sprintf("CREATE UNIQUE INDEX IF NOT EXISTS %s_unique_idx ON %s (preference_id, user_id)", tableName, fmt.Sprintf("%s_new", tableName))) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return err + } + + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +func (migration *updatePreferences) Down(context.Context, *bun.DB) error { + return nil +} + +func (migration *updatePreferences) CopyOldOrgPreferencesToNewOrgPreferences(existingOrgPreferences []*existingOrgPreference) []*newOrgPreference { + newOrgPreferences := make([]*newOrgPreference, 0) + for _, preference := range existingOrgPreferences { + newOrgPreferences = append(newOrgPreferences, &newOrgPreference{ + Identifiable: types.Identifiable{ + ID: valuer.GenerateUUID(), + }, + PreferenceID: preference.PreferenceID, + PreferenceValue: preference.PreferenceValue, + OrgID: preference.OrgID, + }) + } + return newOrgPreferences +} + +func (migration *updatePreferences) CopyOldUserPreferencesToNewUserPreferences(existingUserPreferences []*existingUserPreference) []*newUserPreference { + newUserPreferences := make([]*newUserPreference, 0) + for _, preference := range existingUserPreferences { + newUserPreferences = append(newUserPreferences, &newUserPreference{ + Identifiable: types.Identifiable{ + ID: valuer.GenerateUUID(), + }, + PreferenceID: preference.PreferenceID, + PreferenceValue: preference.PreferenceValue, + UserID: preference.UserID, + }) + } + return newUserPreferences +} diff --git a/pkg/sqlmigration/sqlmigration.go b/pkg/sqlmigration/sqlmigration.go index faf6834cf9..cd56777a88 100644 --- a/pkg/sqlmigration/sqlmigration.go +++ b/pkg/sqlmigration/sqlmigration.go @@ -25,6 +25,11 @@ var ( ErrNoExecute = errors.New("no execute") ) +var ( + OrgReference = "org" + UserReference = "user" +) + func New( ctx context.Context, settings factory.ProviderSettings, diff --git a/pkg/sqlstore/sqlitesqlstore/dialect.go b/pkg/sqlstore/sqlitesqlstore/dialect.go index d49e411188..688a0195d2 100644 --- a/pkg/sqlstore/sqlitesqlstore/dialect.go +++ b/pkg/sqlstore/sqlitesqlstore/dialect.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" + "github.com/SigNoz/signoz/pkg/errors" "github.com/uptrace/bun" ) @@ -14,6 +15,16 @@ var ( Text = "TEXT" ) +var ( + Org = "org" + User = "user" +) + +var ( + OrgReference = `("org_id") REFERENCES "organizations" ("id")` + UserReference = `("user_id") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE` +) + type dialect struct { } @@ -229,7 +240,10 @@ func (dialect *dialect) AddNotNullDefaultToColumn(ctx context.Context, bun bun.I return nil } -func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error { +func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error { + if reference == "" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference") + } oldTableName := bun.Dialect().Tables().Get(reflect.TypeOf(oldModel)).Name newTableName := bun.Dialect().Tables().Get(reflect.TypeOf(newModel)).Name @@ -241,11 +255,74 @@ func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldMo return nil } + fkReference := "" + if reference == Org { + fkReference = OrgReference + } else if reference == User { + fkReference = UserReference + } + _, err = bun. NewCreateTable(). IfNotExists(). Model(newModel). - ForeignKey(`("org_id") REFERENCES "organizations" ("id")`). + ForeignKey(fkReference). + Exec(ctx) + + if err != nil { + return err + } + + err = cb(ctx) + if err != nil { + return err + } + + _, err = bun. + NewDropTable(). + IfExists(). + Model(oldModel). + Exec(ctx) + if err != nil { + return err + } + + _, err = bun. + ExecContext(ctx, fmt.Sprintf("ALTER TABLE %s RENAME TO %s", newTableName, oldTableName)) + if err != nil { + return err + } + + return nil +} + +func (dialect *dialect) AddPrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error { + if reference == "" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "cannot run migration without reference") + } + oldTableName := bun.Dialect().Tables().Get(reflect.TypeOf(oldModel)).Name + newTableName := bun.Dialect().Tables().Get(reflect.TypeOf(newModel)).Name + + identityExists, err := dialect.ColumnExists(ctx, bun, oldTableName, Identity) + if err != nil { + return err + } + if identityExists { + return nil + } + + fkReference := "" + if reference == Org { + fkReference = OrgReference + } else if reference == User { + fkReference = UserReference + } + + _, err = bun. + NewCreateTable(). + IfNotExists(). + Model(newModel). + ForeignKey(fkReference). Exec(ctx) if err != nil { diff --git a/pkg/sqlstore/sqlstore.go b/pkg/sqlstore/sqlstore.go index 9d54783207..254388ce06 100644 --- a/pkg/sqlstore/sqlstore.go +++ b/pkg/sqlstore/sqlstore.go @@ -44,5 +44,6 @@ type SQLDialect interface { ColumnExists(context.Context, bun.IDB, string, string) (bool, error) RenameColumn(context.Context, bun.IDB, string, string, string) (bool, error) RenameTableAndModifyModel(context.Context, bun.IDB, interface{}, interface{}, func(context.Context) error) error - UpdatePrimaryKey(context.Context, bun.IDB, interface{}, interface{}, func(context.Context) error) error + UpdatePrimaryKey(context.Context, bun.IDB, interface{}, interface{}, string, func(context.Context) error) error + AddPrimaryKey(context.Context, bun.IDB, interface{}, interface{}, string, func(context.Context) error) error } diff --git a/pkg/sqlstore/sqlstoretest/dialect.go b/pkg/sqlstore/sqlstoretest/dialect.go index 36f7f5b07f..b3d09183fb 100644 --- a/pkg/sqlstore/sqlstoretest/dialect.go +++ b/pkg/sqlstore/sqlstoretest/dialect.go @@ -37,7 +37,11 @@ func (dialect *dialect) AddNotNullDefaultToColumn(ctx context.Context, bun bun.I return nil } -func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, cb func(context.Context) error) error { +func (dialect *dialect) UpdatePrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error { + return nil +} + +func (dialect *dialect) AddPrimaryKey(ctx context.Context, bun bun.IDB, oldModel interface{}, newModel interface{}, reference string, cb func(context.Context) error) error { return nil } diff --git a/pkg/types/preference.go b/pkg/types/preference.go deleted file mode 100644 index 7d98b4dbd3..0000000000 --- a/pkg/types/preference.go +++ /dev/null @@ -1,21 +0,0 @@ -package types - -import "github.com/uptrace/bun" - -// on_delete:CASCADE,on_update:CASCADE not working -type UserPreference struct { - bun.BaseModel `bun:"table:user_preference"` - - PreferenceID string `bun:"preference_id,type:text,pk"` - PreferenceValue string `bun:"preference_value,type:text"` - UserID string `bun:"user_id,type:text,pk"` -} - -// on_delete:CASCADE,on_update:CASCADE not working -type OrgPreference struct { - bun.BaseModel `bun:"table:org_preference"` - - PreferenceID string `bun:"preference_id,pk,type:text,notnull"` - PreferenceValue string `bun:"preference_value,type:text,notnull"` - OrgID string `bun:"org_id,pk,type:text,notnull"` -} diff --git a/pkg/types/preferencetypes/preference.go b/pkg/types/preferencetypes/preference.go new file mode 100644 index 0000000000..1ce7410d5d --- /dev/null +++ b/pkg/types/preferencetypes/preference.go @@ -0,0 +1,290 @@ +package preferencetypes + +import ( + "context" + "fmt" + "strings" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/types" + "github.com/uptrace/bun" +) + +type GettablePreference struct { + PreferenceID string `json:"preference_id" db:"preference_id"` + PreferenceValue interface{} `json:"preference_value" db:"preference_value"` +} + +type UpdatablePreference struct { + PreferenceValue interface{} `json:"preference_value" db:"preference_value"` +} + +type StorableOrgPreference struct { + bun.BaseModel `bun:"table:org_preference"` + types.Identifiable + PreferenceID string `bun:"preference_id,type:text,notnull"` + PreferenceValue string `bun:"preference_value,type:text,notnull"` + OrgID string `bun:"org_id,type:text,notnull"` +} + +type StorableUserPreference struct { + bun.BaseModel `bun:"table:user_preference"` + types.Identifiable + PreferenceID string `bun:"preference_id,type:text,notnull"` + PreferenceValue string `bun:"preference_value,type:text,notnull"` + UserID string `bun:"user_id,type:text,notnull"` +} + +type Preference struct { + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + ValueType string `json:"valueType"` + DefaultValue interface{} `json:"defaultValue"` + AllowedValues []interface{} `json:"allowedValues"` + IsDiscreteValues bool `json:"isDiscreteValues"` + Range Range `json:"range"` + AllowedScopes []string `json:"allowedScopes"` +} + +func NewDefaultPreferenceMap() map[string]Preference { + return map[string]Preference{ + "ORG_ONBOARDING": { + Key: "ORG_ONBOARDING", + Name: "Organisation Onboarding", + Description: "Organisation Onboarding", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"org"}, + }, + "WELCOME_CHECKLIST_DO_LATER": { + Key: "WELCOME_CHECKLIST_DO_LATER", + Name: "Welcome Checklist Do Later", + Description: "Welcome Checklist Do Later", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + "WELCOME_CHECKLIST_SEND_LOGS_SKIPPED": { + Key: "WELCOME_CHECKLIST_SEND_LOGS_SKIPPED", + Name: "Welcome Checklist Send Logs Skipped", + Description: "Welcome Checklist Send Logs Skipped", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + "WELCOME_CHECKLIST_SEND_TRACES_SKIPPED": { + Key: "WELCOME_CHECKLIST_SEND_TRACES_SKIPPED", + Name: "Welcome Checklist Send Traces Skipped", + Description: "Welcome Checklist Send Traces Skipped", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + "WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED": { + Key: "WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED", + Name: "Welcome Checklist Send Infra Metrics Skipped", + Description: "Welcome Checklist Send Infra Metrics Skipped", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + "WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED": { + Key: "WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED", + Name: "Welcome Checklist Setup Dashboards Skipped", + Description: "Welcome Checklist Setup Dashboards Skipped", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + "WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED": { + Key: "WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED", + Name: "Welcome Checklist Setup Alerts Skipped", + Description: "Welcome Checklist Setup Alerts Skipped", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + "WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED": { + Key: "WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED", + Name: "Welcome Checklist Setup Saved View Skipped", + Description: "Welcome Checklist Setup Saved View Skipped", + ValueType: "boolean", + DefaultValue: false, + AllowedValues: []interface{}{true, false}, + IsDiscreteValues: true, + AllowedScopes: []string{"user"}, + }, + } +} + +func (p *Preference) ErrorValueTypeMismatch() error { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("the preference value is not of expected type: %s", p.ValueType)) +} + +func (p *Preference) checkIfInAllowedValues(preferenceValue interface{}) (bool, error) { + + switch p.ValueType { + case PreferenceValueTypeInteger: + _, ok := preferenceValue.(int64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeFloat: + _, ok := preferenceValue.(float64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeString: + _, ok := preferenceValue.(string) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeBoolean: + _, ok := preferenceValue.(bool) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + } + isInAllowedValues := false + for _, value := range p.AllowedValues { + switch p.ValueType { + case PreferenceValueTypeInteger: + allowedValue, ok := value.(int64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + case PreferenceValueTypeFloat: + allowedValue, ok := value.(float64) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + case PreferenceValueTypeString: + allowedValue, ok := value.(string) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + case PreferenceValueTypeBoolean: + allowedValue, ok := value.(bool) + if !ok { + return false, p.ErrorValueTypeMismatch() + } + + if allowedValue == preferenceValue { + isInAllowedValues = true + } + } + } + return isInAllowedValues, nil +} + +func (p *Preference) IsValidValue(preferenceValue interface{}) error { + typeSafeValue := preferenceValue + switch p.ValueType { + case PreferenceValueTypeInteger: + val, ok := preferenceValue.(int64) + if !ok { + floatVal, ok := preferenceValue.(float64) + if !ok || floatVal != float64(int64(floatVal)) { + return p.ErrorValueTypeMismatch() + } + val = int64(floatVal) + typeSafeValue = val + } + if !p.IsDiscreteValues { + if val < p.Range.Min || val > p.Range.Max { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("the preference value is not in the range specified, min: %v , max:%v", p.Range.Min, p.Range.Max)) + } + } + case PreferenceValueTypeString: + _, ok := preferenceValue.(string) + if !ok { + return p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeFloat: + _, ok := preferenceValue.(float64) + if !ok { + return p.ErrorValueTypeMismatch() + } + case PreferenceValueTypeBoolean: + _, ok := preferenceValue.(bool) + if !ok { + return p.ErrorValueTypeMismatch() + } + } + + // check the validity of the value being part of allowed values or the range specified if any + if p.IsDiscreteValues { + if p.AllowedValues != nil { + isInAllowedValues, valueMisMatchErr := p.checkIfInAllowedValues(typeSafeValue) + + if valueMisMatchErr != nil { + return valueMisMatchErr + } + if !isInAllowedValues { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, fmt.Sprintf("the preference value is not in the list of allowedValues: %v", p.AllowedValues)) + } + } + } + return nil +} + +func (p *Preference) IsEnabledForScope(scope string) bool { + isPreferenceEnabledForGivenScope := false + if p.AllowedScopes != nil { + for _, allowedScope := range p.AllowedScopes { + if allowedScope == strings.ToLower(scope) { + isPreferenceEnabledForGivenScope = true + } + } + } + return isPreferenceEnabledForGivenScope +} + +func (p *Preference) SanitizeValue(preferenceValue interface{}) interface{} { + switch p.ValueType { + case PreferenceValueTypeBoolean: + if preferenceValue == "1" || preferenceValue == true || preferenceValue == "true" { + return true + } else { + return false + } + default: + return preferenceValue + } +} + +type PreferenceStore interface { + GetOrgPreference(context.Context, string, string) (*StorableOrgPreference, error) + GetAllOrgPreferences(context.Context, string) ([]*StorableOrgPreference, error) + UpsertOrgPreference(context.Context, *StorableOrgPreference) error + GetUserPreference(context.Context, string, string) (*StorableUserPreference, error) + GetAllUserPreferences(context.Context, string) ([]*StorableUserPreference, error) + UpsertUserPreference(context.Context, *StorableUserPreference) error +} diff --git a/pkg/types/preferencetypes/value.go b/pkg/types/preferencetypes/value.go new file mode 100644 index 0000000000..c24b790a3c --- /dev/null +++ b/pkg/types/preferencetypes/value.go @@ -0,0 +1,23 @@ +package preferencetypes + +const ( + PreferenceValueTypeInteger string = "integer" + PreferenceValueTypeFloat string = "float" + PreferenceValueTypeString string = "string" + PreferenceValueTypeBoolean string = "boolean" +) + +const ( + OrgAllowedScope string = "org" + UserAllowedScope string = "user" +) + +type Range struct { + Min int64 `json:"min"` + Max int64 `json:"max"` +} + +type PreferenceWithValue struct { + Preference + Value interface{} `json:"value"` +}