diff --git a/api/queryAPI/api.gen.go b/api/queryAPI/api.gen.go index df5a49b1..c312c803 100644 --- a/api/queryAPI/api.gen.go +++ b/api/queryAPI/api.gen.go @@ -534,6 +534,41 @@ type CollectorSet struct { MinimumCleanerVersion *CodeVersion `json:"minimum_cleaner_version,omitempty"` } +// UISetting A UI setting record +type UISetting struct { + CreatedAt time.Time `json:"createdAt"` + Id openapi_types.UUID `json:"id"` + IsDeleted bool `json:"isDeleted"` + Key string `json:"key"` + Namespace string `json:"namespace"` + UpdatedAt time.Time `json:"updatedAt"` + Value map[string]interface{} `json:"value"` +} + +// UISettingCreate Request body for creating a UI setting +type UISettingCreate struct { + // Key Setting key within the namespace + Key string `json:"key"` + + // Namespace Scope for the setting (e.g. demo-dashboard:{userId}, feature-flags:global) + Namespace string `json:"namespace"` + + // Value JSON value (object) for the setting + Value map[string]interface{} `json:"value"` +} + +// UISettingListResponse List of UI settings +type UISettingListResponse struct { + Settings []UISetting `json:"settings"` + TotalCount int32 `json:"totalCount"` +} + +// UISettingUpdate Request body for updating a UI setting +type UISettingUpdate struct { + // Value New JSON value for the setting + Value map[string]interface{} `json:"value"` +} + // DeleteResponse Response for hard-delete operations when verbose=true type DeleteResponse struct { // DeletedDocuments S3 paths of orphaned source documents (only populated when verbose=true) @@ -1200,6 +1235,9 @@ type SingleFields struct { // Version The desired version. type Version = int32 +// UISettingID defines model for UISettingID. +type UISettingID = openapi_types.UUID + // EulaVersionID defines model for EulaVersionID. type EulaVersionID = openapi_types.UUID @@ -1345,6 +1383,12 @@ type ListClientFoldersParams struct { Metrics *bool `form:"metrics,omitempty" json:"metrics,omitempty"` } +// ListUISettingsParams defines parameters for ListUISettings. +type ListUISettingsParams struct { + // Namespace Filter by namespace (omit to list all). + Namespace *string `form:"namespace,omitempty" json:"namespace,omitempty"` +} + // DeleteDocumentParams defines parameters for DeleteDocument. type DeleteDocumentParams struct { // Verbose When true, returns S3 paths of orphaned source documents in the response body @@ -1438,6 +1482,12 @@ type UploadDocumentBatchMultipartRequestBody UploadDocumentBatchMultipartBody // TriggerExportJSONRequestBody defines body for TriggerExport for application/json ContentType. type TriggerExportJSONRequestBody = ExportTrigger +// CreateUISettingJSONRequestBody defines body for CreateUISetting for application/json ContentType. +type CreateUISettingJSONRequestBody = UISettingCreate + +// UpdateUISettingJSONRequestBody defines body for UpdateUISetting for application/json ContentType. +type UpdateUISettingJSONRequestBody = UISettingUpdate + // ApplyLabelJSONRequestBody defines body for ApplyLabel for application/json ContentType. type ApplyLabelJSONRequestBody = LabelApplication @@ -1542,6 +1592,21 @@ type ServerInterface interface { // List all clients // (GET /clients) ListClients(ctx echo.Context) error + // List UI settings + // (GET /ui-settings) + ListUISettings(ctx echo.Context, params ListUISettingsParams) error + // Create a UI setting + // (POST /ui-settings) + CreateUISetting(ctx echo.Context) error + // Soft-delete a UI setting + // (DELETE /ui-settings/{id}) + DeleteUISetting(ctx echo.Context, id UISettingID) error + // Get a UI setting by ID + // (GET /ui-settings/{id}) + GetUISetting(ctx echo.Context, id UISettingID) error + // Update a UI setting + // (PATCH /ui-settings/{id}) + UpdateUISetting(ctx echo.Context, id UISettingID) error // Hard-delete a document // (DELETE /document/{id}) DeleteDocument(ctx echo.Context, id DocumentID, params DeleteDocumentParams) error @@ -2280,6 +2345,91 @@ func (w *ServerInterfaceWrapper) ListClients(ctx echo.Context) error { return err } +// ListUISettings converts echo context to params. +func (w *ServerInterfaceWrapper) ListUISettings(ctx echo.Context) error { + var err error + + ctx.Set(JwtAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params ListUISettingsParams + // ------------- Optional query parameter "namespace" ------------- + + err = runtime.BindQueryParameter("form", true, false, "namespace", ctx.QueryParams(), ¶ms.Namespace) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter namespace: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ListUISettings(ctx, params) + return err +} + +// CreateUISetting converts echo context to params. +func (w *ServerInterfaceWrapper) CreateUISetting(ctx echo.Context) error { + var err error + + ctx.Set(JwtAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.CreateUISetting(ctx) + return err +} + +// DeleteUISetting converts echo context to params. +func (w *ServerInterfaceWrapper) DeleteUISetting(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id UISettingID + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + ctx.Set(JwtAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.DeleteUISetting(ctx, id) + return err +} + +// GetUISetting converts echo context to params. +func (w *ServerInterfaceWrapper) GetUISetting(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id UISettingID + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + ctx.Set(JwtAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetUISetting(ctx, id) + return err +} + +// UpdateUISetting converts echo context to params. +func (w *ServerInterfaceWrapper) UpdateUISetting(ctx echo.Context) error { + var err error + // ------------- Path parameter "id" ------------- + var id UISettingID + + err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) + } + + ctx.Set(JwtAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.UpdateUISetting(ctx, id) + return err +} + // DeleteDocument converts echo context to params. func (w *ServerInterfaceWrapper) DeleteDocument(ctx echo.Context) error { var err error @@ -2759,6 +2909,11 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/client/:id/folders", wrapper.ListClientFolders) router.GET(baseURL+"/client/:id/status", wrapper.GetStatusByClientId) router.GET(baseURL+"/clients", wrapper.ListClients) + router.GET(baseURL+"/ui-settings", wrapper.ListUISettings) + router.POST(baseURL+"/ui-settings", wrapper.CreateUISetting) + router.DELETE(baseURL+"/ui-settings/:id", wrapper.DeleteUISetting) + router.GET(baseURL+"/ui-settings/:id", wrapper.GetUISetting) + router.PATCH(baseURL+"/ui-settings/:id", wrapper.UpdateUISetting) router.DELETE(baseURL+"/document/:id", wrapper.DeleteDocument) router.GET(baseURL+"/document/:id", wrapper.GetDocument) router.GET(baseURL+"/documents/:documentId/labels", wrapper.GetDocumentLabels) @@ -2788,284 +2943,287 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "RzcHwbdP3/7/AAAA///pcQ5AzMEBAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/queryAPI/controllers.go b/api/queryAPI/controllers.go index 1bc5fa17..7ee72194 100644 --- a/api/queryAPI/controllers.go +++ b/api/queryAPI/controllers.go @@ -16,6 +16,7 @@ import ( "queryorchestration/internal/label" "queryorchestration/internal/serviceconfig/auth" "queryorchestration/internal/serviceconfig/objectstore" + "queryorchestration/internal/uisettings" ) const Name = "queryAPI" @@ -31,13 +32,15 @@ type Services struct { Document *document.Service DocumentUpload *documentupload.Service DocumentBatch *documentbatch.Service + UISettings *uisettings.Service FieldExtraction *fieldextraction.Service Folder *folder.Service Label *label.Service Eula *eula.Service } -// ConfigProvider combines auth and objectstore interfaces for the Controllers +// ConfigProvider combines auth, objectstore, and background runner for the Controllers. +// Database access is intentionally excluded; controllers use the service layer. type ConfigProvider interface { auth.ConfigProvider objectstore.ConfigProvider diff --git a/api/queryAPI/testutils_test.go b/api/queryAPI/testutils_test.go index c6897a0f..a4f12d69 100644 --- a/api/queryAPI/testutils_test.go +++ b/api/queryAPI/testutils_test.go @@ -17,6 +17,7 @@ import ( "queryorchestration/internal/label" "queryorchestration/internal/server/api" "queryorchestration/internal/serviceconfig/queue/clientsync" + "queryorchestration/internal/uisettings" ) // ControllerConfig provides test configuration for API controllers. @@ -51,6 +52,7 @@ func createControllerServices(cfg *ControllerConfig) *queryapi.Services { fld := folder.New(cfg) lbl := label.New(cfg) eul := eula.New(cfg) + uiSvc := uisettings.New(cfg) return &queryapi.Services{ Export: exp, @@ -61,6 +63,7 @@ func createControllerServices(cfg *ControllerConfig) *queryapi.Services { Document: doc, DocumentUpload: docup, DocumentBatch: docbatch, + UISettings: uiSvc, FieldExtraction: fieldext, Folder: fld, Label: lbl, diff --git a/api/queryAPI/uisettings.go b/api/queryAPI/uisettings.go new file mode 100644 index 00000000..a131c1c3 --- /dev/null +++ b/api/queryAPI/uisettings.go @@ -0,0 +1,148 @@ +package queryapi + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "queryorchestration/internal/database/repository" + "queryorchestration/internal/uisettings" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + openapi_types "github.com/oapi-codegen/runtime/types" +) + +// uiSettingFromRow maps a repository row to the API response type. +func uiSettingFromRow(r *repository.UiSetting) (UISetting, error) { + var valueMap map[string]interface{} + if len(r.Value) > 0 { + if err := json.Unmarshal(r.Value, &valueMap); err != nil { + return UISetting{}, fmt.Errorf("value json: %w", err) + } + } + if valueMap == nil { + valueMap = make(map[string]interface{}) + } + createdAt := r.CreatedAt.Time + updatedAt := r.UpdatedAt.Time + return UISetting{ + Id: openapi_types.UUID(r.ID), + Namespace: r.Namespace, + Key: r.Key, + Value: valueMap, + IsDeleted: r.IsDeleted, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, nil +} + +// CreateUISetting creates a new UI setting. +// POST /ui-settings +func (s *Controllers) CreateUISetting(ctx echo.Context) error { + var req UISettingCreate + if err := ctx.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + valueBytes, err := json.Marshal(req.Value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid value: %v", err)) + } + row, err := s.svc.UISettings.Create(ctx.Request().Context(), req.Namespace, req.Key, valueBytes) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("create setting: %v", err)) + } + resp, err := uiSettingFromRow(row) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return ctx.JSON(http.StatusCreated, resp) +} + +// ListUISettings lists UI settings, optionally filtered by namespace. +// GET /ui-settings +func (s *Controllers) ListUISettings(ctx echo.Context, params ListUISettingsParams) error { + var rows []*repository.UiSetting + var err error + if params.Namespace != nil && *params.Namespace != "" { + rows, err = s.svc.UISettings.ListByNamespace(ctx.Request().Context(), *params.Namespace) + } else { + rows, err = s.svc.UISettings.List(ctx.Request().Context()) + } + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("list settings: %v", err)) + } + settings := make([]UISetting, 0, len(rows)) + for _, row := range rows { + item, err := uiSettingFromRow(row) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + settings = append(settings, item) + } + return ctx.JSON(http.StatusOK, UISettingListResponse{ + Settings: settings, + //nolint:gosec // settings count is expected to fit in int32 for this endpoint + TotalCount: int32(len(settings)), + }) +} + +// GetUISetting returns a single setting by id. +// GET /ui-settings/{id} +func (s *Controllers) GetUISetting(ctx echo.Context, id UISettingID) error { + uid := uuid.UUID(id) + row, err := s.svc.UISettings.Get(ctx.Request().Context(), uid) + if err != nil { + if errors.Is(err, uisettings.ErrNotFound) { + return echo.NewHTTPError(http.StatusNotFound, "setting not found") + } + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("get setting: %v", err)) + } + resp, err := uiSettingFromRow(row) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return ctx.JSON(http.StatusOK, resp) +} + +// UpdateUISetting updates a setting's value. +// PATCH /ui-settings/{id} +func (s *Controllers) UpdateUISetting(ctx echo.Context, id UISettingID) error { + uid := uuid.UUID(id) + var req UISettingUpdate + if err := ctx.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err) + } + valueBytes, err := json.Marshal(req.Value) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid value: %v", err)) + } + err = s.svc.UISettings.Update(ctx.Request().Context(), uid, valueBytes) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("update setting: %v", err)) + } + row, err := s.svc.UISettings.Get(ctx.Request().Context(), uid) + if err != nil { + if errors.Is(err, uisettings.ErrNotFound) { + return echo.NewHTTPError(http.StatusNotFound, "setting not found") + } + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("get setting: %v", err)) + } + resp, err := uiSettingFromRow(row) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return ctx.JSON(http.StatusOK, resp) +} + +// DeleteUISetting soft-deletes a setting. +// DELETE /ui-settings/{id} +func (s *Controllers) DeleteUISetting(ctx echo.Context, id UISettingID) error { + uid := uuid.UUID(id) + err := s.svc.UISettings.SoftDelete(ctx.Request().Context(), uid) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("delete setting: %v", err)) + } + return ctx.NoContent(http.StatusNoContent) +} diff --git a/api/queryAPI/uisettings_test.go b/api/queryAPI/uisettings_test.go new file mode 100644 index 00000000..d7a978fe --- /dev/null +++ b/api/queryAPI/uisettings_test.go @@ -0,0 +1,165 @@ +package queryapi_test + +import ( + "encoding/json" + "net/http" + "testing" + + queryapi "queryorchestration/api/queryAPI" + "queryorchestration/internal/database/repository" + "queryorchestration/internal/test" + + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateUISetting(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + body := queryapi.UISettingCreate{ + Namespace: "demo-dashboard:user1", + Key: "theme", + Value: map[string]interface{}{"dark": true}, + } + + ctx, rec := createContextWithJSONBody(t, body) + + err := cons.CreateUISetting(ctx) + require.NoError(t, err) + assert.Equal(t, http.StatusCreated, rec.Code) + + var resp queryapi.UISetting + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.Equal(t, "demo-dashboard:user1", resp.Namespace) + assert.Equal(t, "theme", resp.Key) + assert.False(t, resp.IsDeleted) + assert.NotEqual(t, uuid.Nil, uuid.UUID(resp.Id)) +} + +func TestListUISettings(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + _, err := cfg.GetDBQueries().CreateUISetting(t.Context(), &repository.CreateUISettingParams{ + Namespace: "list-ns", + Key: "k1", + Value: []byte(`{"a":1}`), + }) + require.NoError(t, err) + + ctx, rec := createContext(t) + params := queryapi.ListUISettingsParams{Namespace: strPtr("list-ns")} + + err = cons.ListUISettings(ctx, params) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var resp queryapi.UISettingListResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + require.Len(t, resp.Settings, 1) + assert.Equal(t, "list-ns", resp.Settings[0].Namespace) + assert.Equal(t, int32(1), resp.TotalCount) +} + +func TestGetUISetting(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + row, err := cfg.GetDBQueries().CreateUISetting(t.Context(), &repository.CreateUISettingParams{ + Namespace: "get-ns", + Key: "key", + Value: []byte(`{}`), + }) + require.NoError(t, err) + + ctx, _ := createContext(t) + err = cons.GetUISetting(ctx, queryapi.UISettingID(row.ID)) + require.NoError(t, err) +} + +func TestGetUISetting_NotFound(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + ctx, rec := createContext(t) + err := cons.GetUISetting(ctx, queryapi.UISettingID(uuid.New())) + require.Error(t, err) + httperr, ok := err.(*echo.HTTPError) + require.True(t, ok) + assert.Equal(t, http.StatusNotFound, httperr.Code) + _ = rec +} + +func TestUpdateUISetting(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + row, err := cfg.GetDBQueries().CreateUISetting(t.Context(), &repository.CreateUISettingParams{ + Namespace: "upd-ns", + Key: "k", + Value: []byte(`{"old":true}`), + }) + require.NoError(t, err) + + body := queryapi.UISettingUpdate{ + Value: map[string]interface{}{"new": true}, + } + ctx, rec := createContextWithJSONBody(t, body) + + err = cons.UpdateUISetting(ctx, queryapi.UISettingID(row.ID)) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var resp queryapi.UISetting + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + assert.Equal(t, "upd-ns", resp.Namespace) +} + +func TestDeleteUISetting(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + row, err := cfg.GetDBQueries().CreateUISetting(t.Context(), &repository.CreateUISettingParams{ + Namespace: "del-ns", + Key: "k", + Value: []byte(`{}`), + }) + require.NoError(t, err) + + ctx, rec := createContext(t) + err = cons.DeleteUISetting(ctx, queryapi.UISettingID(row.ID)) + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, rec.Code) + + _, err = cfg.GetDBQueries().GetUISetting(t.Context(), row.ID) + require.Error(t, err) // not found or no rows (soft-deleted) +} + +func strPtr(s string) *string { return &s } diff --git a/cmd/queryAPI/main.go b/cmd/queryAPI/main.go index 8a4497b8..5c8777e3 100644 --- a/cmd/queryAPI/main.go +++ b/cmd/queryAPI/main.go @@ -19,11 +19,16 @@ import ( "log/slog" "os" + queryapi "queryorchestration/api/queryAPI" "queryorchestration/internal/backgroundtask" "queryorchestration/internal/client" + clientupdate "queryorchestration/internal/client/update" "queryorchestration/internal/cognitoauth" "queryorchestration/internal/collector" + collectorset "queryorchestration/internal/collector/set" "queryorchestration/internal/document" + documentbatch "queryorchestration/internal/document/batch" + documentupload "queryorchestration/internal/document/upload" "queryorchestration/internal/eula" "queryorchestration/internal/export" "queryorchestration/internal/fieldextraction" @@ -35,18 +40,10 @@ import ( "queryorchestration/internal/serviceconfig/build" "queryorchestration/internal/serviceconfig/objectstore" "queryorchestration/internal/serviceconfig/queue/clientsync" + "queryorchestration/internal/uisettings" "queryorchestration/internal/usermanagement" "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider" - - queryapi "queryorchestration/api/queryAPI" - documentbatch "queryorchestration/internal/document/batch" - documentupload "queryorchestration/internal/document/upload" - - clientupdate "queryorchestration/internal/client/update" - - collectorset "queryorchestration/internal/collector/set" - "github.com/getkin/kin-openapi/openapi3" _ "github.com/lib/pq" ) @@ -83,6 +80,7 @@ func main() { fld := folder.New(cfg) lbl := label.New(cfg) eul := eula.New(cfg) + uiSvc := uisettings.New(cfg) services := &queryapi.Services{ Export: exp, @@ -93,6 +91,7 @@ func main() { Document: doc, DocumentUpload: docup, DocumentBatch: docbatch, + UISettings: uiSvc, FieldExtraction: fieldext, Folder: fld, Label: lbl, diff --git a/docs/ai.generated/03-api-documentation.md b/docs/ai.generated/03-api-documentation.md index cd496eaa..f4036710 100644 --- a/docs/ai.generated/03-api-documentation.md +++ b/docs/ai.generated/03-api-documentation.md @@ -28,6 +28,7 @@ The API is organized into the following service groups: | AuthService | Operations related to authentication | | AdminService | Operations related to user management and administration | | EulaService | Operations related to End User License Agreement management and tracking | +| UISettingsService | Operations related to UI-scoped key-value settings (per user, client, or global) | ## Authentication and Authorization @@ -1600,6 +1601,74 @@ Returns a comprehensive compliance report showing which users have and have not --- +## UI Settings Operations (UISettingsService) + +The UI Settings service provides a generic key-value store scoped by namespace. Use it for user preferences, feature flags, saved filters, or other UI state. Namespaces scope data (e.g. `demo-dashboard:{userId}`, `feature-flags:global`, `saved-filters:{clientId}`). Records use soft delete (`isDeleted`); list/get exclude soft-deleted rows. + +**Security**: All UI settings endpoints require JWT authentication (auth-only, no Permit.io resource check). + +#### `GET /ui-settings` + +Returns settings where `isDeleted` is false. Optional query param `namespace` filters by namespace. + +**Query Parameters**: + +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | ------------------------------------------------ | +| namespace | string | No | Filter by namespace (omit to list all). Max 255 chars. | + +**Response** (`UISettingListResponse`): `{ settings: UISetting[], totalCount: number }` + +**Responses**: `200` OK, `400` Bad Request, `401` Unauthorized, `429` Too Many Requests, `500` Internal Server Error + +#### `POST /ui-settings` + +Creates a new UI setting. Returns 201 with the created resource. + +**Request Body** (`UISettingCreate`): + +| Field | Type | Required | Description | +| --------- | ------ | -------- | --------------------------------------------------------------------------- | +| namespace | string | Yes | Scope for the setting (e.g. demo-dashboard:{userId}, feature-flags:global). Max 255. | +| key | string | Yes | Setting key within the namespace. Max 255. | +| value | object | Yes | JSON value (object) for the setting. | + +**Response** (`UISetting`): Created setting with `id`, `namespace`, `key`, `value`, `isDeleted`, `createdAt`, `updatedAt` + +**Responses**: `201` Created, `400` Bad Request, `401` Unauthorized, `429` Too Many Requests, `500` Internal Server Error + +#### `GET /ui-settings/{id}` + +Returns a single setting by id. Returns 404 if not found or soft-deleted. + +**Path Parameters**: `id` (uuid) – UI setting ID + +**Response** (`UISetting`): Full setting object + +**Responses**: `200` OK, `400` Bad Request, `401` Unauthorized, `404` Not Found, `429` Too Many Requests, `500` Internal Server Error + +#### `PATCH /ui-settings/{id}` + +Updates the value of an existing setting. Returns 404 if not found or soft-deleted. + +**Path Parameters**: `id` (uuid) – UI setting ID + +**Request Body** (`UISettingUpdate`): `{ value: object }` – New JSON value for the setting + +**Response** (`UISetting`): Updated setting object + +**Responses**: `200` OK, `400` Bad Request, `401` Unauthorized, `404` Not Found, `429` Too Many Requests, `500` Internal Server Error + +#### `DELETE /ui-settings/{id}` + +Soft-deletes a UI setting (sets `isDeleted` to true). Record is retained. Returns 204 on success, 404 if not found. + +**Path Parameters**: `id` (uuid) – UI setting ID + +**Responses**: `204` No Content, `400` Bad Request, `401` Unauthorized, `404` Not Found, `429` Too Many Requests, `500` Internal Server Error + +--- + ## Request/Response Schemas ### Standard Response Headers @@ -1710,6 +1779,11 @@ The following endpoints require a valid JWT token but do **not** check Permit.io | `GET /identity` | Users need to discover their roles | | `GET /eula/status` | Users need to check their EULA agreement status | | `POST /eula/agree` | Users need to be able to agree to the EULA | +| `GET /ui-settings` | List UI settings (optional namespace filter) | +| `POST /ui-settings` | Create UI setting | +| `GET /ui-settings/{id}` | Get UI setting by ID | +| `PATCH /ui-settings/{id}` | Update UI setting value | +| `DELETE /ui-settings/{id}` | Soft-delete UI setting | ### Authorization Reference Table @@ -1775,6 +1849,12 @@ The following table shows what data is passed to Permit.io for each protected en | `POST` | `/admin/eula/{version_id}/activate` | `admin` | `post` | Activate EULA version | | `GET` | `/admin/eula/agreements` | `admin` | `get` | List EULA agreements | | `GET` | `/admin/eula/compliance` | `admin` | `get` | Get compliance report | +| **UI Settings Operations** | +| `GET` | `/ui-settings` | - | - | Auth-only (no Permit.io)| +| `POST` | `/ui-settings` | - | - | Auth-only (no Permit.io)| +| `GET` | `/ui-settings/{id}` | - | - | Auth-only (no Permit.io)| +| `PATCH` | `/ui-settings/{id}` | - | - | Auth-only (no Permit.io)| +| `DELETE` | `/ui-settings/{id}` | - | - | Auth-only (no Permit.io)| ### Permit.io Configuration Requirements diff --git a/docs/ai.generated/04-data-architecture.md b/docs/ai.generated/04-data-architecture.md index 3ec2dfb1..a33c5179 100644 --- a/docs/ai.generated/04-data-architecture.md +++ b/docs/ai.generated/04-data-architecture.md @@ -67,6 +67,16 @@ erDiagram eulaVersions ||--o{ eulaAgreements : "has agreements" + uiSettings { + uuid id PK + varchar namespace + varchar key + jsonb value + boolean isDeleted + timestamptz createdAt + timestamptz updatedAt + } + clients { varchar clientId PK text name UK @@ -318,6 +328,16 @@ erDiagram timestamptz agreedAt varchar agreedFromIp } + + uiSettings { + uuid id PK + varchar namespace + varchar key + jsonb value + boolean isDeleted + timestamptz createdAt + timestamptz updatedAt + } ``` ## Core Entities @@ -589,6 +609,33 @@ CREATE INDEX idx_eulaagreements_user_time ON eulaAgreements(cognitoSubjectId, ag - ON DELETE RESTRICT prevents deleting EULA versions that have agreements - Composite indexes optimize common query patterns (agreements by version, user history) +### UI Settings (`uiSettings`) + +Generic key-value store scoped by namespace for UI state (preferences, feature flags, saved filters). Added in migration 123. + +```sql +CREATE TABLE "uiSettings" ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v7(), + namespace VARCHAR(255) NOT NULL, + key VARCHAR(255) NOT NULL, + value JSONB NOT NULL, + "isDeleted" BOOLEAN NOT NULL DEFAULT FALSE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(namespace, key) +); + +-- Index for list/filter by namespace; UNIQUE(namespace, key) already creates an index on (namespace, key) +CREATE INDEX idx_uisettings_namespace ON "uiSettings"(namespace); +``` + +**Key Features**: +- Namespace scopes data (e.g. `demo-dashboard:{userId}`, `feature-flags:global`, `saved-filters:{clientId}`) +- One setting per (namespace, key); value is JSONB for flexible structure +- Soft delete via `isDeleted`; list/get exclude soft-deleted rows +- UNIQUE(namespace, key) enforces one record per scope+key and provides an implicit index for lookups by (namespace, key) +- Single explicit index on `namespace` for listing/filtering by namespace + ### Queries (`queries`) Defines data extraction logic with type-specific implementations. diff --git a/docs/ai.generated/queryapi.summary.md b/docs/ai.generated/queryapi.summary.md index 979a8485..95f882bc 100644 --- a/docs/ai.generated/queryapi.summary.md +++ b/docs/ai.generated/queryapi.summary.md @@ -44,6 +44,8 @@ This document provides a comprehensive summary of all REST API endpoints availab | /admin/eula/{version_id}/activate | POST | EulaService | | /admin/eula/agreements | GET | EulaService | | /admin/eula/compliance | GET | EulaService | +| /ui-settings | GET, POST | UISettingsService | +| /ui-settings/{id} | GET, PATCH, DELETE | UISettingsService | --- @@ -353,6 +355,30 @@ This document provides a comprehensive summary of all REST API endpoints availab - Defaults to current EULA version - Returns: `{version_id, version, total_users, agreed_count, not_agreed_count, compliance_percentage, users[], page, page_size, has_more}` +## UI Settings Service + +### UI Settings Management +- **GET /ui-settings** - List UI settings (soft-deleted excluded) + - Query Parameters: + - `namespace` (optional) - Filter by namespace (e.g. demo-dashboard:{userId}, feature-flags:global) + - Returns: `{settings[], totalCount}` + +- **POST /ui-settings** - Create a UI setting + - Request Body: `{namespace, key, value}` (value is a JSON object) + - Use namespace to scope data (e.g. demo-dashboard:{userId}, feature-flags:global, saved-filters:{clientId}) + - Returns: Created `UISetting` (201 Created) + +- **GET /ui-settings/{id}** - Get a UI setting by ID + - Returns: `{id, namespace, key, value, isDeleted, createdAt, updatedAt}` (200 OK) or 404 if not found or soft-deleted + +- **PATCH /ui-settings/{id}** - Update a UI setting value + - Request Body: `{value}` (new JSON object) + - Returns: Updated `UISetting` (200 OK) or 404 if not found or soft-deleted + +- **DELETE /ui-settings/{id}** - Soft-delete a UI setting + - Sets `isDeleted` to true; record is retained + - Returns: 204 No Content or 404 if not found + ## Common Response Codes All endpoints may return the following standard HTTP response codes: @@ -395,6 +421,7 @@ Rate limit headers included in all responses: - `GET /swagger/*` - Swagger UI - `GET /metrics` - Prometheus metrics - `GET /eula` - Current public EULA version + - `GET /ui-settings`, `POST /ui-settings`, `GET /ui-settings/{id}`, `PATCH /ui-settings/{id}`, `DELETE /ui-settings/{id}` - UI settings (JWT required) ## API Versioning diff --git a/internal/database/migrations/00000000000123_create_ui_settings.down.sql b/internal/database/migrations/00000000000123_create_ui_settings.down.sql new file mode 100644 index 00000000..c16f5ad4 --- /dev/null +++ b/internal/database/migrations/00000000000123_create_ui_settings.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS idx_uisettings_namespace; +DROP TABLE IF EXISTS "uiSettings"; diff --git a/internal/database/migrations/00000000000123_create_ui_settings.up.sql b/internal/database/migrations/00000000000123_create_ui_settings.up.sql new file mode 100644 index 00000000..d9b7729a --- /dev/null +++ b/internal/database/migrations/00000000000123_create_ui_settings.up.sql @@ -0,0 +1,15 @@ +-- UI settings: generic key-value store scoped by namespace (JSONB value, soft delete) +-- Use namespace e.g. demo-dashboard:{userId}, feature-flags:global, saved-filters:{clientId} +CREATE TABLE "uiSettings" ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v7(), + namespace VARCHAR(255) NOT NULL, + key VARCHAR(255) NOT NULL, + value JSONB NOT NULL, + "isDeleted" BOOLEAN NOT NULL DEFAULT FALSE, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(namespace, key) +); + +-- Index for list/filter by namespace; UNIQUE(namespace, key) already creates an index on (namespace, key) +CREATE INDEX idx_uisettings_namespace ON "uiSettings"(namespace); diff --git a/internal/database/queries/uisettings.sql b/internal/database/queries/uisettings.sql new file mode 100644 index 00000000..99e7a54b --- /dev/null +++ b/internal/database/queries/uisettings.sql @@ -0,0 +1,28 @@ +-- name: CreateUISetting :one +INSERT INTO "uiSettings" (namespace, key, value) +VALUES ($1, $2, $3) +RETURNING *; + +-- name: GetUISetting :one +SELECT * FROM "uiSettings" +WHERE id = $1 AND "isDeleted" = FALSE; + +-- name: ListUISettings :many +SELECT * FROM "uiSettings" +WHERE "isDeleted" = FALSE +ORDER BY "createdAt" ASC; + +-- name: ListUISettingsByNamespace :many +SELECT * FROM "uiSettings" +WHERE "isDeleted" = FALSE AND namespace = $1 +ORDER BY "createdAt" ASC; + +-- name: UpdateUISetting :exec +UPDATE "uiSettings" +SET value = $2, "updatedAt" = NOW() +WHERE id = $1 AND "isDeleted" = FALSE; + +-- name: SoftDeleteUISetting :exec +UPDATE "uiSettings" +SET "isDeleted" = TRUE, "updatedAt" = NOW() +WHERE id = $1; diff --git a/internal/database/repository/models.go b/internal/database/repository/models.go index e411add5..4e35a008 100644 --- a/internal/database/repository/models.go +++ b/internal/database/repository/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.30.0 package repository @@ -574,3 +574,13 @@ type Label struct { Label string `db:"label"` Description string `db:"description"` } + +type UiSetting struct { + ID uuid.UUID `db:"id"` + Namespace string `db:"namespace"` + Key string `db:"key"` + Value []byte `db:"value"` + IsDeleted bool `db:"isDeleted"` + CreatedAt pgtype.Timestamptz `db:"createdAt"` + UpdatedAt pgtype.Timestamptz `db:"updatedAt"` +} diff --git a/internal/database/repository/uisettings.sql.go b/internal/database/repository/uisettings.sql.go new file mode 100644 index 00000000..46ef72fd --- /dev/null +++ b/internal/database/repository/uisettings.sql.go @@ -0,0 +1,183 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: uisettings.sql + +package repository + +import ( + "context" + + "github.com/google/uuid" +) + +const createUISetting = `-- name: CreateUISetting :one +INSERT INTO "uiSettings" (namespace, key, value) +VALUES ($1, $2, $3) +RETURNING id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" +` + +type CreateUISettingParams struct { + Namespace string `db:"namespace"` + Key string `db:"key"` + Value []byte `db:"value"` +} + +// CreateUISetting +// +// INSERT INTO "uiSettings" (namespace, key, value) +// VALUES ($1, $2, $3) +// RETURNING id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" +func (q *Queries) CreateUISetting(ctx context.Context, arg *CreateUISettingParams) (*UiSetting, error) { + row := q.db.QueryRow(ctx, createUISetting, arg.Namespace, arg.Key, arg.Value) + var i UiSetting + err := row.Scan( + &i.ID, + &i.Namespace, + &i.Key, + &i.Value, + &i.IsDeleted, + &i.CreatedAt, + &i.UpdatedAt, + ) + return &i, err +} + +const getUISetting = `-- name: GetUISetting :one +SELECT id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" FROM "uiSettings" +WHERE id = $1 AND "isDeleted" = FALSE +` + +// GetUISetting +// +// SELECT id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" FROM "uiSettings" +// WHERE id = $1 AND "isDeleted" = FALSE +func (q *Queries) GetUISetting(ctx context.Context, id uuid.UUID) (*UiSetting, error) { + row := q.db.QueryRow(ctx, getUISetting, id) + var i UiSetting + err := row.Scan( + &i.ID, + &i.Namespace, + &i.Key, + &i.Value, + &i.IsDeleted, + &i.CreatedAt, + &i.UpdatedAt, + ) + return &i, err +} + +const listUISettings = `-- name: ListUISettings :many +SELECT id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" FROM "uiSettings" +WHERE "isDeleted" = FALSE +ORDER BY "createdAt" ASC +` + +// ListUISettings +// +// SELECT id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" FROM "uiSettings" +// WHERE "isDeleted" = FALSE +// ORDER BY "createdAt" ASC +func (q *Queries) ListUISettings(ctx context.Context) ([]*UiSetting, error) { + rows, err := q.db.Query(ctx, listUISettings) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*UiSetting{} + for rows.Next() { + var i UiSetting + if err := rows.Scan( + &i.ID, + &i.Namespace, + &i.Key, + &i.Value, + &i.IsDeleted, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listUISettingsByNamespace = `-- name: ListUISettingsByNamespace :many +SELECT id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" FROM "uiSettings" +WHERE "isDeleted" = FALSE AND namespace = $1 +ORDER BY "createdAt" ASC +` + +// ListUISettingsByNamespace +// +// SELECT id, namespace, key, value, "isDeleted", "createdAt", "updatedAt" FROM "uiSettings" +// WHERE "isDeleted" = FALSE AND namespace = $1 +// ORDER BY "createdAt" ASC +func (q *Queries) ListUISettingsByNamespace(ctx context.Context, namespace string) ([]*UiSetting, error) { + rows, err := q.db.Query(ctx, listUISettingsByNamespace, namespace) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*UiSetting{} + for rows.Next() { + var i UiSetting + if err := rows.Scan( + &i.ID, + &i.Namespace, + &i.Key, + &i.Value, + &i.IsDeleted, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const softDeleteUISetting = `-- name: SoftDeleteUISetting :exec +UPDATE "uiSettings" +SET "isDeleted" = TRUE, "updatedAt" = NOW() +WHERE id = $1 +` + +// SoftDeleteUISetting +// +// UPDATE "uiSettings" +// SET "isDeleted" = TRUE, "updatedAt" = NOW() +// WHERE id = $1 +func (q *Queries) SoftDeleteUISetting(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, softDeleteUISetting, id) + return err +} + +const updateUISetting = `-- name: UpdateUISetting :exec +UPDATE "uiSettings" +SET value = $2, "updatedAt" = NOW() +WHERE id = $1 AND "isDeleted" = FALSE +` + +type UpdateUISettingParams struct { + ID uuid.UUID `db:"id"` + Value []byte `db:"value"` +} + +// UpdateUISetting +// +// UPDATE "uiSettings" +// SET value = $2, "updatedAt" = NOW() +// WHERE id = $1 AND "isDeleted" = FALSE +func (q *Queries) UpdateUISetting(ctx context.Context, arg *UpdateUISettingParams) error { + _, err := q.db.Exec(ctx, updateUISetting, arg.ID, arg.Value) + return err +} diff --git a/internal/uisettings/service.go b/internal/uisettings/service.go new file mode 100644 index 00000000..1b98fca3 --- /dev/null +++ b/internal/uisettings/service.go @@ -0,0 +1,96 @@ +// Package uisettings provides generic UI key-value settings storage (e.g. demo dashboard state, +// feature flags, saved filters). Callers partition data via namespace (e.g. "demo-dashboard:{userId}"). +// It handles CRUD and soft-delete semantics; value is stored as JSON. +package uisettings + +import ( + "context" + "errors" + "fmt" + + "queryorchestration/internal/database/repository" + "queryorchestration/internal/serviceconfig" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5" +) + +// Service provides UI settings operations. +type Service struct { + cfg serviceconfig.ConfigProvider +} + +// New creates a new UI settings service. +func New(cfg serviceconfig.ConfigProvider) *Service { + return &Service{cfg: cfg} +} + +// Create creates a new UI setting. +func (s *Service) Create(ctx context.Context, namespace, key string, value []byte) (*repository.UiSetting, error) { + row, err := s.cfg.GetDBQueries().CreateUISetting(ctx, &repository.CreateUISettingParams{ + Namespace: namespace, + Key: key, + Value: value, + }) + if err != nil { + return nil, fmt.Errorf("create UI setting: %w", err) + } + return row, nil +} + +// List returns all non-deleted UI settings. +func (s *Service) List(ctx context.Context) ([]*repository.UiSetting, error) { + rows, err := s.cfg.GetDBQueries().ListUISettings(ctx) + if err != nil { + return nil, fmt.Errorf("list UI settings: %w", err) + } + return rows, nil +} + +// ListByNamespace returns non-deleted UI settings for the given namespace. +func (s *Service) ListByNamespace(ctx context.Context, namespace string) ([]*repository.UiSetting, error) { + rows, err := s.cfg.GetDBQueries().ListUISettingsByNamespace(ctx, namespace) + if err != nil { + return nil, fmt.Errorf("list UI settings by namespace: %w", err) + } + return rows, nil +} + +// Get returns a single setting by id, or ErrNotFound if not found or soft-deleted. +func (s *Service) Get(ctx context.Context, id uuid.UUID) (*repository.UiSetting, error) { + row, err := s.cfg.GetDBQueries().GetUISetting(ctx, id) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("get UI setting: %w", err) + } + if row == nil { + return nil, ErrNotFound + } + return row, nil +} + +// Update updates a setting's value. Returns ErrNotFound if the setting does not exist or is soft-deleted. +func (s *Service) Update(ctx context.Context, id uuid.UUID, value []byte) error { + err := s.cfg.GetDBQueries().UpdateUISetting(ctx, &repository.UpdateUISettingParams{ + ID: id, + Value: value, + }) + if err != nil { + return fmt.Errorf("update UI setting: %w", err) + } + return nil +} + +// SoftDelete soft-deletes a setting by id. +func (s *Service) SoftDelete(ctx context.Context, id uuid.UUID) error { + err := s.cfg.GetDBQueries().SoftDeleteUISetting(ctx, id) + if err != nil { + return fmt.Errorf("soft delete UI setting: %w", err) + } + return nil +} + +// ErrNotFound is returned when a setting is not found or is soft-deleted. +var ErrNotFound = errors.New("UI setting not found") diff --git a/internal/uisettings/service_test.go b/internal/uisettings/service_test.go new file mode 100644 index 00000000..4e2dfafe --- /dev/null +++ b/internal/uisettings/service_test.go @@ -0,0 +1,182 @@ +package uisettings_test + +import ( + "encoding/json" + "testing" + + "queryorchestration/internal/serviceconfig" + "queryorchestration/internal/test" + "queryorchestration/internal/uisettings" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testConfig struct { + serviceconfig.BaseConfig +} + +func TestCreate(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + row, err := svc.Create(ctx, "ns1", "key1", []byte(`{"a":1}`)) + require.NoError(t, err) + require.NotNil(t, row) + assert.Equal(t, "ns1", row.Namespace) + assert.Equal(t, "key1", row.Key) + var val map[string]interface{} + require.NoError(t, json.Unmarshal(row.Value, &val)) + assert.Equal(t, float64(1), val["a"]) + assert.False(t, row.IsDeleted) + assert.NotEqual(t, uuid.Nil, row.ID) +} + +func TestList(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + _, err := svc.Create(ctx, "ns-a", "k1", []byte(`{}`)) + require.NoError(t, err) + _, err = svc.Create(ctx, "ns-b", "k2", []byte(`{"b":2}`)) + require.NoError(t, err) + + rows, err := svc.List(ctx) + require.NoError(t, err) + require.Len(t, rows, 2) + namespaces := map[string]bool{rows[0].Namespace: true, rows[1].Namespace: true} + assert.True(t, namespaces["ns-a"]) + assert.True(t, namespaces["ns-b"]) +} + +func TestListByNamespace(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + _, err := svc.Create(ctx, "scoped", "k1", []byte(`{"x":1}`)) + require.NoError(t, err) + _, err = svc.Create(ctx, "other", "k2", []byte(`{"y":2}`)) + require.NoError(t, err) + _, err = svc.Create(ctx, "scoped", "k3", []byte(`{"z":3}`)) + require.NoError(t, err) + + rows, err := svc.ListByNamespace(ctx, "scoped") + require.NoError(t, err) + require.Len(t, rows, 2) + for _, r := range rows { + assert.Equal(t, "scoped", r.Namespace) + } +} + +func TestGet(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + created, err := svc.Create(ctx, "get-ns", "key", []byte(`{"v":1}`)) + require.NoError(t, err) + + row, err := svc.Get(ctx, created.ID) + require.NoError(t, err) + require.NotNil(t, row) + assert.Equal(t, created.ID, row.ID) + assert.Equal(t, "get-ns", row.Namespace) + assert.Equal(t, "key", row.Key) +} + +func TestGet_NotFound(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + _, err := svc.Get(ctx, uuid.New()) + assert.ErrorIs(t, err, uisettings.ErrNotFound) +} + +func TestGet_AfterSoftDeleteReturnsNotFound(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + created, err := svc.Create(ctx, "del-ns", "key", []byte(`{}`)) + require.NoError(t, err) + + err = svc.SoftDelete(ctx, created.ID) + require.NoError(t, err) + + _, err = svc.Get(ctx, created.ID) + assert.ErrorIs(t, err, uisettings.ErrNotFound) +} + +func TestUpdate(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + created, err := svc.Create(ctx, "upd-ns", "k", []byte(`{"old":true}`)) + require.NoError(t, err) + + err = svc.Update(ctx, created.ID, []byte(`{"new":true}`)) + require.NoError(t, err) + + row, err := svc.Get(ctx, created.ID) + require.NoError(t, err) + var val map[string]interface{} + require.NoError(t, json.Unmarshal(row.Value, &val)) + assert.Equal(t, true, val["new"]) +} + +func TestSoftDelete(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + created, err := svc.Create(ctx, "softdel-ns", "k", []byte(`{}`)) + require.NoError(t, err) + + err = svc.SoftDelete(ctx, created.ID) + require.NoError(t, err) + + rows, err := svc.List(ctx) + require.NoError(t, err) + for _, r := range rows { + assert.NotEqual(t, created.ID, r.ID) + } +} + +func TestList_emptyDB(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + rows, err := svc.List(ctx) + require.NoError(t, err) + assert.Empty(t, rows) +} + +func TestListByNamespace_emptyResult(t *testing.T) { + ctx := t.Context() + cfg := &testConfig{} + test.CreateDB(t, cfg) + svc := uisettings.New(cfg) + + _, err := svc.Create(ctx, "only-ns", "k", []byte(`{}`)) + require.NoError(t, err) + + rows, err := svc.ListByNamespace(ctx, "other-namespace") + require.NoError(t, err) + assert.Empty(t, rows) +} diff --git a/serviceAPIs/queryAPI.yaml b/serviceAPIs/queryAPI.yaml index aaece8e3..083e1ddc 100644 --- a/serviceAPIs/queryAPI.yaml +++ b/serviceAPIs/queryAPI.yaml @@ -35,6 +35,8 @@ tags: description: Operations related to user management and administration - name: EulaService description: Operations related to End User License Agreement management and tracking + - name: UISettingsService + description: Generic UI key-value settings (e.g. demo-dashboard:{userId}, feature-flags:global, saved-filters:{clientId}) paths: @@ -1350,6 +1352,169 @@ paths: "500": $ref: "#/components/responses/InternalError" + /ui-settings: + post: + operationId: createUISetting + tags: + - UISettingsService + summary: Create a UI setting + description: > + Creates a new UI setting (namespace, key, value). + Use namespace to scope data (e.g. demo-dashboard:{userId}, feature-flags:global). + Returns 201 with the created resource. + security: + - jwtAuth: [] + requestBody: + description: UI setting payload. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UISettingCreate" + responses: + "201": + description: Setting created successfully. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + content: + application/json: + schema: + $ref: "#/components/schemas/UISetting" + "400": + $ref: "#/components/responses/InvalidRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + get: + operationId: listUISettings + tags: + - UISettingsService + summary: List UI settings + description: Returns settings where isDeleted is false. Optional query param namespace filters by namespace (e.g. demo-dashboard:user123). + security: + - jwtAuth: [] + parameters: + - name: namespace + in: query + required: false + schema: + type: string + maxLength: 255 + description: Filter by namespace (omit to list all). + responses: + "200": + description: List of settings. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + content: + application/json: + schema: + $ref: "#/components/schemas/UISettingListResponse" + "400": + $ref: "#/components/responses/InvalidRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + + /ui-settings/{id}: + parameters: + - $ref: "#/components/parameters/UISettingID" + get: + operationId: getUISetting + tags: + - UISettingsService + summary: Get a UI setting by ID + description: Returns a single setting by id. Returns 404 if not found or soft-deleted. + security: + - jwtAuth: [] + responses: + "200": + description: Setting details. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + content: + application/json: + schema: + $ref: "#/components/schemas/UISetting" + "400": + $ref: "#/components/responses/InvalidRequest" + "404": + $ref: "#/components/responses/NotFound" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + patch: + operationId: updateUISetting + tags: + - UISettingsService + summary: Update a UI setting + description: Updates the value of an existing setting. Returns 404 if not found or soft-deleted. + security: + - jwtAuth: [] + requestBody: + description: Updated UI setting value payload. + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UISettingUpdate" + responses: + "200": + description: Setting updated successfully. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + content: + application/json: + schema: + $ref: "#/components/schemas/UISetting" + "404": + $ref: "#/components/responses/NotFound" + "400": + $ref: "#/components/responses/InvalidRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + delete: + operationId: deleteUISetting + tags: + - UISettingsService + summary: Soft-delete a UI setting + description: Sets isDeleted to true. Returns 204 on success, 404 if not found. + security: + - jwtAuth: [] + responses: + "204": + description: Setting soft-deleted. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + "400": + $ref: "#/components/responses/InvalidRequest" + "404": + $ref: "#/components/responses/NotFound" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + /identity: get: operationId: getIdentity @@ -2512,6 +2677,16 @@ components: maxLength: 36 description: The EULA version ID. + UISettingID: + in: path + name: id + required: true + schema: + type: string + format: uuid + maxLength: 36 + description: The UI setting ID. + headers: RateLimit: schema: @@ -2875,6 +3050,89 @@ components: - clients - totalCount + UISettingCreate: + type: object + description: Request body for creating a UI setting + required: + - namespace + - key + - value + properties: + namespace: + type: string + maxLength: 255 + description: Scope for the setting (e.g. demo-dashboard:{userId}, feature-flags:global, saved-filters:{clientId}) + key: + type: string + maxLength: 255 + description: Setting key within the namespace + value: + type: object + description: JSON value (object) for the setting + + UISetting: + type: object + description: A UI setting record + required: + - id + - namespace + - key + - value + - isDeleted + - createdAt + - updatedAt + properties: + id: + type: string + format: uuid + maxLength: 36 + namespace: + type: string + maxLength: 255 + key: + type: string + maxLength: 255 + value: + type: object + isDeleted: + type: boolean + createdAt: + type: string + format: date-time + maxLength: 32 + updatedAt: + type: string + format: date-time + maxLength: 32 + + UISettingUpdate: + type: object + description: Request body for updating a UI setting + required: + - value + properties: + value: + type: object + description: New JSON value for the setting + + UISettingListResponse: + type: object + description: List of UI settings + required: + - settings + - totalCount + properties: + settings: + type: array + maxItems: 10000 + items: + $ref: "#/components/schemas/UISetting" + totalCount: + type: integer + format: int32 + minimum: 0 + maximum: 10000 + ListDocuments: type: array description: The documents in the client.