diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 585fbd3626cf..bd5fd74ae3eb 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -49,6 +49,7 @@ import { WorkflowModule } from './app/workflows-v2/workflow.module'; import { WorkflowModuleV1 } from './app/workflows-v1/workflow-v1.module'; import { EnvironmentsModuleV1 } from './app/environments-v1/environments-v1.module'; import { EnvironmentsModule } from './app/environments-v2/environments.module'; +import { SubscriberModule } from './app/subscribers-v2/subscriber.module'; const enterpriseImports = (): Array | ForwardReference> => { const modules: Array | ForwardReference> = []; @@ -97,6 +98,7 @@ const baseModules: Array | Forward IntegrationModule, ChangeModule, SubscribersModule, + SubscriberModule, FeedsModule, LayoutsModule, MessagesModule, diff --git a/apps/api/src/app/subscribers-v2/dtos/subscriber-pagination.dto.ts b/apps/api/src/app/subscribers-v2/dtos/subscriber-pagination.dto.ts new file mode 100644 index 000000000000..189dc72c0d70 --- /dev/null +++ b/apps/api/src/app/subscribers-v2/dtos/subscriber-pagination.dto.ts @@ -0,0 +1,55 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { DirectionEnum, SubscriberPaginationDto } from '@novu/shared'; +import { Type } from 'class-transformer'; +import { IsEnum, IsNumber, IsOptional, IsString, Max, Min } from 'class-validator'; + +export class SubscriberPaginationRequestDto implements SubscriberPaginationDto { + @ApiProperty({ + description: 'Number of items per page', + required: false, + default: 10, + maximum: 100, + }) + @IsNumber() + @Min(1) + @Max(100) + @IsOptional() + @Type(() => Number) + limit: number = 10; + + @ApiProperty({ + description: 'Cursor for pagination', + required: false, + }) + @IsString() + @IsOptional() + cursor?: string; + + @ApiProperty({ + description: 'Sort direction', + required: false, + enum: DirectionEnum, + default: DirectionEnum.DESC, + }) + @IsEnum(DirectionEnum) + @IsOptional() + orderDirection?: DirectionEnum = DirectionEnum.DESC; + + @ApiProperty({ + description: 'Field to order by', + required: false, + enum: ['updatedAt', 'createdAt', 'lastOnlineAt'], + default: 'createdAt', + }) + @IsEnum(['updatedAt', 'createdAt', 'lastOnlineAt']) + @IsOptional() + orderBy?: 'updatedAt' | 'createdAt' | 'lastOnlineAt' = 'createdAt'; + + @ApiProperty({ + description: 'Search query to filter subscribers by name, email, phone, or subscriberId', + required: false, + }) + @IsString() + @IsOptional() + query?: string; +} diff --git a/apps/api/src/app/subscribers-v2/subscriber.controller.ts b/apps/api/src/app/subscribers-v2/subscriber.controller.ts new file mode 100644 index 000000000000..df31bf56fc06 --- /dev/null +++ b/apps/api/src/app/subscribers-v2/subscriber.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, Query, UseGuards, UseInterceptors } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ClassSerializerInterceptor } from '@nestjs/common'; +import { UserSession, UserAuthGuard } from '@novu/application-generic'; +import { ApiCommonResponses } from '../shared/framework/response.decorator'; +import { DirectionEnum, ListSubscriberResponse, UserSessionData } from '@novu/shared'; + +import { ListSubscribersCommand } from './usecases/list-subscribers/list-subscribers.command'; +import { ListSubscribersUseCase } from './usecases/list-subscribers/list-subscribers.usecase'; +import { SubscriberPaginationRequestDto } from './dtos/subscriber-pagination.dto'; + +@Controller({ path: '/subscribers', version: '2' }) +@UseInterceptors(ClassSerializerInterceptor) +@UseGuards(UserAuthGuard) +@ApiTags('Subscribers') +@ApiCommonResponses() +export class SubscriberController { + constructor(private listSubscribersUsecase: ListSubscribersUseCase) {} + + @Get('') + @ApiOperation({ + summary: 'List subscribers', + description: 'Returns a paginated list of subscribers', + }) + @ApiResponse({ + status: 200, + description: 'A list of subscribers with pagination information', + type: ListSubscriberResponse, + }) + async getSubscribers( + @UserSession() user: UserSessionData, + @Query() query: SubscriberPaginationRequestDto + ): Promise { + return await this.listSubscribersUsecase.execute( + ListSubscribersCommand.create({ + environmentId: user.environmentId, + organizationId: user.organizationId, + limit: query.limit || 10, + cursor: query.cursor, + orderDirection: query.orderDirection || DirectionEnum.DESC, + orderBy: query.orderBy || 'createdAt', + query: query.query, + }) + ); + } +} diff --git a/apps/api/src/app/subscribers-v2/subscriber.module.ts b/apps/api/src/app/subscribers-v2/subscriber.module.ts new file mode 100644 index 000000000000..11be0801688b --- /dev/null +++ b/apps/api/src/app/subscribers-v2/subscriber.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { SharedModule } from '../shared/shared.module'; +import { SubscriberController } from './subscriber.controller'; +import { ListSubscribersUseCase } from './usecases/list-subscribers/list-subscribers.usecase'; + +const USE_CASES = [ListSubscribersUseCase]; + +@Module({ + imports: [SharedModule], + controllers: [SubscriberController], + providers: [...USE_CASES], +}) +export class SubscriberModule {} diff --git a/apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.command.ts b/apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.command.ts new file mode 100644 index 000000000000..9acb8a8e91bb --- /dev/null +++ b/apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.command.ts @@ -0,0 +1,26 @@ +import { IsNumber, IsOptional, IsString, Max, Min, IsEnum } from 'class-validator'; +import { EnvironmentCommand } from '../../../shared/commands/project.command'; +import { DirectionEnum } from '@novu/shared'; + +export class ListSubscribersCommand extends EnvironmentCommand { + @IsNumber() + @Min(1) + @Max(100) + limit: number; + + @IsString() + @IsOptional() + cursor?: string; + + @IsEnum(DirectionEnum) + @IsOptional() + orderDirection: DirectionEnum = DirectionEnum.DESC; + + @IsEnum(['updatedAt', 'createdAt', 'lastOnlineAt']) + @IsOptional() + orderBy: 'updatedAt' | 'createdAt' | 'lastOnlineAt' = 'createdAt'; + + @IsString() + @IsOptional() + query?: string; +} diff --git a/apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.usecase.ts b/apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.usecase.ts new file mode 100644 index 000000000000..497f5f01b7b2 --- /dev/null +++ b/apps/api/src/app/subscribers-v2/usecases/list-subscribers/list-subscribers.usecase.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@nestjs/common'; +import { SubscriberRepository } from '@novu/dal'; +import { InstrumentUsecase } from '@novu/application-generic'; +import { DirectionEnum, ISubscriberResponseDto, ListSubscriberResponse } from '@novu/shared'; +import { ListSubscribersCommand } from './list-subscribers.command'; + +@Injectable() +export class ListSubscribersUseCase { + constructor(private subscriberRepository: SubscriberRepository) {} + + @InstrumentUsecase() + async execute(command: ListSubscribersCommand): Promise { + const query = { + _environmentId: command.environmentId, + _organizationId: command.organizationId, + } as const; + + if (command.query) { + Object.assign(query, { + $or: [ + { subscriberId: { $regex: command.query, $options: 'i' } }, + { email: { $regex: command.query, $options: 'i' } }, + { phone: { $regex: command.query, $options: 'i' } }, + { + $expr: { + $regexMatch: { + input: { $concat: ['$firstName', ' ', '$lastName'] }, + regex: command.query, + options: 'i', + }, + }, + }, + ], + }); + } + + if (command.cursor) { + const operator = command.orderDirection === DirectionEnum.ASC ? '$gt' : '$lt'; + Object.assign(query, { + [command.orderBy]: { [operator]: command.cursor }, + }); + } + + const subscribers = await this.subscriberRepository.find(query, undefined, { + limit: command.limit + 1, // Get one extra to determine if there are more items + sort: { [command.orderBy]: command.orderDirection === DirectionEnum.ASC ? 1 : -1 }, + }); + + const hasMore = subscribers.length > command.limit; + const data = hasMore ? subscribers.slice(0, -1) : subscribers; + + return { + subscribers: data, + hasMore, + pageSize: command.limit, + }; + } +} diff --git a/apps/api/src/app/subscribers/usecases/search-by-external-subscriber-ids/search-by-external-subscriber-ids.use-case.ts b/apps/api/src/app/subscribers/usecases/search-by-external-subscriber-ids/search-by-external-subscriber-ids.use-case.ts index a7ac3a7a41c3..937c33960781 100644 --- a/apps/api/src/app/subscribers/usecases/search-by-external-subscriber-ids/search-by-external-subscriber-ids.use-case.ts +++ b/apps/api/src/app/subscribers/usecases/search-by-external-subscriber-ids/search-by-external-subscriber-ids.use-case.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { IExternalSubscribersEntity, SubscriberEntity, SubscriberRepository } from '@novu/dal'; import { SubscriberDto } from '@novu/shared'; @@ -25,7 +25,7 @@ export class SearchByExternalSubscriberIds { } private mapFromEntity(entity: SubscriberEntity): SubscriberDto { - const { _id, createdAt, updatedAt, ...rest } = entity; + const { _id, ...rest } = entity; return { ...rest, diff --git a/apps/dashboard/src/api/subscribers.ts b/apps/dashboard/src/api/subscribers.ts new file mode 100644 index 000000000000..97f8ae944979 --- /dev/null +++ b/apps/dashboard/src/api/subscribers.ts @@ -0,0 +1,22 @@ +import type { IEnvironment, ListSubscriberResponse } from '@novu/shared'; +import { getV2 } from './api.client'; + +export const getSubscribers = async ({ + environment, + cursor, + limit, + query, +}: { + environment: IEnvironment; + cursor: string; + query: string; + limit: number; +}): Promise => { + const { data } = await getV2<{ data: ListSubscriberResponse }>( + `/subscribers?cursor=${cursor}&limit=${limit}&query=${query}`, + { + environment, + } + ); + return data; +}; diff --git a/apps/dashboard/src/components/icons/add-subscriber-illustration.tsx b/apps/dashboard/src/components/icons/add-subscriber-illustration.tsx new file mode 100644 index 000000000000..87734bcde545 --- /dev/null +++ b/apps/dashboard/src/components/icons/add-subscriber-illustration.tsx @@ -0,0 +1,25 @@ +import type { HTMLAttributes } from 'react'; + +type AddSubscriberIllustrationProps = HTMLAttributes; +export const AddSubscriberIllustration = (props: AddSubscriberIllustrationProps) => { + return ( + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/dashboard/src/components/side-navigation/side-navigation.tsx b/apps/dashboard/src/components/side-navigation/side-navigation.tsx index 750e253253d2..fc4e0f516030 100644 --- a/apps/dashboard/src/components/side-navigation/side-navigation.tsx +++ b/apps/dashboard/src/components/side-navigation/side-navigation.tsx @@ -1,3 +1,9 @@ +import { SidebarContent } from '@/components/side-navigation/sidebar'; +import { useEnvironment } from '@/context/environment/hooks'; +import { useTelemetry } from '@/hooks/use-telemetry'; +import { buildRoute, ROUTES } from '@/utils/routes'; +import { TelemetryEvent } from '@/utils/telemetry'; +import * as Sentry from '@sentry/react'; import { ReactNode, useMemo } from 'react'; import { RiBarChartBoxLine, @@ -9,20 +15,13 @@ import { RiStore3Line, RiUserAddLine, } from 'react-icons/ri'; -import { useEnvironment } from '@/context/environment/hooks'; -import { buildRoute, ROUTES } from '@/utils/routes'; -import { TelemetryEvent } from '@/utils/telemetry'; -import { useTelemetry } from '@/hooks/use-telemetry'; +import { useFetchSubscription } from '../../hooks/use-fetch-subscription'; +import { ChangelogStack } from './changelog-cards'; import { EnvironmentDropdown } from './environment-dropdown'; -import { OrganizationDropdown } from './organization-dropdown'; import { FreeTrialCard } from './free-trial-card'; -import { SubscribersStayTunedModal } from './subscribers-stay-tuned-modal'; -import { SidebarContent } from '@/components/side-navigation/sidebar'; -import { NavigationLink } from './navigation-link'; import { GettingStartedMenuItem } from './getting-started-menu-item'; -import { ChangelogStack } from './changelog-cards'; -import { useFetchSubscription } from '../../hooks/use-fetch-subscription'; -import * as Sentry from '@sentry/react'; +import { NavigationLink } from './navigation-link'; +import { OrganizationDropdown } from './organization-dropdown'; const NavigationGroup = ({ children, label }: { children: ReactNode; label?: string }) => { return ( @@ -68,14 +67,10 @@ export const SideNavigation = () => { Workflows - - track(TelemetryEvent.SUBSCRIBERS_LINK_CLICKED)}> - - - Subscribers - - - + + + Subscribers + { + return ( +
+ +
+ No subscribers yet +

+ A Subscriber is a unique entity for receiving notifications. Add them ahead of time or migrate them + dynamically when sending notifications. +

+
+ +
+ + + Migrate via API + + + + {/* */} +
+
+ ); +}; diff --git a/apps/dashboard/src/components/subscriber-list.tsx b/apps/dashboard/src/components/subscriber-list.tsx new file mode 100644 index 000000000000..1acde6bf1dd3 --- /dev/null +++ b/apps/dashboard/src/components/subscriber-list.tsx @@ -0,0 +1,74 @@ +import { Skeleton } from '@/components/primitives/skeleton'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/primitives/table'; +import { SubscriberListEmpty } from '@/components/subscriber-list-empty'; +import { useFetchSubscribers } from '@/hooks/use-fetch-subscribers'; +import { RiMore2Fill } from 'react-icons/ri'; +import { useSearchParams } from 'react-router-dom'; + +export function SubscriberList() { + const [searchParams] = useSearchParams(); + + const cursor = searchParams.get('cursor') || ''; + const query = searchParams.get('query') || ''; + const limit = parseInt(searchParams.get('limit') || '12'); + + const { data, isPending, isError } = useFetchSubscribers({ + cursor, + limit, + query, + }); + + if (isError) return null; + + if (!isPending && data?.subscribers.length === 0) { + return ; + } + + return ( +
+ + + + Subscribers + Email address + Phone number + Created at + Updated at + + + + + {isPending ? ( + <> + {new Array(limit).fill(0).map((_, index) => ( + + + + + + + + + + + + + + + + + + + + + + ))} + + ) : ( + <>{data.subscribers.map((subscriber) => subscriber.subscriberId)} + )} + +
+
+ ); +} diff --git a/apps/dashboard/src/hooks/use-fetch-subscribers.ts b/apps/dashboard/src/hooks/use-fetch-subscribers.ts new file mode 100644 index 000000000000..fa64cd003d18 --- /dev/null +++ b/apps/dashboard/src/hooks/use-fetch-subscribers.ts @@ -0,0 +1,26 @@ +import { getSubscribers } from '@/api/subscribers'; +import { QueryKeys } from '@/utils/query-keys'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useEnvironment } from '../context/environment/hooks'; + +interface UseSubscribersParams { + cursor?: string; + query?: string; + limit?: number; +} + +export function useFetchSubscribers({ cursor = '', query = '', limit = 12 }: UseSubscribersParams = {}) { + const { currentEnvironment } = useEnvironment(); + + const subscribersQuery = useQuery({ + queryKey: [QueryKeys.fetchSubscribers, currentEnvironment?._id, { cursor, limit, query }], + queryFn: () => getSubscribers({ environment: currentEnvironment!, cursor, limit, query }), + placeholderData: keepPreviousData, + enabled: !!currentEnvironment?._id, + refetchOnWindowFocus: true, + }); + + return { + ...subscribersQuery, + }; +} diff --git a/apps/dashboard/src/main.tsx b/apps/dashboard/src/main.tsx index 38cd5173ed2e..09ea7e81eda4 100644 --- a/apps/dashboard/src/main.tsx +++ b/apps/dashboard/src/main.tsx @@ -1,40 +1,41 @@ -import { StrictMode } from 'react'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import { createRoot } from 'react-dom/client'; import ErrorPage from '@/components/error-page'; -import { RootRoute, AuthRoute, DashboardRoute, CatchAllRoute } from './routes'; -import { OnboardingParentRoute } from './routes/onboarding'; +import { ConfigureWorkflow } from '@/components/workflow-editor/configure-workflow'; +import { EditStepConditions } from '@/components/workflow-editor/steps/conditions/edit-step-conditions'; +import { ConfigureStep } from '@/components/workflow-editor/steps/configure-step'; +import { ConfigureStepTemplate } from '@/components/workflow-editor/steps/configure-step-template'; import { - WorkflowsPage, - SignInPage, - SignUpPage, + ActivityFeed, + ApiKeysPage, + IntegrationsListPage, OrganizationListPage, QuestionnairePage, + SettingsPage, + SignInPage, + SignUpPage, UsecaseSelectPage, - ApiKeysPage, WelcomePage, - IntegrationsListPage, - SettingsPage, - ActivityFeed, + WorkflowsPage, } from '@/pages'; +import { SubscribersPage } from '@/pages/subscribers'; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; +import { CreateIntegrationSidebar } from './components/integrations/components/create-integration-sidebar'; +import { UpdateIntegrationSidebar } from './components/integrations/components/update-integration-sidebar'; +import { ChannelPreferences } from './components/workflow-editor/channel-preferences'; +import { FeatureFlagsProvider } from './context/feature-flags-provider'; import './index.css'; -import { ROUTES } from './utils/routes'; import { EditWorkflowPage } from './pages/edit-workflow'; -import { TestWorkflowPage } from './pages/test-workflow'; -import { initializeSentry } from './utils/sentry'; -import { overrideZodErrorMap } from './utils/validation'; -import { InboxUsecasePage } from './pages/inbox-usecase-page'; import { InboxEmbedPage } from './pages/inbox-embed-page'; -import { ConfigureWorkflow } from '@/components/workflow-editor/configure-workflow'; import { InboxEmbedSuccessPage } from './pages/inbox-embed-success-page'; -import { ChannelPreferences } from './components/workflow-editor/channel-preferences'; -import { FeatureFlagsProvider } from './context/feature-flags-provider'; -import { ConfigureStep } from '@/components/workflow-editor/steps/configure-step'; -import { ConfigureStepTemplate } from '@/components/workflow-editor/steps/configure-step-template'; +import { InboxUsecasePage } from './pages/inbox-usecase-page'; import { RedirectToLegacyStudioAuth } from './pages/redirect-to-legacy-studio-auth'; -import { CreateIntegrationSidebar } from './components/integrations/components/create-integration-sidebar'; -import { UpdateIntegrationSidebar } from './components/integrations/components/update-integration-sidebar'; -import { EditStepConditions } from '@/components/workflow-editor/steps/conditions/edit-step-conditions'; +import { TestWorkflowPage } from './pages/test-workflow'; +import { AuthRoute, CatchAllRoute, DashboardRoute, RootRoute } from './routes'; +import { OnboardingParentRoute } from './routes/onboarding'; +import { ROUTES } from './utils/routes'; +import { initializeSentry } from './utils/sentry'; +import { overrideZodErrorMap } from './utils/validation'; initializeSentry(); overrideZodErrorMap(); @@ -102,6 +103,10 @@ const router = createBrowserRouter([ path: ROUTES.WORKFLOWS, element: , }, + { + path: ROUTES.SUBSCRIBERS, + element: , + }, { path: ROUTES.API_KEYS, element: , diff --git a/apps/dashboard/src/pages/subscribers.tsx b/apps/dashboard/src/pages/subscribers.tsx new file mode 100644 index 000000000000..2dfe4ffadb92 --- /dev/null +++ b/apps/dashboard/src/pages/subscribers.tsx @@ -0,0 +1,26 @@ +import { DashboardLayout } from '@/components/dashboard-layout'; +import { PageMeta } from '@/components/page-meta'; +import { SubscriberList } from '@/components/subscriber-list'; +import { useTelemetry } from '@/hooks/use-telemetry'; +import { TelemetryEvent } from '@/utils/telemetry'; +import { useEffect } from 'react'; + +export const SubscribersPage = () => { + const track = useTelemetry(); + + useEffect(() => { + track(TelemetryEvent.SUBSCRIBERS_PAGE_VISIT); + }, [track]); + + return ( + <> + + Subscribers}> +
+
+
+ +
+ + ); +}; diff --git a/apps/dashboard/src/utils/query-keys.ts b/apps/dashboard/src/utils/query-keys.ts index 4d6bad6cd329..a8d614f92049 100644 --- a/apps/dashboard/src/utils/query-keys.ts +++ b/apps/dashboard/src/utils/query-keys.ts @@ -9,4 +9,5 @@ export const QueryKeys = Object.freeze({ getApiKeys: 'getApiKeys', fetchIntegrations: 'fetchIntegrations', fetchActivity: 'fetchActivity', + fetchSubscribers: 'fetchSubscribers', }); diff --git a/apps/dashboard/src/utils/routes.ts b/apps/dashboard/src/utils/routes.ts index 869d22859898..d7fb1a4425f7 100644 --- a/apps/dashboard/src/utils/routes.ts +++ b/apps/dashboard/src/utils/routes.ts @@ -29,6 +29,7 @@ export const ROUTES = { INTEGRATIONS_UPDATE: '/integrations/:integrationId/update', API_KEYS: '/env/:environmentSlug/api-keys', ACTIVITY_FEED: '/env/:environmentSlug/activity-feed', + SUBSCRIBERS: '/env/:environmentSlug/subscribers', } as const; export const buildRoute = (route: string, params: Record) => { diff --git a/apps/dashboard/src/utils/telemetry.ts b/apps/dashboard/src/utils/telemetry.ts index 361a526f8488..2349d2a1e073 100644 --- a/apps/dashboard/src/utils/telemetry.ts +++ b/apps/dashboard/src/utils/telemetry.ts @@ -39,4 +39,5 @@ export enum TelemetryEvent { SHARE_FEEDBACK_LINK_CLICKED = 'Share feedback link clicked', VARIABLE_POPOVER_OPENED = 'Variable popover opened - [Variable Editor]', VARIABLE_POPOVER_APPLIED = 'Variable popover applied - [Variable Editor]', + SUBSCRIBERS_PAGE_VISIT = 'Subscribers page visit', } diff --git a/packages/shared/src/dto/index.ts b/packages/shared/src/dto/index.ts index a5faebb0a929..46ba8075d997 100644 --- a/packages/shared/src/dto/index.ts +++ b/packages/shared/src/dto/index.ts @@ -16,3 +16,4 @@ export * from './topic'; export * from './widget'; export * from './workflow-override'; export * from './workflows'; +export * from './pagination'; diff --git a/packages/shared/src/dto/pagination/index.ts b/packages/shared/src/dto/pagination/index.ts new file mode 100644 index 000000000000..5f10edb7ba6e --- /dev/null +++ b/packages/shared/src/dto/pagination/index.ts @@ -0,0 +1 @@ +export * from './pagination.dto'; diff --git a/packages/shared/src/dto/pagination/pagination.dto.ts b/packages/shared/src/dto/pagination/pagination.dto.ts new file mode 100644 index 000000000000..b6b4cd851453 --- /dev/null +++ b/packages/shared/src/dto/pagination/pagination.dto.ts @@ -0,0 +1,8 @@ +import { DirectionEnum } from '../../types'; + +export class CursorPaginationDto { + limit?: number; + cursor?: string; + orderDirection?: DirectionEnum; + orderBy?: K; +} diff --git a/packages/shared/src/dto/subscriber/subscriber.dto.ts b/packages/shared/src/dto/subscriber/subscriber.dto.ts index dac5a851e0cd..3c6a56944ae5 100644 --- a/packages/shared/src/dto/subscriber/subscriber.dto.ts +++ b/packages/shared/src/dto/subscriber/subscriber.dto.ts @@ -1,4 +1,5 @@ import { ChatProviderIdEnum, ISubscriberChannel, PushProviderIdEnum } from '../../types'; +import { CursorPaginationDto } from '../pagination'; interface IChannelCredentials { webhookUrl?: string; @@ -24,7 +25,11 @@ export class SubscriberDto { subscriberId: string; channels?: IChannelSettings[]; deleted: boolean; + createdAt: string; + updatedAt: string; + lastOnlineAt?: string; } + export interface ISubscriberFeedResponseDto { _id?: string; firstName?: string; @@ -52,3 +57,18 @@ export interface ISubscriberResponseDto { updatedAt: string; __v?: number; } + +export type KeysOfSubscriberDto = keyof SubscriberDto; + +export class SubscriberPaginationDto extends CursorPaginationDto< + SubscriberDto, + 'updatedAt' | 'createdAt' | 'lastOnlineAt' +> { + query?: string; +} + +export class ListSubscriberResponse { + subscribers: ISubscriberResponseDto[]; + hasMore: boolean; + pageSize: number; +}