-
Notifications
You must be signed in to change notification settings - Fork 875
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
Conversation
acc1f08
to
6c9e41f
Compare
e035bf9
to
27c1592
Compare
title TEXT NOT NULL, | ||
content TEXT NOT NULL, | ||
icon TEXT NOT NULL, | ||
actions JSONB NOT NULL, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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}, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
-- 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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[], |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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}, |
There was a problem hiding this comment.
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.
Update on the PR with some changes :
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. |
There was a problem hiding this 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}, |
There was a problem hiding this comment.
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?
-- name: CountUnreadInboxNotificationsByUserID :one | ||
SELECT COUNT(*) FROM inbox_notifications WHERE user_id = $1 AND read_at IS NULL; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
coderd/database/types.go
Outdated
type InboxNotificationReadStatus string | ||
|
||
const ( | ||
InboxNotificationReadStatusRead InboxNotificationReadStatus = "READ" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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:
-- name: GetInboxNotificationsByUserIDFilteredByTemplatesAndTargets :many | |
-- name: GetFilteredInboxNotificationsByUserID :many |
There was a problem hiding this comment.
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}, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
This PR is linked to the following issue.
The objective is to create the DB layer and migration for the new
Coder Inbox
.