Enregistrement et affichage d'un flux d'évènements
- Ajout d'une nouvelle entité "Event" - Affichage d'une "timeline" sur le tableau de bord - Création semi-automatique des évènements lors des modifications par les utilisateurs
This commit is contained in:
@ -2,7 +2,7 @@ package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
@ -12,6 +12,11 @@ import (
|
||||
)
|
||||
|
||||
func handleCreateDecisionSupportFile(ctx context.Context, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
authorized, err := isAuthorized(ctx, &model.DecisionSupportFile{}, model.ActionCreate)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
@ -21,9 +26,6 @@ func handleCreateDecisionSupportFile(ctx context.Context, changes *model.Decisio
|
||||
return nil, errs.WithStack(ErrForbidden)
|
||||
}
|
||||
|
||||
ctn := container.Must(ctx)
|
||||
db := orm.Must(ctn).DB()
|
||||
|
||||
repo := model.NewDSFRepository(db)
|
||||
|
||||
dsf, err := repo.Create(ctx, changes)
|
||||
@ -31,21 +33,29 @@ func handleCreateDecisionSupportFile(ctx context.Context, changes *model.Decisio
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeCreated, dsf); err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
return dsf, nil
|
||||
}
|
||||
|
||||
func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *model.DecisionSupportFileChanges) (*model.DecisionSupportFile, error) {
|
||||
ctn := container.Must(ctx)
|
||||
db := orm.Must(ctn).DB()
|
||||
|
||||
repo := model.NewDSFRepository(db)
|
||||
|
||||
dsf, err := repo.Find(ctx, id)
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
authorized, err := isAuthorized(ctx, dsf, model.ActionUpdate)
|
||||
repo := model.NewDSFRepository(db)
|
||||
|
||||
prevDsf, err := repo.Find(ctx, id)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
authorized, err := isAuthorized(ctx, prevDsf, model.ActionUpdate)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
@ -54,11 +64,31 @@ func handleUpdateDecisionSupportFile(ctx context.Context, id string, changes *mo
|
||||
return nil, errs.WithStack(ErrForbidden)
|
||||
}
|
||||
|
||||
dsf, err = repo.Update(ctx, id, changes)
|
||||
dsf, err := repo.Update(ctx, id, changes)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if changes != nil && changes.Status != nil && prevDsf.Status != *changes.Status {
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeStatusChanged, dsf); err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if changes != nil && changes.Title != nil && prevDsf.Title != *changes.Title {
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeTitleChanged, dsf); err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if changes != nil && !reflect.DeepEqual(prevDsf.Sections, dsf.Sections) {
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeUpdated, dsf); err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return dsf, nil
|
||||
}
|
||||
|
||||
@ -88,13 +118,3 @@ func handleDecisionSupportFiles(ctx context.Context, filter *model.DecisionSuppo
|
||||
|
||||
return dsfs, nil
|
||||
}
|
||||
|
||||
func handleSections(ctx context.Context, dsf *model.DecisionSupportFile) (map[string]interface{}, error) {
|
||||
sections := make(map[string]interface{})
|
||||
|
||||
if err := json.Unmarshal(dsf.Sections.RawMessage, §ions); err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
return sections, nil
|
||||
}
|
||||
|
24
internal/graph/event_handler.go
Normal file
24
internal/graph/event_handler.go
Normal file
@ -0,0 +1,24 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/model"
|
||||
"forge.cadoles.com/Cadoles/daddy/internal/orm"
|
||||
errs "github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func handleEvents(ctx context.Context, filter *model.EventFilter) ([]*model.Event, error) {
|
||||
ctn := container.Must(ctx)
|
||||
db := orm.Must(ctn).DB()
|
||||
|
||||
repo := model.NewEventRepository(db)
|
||||
|
||||
events, err := repo.Search(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
@ -18,6 +18,16 @@ type Workgroup {
|
||||
members: [User]!
|
||||
}
|
||||
|
||||
type Event {
|
||||
id: ID!
|
||||
type: String!
|
||||
createdAt: Time!
|
||||
updatedAt: Time!
|
||||
objectId: ID!
|
||||
objectType: String!
|
||||
user: User!
|
||||
}
|
||||
|
||||
input WorkgroupsFilter {
|
||||
ids: [ID]
|
||||
}
|
||||
@ -44,9 +54,19 @@ input AuthorizationObject {
|
||||
decisionSupportFileId: ID
|
||||
}
|
||||
|
||||
input EventFilter {
|
||||
objectType: String
|
||||
objectId: ID
|
||||
userId: ID
|
||||
type: String
|
||||
from: Time
|
||||
to: Time
|
||||
}
|
||||
|
||||
type Query {
|
||||
userProfile: User
|
||||
workgroups(filter: WorkgroupsFilter): [Workgroup]!
|
||||
decisionSupportFiles(filter: DecisionSupportFileFilter): [DecisionSupportFile]!
|
||||
events(filter: EventFilter): [Event]!
|
||||
isAuthorized(action: String!, object: AuthorizationObject!): Boolean!
|
||||
}
|
||||
|
@ -15,8 +15,16 @@ func (r *decisionSupportFileResolver) ID(ctx context.Context, obj *model1.Decisi
|
||||
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||
}
|
||||
|
||||
func (r *decisionSupportFileResolver) Sections(ctx context.Context, obj *model1.DecisionSupportFile) (map[string]interface{}, error) {
|
||||
return handleSections(ctx, obj)
|
||||
func (r *eventResolver) ID(ctx context.Context, obj *model1.Event) (string, error) {
|
||||
return strconv.FormatUint(uint64(obj.ID), 10), nil
|
||||
}
|
||||
|
||||
func (r *eventResolver) Type(ctx context.Context, obj *model1.Event) (string, error) {
|
||||
return string(obj.Type), nil
|
||||
}
|
||||
|
||||
func (r *eventResolver) ObjectID(ctx context.Context, obj *model1.Event) (string, error) {
|
||||
return strconv.FormatUint(uint64(obj.ObjectID), 10), nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) {
|
||||
@ -31,6 +39,10 @@ func (r *queryResolver) DecisionSupportFiles(ctx context.Context, filter *model1
|
||||
return handleDecisionSupportFiles(ctx, filter)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Events(ctx context.Context, filter *model1.EventFilter) ([]*model1.Event, error) {
|
||||
return handleEvents(ctx, filter)
|
||||
}
|
||||
|
||||
func (r *queryResolver) IsAuthorized(ctx context.Context, action string, object model1.AuthorizationObject) (bool, error) {
|
||||
return handleIsAuthorized(ctx, action, object)
|
||||
}
|
||||
@ -48,6 +60,9 @@ func (r *Resolver) DecisionSupportFile() generated.DecisionSupportFileResolver {
|
||||
return &decisionSupportFileResolver{r}
|
||||
}
|
||||
|
||||
// Event returns generated.EventResolver implementation.
|
||||
func (r *Resolver) Event() generated.EventResolver { return &eventResolver{r} }
|
||||
|
||||
// Query returns generated.QueryResolver implementation.
|
||||
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
@ -58,6 +73,17 @@ func (r *Resolver) User() generated.UserResolver { return &userResolver{r} }
|
||||
func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} }
|
||||
|
||||
type decisionSupportFileResolver struct{ *Resolver }
|
||||
type eventResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
type userResolver struct{ *Resolver }
|
||||
type workgroupResolver struct{ *Resolver }
|
||||
|
||||
// !!! WARNING !!!
|
||||
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
||||
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
||||
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
||||
// it when you're done.
|
||||
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||
func (r *decisionSupportFileResolver) Sections(ctx context.Context, obj *model1.DecisionSupportFile) (map[string]interface{}, error) {
|
||||
return obj.Sections, nil
|
||||
}
|
||||
|
@ -72,6 +72,12 @@ func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Wor
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeJoined, workgroup); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
@ -102,6 +108,12 @@ func handleLeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workg
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeLeaved, workgroup); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
@ -115,7 +127,7 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
|
||||
return nil, errs.WithStack(ErrForbidden)
|
||||
}
|
||||
|
||||
db, err := getDB(ctx)
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -127,11 +139,17 @@ func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges)
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeCreated, workgroup); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) {
|
||||
db, err := getDB(ctx)
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -157,11 +175,17 @@ func handleCloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workg
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeClosed, workgroup); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
||||
func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) {
|
||||
db, err := getDB(ctx)
|
||||
user, db, err := getSessionUser(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -187,5 +211,11 @@ func handleUpdateWorkgroup(ctx context.Context, workgroupID string, changes mode
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
eventRepo := model.NewEventRepository(db)
|
||||
|
||||
if _, err := eventRepo.Add(ctx, user, model.EventTypeUpdated, workgroup); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return workgroup, nil
|
||||
}
|
||||
|
@ -1,19 +1,55 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jinzhu/gorm/dialects/postgres"
|
||||
errs "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const ObjectTypeDecisionSupportFile = "dsf"
|
||||
|
||||
type DecisionSupportFile struct {
|
||||
gorm.Model
|
||||
Title string `json:"title"`
|
||||
Sections postgres.Jsonb `json:"sections"`
|
||||
Status string `json:"status"`
|
||||
WorkgroupID uint `json:"-"`
|
||||
Workgroup *Workgroup `json:"workgroup"`
|
||||
VotedAt time.Time `json:"votedAt"`
|
||||
ClosedAt time.Time `json:"closedAt"`
|
||||
Title string `json:"title"`
|
||||
SectionsJSON postgres.Jsonb `json:"-" gorm:"column:sections;"`
|
||||
Sections map[string]interface{} `gorm:"-"`
|
||||
Status string `json:"status"`
|
||||
WorkgroupID uint `json:"-"`
|
||||
Workgroup *Workgroup `json:"workgroup" gorm:"association_autoupdate:false"`
|
||||
VotedAt time.Time `json:"votedAt"`
|
||||
ClosedAt time.Time `json:"closedAt"`
|
||||
}
|
||||
|
||||
func (f *DecisionSupportFile) ObjectID() uint {
|
||||
return f.ID
|
||||
}
|
||||
|
||||
func (f *DecisionSupportFile) ObjectType() string {
|
||||
return ObjectTypeDecisionSupportFile
|
||||
}
|
||||
|
||||
func (f *DecisionSupportFile) BeforeSave() error {
|
||||
rawSections, err := json.Marshal(f.Sections)
|
||||
if err != nil {
|
||||
return errs.WithStack(err)
|
||||
}
|
||||
|
||||
f.SectionsJSON = postgres.Jsonb{RawMessage: rawSections}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *DecisionSupportFile) AfterFind() (err error) {
|
||||
sections := make(map[string]interface{})
|
||||
|
||||
if err := json.Unmarshal(f.SectionsJSON.RawMessage, §ions); err != nil {
|
||||
return errs.WithStack(err)
|
||||
}
|
||||
|
||||
f.Sections = sections
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,11 +2,9 @@ package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jinzhu/gorm/dialects/postgres"
|
||||
errs "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -69,12 +67,7 @@ func (r *DSFRepository) updateFromChanges(dsf *DecisionSupportFile, changes *Dec
|
||||
dsf.Workgroup = wg
|
||||
|
||||
if changes.Sections != nil {
|
||||
rawSections, err := json.Marshal(changes.Sections)
|
||||
if err != nil {
|
||||
return errs.WithStack(err)
|
||||
}
|
||||
|
||||
dsf.Sections = postgres.Jsonb{RawMessage: rawSections}
|
||||
dsf.Sections = changes.Sections
|
||||
}
|
||||
|
||||
if changes.Title != nil {
|
||||
|
31
internal/model/event.go
Normal file
31
internal/model/event.go
Normal file
@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeCreated EventType = "created"
|
||||
EventTypeUpdated EventType = "updated"
|
||||
EventTypeLeaved EventType = "leaved"
|
||||
EventTypeJoined EventType = "joined"
|
||||
EventTypeClosed EventType = "closed"
|
||||
EventTypeStatusChanged EventType = "status-changed"
|
||||
EventTypeTitleChanged EventType = "title-changed"
|
||||
)
|
||||
|
||||
type EventObject interface {
|
||||
ObjectID() uint
|
||||
ObjectType() string
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
gorm.Model
|
||||
UserID uint `json:"-"`
|
||||
User *User `json:"user" gorm:"association_autoupdate:false"`
|
||||
ObjectType string `json:"objectType"`
|
||||
ObjectID uint `json:"objectId"`
|
||||
Type EventType `json:"type"`
|
||||
}
|
73
internal/model/event_repository.go
Normal file
73
internal/model/event_repository.go
Normal file
@ -0,0 +1,73 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
errs "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type EventRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func (r *EventRepository) Add(ctx context.Context, user *User, eventType EventType, obj EventObject) (*Event, error) {
|
||||
evt := &Event{
|
||||
Type: eventType,
|
||||
User: user,
|
||||
ObjectID: obj.ObjectID(),
|
||||
ObjectType: obj.ObjectType(),
|
||||
}
|
||||
|
||||
if err := r.db.Save(&evt).Error; err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
return evt, nil
|
||||
}
|
||||
|
||||
func (r *EventRepository) Search(ctx context.Context, filter *EventFilter) ([]*Event, error) {
|
||||
query := r.db.Model(&Event{}).Preload("User")
|
||||
|
||||
if filter == nil {
|
||||
filter = &EventFilter{}
|
||||
}
|
||||
|
||||
if filter.ObjectID != nil {
|
||||
query = query.Where("object_id = ?", filter.ObjectID)
|
||||
}
|
||||
|
||||
if filter.ObjectType != nil {
|
||||
query = query.Where("object_type = ?", filter.ObjectType)
|
||||
}
|
||||
|
||||
if filter.UserID != nil {
|
||||
query = query.Where("user_id = ?", filter.UserID)
|
||||
}
|
||||
|
||||
if filter.Type != nil {
|
||||
query = query.Where("type = ?", filter.Type)
|
||||
}
|
||||
|
||||
if filter.From != nil {
|
||||
query = query.Where("created_at >= ?", filter.From)
|
||||
}
|
||||
|
||||
if filter.To != nil {
|
||||
query = query.Where("created_at <= ?", filter.To)
|
||||
}
|
||||
|
||||
query = query.Order("created_at DESC")
|
||||
|
||||
events := make([]*Event, 0)
|
||||
|
||||
if err := query.Find(&events).Error; err != nil {
|
||||
return nil, errs.WithStack(err)
|
||||
}
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func NewEventRepository(db *gorm.DB) *EventRepository {
|
||||
return &EventRepository{db}
|
||||
}
|
@ -11,7 +11,7 @@ type User struct {
|
||||
Name *string `json:"name"`
|
||||
Email string `json:"email" gorm:"unique;not null"`
|
||||
ConnectedAt time.Time `json:"connectedAt"`
|
||||
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"`
|
||||
Workgroups []*Workgroup `gorm:"many2many:users_workgroups;association_autoupdate:false"`
|
||||
}
|
||||
|
||||
type ProfileChanges struct {
|
||||
|
@ -6,11 +6,21 @@ import (
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
const ObjectTypeWorkgroup = "workgroup"
|
||||
|
||||
type Workgroup struct {
|
||||
gorm.Model
|
||||
Name *string `json:"name"`
|
||||
ClosedAt time.Time `json:"closedAt"`
|
||||
Members []*User `gorm:"many2many:users_workgroups;"`
|
||||
Members []*User `gorm:"many2many:users_workgroups;association_autoupdate:false"`
|
||||
}
|
||||
|
||||
func (w *Workgroup) ObjectID() uint {
|
||||
return w.ID
|
||||
}
|
||||
|
||||
func (w *Workgroup) ObjectType() string {
|
||||
return ObjectTypeWorkgroup
|
||||
}
|
||||
|
||||
type WorkgroupChanges struct {
|
||||
|
@ -106,29 +106,35 @@ func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, wo
|
||||
}
|
||||
|
||||
func (r *WorkgroupRepository) RemoveUserFromWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) {
|
||||
user := &User{}
|
||||
|
||||
err := r.db.First(user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
workgroup := &Workgroup{}
|
||||
workgroup.ID = workgroupID
|
||||
|
||||
err = r.db.Model(user).
|
||||
Association("Workgroups").
|
||||
Delete(workgroup).
|
||||
Error
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
user := &User{}
|
||||
err := tx.First(user, "id = ?", userID).Error
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not find user")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not add user to workgroup")
|
||||
}
|
||||
err = tx.Model(user).
|
||||
Association("Workgroups").
|
||||
Delete(workgroup).
|
||||
Error
|
||||
|
||||
err = r.db.Model(workgroup).
|
||||
Preload("Members").
|
||||
First(workgroup, "id = ?", workgroupID).
|
||||
Error
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not add user to workgroup")
|
||||
}
|
||||
|
||||
err = tx.Model(workgroup).
|
||||
Preload("Members").
|
||||
First(workgroup, "id = ?", workgroupID).
|
||||
Error
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
Reference in New Issue
Block a user