-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
feat(js): js sdk feeds module #5688
Conversation
❌ Deploy Preview for novu-design failed. Why did it fail? →
|
private removeNullUndefined(obj) { | ||
return Object.fromEntries( | ||
Object.entries(obj).filter(([_, value]) => value != 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.
the helper function that is used on the URL query params to remove the keys with null
or undefined
values
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-pick comment: Alternatively we can use the URLSearchParams interface.
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.
executedType: `${ButtonTypeEnum}`, | ||
status: `${MessageActionStatusEnum}`, |
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 is the way to convert enum keys to union, this way we don't require using the enum from @novu/shared
@@ -1,5 +1,4 @@ | |||
export enum ButtonTypeEnum { | |||
PRIMARY = 'primary', | |||
SECONDARY = 'secondary', | |||
CLICKED = 'clicked', |
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.
not used
/** | ||
* @deprecated use markMessagesAs instead | ||
*/ |
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 endpoint is deprecated so this function as well
async markMessagesAs({ | ||
messageId, | ||
markAs, | ||
}: { | ||
messageId: string | string[]; | ||
markAs: `${MarkMessagesAsEnum}`; | ||
}): Promise<INotificationDto[]> { | ||
return await this.httpClient.post(`/widgets/messages/mark-as`, { | ||
messageId, | ||
markAs, | ||
}); | ||
} |
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 new function that should be used instead
this.cta = notification.cta; | ||
this.payload = notification.payload; | ||
this.overrides = notification.overrides; | ||
} | ||
|
||
markAsRead(): Promise<Notification> { |
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 interface for some of these functions is simpler and doesn't require passing any args
import type { NotificationActionStatus, NotificationButton, NotificationStatus } from '../types'; | ||
import { Notification } from './notification'; | ||
|
||
export type FetchFeedArgs = { |
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.
types for all function args from feeds module
ApiServiceSingleton.getInstance({ backendUrl: options.backendUrl }); | ||
this.#emitter = NovuEventEmitter.getInstance({ recreate: true }); |
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 singleton are created with this class initialization
@@ -0,0 +1,43 @@ | |||
import { Novu } from './src'; |
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 test file to verify the sdk, I use it with command tsx ./test-sdk.ts
"noImplicitAny": true, | ||
"strictNullChecks": true, |
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.
stricter ts checks
this._emitter = NovuEventEmitter.getInstance(); | ||
this._apiService = ApiServiceSingleton.getInstance(); |
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.
Alternatively we can use a factory pattern and get the event emitter from the closure. Testing would be slightly easier in that case due to dependency injection (DI).
} | ||
|
||
async markNotificationActionAs(args: MarkNotificationActionAsByIdArgs): Promise<Notification>; | ||
async markNotificationActionAs(args: MarkNotificationActionAsByInstanceArgs): Promise<Notification>; |
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.
Let’s add specific method per action for better DX.
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.
Then we should do the same for the other functions as well, like the same way I did in the Notification class:
- markNotificationAsRead, markNotificationAsSeen, markNotificationAsUnseen, markNotificationAsUnread
- markNotificationPrimaryActionAsDone, markNotificationPrimaryActionAsPending, markNotificationSecondaryActionAsDone, markNotificationSecondaryActionAsPending
But then the question would be whether we should also emit separate events?
DONE = 'done', | ||
} | ||
|
||
export enum ActorType { |
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 enum is convoluted. Is its purpose to drive the icon selection?
the none status should be replaced with the null type. The as far as I can tell there is a user vs system actor.
Id also argue that it might be more helpful to replace it with an icon property.
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'm not sure either if it's useful to the users at all, like they are just interested in the actor icon URL and nothing more.
But that's just what we have rn and we can improve later.
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.
|
||
export type Session = { | ||
token: string; | ||
profile: { |
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.
profile: { | |
subscriber: |
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 responded in another PR: https://github.com/novuhq/novu/pull/5665/files#r1630782252. I'll create a ticket for that.
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.
0a7eea7
to
8e8c303
Compare
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.
Looks good,
Have added couple of small comments
case NotificationStatus.SEEN: | ||
return { seen: true }; | ||
case NotificationStatus.UNSEEN: | ||
return { seen: false }; |
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.
return { seen: false }; | |
return { seen: false, read: false }; |
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 didn't add that just because we haven't fixed that on the server side. Like to not confuse the users with an optimistic state different from the result (server) state.
return { read: notification.read, seen: notification.seen }; | ||
case NotificationStatus.SEEN: | ||
case NotificationStatus.UNSEEN: | ||
return { seen: notification.seen }; |
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.
return { seen: notification.seen }; | |
return { seen: notification.seen, read: notification.read }; |
|
||
export class Feeds extends BaseModule { | ||
async fetch({ page = 1, ...restOptions }: FetchFeedOptions): Promise<PaginatedResponse<Notification>> { | ||
async fetch({ page = 0, status, ...restOptions }: FetchFeedArgs = {}): Promise<PaginatedResponse<Notification>> { |
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.
💭 thought (non-blocking): I think we should start page numbers from 1
and handle internally by page = --page;
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.
yeah, everywhere in our APIs we are using a page from 0, and in sdks as well :(
9448d67
to
e62dd85
Compare
private removeNullUndefined(obj) { | ||
return Object.fromEntries( | ||
Object.entries(obj).filter(([_, value]) => value != 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.
nit-pick comment: Alternatively we can use the URLSearchParams interface.
messageId: string | string[]; | ||
markAs: `${MarkMessagesAsEnum}`; | ||
}): Promise<INotificationDto[]> { | ||
return await this.httpClient.post(`/widgets/messages/mark-as`, { |
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.
In the new API, I'd like to suggest having a dedicated method per action in a RESTful way. /v1/inbox/messages/:id/mark-as-read
without any body. This is not actionable for now.
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'll create a ticket.
this._emitter = NovuEventEmitter.getInstance(); | ||
this._apiService = ApiServiceSingleton.getInstance(); |
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.
Correct, that's where DI helps.
export type Events = SessionInitializeEvents & | ||
FeedFetchEvents & | ||
FeedFetchCountEvents & | ||
FeedMarkNotificationsAsEvents & | ||
FeedMarkAllNotificationsAsEvents & | ||
FeedRemoveNotificationsEvents & | ||
FeedRemoveAllNotificationsEvents & | ||
NotificationMarkAsEvents & | ||
NotificationMarkActionAsEvents & | ||
NotificationRemoveEvents; |
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 am torn about this one. It feels a bit convoluted if we start from the event naming convention, deconstruct the parts, and then define the payload based on that. How about the opposite: start with an event map and build types from it?
For example.
const response = await this._apiService.getNotificationsList(page, restOptions); | ||
const response = await this._apiService.getNotificationsList(page, { | ||
...restOptions, | ||
...(status && SEEN_OR_UNSEEN.includes(status) && { seen: status === NotificationStatus.SEEN }), |
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 conversion of status between the SDK and the apiService seems unnecessary. Is it mandatory to keep the apiService backward compatible?
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.
Unfortunately, it is a public package :( but I think that we would need to deprecate it and suggest using the new SDK instead
); | ||
} | ||
|
||
async markNotificationsAs(args: MarkNotificationsAsByIdsArgs): Promise<Notification[]>; |
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 suggest we go with a specific method on this public API for better DX, such as novu.feeds.markAllAsRead()
.
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 you are saying to go additionally with these then yes we can introduce this granularity, but need to be consistent with that pattern and implement also for other methods:
- for single notification:
markNotificationAsRead, markNotificationAsUnread, markNotificationAsSeen, markNotificationAsUnseen
- for selected notifications:
markNotificationsAsRead, markNotificationsAsUnread, markNotificationsAsSeen, markNotificationsAsUnseen
- for all notifications:
markAllAsRead, markAllAsUnread, markAllAsSeen, markAllAsUnseen
- similar pattern to actions
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.
And then the question is whether all these should be treated as a separate events
69aa508
to
4fd00de
Compare
4fd00de
to
ab164a5
Compare
* feat(js): js sdk feeds module (#5688) * feat(js): improve the package json exports and tsup config * feat(js): lazy session initialization and interface fixes * feat(js): js sdk feeds module * feat(js): js sdk preferences (#5701) * feat(js): js sdk preferences * feat(js): handling the web socket connection and events (#5704) * feat(js): handling the web socket connection and events * feat: ui solid --------- Co-authored-by: Biswajeet Das <[email protected]> * fix: caching for session initialize * fix: worker --------- Co-authored-by: Paweł Tymczuk <[email protected]> Co-authored-by: Biswajeet Das <[email protected]> Co-authored-by: Dima Grossman <[email protected]>
What changed? Why was the change needed?
The changes include:
@novu/client
ApiService types improvementsScreenshots
The UMD size limits checker script
Testing the JS SDK
Expand for optional sections
Related enterprise PR
Special notes for your reviewer