feat: add bell notification system for task assignment
This commit is contained in:
@ -68,6 +68,6 @@ func initConfig() {
|
||||
// Execute the root cobra command
|
||||
func Execute() {
|
||||
rootCmd.SetVersionTemplate(VersionTemplate())
|
||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
|
||||
rootCmd.AddCommand(newTokenCmd(), newWebCmd(), newMigrateCmd(), newWorkerCmd(), newResetPasswordCmd(), newSeedCmd())
|
||||
rootCmd.Execute()
|
||||
}
|
||||
|
93
internal/commands/token.go
Normal file
93
internal/commands/token.go
Normal file
@ -0,0 +1,93 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jordanknott/taskcafe/internal/config"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func newTokenCmd() *cobra.Command {
|
||||
cc := &cobra.Command{
|
||||
Use: "token [username]",
|
||||
Short: "Creates an access token for a user",
|
||||
Long: "Creates an access token for a user",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
Formatter := new(log.TextFormatter)
|
||||
Formatter.TimestampFormat = "02-01-2006 15:04:05"
|
||||
Formatter.FullTimestamp = true
|
||||
log.SetFormatter(Formatter)
|
||||
log.SetLevel(log.InfoLevel)
|
||||
appConfig, err := config.GetAppConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dbConnection *sqlx.DB
|
||||
var retryDuration time.Duration
|
||||
maxRetryNumber := 4
|
||||
for i := 0; i < maxRetryNumber; i++ {
|
||||
dbConnection, err = sqlx.Connect("postgres", appConfig.Database.GetDatabaseConnectionUri())
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
retryDuration = time.Duration(i*2) * time.Second
|
||||
log.WithFields(log.Fields{"retryNumber": i, "retryDuration": retryDuration}).WithError(err).Error("issue connecting to database, retrying")
|
||||
if i != maxRetryNumber-1 {
|
||||
time.Sleep(retryDuration)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbConnection.SetMaxOpenConns(25)
|
||||
dbConnection.SetMaxIdleConns(25)
|
||||
dbConnection.SetConnMaxLifetime(5 * time.Minute)
|
||||
defer dbConnection.Close()
|
||||
|
||||
if viper.GetBool("migrate") {
|
||||
log.Info("running auto schema migrations")
|
||||
if err = runMigration(dbConnection); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
repository := db.NewRepository(dbConnection)
|
||||
user, err := repository.GetUserAccountByUsername(ctx, args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := repository.CreateAuthToken(ctx, db.CreateAuthTokenParams{
|
||||
UserID: user.UserID,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24 * 7),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Created token: %s\n", token.TokenID.String())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server")
|
||||
cc.Flags().IntVar(&teams, "teams", 5, "number of teams to generate")
|
||||
cc.Flags().IntVar(&projects, "projects", 10, "number of projects to create per team (personal projects are included)")
|
||||
cc.Flags().IntVar(&taskGroups, "task_groups", 5, "number of task groups to generate per project")
|
||||
cc.Flags().IntVar(&tasks, "tasks", 25, "number of tasks to generate per task group")
|
||||
|
||||
viper.SetDefault("migrate", false)
|
||||
return cc
|
||||
}
|
@ -10,6 +10,34 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AccountSetting struct {
|
||||
AccountSettingID string `json:"account_setting_id"`
|
||||
Constrained bool `json:"constrained"`
|
||||
DataType string `json:"data_type"`
|
||||
ConstrainedDefaultValue sql.NullString `json:"constrained_default_value"`
|
||||
UnconstrainedDefaultValue sql.NullString `json:"unconstrained_default_value"`
|
||||
}
|
||||
|
||||
type AccountSettingAllowedValue struct {
|
||||
AllowedValueID uuid.UUID `json:"allowed_value_id"`
|
||||
SettingID int32 `json:"setting_id"`
|
||||
ItemValue string `json:"item_value"`
|
||||
}
|
||||
|
||||
type AccountSettingDataType struct {
|
||||
DataTypeID string `json:"data_type_id"`
|
||||
}
|
||||
|
||||
type AccountSettingValue struct {
|
||||
AccountSettingID uuid.UUID `json:"account_setting_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
SettingID int32 `json:"setting_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
AllowedValueID uuid.UUID `json:"allowed_value_id"`
|
||||
UnconstrainedValue sql.NullString `json:"unconstrained_value"`
|
||||
}
|
||||
|
||||
type AuthToken struct {
|
||||
TokenID uuid.UUID `json:"token_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
@ -172,6 +200,13 @@ type TaskLabel struct {
|
||||
AssignedDate time.Time `json:"assigned_date"`
|
||||
}
|
||||
|
||||
type TaskWatcher struct {
|
||||
TaskWatcherID uuid.UUID `json:"task_watcher_id"`
|
||||
TaskID uuid.UUID `json:"task_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
WatchedAt time.Time `json:"watched_at"`
|
||||
}
|
||||
|
||||
type Team struct {
|
||||
TeamID uuid.UUID `json:"team_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
const createNotification = `-- name: CreateNotification :one
|
||||
@ -142,16 +143,285 @@ func (q *Queries) GetAllNotificationsForUserID(ctx context.Context, userID uuid.
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNotificationsForUserIDCursor = `-- name: GetNotificationsForUserIDCursor :many
|
||||
SELECT notified_id, nn.notification_id, nn.user_id, read, read_at, n.notification_id, caused_by, action_type, data, created_on, user_account.user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM notification_notified AS nn
|
||||
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE (n.created_on, n.notification_id) < ($1::timestamptz, $2::uuid)
|
||||
AND nn.user_id = $3::uuid
|
||||
ORDER BY n.created_on DESC
|
||||
LIMIT $4::int
|
||||
`
|
||||
|
||||
type GetNotificationsForUserIDCursorParams struct {
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
LimitRows int32 `json:"limit_rows"`
|
||||
}
|
||||
|
||||
type GetNotificationsForUserIDCursorRow struct {
|
||||
NotifiedID uuid.UUID `json:"notified_id"`
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Read bool `json:"read"`
|
||||
ReadAt sql.NullTime `json:"read_at"`
|
||||
NotificationID_2 uuid.UUID `json:"notification_id_2"`
|
||||
CausedBy uuid.UUID `json:"caused_by"`
|
||||
ActionType string `json:"action_type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
UserID_2 uuid.UUID `json:"user_id_2"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
ProfileBgColor string `json:"profile_bg_color"`
|
||||
FullName string `json:"full_name"`
|
||||
Initials string `json:"initials"`
|
||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||
RoleCode string `json:"role_code"`
|
||||
Bio string `json:"bio"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetNotificationsForUserIDCursor(ctx context.Context, arg GetNotificationsForUserIDCursorParams) ([]GetNotificationsForUserIDCursorRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getNotificationsForUserIDCursor,
|
||||
arg.CreatedOn,
|
||||
arg.NotificationID,
|
||||
arg.UserID,
|
||||
arg.LimitRows,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetNotificationsForUserIDCursorRow
|
||||
for rows.Next() {
|
||||
var i GetNotificationsForUserIDCursorRow
|
||||
if err := rows.Scan(
|
||||
&i.NotifiedID,
|
||||
&i.NotificationID,
|
||||
&i.UserID,
|
||||
&i.Read,
|
||||
&i.ReadAt,
|
||||
&i.NotificationID_2,
|
||||
&i.CausedBy,
|
||||
&i.ActionType,
|
||||
&i.Data,
|
||||
&i.CreatedOn,
|
||||
&i.UserID_2,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNotificationsForUserIDPaged = `-- name: GetNotificationsForUserIDPaged :many
|
||||
SELECT notified_id, nn.notification_id, nn.user_id, read, read_at, n.notification_id, caused_by, action_type, data, created_on, user_account.user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM notification_notified AS nn
|
||||
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE nn.user_id = $1::uuid
|
||||
AND ($2::boolean = false OR nn.read = false)
|
||||
AND ($3::boolean = false OR n.action_type = ANY($4::text[]))
|
||||
ORDER BY n.created_on DESC
|
||||
LIMIT $5::int
|
||||
`
|
||||
|
||||
type GetNotificationsForUserIDPagedParams struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
EnableUnread bool `json:"enable_unread"`
|
||||
EnableActionType bool `json:"enable_action_type"`
|
||||
ActionType []string `json:"action_type"`
|
||||
LimitRows int32 `json:"limit_rows"`
|
||||
}
|
||||
|
||||
type GetNotificationsForUserIDPagedRow struct {
|
||||
NotifiedID uuid.UUID `json:"notified_id"`
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Read bool `json:"read"`
|
||||
ReadAt sql.NullTime `json:"read_at"`
|
||||
NotificationID_2 uuid.UUID `json:"notification_id_2"`
|
||||
CausedBy uuid.UUID `json:"caused_by"`
|
||||
ActionType string `json:"action_type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
UserID_2 uuid.UUID `json:"user_id_2"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
ProfileBgColor string `json:"profile_bg_color"`
|
||||
FullName string `json:"full_name"`
|
||||
Initials string `json:"initials"`
|
||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||
RoleCode string `json:"role_code"`
|
||||
Bio string `json:"bio"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetNotificationsForUserIDPaged(ctx context.Context, arg GetNotificationsForUserIDPagedParams) ([]GetNotificationsForUserIDPagedRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getNotificationsForUserIDPaged,
|
||||
arg.UserID,
|
||||
arg.EnableUnread,
|
||||
arg.EnableActionType,
|
||||
pq.Array(arg.ActionType),
|
||||
arg.LimitRows,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetNotificationsForUserIDPagedRow
|
||||
for rows.Next() {
|
||||
var i GetNotificationsForUserIDPagedRow
|
||||
if err := rows.Scan(
|
||||
&i.NotifiedID,
|
||||
&i.NotificationID,
|
||||
&i.UserID,
|
||||
&i.Read,
|
||||
&i.ReadAt,
|
||||
&i.NotificationID_2,
|
||||
&i.CausedBy,
|
||||
&i.ActionType,
|
||||
&i.Data,
|
||||
&i.CreatedOn,
|
||||
&i.UserID_2,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getNotifiedByID = `-- name: GetNotifiedByID :one
|
||||
SELECT notified_id, nn.notification_id, nn.user_id, read, read_at, n.notification_id, caused_by, action_type, data, created_on, user_account.user_id, created_at, email, username, password_hash, profile_bg_color, full_name, initials, profile_avatar_url, role_code, bio, active FROM notification_notified as nn
|
||||
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE notified_id = $1
|
||||
`
|
||||
|
||||
type GetNotifiedByIDRow struct {
|
||||
NotifiedID uuid.UUID `json:"notified_id"`
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Read bool `json:"read"`
|
||||
ReadAt sql.NullTime `json:"read_at"`
|
||||
NotificationID_2 uuid.UUID `json:"notification_id_2"`
|
||||
CausedBy uuid.UUID `json:"caused_by"`
|
||||
ActionType string `json:"action_type"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
UserID_2 uuid.UUID `json:"user_id_2"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
ProfileBgColor string `json:"profile_bg_color"`
|
||||
FullName string `json:"full_name"`
|
||||
Initials string `json:"initials"`
|
||||
ProfileAvatarUrl sql.NullString `json:"profile_avatar_url"`
|
||||
RoleCode string `json:"role_code"`
|
||||
Bio string `json:"bio"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetNotifiedByID(ctx context.Context, notifiedID uuid.UUID) (GetNotifiedByIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getNotifiedByID, notifiedID)
|
||||
var i GetNotifiedByIDRow
|
||||
err := row.Scan(
|
||||
&i.NotifiedID,
|
||||
&i.NotificationID,
|
||||
&i.UserID,
|
||||
&i.Read,
|
||||
&i.ReadAt,
|
||||
&i.NotificationID_2,
|
||||
&i.CausedBy,
|
||||
&i.ActionType,
|
||||
&i.Data,
|
||||
&i.CreatedOn,
|
||||
&i.UserID_2,
|
||||
&i.CreatedAt,
|
||||
&i.Email,
|
||||
&i.Username,
|
||||
&i.PasswordHash,
|
||||
&i.ProfileBgColor,
|
||||
&i.FullName,
|
||||
&i.Initials,
|
||||
&i.ProfileAvatarUrl,
|
||||
&i.RoleCode,
|
||||
&i.Bio,
|
||||
&i.Active,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const hasUnreadNotification = `-- name: HasUnreadNotification :one
|
||||
SELECT EXISTS (SELECT 1 FROM notification_notified WHERE read = false AND user_id = $1)
|
||||
`
|
||||
|
||||
func (q *Queries) HasUnreadNotification(ctx context.Context, userID uuid.UUID) (bool, error) {
|
||||
row := q.db.QueryRowContext(ctx, hasUnreadNotification, userID)
|
||||
var exists bool
|
||||
err := row.Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
const markNotificationAsRead = `-- name: MarkNotificationAsRead :exec
|
||||
UPDATE notification_notified SET read = true, read_at = $2 WHERE user_id = $1
|
||||
UPDATE notification_notified SET read = $3, read_at = $2 WHERE user_id = $1 AND notified_id = $4
|
||||
`
|
||||
|
||||
type MarkNotificationAsReadParams struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
ReadAt sql.NullTime `json:"read_at"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
ReadAt sql.NullTime `json:"read_at"`
|
||||
Read bool `json:"read"`
|
||||
NotifiedID uuid.UUID `json:"notified_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkNotificationAsRead(ctx context.Context, arg MarkNotificationAsReadParams) error {
|
||||
_, err := q.db.ExecContext(ctx, markNotificationAsRead, arg.UserID, arg.ReadAt)
|
||||
_, err := q.db.ExecContext(ctx, markNotificationAsRead,
|
||||
arg.UserID,
|
||||
arg.ReadAt,
|
||||
arg.Read,
|
||||
arg.NotifiedID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ type Querier interface {
|
||||
CreateTaskComment(ctx context.Context, arg CreateTaskCommentParams) (TaskComment, error)
|
||||
CreateTaskGroup(ctx context.Context, arg CreateTaskGroupParams) (TaskGroup, error)
|
||||
CreateTaskLabelForTask(ctx context.Context, arg CreateTaskLabelForTaskParams) (TaskLabel, error)
|
||||
CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherParams) (TaskWatcher, error)
|
||||
CreateTeam(ctx context.Context, arg CreateTeamParams) (Team, error)
|
||||
CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error)
|
||||
CreateTeamProject(ctx context.Context, arg CreateTeamProjectParams) (Project, error)
|
||||
@ -54,6 +55,7 @@ type Querier interface {
|
||||
DeleteTaskGroupByID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
|
||||
DeleteTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) error
|
||||
DeleteTaskLabelForTaskByProjectLabelID(ctx context.Context, arg DeleteTaskLabelForTaskByProjectLabelIDParams) error
|
||||
DeleteTaskWatcher(ctx context.Context, arg DeleteTaskWatcherParams) error
|
||||
DeleteTasksByTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) (int64, error)
|
||||
DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
|
||||
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
|
||||
@ -87,6 +89,9 @@ type Querier interface {
|
||||
GetMemberData(ctx context.Context, projectID uuid.UUID) ([]UserAccount, error)
|
||||
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||
GetNotificationsForUserIDCursor(ctx context.Context, arg GetNotificationsForUserIDCursorParams) ([]GetNotificationsForUserIDCursorRow, error)
|
||||
GetNotificationsForUserIDPaged(ctx context.Context, arg GetNotificationsForUserIDPagedParams) ([]GetNotificationsForUserIDPagedRow, error)
|
||||
GetNotifiedByID(ctx context.Context, notifiedID uuid.UUID) (GetNotifiedByIDRow, error)
|
||||
GetPersonalProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
|
||||
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
|
||||
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
|
||||
@ -94,6 +99,7 @@ type Querier interface {
|
||||
GetProjectIDForTaskChecklistItem(ctx context.Context, taskChecklistItemID uuid.UUID) (uuid.UUID, error)
|
||||
GetProjectIDForTaskGroup(ctx context.Context, taskGroupID uuid.UUID) (uuid.UUID, error)
|
||||
GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID) ([]GetProjectIdMappingsRow, error)
|
||||
GetProjectInfoForTask(ctx context.Context, taskID uuid.UUID) (GetProjectInfoForTaskRow, error)
|
||||
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
|
||||
GetProjectLabelsForProject(ctx context.Context, projectID uuid.UUID) ([]ProjectLabel, error)
|
||||
GetProjectMemberInvitedIDByEmail(ctx context.Context, email string) (GetProjectMemberInvitedIDByEmailRow, error)
|
||||
@ -116,6 +122,7 @@ type Querier interface {
|
||||
GetTaskLabelByID(ctx context.Context, taskLabelID uuid.UUID) (TaskLabel, error)
|
||||
GetTaskLabelForTaskByProjectLabelID(ctx context.Context, arg GetTaskLabelForTaskByProjectLabelIDParams) (TaskLabel, error)
|
||||
GetTaskLabelsForTaskID(ctx context.Context, taskID uuid.UUID) ([]TaskLabel, error)
|
||||
GetTaskWatcher(ctx context.Context, arg GetTaskWatcherParams) (TaskWatcher, error)
|
||||
GetTasksForTaskGroupID(ctx context.Context, taskGroupID uuid.UUID) ([]Task, error)
|
||||
GetTeamByID(ctx context.Context, teamID uuid.UUID) (Team, error)
|
||||
GetTeamMemberByID(ctx context.Context, arg GetTeamMemberByIDParams) (TeamMember, error)
|
||||
@ -131,6 +138,7 @@ type Querier interface {
|
||||
GetUserRolesForProject(ctx context.Context, arg GetUserRolesForProjectParams) (GetUserRolesForProjectRow, error)
|
||||
HasActiveUser(ctx context.Context) (bool, error)
|
||||
HasAnyUser(ctx context.Context) (bool, error)
|
||||
HasUnreadNotification(ctx context.Context, userID uuid.UUID) (bool, error)
|
||||
MarkNotificationAsRead(ctx context.Context, arg MarkNotificationAsReadParams) error
|
||||
SetFirstUserActive(ctx context.Context) (UserAccount, error)
|
||||
SetInactiveLastMoveForTaskID(ctx context.Context, taskID uuid.UUID) error
|
||||
|
@ -4,8 +4,17 @@ SELECT * FROM notification_notified AS nn
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE nn.user_id = $1;
|
||||
|
||||
-- name: GetNotifiedByID :one
|
||||
SELECT * FROM notification_notified as nn
|
||||
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE notified_id = $1;
|
||||
|
||||
-- name: HasUnreadNotification :one
|
||||
SELECT EXISTS (SELECT 1 FROM notification_notified WHERE read = false AND user_id = $1);
|
||||
|
||||
-- name: MarkNotificationAsRead :exec
|
||||
UPDATE notification_notified SET read = true, read_at = $2 WHERE user_id = $1;
|
||||
UPDATE notification_notified SET read = $3, read_at = $2 WHERE user_id = $1 AND notified_id = $4;
|
||||
|
||||
-- name: CreateNotification :one
|
||||
INSERT INTO notification (caused_by, data, action_type, created_on)
|
||||
@ -13,3 +22,22 @@ INSERT INTO notification (caused_by, data, action_type, created_on)
|
||||
|
||||
-- name: CreateNotificationNotifed :one
|
||||
INSERT INTO notification_notified (notification_id, user_id) VALUES ($1, $2) RETURNING *;
|
||||
|
||||
-- name: GetNotificationsForUserIDPaged :many
|
||||
SELECT * FROM notification_notified AS nn
|
||||
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE nn.user_id = @user_id::uuid
|
||||
AND (@enable_unread::boolean = false OR nn.read = false)
|
||||
AND (@enable_action_type::boolean = false OR n.action_type = ANY(@action_type::text[]))
|
||||
ORDER BY n.created_on DESC
|
||||
LIMIT @limit_rows::int;
|
||||
|
||||
-- name: GetNotificationsForUserIDCursor :many
|
||||
SELECT * FROM notification_notified AS nn
|
||||
INNER JOIN notification AS n ON n.notification_id = nn.notification_id
|
||||
LEFT JOIN user_account ON user_account.user_id = n.caused_by
|
||||
WHERE (n.created_on, n.notification_id) < (@created_on::timestamptz, @notification_id::uuid)
|
||||
AND nn.user_id = @user_id::uuid
|
||||
ORDER BY n.created_on DESC
|
||||
LIMIT @limit_rows::int;
|
||||
|
@ -1,3 +1,12 @@
|
||||
-- name: GetTaskWatcher :one
|
||||
SELECT * FROM task_watcher WHERE user_id = $1 AND task_id = $2;
|
||||
|
||||
-- name: CreateTaskWatcher :one
|
||||
INSERT INTO task_watcher (user_id, task_id, watched_at) VALUES ($1, $2, $3) RETURNING *;
|
||||
|
||||
-- name: DeleteTaskWatcher :exec
|
||||
DELETE FROM task_watcher WHERE user_id = $1 AND task_id = $2;
|
||||
|
||||
-- name: CreateTask :one
|
||||
INSERT INTO task (task_group_id, created_at, name, position)
|
||||
VALUES($1, $2, $3, $4) RETURNING *;
|
||||
@ -44,6 +53,12 @@ SELECT project_id FROM task
|
||||
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
||||
WHERE task_id = $1;
|
||||
|
||||
-- name: GetProjectInfoForTask :one
|
||||
SELECT project.project_id, project.name FROM task
|
||||
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
||||
INNER JOIN project ON task_group.project_id = project.project_id
|
||||
WHERE task_id = $1;
|
||||
|
||||
-- name: CreateTaskComment :one
|
||||
INSERT INTO task_comment (task_id, message, created_at, created_by)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *;
|
||||
|
@ -120,6 +120,28 @@ func (q *Queries) CreateTaskComment(ctx context.Context, arg CreateTaskCommentPa
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createTaskWatcher = `-- name: CreateTaskWatcher :one
|
||||
INSERT INTO task_watcher (user_id, task_id, watched_at) VALUES ($1, $2, $3) RETURNING task_watcher_id, task_id, user_id, watched_at
|
||||
`
|
||||
|
||||
type CreateTaskWatcherParams struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
TaskID uuid.UUID `json:"task_id"`
|
||||
WatchedAt time.Time `json:"watched_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateTaskWatcher(ctx context.Context, arg CreateTaskWatcherParams) (TaskWatcher, error) {
|
||||
row := q.db.QueryRowContext(ctx, createTaskWatcher, arg.UserID, arg.TaskID, arg.WatchedAt)
|
||||
var i TaskWatcher
|
||||
err := row.Scan(
|
||||
&i.TaskWatcherID,
|
||||
&i.TaskID,
|
||||
&i.UserID,
|
||||
&i.WatchedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteTaskByID = `-- name: DeleteTaskByID :exec
|
||||
DELETE FROM task WHERE task_id = $1
|
||||
`
|
||||
@ -148,6 +170,20 @@ func (q *Queries) DeleteTaskCommentByID(ctx context.Context, taskCommentID uuid.
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteTaskWatcher = `-- name: DeleteTaskWatcher :exec
|
||||
DELETE FROM task_watcher WHERE user_id = $1 AND task_id = $2
|
||||
`
|
||||
|
||||
type DeleteTaskWatcherParams struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
TaskID uuid.UUID `json:"task_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteTaskWatcher(ctx context.Context, arg DeleteTaskWatcherParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteTaskWatcher, arg.UserID, arg.TaskID)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteTasksByTaskGroupID = `-- name: DeleteTasksByTaskGroupID :execrows
|
||||
DELETE FROM task where task_group_id = $1
|
||||
`
|
||||
@ -409,6 +445,25 @@ func (q *Queries) GetProjectIdMappings(ctx context.Context, dollar_1 []uuid.UUID
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getProjectInfoForTask = `-- name: GetProjectInfoForTask :one
|
||||
SELECT project.project_id, project.name FROM task
|
||||
INNER JOIN task_group ON task_group.task_group_id = task.task_group_id
|
||||
INNER JOIN project ON task_group.project_id = project.project_id
|
||||
WHERE task_id = $1
|
||||
`
|
||||
|
||||
type GetProjectInfoForTaskRow struct {
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetProjectInfoForTask(ctx context.Context, taskID uuid.UUID) (GetProjectInfoForTaskRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getProjectInfoForTask, taskID)
|
||||
var i GetProjectInfoForTaskRow
|
||||
err := row.Scan(&i.ProjectID, &i.Name)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getRecentlyAssignedTaskForUserID = `-- name: GetRecentlyAssignedTaskForUserID :many
|
||||
SELECT task.task_id, task.task_group_id, task.created_at, task.name, task.position, task.description, task.due_date, task.complete, task.completed_at, task.has_time FROM task_assigned INNER JOIN
|
||||
task ON task.task_id = task_assigned.task_id WHERE user_id = $1
|
||||
@ -488,6 +543,27 @@ func (q *Queries) GetTaskByID(ctx context.Context, taskID uuid.UUID) (Task, erro
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTaskWatcher = `-- name: GetTaskWatcher :one
|
||||
SELECT task_watcher_id, task_id, user_id, watched_at FROM task_watcher WHERE user_id = $1 AND task_id = $2
|
||||
`
|
||||
|
||||
type GetTaskWatcherParams struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
TaskID uuid.UUID `json:"task_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetTaskWatcher(ctx context.Context, arg GetTaskWatcherParams) (TaskWatcher, error) {
|
||||
row := q.db.QueryRowContext(ctx, getTaskWatcher, arg.UserID, arg.TaskID)
|
||||
var i TaskWatcher
|
||||
err := row.Scan(
|
||||
&i.TaskWatcherID,
|
||||
&i.TaskID,
|
||||
&i.UserID,
|
||||
&i.WatchedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTasksForTaskGroupID = `-- name: GetTasksForTaskGroupID :many
|
||||
SELECT task_id, task_group_id, created_at, name, position, description, due_date, complete, completed_at, has_time FROM task WHERE task_group_id = $1
|
||||
`
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
@ -31,6 +32,10 @@ func NewHandler(repo db.Repository, appConfig config.AppConfig) http.Handler {
|
||||
Resolvers: &Resolver{
|
||||
Repository: repo,
|
||||
AppConfig: appConfig,
|
||||
Notifications: NotificationObservers{
|
||||
Mu: sync.Mutex{},
|
||||
Subscribers: make(map[string]map[string]chan *Notified),
|
||||
},
|
||||
},
|
||||
}
|
||||
c.Directives.HasRole = func(ctx context.Context, obj interface{}, next graphql.Resolver, roles []RoleLevel, level ActionLevel, typeArg ObjectType) (interface{}, error) {
|
||||
@ -223,16 +228,6 @@ func ConvertToRoleCode(r string) RoleCode {
|
||||
return RoleCodeObserver
|
||||
}
|
||||
|
||||
// GetActionType converts integer to ActionType enum
|
||||
func GetActionType(actionType int32) ActionType {
|
||||
switch actionType {
|
||||
case 1:
|
||||
return ActionTypeTaskMemberAdded
|
||||
default:
|
||||
panic("Not a valid entity type!")
|
||||
}
|
||||
}
|
||||
|
||||
type MemberType string
|
||||
|
||||
const (
|
||||
|
@ -3,8 +3,12 @@ package graph
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetOwnedList todo: remove this
|
||||
@ -12,6 +16,57 @@ func GetOwnedList(ctx context.Context, r db.Repository, user db.UserAccount) (*O
|
||||
return &OwnedList{}, nil
|
||||
}
|
||||
|
||||
type CreateNotificationParams struct {
|
||||
NotifiedList []uuid.UUID
|
||||
ActionType ActionType
|
||||
CausedBy uuid.UUID
|
||||
Data map[string]string
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateNotification(ctx context.Context, data CreateNotificationParams) error {
|
||||
now := time.Now().UTC()
|
||||
raw, err := json.Marshal(NotifiedData{Data: data.Data})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while marshal json data for notification")
|
||||
return err
|
||||
}
|
||||
log.WithField("ActionType", data.ActionType).Info("creating notification object")
|
||||
n, err := r.Repository.CreateNotification(ctx, db.CreateNotificationParams{
|
||||
CausedBy: data.CausedBy,
|
||||
ActionType: data.ActionType.String(),
|
||||
CreatedOn: now,
|
||||
Data: json.RawMessage(raw),
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating notification")
|
||||
return err
|
||||
}
|
||||
for _, nn := range data.NotifiedList {
|
||||
log.WithFields(log.Fields{"UserID": nn, "NotificationID": n.NotificationID}).Info("creating notification notified object")
|
||||
notified, err := r.Repository.CreateNotificationNotifed(ctx, db.CreateNotificationNotifedParams{
|
||||
UserID: nn,
|
||||
NotificationID: n.NotificationID,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating notification notified object")
|
||||
return err
|
||||
}
|
||||
for ouid, observers := range r.Notifications.Subscribers {
|
||||
log.WithField("ouid", ouid).Info("checking user subscribers")
|
||||
for oid, ochan := range observers {
|
||||
log.WithField("ouid", ouid).WithField("oid", oid).Info("checking user subscriber")
|
||||
ochan <- &Notified{
|
||||
ID: notified.NotifiedID,
|
||||
Read: notified.Read,
|
||||
ReadAt: ¬ified.ReadAt.Time,
|
||||
Notification: &n,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMemberList returns a list of projects the user is a member of
|
||||
func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*MemberList, error) {
|
||||
projectMemberIDs, err := r.GetMemberProjectIDsForUserID(ctx, user.UserID)
|
||||
@ -45,3 +100,7 @@ func GetMemberList(ctx context.Context, r db.Repository, user db.UserAccount) (*
|
||||
type ActivityData struct {
|
||||
Data map[string]string
|
||||
}
|
||||
|
||||
type NotifiedData struct {
|
||||
Data map[string]string
|
||||
}
|
||||
|
@ -230,6 +230,10 @@ type FindUser struct {
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
}
|
||||
|
||||
type HasUnreadNotificationsResult struct {
|
||||
Unread bool `json:"unread"`
|
||||
}
|
||||
|
||||
type InviteProjectMembers struct {
|
||||
ProjectID uuid.UUID `json:"projectID"`
|
||||
Members []MemberInvite `json:"members"`
|
||||
@ -367,6 +371,10 @@ type NotificationData struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type NotificationToggleReadInput struct {
|
||||
NotifiedID uuid.UUID `json:"notifiedID"`
|
||||
}
|
||||
|
||||
type Notified struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Notification *db.Notification `json:"notification"`
|
||||
@ -374,6 +382,18 @@ type Notified struct {
|
||||
ReadAt *time.Time `json:"readAt"`
|
||||
}
|
||||
|
||||
type NotifiedInput struct {
|
||||
Limit int `json:"limit"`
|
||||
Cursor *string `json:"cursor"`
|
||||
Filter NotificationFilter `json:"filter"`
|
||||
}
|
||||
|
||||
type NotifiedResult struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
Notified []Notified `json:"notified"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
}
|
||||
|
||||
type OwnedList struct {
|
||||
Teams []db.Team `json:"teams"`
|
||||
Projects []db.Project `json:"projects"`
|
||||
@ -384,6 +404,11 @@ type OwnersList struct {
|
||||
Teams []uuid.UUID `json:"teams"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
EndCursor string `json:"endCursor"`
|
||||
HasNextPage bool `json:"hasNextPage"`
|
||||
}
|
||||
|
||||
type ProfileIcon struct {
|
||||
URL *string `json:"url"`
|
||||
Initials *string `json:"initials"`
|
||||
@ -479,6 +504,10 @@ type ToggleTaskLabelPayload struct {
|
||||
Task *db.Task `json:"task"`
|
||||
}
|
||||
|
||||
type ToggleTaskWatch struct {
|
||||
TaskID uuid.UUID `json:"taskID"`
|
||||
}
|
||||
|
||||
type UnassignTaskInput struct {
|
||||
TaskID uuid.UUID `json:"taskID"`
|
||||
UserID uuid.UUID `json:"userID"`
|
||||
@ -671,16 +700,42 @@ func (e ActionLevel) MarshalGQL(w io.Writer) {
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
ActionTypeTaskMemberAdded ActionType = "TASK_MEMBER_ADDED"
|
||||
ActionTypeTeamAdded ActionType = "TEAM_ADDED"
|
||||
ActionTypeTeamRemoved ActionType = "TEAM_REMOVED"
|
||||
ActionTypeProjectAdded ActionType = "PROJECT_ADDED"
|
||||
ActionTypeProjectRemoved ActionType = "PROJECT_REMOVED"
|
||||
ActionTypeProjectArchived ActionType = "PROJECT_ARCHIVED"
|
||||
ActionTypeDueDateAdded ActionType = "DUE_DATE_ADDED"
|
||||
ActionTypeDueDateRemoved ActionType = "DUE_DATE_REMOVED"
|
||||
ActionTypeDueDateChanged ActionType = "DUE_DATE_CHANGED"
|
||||
ActionTypeTaskAssigned ActionType = "TASK_ASSIGNED"
|
||||
ActionTypeTaskMoved ActionType = "TASK_MOVED"
|
||||
ActionTypeTaskArchived ActionType = "TASK_ARCHIVED"
|
||||
ActionTypeTaskAttachmentUploaded ActionType = "TASK_ATTACHMENT_UPLOADED"
|
||||
ActionTypeCommentMentioned ActionType = "COMMENT_MENTIONED"
|
||||
ActionTypeCommentOther ActionType = "COMMENT_OTHER"
|
||||
)
|
||||
|
||||
var AllActionType = []ActionType{
|
||||
ActionTypeTaskMemberAdded,
|
||||
ActionTypeTeamAdded,
|
||||
ActionTypeTeamRemoved,
|
||||
ActionTypeProjectAdded,
|
||||
ActionTypeProjectRemoved,
|
||||
ActionTypeProjectArchived,
|
||||
ActionTypeDueDateAdded,
|
||||
ActionTypeDueDateRemoved,
|
||||
ActionTypeDueDateChanged,
|
||||
ActionTypeTaskAssigned,
|
||||
ActionTypeTaskMoved,
|
||||
ActionTypeTaskArchived,
|
||||
ActionTypeTaskAttachmentUploaded,
|
||||
ActionTypeCommentMentioned,
|
||||
ActionTypeCommentOther,
|
||||
}
|
||||
|
||||
func (e ActionType) IsValid() bool {
|
||||
switch e {
|
||||
case ActionTypeTaskMemberAdded:
|
||||
case ActionTypeTeamAdded, ActionTypeTeamRemoved, ActionTypeProjectAdded, ActionTypeProjectRemoved, ActionTypeProjectArchived, ActionTypeDueDateAdded, ActionTypeDueDateRemoved, ActionTypeDueDateChanged, ActionTypeTaskAssigned, ActionTypeTaskMoved, ActionTypeTaskArchived, ActionTypeTaskAttachmentUploaded, ActionTypeCommentMentioned, ActionTypeCommentOther:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@ -860,6 +915,51 @@ func (e MyTasksStatus) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type NotificationFilter string
|
||||
|
||||
const (
|
||||
NotificationFilterAll NotificationFilter = "ALL"
|
||||
NotificationFilterUnread NotificationFilter = "UNREAD"
|
||||
NotificationFilterAssigned NotificationFilter = "ASSIGNED"
|
||||
NotificationFilterMentioned NotificationFilter = "MENTIONED"
|
||||
)
|
||||
|
||||
var AllNotificationFilter = []NotificationFilter{
|
||||
NotificationFilterAll,
|
||||
NotificationFilterUnread,
|
||||
NotificationFilterAssigned,
|
||||
NotificationFilterMentioned,
|
||||
}
|
||||
|
||||
func (e NotificationFilter) IsValid() bool {
|
||||
switch e {
|
||||
case NotificationFilterAll, NotificationFilterUnread, NotificationFilterAssigned, NotificationFilterMentioned:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e NotificationFilter) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *NotificationFilter) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = NotificationFilter(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid NotificationFilter", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e NotificationFilter) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type ObjectType string
|
||||
|
||||
const (
|
||||
|
@ -6,32 +6,88 @@ package graph
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/jordanknott/taskcafe/internal/logger"
|
||||
"github.com/jordanknott/taskcafe/internal/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (r *mutationResolver) NotificationToggleRead(ctx context.Context, input NotificationToggleReadInput) (*Notified, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
return &Notified{}, errors.New("unknown user ID")
|
||||
}
|
||||
notified, err := r.Repository.GetNotifiedByID(ctx, input.NotifiedID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting notified by ID")
|
||||
return &Notified{}, err
|
||||
}
|
||||
readAt := time.Now().UTC()
|
||||
read := true
|
||||
if notified.Read {
|
||||
read = false
|
||||
err = r.Repository.MarkNotificationAsRead(ctx, db.MarkNotificationAsReadParams{
|
||||
UserID: userID,
|
||||
NotifiedID: input.NotifiedID,
|
||||
Read: false,
|
||||
ReadAt: sql.NullTime{
|
||||
Valid: false,
|
||||
Time: time.Time{},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
err = r.Repository.MarkNotificationAsRead(ctx, db.MarkNotificationAsReadParams{
|
||||
UserID: userID,
|
||||
Read: true,
|
||||
NotifiedID: input.NotifiedID,
|
||||
ReadAt: sql.NullTime{
|
||||
Valid: true,
|
||||
Time: readAt,
|
||||
},
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while marking notification as read")
|
||||
return &Notified{}, err
|
||||
}
|
||||
|
||||
return &Notified{
|
||||
ID: notified.NotifiedID,
|
||||
Read: read,
|
||||
ReadAt: &readAt,
|
||||
Notification: &db.Notification{
|
||||
NotificationID: notified.NotificationID,
|
||||
CausedBy: notified.CausedBy,
|
||||
ActionType: notified.ActionType,
|
||||
Data: notified.Data,
|
||||
CreatedOn: notified.CreatedOn,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
|
||||
return obj.NotificationID, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) ActionType(ctx context.Context, obj *db.Notification) (ActionType, error) {
|
||||
return ActionTypeTaskMemberAdded, nil // TODO
|
||||
actionType := ActionType(obj.ActionType)
|
||||
if !actionType.IsValid() {
|
||||
log.WithField("ActionType", obj.ActionType).Error("ActionType is invalid")
|
||||
return actionType, errors.New("ActionType is invalid")
|
||||
}
|
||||
return ActionType(obj.ActionType), nil // TODO
|
||||
}
|
||||
|
||||
func (r *notificationResolver) CausedBy(ctx context.Context, obj *db.Notification) (*NotificationCausedBy, error) {
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, obj.CausedBy)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return &NotificationCausedBy{
|
||||
Fullname: "Unknown user",
|
||||
Username: "unknown",
|
||||
ID: obj.CausedBy,
|
||||
}, nil
|
||||
return nil, nil
|
||||
}
|
||||
log.WithError(err).Error("error while resolving Notification.CausedBy")
|
||||
return &NotificationCausedBy{}, err
|
||||
@ -44,7 +100,16 @@ func (r *notificationResolver) CausedBy(ctx context.Context, obj *db.Notificatio
|
||||
}
|
||||
|
||||
func (r *notificationResolver) Data(ctx context.Context, obj *db.Notification) ([]NotificationData, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
notifiedData := NotifiedData{}
|
||||
err := json.Unmarshal(obj.Data, ¬ifiedData)
|
||||
if err != nil {
|
||||
return []NotificationData{}, err
|
||||
}
|
||||
data := []NotificationData{}
|
||||
for key, value := range notifiedData.Data {
|
||||
data = append(data, NotificationData{Key: key, Value: value})
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) CreatedAt(ctx context.Context, obj *db.Notification) (*time.Time, error) {
|
||||
@ -86,8 +151,183 @@ func (r *queryResolver) Notifications(ctx context.Context) ([]Notified, error) {
|
||||
return userNotifications, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Notified(ctx context.Context, input NotifiedInput) (*NotifiedResult, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
return &NotifiedResult{}, errors.New("userID is not found")
|
||||
}
|
||||
log.WithField("userID", userID).Info("fetching notified")
|
||||
if input.Cursor != nil {
|
||||
t, id, err := utils.DecodeCursor(*input.Cursor)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error decoding cursor")
|
||||
return &NotifiedResult{}, err
|
||||
}
|
||||
n, err := r.Repository.GetNotificationsForUserIDCursor(ctx, db.GetNotificationsForUserIDCursorParams{
|
||||
CreatedOn: t,
|
||||
NotificationID: id,
|
||||
LimitRows: int32(input.Limit + 1),
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error decoding fetching notifications")
|
||||
return &NotifiedResult{}, err
|
||||
}
|
||||
hasNextPage := false
|
||||
log.WithFields(log.Fields{
|
||||
"nLen": len(n),
|
||||
"cursorTime": t,
|
||||
"cursorId": id,
|
||||
"limit": input.Limit,
|
||||
}).Info("fetched notified")
|
||||
endCursor := n[len(n)-1]
|
||||
if len(n) == input.Limit+1 {
|
||||
hasNextPage = true
|
||||
n = n[:len(n)-1]
|
||||
endCursor = n[len(n)-1]
|
||||
}
|
||||
userNotifications := []Notified{}
|
||||
for _, notified := range n {
|
||||
var readAt *time.Time
|
||||
if notified.ReadAt.Valid {
|
||||
readAt = ¬ified.ReadAt.Time
|
||||
}
|
||||
n := Notified{
|
||||
ID: notified.NotifiedID,
|
||||
Read: notified.Read,
|
||||
ReadAt: readAt,
|
||||
Notification: &db.Notification{
|
||||
NotificationID: notified.NotificationID,
|
||||
CausedBy: notified.CausedBy,
|
||||
ActionType: notified.ActionType,
|
||||
Data: notified.Data,
|
||||
CreatedOn: notified.CreatedOn,
|
||||
},
|
||||
}
|
||||
userNotifications = append(userNotifications, n)
|
||||
}
|
||||
pageInfo := &PageInfo{
|
||||
HasNextPage: hasNextPage,
|
||||
EndCursor: utils.EncodeCursor(endCursor.CreatedOn, endCursor.NotificationID),
|
||||
}
|
||||
log.WithField("pageInfo", pageInfo).Info("created page info")
|
||||
return &NotifiedResult{
|
||||
TotalCount: len(n) - 1,
|
||||
PageInfo: pageInfo,
|
||||
Notified: userNotifications,
|
||||
}, nil
|
||||
}
|
||||
enableRead := false
|
||||
enableActionType := false
|
||||
actionTypes := []string{}
|
||||
switch input.Filter {
|
||||
case NotificationFilterUnread:
|
||||
enableRead = true
|
||||
break
|
||||
case NotificationFilterMentioned:
|
||||
enableActionType = true
|
||||
actionTypes = []string{"COMMENT_MENTIONED"}
|
||||
break
|
||||
case NotificationFilterAssigned:
|
||||
enableActionType = true
|
||||
actionTypes = []string{"TASK_ASSIGNED"}
|
||||
break
|
||||
}
|
||||
n, err := r.Repository.GetNotificationsForUserIDPaged(ctx, db.GetNotificationsForUserIDPagedParams{
|
||||
LimitRows: int32(input.Limit + 1),
|
||||
EnableUnread: enableRead,
|
||||
EnableActionType: enableActionType,
|
||||
ActionType: actionTypes,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error decoding fetching notifications")
|
||||
return &NotifiedResult{}, err
|
||||
}
|
||||
hasNextPage := false
|
||||
log.WithFields(log.Fields{
|
||||
"nLen": len(n),
|
||||
"limit": input.Limit,
|
||||
}).Info("fetched notified")
|
||||
endCursor := n[len(n)-1]
|
||||
if len(n) == input.Limit+1 {
|
||||
hasNextPage = true
|
||||
n = n[:len(n)-1]
|
||||
endCursor = n[len(n)-1]
|
||||
}
|
||||
userNotifications := []Notified{}
|
||||
for _, notified := range n {
|
||||
var readAt *time.Time
|
||||
if notified.ReadAt.Valid {
|
||||
readAt = ¬ified.ReadAt.Time
|
||||
}
|
||||
n := Notified{
|
||||
ID: notified.NotifiedID,
|
||||
Read: notified.Read,
|
||||
ReadAt: readAt,
|
||||
Notification: &db.Notification{
|
||||
NotificationID: notified.NotificationID,
|
||||
CausedBy: notified.CausedBy,
|
||||
ActionType: notified.ActionType,
|
||||
Data: notified.Data,
|
||||
CreatedOn: notified.CreatedOn,
|
||||
},
|
||||
}
|
||||
userNotifications = append(userNotifications, n)
|
||||
}
|
||||
pageInfo := &PageInfo{
|
||||
HasNextPage: hasNextPage,
|
||||
EndCursor: utils.EncodeCursor(endCursor.CreatedOn, endCursor.NotificationID),
|
||||
}
|
||||
log.WithField("pageInfo", pageInfo).Info("created page info")
|
||||
return &NotifiedResult{
|
||||
TotalCount: len(n),
|
||||
PageInfo: pageInfo,
|
||||
Notified: userNotifications,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) HasUnreadNotifications(ctx context.Context) (*HasUnreadNotificationsResult, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
return &HasUnreadNotificationsResult{}, errors.New("userID is missing")
|
||||
}
|
||||
unread, err := r.Repository.HasUnreadNotification(ctx, userID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while fetching unread notifications")
|
||||
return &HasUnreadNotificationsResult{}, err
|
||||
}
|
||||
return &HasUnreadNotificationsResult{
|
||||
Unread: unread,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *subscriptionResolver) NotificationAdded(ctx context.Context) (<-chan *Notified, error) {
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
notified := make(chan *Notified, 1)
|
||||
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
return notified, errors.New("userID is not found")
|
||||
}
|
||||
|
||||
id := uuid.New().String()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
r.Notifications.Mu.Lock()
|
||||
if _, ok := r.Notifications.Subscribers[userID.String()]; ok {
|
||||
delete(r.Notifications.Subscribers[userID.String()], id)
|
||||
}
|
||||
r.Notifications.Mu.Unlock()
|
||||
}()
|
||||
|
||||
r.Notifications.Mu.Lock()
|
||||
if _, ok := r.Notifications.Subscribers[userID.String()]; !ok {
|
||||
r.Notifications.Subscribers[userID.String()] = make(map[string]chan *Notified)
|
||||
}
|
||||
log.WithField("userID", userID).WithField("id", id).Info("adding new channel")
|
||||
r.Notifications.Subscribers[userID.String()][id] = notified
|
||||
r.Notifications.Mu.Unlock()
|
||||
return notified, nil
|
||||
}
|
||||
|
||||
// Notification returns NotificationResolver implementation.
|
||||
|
@ -10,9 +10,14 @@ import (
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
)
|
||||
|
||||
type NotificationObservers struct {
|
||||
Subscribers map[string]map[string]chan *Notified
|
||||
Mu sync.Mutex
|
||||
}
|
||||
|
||||
// Resolver handles resolving GraphQL queries & mutations
|
||||
type Resolver struct {
|
||||
Repository db.Repository
|
||||
AppConfig config.AppConfig
|
||||
mu sync.Mutex
|
||||
Repository db.Repository
|
||||
AppConfig config.AppConfig
|
||||
Notifications NotificationObservers
|
||||
}
|
||||
|
@ -62,7 +62,6 @@ func (r *queryResolver) FindUser(ctx context.Context, input FindUser) (*db.UserA
|
||||
}
|
||||
|
||||
func (r *queryResolver) FindProject(ctx context.Context, input FindProject) (*db.Project, error) {
|
||||
logger.New(ctx).Info("finding project user")
|
||||
_, isLoggedIn := GetUser(ctx)
|
||||
if !isLoggedIn {
|
||||
isPublic, _ := IsProjectPublic(ctx, r.Repository, input.ProjectID)
|
||||
|
@ -4,10 +4,60 @@ extend type Subscription {
|
||||
|
||||
extend type Query {
|
||||
notifications: [Notified!]!
|
||||
notified(input: NotifiedInput!): NotifiedResult!
|
||||
hasUnreadNotifications: HasUnreadNotificationsResult!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
notificationToggleRead(input: NotificationToggleReadInput!): Notified!
|
||||
}
|
||||
|
||||
type HasUnreadNotificationsResult {
|
||||
unread: Boolean!
|
||||
}
|
||||
input NotificationToggleReadInput {
|
||||
notifiedID: UUID!
|
||||
}
|
||||
|
||||
input NotifiedInput {
|
||||
limit: Int!
|
||||
cursor: String
|
||||
filter: NotificationFilter!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
endCursor: String!
|
||||
hasNextPage: Boolean!
|
||||
}
|
||||
|
||||
type NotifiedResult {
|
||||
totalCount: Int!
|
||||
notified: [Notified!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
TASK_MEMBER_ADDED
|
||||
TEAM_ADDED
|
||||
TEAM_REMOVED
|
||||
PROJECT_ADDED
|
||||
PROJECT_REMOVED
|
||||
PROJECT_ARCHIVED
|
||||
DUE_DATE_ADDED
|
||||
DUE_DATE_REMOVED
|
||||
DUE_DATE_CHANGED
|
||||
TASK_ASSIGNED
|
||||
TASK_MOVED
|
||||
TASK_ARCHIVED
|
||||
TASK_ATTACHMENT_UPLOADED
|
||||
COMMENT_MENTIONED
|
||||
COMMENT_OTHER
|
||||
}
|
||||
|
||||
enum NotificationFilter {
|
||||
ALL
|
||||
UNREAD
|
||||
ASSIGNED
|
||||
MENTIONED
|
||||
}
|
||||
|
||||
type NotificationData {
|
||||
@ -24,7 +74,7 @@ type NotificationCausedBy {
|
||||
type Notification {
|
||||
id: ID!
|
||||
actionType: ActionType!
|
||||
causedBy: NotificationCausedBy!
|
||||
causedBy: NotificationCausedBy
|
||||
data: [NotificationData!]!
|
||||
createdAt: Time!
|
||||
}
|
||||
|
@ -4,10 +4,60 @@ extend type Subscription {
|
||||
|
||||
extend type Query {
|
||||
notifications: [Notified!]!
|
||||
notified(input: NotifiedInput!): NotifiedResult!
|
||||
hasUnreadNotifications: HasUnreadNotificationsResult!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
notificationToggleRead(input: NotificationToggleReadInput!): Notified!
|
||||
}
|
||||
|
||||
type HasUnreadNotificationsResult {
|
||||
unread: Boolean!
|
||||
}
|
||||
input NotificationToggleReadInput {
|
||||
notifiedID: UUID!
|
||||
}
|
||||
|
||||
input NotifiedInput {
|
||||
limit: Int!
|
||||
cursor: String
|
||||
filter: NotificationFilter!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
endCursor: String!
|
||||
hasNextPage: Boolean!
|
||||
}
|
||||
|
||||
type NotifiedResult {
|
||||
totalCount: Int!
|
||||
notified: [Notified!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
TASK_MEMBER_ADDED
|
||||
TEAM_ADDED
|
||||
TEAM_REMOVED
|
||||
PROJECT_ADDED
|
||||
PROJECT_REMOVED
|
||||
PROJECT_ARCHIVED
|
||||
DUE_DATE_ADDED
|
||||
DUE_DATE_REMOVED
|
||||
DUE_DATE_CHANGED
|
||||
TASK_ASSIGNED
|
||||
TASK_MOVED
|
||||
TASK_ARCHIVED
|
||||
TASK_ATTACHMENT_UPLOADED
|
||||
COMMENT_MENTIONED
|
||||
COMMENT_OTHER
|
||||
}
|
||||
|
||||
enum NotificationFilter {
|
||||
ALL
|
||||
UNREAD
|
||||
ASSIGNED
|
||||
MENTIONED
|
||||
}
|
||||
|
||||
type NotificationData {
|
||||
@ -24,7 +74,7 @@ type NotificationCausedBy {
|
||||
type Notification {
|
||||
id: ID!
|
||||
actionType: ActionType!
|
||||
causedBy: NotificationCausedBy!
|
||||
causedBy: NotificationCausedBy
|
||||
data: [NotificationData!]!
|
||||
createdAt: Time!
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ type Task {
|
||||
name: String!
|
||||
position: Float!
|
||||
description: String
|
||||
watched: Boolean!
|
||||
dueDate: Time
|
||||
hasTime: Boolean!
|
||||
complete: Boolean!
|
||||
@ -352,6 +353,8 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
updateTaskDueDate(input: UpdateTaskDueDate!):
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
toggleTaskWatch(input: ToggleTaskWatch!):
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
|
||||
assignTask(input: AssignTaskInput):
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
@ -359,6 +362,10 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
}
|
||||
|
||||
input ToggleTaskWatch {
|
||||
taskID: UUID!
|
||||
}
|
||||
|
||||
input NewTask {
|
||||
taskGroupID: UUID!
|
||||
name: String!
|
||||
|
@ -27,6 +27,7 @@ type Task {
|
||||
name: String!
|
||||
position: Float!
|
||||
description: String
|
||||
watched: Boolean!
|
||||
dueDate: Time
|
||||
hasTime: Boolean!
|
||||
complete: Boolean!
|
||||
|
@ -14,6 +14,8 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
updateTaskDueDate(input: UpdateTaskDueDate!):
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
toggleTaskWatch(input: ToggleTaskWatch!):
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
|
||||
assignTask(input: AssignTaskInput):
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
@ -21,6 +23,10 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN, MEMBER], level: PROJECT, type: TASK)
|
||||
}
|
||||
|
||||
input ToggleTaskWatch {
|
||||
taskID: UUID!
|
||||
}
|
||||
|
||||
input NewTask {
|
||||
taskGroupID: UUID!
|
||||
name: String!
|
||||
|
@ -543,6 +543,45 @@ func (r *mutationResolver) UpdateTaskDueDate(ctx context.Context, input UpdateTa
|
||||
return &task, err
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ToggleTaskWatch(ctx context.Context, input ToggleTaskWatch) (*db.Task, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
log.Error("user ID is missing")
|
||||
return &db.Task{}, errors.New("user ID is unknown")
|
||||
}
|
||||
_, err := r.Repository.GetTaskWatcher(ctx, db.GetTaskWatcherParams{UserID: userID, TaskID: input.TaskID})
|
||||
|
||||
isWatching := true
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
log.WithError(err).Error("error while getting task watcher")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
isWatching = false
|
||||
}
|
||||
|
||||
if isWatching {
|
||||
err := r.Repository.DeleteTaskWatcher(ctx, db.DeleteTaskWatcherParams{UserID: userID, TaskID: input.TaskID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting deleteing task watcher")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
} else {
|
||||
now := time.Now().UTC()
|
||||
_, err := r.Repository.CreateTaskWatcher(ctx, db.CreateTaskWatcherParams{UserID: userID, TaskID: input.TaskID, WatchedAt: now})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating task watcher")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
}
|
||||
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting task by id")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInput) (*db.Task, error) {
|
||||
assignedDate := time.Now().UTC()
|
||||
assignedTask, err := r.Repository.CreateTaskAssigned(ctx, db.CreateTaskAssignedParams{input.TaskID, input.UserID, assignedDate})
|
||||
@ -552,20 +591,80 @@ func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInpu
|
||||
"assignedTaskID": assignedTask.TaskAssignedID,
|
||||
}).Info("assigned task")
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating task assigned")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
// r.NotificationQueue.TaskMemberWasAdded(assignedTask.TaskID, userID, assignedTask.UserID)
|
||||
_, err = r.Repository.GetTaskWatcher(ctx, db.GetTaskWatcherParams{UserID: assignedTask.UserID, TaskID: assignedTask.TaskID})
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
log.WithError(err).Error("error while fetching task watcher")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
_, err = r.Repository.CreateTaskWatcher(ctx, db.CreateTaskWatcherParams{UserID: assignedTask.UserID, TaskID: assignedTask.TaskID, WatchedAt: assignedDate})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating task assigned task watcher")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
}
|
||||
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
log.Error("error getting user ID")
|
||||
return &db.Task{}, errors.New("UserID is missing")
|
||||
}
|
||||
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||
return &task, err
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting task by ID")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
if userID != assignedTask.UserID {
|
||||
causedBy, err := r.Repository.GetUserAccountByID(ctx, userID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting user account in assign task")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
project, err := r.Repository.GetProjectInfoForTask(ctx, input.TaskID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting project in assign task")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
err = r.CreateNotification(ctx, CreateNotificationParams{
|
||||
ActionType: ActionTypeTaskAssigned,
|
||||
CausedBy: userID,
|
||||
NotifiedList: []uuid.UUID{assignedTask.UserID},
|
||||
Data: map[string]string{
|
||||
"CausedByUsername": causedBy.Username,
|
||||
"CausedByFullName": causedBy.FullName,
|
||||
"TaskID": assignedTask.TaskID.String(),
|
||||
"TaskName": task.Name,
|
||||
"ProjectID": project.ProjectID.String(),
|
||||
"ProjectName": project.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return &task, err
|
||||
}
|
||||
|
||||
// r.NotificationQueue.TaskMemberWasAdded(assignedTask.TaskID, userID, assignedTask.UserID)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) UnassignTask(ctx context.Context, input *UnassignTaskInput) (*db.Task, error) {
|
||||
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while getting task by ID")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
_, err = r.Repository.DeleteTaskAssignedByID(ctx, db.DeleteTaskAssignedByIDParams{input.TaskID, input.UserID})
|
||||
log.WithFields(log.Fields{"UserID": input.UserID, "TaskID": input.TaskID}).Info("deleting task assignment")
|
||||
_, err = r.Repository.DeleteTaskAssignedByID(ctx, db.DeleteTaskAssignedByIDParams{TaskID: input.TaskID, UserID: input.UserID})
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.WithError(err).Error("error while deleting task by ID")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
err = r.Repository.DeleteTaskWatcher(ctx, db.DeleteTaskWatcherParams{UserID: input.UserID, TaskID: input.TaskID})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while creating task assigned task watcher")
|
||||
return &db.Task{}, err
|
||||
}
|
||||
return &task, nil
|
||||
@ -591,6 +690,23 @@ func (r *taskResolver) Description(ctx context.Context, obj *db.Task) (*string,
|
||||
return &task.Description.String, nil
|
||||
}
|
||||
|
||||
func (r *taskResolver) Watched(ctx context.Context, obj *db.Task) (bool, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
log.Error("user ID is missing")
|
||||
return false, errors.New("user ID is unknown")
|
||||
}
|
||||
_, err := r.Repository.GetTaskWatcher(ctx, db.GetTaskWatcherParams{UserID: userID, TaskID: obj.TaskID})
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
log.WithError(err).Error("error while getting task watcher")
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *taskResolver) DueDate(ctx context.Context, obj *db.Task) (*time.Time, error) {
|
||||
if obj.DueDate.Valid {
|
||||
return &obj.DueDate.Time, nil
|
||||
|
@ -18,30 +18,36 @@ type AuthenticationMiddleware struct {
|
||||
// Middleware returns the middleware handler
|
||||
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Info("middleware")
|
||||
requestID := uuid.New()
|
||||
foundToken := true
|
||||
tokenRaw := ""
|
||||
c, err := r.Cookie("authToken")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
foundToken = false
|
||||
}
|
||||
}
|
||||
if !foundToken {
|
||||
token := r.Header.Get("Authorization")
|
||||
if token != "" {
|
||||
tokenRaw = token
|
||||
}
|
||||
|
||||
token := r.Header.Get("Authorization")
|
||||
if token != "" {
|
||||
tokenRaw = token
|
||||
} else {
|
||||
foundToken = false
|
||||
}
|
||||
|
||||
if !foundToken {
|
||||
c, err := r.Cookie("authToken")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
log.WithError(err).Error("error while fetching authToken")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
log.WithError(err).Error("error while fetching authToken")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tokenRaw = c.Value
|
||||
}
|
||||
authTokenID, err := uuid.Parse(tokenRaw)
|
||||
log.Info("checking if logged in")
|
||||
ctx := r.Context()
|
||||
if err == nil {
|
||||
token, err := m.repo.GetAuthTokenByID(r.Context(), authTokenID)
|
||||
if err == nil {
|
||||
log.WithField("tokenID", authTokenID).WithField("userID", token.UserID).Info("setting auth token")
|
||||
ctx = context.WithValue(ctx, utils.UserIDKey, token.UserID)
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ func NewRouter(dbConnection *sqlx.DB, job *machinery.Server, appConfig config.Ap
|
||||
r.Group(func(mux chi.Router) {
|
||||
mux.Use(auth.Middleware)
|
||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||
mux.Handle("/graphql", graph.NewHandler(*repository, appConfig))
|
||||
mux.Mount("/graphql", graph.NewHandler(*repository, appConfig))
|
||||
})
|
||||
|
||||
frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}
|
||||
|
36
internal/utils/cursor.go
Normal file
36
internal/utils/cursor.go
Normal file
@ -0,0 +1,36 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func DecodeCursor(encodedCursor string) (res time.Time, id uuid.UUID, err error) {
|
||||
byt, err := base64.StdEncoding.DecodeString(encodedCursor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
arrStr := strings.Split(string(byt), ",")
|
||||
if len(arrStr) != 2 {
|
||||
err = errors.New("cursor is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
res, err = time.Parse(time.RFC3339Nano, arrStr[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id = uuid.MustParse(arrStr[1])
|
||||
return
|
||||
}
|
||||
|
||||
func EncodeCursor(t time.Time, id uuid.UUID) string {
|
||||
key := fmt.Sprintf("%s,%s", t.Format(time.RFC3339Nano), id.String())
|
||||
return base64.StdEncoding.EncodeToString([]byte(key))
|
||||
}
|
Reference in New Issue
Block a user