Make it possible to resolve notifications
This commit is contained in:
parent
20186f65bf
commit
fc40309dd0
8 changed files with 266 additions and 96 deletions
26
arcgis.go
26
arcgis.go
|
|
@ -22,7 +22,6 @@ import (
|
|||
|
||||
"github.com/Gleipnir-Technology/arcgis-go"
|
||||
"github.com/Gleipnir-Technology/arcgis-go/fieldseeker"
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/sql"
|
||||
"github.com/aarondl/opt/omit"
|
||||
|
|
@ -194,26 +193,23 @@ func updateArcgisUserData(ctx context.Context, user *models.User, access_token s
|
|||
slog.Error("Cannot get webhooks - ArcGIS ID is null", slog.Int("org.id", int(org.ID)))
|
||||
}
|
||||
client.Context = &arcgis_id
|
||||
err = maybeCreateWebhook(ctx, fieldseekerClient)
|
||||
if err != nil {
|
||||
slog.Error("Failed to manage webhooks", slog.String("err", err.Error()))
|
||||
}
|
||||
maybeCreateWebhook(ctx, fieldseekerClient)
|
||||
clearNotificationsOauth(ctx, user)
|
||||
NewOAuthTokenChannel <- struct{}{}
|
||||
}
|
||||
|
||||
func maybeCreateWebhook(ctx context.Context, client *fieldseeker.FieldSeeker) error {
|
||||
func maybeCreateWebhook(ctx context.Context, client *fieldseeker.FieldSeeker) {
|
||||
webhooks, err := client.WebhookList()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get webhooks: %v", err)
|
||||
slog.Error("Failed to get webhooks", slog.String("err", err.Error()))
|
||||
}
|
||||
for _, hook := range webhooks {
|
||||
if hook.Name == "Nidus Sync" {
|
||||
return nil
|
||||
slog.Info("Found nidus sync hook")
|
||||
} else {
|
||||
slog.Info("Found webhook", slog.String("name", hook.Name))
|
||||
}
|
||||
}
|
||||
return errors.New("Not implemented")
|
||||
}
|
||||
|
||||
func handleOauthAccessCode(ctx context.Context, user *models.User, code string) error {
|
||||
|
|
@ -513,17 +509,7 @@ func markTokenFailed(ctx context.Context, oauth *models.OauthToken) {
|
|||
slog.Error("Failed to get oauth user", slog.String("err", err.Error()))
|
||||
return
|
||||
}
|
||||
notificationSetter := models.NotificationSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
Message: omit.From("Oauth token invalidated"),
|
||||
Link: omit.From("/oauth/refresh"),
|
||||
Type: omit.From(enums.NotificationtypeOauthTokenInvalidated),
|
||||
}
|
||||
err = user.InsertUserNotifications(ctx, PGInstance.BobDB, ¬ificationSetter)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get oauth user", slog.String("err", err.Error()))
|
||||
return
|
||||
}
|
||||
notifyOauthInvalid(ctx, user)
|
||||
slog.Info("Marked oauth token invalid", slog.Int("id", int(oauth.ID)))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@ var Notifications = Table[
|
|||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
ResolvedAt: column{
|
||||
Name: "resolved_at",
|
||||
DBType: "timestamp without time zone",
|
||||
Default: "NULL",
|
||||
Comment: "",
|
||||
Nullable: true,
|
||||
Generated: false,
|
||||
AutoIncr: false,
|
||||
},
|
||||
},
|
||||
Indexes: notificationIndexes{
|
||||
NotificationPkey: index{
|
||||
|
|
@ -88,6 +97,28 @@ var Notifications = Table[
|
|||
Where: "",
|
||||
Include: []string{},
|
||||
},
|
||||
UniqueUserLinkNotResolved: index{
|
||||
Type: "btree",
|
||||
Name: "unique_user_link_not_resolved",
|
||||
Columns: []indexColumn{
|
||||
{
|
||||
Name: "user_id",
|
||||
Desc: null.FromCond(false, true),
|
||||
IsExpression: false,
|
||||
},
|
||||
{
|
||||
Name: "link",
|
||||
Desc: null.FromCond(false, true),
|
||||
IsExpression: false,
|
||||
},
|
||||
},
|
||||
Unique: true,
|
||||
Comment: "",
|
||||
NullsFirst: []bool{false, false},
|
||||
NullsDistinct: false,
|
||||
Where: "(resolved_at IS NULL)",
|
||||
Include: []string{},
|
||||
},
|
||||
},
|
||||
PrimaryKey: &constraint{
|
||||
Name: "notification_pkey",
|
||||
|
|
@ -116,21 +147,23 @@ type notificationColumns struct {
|
|||
Message column
|
||||
Type column
|
||||
UserID column
|
||||
ResolvedAt column
|
||||
}
|
||||
|
||||
func (c notificationColumns) AsSlice() []column {
|
||||
return []column{
|
||||
c.ID, c.Created, c.Link, c.Message, c.Type, c.UserID,
|
||||
c.ID, c.Created, c.Link, c.Message, c.Type, c.UserID, c.ResolvedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type notificationIndexes struct {
|
||||
NotificationPkey index
|
||||
UniqueUserLinkNotResolved index
|
||||
}
|
||||
|
||||
func (i notificationIndexes) AsSlice() []index {
|
||||
return []index{
|
||||
i.NotificationPkey,
|
||||
i.NotificationPkey, i.UniqueUserLinkNotResolved,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3619,6 +3619,7 @@ func (f *Factory) FromExistingNotification(m *models.Notification) *Notification
|
|||
o.Message = func() string { return m.Message }
|
||||
o.Type = func() enums.Notificationtype { return m.Type }
|
||||
o.UserID = func() int32 { return m.UserID }
|
||||
o.ResolvedAt = func() null.Val[time.Time] { return m.ResolvedAt }
|
||||
|
||||
ctx := context.Background()
|
||||
if m.R.UserUser != nil {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import (
|
|||
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
|
||||
models "github.com/Gleipnir-Technology/nidus-sync/models"
|
||||
"github.com/aarondl/opt/null"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/jaswdr/faker/v2"
|
||||
"github.com/stephenafamo/bob"
|
||||
)
|
||||
|
|
@ -42,6 +44,7 @@ type NotificationTemplate struct {
|
|||
Message func() string
|
||||
Type func() enums.Notificationtype
|
||||
UserID func() int32
|
||||
ResolvedAt func() null.Val[time.Time]
|
||||
|
||||
r notificationR
|
||||
f *Factory
|
||||
|
|
@ -104,6 +107,10 @@ func (o NotificationTemplate) BuildSetter() *models.NotificationSetter {
|
|||
val := o.UserID()
|
||||
m.UserID = omit.From(val)
|
||||
}
|
||||
if o.ResolvedAt != nil {
|
||||
val := o.ResolvedAt()
|
||||
m.ResolvedAt = omitnull.FromNull(val)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
@ -144,6 +151,9 @@ func (o NotificationTemplate) Build() *models.Notification {
|
|||
if o.UserID != nil {
|
||||
m.UserID = o.UserID()
|
||||
}
|
||||
if o.ResolvedAt != nil {
|
||||
m.ResolvedAt = o.ResolvedAt()
|
||||
}
|
||||
|
||||
o.setModelRels(m)
|
||||
|
||||
|
|
@ -309,6 +319,7 @@ func (m notificationMods) RandomizeAllColumns(f *faker.Faker) NotificationMod {
|
|||
NotificationMods.RandomMessage(f),
|
||||
NotificationMods.RandomType(f),
|
||||
NotificationMods.RandomUserID(f),
|
||||
NotificationMods.RandomResolvedAt(f),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -498,6 +509,59 @@ func (m notificationMods) RandomUserID(f *faker.Faker) NotificationMod {
|
|||
})
|
||||
}
|
||||
|
||||
// Set the model columns to this value
|
||||
func (m notificationMods) ResolvedAt(val null.Val[time.Time]) NotificationMod {
|
||||
return NotificationModFunc(func(_ context.Context, o *NotificationTemplate) {
|
||||
o.ResolvedAt = func() null.Val[time.Time] { return val }
|
||||
})
|
||||
}
|
||||
|
||||
// Set the Column from the function
|
||||
func (m notificationMods) ResolvedAtFunc(f func() null.Val[time.Time]) NotificationMod {
|
||||
return NotificationModFunc(func(_ context.Context, o *NotificationTemplate) {
|
||||
o.ResolvedAt = f
|
||||
})
|
||||
}
|
||||
|
||||
// Clear any values for the column
|
||||
func (m notificationMods) UnsetResolvedAt() NotificationMod {
|
||||
return NotificationModFunc(func(_ context.Context, o *NotificationTemplate) {
|
||||
o.ResolvedAt = nil
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
// The generated value is sometimes null
|
||||
func (m notificationMods) RandomResolvedAt(f *faker.Faker) NotificationMod {
|
||||
return NotificationModFunc(func(_ context.Context, o *NotificationTemplate) {
|
||||
o.ResolvedAt = func() null.Val[time.Time] {
|
||||
if f == nil {
|
||||
f = &defaultFaker
|
||||
}
|
||||
|
||||
val := random_time_Time(f)
|
||||
return null.From(val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Generates a random value for the column using the given faker
|
||||
// if faker is nil, a default faker is used
|
||||
// The generated value is never null
|
||||
func (m notificationMods) RandomResolvedAtNotNull(f *faker.Faker) NotificationMod {
|
||||
return NotificationModFunc(func(_ context.Context, o *NotificationTemplate) {
|
||||
o.ResolvedAt = func() null.Val[time.Time] {
|
||||
if f == nil {
|
||||
f = &defaultFaker
|
||||
}
|
||||
|
||||
val := random_time_Time(f)
|
||||
return null.From(val)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m notificationMods) WithParentsCascading() NotificationMod {
|
||||
return NotificationModFunc(func(ctx context.Context, o *NotificationTemplate) {
|
||||
if isDone, _ := notificationWithParentsCascadingCtx.Value(ctx); isDone {
|
||||
|
|
|
|||
33
html.go
33
html.go
|
|
@ -15,7 +15,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/models"
|
||||
"github.com/aarondl/opt/null"
|
||||
//"github.com/riverqueue/river/rivershared/util/slogutil"
|
||||
|
|
@ -464,38 +463,6 @@ func timeSince(t *time.Time) string {
|
|||
}
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Link string
|
||||
Message string
|
||||
Time time.Time
|
||||
Type string
|
||||
}
|
||||
|
||||
func notificationsForUser(ctx context.Context, u *models.User) ([]Notification, error) {
|
||||
results := make([]Notification, 0)
|
||||
notifications, err := u.UserNotifications().All(ctx, PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("Failed to get notifications: %v", err)
|
||||
}
|
||||
for _, n := range notifications {
|
||||
results = append(results, Notification{
|
||||
Link: n.Link,
|
||||
Message: n.Message,
|
||||
Time: n.Created,
|
||||
Type: notificationTypeName(n.Type),
|
||||
})
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func notificationTypeName(t enums.Notificationtype) string {
|
||||
switch t {
|
||||
case enums.NotificationtypeOauthTokenInvalidated:
|
||||
return "alert"
|
||||
default:
|
||||
return "unknown-type"
|
||||
}
|
||||
}
|
||||
func renderOrError(w http.ResponseWriter, template BuiltTemplate, context interface{}) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := template.ExecuteTemplate(buf, context)
|
||||
|
|
|
|||
9
migrations/00012_notification_unique_link.sql
Normal file
9
migrations/00012_notification_unique_link.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- +goose Up
|
||||
ALTER TABLE notification ADD COLUMN resolved_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
CREATE UNIQUE INDEX unique_user_link_not_resolved
|
||||
ON notification (user_id, link)
|
||||
WHERE resolved_at IS NULL;
|
||||
|
||||
-- +goose Down
|
||||
DROP INDEX unique_user_link_not_resolved;
|
||||
ALTER TABLE notification DROP COLUMN resolved_at;
|
||||
|
|
@ -10,7 +10,9 @@ import (
|
|||
"time"
|
||||
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
|
||||
"github.com/aarondl/opt/null"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/psql"
|
||||
"github.com/stephenafamo/bob/dialect/psql/dialect"
|
||||
|
|
@ -31,6 +33,7 @@ type Notification struct {
|
|||
Message string `db:"message" `
|
||||
Type enums.Notificationtype `db:"type" `
|
||||
UserID int32 `db:"user_id" `
|
||||
ResolvedAt null.Val[time.Time] `db:"resolved_at" `
|
||||
|
||||
R notificationR `db:"-" `
|
||||
}
|
||||
|
|
@ -53,7 +56,7 @@ type notificationR struct {
|
|||
func buildNotificationColumns(alias string) notificationColumns {
|
||||
return notificationColumns{
|
||||
ColumnsExpr: expr.NewColumnsExpr(
|
||||
"id", "created", "link", "message", "type", "user_id",
|
||||
"id", "created", "link", "message", "type", "user_id", "resolved_at",
|
||||
).WithParent("notification"),
|
||||
tableAlias: alias,
|
||||
ID: psql.Quote(alias, "id"),
|
||||
|
|
@ -62,6 +65,7 @@ func buildNotificationColumns(alias string) notificationColumns {
|
|||
Message: psql.Quote(alias, "message"),
|
||||
Type: psql.Quote(alias, "type"),
|
||||
UserID: psql.Quote(alias, "user_id"),
|
||||
ResolvedAt: psql.Quote(alias, "resolved_at"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +78,7 @@ type notificationColumns struct {
|
|||
Message psql.Expression
|
||||
Type psql.Expression
|
||||
UserID psql.Expression
|
||||
ResolvedAt psql.Expression
|
||||
}
|
||||
|
||||
func (c notificationColumns) Alias() string {
|
||||
|
|
@ -94,10 +99,11 @@ type NotificationSetter struct {
|
|||
Message omit.Val[string] `db:"message" `
|
||||
Type omit.Val[enums.Notificationtype] `db:"type" `
|
||||
UserID omit.Val[int32] `db:"user_id" `
|
||||
ResolvedAt omitnull.Val[time.Time] `db:"resolved_at" `
|
||||
}
|
||||
|
||||
func (s NotificationSetter) SetColumns() []string {
|
||||
vals := make([]string, 0, 6)
|
||||
vals := make([]string, 0, 7)
|
||||
if s.ID.IsValue() {
|
||||
vals = append(vals, "id")
|
||||
}
|
||||
|
|
@ -116,6 +122,9 @@ func (s NotificationSetter) SetColumns() []string {
|
|||
if s.UserID.IsValue() {
|
||||
vals = append(vals, "user_id")
|
||||
}
|
||||
if !s.ResolvedAt.IsUnset() {
|
||||
vals = append(vals, "resolved_at")
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +147,9 @@ func (s NotificationSetter) Overwrite(t *Notification) {
|
|||
if s.UserID.IsValue() {
|
||||
t.UserID = s.UserID.MustGet()
|
||||
}
|
||||
if !s.ResolvedAt.IsUnset() {
|
||||
t.ResolvedAt = s.ResolvedAt.MustGetNull()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NotificationSetter) Apply(q *dialect.InsertQuery) {
|
||||
|
|
@ -146,7 +158,7 @@ func (s *NotificationSetter) Apply(q *dialect.InsertQuery) {
|
|||
})
|
||||
|
||||
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
|
||||
vals := make([]bob.Expression, 6)
|
||||
vals := make([]bob.Expression, 7)
|
||||
if s.ID.IsValue() {
|
||||
vals[0] = psql.Arg(s.ID.MustGet())
|
||||
} else {
|
||||
|
|
@ -183,6 +195,12 @@ func (s *NotificationSetter) Apply(q *dialect.InsertQuery) {
|
|||
vals[5] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
if !s.ResolvedAt.IsUnset() {
|
||||
vals[6] = psql.Arg(s.ResolvedAt.MustGetNull())
|
||||
} else {
|
||||
vals[6] = psql.Raw("DEFAULT")
|
||||
}
|
||||
|
||||
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
|
||||
}))
|
||||
}
|
||||
|
|
@ -192,7 +210,7 @@ func (s NotificationSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
|
|||
}
|
||||
|
||||
func (s NotificationSetter) Expressions(prefix ...string) []bob.Expression {
|
||||
exprs := make([]bob.Expression, 0, 6)
|
||||
exprs := make([]bob.Expression, 0, 7)
|
||||
|
||||
if s.ID.IsValue() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
|
|
@ -236,6 +254,13 @@ func (s NotificationSetter) Expressions(prefix ...string) []bob.Expression {
|
|||
}})
|
||||
}
|
||||
|
||||
if !s.ResolvedAt.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
psql.Quote(append(prefix, "resolved_at")...),
|
||||
psql.Arg(s.ResolvedAt),
|
||||
}})
|
||||
}
|
||||
|
||||
return exprs
|
||||
}
|
||||
|
||||
|
|
@ -541,6 +566,7 @@ type notificationWhere[Q psql.Filterable] struct {
|
|||
Message psql.WhereMod[Q, string]
|
||||
Type psql.WhereMod[Q, enums.Notificationtype]
|
||||
UserID psql.WhereMod[Q, int32]
|
||||
ResolvedAt psql.WhereNullMod[Q, time.Time]
|
||||
}
|
||||
|
||||
func (notificationWhere[Q]) AliasedAs(alias string) notificationWhere[Q] {
|
||||
|
|
@ -555,6 +581,7 @@ func buildNotificationWhere[Q psql.Filterable](cols notificationColumns) notific
|
|||
Message: psql.Where[Q, string](cols.Message),
|
||||
Type: psql.Where[Q, enums.Notificationtype](cols.Type),
|
||||
UserID: psql.Where[Q, int32](cols.UserID),
|
||||
ResolvedAt: psql.WhereNull[Q, time.Time](cols.ResolvedAt),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
83
notification.go
Normal file
83
notification.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/models"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
)
|
||||
|
||||
var (
|
||||
NotificationPathOauthReset string = "/oauth/refresh"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
Link string
|
||||
Message string
|
||||
Time time.Time
|
||||
Type string
|
||||
}
|
||||
|
||||
// Clear all notifications for a given user with the given path
|
||||
func clearNotificationsOauth(ctx context.Context, user *models.User) {
|
||||
setter := models.NotificationSetter{
|
||||
ResolvedAt: omitnull.From(time.Now()),
|
||||
}
|
||||
updater := models.Notifications.Update(
|
||||
//models.SelectWhere.Notifications.Link.EQ(NotificationPathOauthReset),
|
||||
models.UpdateWhere.Notifications.Link.EQ(NotificationPathOauthReset),
|
||||
models.UpdateWhere.Notifications.UserID.EQ(user.ID),
|
||||
setter.UpdateMod(),
|
||||
)
|
||||
updater.Exec(ctx, PGInstance.BobDB)
|
||||
//user.UserNotifications(
|
||||
//models.SelectWhere.Notifications.Link.EQ(NotificationPathOauthReset),
|
||||
//).UpdateAll()
|
||||
}
|
||||
|
||||
func notifyOauthInvalid(ctx context.Context, user *models.User) {
|
||||
notificationSetter := models.NotificationSetter{
|
||||
Created: omit.From(time.Now()),
|
||||
Message: omit.From("Oauth token invalidated"),
|
||||
Link: omit.From(NotificationPathOauthReset),
|
||||
Type: omit.From(enums.NotificationtypeOauthTokenInvalidated),
|
||||
}
|
||||
err := user.InsertUserNotifications(ctx, PGInstance.BobDB, ¬ificationSetter)
|
||||
if err != nil {
|
||||
slog.Error("Failed to get oauth user", slog.String("err", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func notificationsForUser(ctx context.Context, u *models.User) ([]Notification, error) {
|
||||
results := make([]Notification, 0)
|
||||
notifications, err := u.UserNotifications(
|
||||
models.SelectWhere.Notifications.ResolvedAt.IsNull(),
|
||||
).All(ctx, PGInstance.BobDB)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("Failed to get notifications: %v", err)
|
||||
}
|
||||
for _, n := range notifications {
|
||||
results = append(results, Notification{
|
||||
Link: n.Link,
|
||||
Message: n.Message,
|
||||
Time: n.Created,
|
||||
Type: notificationTypeName(n.Type),
|
||||
})
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func notificationTypeName(t enums.Notificationtype) string {
|
||||
switch t {
|
||||
case enums.NotificationtypeOauthTokenInvalidated:
|
||||
return "alert"
|
||||
default:
|
||||
return "unknown-type"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue