feat: add notification UI

showPopup was also refactored to be better
This commit is contained in:
Jordan Knott
2020-08-12 20:54:14 -05:00
parent feea209507
commit 0caa803d27
34 changed files with 2516 additions and 104 deletions

View File

@ -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()
}

View File

@ -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
}

View 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(&notification.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
}

View File

@ -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"`

View 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
}

View File

@ -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)

View 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

View File

@ -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!")
}
}

View File

@ -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 (

View File

@ -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 {

View File

@ -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 &notificationResolver{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 }

View 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!
}

View File

@ -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 {

View 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...)
}

View 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
}