diff --git a/cmd/server/migration.go b/cmd/server/migration.go index 083ee3e..f4fc39a 100644 --- a/cmd/server/migration.go +++ b/cmd/server/migration.go @@ -79,6 +79,7 @@ func applyMigration(ctx context.Context, ctn *service.Container) error { // nolint: gochecknoglobals var initialModels = []interface{}{ &model.User{}, + &model.Workgroup{}, } func m000initialSchema() orm.Migration { diff --git a/internal/graph/helper.go b/internal/graph/helper.go index 73ec676..3351cc7 100644 --- a/internal/graph/helper.go +++ b/internal/graph/helper.go @@ -3,7 +3,9 @@ package graph import ( "context" + "forge.cadoles.com/Cadoles/daddy/internal/model" "forge.cadoles.com/Cadoles/daddy/internal/orm" + "forge.cadoles.com/Cadoles/daddy/internal/session" "github.com/jinzhu/gorm" "github.com/pkg/errors" @@ -23,3 +25,24 @@ func getDB(ctx context.Context) (*gorm.DB, error) { return orm.DB(), nil } + +func getSessionUser(ctx context.Context) (*model.User, *gorm.DB, error) { + db, err := getDB(ctx) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + userEmail, err := session.UserEmail(ctx) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + repo := model.NewUserRepository(db) + + user, err := repo.FindUserByEmail(ctx, userEmail) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + return user, db, nil +} diff --git a/internal/graph/mutation.graphql b/internal/graph/mutation.graphql index 3f0f71a..7dcc5da 100644 --- a/internal/graph/mutation.graphql +++ b/internal/graph/mutation.graphql @@ -2,6 +2,15 @@ input ProfileChanges { name: String } +input WorkgroupChanges { + name: String +} + type Mutation { updateProfile(changes: ProfileChanges!): User! + joinWorkgroup(workgroupId: ID!): Workgroup! + leaveWorkgroup(workgroupId: ID!): Workgroup! + createWorkgroup(changes: WorkgroupChanges!): Workgroup! + closeWorkgroup(workgroupId: ID!): Workgroup! + updateWorkgroup(workgroupId: ID!, changes: WorkgroupChanges!): Workgroup! } \ No newline at end of file diff --git a/internal/graph/mutation.resolvers.go b/internal/graph/mutation.resolvers.go index c979e19..7f8872c 100644 --- a/internal/graph/mutation.resolvers.go +++ b/internal/graph/mutation.resolvers.go @@ -14,6 +14,26 @@ func (r *mutationResolver) UpdateProfile(ctx context.Context, changes model.Prof return handleUpdateUserProfile(ctx, changes) } +func (r *mutationResolver) JoinWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { + return handleJoinWorkgroup(ctx, workgroupID) +} + +func (r *mutationResolver) LeaveWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { + return handleLeaveWorkgroup(ctx, workgroupID) +} + +func (r *mutationResolver) CreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) { + return handleCreateWorkgroup(ctx, changes) +} + +func (r *mutationResolver) CloseWorkgroup(ctx context.Context, workgroupID string) (*model.Workgroup, error) { + return handleCloseWorkgroup(ctx, workgroupID) +} + +func (r *mutationResolver) UpdateWorkgroup(ctx context.Context, workgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) { + return handleUpdateWorkgroup(ctx, workgroupID, changes) +} + // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } diff --git a/internal/graph/query.graphql b/internal/graph/query.graphql index b11434c..e4921eb 100644 --- a/internal/graph/query.graphql +++ b/internal/graph/query.graphql @@ -1,12 +1,23 @@ scalar Time type User { + id: ID! name: String email: String! connectedAt: Time! createdAt: Time! + workgroups:[Workgroup]! +} + +type Workgroup { + id: ID! + name: String + createdAt: Time! + closedAt: Time + members: [User]! } type Query { userProfile: User + workgroups: [Workgroup]! } diff --git a/internal/graph/query.resolvers.go b/internal/graph/query.resolvers.go index 33af4b3..3a394aa 100644 --- a/internal/graph/query.resolvers.go +++ b/internal/graph/query.resolvers.go @@ -5,6 +5,7 @@ package graph import ( "context" + "strconv" "forge.cadoles.com/Cadoles/daddy/internal/graph/generated" model1 "forge.cadoles.com/Cadoles/daddy/internal/model" @@ -14,7 +15,37 @@ func (r *queryResolver) UserProfile(ctx context.Context) (*model1.User, error) { return handleUserProfile(ctx) } +func (r *queryResolver) Workgroups(ctx context.Context) ([]*model1.Workgroup, error) { + return handleWorkgroups(ctx) +} + +func (r *userResolver) ID(ctx context.Context, obj *model1.User) (string, error) { + return strconv.FormatUint(uint64(obj.ID), 10), nil +} + +func (r *workgroupResolver) ID(ctx context.Context, obj *model1.Workgroup) (string, error) { + return strconv.FormatUint(uint64(obj.ID), 10), nil +} + // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } +// User returns generated.UserResolver implementation. +func (r *Resolver) User() generated.UserResolver { return &userResolver{r} } + +// Workgroup returns generated.WorkgroupResolver implementation. +func (r *Resolver) Workgroup() generated.WorkgroupResolver { return &workgroupResolver{r} } + 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 *workgroupResolver) Users(ctx context.Context, obj *model1.Workgroup) ([]*model1.User, error) { + return obj.Members, nil +} diff --git a/internal/graph/user_profile.go b/internal/graph/user_profile_handler.go similarity index 55% rename from internal/graph/user_profile.go rename to internal/graph/user_profile_handler.go index 8298352..62564ea 100644 --- a/internal/graph/user_profile.go +++ b/internal/graph/user_profile_handler.go @@ -3,26 +3,12 @@ package graph import ( "context" - "forge.cadoles.com/Cadoles/daddy/internal/session" - "forge.cadoles.com/Cadoles/daddy/internal/model" "github.com/pkg/errors" ) func handleUserProfile(ctx context.Context) (*model.User, error) { - db, err := getDB(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - userEmail, err := session.UserEmail(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - repo := model.NewUserRepository(db) - - user, err := repo.FindUserByEmail(ctx, userEmail) + user, _, err := getSessionUser(ctx) if err != nil { return nil, errors.WithStack(err) } @@ -31,12 +17,7 @@ func handleUserProfile(ctx context.Context) (*model.User, error) { } func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) (*model.User, error) { - db, err := getDB(ctx) - if err != nil { - return nil, errors.WithStack(err) - } - - userEmail, err := session.UserEmail(ctx) + user, db, err := getSessionUser(ctx) if err != nil { return nil, errors.WithStack(err) } @@ -49,7 +30,7 @@ func handleUpdateUserProfile(ctx context.Context, changes model.ProfileChanges) userChanges.Name = changes.Name } - user, err := repo.UpdateUserByEmail(ctx, userEmail, userChanges) + user, err = repo.UpdateUserByEmail(ctx, user.Email, userChanges) if err != nil { return nil, errors.WithStack(err) } diff --git a/internal/graph/workgroup_handler.go b/internal/graph/workgroup_handler.go new file mode 100644 index 0000000..286d016 --- /dev/null +++ b/internal/graph/workgroup_handler.go @@ -0,0 +1,134 @@ +package graph + +import ( + "context" + "strconv" + + "forge.cadoles.com/Cadoles/daddy/internal/model" + "github.com/pkg/errors" +) + +func handleWorkgroups(ctx context.Context) ([]*model.Workgroup, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroups, err := repo.FindWorkgroups(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroups, nil +} + +func handleJoinWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + user, db, err := getSessionUser(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.AddUserToWorkgroup(ctx, user.ID, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleLeaveWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + user, db, err := getSessionUser(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.RemoveUserFromWorkgroup(ctx, user.ID, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleCreateWorkgroup(ctx context.Context, changes model.WorkgroupChanges) (*model.Workgroup, error) { + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.CreateWorkgroup(ctx, changes) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleCloseWorkgroup(ctx context.Context, rawWorkgroupID string) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.CloseWorkgroup(ctx, workgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func handleUpdateWorkgroup(ctx context.Context, rawWorkgroupID string, changes model.WorkgroupChanges) (*model.Workgroup, error) { + workgroupID, err := parseWorkgroupID(rawWorkgroupID) + if err != nil { + return nil, errors.WithStack(err) + } + + db, err := getDB(ctx) + if err != nil { + return nil, errors.WithStack(err) + } + + repo := model.NewWorkgroupRepository(db) + + workgroup, err := repo.UpdateWorkgroup(ctx, workgroupID, changes) + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func parseWorkgroupID(workgroupID string) (uint, error) { + workgroupID64, err := strconv.ParseUint(workgroupID, 10, 32) + if err != nil { + return 0, errors.WithStack(err) + } + + return uint(workgroupID64), nil +} diff --git a/internal/model/user.go b/internal/model/user.go index f2877b5..79a88e0 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -1,13 +1,17 @@ package model -import "time" +import ( + "time" + + "github.com/jinzhu/gorm" +) type User struct { - ID *uint `gorm:"primary_key"` - Name *string `json:"name"` - Email string `json:"email" gorm:"unique;not null"` - ConnectedAt time.Time `json:"connectedAt"` - CreatedAt time.Time `json:"createdAt"` + gorm.Model + Name *string `json:"name"` + Email string `json:"email" gorm:"unique;not null"` + ConnectedAt time.Time `json:"connectedAt"` + Workgroups []*Workgroup `gorm:"many2many:users_workgroups;"` } type ProfileChanges struct { diff --git a/internal/model/user_repository.go b/internal/model/user_repository.go index ec24928..f84a70c 100644 --- a/internal/model/user_repository.go +++ b/internal/model/user_repository.go @@ -15,8 +15,7 @@ type UserRepository struct { func (r *UserRepository) CreateOrConnectUser(ctx context.Context, email string) (*User, error) { user := &User{ - Email: email, - CreatedAt: time.Now(), + Email: email, } err := orm.WithTx(ctx, r.db, func(ctx context.Context, tx *gorm.DB) error { @@ -44,7 +43,7 @@ func (r *UserRepository) FindUserByEmail(ctx context.Context, email string) (*Us Email: email, } - err := r.db.First(user, "email = ?", email).Error + err := r.db.Model(user).Preload("Workgroups").First(user, "email = ?", email).Error if err != nil { return nil, errors.Wrap(err, "could not find user") } diff --git a/internal/model/workgroup.go b/internal/model/workgroup.go new file mode 100644 index 0000000..64b2c0a --- /dev/null +++ b/internal/model/workgroup.go @@ -0,0 +1,18 @@ +package model + +import ( + "time" + + "github.com/jinzhu/gorm" +) + +type Workgroup struct { + gorm.Model + Name *string `json:"name"` + ClosedAt time.Time `json:"closedAt"` + Members []*User `gorm:"many2many:users_workgroups;"` +} + +type WorkgroupChanges struct { + Name *string `json:"name"` +} diff --git a/internal/model/workgroup_repository.go b/internal/model/workgroup_repository.go new file mode 100644 index 0000000..283c1fe --- /dev/null +++ b/internal/model/workgroup_repository.go @@ -0,0 +1,140 @@ +package model + +import ( + "context" + "time" + + "github.com/jinzhu/gorm" + "github.com/pkg/errors" +) + +type WorkgroupRepository struct { + db *gorm.DB +} + +func (r *WorkgroupRepository) FindWorkgroups(ctx context.Context, criteria ...interface{}) ([]*Workgroup, error) { + workgroups := make([]*Workgroup, 0) + if err := r.db.Model(&Workgroup{}).Preload("Members").Find(&workgroups, criteria...).Error; err != nil { + return nil, errors.WithStack(err) + } + + return workgroups, nil +} + +func (r *WorkgroupRepository) UpdateWorkgroup(ctx context.Context, workgroupID uint, changes WorkgroupChanges) (*Workgroup, error) { + workgroup := &Workgroup{ + Name: changes.Name, + } + workgroup.ID = workgroupID + + err := r.db.Model(workgroup). + Update(workgroup). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func (r *WorkgroupRepository) CreateWorkgroup(ctx context.Context, changes WorkgroupChanges) (*Workgroup, error) { + workgroup := &Workgroup{ + Name: changes.Name, + } + + if err := r.db.Model(&Workgroup{}).Create(workgroup).Error; err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func (r *WorkgroupRepository) CloseWorkgroup(ctx context.Context, workgroupID uint) (*Workgroup, error) { + workgroup := &Workgroup{} + + err := r.db.Model(workgroup). + Where("id = ?", workgroupID). + UpdateColumn("closedAt", time.Now()). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + err = r.db.Model(workgroup).Preload("Members").First(workgroup, "id = ?", workgroupID).Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func (r *WorkgroupRepository) AddUserToWorkgroup(ctx context.Context, userID, workgroupID uint) (*Workgroup, error) { + user := &User{} + + err := r.db.Model(user).Preload("Workgroups").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"). + Append(workgroup). + Error + + if err != nil { + return nil, errors.Wrap(err, "could not add user to workgroup") + } + + err = r.db.Model(workgroup). + Preload("Members"). + First(workgroup, "id = ?", workgroupID). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +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 + + if err != nil { + return nil, errors.Wrap(err, "could not add user to workgroup") + } + + err = r.db.Model(workgroup). + Preload("Members"). + First(workgroup, "id = ?", workgroupID). + Error + if err != nil { + return nil, errors.WithStack(err) + } + + return workgroup, nil +} + +func NewWorkgroupRepository(db *gorm.DB) *WorkgroupRepository { + return &WorkgroupRepository{db} +}