feat: add notification UI
showPopup was also refactored to be better
This commit is contained in:
@ -67,6 +67,6 @@ func initConfig() {
|
||||
// Execute the root cobra command
|
||||
func Execute() {
|
||||
rootCmd.SetVersionTemplate(versionTemplate)
|
||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd())
|
||||
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd(), newWorkerCmd())
|
||||
rootCmd.Execute()
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ func newWebCmd() *cobra.Command {
|
||||
db.SetMaxIdleConns(25)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
defer db.Close()
|
||||
|
||||
if viper.GetBool("migrate") {
|
||||
log.Info("running auto schema migrations")
|
||||
if err = runMigration(db); err != nil {
|
||||
@ -74,6 +75,9 @@ func newWebCmd() *cobra.Command {
|
||||
viper.SetDefault("database.name", "taskcafe")
|
||||
viper.SetDefault("database.user", "taskcafe")
|
||||
viper.SetDefault("database.password", "taskcafe_test")
|
||||
|
||||
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
||||
return cc
|
||||
}
|
||||
|
||||
|
83
internal/commands/worker.go
Normal file
83
internal/commands/worker.go
Normal file
@ -0,0 +1,83 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"time"
|
||||
|
||||
"github.com/RichardKnop/machinery/v1"
|
||||
"github.com/RichardKnop/machinery/v1/config"
|
||||
queueLog "github.com/RichardKnop/machinery/v1/log"
|
||||
"github.com/jmoiron/sqlx"
|
||||
repo "github.com/jordanknott/taskcafe/internal/db"
|
||||
"github.com/jordanknott/taskcafe/internal/notification"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func newWorkerCmd() *cobra.Command {
|
||||
cc := &cobra.Command{
|
||||
Use: "worker",
|
||||
Short: "Run the task queue worker",
|
||||
Long: "Run the task queue worker",
|
||||
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)
|
||||
|
||||
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
|
||||
viper.GetString("database.user"),
|
||||
viper.GetString("database.password"),
|
||||
viper.GetString("database.host"),
|
||||
viper.GetString("database.name"),
|
||||
)
|
||||
db, err := sqlx.Connect("postgres", connection)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
db.SetMaxOpenConns(25)
|
||||
db.SetMaxIdleConns(25)
|
||||
db.SetConnMaxLifetime(5 * time.Minute)
|
||||
defer db.Close()
|
||||
|
||||
var cnf = &config.Config{
|
||||
Broker: viper.GetString("queue.broker"),
|
||||
DefaultQueue: "machinery_tasks",
|
||||
ResultBackend: viper.GetString("queue.store"),
|
||||
AMQP: &config.AMQPConfig{
|
||||
Exchange: "machinery_exchange",
|
||||
ExchangeType: "direct",
|
||||
BindingKey: "machinery_task",
|
||||
},
|
||||
}
|
||||
|
||||
log.Info("starting task queue server instance")
|
||||
server, err := machinery.NewServer(cnf)
|
||||
if err != nil {
|
||||
// do something with the error
|
||||
}
|
||||
queueLog.Set(¬ification.MachineryLogger{})
|
||||
repo := *repo.NewRepository(db)
|
||||
notification.RegisterTasks(server, repo)
|
||||
|
||||
worker := server.NewWorker("taskcafe_worker", 10)
|
||||
log.Info("starting task queue worker")
|
||||
err = worker.Launch()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error while launching ")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
viper.SetDefault("database.host", "127.0.0.1")
|
||||
viper.SetDefault("database.name", "taskcafe")
|
||||
viper.SetDefault("database.user", "taskcafe")
|
||||
viper.SetDefault("database.password", "taskcafe_test")
|
||||
|
||||
viper.SetDefault("queue.broker", "amqp://guest:guest@localhost:5672/")
|
||||
viper.SetDefault("queue.store", "memcache://localhost:11211")
|
||||
return cc
|
||||
}
|
@ -16,6 +16,22 @@ type LabelColor struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
NotificationObjectID uuid.UUID `json:"notification_object_id"`
|
||||
NotifierID uuid.UUID `json:"notifier_id"`
|
||||
Read bool `json:"read"`
|
||||
}
|
||||
|
||||
type NotificationObject struct {
|
||||
NotificationObjectID uuid.UUID `json:"notification_object_id"`
|
||||
EntityID uuid.UUID `json:"entity_id"`
|
||||
ActionType int32 `json:"action_type"`
|
||||
ActorID uuid.UUID `json:"actor_id"`
|
||||
EntityType int32 `json:"entity_type"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
177
internal/db/notification.sql.go
Normal file
177
internal/db/notification.sql.go
Normal file
@ -0,0 +1,177 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// source: notification.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const createNotification = `-- name: CreateNotification :one
|
||||
INSERT INTO notification(notification_object_id, notifier_id)
|
||||
VALUES ($1, $2) RETURNING notification_id, notification_object_id, notifier_id, read
|
||||
`
|
||||
|
||||
type CreateNotificationParams struct {
|
||||
NotificationObjectID uuid.UUID `json:"notification_object_id"`
|
||||
NotifierID uuid.UUID `json:"notifier_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error) {
|
||||
row := q.db.QueryRowContext(ctx, createNotification, arg.NotificationObjectID, arg.NotifierID)
|
||||
var i Notification
|
||||
err := row.Scan(
|
||||
&i.NotificationID,
|
||||
&i.NotificationObjectID,
|
||||
&i.NotifierID,
|
||||
&i.Read,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const createNotificationObject = `-- name: CreateNotificationObject :one
|
||||
INSERT INTO notification_object(entity_type, action_type, entity_id, created_on, actor_id)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING notification_object_id, entity_id, action_type, actor_id, entity_type, created_on
|
||||
`
|
||||
|
||||
type CreateNotificationObjectParams struct {
|
||||
EntityType int32 `json:"entity_type"`
|
||||
ActionType int32 `json:"action_type"`
|
||||
EntityID uuid.UUID `json:"entity_id"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
ActorID uuid.UUID `json:"actor_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error) {
|
||||
row := q.db.QueryRowContext(ctx, createNotificationObject,
|
||||
arg.EntityType,
|
||||
arg.ActionType,
|
||||
arg.EntityID,
|
||||
arg.CreatedOn,
|
||||
arg.ActorID,
|
||||
)
|
||||
var i NotificationObject
|
||||
err := row.Scan(
|
||||
&i.NotificationObjectID,
|
||||
&i.EntityID,
|
||||
&i.ActionType,
|
||||
&i.ActorID,
|
||||
&i.EntityType,
|
||||
&i.CreatedOn,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAllNotificationsForUserID = `-- name: GetAllNotificationsForUserID :many
|
||||
SELECT n.notification_id, n.notification_object_id, n.notifier_id, n.read FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notifier_id = $1 ORDER BY no.created_on DESC
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getAllNotificationsForUserID, notifierID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Notification
|
||||
for rows.Next() {
|
||||
var i Notification
|
||||
if err := rows.Scan(
|
||||
&i.NotificationID,
|
||||
&i.NotificationObjectID,
|
||||
&i.NotifierID,
|
||||
&i.Read,
|
||||
); 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 getEntityForNotificationID = `-- name: GetEntityForNotificationID :one
|
||||
SELECT no.created_on, no.entity_id, no.entity_type, no.action_type, no.actor_id FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notification_id = $1
|
||||
`
|
||||
|
||||
type GetEntityForNotificationIDRow struct {
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
EntityID uuid.UUID `json:"entity_id"`
|
||||
EntityType int32 `json:"entity_type"`
|
||||
ActionType int32 `json:"action_type"`
|
||||
ActorID uuid.UUID `json:"actor_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEntityForNotificationID, notificationID)
|
||||
var i GetEntityForNotificationIDRow
|
||||
err := row.Scan(
|
||||
&i.CreatedOn,
|
||||
&i.EntityID,
|
||||
&i.EntityType,
|
||||
&i.ActionType,
|
||||
&i.ActorID,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getEntityIDForNotificationID = `-- name: GetEntityIDForNotificationID :one
|
||||
SELECT no.entity_id FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notification_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error) {
|
||||
row := q.db.QueryRowContext(ctx, getEntityIDForNotificationID, notificationID)
|
||||
var entity_id uuid.UUID
|
||||
err := row.Scan(&entity_id)
|
||||
return entity_id, err
|
||||
}
|
||||
|
||||
const getNotificationForNotificationID = `-- name: GetNotificationForNotificationID :one
|
||||
SELECT n.notification_id, n.notification_object_id, n.notifier_id, n.read, no.notification_object_id, no.entity_id, no.action_type, no.actor_id, no.entity_type, no.created_on FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notification_id = $1
|
||||
`
|
||||
|
||||
type GetNotificationForNotificationIDRow struct {
|
||||
NotificationID uuid.UUID `json:"notification_id"`
|
||||
NotificationObjectID uuid.UUID `json:"notification_object_id"`
|
||||
NotifierID uuid.UUID `json:"notifier_id"`
|
||||
Read bool `json:"read"`
|
||||
NotificationObjectID_2 uuid.UUID `json:"notification_object_id_2"`
|
||||
EntityID uuid.UUID `json:"entity_id"`
|
||||
ActionType int32 `json:"action_type"`
|
||||
ActorID uuid.UUID `json:"actor_id"`
|
||||
EntityType int32 `json:"entity_type"`
|
||||
CreatedOn time.Time `json:"created_on"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error) {
|
||||
row := q.db.QueryRowContext(ctx, getNotificationForNotificationID, notificationID)
|
||||
var i GetNotificationForNotificationIDRow
|
||||
err := row.Scan(
|
||||
&i.NotificationID,
|
||||
&i.NotificationObjectID,
|
||||
&i.NotifierID,
|
||||
&i.Read,
|
||||
&i.NotificationObjectID_2,
|
||||
&i.EntityID,
|
||||
&i.ActionType,
|
||||
&i.ActorID,
|
||||
&i.EntityType,
|
||||
&i.CreatedOn,
|
||||
)
|
||||
return i, err
|
||||
}
|
@ -10,6 +10,8 @@ import (
|
||||
|
||||
type Querier interface {
|
||||
CreateLabelColor(ctx context.Context, arg CreateLabelColorParams) (LabelColor, error)
|
||||
CreateNotification(ctx context.Context, arg CreateNotificationParams) (Notification, error)
|
||||
CreateNotificationObject(ctx context.Context, arg CreateNotificationObjectParams) (NotificationObject, error)
|
||||
CreateOrganization(ctx context.Context, arg CreateOrganizationParams) (Organization, error)
|
||||
CreateProject(ctx context.Context, arg CreateProjectParams) (Project, error)
|
||||
CreateProjectLabel(ctx context.Context, arg CreateProjectLabelParams) (ProjectLabel, error)
|
||||
@ -42,6 +44,7 @@ type Querier interface {
|
||||
DeleteTeamByID(ctx context.Context, teamID uuid.UUID) error
|
||||
DeleteTeamMember(ctx context.Context, arg DeleteTeamMemberParams) error
|
||||
DeleteUserAccountByID(ctx context.Context, userID uuid.UUID) error
|
||||
GetAllNotificationsForUserID(ctx context.Context, notifierID uuid.UUID) ([]Notification, error)
|
||||
GetAllOrganizations(ctx context.Context) ([]Organization, error)
|
||||
GetAllProjects(ctx context.Context) ([]Project, error)
|
||||
GetAllProjectsForTeam(ctx context.Context, teamID uuid.UUID) ([]Project, error)
|
||||
@ -51,10 +54,13 @@ type Querier interface {
|
||||
GetAllUserAccounts(ctx context.Context) ([]UserAccount, error)
|
||||
GetAllVisibleProjectsForUserID(ctx context.Context, userID uuid.UUID) ([]Project, error)
|
||||
GetAssignedMembersForTask(ctx context.Context, taskID uuid.UUID) ([]TaskAssigned, error)
|
||||
GetEntityForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetEntityForNotificationIDRow, error)
|
||||
GetEntityIDForNotificationID(ctx context.Context, notificationID uuid.UUID) (uuid.UUID, error)
|
||||
GetLabelColorByID(ctx context.Context, labelColorID uuid.UUID) (LabelColor, error)
|
||||
GetLabelColors(ctx context.Context) ([]LabelColor, error)
|
||||
GetMemberProjectIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||
GetMemberTeamIDsForUserID(ctx context.Context, userID uuid.UUID) ([]uuid.UUID, error)
|
||||
GetNotificationForNotificationID(ctx context.Context, notificationID uuid.UUID) (GetNotificationForNotificationIDRow, error)
|
||||
GetProjectByID(ctx context.Context, projectID uuid.UUID) (Project, error)
|
||||
GetProjectIDForTask(ctx context.Context, taskID uuid.UUID) (uuid.UUID, error)
|
||||
GetProjectLabelByID(ctx context.Context, projectLabelID uuid.UUID) (ProjectLabel, error)
|
||||
|
28
internal/db/query/notification.sql
Normal file
28
internal/db/query/notification.sql
Normal file
@ -0,0 +1,28 @@
|
||||
-- name: GetAllNotificationsForUserID :many
|
||||
SELECT n.* FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notifier_id = $1 ORDER BY no.created_on DESC;
|
||||
|
||||
-- name: GetNotificationForNotificationID :one
|
||||
SELECT n.*, no.* FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notification_id = $1;
|
||||
|
||||
-- name: CreateNotificationObject :one
|
||||
INSERT INTO notification_object(entity_type, action_type, entity_id, created_on, actor_id)
|
||||
VALUES ($1, $2, $3, $4, $5) RETURNING *;
|
||||
|
||||
-- name: GetEntityIDForNotificationID :one
|
||||
SELECT no.entity_id FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notification_id = $1;
|
||||
|
||||
-- name: GetEntityForNotificationID :one
|
||||
SELECT no.created_on, no.entity_id, no.entity_type, no.action_type, no.actor_id FROM notification as n
|
||||
INNER JOIN notification_object as no ON no.notification_object_id = n.notification_object_id
|
||||
WHERE n.notification_id = $1;
|
||||
|
||||
-- name: CreateNotification :one
|
||||
INSERT INTO notification(notification_object_id, notifier_id)
|
||||
VALUES ($1, $2) RETURNING *;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -41,19 +41,34 @@ func NewHandler(repo db.Repository) http.Handler {
|
||||
|
||||
var subjectID uuid.UUID
|
||||
in := graphql.GetResolverContext(ctx).Args["input"]
|
||||
if typeArg == ObjectTypeProject || typeArg == ObjectTypeTeam {
|
||||
val := reflect.ValueOf(in) // could be any underlying type
|
||||
fieldName := "ProjectID"
|
||||
if typeArg == ObjectTypeTeam {
|
||||
fieldName = "TeamID"
|
||||
}
|
||||
subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
|
||||
if !ok {
|
||||
return nil, errors.New("error while casting subject uuid")
|
||||
}
|
||||
val := reflect.ValueOf(in) // could be any underlying type
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = reflect.Indirect(val)
|
||||
}
|
||||
var fieldName string
|
||||
switch typeArg {
|
||||
case ObjectTypeTeam:
|
||||
fieldName = "TeamID"
|
||||
case ObjectTypeTask:
|
||||
fieldName = "TaskID"
|
||||
default:
|
||||
fieldName = "ProjectID"
|
||||
}
|
||||
log.WithFields(log.Fields{"typeArg": typeArg, "fieldName": fieldName}).Info("getting field by name")
|
||||
subjectID, ok = val.FieldByName(fieldName).Interface().(uuid.UUID)
|
||||
if !ok {
|
||||
return nil, errors.New("error while casting subject uuid")
|
||||
}
|
||||
|
||||
var err error
|
||||
if level == ActionLevelProject {
|
||||
if typeArg == ObjectTypeTask {
|
||||
log.WithFields(log.Fields{"subjectID": subjectID}).Info("fetching project ID using task ID")
|
||||
subjectID, err = repo.GetProjectIDForTask(ctx, subjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
roles, err := GetProjectRoles(ctx, repo, subjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -151,3 +166,23 @@ func ConvertToRoleCode(r string) RoleCode {
|
||||
}
|
||||
return RoleCodeObserver
|
||||
}
|
||||
|
||||
// GetEntityType converts integer to EntityType enum
|
||||
func GetEntityType(entityType int32) EntityType {
|
||||
switch entityType {
|
||||
case 1:
|
||||
return EntityTypeTask
|
||||
default:
|
||||
panic("Not a valid entity type!")
|
||||
}
|
||||
}
|
||||
|
||||
// GetActionType converts integer to ActionType enum
|
||||
func GetActionType(actionType int32) ActionType {
|
||||
switch actionType {
|
||||
case 1:
|
||||
return ActionTypeTaskMemberAdded
|
||||
default:
|
||||
panic("Not a valid entity type!")
|
||||
}
|
||||
}
|
||||
|
@ -245,6 +245,18 @@ type NewUserAccount struct {
|
||||
RoleCode string `json:"roleCode"`
|
||||
}
|
||||
|
||||
type NotificationActor struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Type ActorType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type NotificationEntity struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Type EntityType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type OwnedList struct {
|
||||
Teams []db.Team `json:"teams"`
|
||||
Projects []db.Project `json:"projects"`
|
||||
@ -470,6 +482,123 @@ func (e ActionLevel) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type ActionType string
|
||||
|
||||
const (
|
||||
ActionTypeTaskMemberAdded ActionType = "TASK_MEMBER_ADDED"
|
||||
)
|
||||
|
||||
var AllActionType = []ActionType{
|
||||
ActionTypeTaskMemberAdded,
|
||||
}
|
||||
|
||||
func (e ActionType) IsValid() bool {
|
||||
switch e {
|
||||
case ActionTypeTaskMemberAdded:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e ActionType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *ActionType) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = ActionType(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid ActionType", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ActionType) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type ActorType string
|
||||
|
||||
const (
|
||||
ActorTypeUser ActorType = "USER"
|
||||
)
|
||||
|
||||
var AllActorType = []ActorType{
|
||||
ActorTypeUser,
|
||||
}
|
||||
|
||||
func (e ActorType) IsValid() bool {
|
||||
switch e {
|
||||
case ActorTypeUser:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e ActorType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *ActorType) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = ActorType(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid ActorType", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ActorType) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type EntityType string
|
||||
|
||||
const (
|
||||
EntityTypeTask EntityType = "TASK"
|
||||
)
|
||||
|
||||
var AllEntityType = []EntityType{
|
||||
EntityTypeTask,
|
||||
}
|
||||
|
||||
func (e EntityType) IsValid() bool {
|
||||
switch e {
|
||||
case EntityTypeTask:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e EntityType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *EntityType) UnmarshalGQL(v interface{}) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = EntityType(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid EntityType", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e EntityType) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
type ObjectType string
|
||||
|
||||
const (
|
||||
|
@ -229,6 +229,43 @@ input FindTeam {
|
||||
teamID: UUID!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
notifications: [Notification!]!
|
||||
}
|
||||
|
||||
enum EntityType {
|
||||
TASK
|
||||
}
|
||||
|
||||
enum ActorType {
|
||||
USER
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
TASK_MEMBER_ADDED
|
||||
}
|
||||
|
||||
type NotificationActor {
|
||||
id: UUID!
|
||||
type: ActorType!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type NotificationEntity {
|
||||
id: UUID!
|
||||
type: EntityType!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Notification {
|
||||
id: ID!
|
||||
entity: NotificationEntity!
|
||||
actionType: ActionType!
|
||||
actor: NotificationActor!
|
||||
read: Boolean!
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createProject(input: NewProject!): Project! @hasRole(roles: [ADMIN], level: TEAM, type: TEAM)
|
||||
deleteProject(input: DeleteProject!):
|
||||
@ -355,9 +392,9 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
|
||||
assignTask(input: AssignTaskInput):
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||
unassignTask(input: UnassignTaskInput):
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||
}
|
||||
|
||||
input NewTask {
|
||||
|
@ -269,6 +269,7 @@ func (r *mutationResolver) AssignTask(ctx context.Context, input *AssignTaskInpu
|
||||
if err != nil {
|
||||
return &db.Task{}, err
|
||||
}
|
||||
// r.NotificationQueue.TaskMemberWasAdded(assignedTask.TaskID, userID, assignedTask.UserID)
|
||||
task, err := r.Repository.GetTaskByID(ctx, input.TaskID)
|
||||
return &task, err
|
||||
}
|
||||
@ -720,6 +721,61 @@ func (r *mutationResolver) UpdateUserRole(ctx context.Context, input UpdateUserR
|
||||
return &UpdateUserRolePayload{User: &user}, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) ID(ctx context.Context, obj *db.Notification) (uuid.UUID, error) {
|
||||
return obj.NotificationID, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) Entity(ctx context.Context, obj *db.Notification) (*NotificationEntity, error) {
|
||||
log.WithFields(log.Fields{"notificationID": obj.NotificationID}).Info("fetching entity for notification")
|
||||
entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
|
||||
log.WithFields(log.Fields{"entityID": entity.EntityID}).Info("fetched entity")
|
||||
if err != nil {
|
||||
return &NotificationEntity{}, err
|
||||
}
|
||||
entityType := GetEntityType(entity.EntityType)
|
||||
switch entityType {
|
||||
case EntityTypeTask:
|
||||
task, err := r.Repository.GetTaskByID(ctx, entity.EntityID)
|
||||
if err != nil {
|
||||
return &NotificationEntity{}, err
|
||||
}
|
||||
return &NotificationEntity{Type: entityType, ID: entity.EntityID, Name: task.Name}, err
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("not implemented"))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *notificationResolver) ActionType(ctx context.Context, obj *db.Notification) (ActionType, error) {
|
||||
entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
|
||||
if err != nil {
|
||||
return ActionTypeTaskMemberAdded, err
|
||||
}
|
||||
actionType := GetActionType(entity.ActionType)
|
||||
return actionType, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) Actor(ctx context.Context, obj *db.Notification) (*NotificationActor, error) {
|
||||
entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
|
||||
if err != nil {
|
||||
return &NotificationActor{}, err
|
||||
}
|
||||
log.WithFields(log.Fields{"entityID": entity.ActorID}).Info("fetching actor")
|
||||
user, err := r.Repository.GetUserAccountByID(ctx, entity.ActorID)
|
||||
if err != nil {
|
||||
return &NotificationActor{}, err
|
||||
}
|
||||
return &NotificationActor{ID: entity.ActorID, Name: user.FullName, Type: ActorTypeUser}, nil
|
||||
}
|
||||
|
||||
func (r *notificationResolver) CreatedAt(ctx context.Context, obj *db.Notification) (*time.Time, error) {
|
||||
entity, err := r.Repository.GetEntityForNotificationID(ctx, obj.NotificationID)
|
||||
if err != nil {
|
||||
return &time.Time{}, err
|
||||
}
|
||||
return &entity.CreatedOn, nil
|
||||
}
|
||||
|
||||
func (r *organizationResolver) ID(ctx context.Context, obj *db.Organization) (uuid.UUID, error) {
|
||||
return obj.OrganizationID, nil
|
||||
}
|
||||
@ -1011,6 +1067,21 @@ func (r *queryResolver) Me(ctx context.Context) (*MePayload, error) {
|
||||
return &MePayload{User: &user, TeamRoles: teamRoles, ProjectRoles: projectRoles}, err
|
||||
}
|
||||
|
||||
func (r *queryResolver) Notifications(ctx context.Context) ([]db.Notification, error) {
|
||||
userID, ok := GetUserID(ctx)
|
||||
log.WithFields(log.Fields{"userID": userID}).Info("fetching notifications")
|
||||
if !ok {
|
||||
return []db.Notification{}, errors.New("user id is missing")
|
||||
}
|
||||
notifications, err := r.Repository.GetAllNotificationsForUserID(ctx, userID)
|
||||
if err == sql.ErrNoRows {
|
||||
return []db.Notification{}, nil
|
||||
} else if err != nil {
|
||||
return []db.Notification{}, err
|
||||
}
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
func (r *refreshTokenResolver) ID(ctx context.Context, obj *db.RefreshToken) (uuid.UUID, error) {
|
||||
return obj.TokenID, nil
|
||||
}
|
||||
@ -1268,6 +1339,9 @@ func (r *Resolver) LabelColor() LabelColorResolver { return &labelColorResolver{
|
||||
// Mutation returns MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
// Notification returns NotificationResolver implementation.
|
||||
func (r *Resolver) Notification() NotificationResolver { return ¬ificationResolver{r} }
|
||||
|
||||
// Organization returns OrganizationResolver implementation.
|
||||
func (r *Resolver) Organization() OrganizationResolver { return &organizationResolver{r} }
|
||||
|
||||
@ -1308,6 +1382,7 @@ func (r *Resolver) UserAccount() UserAccountResolver { return &userAccountResolv
|
||||
|
||||
type labelColorResolver struct{ *Resolver }
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type notificationResolver struct{ *Resolver }
|
||||
type organizationResolver struct{ *Resolver }
|
||||
type projectResolver struct{ *Resolver }
|
||||
type projectLabelResolver struct{ *Resolver }
|
||||
|
36
internal/graph/schema/notification.gql
Normal file
36
internal/graph/schema/notification.gql
Normal file
@ -0,0 +1,36 @@
|
||||
extend type Query {
|
||||
notifications: [Notification!]!
|
||||
}
|
||||
|
||||
enum EntityType {
|
||||
TASK
|
||||
}
|
||||
|
||||
enum ActorType {
|
||||
USER
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
TASK_MEMBER_ADDED
|
||||
}
|
||||
|
||||
type NotificationActor {
|
||||
id: UUID!
|
||||
type: ActorType!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type NotificationEntity {
|
||||
id: UUID!
|
||||
type: EntityType!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Notification {
|
||||
id: ID!
|
||||
entity: NotificationEntity!
|
||||
actionType: ActionType!
|
||||
actor: NotificationActor!
|
||||
read: Boolean!
|
||||
createdAt: Time!
|
||||
}
|
@ -16,9 +16,9 @@ extend type Mutation {
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
|
||||
assignTask(input: AssignTaskInput):
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||
unassignTask(input: UnassignTaskInput):
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: PROJECT)
|
||||
Task! @hasRole(roles: [ADMIN], level: PROJECT, type: TASK)
|
||||
}
|
||||
|
||||
input NewTask {
|
||||
|
53
internal/notification/logger.go
Normal file
53
internal/notification/logger.go
Normal file
@ -0,0 +1,53 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// MachineryLogger is a customer logger for machinery worker
|
||||
type MachineryLogger struct{}
|
||||
|
||||
// Print sends to logrus.Info
|
||||
func (m *MachineryLogger) Print(args ...interface{}) {
|
||||
log.Info(args...)
|
||||
}
|
||||
|
||||
// Printf sends to logrus.Infof
|
||||
func (m *MachineryLogger) Printf(format string, args ...interface{}) {
|
||||
log.Infof(format, args...)
|
||||
}
|
||||
|
||||
// Println sends to logrus.Info
|
||||
func (m *MachineryLogger) Println(args ...interface{}) {
|
||||
log.Info(args...)
|
||||
}
|
||||
|
||||
// Fatal sends to logrus.Fatal
|
||||
func (m *MachineryLogger) Fatal(args ...interface{}) {
|
||||
log.Fatal(args...)
|
||||
}
|
||||
|
||||
// Fatalf sends to logrus.Fatalf
|
||||
func (m *MachineryLogger) Fatalf(format string, args ...interface{}) {
|
||||
log.Fatalf(format, args...)
|
||||
}
|
||||
|
||||
// Fatalln sends to logrus.Fatal
|
||||
func (m *MachineryLogger) Fatalln(args ...interface{}) {
|
||||
log.Fatal(args...)
|
||||
}
|
||||
|
||||
// Panic sends to logrus.Panic
|
||||
func (m *MachineryLogger) Panic(args ...interface{}) {
|
||||
log.Panic(args...)
|
||||
}
|
||||
|
||||
// Panicf sends to logrus.Panic
|
||||
func (m *MachineryLogger) Panicf(format string, args ...interface{}) {
|
||||
log.Panic(args...)
|
||||
}
|
||||
|
||||
// Panicln sends to logrus.Panic
|
||||
func (m *MachineryLogger) Panicln(args ...interface{}) {
|
||||
log.Panic(args...)
|
||||
}
|
78
internal/notification/notification.go
Normal file
78
internal/notification/notification.go
Normal file
@ -0,0 +1,78 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/RichardKnop/machinery/v1"
|
||||
"github.com/RichardKnop/machinery/v1/tasks"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jordanknott/taskcafe/internal/db"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func RegisterTasks(server *machinery.Server, repo db.Repository) {
|
||||
tasks := NotificationTasks{repo}
|
||||
server.RegisterTasks(map[string]interface{}{
|
||||
"taskMemberWasAdded": tasks.TaskMemberWasAdded,
|
||||
})
|
||||
}
|
||||
|
||||
type NotificationTasks struct {
|
||||
Repository db.Repository
|
||||
}
|
||||
|
||||
func (m *NotificationTasks) TaskMemberWasAdded(taskID, notifierID, notifiedID string) (bool, error) {
|
||||
tid := uuid.MustParse(taskID)
|
||||
notifier := uuid.MustParse(notifierID)
|
||||
notified := uuid.MustParse(notifiedID)
|
||||
if notifier == notified {
|
||||
return true, nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC()
|
||||
notificationObject, err := m.Repository.CreateNotificationObject(ctx, db.CreateNotificationObjectParams{EntityType: 1, EntityID: tid, ActionType: 1, ActorID: notifier, CreatedOn: now})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
notification, err := m.Repository.CreateNotification(ctx, db.CreateNotificationParams{NotificationObjectID: notificationObject.NotificationObjectID, NotifierID: notified})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
log.WithFields(log.Fields{"notificationID": notification.NotificationID}).Info("created new notification")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type NotificationQueue struct {
|
||||
Server *machinery.Server
|
||||
}
|
||||
|
||||
func (n *NotificationQueue) TaskMemberWasAdded(taskID, notifier, notified uuid.UUID) error {
|
||||
task := tasks.Signature{
|
||||
Name: "taskMemberWasAdded",
|
||||
Args: []tasks.Arg{
|
||||
{
|
||||
Name: "taskID",
|
||||
Type: "string",
|
||||
Value: taskID.String(),
|
||||
},
|
||||
{
|
||||
Name: "notifierID",
|
||||
Type: "string",
|
||||
Value: notifier.String(),
|
||||
},
|
||||
{
|
||||
Name: "notifiedID",
|
||||
Type: "string",
|
||||
Value: notified.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := n.Server.SendTask(&task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user