Complete User CRUD

This commit is contained in:
Deluan 2020-01-19 21:39:37 -05:00
parent 1c04a19910
commit 2ab0cecd48
11 changed files with 201 additions and 45 deletions

View File

@ -1,14 +1,14 @@
GO_VERSION=1.13
NODE_VERSION=12.14.1
.PHONY: run
run: check_go_env data
@reflex -d none -c reflex.conf
.PHONY: dev
dev: check_env data
@goreman -f Procfile.dev -b 4533 start
.PHONY: server
server: check_go_env data
@reflex -d none -c reflex.conf
.PHONY: watch
watch: check_go_env
ginkgo watch -notify ./...

View File

@ -32,7 +32,7 @@ the steps in the [Development Environment](#development-environment) section bel
```
$ export SONIC_MUSICFOLDER="/path/to/your/music/folder"
$ make run
$ make
```
The server should start listening for requests. The default configuration is:
@ -58,7 +58,7 @@ Some useful commands:
```bash
# Start local server (with hot reload)
$ make run
$ make
# Run all tests
$ make test

4
go.mod
View File

@ -8,14 +8,12 @@ require (
github.com/astaxie/beego v1.12.0
github.com/bradleyjkemp/cupaloy v2.3.0+incompatible
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a // indirect
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8
github.com/fatih/camelcase v0.0.0-20160318181535-f6a740d52f96 // indirect
github.com/fatih/structs v1.0.0 // indirect
github.com/go-chi/chi v4.0.3+incompatible
github.com/go-chi/cors v1.0.0
github.com/google/uuid v1.1.1 // indirect
github.com/google/uuid v1.1.1
github.com/google/wire v0.4.0
github.com/kennygrant/sanitize v0.0.0-20170120101633-6a0bfdde8629
github.com/koding/multiconfig v0.0.0-20170327155832-26b6dfd3a84a

4
go.sum
View File

@ -24,10 +24,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4 h1:VSoAcWJvj656TSyWbJ5KuGsi/J8dO5+iO9+5/7I8wao=
github.com/deluan/rest v0.0.0-20200114062534-0653ffe9eab4/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8=
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131 h1:siEGb+iB1Ea75U7BnkYVSqSRzE6QHlXCbqEXenxRmhQ=
github.com/dhowden/itl v0.0.0-20170329215456-9fbe21093131/go.mod h1:eVWQJVQ67aMvYhpkDwaH2Goy2vo6v8JCMfGXfQ9sPtw=
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a h1:7MucP9rMAsQRcRE1sGpvMZoTxFYZlDmfDvCH+z7H+90=
github.com/dhowden/plist v0.0.0-20141002110153-5db6e0d9931a/go.mod h1:sLjdR6uwx3L6/Py8F+QgAfeiuY87xuYGwCDqRFrvCzw=
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8 h1:nmFnmD8VZXkjDHIb1Gnfz50cgzUvGN72zLjPRXBW/hU=
github.com/dhowden/tag v0.0.0-20191122115059-7e5c04feccd8/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8=

View File

@ -7,8 +7,8 @@ type User struct {
Name string
Password string
IsAdmin bool
LastLoginAt time.Time
LastAccessAt time.Time
LastLoginAt *time.Time
LastAccessAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -1,12 +1,15 @@
package persistence
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/astaxie/beego/orm"
"github.com/cloudsonic/sonic-server/model"
"github.com/deluan/rest"
"github.com/google/uuid"
)
type resourceRepository struct {
@ -20,19 +23,43 @@ type resourceRepository struct {
func NewResource(o orm.Ormer, model interface{}, mappedModel interface{}) model.ResourceRepository {
r := &resourceRepository{model: model, mappedModel: mappedModel, ormer: o}
r.instanceType = reflect.TypeOf(mappedModel)
// Get type of mappedModel (which is a *struct)
rv := reflect.ValueOf(mappedModel)
for rv.Kind() == reflect.Ptr || rv.Kind() == reflect.Interface {
rv = rv.Elem()
}
r.instanceType = rv.Type()
r.sliceType = reflect.SliceOf(r.instanceType)
return r
}
func (r *resourceRepository) newQuery() orm.QuerySeter {
return r.ormer.QueryTable(r.mappedModel)
func (r *resourceRepository) EntityName() string {
return r.instanceType.Name()
}
func (r *resourceRepository) newQuery(options ...rest.QueryOptions) orm.QuerySeter {
qs := r.ormer.QueryTable(r.mappedModel)
if len(options) > 0 {
qs = r.addOptions(qs, options)
qs = r.addFilters(qs, r.buildFilters(qs, options))
}
return qs
}
func (r *resourceRepository) NewInstance() interface{} {
return reflect.New(r.instanceType).Interface()
}
func (r *resourceRepository) NewSlice() interface{} {
slice := reflect.MakeSlice(r.sliceType, 0, 0)
x := reflect.New(slice.Type())
x.Elem().Set(slice)
return x.Interface()
}
func (r *resourceRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
qs := r.newQuery()
qs = r.addOptions(qs, options)
//qs = r.addFilters(qs, r.buildFilters(qs, options), r.getRestriction())
qs := r.newQuery(options...)
dataSet := r.NewSlice()
_, err := qs.All(dataSet)
if err == orm.ErrNoRows {
@ -42,8 +69,7 @@ func (r *resourceRepository) ReadAll(options ...rest.QueryOptions) (interface{},
}
func (r *resourceRepository) Count(options ...rest.QueryOptions) (int64, error) {
qs := r.newQuery()
//qs = r.addFilters(qs, r.buildFilters(qs, options), r.getRestriction())
qs := r.newQuery(options...)
count, err := qs.Count()
if err == orm.ErrNoRows {
err = rest.ErrNotFound
@ -51,11 +77,44 @@ func (r *resourceRepository) Count(options ...rest.QueryOptions) (int64, error)
return count, err
}
func (r *resourceRepository) NewSlice() interface{} {
slice := reflect.MakeSlice(r.sliceType, 0, 0)
x := reflect.New(slice.Type())
x.Elem().Set(slice)
return x.Interface()
func (r *resourceRepository) Read(id string) (interface{}, error) {
qs := r.newQuery().Filter("id", id)
data := r.NewInstance()
err := qs.One(data)
if err == orm.ErrNoRows {
return data, rest.ErrNotFound
}
return data, err
}
func setUUID(p interface{}) {
f := reflect.ValueOf(p).Elem().FieldByName("ID")
if f.Kind() == reflect.String {
id, _ := uuid.NewRandom()
f.SetString(id.String())
}
}
func (r *resourceRepository) Save(p interface{}) (string, error) {
setUUID(p)
id, err := r.ormer.Insert(p)
if err != nil {
if err.Error() != "LastInsertId is not supported by this driver" {
return "", err
}
}
return strconv.FormatInt(id, 10), nil
}
func (r *resourceRepository) Update(p interface{}, cols ...string) error {
count, err := r.ormer.Update(p, cols...)
if err != nil {
return err
}
if count == 0 {
return rest.ErrNotFound
}
return err
}
func (r *resourceRepository) addOptions(qs orm.QuerySeter, options []rest.QueryOptions) orm.QuerySeter {
@ -87,3 +146,59 @@ func (r *resourceRepository) addOptions(qs orm.QuerySeter, options []rest.QueryO
}
return qs
}
func (r *resourceRepository) addFilters(qs orm.QuerySeter, conditions ...*orm.Condition) orm.QuerySeter {
var cond *orm.Condition
for _, c := range conditions {
if c != nil {
if cond == nil {
cond = c
} else {
cond = cond.AndCond(c)
}
}
}
if cond != nil {
return qs.SetCond(cond)
}
return qs
}
func unmarshalValue(val interface{}) string {
switch v := val.(type) {
case float64:
return strconv.FormatFloat(v, 'f', -1, 64)
case string:
return v
default:
return fmt.Sprintf("%v", val)
}
}
func (r *resourceRepository) buildFilters(qs orm.QuerySeter, options []rest.QueryOptions) *orm.Condition {
if len(options) == 0 {
return nil
}
cond := orm.NewCondition()
clauses := cond
for f, v := range options[0].Filters {
fn := strings.Replace(f, ".", "__", -1)
s := unmarshalValue(v)
if strings.HasSuffix(fn, "Id") || strings.HasSuffix(fn, "__id") {
clauses = IdFilter(clauses, fn, s)
} else {
clauses = StartsWithFilter(clauses, fn, s)
}
}
return clauses
}
func IdFilter(cond *orm.Condition, field, value string) *orm.Condition {
field = strings.TrimSuffix(field, "Id") + "__id"
return cond.And(field, value)
}
func StartsWithFilter(cond *orm.Condition, field, value string) *orm.Condition {
return cond.And(field+"__istartswith", value)
}

View File

@ -7,14 +7,14 @@ import (
)
type user struct {
ID string `json:"id" orm:"pk;column(id)"`
Name string `json:"name" orm:"index"`
Password string `json:"-"`
IsAdmin bool `json:"isAdmin"`
LastLoginAt time.Time `json:"lastLoginAt"`
LastAccessAt time.Time `json:"lastAccessAt"`
CreatedAt time.Time `json:"createdAt" orm:"auto_now_add;type(datetime)"`
UpdatedAt time.Time `json:"updatedAt" orm:"auto_now;type(datetime)"`
ID string `json:"id" orm:"pk;column(id)"`
Name string `json:"name" orm:"index"`
Password string `json:"-"`
IsAdmin bool `json:"isAdmin"`
LastLoginAt *time.Time `json:"lastLoginAt" orm:"null"`
LastAccessAt *time.Time `json:"lastAccessAt" orm:"null"`
CreatedAt time.Time `json:"createdAt" orm:"auto_now_add;type(datetime)"`
UpdatedAt time.Time `json:"updatedAt" orm:"auto_now;type(datetime)"`
}
var _ = model.User(user{})

21
ui/src/user/UserCreate.js Normal file
View File

@ -0,0 +1,21 @@
import React from 'react'
import {
BooleanInput,
Create,
TextInput,
PasswordInput,
required,
SimpleForm
} from 'react-admin'
const UserCreate = (props) => (
<Create {...props}>
<SimpleForm redirect="list">
<TextInput source="name" validate={[required()]} />
<PasswordInput source="password" validate={[required()]} />
<BooleanInput source="isAdmin" initialValue={false} />
</SimpleForm>
</Create>
)
export default UserCreate

26
ui/src/user/UserEdit.js Normal file
View File

@ -0,0 +1,26 @@
import React from 'react'
import {
TextInput,
BooleanInput,
DateField,
PasswordInput,
Edit,
required,
SimpleForm
} from 'react-admin'
const UserEdit = (props) => (
<Edit {...props}>
<SimpleForm>
<TextInput source="name" validate={[required()]} />
<PasswordInput source="password" validate={[required()]} />
<BooleanInput source="isAdmin" initialValue={false} />
<DateField source="lastLoginAt" />
<DateField source="lastAccessAt" />
<DateField source="updatedAt" />
<DateField source="createdAt" />
</SimpleForm>
</Edit>
)
export default UserEdit

View File

@ -2,8 +2,8 @@ import React from 'react'
import {
BooleanField,
Datagrid,
DateField,
Filter,
DateField,
List,
SearchInput,
SimpleList,
@ -13,7 +13,7 @@ import { useMediaQuery } from '@material-ui/core'
const UserFilter = (props) => (
<Filter {...props}>
<SearchInput source="q" alwaysOn />
<SearchInput source="name" alwaysOn />
</Filter>
)
@ -30,10 +30,10 @@ const UserList = (props) => {
{isXsmall ? (
<SimpleList
primaryText={(record) => record.name}
secondaryText={(record) => record.email}
tertiaryText={(record) => (record.isAdmin ? '[admin]' : '')}
/>
) : (
<Datagrid>
<Datagrid rowClick="edit">
<TextField source="name" />
<BooleanField source="isAdmin" />
<DateField source="lastLoginAt" locales="pt-BR" />

View File

@ -1,11 +1,11 @@
import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle'
import SupervisedUserCircleIcon from '@material-ui/icons/Group'
import UserList from './UserList'
// import UserEdit from './UserEdit'
// import UserCreate from './UserCreate'
import UserEdit from './UserEdit'
import UserCreate from './UserCreate'
export default {
list: UserList,
// edit: UserEdit,
// create: UserCreate,
edit: UserEdit,
create: UserCreate,
icon: SupervisedUserCircleIcon
}