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:
+436
-278
@@ -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{
|
||||
|
||||
"H4sIAAAAAAAC/+y9C3MbOZIw+FcQ3L1oaYakSEryQ19s3KoluVt7fugkeea+aflTgFUgiXGxwAFQkukO",
|
||||
"//cLJIAqVBXqQYqUZY92J6ItFh6JRGYikcjHn52AzRcsJrEUnaM/OzOCQ8Lhn5dYkrd0TqX6IyQi4HQh",
|
||||
"KYs7R/AJReobovGE8TlWHxCNkf4D3XTg6/+6p3HI7v9L0jnp6X/fdDrdDvmC54uIdI46w8HANhoOOt2O",
|
||||
"CGZkjtWM3jYvVJs5/vKWxFM56xztj7qdBZaScAXW//lj0Hv96a+2sf7rPzvdjlwu1EBCchpPO9++fVO9",
|
||||
"OJ4Tadb6K5bB7Py0vNLrGUFj9REli4jhEJ2f9jvdDlXfFljOOt1OjOdqcGh1S8NOt8PJvxLKSdg5kjwh",
|
||||
"7qL+k5NJ56jzH3sZ1vf0V7FnYVDQnUSUxLIKoAC+VoPyACDSiRUUpyxI5jVwhOb7ViBxJlewnCUR/hvh",
|
||||
"grK4Cpyzj2+P0Z1uUw2SadC0U5qQO0edJIGWLtG9KFNUt3P2ZcF4JaoIfN0KotKJFRQfBeFnc0yjMhgf",
|
||||
"L9/2SBywkIQoEYQjotohHIacCFEBF7RphyjbNMeeAy/zcSIWLBbE8F74G5bkHi/VXwGLJYlB5uDFIqIB",
|
||||
"iJa9fwq1hj/booRzxt8RIfCU6BnzqDj7oiQGjpAg/I4GBBHVQaGgSv75ZjNt97KGMNUJiycRDeSjreaS",
|
||||
"CJbwgCAccYLDJSJfqJACMY6EVJI6MBBtaIFvGB/TMCTxo63wPBbJZEIDEHsLwudUKB4WSDL1pyJBJGdU",
|
||||
"IByoHhta53msqQSge8S1OrSpuHSDpHke3+GIhpfkXwkR8hGXBNMirudFYxYuN7Si90y+YUkcPj6zxUyi",
|
||||
"iZp6Qyu5Zuwdjpdmb8RjLghkMUoWLEaSMTTH8dLuldjA6rqdSyL5snc8kYSXz6X3yXxMOGITJEjA4lCg",
|
||||
"MZkwTpDkSxpPEZ5iCiydaoRDdax4TiAay/2RPoHoPJl3jl69OBgMup05jfXf2WlEY0mmhCuEqFMzxomc",
|
||||
"MU6/kvDxEX8/IzFKHBA2QlHfLIpgjONwTmOlHByDhLRze1R7C9WEcaMmxHgckb2QCvVfI2IFuqdyhkQS",
|
||||
"BEQIOGcSgXAcIqXrC4nni063s+BsQbik+qDXPctTapCsICdKEyKx2q4/OmZS+AX+8cm9PGRfCzpGtxOw",
|
||||
"aUwluxXJ+J8kkErbK817otsg0wbRkMSSTijhsHg5s4tFVl/JXV3wcDwK9sODHjmcvOi9fPV60MPjIOyR",
|
||||
"yXC0f3D4Qv2SV4dGhy9y15W+73LSNYpUCVxQ6xSbKMBgZ3AglS63YHEOsH+yWdwPGflv81M/YPNOd2VF",
|
||||
"rdsxu1sG5e8zImckhyJoS0K9exYUrSuacceMRQTHauCMRsrKsv1kV2qoxl3gaDA67A0HveGL6+HB0ejw",
|
||||
"aH//H+4CQyxJT82RX+ThwKe3Z5rtH+mCuymOzOyf0p4MaEUtImWpE06w9LJSdtwBRQWqIYg0FJN7vYfA",
|
||||
"RjBbFymduwtMxJniNCHoNJ7D1bzISxU08rGk16OdRJAQYYEsuatp1Uy7GyEa9/aNe1+Pe/8Y9F73b/+v",
|
||||
"v97c9D799b+d3+CHm5u++enTn6PuNy/9TygX8lZfQHwL/EWgKb0jMeALEIuDgCWxNAguUMv/sFmcB3xo",
|
||||
"DoX07zZcGeEmoCZ4TqNlS6hOGdkAUIpOPPz5lgqp+AfI6DNZgqKsyQnRGF0oDVr2KUM7pD/td5FIFoTf",
|
||||
"YkXQXaAP+2+chFQy3jUmh1v1bbePLtW0aJ4Iqe8aatAlS7gzMonvKGdAvX2UKoDQb6LIU9CIxDJaoh4K",
|
||||
"ZiT4rL/daiBJqEa0Ujd39v/RyeBT/KkBVAxKJZkDLopIbUbjHH85170P9SaYv4ZpW8w5XpbkhWUKh2Jd",
|
||||
"QrH700J6tDiQsVKfXBmiENFFNA6iJFS/FM+z81OQJWIZB+aALomRVsckAKhtBr84YiSm/0qI5/B8nEMy",
|
||||
"0FDdYll3hoBqlZ6X91gg00/Rl/pdLIUk87rTZf/o4HDN06XbXkxv5wDXSlNYf4AHzv4iKoy+Bxgye93m",
|
||||
"SK8T2gUKyoR3Gzn9IMlcmNmR0C2EcfPMYIqQt4rD6rCcEp/RLyZJFC1dSszEpr0EaIO6ps4+Op+gCY4E",
|
||||
"6RqNXNt3sg1COwGOoa9iwgBLsovGiYQ7al7ez7BAcWGaPceisttH73Cc4EiLDcYRV3c3NMdLNCbICr9+",
|
||||
"G5LIS3SPZIPTQM6wRPeEkzx23IMghV8BB3BgJXcE0YecVrOIPV7oBAmmjmBMIxL20QmbLzAnWtMqNga5",
|
||||
"rk7HkEglxjKdC/onnAhAf5xEkcIGmS/ksqtQqPsD4CmsO7A9Bsd5rH6IoyVacCLU0HSCcqSj2E5h8bFP",
|
||||
"uvzh1u2YU6LypgTLS4+SjH/efLg8Obs9+f34/W9ntxfHV1d//3B5WhaPTeAVTlfP4ZQp5tmBluPB2qP2",
|
||||
"lESk1VGb3n1D1UNxor7t6lvuhLM5IjiYGf5EOxY/6rhNiXV3vdO2+lJqLkQAk5Fpj3PWmhlv1cLLEJ9a",
|
||||
"HFWgpwoNLeSlXSoMucJhpGli1QnSnWuewk+pKTV6ybDNtb5ybx/3Tp+S/dZu9XaGOs3r5dHwcFv3+hxJ",
|
||||
"NwgNiWnkFYoKbGl0S/cd3CXXvExwtHVz/rgWs/X0848t9XINlqMkZBrG01XZ7R3aUd39JHN4PXh9NDw8",
|
||||
"2h9sWlk/y5lT8vY3AO6RdfeThHOlRFg93YhdF7DNqe5le4vSBym5q5HKW9HkPTaWFpCsq9hX2FW0xupV",
|
||||
"TfOam1Xa7ii5J99JZzNkktPdLDt5dLiTD+/fnF++O1tDb+t2kkXYgrXZBKkdRrp1negfHA0HmxH97ZRI",
|
||||
"y2y1J8FbKjzLu8BTGsNFLjJWN4Vp8z4imcQR0jhXon6hGyvJOycSh1jiksyfYXE7Z5xUH9HqqxoK7i8E",
|
||||
"4TtMI/tM0qga4SmpJhb1FcXwIJd7cOvWPLINB4PcI9uw/Mimp70V9Cupe//TiFsQDnC4ACgJWQtB0/yw",
|
||||
"ER7ahP2JCxDMsQxm6oye0EgWMHHwsg6S0fDg5cGr/ReqVd2rYxfudjWWWw0IPBo4W9Nx5Ejd02NJeclJ",
|
||||
"EUBXrV1TA2exZojG3cRaPvmo2bv5PQTkgMKzlk1ScjpOJBFoB44k8x4CIiP/NCLKd6vaQ0xLp+KrgT0u",
|
||||
"C2dXjOUMb/vFwEBUfDLwgnTKSO9qTsEta0sPBqkuG7V9Oeihy7OLt8cnZ1cIR5E2SWVq7U7MpFKZqKR3",
|
||||
"ZLePrpn6C2H4bG3WBHpa+tYdF1EiAAsxuUcsJtCVkzm7I2lvBn6nUp/68ESoiNj/KvGQBwn7/GAaMY4m",
|
||||
"FBxzJJakjz4oMMDraEJJFCKlVXMSgTUpIviOmNGTOJjheFowmn33d4wy/6pPb9RSVK8yjVzReBqpRd0r",
|
||||
"AtGGM71ydYyhneFwpP8GXM0VMbAY2pe5FWNOJDklnN6R8BjohMWXWJITQJVSGSIakwIWRgOfopwb6yTC",
|
||||
"dH69XJCT0IfC+s5vCLkKZiRMojVmdjobP9XVAfiNs2RBuL9/CxjesvHqnd4Tec/459U7XnAWJtrPceWO",
|
||||
"U47na3W8u4YWq/a8JHQ+fkfkjIXtOhuaPCUiKO9jXY93+MsbQhQtn8dBzj0pZIlW0lJ14XX6f/3Xr70a",
|
||||
"g9HF8uNfBLL1+H01etPQiZyp01sq3HpWfOhd8JhG0QqcljVvPUOA+R1hiWw5g21+rt0By7pvoIRDutC2",
|
||||
"o6pOV3BRWKdH+8Uu5MEFZ0HbSdLmK8/wjq02xTvWfo6QTHASVW9BSPG05Qp10/Yzi5nhuw0yXShm1UsR",
|
||||
"M8OIm+FC8mVGx1RemAtaI35M+2sqS8eVH0MTHNCIyuVx+M9ESKUIXRM+b9V1OiebR+50TiqRO52TjSJ3",
|
||||
"CrZD/mFSPaM+e48jcL9WeutbckeiD5MTzNsdOGaEX7HYOKb0yC0ZJ23dmndMj99x0ISeDW8KjHlJlBas",
|
||||
"X0qb5r+k4vOHyTvGJVakfJWMgwgLsQpersgd4VQu1+nTBN41x7GYEN7YrqzE+Oen22A8WsN4dMOMFxEh",
|
||||
"avkuYmOjSF6SCExjYkYXrRhOd1Wq5Mpd47DtQQstW3NSLPFi8xumRq1EoPq40S1jiYwo4WdfgihRTNkS",
|
||||
"UeVurbFmur6hXMhTFkW4moHSpl/UdUeI92N+ipfiesaJmLEobIn1flsspBOtOsMKc7yzfbc08BvweomD",
|
||||
"5aaRsw2ya62TLLCkJJbHU/IOfyn1qO9A41YdIhyQD5MrHSTYkg2KnVozwYKzu6sFCSiO5LLtZPk+K811",
|
||||
"jb+wmM1XmSrr0nomjuMpMUzaah6OJTkTAY6wZHyFaZxelcIj1+od/mJu0RfaiPFwEs6N7zdnbYT9OKHz",
|
||||
"8QmLTSzzGxxIHaHYbvR2459NJiRQevBpATv67a6JeWCMzR+FMOxG5Y4ekbO798ZU30xsaY8FLWkbde2v",
|
||||
"ady6fWs5mLY2L4vrbtcdiZO2Qi5t3ZpBTaR161UJyRbq2D3FNFoaVt0gGdnhV1VySv3aI8D0bKPkZG23",
|
||||
"qHzYSTavfRRG3rz6YScwcuBDfPYlIEKczDCfmufAB4sFO8dl4QDb6AJas4PkdDrVtoVt0IIZ/gQv0tGP",
|
||||
"53KDEyTB5o+CpNpakQQbPSKSmMoPk3cEi4S3ubf7ntfap5ih+ZfCzmD4+vDVIJz0yOTFYe/li5cveq9C",
|
||||
"8rr3+uBg/xC/3t/ff4ldX5lWGUsMQFcV3kMKqMyxDOcAdOJzF5wpxlMjdiGTEHhVKmDA+V79iOOARHnf",
|
||||
"mgIMH2FUx88SR9GHSefojxb5cnTfq2Q+x3zZ+db9s+gWAHDcTmhEYjyvi5VLm+iIBN0R5RboPN6GLNjv",
|
||||
"L8JJp6v+OXwJ/654vD0YvH6x2uutcalperP91C25YksNtuuNiscskYUtdPxQzUIzDOW3xe/0ZJHmjqmf",
|
||||
"yLEJFSw9+UJT/c9WPize7a33YjEOK7faK7PR2cdAZBwvqMggz/yODjfh7VPwrrGYyIP7qUpmaBy0iFrg",
|
||||
"RCY8JqF2oS1uuEA4CMgCvE4YzxO2Z6eMq3GrhFWuE2JjByNx0k63CfcnK0KSGU8MvRCPz+Ke3rG94+Pj",
|
||||
"PZuMag9a760sNDntcTIhnMRBUV8ejpq8DJ3kXymYzvIa9tbSd9nlQn9oZOdNbKGJ8G3uk6UIc0S+1/Pz",
|
||||
"79aVO0eJaZ88DbZx9YyTSDs65t0b27mY+6HJO5av5TduzhhLgKLOxTFt1OKUGW3G05BqX6db9aUdkDGL",
|
||||
"exenb+BMEDrJDaIxwjyY0TvS2jG0PYiM0ymNcZSe02XoPpgm6UGVOrzDPpIQ/eP8wgdiJ11y/ytd5Lez",
|
||||
"7cls9mf1TU47IsHQBOf8+g42tL8LzqacCHG7IDwgvoPvIiUvZBsj07jgZ1sP07Apmc6aB4E+BmtQWzy5",
|
||||
"MwRTh6VzhDkYbOvk1mI+E5c+8nWOgeLi/NTkkSKVnOvZ8pzk8502Wmif4Phqqb2GChm6NC+ZxJJKV4gi",
|
||||
"dg9pBsAECNHAouNzKDcjV+RAUfeI7GhysqDoLG35U2u1s8fKieb2YNgr7iXsHIxRja+GBJzEphGk+aCk",
|
||||
"4+Pj8v2rWcrYOX9l4bJ2Xn1BXB93ZUxUo0Bp+i10z4DFEtNYJ6awTsTg3gujiDLA5kP13cI0yGdq6Ld1",
|
||||
"gT9lgV5Ai5uVYdKTdtcGZ0UbFzAWKTmIqjfnvfeodCillOng+JiToj24ZfienrLKYACvUBNKtPe2azrQ",
|
||||
"oPQdq8H5+9ur//3+pNPtvP9wDf+EqCP7x/n737zGAhcAP48c23Wb+V29WeOuTIftTqzc4kvhpfrn6n2q",
|
||||
"iogoSEdQCU2mBRNR5eEbHEOwezuIrcRfT16Wl8NC17vZkxKYCIUVFLCQ2Fy8RU55cZAzwY1G+/svR4P9",
|
||||
"F68OD16+fNEimOiERRGxT17FQAbzCc1ZSKI+es8kQT1kBryV5Iu8tTmCZ1igMSGxCTAIkaBxQJBqo2Q7",
|
||||
"N6nNqEAhWXAS+DdEn5B20CYMW+yte+OKsCRCrjGbxUCgDm7C247g7rhfWmkdqGr4EsTdIsK8bGO3EeIR",
|
||||
"qsUcS+QikSYMQRF4fz3RZqe7It4IvyVcFXMhSxilnVYjsg0S0Ia2tIT9lfJkzDAPezqcHqlVYZMeUt22",
|
||||
"7wgfM0H+S93WS8s2Ifin1Wr/1T5aYDmDc4TxxQzHcJGC9KvZFWCHQWoVtkgiiKkqzbzr6gwFGGyScU+c",
|
||||
"9flpMSuDbb2GsV3sX2D1sTjJmySKkFkn2hH7R3t74yT4TOTeZ7LctQAUF6/uGcVgsNFBk5HKWWwKkI/5",
|
||||
"CtpSU7BgaR99Y2bqWNMhWNAZNnT4faerRTeDuAIrgLOzmNNg5gv2t1+yXPuhfifR8cU2KARH1fHEax0z",
|
||||
"isKu6Ffy61L6Hk3e0IggQb8SpZ2PlxCzCSmSlESIyBQHy4xDd2vP/4PR64PXL16OXh822RbWMA45HFsy",
|
||||
"+5TteCwKCfeJggsMIYq6ATo/7ep8UDSzRCglwWTbwqZdvZTwWhJnWFyTL/KSBIzX5BPDJRVlx4Qgpr/s",
|
||||
"Ig6D2Fxh6RuHg5HyXX6GxayJSn5XbVpxlFvBQakBY+JLo3IcRUh/Q5BrmYRIshKsre58b9UwBnklOVa+",
|
||||
"9FmzjV8ypzQFonnB2R0NFR8mHCKWtfV4h87nicTjCA6ZZhKTud2tWwsoPmfpfqaHsVfWuIoY7GGRlFL0",
|
||||
"10mhxjIflQ/T4/3Jq97L/YNXvdd4POwNyeHheH8yfEXCl2uclSk88RvNSOWUUxYiJQQdljPZdK3MtCIR",
|
||||
"7UR0OpM6r/Mcyy4iXyAA2T753dIQYst1+KovuPxZGlZLw8cUdM9S7d9Hqq0vySofVHPiTJjX1YzX+75k",
|
||||
"LFuincoV+xaWqyXgycCX/qVYHSqGuIJatTfONboYEWS0MqW6qEDmnUF1UV1vzfQmScKFrZSlaMGC0Fmy",
|
||||
"RKdaSHM5IPvxW/mmV5FC7ThTX2HmVLmdwL/HyXSqyHJH4JhK+pWEu/kDCLyUjOHRJLANGdGaWJoCIssL",
|
||||
"1fCmn19+EdZ3OJjRmPQ4wSEUQtAQQ/gZndh8asa1YsrxfI4lDdAMx2FE42ke8I+C8LR0SQ6wFwc+z6Cq",
|
||||
"nTckgfSvY4UseMfBSSFtdrZfU3pH0JygKWM5P6E1DCcFErZQegk4ifDxlBMy994BoXQXtt+NlC8bSlSD",
|
||||
"8LjubT8b4x4LM045ZdyL3mDYGx5eDwdH+4Ojwfop4zREbzibn3uyHp5fpGnjIFfJ/YwGMyd9HHTOl8N7",
|
||||
"PeoPX7zqD/vDQSHJ3sFhdZmLK53c6rw5O2CWG/yRallkZdsqdt3ayUyfHDb6g3WSkTlz+jBSKBO3hoLq",
|
||||
"TJBGX9fMIVUbtJMmlZ1RIRlfojtK7vNFGK4Jn4MFxEQPoTsPCrxhop51HhfYqbjnW3IrTWoq0EFuwiwh",
|
||||
"JZaQ+xJMPimwO6YYhi1T0YU6ZinZ5hGmGhUyLTaVn/Ndn4pc5K6iSE95mu5mQqkgDRqlYNt0cnnZKPxS",
|
||||
"cV7/lloeo5XqmZfaze6X/76p6wCd3zV1Xba7tfnrDrfhF5OjrJWSxuUorHURDC3Q4Bkmzhb+A+gLP+Vx",
|
||||
"+GQOH59sbym8q2gTEuNRHAfkIk3dWSm0dQ1kJw+0uh+kAyBOFoyXqfQ7irWrtaXa4Uak2rk9hOpFmz6r",
|
||||
"Ni/FDBQXeEpaQAGnlgvF/ip70CxEHXkJ+5KDLoewZmK91KTmZ/YSSWqzaXYnmRM3RXCh8FhegjVpD6ap",
|
||||
"E8GxyPFRU38v/0Ea+9TC034IB4qK9KsfY21K8KTy9WCmtQ6VgaBU4FWzseZFll15N83S6mC0mTIaYw3U",
|
||||
"6qiQNBAt5ZeWoRXOc8Xkvvczhmb4jnguv8PRhpg6A/kic3Iuy+z0WyvoXrnATSKGZRvPaCeFC5PHayEK",
|
||||
"MqmWwNnfpAD86GcGf3LkysJZ21EtHQC7OVIrobRi45tZAnjSf1k1yguNW/NBfWGNtD6Qbpy+CbhKWJur",
|
||||
"UAvN1jExmdcoOnGIaXdTOm5jTM4GbGTNC2gwnDUHDj11Q5pJk1xjWflFpLmUjYHfLUOyY/xKoYAVgkDN",
|
||||
"mMU93TKzyZTNKz3jibqamaW+9N0DbUD/CzWRQwu7UANJ+MsIuCai3I5YMq8UNxfJOKJB9Q1Xf0+TTudS",
|
||||
"vxekQ7FETFqr2uNaBl3hEZXG6B3mn0N2H5unnxzG/gMVjY838U38H/+Broh+eR2qv39dokToVwYqkMnp",
|
||||
"0e/3i85oB68OX3otp2lOGa8/tBFeVKQX0jEJ8JygtB/aYQv9YuQXYIPh9UBJr00KMLqVW7P0W45/T+Y4",
|
||||
"zh6ZdKt1bMTNIuWuyhDxtw3bIHz38oyc7RItIVdxkHa+r+YgIwMLT0kV15gf7wD9W9NuFWFWQr8RbN92",
|
||||
"Nh+WWi5VQnTikVs+YhqtZ9DKT78dm9YMi+N19TmSE9wrF5PLpi5h2rP2Km6p3JzsWLAIss/zXr90dfTX",
|
||||
"sIkjqu8VGmyXRxPO6Yy/eu6057niiVBrAS5Waaf8ClZ6eoLRHqZjdH+sw9uEmPqo4cTEdGa17DzVnUYb",
|
||||
"MaEbKFbZbltpd7ubvRFlh82J+Bm1HSrMoVAnT6mAQriOADXRxyvcjH8YtcrW3r9ROtVNR/1jNBgd9AbD",
|
||||
"m87uY+tcLnO7LOZuXMNJUxUBXqr65FSwj8n9j3u5yZde+km4f9vss2q9qtbMBL5tY2LKn7ZgoNUgKbDT",
|
||||
"6rcXA+9KTh9mFrGlEoG6pPyzo8WKjhbFrcneRDdjkk+HrvfmcSBo/Q7lxo6u8ADlTLWyc0fhAbDsKDAe",
|
||||
"c3JHgfhzagbETlDhcXh61que9arvb65qrRitXg4zRxaVcZ0PZ4MqC9aqtL9dLQLtmGqZuw/WJ3zB7mdf",
|
||||
"FozLyirvbvA/gabIJMvNCkRuJuRW5zK4jVhQ4WV0PSPIfkVwtbVPZQawr3QBMV/onkaRUoYgdVs+DgBC",
|
||||
"zPHe8Z7us6eu5oPRcH9PCZx+IO4Kr1ODg1c5vEL//l/V/8wIfwx6rz/9+erbXv+vNzdqBC/3tkuzojej",
|
||||
"Is2KG+VYk3JFj1EVzGhQVRHKSHoH4eGw93K4H/RevyKkd/DigLzafzUcTob7a4jn3Hr8ke9MCArhJBow",
|
||||
"KC4q3Gw5blpdGt/a3F9Zkt1PlRNfa0qtp2pDzsabUoPR94RAkgjy6Eqvr8Ab+KA9AwK2IGiMBQmROsch",
|
||||
"Mtkk6rjDUaJX10pngXAxPXReZ4FH06L3MY2nRCh41gEz7YwWNt7JEw5G4vC2OpUPVCpWn0FRtQP2W0jR",
|
||||
"/ZGfZbisnA7qzvINTlgWjj5xWYrgg0Os+nST1vxmrvvFQHXtXCV0bVkch25hWY9JOq1R64smg56Qyrkw",
|
||||
"h7407OAo0jFQ4FgTsFhQIfUjEU8CmfB8qpDaCtv5YrltcozVGC7hDdm62GQGEi3a7SqaHrjTLbfV7RsN",
|
||||
"lPnsJ+2DXvVuZdtQ1/PKbVufkcRt2c1ttYu8T22IsuqN8I2X+qyClI+G2xzlbZ+o1rbJD3rDF9fDg6PR",
|
||||
"y6Ph4TZs8nnSLlnjnyJ5+25qJcLZuD+7F7Hrc1qL+5IxaewMe3AK7ra25hQqJgwbnfhgrTlmz+5Tbdi+",
|
||||
"YJVuIQKqnz+LG5m+Txv/V8hppSP0ynr9D85tnvv+M6u1NS88DXZZkSOM8lyhCOtXmIBFydz79qKj81fQ",
|
||||
"0k/SPpChRF0ZWuXSKqf7UzsClwW/qq2/KTKfJJF1u9Bqf+52sbqj5SqGUWeFXQdfKegN23LiYrjmaqi1",
|
||||
"ikm6ZelMuXtiRIS4lTOs5jflju2fQcQECW8VvfE7sN+yBYndvyMykbflZpxOZ77fTdIHYAH9L98ltCp9",
|
||||
"j/69FCrF+BTH9KtaYSHZdcm0cr5alYIfRmynPkUl0Z3mFNuw5PZKYCezz9bl7pxIToMV6v5o6N7pbucx",
|
||||
"FFYsl8Jxct6bGbJsQRqXffTBkzjSNIbEkTcxFU5+ERP5wPXNto/eJ1GEmJwRfk8F6d8oTss/A8CjFq/I",
|
||||
"L+lJo7QSssfNyG58lFj4k1JqkHROSrgwgylCX5b2dvvokrEU8BkWuuVNZ++mk7eqpUVZxN5oMDooPqQX",
|
||||
"7P57/b+0tMYD2N1MFqx0LALYTd4KJbOFXuxjiKlV9TqyNdlQTbsfzPMZWjyQiNeRGGsS7UdBNI3CLvKM",
|
||||
"hLdOs5XkWkui9ZW3NOgNNbdMo8ZR+ugMBzO7i+ZwF4hKAckuFfxdMM5ZiujfxEoG9CJyR6IUFjCr2SZI",
|
||||
"EGAjJYP66MQk8w9wrGj4Jk6bSQZRVrG2wTn0jGaUcMyD2XJPckIyI52WtO0MyJozWhVXyyl2BnHVu/Mu",
|
||||
"O7ZaHTw4j11XeiAoAFaWIeMlpH6DkzFNlHVR2OBaj4vmei0VeRU1RGi81Ens8snETrGYjRnm4e0lweHS",
|
||||
"KMrnYH5W6INAwA8nl7cXttAJBHfqTXDhdxXsDL/ViQc/gm+PGz2kj3SvBNyS8AEHiNM1S9WUwRw2BtKv",
|
||||
"HEee4q8EbDclqUa6NnpV2cUWfncKVvmJfCdNsGmhMbn1qfxFIP3IQmK5+0zzGvdPn6rWIKXLivykjo7F",
|
||||
"dX5SeHekAh490tUUs2T4Dv33mWq26YO/p2ELN6YA+BD1u0kvWZOl0mTBrHuTr7BknIeVGSOP7YsbvPpX",
|
||||
"FBNqI3zVH3LZ30CqFh96tEO/XK5WgMheon8RiLOICNBc5IxQjhaEz6nwO1VC22pVyQwlBJ3GWaSP1scV",
|
||||
"pfkmaKWjpItkEVnVAKVhrkUdi7z7r3rWrEapfnXIyg1XeTsI84lJ4fbMCj4/Sp72oIQDDpTkVLDcUXLv",
|
||||
"yDi1f9ovQbSoetDtfCYVqV9h0Z/Jci+j4nyYRxJSCblT3UlGeW77A/e+Dnqvb3uf/GznF3oF3yeApFyd",
|
||||
"yTd/S7ulu1uVROw0QlOOY3WDHC+1VaS4M9nz7JEC2zFJH0XqaqKaqj3RXytqH9cg76gJkRkjHDYygtry",
|
||||
"tOaCiwofb8DBdbxYRLTK1co5ovBioWgT3TP+eRKxe60dwBc3lXPhtVjnfG6+ysPoVmiV9I7NXOMjq1EV",
|
||||
"aALWAefvDo4WMxwnc8JpAOyWxOr+EzBOkOLNvJtsXs/plqqhu/t93PuH2eZPLXwtLQYy/FXuX1XucP27",
|
||||
"LiOit8pJwN28ZT4j8bW1DWsToR4Vwh11n62ZjNuQUc5knC5VMfSWyGmTb32aBreVIu5wBW74e569nxZb",
|
||||
"lB8Ey4zSdSjYyzRUyLobxoyUy6pmZXja1lzMpV9vdtm7Kvg3FFJSwdcePKYZjzBzzUy1Y7cGyvBoiDiJ",
|
||||
"dAGqGV2Ub5cYcyLJKeH0joTHcxKHapD36gZU0pDsV3vrCnUvdVweY1NEsfbNt+kamgPmLHXm9j1SsRhW",
|
||||
"6QQwgFdgQewc6NCForBpI2dcUK7V6anzidUCI7N2XnAg4nd/LXASyS5JTO5xdB77kksmkqnrmWqAaByq",
|
||||
"Yxy0Jk+ipGwota6GsdSSRPGy5X3Lqym/+d6puJHaZRszOAcGrRUJpeHndFTTts24ExqR981lQsq6qJ3k",
|
||||
"VlFWfxFO2s6mRrqmvjdt/AXZEG3jxW7iuiBTkVM8OYd/7wvAkvBm7EOzNmBDwyvp97S9Z72IKMmtfbN1",
|
||||
"aUvI82+Gz3B2Uqj7OyoJ/U9/jr5VlRi/+42zZKEW8CaJooqYV1vPxZTu4GiqOrVaZDrDgnrN1s546P3F",
|
||||
"eZt9sEN697sw5PX5+5ZDfpAzvb0t8ADvryk22uJBz+DDw4fceCvgATp68VAYsj0eVPPVqTJDRUaY7//3",
|
||||
"moTpcwFvVQC2ovbr2jkBBQkSTuXySuka1kEJcnIdJz7LoPpViRojaHRg9/Ecf2VpRRB0fnrRR+cKRVrr",
|
||||
"uXxz8url6BD9z9+v0Vhd/xagWwSmcLkFAdYUsXutUiRyxjj9CtOcsJCUfvzIo85RZyblQhzt7YUs+Lrs",
|
||||
"qwb9RPQIFrI37GOAy6xHKeZ7TLUY7dmBoKJ8wBYmCkGnVuscpwYTY+6yOjxbkFip253fiEQfzk9PkGSf",
|
||||
"iXHxguKRns72E5ijP5MHwa2nc6kHfldj//Ne2h0bE8wJf2PJ43/+ft0pWufVVsBgiI0lpjEJTQpwu4c4",
|
||||
"t8/rbidosKA6AEgZH6jld759g+iSCbMpCdTZ6+zDgnI8xXeY40Xy31qXMrcrbQvqaIURnceBmizJIdZp",
|
||||
"X3qbOL44T1XdzE9KHW3/b0L4En3gwYwIqWuu2owF4BQW0YAYg2kZBnDkUid/IkgDPCaSr+Ob7xhkY+o3",
|
||||
"2Rn0B/0hRLItSIyVcO3s9wf9feOwAaS7B5lU9kgSYfXnlHhDR2TCY4EwWpTi4nEheREsNi07q66kcNFx",
|
||||
"wi91hlwb0gPeRaXgexvPbo71NJ2u2vXOUedfavHZbprYZ33r0eBPcBLJh8fAf+uuEPheBZqOx/bC98AQ",
|
||||
"+W+f1LVUG9xhN0eDQSFLB86sanv/FPqQcJ7RssQFJvmATjUwzAX/w3moY/FHbjj8H4WUVFXh2rmUUN7U",
|
||||
"PSnAD88M4vgWVqU5cvx6vNAUAnmrlgUCvaVRxAkLN55fho+rwrEzLlZ/KzqswxHa+ZVIvOtD1bWJNsdo",
|
||||
"TCRO1YAKXA02gKvD3nBtXI3zuLIZMaqQpZddEHqve2qlnW+fvrk81zIVAzj5wPnSItUD4kRySqBSfQLH",
|
||||
"9ySJIjjAZgRbT59LLMlbOqeyCgzTdi9rCAAcaGb29UiZfs/UhLNhf9Bt2NztY5xqMqHutN/c6Q3jYxqG",
|
||||
"BC7yB6PXzT2uGXuH46WBDqJgDtutSh+IUGYvp26C1ElVlj8+KRGY5p7Xu1Q6kpT2gKdKYOm8lZp4Ooo+",
|
||||
"FkxUOSET4UmI1EfX5r4PYYozlkQhGhNPvqPyOahHPcsljze+q7+ycLma4K6VBnej/mBFsemXeaMiH1eL",
|
||||
"rVFBbI1AbK3OgMYR1MOCFTkSM7us8fAtnIjD74vYgoyFg2Q0uh4ePPQ8Gq0rY8PVZOzGdrZxT60L7b+f",
|
||||
"LB20kKUnLJ5EVN/4n6TwPXH9swtZ5Pzy91vXvX/s5QunrXwVyddLEdrPyIBwS+G5UYfsoPHyJsY2kCAo",
|
||||
"CHjVSw1luoDrg9PYXM5tEoibuJDf2IQqap9Y/0XoOFc77vkqtJGrULcitcR46d9o/brpAzQjmRykq/pT",
|
||||
"VcNTLlmgCE0QkqouokhxWT7jCqANxeYgLvqsFEF88O3RZdg/3BzeVem386UnSkUiykUf2tZvyFVBMlmm",
|
||||
"CnXW2l7VysVEKy9pbU/cIFfAUm/WMHfIK3Lxoe/F9eD10fCwAn2Dvvr/wwrUacSRw97kBX7Ze/V6MOyN",
|
||||
"g5D0HMwNnzzqiAd1ozzqPrl1LvXtusGKMTwcrKTA5EuE1twOnZqPz1fDJ3g1LJddbaWYZLWGWigmqjEn",
|
||||
"MxILCkltirXdxIzdq6uCzhamazjpGldxmFaYuomzBPaVmsqpPjtFMct9PpkgopObOGauJkSFfZUKQd/h",
|
||||
"KMQSq58nRAYzEuZL1YCLRxAkHAdeveY3IvMFnJ7VmhS+w02rNVdeZQZc94G84M1lgticSklCSKcuKiij",
|
||||
"/70UoGI1kD665ol+LTVkz+Jo2UVwNYWfYyZ7zqcqyNO6bBnUpeIOD9Z6ChUXn6K1uFDUMXcc6hqjh4N8",
|
||||
"yc9h+oOpvrmfL+qYKyoIlQH9Vf1eDcql9fYH+bp2+vg1FR//yAq16bV+Ry0yX9PLo6alVbRqVbjUprK+",
|
||||
"UuaBZFQ5WzXewOX1oF59HB36cabhnLzo4ZfjV73Xg+GoF4Rk0nMAHfkh3e/F5L4Gb/s9FoUFHW4lfaxU",
|
||||
"V9Wjkp2UDt4pidWp9e9pYDpo7vGeyTcsicMnq8L9RowG56v92EqR+zM72r7VqXKguYsqzUsdYToUu1+l",
|
||||
"C7kW/gfdsB//ffUIHQcBWUhAL5vo9pVG7i5askSf2kYNFUS7bf7Uz7FbsH7/O18YfxrxVCEx6l4g8xcl",
|
||||
"H2hZE5egzk/B9LHAMvC42+kU3QK0Zbgc2rTlaAdoXcfw5dzXd8GCERcumSfmrTPAccygosechZB/pyz8",
|
||||
"9KQbeuGsYvtCPvRazkU7l+SOQnK4dXjWZIr3cK6OkABnvYVxom96h3yW/BuT/AUS2J7kd+jnUc4AU9r3",
|
||||
"+QT4MU8ALS8qy0WsrKLuWf7Wl/gHHhReV5crIkW+KgX2ViZRawJbodKK1XrGWLE7x7Ew4V6SISzZnAY4",
|
||||
"ipY3cRARrF1jF5zcUZZkVqhJhKdw/kBGohmBB2MWkz6yVtSDwWtE1WGkowkAPTdxViUSo5BOJgRGS70H",
|
||||
"WGwmiLxWymPTe/vq+Wh0DU82GxHSXp+TOkmto/aVtGbciG2W8MwZ+cfwSmkSzo/mlJIR3bNQbhTKP4Uf",
|
||||
"ixUURV24hQQ3Ns0Kw8JbKqQwLz7wwHL896v0kQWyfGRm226WXlZnehOMQzoekSxsQZCyd8mxgkJbWX+O",
|
||||
"JxhOBLxwPbVHGIJ5MANtV2fdmWMa7UEIXLptFaAK6Frrp9FcNan6aQWcRdIa614A7EcPojo4ipwMxiTG",
|
||||
"4wjeU0Iq7D9Vk0+tYCIRvF8qykXjZRU4jMtb+OqDxwZQpRCZv83hdAt1RSMspE763AauKwVOSDkJaqgd",
|
||||
"oGJch+15ESUCByz9l5rHB8J6L07tjrKU45tcE0DsPLsffFf3g8RIZnuMwOat6o4OHE5jNGZyljtB1Clx",
|
||||
"Qficyj5lSCwFPF/DuWKrK7pZpoyT5HlI5gsGJp4e4kb/Hg0G6MP/o1Rw7XwWcYLDpc4JZ4bUGZsCHCEs",
|
||||
"JafjRBLRVyroX/6S5vB+E7H7o7/85SYeGjcDKlInXxrngN9ZcArlHvLxg2YZuzfxCJ61bfPATjHBNBJd",
|
||||
"lB6F+gcN4u/X1xfocDBCOzFzKvmQcPcm3vdClKFvJxcyugd4S2E5AFiyxkVojBCmUWRRRrMg13Eiddon",
|
||||
"eSuWcUDC/4LXwpv4sI8udU4z7mQCy0M1JkL2yGTCuOxCGAKNE7DywdQJJ2JX74EaCR2nG41+JTN8RxmH",
|
||||
"7eiZiSA5H0Coplmqa0I2F4nvKGe6tymNLZIF4beg6uhF2n+bPF1dk1/hVn3bVfMYIbAXs7gHE6nRdOY2",
|
||||
"BTASNIILm6k0RVDEplOlaxN+R3hP0JCoYSAxuKt/Z/gxaeAWC3XdtBnB1W+3WRObkw5SlqjxrsuNdJmr",
|
||||
"OV6iMUEYiWSsbqc2SZoWE9l0qjGsT0dAZivqFse1pf9iSE6uVIWFXKIdIJEZFihmbhIws3sXmEuKI3Sl",
|
||||
"l4yuAhJjTpmw22f4FCh8NBgickdixa4emmQcCTYnJdYHcNVgJzMSfM6TpMbUkXHAMBk9dHkHDomWYia9",
|
||||
"fAPjsflCbWUBEXeihEbJUEgkCWQRuJSa1XjApwGOXdFA0I7hp11gKLVvEQ4+oxzbop0UsF2FHbU4zaMV",
|
||||
"wTnpabqy4XrF07o6yuXapo8yES7I2pVzKdCTDMaH2pzXgjxNAOlZwceqI2OOZTCDEifpiYF2aHr+7G5I",
|
||||
"O2kX8rOdZT+HsDzFq7/qNWru9SsOf8OS3Jt0hmtEvRi2rNDvCnaCvT/hNvNNK3wR8aUVUfILx/qI1G2E",
|
||||
"4X5tQahXAvsIon8z5YgKRDknYNAYR2BwBQEilC4xoXwOZTYQXIayuptlW8MpgOJKy1pjwzulZ4yJzUAP",
|
||||
"cyhppufUC6u+i5lmJXG3BS+/FYWCRkOjUNAbtxWhMBq8/G7LWmglJaXMEO3Mk0jSnrYw7D7bSH+Yhyu9",
|
||||
"41quLDKRU3dVbXCdyqkwLUQVGi+1FQ3hMORECK9rVV4/2z576xjjar6G7890/kO56ABlWmqro3C1Tx5j",
|
||||
"jLotCXid1EOVb/g4DveYe5VW2i9LpDGvQFJ9dRt06KdwGVFzpGR4BhNUULyHLDVAG6DKbuf/66kRexqC",
|
||||
"nlmfJ1frjMA7rR8f9e74xUk01s4/tJ4lRXP9PD8Mi/kEDSP6yp3aaggFVGiL1Ibkz5OMKQfbhLVDEssJ",
|
||||
"1QbUlZwkPqaBhS086bQl1Lm5QrV3eHfoovQJYrcgCNAcx3hKRNn84pJuH525Z5/jbBfMcDxVqlUiiDkg",
|
||||
"obBGugqlSJvs+fr1Z9dYYY8tqMiswNiOMrABvhRyML9ZB6S8dRZMcHMqbc7fJUtQyOJfJLrHsa7NBmCm",
|
||||
"dkIS6zidzM5jGsDfronyHaBHGxkvzy7eHp+coSvJsSTT5a6BWOet1LVRHfsbMu2vwASX1ivRLXYU/nTF",
|
||||
"mjsCpshrpv42QxzZolro+O3b1DFFd714+/EqdUtRP5nenMzZHSkNkHpY6t4KNxYrnwlZFDpbY+GRRhKO",
|
||||
"jUFQr+iPTxbVab15Y1qEx7SI4DsfKSWxIZMt2H43aMW1N4q+uyYxY/fCVIuNcWSyZ+qcgtmFyV1uzsaY",
|
||||
"H7LWxpjZFBWSzrQzC+qhY6AKhRVA0IlLDEfoj5uOY9S+6Xy6iQ0l3djyFDcds5sVXbpu00838SU8aje3",
|
||||
"y4N5SRYRDgg6jiK9hRXQpiOo4TygG06UDP1TkcBNx7Hqe1aS+5oHPv8J7ZiZgQqciQ3tgw+lz1/40Yyu",
|
||||
"1e68qdHVke+b9u3d8OXj2VX0Z3AVLRIdtry3shVxz7horO4qWtCCvE/Qp3rw1PiY1w+64OOpdBB1RhcS",
|
||||
"wGbx5eni4IxYcALnQ+i+QXtMjXrmx735H4NvSLNhz3jFPDPhD2r20vu3ntl+T3tHbYffLklPDw+prowD",
|
||||
"dbTMKM7DhX2lYEnGVZ+8E0WAF3hMIyqX9bx2Fj9ZVjOuaM+c9mNymqasFoxmCnQopmrhC6Vbp/X+0qwm",
|
||||
"NaY16H9iy4BsQ9/Ug9e/8Nc+7gcudA9NbbkKzOengAhfNL1G9M/yvP3k0yemJGBZRW+Aj1f2/jTB7Ku9",
|
||||
"IRvWgRt7BPdrFlDYXJ0LqFQVW3RtXfEuClgUkUAy9e8xlsEMJYuI4VAYX/VlHJgiYqKPrljCg0Ixq6t9",
|
||||
"0AHff7hOHw/HibQ1O7GcCceOsOVn7FQYPLU37K7HAh7DzN3UU/Nq36CLTRDjixmOFXt6MK7d2Iwj3Fix",
|
||||
"eWXynzETFY70NlfsY762N79GG+Hke2ZHO2Y9aM5CsjkPI4+lvgaKH1JR+RnUjt8xD3t6R8oij0qBCpGZ",
|
||||
"RSnbXSFDiBm9PjeIo3hsi11YYCap45Qf9uH66SZ+yPYfMstWk9Rqt0WrmbV5onLLtruqsdIoKjVi3fkR",
|
||||
"NOJ686vViFc2u3op/Gexjj5h0yVeWUndS9XG1XIvpd2scDW0XSVjbfNfl4Z5wm0K3HQ6fxYyC/qzzN2G",
|
||||
"zHUpI6UKlyJtg+0K4Csi7fQZSM2i96qaWLcghDNUyBZC2Fx1bH2zdFn9FnLZpx2naFGjPovlzXPEVZ4j",
|
||||
"6pmgKJnTKuzNghkK45ug0uyGOV469XW9kjlX7/pxRHO+xHaNPpwuQy3smSA3GPIqc2XMdbHwkt6Q7tGm",
|
||||
"pLTXdPwRTFRKsYAy5rrMskmdrT1VCsGyTuqeosqsRrJQ18pr8IhfYC73JozPezDc0Z8dEgcspPEU8irT",
|
||||
"XOWha1OS3aF6XeyZfFlEUM/UWGGEXEJyFDWw3paUKfJV1u0EZXlvUaCtd25p2DGNMZiFckXzX+6/PBi+",
|
||||
"Gh3U1pr2FNy1GA0SIfOVpbOSuJZKyqWtDwavc1kR+n9tLNAPSy5X3S9LAL1yjQhjFVvj1mFJwSBS58Z7",
|
||||
"liGbuGsYnoUNrZUYVUfa3tjqbI03Dnus5UzaqQpk7iMkzARY9Qn3qxqCNKZheaeTkNhMLGnSecjnLhNe",
|
||||
"ZU+OgEYep5DO+zJs4jNdVEDGJhNBKkAbbKT29DaNzbBtmuiaEmrkiOSZ3TenMpS57/urDP84v0CYBzNd",
|
||||
"0iOWmEIYhz7dI4IuTt8U1Rx4BNNrWXBmcrI1qBJAfg/VJwyc9SrFV2DgtVUKZ45qrWJM0vMQYZHHYntd",
|
||||
"o+motwO2Oe0rtjG3ey00gNE25E3dC9evDktAgr+FJKHO2+WS1rMI2pjGUcHZWBSIeG2FZO9P+E/LTPTa",
|
||||
"MENCk9oL0Vhzjy1B71hLXfHpM4+WZc32z9IaZ+YcaWdVXZ+fS7+TUdUlnyyR3FaO3W5jWyAOOKGLzES+",
|
||||
"QN2HlZ0vWxz65zGVFN7TlNavJ7JytmzWuuZ0OiX87IupQ7EN060e3MxUZbs1gJqzS2f3elQfsvPwHREC",
|
||||
"T/3FmzV0Uq/h+WVuK/xrKES/AxfrosAP1aeT8e1qLnAXRdYPDI1JxOIpRKqxVEnXJdJTVyMTJicQwcHM",
|
||||
"9PxF3MTnp13wXNIeYwusM0PDZ6S+4SjSdfL0qOZKHLBYSJ4E2rBoms8o4epEXvZv4kvGZC8idySDEjJw",
|
||||
"6Qp7OguXnuw8hEBJ8Kmaq4M2ENptjIrsqt91wdYj2bi/TPOy3W/iHXvGo4AloC9AhOWYRGjMCYb68LvG",
|
||||
"nQ2HoUDsjnBFzUgwW00e4gnH5CZOBAnRvQMeuMTFhIQkrCovrMXbG7OXDcYHx52selE6v6mDhNIax0u9",
|
||||
"xN3+TeyWJMyqty0IB3VFF+zBgsVCL8BnQDDzPh0HNI3NJnOApbb0wUwzw7Mq8x0tCe6eeGwIemM3YkAo",
|
||||
"SlOjOLV7TQNLgVHsW3k4XEHbR3Jv0D4desZ673CzhGdPh40r5YYcHELZjodZRsZtSvBbc7lSCewpbTyN",
|
||||
"dWKKfs0ZJbZPtGqyOquKFd0W9p+nAteTlcgOqTT4jaXWkvXCG1IlxXr7hmRB4hBe+/PxDSRWuy66KFC6",
|
||||
"hOgiSb5IRL5IrgubiK7JVJD7CTQeE+5gktRvItKhIkTBeWpuq9E9Bwh4nme/c4hALRzPquKTCBIIM1ar",
|
||||
"NXq10OusN506IjOBZDQ8GnYdIZRW5xNJMENY3MRad+2mvhJ5kWN9VEzqGUgnVpBbJgCromD7GhIlvck3",
|
||||
"zFgUJhVyRHW+hA5PSJQYrJzFnAYzReS1wuTHTf/3s1irw8JmZNy1WaO1HSLVktOjc+9P+8/z8Nue5tE2",
|
||||
"ziZRZBgayRmW2j41JiRGQL06IjiTGl0ExUiU8rBEcyak4jWwmFEuZL+Ow99qkB7INfnn32yZVJePb/DD",
|
||||
"VM0Nr3+DZ15bdX7geJJBCizY/8Z3XKu5azieOfB7OuFnhKzNLJ4DFPa/hvk8juiWr8GbH44PpcVmp0fG",
|
||||
"dLUxtamPQZKASHA8DA4H3ko9/jehY2BKdbG4Z/zzJGL3xqyb41Jt/OM4+KzOdMeWCknNylyqRl0Ccrb0",
|
||||
"cARjH2fdq96OsrUoiB711SgnGzycDpBZmfisLz+F+niKRhD2MUA1z6szkyQRbjQr+WqPxqHOIvWWBkTd",
|
||||
"To+nnOickWmZanhSIXG4YDSWiAq0SMYRDUBZTtOnGqoupqjyBpFpEM4UyA+tEvr9ajBvoXT+FivlX8CW",
|
||||
"1Ub6G8L4WQvn/zDioWyh9uxMbcFMKHYMlFud7EcfC1omJILwX4SmdWB9E7nhm7h/E1+mlcyGcCqDnpz1",
|
||||
"7kKJs2J9M/gc5mvS3MTqgDy/SHPzUkivxeZY6lLHKMALmSjVHNLcO+WivFWI1RTXbCNyRYObq+FbKLBO",
|
||||
"nEK3miFzv52vUji9ZdtgNZ5PRXnrikJmkyRLt94Kgh1r80yD4NP9ftzaQvV7VKiz/JPsUXYmaytQycpa",
|
||||
"4MDd53eVzahjWkgaQeZKR1cyNovi5hfrknpWEsnmATh1gfCL5lNiKsLZdhMO7AQ8/a+EBp/hJUepMTQm",
|
||||
"Skhb+Q/OLzY6ecHZHNyglQaqQKiwdMJ67Zvp+gJXZNwMl5vMYdhW8isLJoP5OxwlLQW2blEUB2a8+p9X",
|
||||
"ERQzLI7NUuB+963biZk8rlsd5FNvXGEJ0tEDIB3nIQWD8LdvK4kuvfF1cguI0xDusyb5BIs/GCkD+1QU",
|
||||
"NfViDTwe0/djr1gz1VxcaaUT0LNJ5kXpSZSqnSmlTVOzpecIPU2N87zxqlX8CbZjNQa8LD4frpskQeNa",
|
||||
"nVrxqnxqV31V0P3TNwVwM+g5bgYt/cfy7wF5V4WCUbbO1gEF28/Snp31zLSFV76t2Wm35+6ZR0Mbc0QR",
|
||||
"6c8nxvf0lavYFNfxM7/Fq1ZgL/EYpI3QWSV6oAqZQigcL23JnCY21DOUWXAbrwIlCjek438bKK3WpMR4",
|
||||
"vCeCFTjyTRHYn6Yg70+h0LkpjtfhT+8puTejQjK+bPnqbi6iYLsuOfYV+HSNZ/cC7L8b2H7iwzTvGmDR",
|
||||
"29o5oIAwe3XciJ9ASRwYUnmWAN/xgC4dKNZ+O0t5ZU1BkL49rZJd0s7uEQdtlOcCiL8u/5Y+fjxZju/6",
|
||||
"gLF4MKlodAYaQFe1I7DIlJQGwGgsXxy46V5ej0b7+y9Hg/0Xrw4PXr58MWhMTPNElP43PiLJ5weyts5n",
|
||||
"KfOEpMx46XmXrBcwWUhsq2uBjo9U5MD4FMf0a658QqXOD922perD4A+oRTJxoXskdV9P6WM9jeHnKiSP",
|
||||
"paLbza+KmHS4ZO9P/Y/zdaJ1DOu4mfmDGY3CNIKTK+AFvSPR0sSXM5aGi+9ARdxf9n7ZdQrnmhCLPjqf",
|
||||
"uLHikpM09Y9wa5sY7/bb9Kc0GDzlC1AGDgav0QmLJxENpP4qEx6ricBV3j9MV4cgFcNtABjMCcKRYG7h",
|
||||
"/Qz6rYUUpWKnrfs/wJhumW85DoorVIYSdlZz/X8uiNIu3umNQ+vfOeSpCZR/Cx1p0ELQW5nyg4RJ5Xm9",
|
||||
"dUS9x7xoU374/bztobJpL+/6QhqONFPg7ClgyoL0EgKzHkF/0xNV6W+O7mlw93jliht1NQ6gP9tfn4ir",
|
||||
"kC70v65il0U9tbS1ZgfuPZUzGrt2FxvgmAVB2lDHLGuO6N/EH7JQx9YRiGnOmtR0k3mEYhTR6UwS3rsn",
|
||||
"6h9ISw8kZzhGv51do3zoOdohX8yspqY5DXWFczZfJJCGEV54divcjzRmTx2FZ+PxlqXVPrGIy7yBOEdD",
|
||||
"rSzEafBdbMVN0Tb8oCCynFaoqfJZQj2BmE7Yjxay6okqGRUi1Ka3ahagTvzYgrMpOJ+7Sbm8eOpXy6B3",
|
||||
"aWKtLasDdqJqrcAs45nNvqeB1KiMpYxvPyarzZiuPNHovfz79bu3aE7iBC3wVOenc+LBTGl14eWj39mc",
|
||||
"XOBpC58/ddTuzeQ8yvNOHiqAxHRKXxHUOjRkCkY3TTYMV0iSffDq8GWuNsb/6f/lPz3VMUqM+Hs6jYLi",
|
||||
"2Xi6Mbby7mFa8DyRs7ySTUNFeHLZinTLdPqLQFgYd3rOIiJ0ENIF4XMq+5S56jUUSFcfhPZ/mHIcS+3a",
|
||||
"ANqj6t9Hx/HSMw8KcIwCpdLLXKClZCikImB3hBv7I7uPNSReDjq3y93iIWTnaIwo0giD9KMOWp6zgK3B",
|
||||
"IqrX5rLiw6A1yYTf6FzozhO1l/jXdBwEerd8qRN8qcFr2VjfVvf+hP++x3PykLtyISGIxNMpCbWToXN/",
|
||||
"hqlqM3+IX53kAk/g/nZl0L2t6xtgCORTiptnlt1clo8Coouk+KB8HzqXgKkJ5lEbU76q1RtzqtEgpxb9",
|
||||
"cdz7B+59HfRe3376q09D8vrEpGlYIZ8xjSThuUIh3lL/+TKefkibk1een1rNNmJTWu3TlM9WD20R5CcZ",
|
||||
"LxEnIeUkkCZD9wmbxlQydH56YRoqHcWTpBMmLAiNfS3fixJMT+COno2c57+3zOQgKVvdL9/aaG7vIL6L",
|
||||
"Aad5VXg0OHiV2/CZlAtxtLfX/4u3WNyzNKiVBoodAr0XRigoGonhNkbmmCpuX3Cmy+wV8nzC1pn9dA6W",
|
||||
"+vNTdeopBXOMg8+VxP47jsPIkPoHNcoI2T4oTLjVcg31mJIN6JIEhNpgGYtiXTslYCHRWoPLHOrMJ1+C",
|
||||
"GY6nRCBqLmfsM4lFBbecWMgbZN1x/exVIoWFrQVfiREyyXdz02sr/CC0DaVLAQScXF2+UUiVxPpr+2AV",
|
||||
"0le7vVJKjw5WBvbTqpLJIUKEJ2o5mWavaWVFSWXlavVo64us/7tKZv07SZ+cQCkyur18NgkUllSXM37L",
|
||||
"pllaEcQSnZZTIw5LK0cEgTuhSfLrPUv1PClMXvGgAHnAaarGB2MC4zlSTq0MD6PeyAL4TLNbSnyttzDN",
|
||||
"DtPqTITR+Z3/FLngLEz0E6Bu1Ol2Eh6BjVBrPSELvi77AZvv3Q2h1JGZplyZ19CqQJxE2KRTyDJ0G6Ge",
|
||||
"T9BdPi4qhrF1x92RirXI2w7mqt1mrFJez1XHSt0LIaWu9tm1+2LmyFvAV57AZEZU46eJA22CwGyS3H1p",
|
||||
"9UUUg5gc6P3uzW2n0HHPznj5gOe2w+TTrWWjuSTfdiyQ13Mc4ynJ0ruHcxpTIXlxfPX7qhPUJJsrTFve",
|
||||
"RzcHwbdP3/7/AAAA///pcQ5AzMEBAA==",
|
||||
"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",
|
||||
}
|
||||
|
||||
// GetSwagger returns the content of the embedded swagger specification 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user