Merged in jmathison/demo-table (pull request #213)

Add demo dashboard settings table and CRUD API

* Add demo dashboard settings table and CRUD API

- Migration 00000000000123: create demoDashboardSettings table (id, guid, key, value JSONB, isDeleted, timestamps)
- SQLC queries and repository for create, list, listByGuid, get, update, soft delete
- OpenAPI paths and schemas in queryAPI.yaml; regenerate api.gen.go (embedded spec)
- Handlers in api/queryAPI/demodashboardsettings.go; ConfigProvider GetDBQueries for repo access
- Unit tests for demo dashboard settings CRUD

* Fix sqlc-generated demo dashboard setting types

Align demo dashboard settings handlers with sqlc output (DemoDashboardSetting model + pointer slices) so CI codegen/build succeeds.

* Fix CI gosec and gofmt issues

- Suppress gosec G115 for TotalCount len->int32 cast (consistent with other list endpoints)
- gofmt mock GetDBQueries() in test helpers

* gofmt query API demo settings

* Tighten demo dashboard settings OpenAPI constraints

* Regenerate query API embedded spec

* refactor: generic UI settings (table, API, service) and three-layer architecture

- Replace demo-dashboard-settings with generic UI settings:
  - Table uiSettings with namespace (was guid), key, value JSONB; UNIQUE(namespace, key)
  - Migration 123: create_ui_settings (single migration, no rename)
  - OpenAPI: /ui-settings, UISettingsService, UISetting schemas with namespace
  - API types and handlers: CreateUISetting, ListUISettings, GetUISetting, UpdateUISetting, DeleteUISetting

- Add service layer (architecture fix):
  - internal/uisettings: Service with Create, List, ListByNamespace, Get, Update, SoftDelete
  - Controllers call s.svc.UISettings only; remove GetDBQueries from queryAPI ConfigProvider
  - Wire UISettings in Services, main.go, and testutils

- Repository: uisettings.sql + uisettings.sql.go, UISetting model; remove demodashboardsettings
- Controller: uisettings.go (replaces demodashboardsettings.go), ErrNotFound from uisettings package
- Tests: uisettings_test.go with namespace-based list/cre…
* Add UISetting model and generated queries for uiSettings table

- models.go: add UISetting struct (sqlc 1.30.0)
- uisettings.sql.go: generated CRUD for ui_settings feature

Made-with: Cursor

* Use repository.UiSetting to match sqlc-generated type

- service and API use repository.UiSetting (sqlc struct name for uiSettings table)
- uisettings.sql.go: keep CreateUISetting/GetUISetting etc. from query names, return *UiSetting
- Fixes Docker/CI build undefined repository.UISetting

* Fix import formatting for golangci-lint

* Lint fix

* uisettings list test

* tests fixes

* Merge main: resolve api.gen.go conflicts (UI settings + DeleteDocument)

* Doc updates and elimination of index redundancy


Approved-by: Jay Brown
This commit is contained in:
Jacob Mathison
2026-03-04 23:00:50 +00:00
parent 09c61ea9b4
commit 8a4029058b
17 changed files with 1693 additions and 289 deletions
+436 -278
View File
@@ -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(), &params.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
+4 -1
View File
@@ -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
+3
View File
@@ -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,
+148
View File
@@ -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)
}
+165
View File
@@ -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 }
+8 -9
View File
@@ -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,
+80
View File
@@ -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
+47
View File
@@ -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.
+27
View File
@@ -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
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_uisettings_namespace;
DROP TABLE IF EXISTS "uiSettings";
@@ -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);
+28
View File
@@ -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;
+11 -1
View File
@@ -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"`
}
@@ -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
}
+96
View File
@@ -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")
+182
View File
@@ -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)
}
+258
View File
@@ -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.