3
0

Refactored settings

This commit is contained in:
Denis Arh
2019-04-03 15:08:49 +02:00
parent 8c04cf3c8d
commit b84ed43f27
25 changed files with 2112 additions and 851 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
{
"Title": "Settings",
"Interface": "Settings",
"Struct": [
{
"imports": [
"sqlxTypes github.com/jmoiron/sqlx/types"
]
}
],
"Parameters": null,
"Protocol": "",
"Authentication": [],
"Path": "/settings",
"APIs": [
{
"Name": "list",
"Method": "GET",
"Title": "List settings",
"Path": "/",
"Parameters": {
"get": [
{
"name": "prefix",
"title": "Key prefix",
"type": "string"
}
]
}
},
{
"Name": "update",
"Method": "PATCH",
"Title": "Update settings",
"Path": "/",
"Parameters": {
"post": [
{
"name": "values",
"required": true,
"title": "Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting",
"type": "sqlxTypes.JSONText"
}
]
}
},
{
"Name": "get",
"Method": "GET",
"Title": "Check JWT token",
"Path": "/{key}",
"Parameters": {
"get": [
{
"name": "ownerID",
"title": "Owner ID",
"type": "uint64"
}
],
"path": [
{
"name": "key",
"required": true,
"title": "Setting key",
"type": "string"
}
]
}
},
{
"Name": "set",
"Method": "PUT",
"Title": "Set a value for a key",
"Path": "/{key}",
"Parameters": {
"path": [
{
"name": "key",
"required": true,
"title": "Setting key",
"type": "string"
}
],
"post": [
{
"name": "ownerID",
"title": "Owner",
"type": "uint64"
},
{
"name": "value",
"required": true,
"title": "Setting value",
"type": "sqlxTypes.JSONText"
}
]
}
}
]
}

View File

@@ -50,6 +50,8 @@ function types {
./build/gen-type-set --types Organisation --output system/types/organisation.gen.go
./build/gen-type-set --types Credentials --output system/types/credentials.gen.go
./build/gen-type-set --types Value --output internal/settings/types.gen.go --with-primary-key=false --package settings
green "OK"
}

16
crm/db/mysql/statik.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -515,6 +515,79 @@ An organisation may have many roles. Roles may have many channels available. Acc
# Settings
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/settings/` | List settings |
| `PATCH` | `/settings/` | Update settings |
| `GET` | `/settings/{key}` | Check JWT token |
| `PUT` | `/settings/{key}` | Set a value for a key |
## List settings
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/settings/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| prefix | string | GET | Key prefix | N/A | NO |
## Update settings
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/settings/` | HTTP/S | PATCH | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| values | sqlxTypes.JSONText | POST | Array of new settings: `[{ name: ..., value: ... }]`. Omit value to remove setting | N/A | YES |
## Check JWT token
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/settings/{key}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| ownerID | uint64 | GET | Owner ID | N/A | NO |
| key | string | PATH | Setting key | N/A | YES |
## Set a value for a key
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/settings/{key}` | HTTP/S | PUT | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| key | string | PATH | Setting key | N/A | YES |
| ownerID | uint64 | POST | Owner | N/A | NO |
| value | sqlxTypes.JSONText | POST | Setting value | N/A | YES |
---
# Users
| Method | Endpoint | Purpose |

View File

@@ -0,0 +1,132 @@
package settings
import (
"context"
"fmt"
"time"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"gopkg.in/Masterminds/squirrel.v1"
)
type (
repository struct {
dbh *factory.DB
// sql table reference
dbTable string
}
Repository interface {
With(ctx context.Context) Repository
Find(filter Filter) (ss ValueSet, err error)
Get(name string, ownedBy uint64) (value *Value, err error)
Set(value *Value) error
BulkSet(vv ValueSet) error
Delete(name string, ownedBy uint64) (err error)
}
)
func NewRepository(db *factory.DB, table string) Repository {
return &repository{
dbTable: table,
dbh: db,
}
}
func (r *repository) db() *factory.DB {
return r.dbh
}
func (r repository) columns() []string {
return []string{
"name",
"value",
"rel_owner",
"updated_at",
"updated_by",
}
}
func (r *repository) With(ctx context.Context) Repository {
return &repository{
dbTable: r.dbTable,
dbh: r.db().With(ctx),
}
}
func (r *repository) Find(f Filter) (ss ValueSet, err error) {
f.Normalize()
lookup := squirrel.
Select(r.columns()...).
From(r.dbTable).
// Always filter by owner
Where("rel_owner = ?", f.OwnedBy)
if len(f.Prefix) > 0 {
lookup = lookup.Where("name LIKE ?", f.Prefix+"%")
}
if f.Page > 0 {
lookup = lookup.Offset(f.PerPage * f.Page)
}
if f.PerPage > 0 {
lookup = lookup.Limit(f.PerPage)
}
if query, args, err := lookup.ToSql(); err != nil {
return nil, errors.Wrap(err, "could not build lookup query for settings")
} else if err = r.db().Select(&ss, query, args...); err != nil {
return nil, errors.Wrap(err, "could not find settings")
} else {
return ss, nil
}
}
func (r repository) BulkSet(vv ValueSet) error {
// Save all inside a db transaction
return r.db().Transaction(func() (err error) {
return vv.Walk(func(v *Value) error {
return r.Set(v)
})
})
}
func (r *repository) Set(value *Value) error {
value.UpdatedAt = time.Now()
return r.db().Replace(r.dbTable, value)
}
func (r *repository) Delete(name string, ownedBy uint64) error {
_, err := r.db().Exec(
fmt.Sprintf("DELETE FROM %s WHERE name = ? AND rel_owner = ? ", r.dbTable),
name,
ownedBy,
)
return err
}
func (r *repository) Get(name string, ownedBy uint64) (value *Value, err error) {
lookup := squirrel.
Select(r.columns()...).
From(r.dbTable).
Where("rel_owner = ?", ownedBy).
Where("name = ?", name)
value = &Value{}
if query, args, err := lookup.ToSql(); err != nil {
return nil, errors.Wrap(err, "could not build lookup query for settings")
} else if err = r.db().Get(value, query, args...); err != nil {
return nil, errors.Wrap(err, "could not get settings")
} else if value.Name == "" {
return nil, nil
} else {
return value, nil
}
}

View File

@@ -0,0 +1,83 @@
package settings
import (
"context"
)
type (
service struct {
repository Repository
}
Service interface {
With(ctx context.Context) Service
FindByPrefix(prefix string) (vv ValueSet, err error)
BulkSet(vv ValueSet) (err error)
Set(v *Value) (err error)
Get(name string, ownedBy uint64) (out *Value, err error)
Delete(name string, ownedBy uint64) error
GetGlobalString(name string) (out string, err error)
GetGlobalBool(name string) (out bool, err error)
}
)
func NewService(r Repository) Service {
svc := &service{
repository: r,
}
return svc
}
func (s service) With(ctx context.Context) Service {
return &service{
repository: s.repository.With(ctx),
}
}
func (s service) FindByPrefix(prefix string) (ValueSet, error) {
if vv, err := s.repository.Find(Filter{Prefix: prefix}); err != nil {
return nil, err
} else {
return vv, nil
}
}
func (s service) Set(v *Value) (err error) {
return s.repository.Set(v)
}
func (s service) BulkSet(vv ValueSet) (err error) {
return s.repository.BulkSet(vv)
}
func (s service) Get(name string, ownedBy uint64) (out *Value, err error) {
return s.repository.Get(name, ownedBy)
}
func (s service) Delete(name string, ownedBy uint64) error {
return s.repository.Delete(name, ownedBy)
}
func (s service) GetGlobalString(name string) (out string, err error) {
const global = 0
var v *Value
if v, err = s.repository.Get(name, global); err == nil {
err = v.Value.Unmarshal(&out)
}
return
}
func (s service) GetGlobalBool(name string) (out bool, err error) {
const global = 0
var v *Value
if v, err = s.repository.Get(name, global); err == nil {
err = v.Value.Unmarshal(&out)
}
return
}

View File

@@ -0,0 +1,41 @@
package settings
// Hello! This file is auto-generated.
type (
// ValueSet slice of Value
//
// This type is auto-generated.
ValueSet []*Value
)
// Walk iterates through every slice item and calls w(Value) err
//
// This function is auto-generated.
func (set ValueSet) Walk(w func(*Value) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Value) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set ValueSet) Filter(f func(*Value) (bool, error)) (out ValueSet, err error) {
var ok bool
out = ValueSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}

104
internal/settings/types.go Normal file
View File

@@ -0,0 +1,104 @@
package settings
import (
"encoding/json"
"strings"
"time"
"github.com/jmoiron/sqlx/types"
)
type (
Value struct {
Name string `json:"name" db:"name"`
Value types.JSONText `json:"value" db:"value"`
// Setting owner, 0 for global settings
OwnedBy uint64 `json:"-" db:"rel_owner"`
// Who updated & when
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
UpdatedBy uint64 `json:"updatedBy" db:"updated_by"`
}
Filter struct {
Prefix string `json:"prefix"`
OwnedBy uint64 `json:"ownedBy"`
Page uint64 `json:"page"`
PerPage uint64 `json:"perPage"`
}
KV map[string]types.JSONText
)
const (
settingsFilterPerPageMax = 100
)
func (f *Filter) Normalize() {
f.Prefix = strings.TrimSpace(f.Prefix)
if f.PerPage > settingsFilterPerPageMax {
f.PerPage = settingsFilterPerPageMax
}
}
func (v *Value) SetValueAsString(str string) error {
var dummy interface{}
// Test input to be sure we can save it...
if err := json.Unmarshal([]byte(str), &dummy); err != nil {
return err
}
v.Value = types.JSONText(str)
return nil
}
func (v *Value) SetValue(value interface{}) (err error) {
v.Value, err = json.Marshal(value)
return
}
func (ss ValueSet) KV() KV {
m := KV{}
_ = ss.Walk(func(v *Value) error {
m[v.Name] = v.Value
return nil
})
return m
}
func (kv KV) Has(k string) (ok bool) {
_, ok = kv[k]
return
}
func (kv KV) Bool(k string) (out bool) {
out = false
if v, ok := kv[k]; ok {
v.Unmarshal(&out)
}
return
}
func (kv KV) String(k string) (out string) {
out = ""
if v, ok := kv[k]; ok {
v.Unmarshal(&out)
}
return
}
func (kv KV) Filter(prefix string) KV {
var out = KV{}
for k, v := range kv {
if strings.Index(k, prefix) == 0 {
out[k] = v
}
}
return out
}

View File

@@ -0,0 +1,62 @@
package settings
import (
"testing"
"github.com/jmoiron/sqlx/types"
"github.com/crusttech/crust/internal/test"
)
func TestKV_Bool(t *testing.T) {
type args struct {
k string
}
tests := []struct {
name string
kv KV
args args
wantV bool
}{
{
name: "True value should return true",
kv: KV{"true-value": types.JSONText(`true`)},
args: args{k: "true-value"},
wantV: true,
},
{
name: "Null value should return false",
kv: KV{"null-value": types.JSONText(`null`)},
args: args{k: "null-value"},
wantV: false,
},
{
name: "Unexisting value should return false",
kv: KV{},
args: args{k: "unexisting"},
wantV: false,
},
{
name: "Invalid KV should return false",
kv: nil,
args: args{k: "invalid-kv"},
wantV: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if gotV := tt.kv.Bool(tt.args.k); gotV != tt.wantV {
t.Errorf("KV.Bool() = %v, want %v", gotV, tt.wantV)
}
})
}
}
func TestSettingValueAsString(t *testing.T) {
test.NoError(t, (&Value{}).SetValueAsString(`"string"`), "unable to set value as string")
test.NoError(t, (&Value{}).SetValueAsString(`false`), "unable to set value as string")
test.NoError(t, (&Value{}).SetValueAsString(`null`), "unable to set value as string")
test.NoError(t, (&Value{}).SetValueAsString(`42`), "unable to set value as string")
test.NoError(t, (&Value{}).SetValueAsString(`3.14`), "unable to set value as string")
test.Error(t, (&Value{}).SetValueAsString(`error`), "expecting error when not setting JSON")
}

File diff suppressed because one or more lines are too long

88
system/cli/settings.go Normal file
View File

@@ -0,0 +1,88 @@
package cli
import (
"os"
"github.com/spf13/cobra"
"github.com/crusttech/crust/internal/settings"
)
func Settings(rootCmd *cobra.Command, service settings.Service) {
exit := func(err error) {
if err != nil {
rootCmd.Printf("Error: %v\n", err)
os.Exit(1)
} else {
os.Exit(0)
}
}
settingsCmd := &cobra.Command{
Use: "settings",
Short: "Settings management",
}
list := &cobra.Command{
Use: "list",
Short: "List all",
Run: func(cmd *cobra.Command, args []string) {
prefix := cmd.Flags().Lookup("prefix").Value.String()
if kv, err := service.FindByPrefix(prefix); err != nil {
exit(err)
} else {
for _, v := range kv {
cmd.Printf("%s\t%v\n", v.Name, v.Value)
}
}
},
}
list.Flags().String("prefix", "", "Filter settings by prefix")
get := &cobra.Command{
Use: "get [key to get]",
Short: "Get value (raw JSON) for a specific key",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if v, err := service.Get(args[0], 0); err != nil {
exit(err)
} else if v != nil {
cmd.Printf("%v\n", v.Value)
}
exit(nil)
},
}
set := &cobra.Command{
Use: "set [key to set] [value",
Short: "Set value (raw JSON) for a specific key",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
value := args[1]
v := &settings.Value{
Name: args[0],
}
if err := v.SetValueAsString(value); err != nil {
exit(err)
}
exit(service.Set(v))
},
}
del := &cobra.Command{
Use: "delete [key to remove]",
Short: "Set value (raw JSON) for a specific key",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
exit(service.Delete(args[0], 0))
},
}
settingsCmd.AddCommand(list, get, set, del)
rootCmd.AddCommand(settingsCmd)
}

16
system/db/mysql/statik.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
DROP TABLE `settings`;
CREATE TABLE `sys_settings` (
rel_owner BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Value owner, 0 for global settings',
name VARCHAR(200) NOT NULL COMMENT 'Unique set of setting keys',
value JSON COMMENT 'Setting value',
updated_at DATETIME NOT NULL DEFAULT NOW() COMMENT 'When was the value updated',
updated_by BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who created/updated the value',
PRIMARY KEY (name, rel_owner)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@@ -188,6 +188,8 @@ func (r *role) Reset() error {
(2, 'compose:page:*', 'delete', 2),
(2, 'system', 'access', 2),
(2, 'system', 'grant', 2),
(2, 'system', 'settings.read', 2),
(2, 'system', 'settings.manage', 2),
(2, 'system', 'organisation.create', 2),
(2, 'system', 'user.create', 2),
(2, 'system', 'role.create', 2),

View File

@@ -1,63 +0,0 @@
package repository
import (
"context"
"encoding/json"
"github.com/pkg/errors"
"github.com/titpetric/factory"
)
type (
settings struct {
*repository
// sql table reference
settings string
}
Settings interface {
With(ctx context.Context, db *factory.DB) Settings
Get(name string, value interface{}) (bool, error)
Set(name string, value interface{}) error
}
)
func NewSettings(ctx context.Context, db *factory.DB) Settings {
return (&settings{}).With(ctx, db)
}
func (r *settings) With(ctx context.Context, db *factory.DB) Settings {
return &settings{
repository: r.repository.With(ctx, db),
settings: "settings",
}
}
func (r *settings) Set(name string, value interface{}) error {
if jsonValue, err := json.Marshal(value); err != nil {
return errors.Wrap(err, "Error marshaling settings value")
} else {
return r.db().Replace(r.settings, struct {
Key string `db:"name"`
Val json.RawMessage `db:"value"`
}{name, jsonValue})
}
}
func (r *settings) Get(name string, value interface{}) (bool, error) {
sql := "SELECT value FROM " + r.settings + " WHERE name = ?"
var stored json.RawMessage
if err := r.db().Get(&stored, sql, name); err != nil {
return false, errors.Wrap(err, "Error reading settings from the database")
} else if stored == nil {
return false, nil
} else if err := json.Unmarshal(stored, value); err != nil {
return false, errors.Wrap(err, "Error unmarshaling settings value")
}
return true, nil
}

View File

@@ -25,6 +25,9 @@ type (
Effective() (ee []effectivePermission, err error)
CanReadSettings() bool
CanManageSettings() bool
CanCreateOrganisation() bool
CanCreateUser() bool
CanCreateRole() bool
@@ -76,6 +79,8 @@ func (p *permissions) Effective() (ee []effectivePermission, err error) {
}
ee = append(ee, ep("system", "access", p.CanAccess()))
ee = append(ee, ep("system", "settings.read", p.CanReadSettings()))
ee = append(ee, ep("system", "settings.manage", p.CanManageSettings()))
ee = append(ee, ep("system", "application.create", p.CanCreateApplication()))
ee = append(ee, ep("system", "role.create", p.CanCreateRole()))
ee = append(ee, ep("system", "organisation.create", p.CanCreateOrganisation()))
@@ -88,6 +93,14 @@ func (p *permissions) CanAccess() bool {
return p.checkAccess(types.PermissionResource, "access")
}
func (p *permissions) CanReadSettings() bool {
return p.checkAccess(types.PermissionResource, "settings.read")
}
func (p *permissions) CanManageSettings() bool {
return p.checkAccess(types.PermissionResource, "settings.manage")
}
func (p *permissions) CanCreateOrganisation() bool {
return p.checkAccess(types.PermissionResource, "organisation.create")
}

View File

@@ -11,6 +11,7 @@ type (
)
var (
DefaultSettings SettingsService
DefaultAuth AuthService
DefaultUser UserService
DefaultRole RoleService
@@ -22,6 +23,7 @@ var (
func Init() error {
ctx := context.Background()
DefaultSettings = Settings(ctx)
DefaultRules = Rules(ctx)
DefaultPermissions = Permissions(ctx)
DefaultAuth = Auth(ctx)

View File

@@ -0,0 +1,73 @@
package service
import (
"context"
"errors"
internalSettings "github.com/crusttech/crust/internal/settings"
"github.com/crusttech/crust/system/internal/repository"
)
type (
// Wrapper service for system around internal settings service
settings struct {
db db
ctx context.Context
prm PermissionsService
internalSettings internalSettings.Service
}
SettingsService interface {
With(ctx context.Context) SettingsService
FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error)
Set(v *internalSettings.Value) (err error)
BulkSet(vv internalSettings.ValueSet) (err error)
Get(name string, ownedBy uint64) (out *internalSettings.Value, err error)
}
)
func Settings(ctx context.Context) SettingsService {
return (&settings{}).With(ctx)
}
func (svc settings) With(ctx context.Context) SettingsService {
db := repository.DB(ctx)
return &settings{
ctx: ctx,
prm: Permissions(ctx),
internalSettings: internalSettings.NewService(internalSettings.NewRepository(db, "sys_settings")).With(ctx),
}
}
func (svc settings) FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) {
if !svc.prm.CanReadSettings() {
return nil, errors.New("not allowed to read settings")
}
return svc.internalSettings.FindByPrefix(prefix)
}
func (svc settings) Set(v *internalSettings.Value) (err error) {
if !svc.prm.CanManageSettings() {
return errors.New("not allowed to manage settings")
}
return svc.internalSettings.Set(v)
}
func (svc settings) BulkSet(vv internalSettings.ValueSet) (err error) {
if !svc.prm.CanManageSettings() {
return errors.New("not allowed to manage settings")
}
return svc.internalSettings.BulkSet(vv)
}
func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) {
if !svc.prm.CanReadSettings() {
return nil, errors.New("not allowed to read settings")
}
return svc.internalSettings.Get(name, ownedBy)
}

View File

@@ -11,6 +11,8 @@ var (
"system": map[string]bool{
"access": true,
"grant": true,
"settings.read": true,
"settings.manage": true,
"organisation.create": true,
"role.create": true,
"application.create": true,

View File

@@ -0,0 +1,86 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `settings.go`, `settings.util.go` or `settings_test.go` to
implement your API calls, helper functions and tests. The file `settings.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/system/rest/request"
)
// Internal API interface
type SettingsAPI interface {
List(context.Context, *request.SettingsList) (interface{}, error)
Update(context.Context, *request.SettingsUpdate) (interface{}, error)
Get(context.Context, *request.SettingsGet) (interface{}, error)
Set(context.Context, *request.SettingsSet) (interface{}, error)
}
// HTTP API interface
type Settings struct {
List func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Get func(http.ResponseWriter, *http.Request)
Set func(http.ResponseWriter, *http.Request)
}
func NewSettings(sh SettingsAPI) *Settings {
return &Settings{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return sh.List(r.Context(), params)
})
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsUpdate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return sh.Update(r.Context(), params)
})
},
Get: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsGet()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return sh.Get(r.Context(), params)
})
},
Set: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewSettingsSet()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return sh.Set(r.Context(), params)
})
},
}
}
func (sh *Settings) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/settings/", sh.List)
r.Patch("/settings/", sh.Update)
r.Get("/settings/{key}", sh.Get)
r.Put("/settings/{key}", sh.Set)
})
}

View File

@@ -0,0 +1,230 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `settings.go`, `settings.util.go` or `settings_test.go` to
implement your API calls, helper functions and tests. The file `settings.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"io"
"strings"
"encoding/json"
"mime/multipart"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
sqlxTypes "github.com/jmoiron/sqlx/types"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Settings list request parameters
type SettingsList struct {
Prefix string
}
func NewSettingsList() *SettingsList {
return &SettingsList{}
}
func (seReq *SettingsList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(seReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["prefix"]; ok {
seReq.Prefix = val
}
return err
}
var _ RequestFiller = NewSettingsList()
// Settings update request parameters
type SettingsUpdate struct {
Values sqlxTypes.JSONText
}
func NewSettingsUpdate() *SettingsUpdate {
return &SettingsUpdate{}
}
func (seReq *SettingsUpdate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(seReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["values"]; ok {
if seReq.Values, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
return err
}
var _ RequestFiller = NewSettingsUpdate()
// Settings get request parameters
type SettingsGet struct {
OwnerID uint64 `json:",string"`
Key string
}
func NewSettingsGet() *SettingsGet {
return &SettingsGet{}
}
func (seReq *SettingsGet) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(seReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["ownerID"]; ok {
seReq.OwnerID = parseUInt64(val)
}
seReq.Key = chi.URLParam(r, "key")
return err
}
var _ RequestFiller = NewSettingsGet()
// Settings set request parameters
type SettingsSet struct {
Key string
OwnerID uint64 `json:",string"`
Value sqlxTypes.JSONText
}
func NewSettingsSet() *SettingsSet {
return &SettingsSet{}
}
func (seReq *SettingsSet) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(seReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
seReq.Key = chi.URLParam(r, "key")
if val, ok := post["ownerID"]; ok {
seReq.OwnerID = parseUInt64(val)
}
if val, ok := post["value"]; ok {
if seReq.Value, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
return err
}
var _ RequestFiller = NewSettingsSet()

View File

@@ -51,6 +51,7 @@ func MountRoutes(oidcConfig *config.OIDC, socialConfig *config.Social, jwtEncode
handlers.NewOrganisation(Organisation{}.New()).MountRoutes(r)
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewApplication(Application{}.New()).MountRoutes(r)
handlers.NewSettings(Settings{}.New()).MountRoutes(r)
})
}
}

60
system/rest/settings.go Normal file
View File

@@ -0,0 +1,60 @@
package rest
import (
"context"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/settings"
"github.com/crusttech/crust/system/internal/service"
"github.com/crusttech/crust/system/rest/request"
)
var _ = errors.Wrap
type (
Settings struct {
svc struct {
settings service.SettingsService
}
}
)
func (Settings) New() *Settings {
ctrl := &Settings{}
ctrl.svc.settings = service.DefaultSettings
return ctrl
}
func (ctrl *Settings) List(ctx context.Context, r *request.SettingsList) (interface{}, error) {
if vv, err := ctrl.svc.settings.With(ctx).FindByPrefix(r.Prefix); err != nil {
return nil, err
} else {
return vv, err
}
}
func (ctrl *Settings) Update(ctx context.Context, r *request.SettingsUpdate) (interface{}, error) {
values := settings.ValueSet{}
if err := r.Values.Unmarshal(&values); err != nil {
return nil, err
} else if err := ctrl.svc.settings.With(ctx).BulkSet(values); err != nil {
return nil, err
} else {
return true, nil
}
}
func (ctrl *Settings) Get(ctx context.Context, r *request.SettingsGet) (interface{}, error) {
if v, err := ctrl.svc.settings.With(ctx).Get(r.Key, r.OwnerID); err != nil {
return nil, err
} else {
return v, nil
}
}
func (ctrl *Settings) Set(ctx context.Context, r *request.SettingsSet) (interface{}, error) {
return nil, errors.New("Not implemented: Settings.set")
}

View File

@@ -14,7 +14,9 @@ import (
"github.com/crusttech/crust/internal/db"
"github.com/crusttech/crust/internal/mail"
"github.com/crusttech/crust/internal/metrics"
"github.com/crusttech/crust/internal/settings"
migrate "github.com/crusttech/crust/system/db"
"github.com/crusttech/crust/system/internal/repository"
"github.com/crusttech/crust/system/service"
)
@@ -44,6 +46,17 @@ func Init(ctx context.Context) error {
return err
}
// Load settings from the database,
// for now, only at start-up time.
ctx := context.Background()
// settingsRepository := internalRepository.NewSettings(repository.DB(ctx), "sys_settings")
// if ss, err := settingsRepository.Find(types.SettingsFilter{}); err != nil {
// panic(err)
// } else {
// spew.Dump(ss.KV())
// }
settingService := settings.NewService(settings.NewRepository(repository.DB(ctx), "sys_settings"))
// configure resputil options
resputil.SetConfig(resputil.Options{
Pretty: flags.http.Pretty,