feature: add ability to assign tasks

This commit is contained in:
Jordan Knott
2020-04-19 22:02:55 -05:00
parent beaa215bc2
commit c38024e692
60 changed files with 2871 additions and 790 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
package graph
import (
"context"
"net/http"
"os"
"time"
@ -10,6 +11,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid"
"github.com/jordanknott/project-citadel/api/pg"
)
@ -45,3 +47,7 @@ func NewHandler(repo pg.Repository) http.Handler {
func NewPlaygroundHandler(endpoint string) http.Handler {
return playground.Handler("GraphQL Playground", endpoint)
}
func GetUserID(ctx context.Context) (uuid.UUID, bool) {
userID, ok := ctx.Value("userID").(uuid.UUID)
return userID, ok
}

View File

@ -7,6 +7,16 @@ import (
"github.com/jordanknott/project-citadel/api/pg"
)
type AddTaskLabelInput struct {
TaskID uuid.UUID `json:"taskID"`
LabelColorID uuid.UUID `json:"labelColorID"`
}
type AssignTaskInput struct {
TaskID uuid.UUID `json:"taskID"`
UserID uuid.UUID `json:"userID"`
}
type DeleteTaskGroupInput struct {
TaskGroupID uuid.UUID `json:"taskGroupID"`
}
@ -29,6 +39,10 @@ type FindProject struct {
ProjectID string `json:"projectId"`
}
type FindTask struct {
TaskID uuid.UUID `json:"taskID"`
}
type FindUser struct {
UserID string `json:"userId"`
}
@ -37,13 +51,10 @@ type LogoutUser struct {
UserID string `json:"userID"`
}
type NewOrganization struct {
Name string `json:"name"`
}
type NewProject struct {
TeamID string `json:"teamID"`
Name string `json:"name"`
UserID uuid.UUID `json:"userID"`
TeamID uuid.UUID `json:"teamID"`
Name string `json:"name"`
}
type NewRefreshToken struct {
@ -79,16 +90,39 @@ type NewTeam struct {
}
type NewUserAccount struct {
Username string `json:"username"`
Email string `json:"email"`
DisplayName string `json:"displayName"`
Password string `json:"password"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Password string `json:"password"`
}
type ProfileIcon struct {
URL *string `json:"url"`
Initials *string `json:"initials"`
}
type ProjectMember struct {
UserID uuid.UUID `json:"userID"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
ProfileIcon *ProfileIcon `json:"profileIcon"`
}
type ProjectsFilter struct {
TeamID *string `json:"teamID"`
}
type RemoveTaskLabelInput struct {
TaskID uuid.UUID `json:"taskID"`
TaskLabelID uuid.UUID `json:"taskLabelID"`
}
type UpdateTaskDescriptionInput struct {
TaskID uuid.UUID `json:"taskID"`
Description string `json:"description"`
}
type UpdateTaskName struct {
TaskID string `json:"taskID"`
Name string `json:"name"`

View File

@ -1,7 +1,24 @@
scalar Time
scalar UUID
type TaskLabel {
taskLabelID: ID!
labelColorID: UUID!
colorHex: String!
}
type ProfileIcon {
url: String
initials: String
}
type ProjectMember {
userID: ID!
firstName: String!
lastName: String!
profileIcon: ProfileIcon!
}
type RefreshToken {
tokenId: ID!
userId: UUID!
@ -13,30 +30,26 @@ type UserAccount {
userID: ID!
email: String!
createdAt: Time!
displayName: String!
firstName: String!
lastName: String!
username: String!
}
type Organization {
organizationID: ID!
createdAt: Time!
name: String!
teams: [Team!]!
profileIcon: ProfileIcon!
}
type Team {
teamID: ID!
createdAt: Time!
name: String!
projects: [Project!]!
}
type Project {
projectID: ID!
teamID: String!
createdAt: Time!
name: String!
team: Team!
owner: ProjectMember!
taskGroups: [TaskGroup!]!
members: [ProjectMember!]!
}
type TaskGroup {
@ -54,6 +67,9 @@ type Task {
createdAt: Time!
name: String!
position: Float!
description: String
assigned: [ProjectMember!]!
labels: [TaskLabel!]!
}
input ProjectsFilter {
@ -68,14 +84,18 @@ input FindProject {
projectId: String!
}
input FindTask {
taskID: UUID!
}
type Query {
organizations: [Organization!]!
users: [UserAccount!]!
findUser(input: FindUser!): UserAccount!
findProject(input: FindProject!): Project!
teams: [Team!]!
findTask(input: FindTask!): Task!
projects(input: ProjectsFilter): [Project!]!
taskGroups: [TaskGroup!]!
me: UserAccount!
}
input NewRefreshToken {
@ -85,7 +105,8 @@ input NewRefreshToken {
input NewUserAccount {
username: String!
email: String!
displayName: String!
firstName: String!
lastName: String!
password: String!
}
@ -95,7 +116,8 @@ input NewTeam {
}
input NewProject {
teamID: String!
userID: UUID!
teamID: UUID!
name: String!
}
@ -105,10 +127,6 @@ input NewTaskGroup {
position: Float!
}
input NewOrganization {
name: String!
}
input LogoutUser {
userID: String!
}
@ -151,13 +169,31 @@ type DeleteTaskGroupPayload {
taskGroup: TaskGroup!
}
input AssignTaskInput {
taskID: UUID!
userID: UUID!
}
input UpdateTaskDescriptionInput {
taskID: UUID!
description: String!
}
input AddTaskLabelInput {
taskID: UUID!
labelColorID: UUID!
}
input RemoveTaskLabelInput {
taskID: UUID!
taskLabelID: UUID!
}
type Mutation {
createRefreshToken(input: NewRefreshToken!): RefreshToken!
createUserAccount(input: NewUserAccount!): UserAccount!
createOrganization(input: NewOrganization!): Organization!
createTeam(input: NewTeam!): Team!
createProject(input: NewProject!): Project!
@ -166,10 +202,15 @@ type Mutation {
updateTaskGroupLocation(input: NewTaskGroupLocation!): TaskGroup!
deleteTaskGroup(input: DeleteTaskGroupInput!): DeleteTaskGroupPayload!
addTaskLabel(input: AddTaskLabelInput): Task!
removeTaskLabel(input: RemoveTaskLabelInput): Task!
createTask(input: NewTask!): Task!
updateTaskDescription(input: UpdateTaskDescriptionInput!): Task!
updateTaskLocation(input: NewTaskLocation!): Task!
updateTaskName(input: UpdateTaskName!): Task!
deleteTask(input: DeleteTaskInput!): DeleteTaskPayload!
assignTask(input: AssignTaskInput): Task!
logoutUser(input: LogoutUser!): Boolean!
}

View File

@ -6,6 +6,7 @@ package graph
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
@ -24,16 +25,10 @@ func (r *mutationResolver) CreateRefreshToken(ctx context.Context, input NewRefr
func (r *mutationResolver) CreateUserAccount(ctx context.Context, input NewUserAccount) (*pg.UserAccount, error) {
createdAt := time.Now().UTC()
userAccount, err := r.Repository.CreateUserAccount(ctx, pg.CreateUserAccountParams{input.Username, input.Email, input.DisplayName, createdAt, input.Password})
userAccount, err := r.Repository.CreateUserAccount(ctx, pg.CreateUserAccountParams{input.FirstName, input.LastName, input.Email, input.Username, createdAt, input.Password})
return &userAccount, err
}
func (r *mutationResolver) CreateOrganization(ctx context.Context, input NewOrganization) (*pg.Organization, error) {
createdAt := time.Now().UTC()
organization, err := r.Repository.CreateOrganization(ctx, pg.CreateOrganizationParams{createdAt, input.Name})
return &organization, err
}
func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*pg.Team, error) {
organizationID, err := uuid.Parse(input.OrganizationID)
if err != nil {
@ -46,11 +41,7 @@ func (r *mutationResolver) CreateTeam(ctx context.Context, input NewTeam) (*pg.T
func (r *mutationResolver) CreateProject(ctx context.Context, input NewProject) (*pg.Project, error) {
createdAt := time.Now().UTC()
teamID, err := uuid.Parse(input.TeamID)
if err != nil {
return &pg.Project{}, err
}
project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{teamID, createdAt, input.Name})
project, err := r.Repository.CreateProject(ctx, pg.CreateProjectParams{input.UserID, input.TeamID, createdAt, input.Name})
return &project, err
}
@ -89,6 +80,20 @@ func (r *mutationResolver) DeleteTaskGroup(ctx context.Context, input DeleteTask
return &DeleteTaskGroupPayload{true, int(deletedTasks + deletedTaskGroups), &taskGroup}, nil
}
func (r *mutationResolver) AddTaskLabel(ctx context.Context, input *AddTaskLabelInput) (*pg.Task, error) {
assignedDate := time.Now().UTC()
_, err := r.Repository.CreateTaskLabelForTask(ctx, pg.CreateTaskLabelForTaskParams{input.TaskID, input.LabelColorID, assignedDate})
if err != nil {
return &pg.Task{}, err
}
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, nil
}
func (r *mutationResolver) RemoveTaskLabel(ctx context.Context, input *RemoveTaskLabelInput) (*pg.Task, error) {
panic(fmt.Errorf("not implemented"))
}
func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.Task, error) {
taskGroupID, err := uuid.Parse(input.TaskGroupID)
createdAt := time.Now().UTC()
@ -100,6 +105,11 @@ func (r *mutationResolver) CreateTask(ctx context.Context, input NewTask) (*pg.T
return &task, err
}
func (r *mutationResolver) UpdateTaskDescription(ctx context.Context, input UpdateTaskDescriptionInput) (*pg.Task, error) {
task, err := r.Repository.UpdateTaskDescription(ctx, pg.UpdateTaskDescriptionParams{input.TaskID, sql.NullString{String: input.Description, Valid: true}})
return &task, err
}
func (r *mutationResolver) UpdateTaskLocation(ctx context.Context, input NewTaskLocation) (*pg.Task, error) {
taskID, err := uuid.Parse(input.TaskID)
if err != nil {
@ -139,6 +149,21 @@ func (r *mutationResolver) DeleteTask(ctx context.Context, input DeleteTaskInput
return &DeleteTaskPayload{taskID.String()}, nil
}
func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInput) (*pg.Task, error) {
assignedDate := time.Now().UTC()
assignedTask, err := r.Repository.CreateTaskAssigned(ctx, pg.CreateTaskAssignedParams{input.TaskID, input.UserID, assignedDate})
log.WithFields(log.Fields{
"userID": assignedTask.UserID,
"taskID": assignedTask.TaskID,
"assignedTaskID": assignedTask.TaskAssignedID,
}).Info("assigned task")
if err != nil {
return &pg.Task{}, err
}
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, err
}
func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bool, error) {
userID, err := uuid.Parse(input.UserID)
if err != nil {
@ -149,21 +174,35 @@ func (r *mutationResolver) LogoutUser(ctx context.Context, input LogoutUser) (bo
return true, err
}
func (r *organizationResolver) Teams(ctx context.Context, obj *pg.Organization) ([]pg.Team, error) {
teams, err := r.Repository.GetTeamsForOrganization(ctx, obj.OrganizationID)
return teams, err
func (r *projectResolver) Team(ctx context.Context, obj *pg.Project) (*pg.Team, error) {
team, err := r.Repository.GetTeamByID(ctx, obj.TeamID)
return &team, err
}
func (r *projectResolver) TeamID(ctx context.Context, obj *pg.Project) (string, error) {
return obj.TeamID.String(), nil
func (r *projectResolver) Owner(ctx context.Context, obj *pg.Project) (*ProjectMember, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
if err != nil {
return &ProjectMember{}, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
return &ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon}, nil
}
func (r *projectResolver) TaskGroups(ctx context.Context, obj *pg.Project) ([]pg.TaskGroup, error) {
return r.Repository.GetTaskGroupsForProject(ctx, obj.ProjectID)
}
func (r *queryResolver) Organizations(ctx context.Context) ([]pg.Organization, error) {
return r.Repository.GetAllOrganizations(ctx)
func (r *projectResolver) Members(ctx context.Context, obj *pg.Project) ([]ProjectMember, error) {
user, err := r.Repository.GetUserAccountByID(ctx, obj.Owner)
members := []ProjectMember{}
if err != nil {
return members, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
members = append(members, ProjectMember{obj.Owner, user.FirstName, user.LastName, profileIcon})
return members, nil
}
func (r *queryResolver) Users(ctx context.Context) ([]pg.UserAccount, error) {
@ -204,8 +243,9 @@ func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*pg
return &project, err
}
func (r *queryResolver) Teams(ctx context.Context) ([]pg.Team, error) {
return r.Repository.GetAllTeams(ctx)
func (r *queryResolver) FindTask(ctx context.Context, input FindTask) (*pg.Task, error) {
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
return &task, err
}
func (r *queryResolver) Projects(ctx context.Context, input *ProjectsFilter) ([]pg.Project, error) {
@ -223,11 +263,59 @@ func (r *queryResolver) TaskGroups(ctx context.Context) ([]pg.TaskGroup, error)
return r.Repository.GetAllTaskGroups(ctx)
}
func (r *queryResolver) Me(ctx context.Context) (*pg.UserAccount, error) {
userID, ok := GetUserID(ctx)
if !ok {
return &pg.UserAccount{}, fmt.Errorf("internal server error")
}
log.WithFields(log.Fields{
"userID": userID,
}).Info("getting user account")
user, err := r.Repository.GetUserAccountByID(ctx, userID)
if err != nil {
return &pg.UserAccount{}, err
}
return &user, err
}
func (r *taskResolver) TaskGroup(ctx context.Context, obj *pg.Task) (*pg.TaskGroup, error) {
taskGroup, err := r.Repository.GetTaskGroupByID(ctx, obj.TaskGroupID)
return &taskGroup, err
}
func (r *taskResolver) Description(ctx context.Context, obj *pg.Task) (*string, error) {
task, err := r.Repository.GetTaskByID(ctx, obj.TaskID)
if err != nil {
return nil, err
}
if !task.Description.Valid {
return nil, nil
}
return &task.Description.String, nil
}
func (r *taskResolver) Assigned(ctx context.Context, obj *pg.Task) ([]ProjectMember, error) {
taskMemberLinks, err := r.Repository.GetAssignedMembersForTask(ctx, obj.TaskID)
taskMembers := []ProjectMember{}
if err != nil {
return taskMembers, err
}
for _, taskMemberLink := range taskMemberLinks {
user, err := r.Repository.GetUserAccountByID(ctx, taskMemberLink.UserID)
if err != nil {
return taskMembers, err
}
initials := string([]rune(user.FirstName)[0]) + string([]rune(user.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
taskMembers = append(taskMembers, ProjectMember{taskMemberLink.UserID, user.FirstName, user.LastName, profileIcon})
}
return taskMembers, nil
}
func (r *taskResolver) Labels(ctx context.Context, obj *pg.Task) ([]pg.TaskLabel, error) {
return r.Repository.GetTaskLabelsForTaskID(ctx, obj.TaskID)
}
func (r *taskGroupResolver) ProjectID(ctx context.Context, obj *pg.TaskGroup) (string, error) {
return obj.ProjectID.String(), nil
}
@ -237,16 +325,23 @@ func (r *taskGroupResolver) Tasks(ctx context.Context, obj *pg.TaskGroup) ([]pg.
return tasks, err
}
func (r *teamResolver) Projects(ctx context.Context, obj *pg.Team) ([]pg.Project, error) {
return r.Repository.GetAllProjectsForTeam(ctx, obj.TeamID)
func (r *taskLabelResolver) ColorHex(ctx context.Context, obj *pg.TaskLabel) (string, error) {
labelColor, err := r.Repository.GetLabelColorByID(ctx, obj.LabelColorID)
if err != nil {
return "", err
}
return labelColor.ColorHex, nil
}
func (r *userAccountResolver) ProfileIcon(ctx context.Context, obj *pg.UserAccount) (*ProfileIcon, error) {
initials := string([]rune(obj.FirstName)[0]) + string([]rune(obj.LastName)[0])
profileIcon := &ProfileIcon{nil, &initials}
return profileIcon, nil
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Organization returns OrganizationResolver implementation.
func (r *Resolver) Organization() OrganizationResolver { return &organizationResolver{r} }
// Project returns ProjectResolver implementation.
func (r *Resolver) Project() ProjectResolver { return &projectResolver{r} }
@ -259,13 +354,26 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
// TaskGroup returns TaskGroupResolver implementation.
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
// Team returns TeamResolver implementation.
func (r *Resolver) Team() TeamResolver { return &teamResolver{r} }
// TaskLabel returns TaskLabelResolver implementation.
func (r *Resolver) TaskLabel() TaskLabelResolver { return &taskLabelResolver{r} }
// UserAccount returns UserAccountResolver implementation.
func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolver{r} }
type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type projectResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
type taskGroupResolver struct{ *Resolver }
type teamResolver struct{ *Resolver }
type taskLabelResolver struct{ *Resolver }
type userAccountResolver 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 *userAccountResolver) DisplayName(ctx context.Context, obj *pg.UserAccount) (string, error) {
return obj.FirstName + " " + obj.LastName, nil
}