Skip to content
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

Merged
merged 8 commits into from
Jun 20, 2024
Merged

feat(js): js sdk feeds module #5688

merged 8 commits into from
Jun 20, 2024

Conversation

LetItRock
Copy link
Contributor

@LetItRock LetItRock commented Jun 6, 2024

What changed? Why was the change needed?

The changes include:

  • @novu/client ApiService types improvements
  • The build configuration for ESM, CJS - preserving folders structure, UMD - minification and gzip
  • Improvements to the size limits checker script
  • The Feeds module
  • The Notification ORM pattern
  • Event Emitter types improvements

Screenshots

The UMD size limits checker script

Screenshot 2024-06-06 at 17 29 52

Testing the JS SDK

Screenshot 2024-06-06 at 17 29 31

Expand for optional sections

Related enterprise PR

Special notes for your reviewer

@LetItRock LetItRock self-assigned this Jun 6, 2024
Copy link

linear bot commented Jun 6, 2024

COM-29 Implement Feeds

Copy link

netlify bot commented Jun 6, 2024

Deploy Preview for novu-design failed. Why did it fail? →

Name Link
🔨 Latest commit ab164a5
🔍 Latest deploy log https://app.netlify.com/sites/novu-design/deploys/66741e720f96850008fa2ab9

Comment on lines +47 to +51
private removeNullUndefined(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value != null)
);
}
Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it doesn't work :(
Screenshot 2024-06-12 at 18 21 40

Comment on lines +67 to +68
executedType: `${ButtonTypeEnum}`,
status: `${MessageActionStatusEnum}`,
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 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',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not used

Comment on lines +81 to +83
/**
* @deprecated use markMessagesAs instead
*/
Copy link
Contributor Author

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

Comment on lines +99 to +110
async markMessagesAs({
messageId,
markAs,
}: {
messageId: string | string[];
markAs: `${MarkMessagesAsEnum}`;
}): Promise<INotificationDto[]> {
return await this.httpClient.post(`/widgets/messages/mark-as`, {
messageId,
markAs,
});
}
Copy link
Contributor Author

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> {
Copy link
Contributor Author

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 = {
Copy link
Contributor Author

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

Comment on lines +23 to +24
ApiServiceSingleton.getInstance({ backendUrl: options.backendUrl });
this.#emitter = NovuEventEmitter.getInstance({ recreate: true });
Copy link
Contributor Author

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';
Copy link
Contributor Author

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

Comment on lines +8 to +9
"noImplicitAny": true,
"strictNullChecks": true,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

stricter ts checks

Comment on lines +20 to +21
this._emitter = NovuEventEmitter.getInstance();
this._apiService = ApiServiceSingleton.getInstance();
Copy link
Contributor

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>;
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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: {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
profile: {
subscriber:

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

@BiswaViraj BiswaViraj left a 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 };
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return { seen: false };
return { seen: false, read: false };

Copy link
Contributor Author

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 };
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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>> {
Copy link
Member

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;‏

Copy link
Contributor Author

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 :(

@LetItRock LetItRock requested a review from BiswaViraj June 7, 2024 14:55
Base automatically changed from com-28-js-sdk-session-lazy-initialization to next June 11, 2024 08:20
Comment on lines +47 to +51
private removeNullUndefined(obj) {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value != null)
);
}
Copy link
Contributor

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`, {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +20 to +21
this._emitter = NovuEventEmitter.getInstance();
this._apiService = ApiServiceSingleton.getInstance();
Copy link
Contributor

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.

Comment on lines +107 to +116
export type Events = SessionInitializeEvents &
FeedFetchEvents &
FeedFetchCountEvents &
FeedMarkNotificationsAsEvents &
FeedMarkAllNotificationsAsEvents &
FeedRemoveNotificationsEvents &
FeedRemoveAllNotificationsEvents &
NotificationMarkAsEvents &
NotificationMarkActionAsEvents &
NotificationRemoveEvents;
Copy link
Contributor

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 }),
Copy link
Contributor

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?

Copy link
Contributor Author

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

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().

Copy link
Contributor Author

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

Copy link
Contributor Author

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

@LetItRock LetItRock merged commit 9088aab into next Jun 20, 2024
28 checks passed
@LetItRock LetItRock deleted the com-29-js-sdk-feeds branch June 20, 2024 13:47
github-actions bot added a commit that referenced this pull request Jun 21, 2024
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants