Make it possible to resolve notifications

This commit is contained in:
Eli Ribble 2025-11-13 16:46:30 +00:00
parent 20186f65bf
commit fc40309dd0
No known key found for this signature in database
8 changed files with 266 additions and 96 deletions

View file

@ -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, &notificationSetter)
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)))
}

View file

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

View file

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

View file

@ -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
View file

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

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

View file

@ -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
View 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, &notificationSetter)
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"
}
}