feat: add notifications inbox db (#16599)

This PR is linked [to the following
issue](https://github.com/coder/internal/issues/334).

The objective is to create the DB layer and migration for the new `Coder
Inbox`.
This commit is contained in:
Vincent Vielle
2025-03-03 10:12:48 +01:00
committed by GitHub
parent d0e2060692
commit c074f77a4f
27 changed files with 966 additions and 0 deletions
+33
View File
@@ -281,6 +281,7 @@ var (
DisplayName: "Notifier",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceInboxNotification.Type: {policy.ActionCreate},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
@@ -1126,6 +1127,14 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
return q.db.CleanTailnetTunnels(ctx)
}
func (q *querier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceInboxNotification.WithOwner(userID.String())); err != nil {
return 0, err
}
return q.db.CountUnreadInboxNotificationsByUserID(ctx, userID)
}
// TODO: Handle org scoped lookups
func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
roleObject := rbac.ResourceAssignRole
if arg.OrganizationID != uuid.Nil {
@@ -1689,6 +1698,10 @@ func (q *querier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]dat
return q.db.GetFileTemplates(ctx, fileID)
}
func (q *querier) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg database.GetFilteredInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetFilteredInboxNotificationsByUserID)(ctx, arg)
}
func (q *querier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetGitSSHKey)(ctx, userID)
}
@@ -1748,6 +1761,14 @@ func (q *querier) GetHungProvisionerJobs(ctx context.Context, hungSince time.Tim
return q.db.GetHungProvisionerJobs(ctx, hungSince)
}
func (q *querier) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (database.InboxNotification, error) {
return fetchWithAction(q.log, q.auth, policy.ActionRead, q.db.GetInboxNotificationByID)(ctx, id)
}
func (q *querier) GetInboxNotificationsByUserID(ctx context.Context, userID database.GetInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetInboxNotificationsByUserID)(ctx, userID)
}
func (q *querier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) {
if _, err := fetch(q.log, q.auth, q.db.GetWorkspaceByID)(ctx, arg.WorkspaceID); err != nil {
return database.JfrogXrayScan{}, err
@@ -3079,6 +3100,10 @@ func (q *querier) InsertGroupMember(ctx context.Context, arg database.InsertGrou
return update(q.log, q.auth, fetch, q.db.InsertGroupMember)(ctx, arg)
}
func (q *querier) InsertInboxNotification(ctx context.Context, arg database.InsertInboxNotificationParams) (database.InboxNotification, error) {
return insert(q.log, q.auth, rbac.ResourceInboxNotification.WithOwner(arg.UserID.String()), q.db.InsertInboxNotification)(ctx, arg)
}
func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseParams) (database.License, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceLicense); err != nil {
return database.License{}, err
@@ -3666,6 +3691,14 @@ func (q *querier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfte
return q.db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
}
func (q *querier) UpdateInboxNotificationReadStatus(ctx context.Context, args database.UpdateInboxNotificationReadStatusParams) error {
fetchFunc := func(ctx context.Context, args database.UpdateInboxNotificationReadStatusParams) (database.InboxNotification, error) {
return q.db.GetInboxNotificationByID(ctx, args.ID)
}
return update(q.log, q.auth, fetchFunc, q.db.UpdateInboxNotificationReadStatus)(ctx, args)
}
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
// Authorized fetch will check that the actor has read access to the org member since the org member is returned.
member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{
+135
View File
@@ -4466,6 +4466,141 @@ func (s *MethodTestSuite) TestNotifications() {
Disableds: []bool{true, false},
}).Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionUpdate)
}))
s.Run("GetInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
notifID := uuid.New()
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
ID: notifID,
UserID: u.ID,
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
Title: "test title",
Content: "test content notification",
Icon: "https://coder.com/favicon.ico",
Actions: json.RawMessage("{}"),
})
check.Args(database.GetInboxNotificationsByUserIDParams{
UserID: u.ID,
ReadStatus: database.InboxNotificationReadStatusAll,
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionRead).Returns([]database.InboxNotification{notif})
}))
s.Run("GetFilteredInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
notifID := uuid.New()
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
ID: notifID,
UserID: u.ID,
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
Targets: targets,
Title: "test title",
Content: "test content notification",
Icon: "https://coder.com/favicon.ico",
Actions: json.RawMessage("{}"),
})
check.Args(database.GetFilteredInboxNotificationsByUserIDParams{
UserID: u.ID,
Templates: []uuid.UUID{notifications.TemplateWorkspaceAutoUpdated},
Targets: []uuid.UUID{u.ID},
ReadStatus: database.InboxNotificationReadStatusAll,
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionRead).Returns([]database.InboxNotification{notif})
}))
s.Run("GetInboxNotificationByID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
notifID := uuid.New()
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
ID: notifID,
UserID: u.ID,
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
Targets: targets,
Title: "test title",
Content: "test content notification",
Icon: "https://coder.com/favicon.ico",
Actions: json.RawMessage("{}"),
})
check.Args(notifID).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionRead).Returns(notif)
}))
s.Run("CountUnreadInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
notifID := uuid.New()
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
_ = dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
ID: notifID,
UserID: u.ID,
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
Targets: targets,
Title: "test title",
Content: "test content notification",
Icon: "https://coder.com/favicon.ico",
Actions: json.RawMessage("{}"),
})
check.Args(u.ID).Asserts(rbac.ResourceInboxNotification.WithOwner(u.ID.String()), policy.ActionRead).Returns(int64(1))
}))
s.Run("InsertInboxNotification", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
notifID := uuid.New()
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
check.Args(database.InsertInboxNotificationParams{
ID: notifID,
UserID: u.ID,
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
Targets: targets,
Title: "test title",
Content: "test content notification",
Icon: "https://coder.com/favicon.ico",
Actions: json.RawMessage("{}"),
}).Asserts(rbac.ResourceInboxNotification.WithOwner(u.ID.String()), policy.ActionCreate)
}))
s.Run("UpdateInboxNotificationReadStatus", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
notifID := uuid.New()
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
readAt := dbtestutil.NowInDefaultTimezone()
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
ID: notifID,
UserID: u.ID,
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
Targets: targets,
Title: "test title",
Content: "test content notification",
Icon: "https://coder.com/favicon.ico",
Actions: json.RawMessage("{}"),
})
notif.ReadAt = sql.NullTime{Time: readAt, Valid: true}
check.Args(database.UpdateInboxNotificationReadStatusParams{
ID: notifID,
ReadAt: sql.NullTime{Time: readAt, Valid: true},
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestOAuth2ProviderApps() {
+16
View File
@@ -450,6 +450,22 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat
return mem
}
func NotificationInbox(t testing.TB, db database.Store, orig database.InsertInboxNotificationParams) database.InboxNotification {
notification, err := db.InsertInboxNotification(genCtx, database.InsertInboxNotificationParams{
ID: takeFirst(orig.ID, uuid.New()),
UserID: takeFirst(orig.UserID, uuid.New()),
TemplateID: takeFirst(orig.TemplateID, uuid.New()),
Targets: takeFirstSlice(orig.Targets, []uuid.UUID{}),
Title: takeFirst(orig.Title, testutil.GetRandomName(t)),
Content: takeFirst(orig.Content, testutil.GetRandomName(t)),
Icon: takeFirst(orig.Icon, ""),
Actions: orig.Actions,
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
})
require.NoError(t, err, "insert notification")
return notification
}
func Group(t testing.TB, db database.Store, orig database.Group) database.Group {
t.Helper()
+130
View File
@@ -67,6 +67,7 @@ func New() database.Store {
gitSSHKey: make([]database.GitSSHKey, 0),
notificationMessages: make([]database.NotificationMessage, 0),
notificationPreferences: make([]database.NotificationPreference, 0),
InboxNotification: make([]database.InboxNotification, 0),
parameterSchemas: make([]database.ParameterSchema, 0),
provisionerDaemons: make([]database.ProvisionerDaemon, 0),
provisionerKeys: make([]database.ProvisionerKey, 0),
@@ -206,6 +207,7 @@ type data struct {
notificationMessages []database.NotificationMessage
notificationPreferences []database.NotificationPreference
notificationReportGeneratorLogs []database.NotificationReportGeneratorLog
InboxNotification []database.InboxNotification
oauth2ProviderApps []database.OAuth2ProviderApp
oauth2ProviderAppSecrets []database.OAuth2ProviderAppSecret
oauth2ProviderAppCodes []database.OAuth2ProviderAppCode
@@ -1606,6 +1608,26 @@ func (*FakeQuerier) CleanTailnetTunnels(context.Context) error {
return ErrUnimplemented
}
func (q *FakeQuerier) CountUnreadInboxNotificationsByUserID(_ context.Context, userID uuid.UUID) (int64, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var count int64
for _, notification := range q.InboxNotification {
if notification.UserID != userID {
continue
}
if notification.ReadAt.Valid {
continue
}
count++
}
return count, nil
}
func (q *FakeQuerier) CustomRoles(_ context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
@@ -3130,6 +3152,45 @@ func (q *FakeQuerier) GetFileTemplates(_ context.Context, id uuid.UUID) ([]datab
return rows, nil
}
func (q *FakeQuerier) GetFilteredInboxNotificationsByUserID(_ context.Context, arg database.GetFilteredInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
notifications := make([]database.InboxNotification, 0)
for _, notification := range q.InboxNotification {
if notification.UserID == arg.UserID {
for _, template := range arg.Templates {
templateFound := false
if notification.TemplateID == template {
templateFound = true
}
if !templateFound {
continue
}
}
for _, target := range arg.Targets {
isFound := false
for _, insertedTarget := range notification.Targets {
if insertedTarget == target {
isFound = true
break
}
}
if !isFound {
continue
}
notifications = append(notifications, notification)
}
}
}
return notifications, nil
}
func (q *FakeQuerier) GetGitSSHKey(_ context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -3328,6 +3389,33 @@ func (q *FakeQuerier) GetHungProvisionerJobs(_ context.Context, hungSince time.T
return hungJobs, nil
}
func (q *FakeQuerier) GetInboxNotificationByID(_ context.Context, id uuid.UUID) (database.InboxNotification, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, notification := range q.InboxNotification {
if notification.ID == id {
return notification, nil
}
}
return database.InboxNotification{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetInboxNotificationsByUserID(_ context.Context, params database.GetInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
notifications := make([]database.InboxNotification, 0)
for _, notification := range q.InboxNotification {
if notification.UserID == params.UserID {
notifications = append(notifications, notification)
}
}
return notifications, nil
}
func (q *FakeQuerier) GetJFrogXrayScanByWorkspaceAndAgentID(_ context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) {
err := validateDatabaseType(arg)
if err != nil {
@@ -7965,6 +8053,30 @@ func (q *FakeQuerier) InsertGroupMember(_ context.Context, arg database.InsertGr
return nil
}
func (q *FakeQuerier) InsertInboxNotification(_ context.Context, arg database.InsertInboxNotificationParams) (database.InboxNotification, error) {
if err := validateDatabaseType(arg); err != nil {
return database.InboxNotification{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
notification := database.InboxNotification{
ID: arg.ID,
UserID: arg.UserID,
TemplateID: arg.TemplateID,
Targets: arg.Targets,
Title: arg.Title,
Content: arg.Content,
Icon: arg.Icon,
Actions: arg.Actions,
CreatedAt: time.Now(),
}
q.InboxNotification = append(q.InboxNotification, notification)
return notification, nil
}
func (q *FakeQuerier) InsertLicense(
_ context.Context, arg database.InsertLicenseParams,
) (database.License, error) {
@@ -9679,6 +9791,24 @@ func (q *FakeQuerier) UpdateInactiveUsersToDormant(_ context.Context, params dat
return updated, nil
}
func (q *FakeQuerier) UpdateInboxNotificationReadStatus(_ context.Context, arg database.UpdateInboxNotificationReadStatusParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i := range q.InboxNotification {
if q.InboxNotification[i].ID == arg.ID {
q.InboxNotification[i].ReadAt = arg.ReadAt
}
}
return nil
}
func (q *FakeQuerier) UpdateMemberRoles(_ context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
if err := validateDatabaseType(arg); err != nil {
return database.OrganizationMember{}, err
+42
View File
@@ -178,6 +178,13 @@ func (m queryMetricsStore) CleanTailnetTunnels(ctx context.Context) error {
return r0
}
func (m queryMetricsStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
start := time.Now()
r0, r1 := m.s.CountUnreadInboxNotificationsByUserID(ctx, userID)
m.queryLatencies.WithLabelValues("CountUnreadInboxNotificationsByUserID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
start := time.Now()
r0, r1 := m.s.CustomRoles(ctx, arg)
@@ -710,6 +717,13 @@ func (m queryMetricsStore) GetFileTemplates(ctx context.Context, fileID uuid.UUI
return rows, err
}
func (m queryMetricsStore) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg database.GetFilteredInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
start := time.Now()
r0, r1 := m.s.GetFilteredInboxNotificationsByUserID(ctx, arg)
m.queryLatencies.WithLabelValues("GetFilteredInboxNotificationsByUserID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
start := time.Now()
key, err := m.s.GetGitSSHKey(ctx, userID)
@@ -773,6 +787,20 @@ func (m queryMetricsStore) GetHungProvisionerJobs(ctx context.Context, hungSince
return jobs, err
}
func (m queryMetricsStore) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (database.InboxNotification, error) {
start := time.Now()
r0, r1 := m.s.GetInboxNotificationByID(ctx, id)
m.queryLatencies.WithLabelValues("GetInboxNotificationByID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetInboxNotificationsByUserID(ctx context.Context, userID database.GetInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
start := time.Now()
r0, r1 := m.s.GetInboxNotificationsByUserID(ctx, userID)
m.queryLatencies.WithLabelValues("GetInboxNotificationsByUserID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) {
start := time.Now()
r0, r1 := m.s.GetJFrogXrayScanByWorkspaceAndAgentID(ctx, arg)
@@ -1879,6 +1907,13 @@ func (m queryMetricsStore) InsertGroupMember(ctx context.Context, arg database.I
return err
}
func (m queryMetricsStore) InsertInboxNotification(ctx context.Context, arg database.InsertInboxNotificationParams) (database.InboxNotification, error) {
start := time.Now()
r0, r1 := m.s.InsertInboxNotification(ctx, arg)
m.queryLatencies.WithLabelValues("InsertInboxNotification").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) InsertLicense(ctx context.Context, arg database.InsertLicenseParams) (database.License, error) {
start := time.Now()
license, err := m.s.InsertLicense(ctx, arg)
@@ -2334,6 +2369,13 @@ func (m queryMetricsStore) UpdateInactiveUsersToDormant(ctx context.Context, las
return r0, r1
}
func (m queryMetricsStore) UpdateInboxNotificationReadStatus(ctx context.Context, arg database.UpdateInboxNotificationReadStatusParams) error {
start := time.Now()
r0 := m.s.UpdateInboxNotificationReadStatus(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateInboxNotificationReadStatus").Observe(time.Since(start).Seconds())
return r0
}
func (m queryMetricsStore) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
start := time.Now()
member, err := m.s.UpdateMemberRoles(ctx, arg)
+89
View File
@@ -232,6 +232,21 @@ func (mr *MockStoreMockRecorder) CleanTailnetTunnels(ctx any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetTunnels", reflect.TypeOf((*MockStore)(nil).CleanTailnetTunnels), ctx)
}
// CountUnreadInboxNotificationsByUserID mocks base method.
func (m *MockStore) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountUnreadInboxNotificationsByUserID", ctx, userID)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountUnreadInboxNotificationsByUserID indicates an expected call of CountUnreadInboxNotificationsByUserID.
func (mr *MockStoreMockRecorder) CountUnreadInboxNotificationsByUserID(ctx, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountUnreadInboxNotificationsByUserID", reflect.TypeOf((*MockStore)(nil).CountUnreadInboxNotificationsByUserID), ctx, userID)
}
// CustomRoles mocks base method.
func (m *MockStore) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
m.ctrl.T.Helper()
@@ -1417,6 +1432,21 @@ func (mr *MockStoreMockRecorder) GetFileTemplates(ctx, fileID any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFileTemplates", reflect.TypeOf((*MockStore)(nil).GetFileTemplates), ctx, fileID)
}
// GetFilteredInboxNotificationsByUserID mocks base method.
func (m *MockStore) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg database.GetFilteredInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFilteredInboxNotificationsByUserID", ctx, arg)
ret0, _ := ret[0].([]database.InboxNotification)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFilteredInboxNotificationsByUserID indicates an expected call of GetFilteredInboxNotificationsByUserID.
func (mr *MockStoreMockRecorder) GetFilteredInboxNotificationsByUserID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilteredInboxNotificationsByUserID", reflect.TypeOf((*MockStore)(nil).GetFilteredInboxNotificationsByUserID), ctx, arg)
}
// GetGitSSHKey mocks base method.
func (m *MockStore) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
m.ctrl.T.Helper()
@@ -1552,6 +1582,36 @@ func (mr *MockStoreMockRecorder) GetHungProvisionerJobs(ctx, updatedAt any) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHungProvisionerJobs", reflect.TypeOf((*MockStore)(nil).GetHungProvisionerJobs), ctx, updatedAt)
}
// GetInboxNotificationByID mocks base method.
func (m *MockStore) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (database.InboxNotification, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInboxNotificationByID", ctx, id)
ret0, _ := ret[0].(database.InboxNotification)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetInboxNotificationByID indicates an expected call of GetInboxNotificationByID.
func (mr *MockStoreMockRecorder) GetInboxNotificationByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInboxNotificationByID", reflect.TypeOf((*MockStore)(nil).GetInboxNotificationByID), ctx, id)
}
// GetInboxNotificationsByUserID mocks base method.
func (m *MockStore) GetInboxNotificationsByUserID(ctx context.Context, arg database.GetInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInboxNotificationsByUserID", ctx, arg)
ret0, _ := ret[0].([]database.InboxNotification)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetInboxNotificationsByUserID indicates an expected call of GetInboxNotificationsByUserID.
func (mr *MockStoreMockRecorder) GetInboxNotificationsByUserID(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInboxNotificationsByUserID", reflect.TypeOf((*MockStore)(nil).GetInboxNotificationsByUserID), ctx, arg)
}
// GetJFrogXrayScanByWorkspaceAndAgentID mocks base method.
func (m *MockStore) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) {
m.ctrl.T.Helper()
@@ -3962,6 +4022,21 @@ func (mr *MockStoreMockRecorder) InsertGroupMember(ctx, arg any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertGroupMember", reflect.TypeOf((*MockStore)(nil).InsertGroupMember), ctx, arg)
}
// InsertInboxNotification mocks base method.
func (m *MockStore) InsertInboxNotification(ctx context.Context, arg database.InsertInboxNotificationParams) (database.InboxNotification, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertInboxNotification", ctx, arg)
ret0, _ := ret[0].(database.InboxNotification)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertInboxNotification indicates an expected call of InsertInboxNotification.
func (mr *MockStoreMockRecorder) InsertInboxNotification(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertInboxNotification", reflect.TypeOf((*MockStore)(nil).InsertInboxNotification), ctx, arg)
}
// InsertLicense mocks base method.
func (m *MockStore) InsertLicense(ctx context.Context, arg database.InsertLicenseParams) (database.License, error) {
m.ctrl.T.Helper()
@@ -4951,6 +5026,20 @@ func (mr *MockStoreMockRecorder) UpdateInactiveUsersToDormant(ctx, arg any) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInactiveUsersToDormant", reflect.TypeOf((*MockStore)(nil).UpdateInactiveUsersToDormant), ctx, arg)
}
// UpdateInboxNotificationReadStatus mocks base method.
func (m *MockStore) UpdateInboxNotificationReadStatus(ctx context.Context, arg database.UpdateInboxNotificationReadStatusParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateInboxNotificationReadStatus", ctx, arg)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateInboxNotificationReadStatus indicates an expected call of UpdateInboxNotificationReadStatus.
func (mr *MockStoreMockRecorder) UpdateInboxNotificationReadStatus(ctx, arg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInboxNotificationReadStatus", reflect.TypeOf((*MockStore)(nil).UpdateInboxNotificationReadStatus), ctx, arg)
}
// UpdateMemberRoles mocks base method.
func (m *MockStore) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
m.ctrl.T.Helper()
+32
View File
@@ -66,6 +66,12 @@ CREATE TYPE group_source AS ENUM (
'oidc'
);
CREATE TYPE inbox_notification_read_status AS ENUM (
'all',
'unread',
'read'
);
CREATE TYPE log_level AS ENUM (
'trace',
'debug',
@@ -899,6 +905,19 @@ CREATE VIEW group_members_expanded AS
COMMENT ON VIEW group_members_expanded IS 'Joins group members with user information, organization ID, group name. Includes both regular group members and organization members (as part of the "Everyone" group).';
CREATE TABLE inbox_notifications (
id uuid NOT NULL,
user_id uuid NOT NULL,
template_id uuid NOT NULL,
targets uuid[],
title text NOT NULL,
content text NOT NULL,
icon text NOT NULL,
actions jsonb NOT NULL,
read_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now() NOT NULL
);
CREATE TABLE jfrog_xray_scans (
agent_id uuid NOT NULL,
workspace_id uuid NOT NULL,
@@ -2048,6 +2067,9 @@ ALTER TABLE ONLY groups
ALTER TABLE ONLY groups
ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
ALTER TABLE ONLY inbox_notifications
ADD CONSTRAINT inbox_notifications_pkey PRIMARY KEY (id);
ALTER TABLE ONLY jfrog_xray_scans
ADD CONSTRAINT jfrog_xray_scans_pkey PRIMARY KEY (agent_id, workspace_id);
@@ -2278,6 +2300,10 @@ CREATE INDEX idx_custom_roles_id ON custom_roles USING btree (id);
CREATE UNIQUE INDEX idx_custom_roles_name_lower ON custom_roles USING btree (lower(name));
CREATE INDEX idx_inbox_notifications_user_id_read_at ON inbox_notifications USING btree (user_id, read_at);
CREATE INDEX idx_inbox_notifications_user_id_template_id_targets ON inbox_notifications USING btree (user_id, template_id, targets);
CREATE INDEX idx_notification_messages_status ON notification_messages USING btree (status);
CREATE INDEX idx_organization_member_organization_id_uuid ON organization_members USING btree (organization_id);
@@ -2474,6 +2500,12 @@ ALTER TABLE ONLY group_members
ALTER TABLE ONLY groups
ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY inbox_notifications
ADD CONSTRAINT inbox_notifications_template_id_fkey FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
ALTER TABLE ONLY inbox_notifications
ADD CONSTRAINT inbox_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY jfrog_xray_scans
ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
@@ -14,6 +14,8 @@ const (
ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ForeignKeyInboxNotificationsTemplateID ForeignKeyConstraint = "inbox_notifications_template_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_template_id_fkey FOREIGN KEY (template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
ForeignKeyInboxNotificationsUserID ForeignKeyConstraint = "inbox_notifications_user_id_fkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyJfrogXrayScansAgentID ForeignKeyConstraint = "jfrog_xray_scans_agent_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE CASCADE;
ForeignKeyJfrogXrayScansWorkspaceID ForeignKeyConstraint = "jfrog_xray_scans_workspace_id_fkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_workspace_id_fkey FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
ForeignKeyNotificationMessagesNotificationTemplateID ForeignKeyConstraint = "notification_messages_notification_template_id_fkey" // ALTER TABLE ONLY notification_messages ADD CONSTRAINT notification_messages_notification_template_id_fkey FOREIGN KEY (notification_template_id) REFERENCES notification_templates(id) ON DELETE CASCADE;
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS inbox_notifications;
DROP TYPE IF EXISTS inbox_notification_read_status;
@@ -0,0 +1,17 @@
CREATE TYPE inbox_notification_read_status AS ENUM ('all', 'unread', 'read');
CREATE TABLE inbox_notifications (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
template_id UUID NOT NULL REFERENCES notification_templates(id) ON DELETE CASCADE,
targets UUID[],
title TEXT NOT NULL,
content TEXT NOT NULL,
icon TEXT NOT NULL,
actions JSONB NOT NULL,
read_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_inbox_notifications_user_id_read_at ON inbox_notifications(user_id, read_at);
CREATE INDEX idx_inbox_notifications_user_id_template_id_targets ON inbox_notifications(user_id, template_id, targets);
@@ -0,0 +1,25 @@
INSERT INTO
inbox_notifications (
id,
user_id,
template_id,
targets,
title,
content,
icon,
actions,
read_at,
created_at
)
VALUES (
'68b396aa-7f53-4bf1-b8d8-4cbf5fa244e5', -- uuid
'5755e622-fadd-44ca-98da-5df070491844', -- uuid
'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', -- uuid
ARRAY[]::UUID[], -- uuid[]
'Test Notification',
'This is a test notification',
'https://test.coder.com/favicon.ico',
'{}',
'2025-01-01 00:00:00',
'2025-01-01 00:00:00'
);
+6
View File
@@ -168,6 +168,12 @@ func (TemplateVersion) RBACObject(template Template) rbac.Object {
return template.RBACObject()
}
func (i InboxNotification) RBACObject() rbac.Object {
return rbac.ResourceInboxNotification.
WithID(i.ID).
WithOwner(i.UserID.String())
}
// RBACObjectNoTemplate is for orphaned template versions.
func (v TemplateVersion) RBACObjectNoTemplate() rbac.Object {
return rbac.ResourceTemplate.InOrg(v.OrganizationID)
+74
View File
@@ -543,6 +543,67 @@ func AllGroupSourceValues() []GroupSource {
}
}
type InboxNotificationReadStatus string
const (
InboxNotificationReadStatusAll InboxNotificationReadStatus = "all"
InboxNotificationReadStatusUnread InboxNotificationReadStatus = "unread"
InboxNotificationReadStatusRead InboxNotificationReadStatus = "read"
)
func (e *InboxNotificationReadStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = InboxNotificationReadStatus(s)
case string:
*e = InboxNotificationReadStatus(s)
default:
return fmt.Errorf("unsupported scan type for InboxNotificationReadStatus: %T", src)
}
return nil
}
type NullInboxNotificationReadStatus struct {
InboxNotificationReadStatus InboxNotificationReadStatus `json:"inbox_notification_read_status"`
Valid bool `json:"valid"` // Valid is true if InboxNotificationReadStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullInboxNotificationReadStatus) Scan(value interface{}) error {
if value == nil {
ns.InboxNotificationReadStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.InboxNotificationReadStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullInboxNotificationReadStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.InboxNotificationReadStatus), nil
}
func (e InboxNotificationReadStatus) Valid() bool {
switch e {
case InboxNotificationReadStatusAll,
InboxNotificationReadStatusUnread,
InboxNotificationReadStatusRead:
return true
}
return false
}
func AllInboxNotificationReadStatusValues() []InboxNotificationReadStatus {
return []InboxNotificationReadStatus{
InboxNotificationReadStatusAll,
InboxNotificationReadStatusUnread,
InboxNotificationReadStatusRead,
}
}
type LogLevel string
const (
@@ -2557,6 +2618,19 @@ type GroupMemberTable struct {
GroupID uuid.UUID `db:"group_id" json:"group_id"`
}
type InboxNotification struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Targets []uuid.UUID `db:"targets" json:"targets"`
Title string `db:"title" json:"title"`
Content string `db:"content" json:"content"`
Icon string `db:"icon" json:"icon"`
Actions json.RawMessage `db:"actions" json:"actions"`
ReadAt sql.NullTime `db:"read_at" json:"read_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type JfrogXrayScan struct {
AgentID uuid.UUID `db:"agent_id" json:"agent_id"`
WorkspaceID uuid.UUID `db:"workspace_id" json:"workspace_id"`
+18
View File
@@ -63,6 +63,7 @@ type sqlcQuerier interface {
CleanTailnetCoordinators(ctx context.Context) error
CleanTailnetLostPeers(ctx context.Context) error
CleanTailnetTunnels(ctx context.Context) error
CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error)
CustomRoles(ctx context.Context, arg CustomRolesParams) ([]CustomRole, error)
DeleteAPIKeyByID(ctx context.Context, id string) error
DeleteAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
@@ -158,6 +159,14 @@ type sqlcQuerier interface {
GetFileByID(ctx context.Context, id uuid.UUID) (File, error)
// Get all templates that use a file.
GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]GetFileTemplatesRow, error)
// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param templates: The template IDs to filter by - the template_id = ANY(@templates::UUID[]) condition checks if the template_id is in the @templates array
// param targets: The target IDs to filter by - the targets @> COALESCE(@targets, ARRAY[]::UUID[]) condition checks if the targets array (from the DB) contains all the elements in the @targets array
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
GetFilteredInboxNotificationsByUserID(ctx context.Context, arg GetFilteredInboxNotificationsByUserIDParams) ([]InboxNotification, error)
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error)
GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error)
@@ -170,6 +179,13 @@ type sqlcQuerier interface {
GetGroups(ctx context.Context, arg GetGroupsParams) ([]GetGroupsRow, error)
GetHealthSettings(ctx context.Context) (string, error)
GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error)
GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (InboxNotification, error)
// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
GetInboxNotificationsByUserID(ctx context.Context, arg GetInboxNotificationsByUserIDParams) ([]InboxNotification, error)
GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error)
GetLastUpdateCheck(ctx context.Context) (string, error)
GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error)
@@ -396,6 +412,7 @@ type sqlcQuerier interface {
InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error)
InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error)
InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error
InsertInboxNotification(ctx context.Context, arg InsertInboxNotificationParams) (InboxNotification, error)
InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error)
InsertMemoryResourceMonitor(ctx context.Context, arg InsertMemoryResourceMonitorParams) (WorkspaceAgentMemoryResourceMonitor, error)
// Inserts any group by name that does not exist. All new groups are given
@@ -479,6 +496,7 @@ type sqlcQuerier interface {
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
UpdateInactiveUsersToDormant(ctx context.Context, arg UpdateInactiveUsersToDormantParams) ([]UpdateInactiveUsersToDormantRow, error)
UpdateInboxNotificationReadStatus(ctx context.Context, arg UpdateInboxNotificationReadStatusParams) error
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
UpdateMemoryResourceMonitor(ctx context.Context, arg UpdateMemoryResourceMonitorParams) error
UpdateNotificationTemplateMethodByID(ctx context.Context, arg UpdateNotificationTemplateMethodByIDParams) (NotificationTemplate, error)
+237
View File
@@ -4298,6 +4298,243 @@ func (q *sqlQuerier) UpsertNotificationReportGeneratorLog(ctx context.Context, a
return err
}
const countUnreadInboxNotificationsByUserID = `-- name: CountUnreadInboxNotificationsByUserID :one
SELECT COUNT(*) FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL
`
func (q *sqlQuerier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
row := q.db.QueryRowContext(ctx, countUnreadInboxNotificationsByUserID, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const getFilteredInboxNotificationsByUserID = `-- name: GetFilteredInboxNotificationsByUserID :many
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
user_id = $1 AND
template_id = ANY($2::UUID[]) AND
targets @> COALESCE($3, ARRAY[]::UUID[]) AND
($4::inbox_notification_read_status = 'all' OR ($4::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($4::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
($5::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $5::TIMESTAMPTZ)
ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF($6 :: INT, 0), 25))
`
type GetFilteredInboxNotificationsByUserIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Templates []uuid.UUID `db:"templates" json:"templates"`
Targets []uuid.UUID `db:"targets" json:"targets"`
ReadStatus InboxNotificationReadStatus `db:"read_status" json:"read_status"`
CreatedAtOpt time.Time `db:"created_at_opt" json:"created_at_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param templates: The template IDs to filter by - the template_id = ANY(@templates::UUID[]) condition checks if the template_id is in the @templates array
// param targets: The target IDs to filter by - the targets @> COALESCE(@targets, ARRAY[]::UUID[]) condition checks if the targets array (from the DB) contains all the elements in the @targets array
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
func (q *sqlQuerier) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg GetFilteredInboxNotificationsByUserIDParams) ([]InboxNotification, error) {
rows, err := q.db.QueryContext(ctx, getFilteredInboxNotificationsByUserID,
arg.UserID,
pq.Array(arg.Templates),
pq.Array(arg.Targets),
arg.ReadStatus,
arg.CreatedAtOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []InboxNotification
for rows.Next() {
var i InboxNotification
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
); 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 getInboxNotificationByID = `-- name: GetInboxNotificationByID :one
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE id = $1
`
func (q *sqlQuerier) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (InboxNotification, error) {
row := q.db.QueryRowContext(ctx, getInboxNotificationByID, id)
var i InboxNotification
err := row.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
)
return i, err
}
const getInboxNotificationsByUserID = `-- name: GetInboxNotificationsByUserID :many
SELECT id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at FROM inbox_notifications WHERE
user_id = $1 AND
($2::inbox_notification_read_status = 'all' OR ($2::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR ($2::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
($3::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < $3::TIMESTAMPTZ)
ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF($4 :: INT, 0), 25))
`
type GetInboxNotificationsByUserIDParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
ReadStatus InboxNotificationReadStatus `db:"read_status" json:"read_status"`
CreatedAtOpt time.Time `db:"created_at_opt" json:"created_at_opt"`
LimitOpt int32 `db:"limit_opt" json:"limit_opt"`
}
// Fetches inbox notifications for a user filtered by templates and targets
// param user_id: The user ID
// param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
// param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
// param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
func (q *sqlQuerier) GetInboxNotificationsByUserID(ctx context.Context, arg GetInboxNotificationsByUserIDParams) ([]InboxNotification, error) {
rows, err := q.db.QueryContext(ctx, getInboxNotificationsByUserID,
arg.UserID,
arg.ReadStatus,
arg.CreatedAtOpt,
arg.LimitOpt,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []InboxNotification
for rows.Next() {
var i InboxNotification
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
); 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 insertInboxNotification = `-- name: InsertInboxNotification :one
INSERT INTO
inbox_notifications (
id,
user_id,
template_id,
targets,
title,
content,
icon,
actions,
created_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, user_id, template_id, targets, title, content, icon, actions, read_at, created_at
`
type InsertInboxNotificationParams struct {
ID uuid.UUID `db:"id" json:"id"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
Targets []uuid.UUID `db:"targets" json:"targets"`
Title string `db:"title" json:"title"`
Content string `db:"content" json:"content"`
Icon string `db:"icon" json:"icon"`
Actions json.RawMessage `db:"actions" json:"actions"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (q *sqlQuerier) InsertInboxNotification(ctx context.Context, arg InsertInboxNotificationParams) (InboxNotification, error) {
row := q.db.QueryRowContext(ctx, insertInboxNotification,
arg.ID,
arg.UserID,
arg.TemplateID,
pq.Array(arg.Targets),
arg.Title,
arg.Content,
arg.Icon,
arg.Actions,
arg.CreatedAt,
)
var i InboxNotification
err := row.Scan(
&i.ID,
&i.UserID,
&i.TemplateID,
pq.Array(&i.Targets),
&i.Title,
&i.Content,
&i.Icon,
&i.Actions,
&i.ReadAt,
&i.CreatedAt,
)
return i, err
}
const updateInboxNotificationReadStatus = `-- name: UpdateInboxNotificationReadStatus :exec
UPDATE
inbox_notifications
SET
read_at = $1
WHERE
id = $2
`
type UpdateInboxNotificationReadStatusParams struct {
ReadAt sql.NullTime `db:"read_at" json:"read_at"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateInboxNotificationReadStatus(ctx context.Context, arg UpdateInboxNotificationReadStatusParams) error {
_, err := q.db.ExecContext(ctx, updateInboxNotificationReadStatus, arg.ReadAt, arg.ID)
return err
}
const deleteOAuth2ProviderAppByID = `-- name: DeleteOAuth2ProviderAppByID :exec
DELETE FROM oauth2_provider_apps WHERE id = $1
`
@@ -0,0 +1,59 @@
-- name: GetInboxNotificationsByUserID :many
-- Fetches inbox notifications for a user filtered by templates and targets
-- param user_id: The user ID
-- param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
-- param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
-- param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
SELECT * FROM inbox_notifications WHERE
user_id = @user_id AND
(@read_status::inbox_notification_read_status = 'all' OR (@read_status::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR (@read_status::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
(@created_at_opt::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < @created_at_opt::TIMESTAMPTZ)
ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF(@limit_opt :: INT, 0), 25));
-- name: GetFilteredInboxNotificationsByUserID :many
-- Fetches inbox notifications for a user filtered by templates and targets
-- param user_id: The user ID
-- param templates: The template IDs to filter by - the template_id = ANY(@templates::UUID[]) condition checks if the template_id is in the @templates array
-- param targets: The target IDs to filter by - the targets @> COALESCE(@targets, ARRAY[]::UUID[]) condition checks if the targets array (from the DB) contains all the elements in the @targets array
-- param read_status: The read status to filter by - can be any of 'ALL', 'UNREAD', 'READ'
-- param created_at_opt: The created_at timestamp to filter by. This parameter is usd for pagination - it fetches notifications created before the specified timestamp if it is not the zero value
-- param limit_opt: The limit of notifications to fetch. If the limit is not specified, it defaults to 25
SELECT * FROM inbox_notifications WHERE
user_id = @user_id AND
template_id = ANY(@templates::UUID[]) AND
targets @> COALESCE(@targets, ARRAY[]::UUID[]) AND
(@read_status::inbox_notification_read_status = 'all' OR (@read_status::inbox_notification_read_status = 'unread' AND read_at IS NULL) OR (@read_status::inbox_notification_read_status = 'read' AND read_at IS NOT NULL)) AND
(@created_at_opt::TIMESTAMPTZ = '0001-01-01 00:00:00Z' OR created_at < @created_at_opt::TIMESTAMPTZ)
ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF(@limit_opt :: INT, 0), 25));
-- name: GetInboxNotificationByID :one
SELECT * FROM inbox_notifications WHERE id = $1;
-- name: CountUnreadInboxNotificationsByUserID :one
SELECT COUNT(*) FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL;
-- name: InsertInboxNotification :one
INSERT INTO
inbox_notifications (
id,
user_id,
template_id,
targets,
title,
content,
icon,
actions,
created_at
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *;
-- name: UpdateInboxNotificationReadStatus :exec
UPDATE
inbox_notifications
SET
read_at = $1
WHERE
id = $2;
+1
View File
@@ -21,6 +21,7 @@ const (
UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
UniqueGroupsPkey UniqueConstraint = "groups_pkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
UniqueInboxNotificationsPkey UniqueConstraint = "inbox_notifications_pkey" // ALTER TABLE ONLY inbox_notifications ADD CONSTRAINT inbox_notifications_pkey PRIMARY KEY (id);
UniqueJfrogXrayScansPkey UniqueConstraint = "jfrog_xray_scans_pkey" // ALTER TABLE ONLY jfrog_xray_scans ADD CONSTRAINT jfrog_xray_scans_pkey PRIMARY KEY (agent_id, workspace_id);
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
UniqueLicensesPkey UniqueConstraint = "licenses_pkey" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_pkey PRIMARY KEY (id);