Skip to content

Commit c074f77

Browse files
authored
feat: add notifications inbox db (#16599)
This PR is linked [to the following issue](coder/internal#334). The objective is to create the DB layer and migration for the new `Coder Inbox`.
1 parent d0e2060 commit c074f77

27 files changed

+966
-0
lines changed

coderd/apidoc/docs.go

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbauthz/dbauthz.go

+33
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ var (
281281
DisplayName: "Notifier",
282282
Site: rbac.Permissions(map[string][]policy.Action{
283283
rbac.ResourceNotificationMessage.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
284+
rbac.ResourceInboxNotification.Type: {policy.ActionCreate},
284285
}),
285286
Org: map[string][]rbac.Permission{},
286287
User: []rbac.Permission{},
@@ -1126,6 +1127,14 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
11261127
return q.db.CleanTailnetTunnels(ctx)
11271128
}
11281129

1130+
func (q *querier) CountUnreadInboxNotificationsByUserID(ctx context.Context, userID uuid.UUID) (int64, error) {
1131+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceInboxNotification.WithOwner(userID.String())); err != nil {
1132+
return 0, err
1133+
}
1134+
return q.db.CountUnreadInboxNotificationsByUserID(ctx, userID)
1135+
}
1136+
1137+
// TODO: Handle org scoped lookups
11291138
func (q *querier) CustomRoles(ctx context.Context, arg database.CustomRolesParams) ([]database.CustomRole, error) {
11301139
roleObject := rbac.ResourceAssignRole
11311140
if arg.OrganizationID != uuid.Nil {
@@ -1689,6 +1698,10 @@ func (q *querier) GetFileTemplates(ctx context.Context, fileID uuid.UUID) ([]dat
16891698
return q.db.GetFileTemplates(ctx, fileID)
16901699
}
16911700

1701+
func (q *querier) GetFilteredInboxNotificationsByUserID(ctx context.Context, arg database.GetFilteredInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
1702+
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetFilteredInboxNotificationsByUserID)(ctx, arg)
1703+
}
1704+
16921705
func (q *querier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.GitSSHKey, error) {
16931706
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetGitSSHKey)(ctx, userID)
16941707
}
@@ -1748,6 +1761,14 @@ func (q *querier) GetHungProvisionerJobs(ctx context.Context, hungSince time.Tim
17481761
return q.db.GetHungProvisionerJobs(ctx, hungSince)
17491762
}
17501763

1764+
func (q *querier) GetInboxNotificationByID(ctx context.Context, id uuid.UUID) (database.InboxNotification, error) {
1765+
return fetchWithAction(q.log, q.auth, policy.ActionRead, q.db.GetInboxNotificationByID)(ctx, id)
1766+
}
1767+
1768+
func (q *querier) GetInboxNotificationsByUserID(ctx context.Context, userID database.GetInboxNotificationsByUserIDParams) ([]database.InboxNotification, error) {
1769+
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetInboxNotificationsByUserID)(ctx, userID)
1770+
}
1771+
17511772
func (q *querier) GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg database.GetJFrogXrayScanByWorkspaceAndAgentIDParams) (database.JfrogXrayScan, error) {
17521773
if _, err := fetch(q.log, q.auth, q.db.GetWorkspaceByID)(ctx, arg.WorkspaceID); err != nil {
17531774
return database.JfrogXrayScan{}, err
@@ -3079,6 +3100,10 @@ func (q *querier) InsertGroupMember(ctx context.Context, arg database.InsertGrou
30793100
return update(q.log, q.auth, fetch, q.db.InsertGroupMember)(ctx, arg)
30803101
}
30813102

3103+
func (q *querier) InsertInboxNotification(ctx context.Context, arg database.InsertInboxNotificationParams) (database.InboxNotification, error) {
3104+
return insert(q.log, q.auth, rbac.ResourceInboxNotification.WithOwner(arg.UserID.String()), q.db.InsertInboxNotification)(ctx, arg)
3105+
}
3106+
30823107
func (q *querier) InsertLicense(ctx context.Context, arg database.InsertLicenseParams) (database.License, error) {
30833108
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceLicense); err != nil {
30843109
return database.License{}, err
@@ -3666,6 +3691,14 @@ func (q *querier) UpdateInactiveUsersToDormant(ctx context.Context, lastSeenAfte
36663691
return q.db.UpdateInactiveUsersToDormant(ctx, lastSeenAfter)
36673692
}
36683693

3694+
func (q *querier) UpdateInboxNotificationReadStatus(ctx context.Context, args database.UpdateInboxNotificationReadStatusParams) error {
3695+
fetchFunc := func(ctx context.Context, args database.UpdateInboxNotificationReadStatusParams) (database.InboxNotification, error) {
3696+
return q.db.GetInboxNotificationByID(ctx, args.ID)
3697+
}
3698+
3699+
return update(q.log, q.auth, fetchFunc, q.db.UpdateInboxNotificationReadStatus)(ctx, args)
3700+
}
3701+
36693702
func (q *querier) UpdateMemberRoles(ctx context.Context, arg database.UpdateMemberRolesParams) (database.OrganizationMember, error) {
36703703
// Authorized fetch will check that the actor has read access to the org member since the org member is returned.
36713704
member, err := database.ExpectOne(q.OrganizationMembers(ctx, database.OrganizationMembersParams{

coderd/database/dbauthz/dbauthz_test.go

+135
Original file line numberDiff line numberDiff line change
@@ -4466,6 +4466,141 @@ func (s *MethodTestSuite) TestNotifications() {
44664466
Disableds: []bool{true, false},
44674467
}).Asserts(rbac.ResourceNotificationPreference.WithOwner(user.ID.String()), policy.ActionUpdate)
44684468
}))
4469+
4470+
s.Run("GetInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) {
4471+
u := dbgen.User(s.T(), db, database.User{})
4472+
4473+
notifID := uuid.New()
4474+
4475+
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
4476+
ID: notifID,
4477+
UserID: u.ID,
4478+
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
4479+
Title: "test title",
4480+
Content: "test content notification",
4481+
Icon: "https://coder.com/favicon.ico",
4482+
Actions: json.RawMessage("{}"),
4483+
})
4484+
4485+
check.Args(database.GetInboxNotificationsByUserIDParams{
4486+
UserID: u.ID,
4487+
ReadStatus: database.InboxNotificationReadStatusAll,
4488+
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionRead).Returns([]database.InboxNotification{notif})
4489+
}))
4490+
4491+
s.Run("GetFilteredInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) {
4492+
u := dbgen.User(s.T(), db, database.User{})
4493+
4494+
notifID := uuid.New()
4495+
4496+
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
4497+
4498+
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
4499+
ID: notifID,
4500+
UserID: u.ID,
4501+
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
4502+
Targets: targets,
4503+
Title: "test title",
4504+
Content: "test content notification",
4505+
Icon: "https://coder.com/favicon.ico",
4506+
Actions: json.RawMessage("{}"),
4507+
})
4508+
4509+
check.Args(database.GetFilteredInboxNotificationsByUserIDParams{
4510+
UserID: u.ID,
4511+
Templates: []uuid.UUID{notifications.TemplateWorkspaceAutoUpdated},
4512+
Targets: []uuid.UUID{u.ID},
4513+
ReadStatus: database.InboxNotificationReadStatusAll,
4514+
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionRead).Returns([]database.InboxNotification{notif})
4515+
}))
4516+
4517+
s.Run("GetInboxNotificationByID", s.Subtest(func(db database.Store, check *expects) {
4518+
u := dbgen.User(s.T(), db, database.User{})
4519+
4520+
notifID := uuid.New()
4521+
4522+
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
4523+
4524+
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
4525+
ID: notifID,
4526+
UserID: u.ID,
4527+
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
4528+
Targets: targets,
4529+
Title: "test title",
4530+
Content: "test content notification",
4531+
Icon: "https://coder.com/favicon.ico",
4532+
Actions: json.RawMessage("{}"),
4533+
})
4534+
4535+
check.Args(notifID).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionRead).Returns(notif)
4536+
}))
4537+
4538+
s.Run("CountUnreadInboxNotificationsByUserID", s.Subtest(func(db database.Store, check *expects) {
4539+
u := dbgen.User(s.T(), db, database.User{})
4540+
4541+
notifID := uuid.New()
4542+
4543+
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
4544+
4545+
_ = dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
4546+
ID: notifID,
4547+
UserID: u.ID,
4548+
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
4549+
Targets: targets,
4550+
Title: "test title",
4551+
Content: "test content notification",
4552+
Icon: "https://coder.com/favicon.ico",
4553+
Actions: json.RawMessage("{}"),
4554+
})
4555+
4556+
check.Args(u.ID).Asserts(rbac.ResourceInboxNotification.WithOwner(u.ID.String()), policy.ActionRead).Returns(int64(1))
4557+
}))
4558+
4559+
s.Run("InsertInboxNotification", s.Subtest(func(db database.Store, check *expects) {
4560+
u := dbgen.User(s.T(), db, database.User{})
4561+
4562+
notifID := uuid.New()
4563+
4564+
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
4565+
4566+
check.Args(database.InsertInboxNotificationParams{
4567+
ID: notifID,
4568+
UserID: u.ID,
4569+
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
4570+
Targets: targets,
4571+
Title: "test title",
4572+
Content: "test content notification",
4573+
Icon: "https://coder.com/favicon.ico",
4574+
Actions: json.RawMessage("{}"),
4575+
}).Asserts(rbac.ResourceInboxNotification.WithOwner(u.ID.String()), policy.ActionCreate)
4576+
}))
4577+
4578+
s.Run("UpdateInboxNotificationReadStatus", s.Subtest(func(db database.Store, check *expects) {
4579+
u := dbgen.User(s.T(), db, database.User{})
4580+
4581+
notifID := uuid.New()
4582+
4583+
targets := []uuid.UUID{u.ID, notifications.TemplateWorkspaceAutoUpdated}
4584+
readAt := dbtestutil.NowInDefaultTimezone()
4585+
4586+
notif := dbgen.NotificationInbox(s.T(), db, database.InsertInboxNotificationParams{
4587+
ID: notifID,
4588+
UserID: u.ID,
4589+
TemplateID: notifications.TemplateWorkspaceAutoUpdated,
4590+
Targets: targets,
4591+
Title: "test title",
4592+
Content: "test content notification",
4593+
Icon: "https://coder.com/favicon.ico",
4594+
Actions: json.RawMessage("{}"),
4595+
})
4596+
4597+
notif.ReadAt = sql.NullTime{Time: readAt, Valid: true}
4598+
4599+
check.Args(database.UpdateInboxNotificationReadStatusParams{
4600+
ID: notifID,
4601+
ReadAt: sql.NullTime{Time: readAt, Valid: true},
4602+
}).Asserts(rbac.ResourceInboxNotification.WithID(notifID).WithOwner(u.ID.String()), policy.ActionUpdate)
4603+
}))
44694604
}
44704605

44714606
func (s *MethodTestSuite) TestOAuth2ProviderApps() {

coderd/database/dbgen/dbgen.go

+16
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,22 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat
450450
return mem
451451
}
452452

453+
func NotificationInbox(t testing.TB, db database.Store, orig database.InsertInboxNotificationParams) database.InboxNotification {
454+
notification, err := db.InsertInboxNotification(genCtx, database.InsertInboxNotificationParams{
455+
ID: takeFirst(orig.ID, uuid.New()),
456+
UserID: takeFirst(orig.UserID, uuid.New()),
457+
TemplateID: takeFirst(orig.TemplateID, uuid.New()),
458+
Targets: takeFirstSlice(orig.Targets, []uuid.UUID{}),
459+
Title: takeFirst(orig.Title, testutil.GetRandomName(t)),
460+
Content: takeFirst(orig.Content, testutil.GetRandomName(t)),
461+
Icon: takeFirst(orig.Icon, ""),
462+
Actions: orig.Actions,
463+
CreatedAt: takeFirst(orig.CreatedAt, dbtime.Now()),
464+
})
465+
require.NoError(t, err, "insert notification")
466+
return notification
467+
}
468+
453469
func Group(t testing.TB, db database.Store, orig database.Group) database.Group {
454470
t.Helper()
455471

0 commit comments

Comments
 (0)