Skip to content

feat: add notifications inbox db #16599

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Mar 3, 2025
Merged

feat: add notifications inbox db #16599

merged 40 commits into from
Mar 3, 2025

Conversation

defelmnq
Copy link
Contributor

This PR is linked to the following issue.

The objective is to create the DB layer and migration for the new Coder Inbox.

@defelmnq defelmnq self-assigned this Feb 18, 2025
@defelmnq defelmnq force-pushed the notif-inbox/int-334 branch from acc1f08 to 6c9e41f Compare February 20, 2025 11:03
@defelmnq defelmnq force-pushed the notif-inbox/int-334 branch from e035bf9 to 27c1592 Compare February 21, 2025 05:41
@defelmnq defelmnq marked this pull request as ready for review February 21, 2025 07:01
title TEXT NOT NULL,
content TEXT NOT NULL,
icon TEXT NOT NULL,
actions JSONB NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that you have title and content, I assume you're going to be accepting the fully rendered content (probably Markdown) of the notification in these fields. That means that CTAs will already be included in content, I think. What role will actions play here?

Copy link
Contributor Author

@defelmnq defelmnq Feb 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to generate the title, content and actions using a Dispatcher method similar to SMTP.
the payload field contains everything to have these three elements.

The reason why I had in mind to keep the actions separated from the body is that , based on the client (VSCode for example) we will not be able to handle the actions the same way. If we integrate the actions in the body using markdown it will not be compatible with everything.

So we just return labels and associated URLs and each client can handle it their way.

wdyt ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we integrate the actions in the body using markdown it will not be compatible with everything.

I think this is how it's already done, so we may need to change some things around later.
For now let's go with this until we reach any blockers.

},
Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgMemberMe, orgAdmin},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does orgMemberMe have privileges to policy.ActionCreate inbox notifications?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my RBAC understanding orgMemberMe is the current user.
Associated to this part on the which we use the currentUser id.

In the definition of the ResourceInboxNotification, I assign the currentUser to InboxNotification using the WithOwner(currentUser.String()) method.

TL;DR - orgMemberMe is the owner of the resource in the test - and the owner of the resource should be able to access it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want memberMe in here as well?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@defelmnq I understand why the current user should be able to read their own messages, but why should they be allowed to create new messages? That should be a system privilege.

Comment on lines 1 to 5
-- name: FetchUnreadInboxNotificationsByUserID :many
SELECT * FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL ORDER BY created_at DESC;

-- name: FetchInboxNotificationsByUserID :many
SELECT * FROM inbox_notifications WHERE user_id = $1 ORDER BY created_at DESC;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These queries are identical except for read_at IS NULL; rather parameterise FetchInboxNotificationsByUserID so you can explicitly filter for read/unread, or neither.

FetchUnreadInboxNotificationsByUserIDFilteredByTemplatesAndTargets could be refactored out according to the same pattern, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I tried to iterate here and from what I found on the internet - I can't find a way to achieve that with sqlc.

sqlc, with narg, allow to have null parameters and optional values, with default values if we want - here the objective is a bit different as we want to completely change the query and the WHERE clause based on if a parameter is present or not.

If you have more idea, I could check.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can alternatively just write a custom query yourself (see coderd/database/modelqueries.go) and not need to deal with sqlc's idiosyncracies.

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[],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Targets do not reference a specific table.

Targets represent another entity which is connected to the notification - this field will be used to filter and search.

@@ -0,0 +1,15 @@
CREATE TABLE inbox_notifications (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

references the user that receives the notification.

},
Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgMemberMe, orgAdmin},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my RBAC understanding orgMemberMe is the current user.
Associated to this part on the which we use the currentUser id.

In the definition of the ResourceInboxNotification, I assign the currentUser to InboxNotification using the WithOwner(currentUser.String()) method.

TL;DR - orgMemberMe is the owner of the resource in the test - and the owner of the resource should be able to access it.

@defelmnq
Copy link
Contributor Author

Update on the PR with some changes :

  • Naming for the DB methods has been changed to fit with the existing ones.
  • Added a new method CountUnreadNotificationsByUserID which was missing
  • Added pagination to list methods

About the pagination - because of the way notifications can be pushed (in real-time , potentially a lot of notifs quickly) - I decided to paginate using IDs instead of an offset which would be hard to maintain.

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking comments, some suggestions below.

},
Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgMemberMe, orgAdmin},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want memberMe in here as well?

Comment on lines +46 to +47
-- name: CountUnreadInboxNotificationsByUserID :one
SELECT COUNT(*) FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do we plan to use this query? If we're just displaying the unread notifications count in the UI, would it suffice to just call len(GetUnreadInboxNotificationsByUserID()? My assumption is that the UI will be fetching unread notifications anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query will be called in two cases :

  • An endpoint to get the number of unread notifications (without any notifications - just the count)
  • Whenever you want to list the number of notifications (unread or not, filtered or not) - we'll include the unread_count

-- name: GetUnreadInboxNotificationsByUserIDFilteredByTemplatesAndTargets :many
-- 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a caller, it's not really clear to me what kind of UUID a target should be without looking at example usages. Could this be reworded slightly to be clearer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obligatory reminder to double-check migration number before merging!

type InboxNotificationReadStatus string

const (
InboxNotificationReadStatusRead InboxNotificationReadStatus = "READ"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather create a SQL type for this to get automatic codegen
Nit: make this lowercase to match other types' values

ORDER BY created_at DESC
LIMIT (COALESCE(NULLIF(@limit_opt :: INT, 0), 25));

-- name: GetInboxNotificationsByUserIDFilteredByTemplatesAndTargets :many
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: for future-proofing, rather drop the specificity here in case we add more filter options later:

Suggested change
-- name: GetInboxNotificationsByUserIDFilteredByTemplatesAndTargets :many
-- name: GetFilteredInboxNotificationsByUserID :many

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed ✅

},
Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, orgMemberMe, orgAdmin},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@defelmnq I understand why the current user should be able to read their own messages, but why should they be allowed to create new messages? That should be a system privilege.

Copy link
Contributor

@dannykopping dannykopping left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@defelmnq defelmnq merged commit c074f77 into main Mar 3, 2025
33 of 35 checks passed
@defelmnq defelmnq deleted the notif-inbox/int-334 branch March 3, 2025 09:12
@github-actions github-actions bot locked and limited conversation to collaborators Mar 3, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants