N
Novu5w ago
Sigex

Headless service `fetchNotifications` should accept 'archived' filter

Really need this to be merged. https://github.com/novuhq/novu/pull/7136 It's to add the archived filter onto the fetch Notifications. It's already on getFilterQueryForMessage() just need to add it in a couple of other places. Mainly apps/api/src/app/subscribers/dtos/get-in-app-notification-feed-for-subscriber.dto.ts
GitHub
fix(api): headless service fetchNotifications should accept archive...
What changed? Why was the change needed? The fetchNotifications() within headless.service.ts uses the type IStoreQuery for the query. But this property does not have archived: boolean even though ...
6 Replies
Sigex
SigexOP5w ago
@Pawan Jain
Pawan Jain
Pawan Jain5w ago
@Sigex I have few questions: 1. Are you using @novu/headless npm package? 2. Do you have @novu/framework based workflows or old UI based workflows? 3. Are you using novu cloud or self hosting Novu?
Sigex
SigexOP5w ago
Hi Pawan, 1.Yes. We use the same approach as Midday
import { HeadlessService, type IStoreQuery } from "@novu/headless";

// ...

headlessService.fetchNotifications({
query,
listener: ({}) => {},
onSuccess: (response) => {
setLoading(false);
setNotifications(response.data);
},
});
import { HeadlessService, type IStoreQuery } from "@novu/headless";

// ...

headlessService.fetchNotifications({
query,
listener: ({}) => {},
onSuccess: (response) => {
setLoading(false);
setNotifications(response.data);
},
});
https://github.com/midday-ai/midday/blob/main/apps/dashboard/src/hooks/use-notifications.ts 2. Yes. We use @novu/framework to define workflow's and we use @novu/framework/next 3. Novu cloud. --- The query object for the fetchNotifications is of type IStoreQuery
export interface IStoreQuery {
feedIdentifier?: string | string[];
seen?: boolean;
read?: boolean;
limit?: number;
payload?: Record<string, unknown>;
}
export interface IStoreQuery {
feedIdentifier?: string | string[];
seen?: boolean;
read?: boolean;
limit?: number;
payload?: Record<string, unknown>;
}
Notice the lack of archived as one of the allowed filters. The fetchNotifications for the Headless Service is defined in. packages/headless/src/lib/headless.service.ts The heavy work is done by this.api.getNotificationsList(page, query) That hands off too;
await this.httpClient.getFullResponse(`/widgets/notifications/feed`, {
page,
...(payloadString && { payload: payloadString }),
...rest,
});
await this.httpClient.getFullResponse(`/widgets/notifications/feed`, {
page,
...(payloadString && { payload: payloadString }),
...rest,
});
The widgets controller has a Get method which uses CQRS to execute the following command GetNotificationsFeedCommand.
GitHub
midday/apps/dashboard/src/hooks/use-notifications.ts at main · midd...
Invoicing, Time tracking, File reconciliation, Storage, Financial Overview & your own Assistant made for Freelancers - midday-ai/midday
Sigex
SigexOP5w ago
@UseGuards(AuthGuard('subscriberJwt'))
@Get('/notifications/feed')
@ApiQuery({
name: 'seen',
type: Boolean,
required: false,
})
async getNotificationsFeed(
@SubscriberSession() subscriberSession: SubscriberEntity,
@Query() query: GetNotificationsFeedDto
) {
let feedsQuery: string[] | undefined;
if (query.feedIdentifier) {
feedsQuery = Array.isArray(query.feedIdentifier) ? query.feedIdentifier : [query.feedIdentifier];
}

const command = GetNotificationsFeedCommand.create({
organizationId: subscriberSession._organizationId,
subscriberId: subscriberSession.subscriberId,
environmentId: subscriberSession._environmentId,
page: query.page,
feedId: feedsQuery,
query: { seen: query.seen, read: query.read },
limit: query.limit,
payload: query.payload,
});

return await this.getNotificationsFeedUsecase.execute(command);
}
@UseGuards(AuthGuard('subscriberJwt'))
@Get('/notifications/feed')
@ApiQuery({
name: 'seen',
type: Boolean,
required: false,
})
async getNotificationsFeed(
@SubscriberSession() subscriberSession: SubscriberEntity,
@Query() query: GetNotificationsFeedDto
) {
let feedsQuery: string[] | undefined;
if (query.feedIdentifier) {
feedsQuery = Array.isArray(query.feedIdentifier) ? query.feedIdentifier : [query.feedIdentifier];
}

const command = GetNotificationsFeedCommand.create({
organizationId: subscriberSession._organizationId,
subscriberId: subscriberSession.subscriberId,
environmentId: subscriberSession._environmentId,
page: query.page,
feedId: feedsQuery,
query: { seen: query.seen, read: query.read },
limit: query.limit,
payload: query.payload,
});

return await this.getNotificationsFeedUsecase.execute(command);
}
The main execute for this makes a call to;
const feed = await this.messageRepository.findBySubscriberChannel(
command.environmentId,
subscriber._id,
ChannelTypeEnum.IN_APP,
{ feedId: command.feedId, seen: command.query.seen, read: command.query.read, payload },
{
limit: command.limit,
skip: command.page * command.limit,
}
);
const feed = await this.messageRepository.findBySubscriberChannel(
command.environmentId,
subscriber._id,
ChannelTypeEnum.IN_APP,
{ feedId: command.feedId, seen: command.query.seen, read: command.query.read, payload },
{
limit: command.limit,
skip: command.page * command.limit,
}
);
Heading over to findBySubscriberChannel we see;
async findBySubscriberChannel(
environmentId: string,
subscriberId: string,
channel: ChannelTypeEnum,
query: { feedId?: string[]; seen?: boolean; read?: boolean; archived?: boolean; payload?: object } = {},
options: { limit: number; skip?: number } = { limit: 10 }
) {
const requestQuery = await this.getFilterQueryForMessage(environmentId, subscriberId, channel, query);

const messages = await this.MongooseModel.find(requestQuery, '', {
limit: options.limit,
skip: options.skip,
sort: '-createdAt',
})
.read('secondaryPreferred')
.populate('template', '_id tags')
.populate('subscriber', '_id firstName lastName avatar subscriberId')
.populate('actorSubscriber', '_id firstName lastName avatar subscriberId');

return this.mapEntities(messages);
}
async findBySubscriberChannel(
environmentId: string,
subscriberId: string,
channel: ChannelTypeEnum,
query: { feedId?: string[]; seen?: boolean; read?: boolean; archived?: boolean; payload?: object } = {},
options: { limit: number; skip?: number } = { limit: 10 }
) {
const requestQuery = await this.getFilterQueryForMessage(environmentId, subscriberId, channel, query);

const messages = await this.MongooseModel.find(requestQuery, '', {
limit: options.limit,
skip: options.skip,
sort: '-createdAt',
})
.read('secondaryPreferred')
.populate('template', '_id tags')
.populate('subscriber', '_id firstName lastName avatar subscriberId')
.populate('actorSubscriber', '_id firstName lastName avatar subscriberId');

return this.mapEntities(messages);
}
Now this is where it gets interesting... So far all the way down this Rabit Hole archived has not been present on the query object.
query: { feedId?: string[]; seen?: boolean; read?: boolean; archived?: boolean; payload?: object } = {},
query: { feedId?: string[]; seen?: boolean; read?: boolean; archived?: boolean; payload?: object } = {},
But... the query object is parsed into getFilterQueryForMessage which does support archived.
private async getFilterQueryForMessage(
environmentId: string,
subscriberId: string,
channel: ChannelTypeEnum,
query: {
feedId?: string[];
tags?: string[];
seen?: boolean;
read?: boolean;
archived?: boolean;
payload?: object;
} = {}
): Promise<MessageQuery & EnforceEnvId> {
// ...
}
private async getFilterQueryForMessage(
environmentId: string,
subscriberId: string,
channel: ChannelTypeEnum,
query: {
feedId?: string[];
tags?: string[];
seen?: boolean;
read?: boolean;
archived?: boolean;
payload?: object;
} = {}
): Promise<MessageQuery & EnforceEnvId> {
// ...
}
This getFilterQueryForMessage is used by getCount as well which does accept archived. So the solution here is to simply pass archived all the way down. Note: The IStoreQuery we use filters using the payload. Which is not available on the notifications.list which can be accessed via useNovu. So that one is not suitable. I am aware it does have archived as a filter.
Novu_Bot
Novu_Bot5w ago
@Sigex, you just advanced to level 1!
Sigex
SigexOP5w ago
I noticed I missed a bit; https://github.com/novuhq/novu/pull/7136/files so I updated the PR.

Did you find this page helpful?