From 319f70d603c99f560adf7c5e1bfc22e45c58ede5 Mon Sep 17 00:00:00 2001 From: xixas Date: Thu, 11 Jan 2024 12:32:43 +0530 Subject: [PATCH 1/6] feat: add pagination support to graphql query --- .../dtos/notification-response.dto.ts | 17 +++++++++++++++++ .../notifications/dtos/query-options.dto.ts | 17 +++++++++++++++++ .../notifications/notifications.resolver.ts | 13 +++++++++---- .../notifications/notifications.service.ts | 12 +++++++++--- 4 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/modules/notifications/dtos/notification-response.dto.ts create mode 100644 apps/api/src/modules/notifications/dtos/query-options.dto.ts diff --git a/apps/api/src/modules/notifications/dtos/notification-response.dto.ts b/apps/api/src/modules/notifications/dtos/notification-response.dto.ts new file mode 100644 index 00000000..234deec0 --- /dev/null +++ b/apps/api/src/modules/notifications/dtos/notification-response.dto.ts @@ -0,0 +1,17 @@ +import { ObjectType, Field, Int } from '@nestjs/graphql'; +import { Notification } from '../entities/notification.entity'; + +@ObjectType() +export class NotificationResponse { + @Field(() => [Notification]) + notifications: Notification[]; + + @Field(() => Int) + total: number; + + @Field(() => Int) + offset: number; + + @Field(() => Int) + limit: number; +} diff --git a/apps/api/src/modules/notifications/dtos/query-options.dto.ts b/apps/api/src/modules/notifications/dtos/query-options.dto.ts new file mode 100644 index 00000000..90fa5132 --- /dev/null +++ b/apps/api/src/modules/notifications/dtos/query-options.dto.ts @@ -0,0 +1,17 @@ +import { Field, Int, InputType } from '@nestjs/graphql'; +import { IsOptional, IsInt, Min } from 'class-validator'; + +@InputType() +export class QueryOptionsDto { + @Field(() => Int, { defaultValue: 0 }) + @IsOptional() + @IsInt() + @Min(0) + offset?: number = 0; + + @Field(() => Int, { defaultValue: 10 }) + @IsOptional() + @IsInt() + @Min(1) + limit?: number = 10; +} diff --git a/apps/api/src/modules/notifications/notifications.resolver.ts b/apps/api/src/modules/notifications/notifications.resolver.ts index f2e79586..68bc2a28 100644 --- a/apps/api/src/modules/notifications/notifications.resolver.ts +++ b/apps/api/src/modules/notifications/notifications.resolver.ts @@ -1,18 +1,23 @@ -import { Query, Resolver } from '@nestjs/graphql'; +import { Args, Query, Resolver } from '@nestjs/graphql'; import { NotificationsService } from './notifications.service'; import { Notification } from './entities/notification.entity'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ApiKeyGuard } from 'src/common/guards/api-key/api-key.guard'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { UseGuards } from '@nestjs/common'; +import { NotificationResponse } from './dtos/notification-response.dto'; +import { QueryOptionsDto } from './dtos/query-options.dto'; @Resolver(() => Notification) // @UseGuards(ApiKeyGuard) export class NotificationsResolver { constructor(private readonly notificationsService: NotificationsService) {} - @Query(() => [Notification], { name: 'notifications' }) - async findAll(): Promise { - return this.notificationsService.getAllNotifications(); + @Query(() => NotificationResponse, { name: 'notifications' }) + async findAll( + @Args('options', { type: () => QueryOptionsDto, nullable: true, defaultValue: {} }) + options: QueryOptionsDto, + ): Promise { + return this.notificationsService.getAllNotifications(options); } } diff --git a/apps/api/src/modules/notifications/notifications.service.ts b/apps/api/src/modules/notifications/notifications.service.ts index d01bd5d8..0dadb180 100644 --- a/apps/api/src/modules/notifications/notifications.service.ts +++ b/apps/api/src/modules/notifications/notifications.service.ts @@ -7,6 +7,8 @@ import { NotificationQueueProducer } from 'src/jobs/producers/notifications/noti import { Status } from 'src/common/constants/database'; import { CreateNotificationDto } from './dtos/create-notification.dto'; import { ConfigService } from '@nestjs/config'; +import { QueryOptionsDto } from './dtos/query-options.dto'; +import { NotificationResponse } from './dtos/notification-response.dto'; @Injectable() export class NotificationsService { @@ -98,12 +100,16 @@ export class NotificationsService { }); } - getAllNotifications(): Promise { - this.logger.log('Getting all active notifications'); - return this.notificationRepository.find({ + async getAllNotifications(options: QueryOptionsDto): Promise { + this.logger.log('Getting all active notifications with options'); + const [notifications, total] = await this.notificationRepository.findAndCount({ where: { status: Status.ACTIVE, }, + skip: options.offset, + take: options.limit, }); + + return { notifications, total, offset: options.offset, limit: options.limit }; } } From 7aa440332fbe53d9feba1e2d1687d2c0d285059b Mon Sep 17 00:00:00 2001 From: xixas Date: Thu, 11 Jan 2024 14:35:18 +0530 Subject: [PATCH 2/6] feat: add sort support to graphql query --- .../api/src/common/constants/notifications.ts | 10 ++++++++++ .../notifications/dtos/query-options.dto.ts | 19 ++++++++++++++---- .../notifications/notifications.service.ts | 20 +++++++++++++++---- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/apps/api/src/common/constants/notifications.ts b/apps/api/src/common/constants/notifications.ts index d3b43325..d2dfddf5 100644 --- a/apps/api/src/common/constants/notifications.ts +++ b/apps/api/src/common/constants/notifications.ts @@ -1,4 +1,5 @@ import { ConfigService } from '@nestjs/config'; +import { registerEnumType } from '@nestjs/graphql'; export function generateEnabledChannelEnum(configService: ConfigService): Record { const enabledChannels: Record = {}; @@ -30,3 +31,12 @@ export const ChannelType = { MAILGUN: 2, WA_360_DAILOG: 3, }; + +export enum SortOrder { + ASC = 'ASC', + DESC = 'DESC', +} + +registerEnumType(SortOrder, { + name: 'SortOrder', +}); diff --git a/apps/api/src/modules/notifications/dtos/query-options.dto.ts b/apps/api/src/modules/notifications/dtos/query-options.dto.ts index 90fa5132..da53d520 100644 --- a/apps/api/src/modules/notifications/dtos/query-options.dto.ts +++ b/apps/api/src/modules/notifications/dtos/query-options.dto.ts @@ -1,17 +1,28 @@ -import { Field, Int, InputType } from '@nestjs/graphql'; -import { IsOptional, IsInt, Min } from 'class-validator'; +import { InputType, Field, Int } from '@nestjs/graphql'; +import { IsOptional, IsInt, Min, IsString, IsEnum } from 'class-validator'; +import { SortOrder } from 'src/common/constants/notifications'; @InputType() export class QueryOptionsDto { - @Field(() => Int, { defaultValue: 0 }) + @Field(() => Int, { defaultValue: 0, nullable: true }) @IsOptional() @IsInt() @Min(0) offset?: number = 0; - @Field(() => Int, { defaultValue: 10 }) + @Field(() => Int, { defaultValue: 10, nullable: true }) @IsOptional() @IsInt() @Min(1) limit?: number = 10; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + sortBy?: string; + + @Field(() => SortOrder, { nullable: true, defaultValue: SortOrder.ASC }) + @IsOptional() + @IsEnum(SortOrder) + sortOrder?: SortOrder = SortOrder.ASC; } diff --git a/apps/api/src/modules/notifications/notifications.service.ts b/apps/api/src/modules/notifications/notifications.service.ts index 0dadb180..6580ec7b 100644 --- a/apps/api/src/modules/notifications/notifications.service.ts +++ b/apps/api/src/modules/notifications/notifications.service.ts @@ -1,8 +1,12 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { FindManyOptions, Repository } from 'typeorm'; import { Notification } from './entities/notification.entity'; -import { DeliveryStatus, generateEnabledChannelEnum } from 'src/common/constants/notifications'; +import { + DeliveryStatus, + SortOrder, + generateEnabledChannelEnum, +} from 'src/common/constants/notifications'; import { NotificationQueueProducer } from 'src/jobs/producers/notifications/notifications.job.producer'; import { Status } from 'src/common/constants/database'; import { CreateNotificationDto } from './dtos/create-notification.dto'; @@ -102,13 +106,21 @@ export class NotificationsService { async getAllNotifications(options: QueryOptionsDto): Promise { this.logger.log('Getting all active notifications with options'); - const [notifications, total] = await this.notificationRepository.findAndCount({ + const queryOptions: FindManyOptions = { where: { status: Status.ACTIVE, }, skip: options.offset, take: options.limit, - }); + }; + + if (options.sortBy) { + queryOptions.order = { + [options.sortBy]: options.sortOrder === SortOrder.ASC ? SortOrder.ASC : SortOrder.DESC, + }; + } + + const [notifications, total] = await this.notificationRepository.findAndCount(queryOptions); return { notifications, total, offset: options.offset, limit: options.limit }; } From 5869c554192744ef86ced5ecf4eec31b5b85ea88 Mon Sep 17 00:00:00 2001 From: xixas Date: Thu, 11 Jan 2024 16:05:34 +0530 Subject: [PATCH 3/6] feat: add filter support to graphql query --- .../notifications/dtos/query-options.dto.ts | 5 ++ .../dtos/universal-filter.dto.ts | 13 +++++ .../notifications/notifications.service.ts | 58 +++++++++++++++---- 3 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 apps/api/src/modules/notifications/dtos/universal-filter.dto.ts diff --git a/apps/api/src/modules/notifications/dtos/query-options.dto.ts b/apps/api/src/modules/notifications/dtos/query-options.dto.ts index da53d520..a1ff233c 100644 --- a/apps/api/src/modules/notifications/dtos/query-options.dto.ts +++ b/apps/api/src/modules/notifications/dtos/query-options.dto.ts @@ -1,6 +1,7 @@ import { InputType, Field, Int } from '@nestjs/graphql'; import { IsOptional, IsInt, Min, IsString, IsEnum } from 'class-validator'; import { SortOrder } from 'src/common/constants/notifications'; +import { UniversalFilter } from './universal-filter.dto'; @InputType() export class QueryOptionsDto { @@ -25,4 +26,8 @@ export class QueryOptionsDto { @IsOptional() @IsEnum(SortOrder) sortOrder?: SortOrder = SortOrder.ASC; + + @Field(() => [UniversalFilter], { nullable: true }) + @IsOptional() + filters?: UniversalFilter[]; } diff --git a/apps/api/src/modules/notifications/dtos/universal-filter.dto.ts b/apps/api/src/modules/notifications/dtos/universal-filter.dto.ts new file mode 100644 index 00000000..ea9ae64f --- /dev/null +++ b/apps/api/src/modules/notifications/dtos/universal-filter.dto.ts @@ -0,0 +1,13 @@ +import { Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class UniversalFilter { + @Field() + field: string; + + @Field() + operator: string; + + @Field() + value: string; +} diff --git a/apps/api/src/modules/notifications/notifications.service.ts b/apps/api/src/modules/notifications/notifications.service.ts index 6580ec7b..410738af 100644 --- a/apps/api/src/modules/notifications/notifications.service.ts +++ b/apps/api/src/modules/notifications/notifications.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindManyOptions, Repository } from 'typeorm'; +import { Equal, FindManyOptions, LessThan, Like, MoreThan, Repository } from 'typeorm'; import { Notification } from './entities/notification.entity'; import { DeliveryStatus, @@ -106,22 +106,60 @@ export class NotificationsService { async getAllNotifications(options: QueryOptionsDto): Promise { this.logger.log('Getting all active notifications with options'); + + const whereConditions = { status: Status.ACTIVE }; + + options.filters?.forEach((filter) => { + const field = filter.field; + const value = filter.value; + + switch (filter.operator) { + case 'eq': + whereConditions[field] = Equal(value); + break; + case 'contains': + if (typeof value === 'string') { + whereConditions[field] = Like(`%${value}%`); + } + + break; + case 'gt': + if (this.isDateField(field)) { + whereConditions[field] = MoreThan(new Date(value)); + } else { + whereConditions[field] = MoreThan(value); + } + + break; + case 'lt': + if (this.isDateField(field)) { + whereConditions[field] = LessThan(new Date(value)); + } else { + whereConditions[field] = LessThan(value); + } + + break; + } + }); + const queryOptions: FindManyOptions = { - where: { - status: Status.ACTIVE, - }, + where: whereConditions, skip: options.offset, take: options.limit, + order: options.sortBy + ? { [options.sortBy]: options.sortOrder === SortOrder.ASC ? 'ASC' : 'DESC' } + : undefined, }; - if (options.sortBy) { - queryOptions.order = { - [options.sortBy]: options.sortOrder === SortOrder.ASC ? SortOrder.ASC : SortOrder.DESC, - }; - } - const [notifications, total] = await this.notificationRepository.findAndCount(queryOptions); return { notifications, total, offset: options.offset, limit: options.limit }; } + + // Helper method to check if a field is a date field + private isDateField(field: string): boolean { + // List all date fields from your Notification entity + const dateFields = ['createdOn', 'updatedOn']; + return dateFields.includes(field); + } } From 139d8062b5f8de4253f0f59dc159c3ff7c554f8f Mon Sep 17 00:00:00 2001 From: xixas Date: Tue, 16 Jan 2024 11:45:30 +0530 Subject: [PATCH 4/6] feat: add search support to graphql query --- .../notifications/dtos/query-options.dto.ts | 5 ++ .../notifications/notifications.service.ts | 79 +++++++++++++------ 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/apps/api/src/modules/notifications/dtos/query-options.dto.ts b/apps/api/src/modules/notifications/dtos/query-options.dto.ts index a1ff233c..0ead1f78 100644 --- a/apps/api/src/modules/notifications/dtos/query-options.dto.ts +++ b/apps/api/src/modules/notifications/dtos/query-options.dto.ts @@ -30,4 +30,9 @@ export class QueryOptionsDto { @Field(() => [UniversalFilter], { nullable: true }) @IsOptional() filters?: UniversalFilter[]; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + search?: string; } diff --git a/apps/api/src/modules/notifications/notifications.service.ts b/apps/api/src/modules/notifications/notifications.service.ts index 410738af..ee4aeb3f 100644 --- a/apps/api/src/modules/notifications/notifications.service.ts +++ b/apps/api/src/modules/notifications/notifications.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Equal, FindManyOptions, LessThan, Like, MoreThan, Repository } from 'typeorm'; +import { Brackets, Repository } from 'typeorm'; import { Notification } from './entities/notification.entity'; import { DeliveryStatus, @@ -103,55 +103,82 @@ export class NotificationsService { }, }); } - async getAllNotifications(options: QueryOptionsDto): Promise { this.logger.log('Getting all active notifications with options'); - const whereConditions = { status: Status.ACTIVE }; + const queryBuilder = this.notificationRepository.createQueryBuilder('notification'); + + // Base where condition + queryBuilder.where('notification.status = :status', { status: Status.ACTIVE }); + + // Search functionality using OR condition + if (options.search) { + const searchableFields = ['createdBy', 'data', 'result']; + queryBuilder.andWhere( + new Brackets((qb) => { + searchableFields.forEach((field, index) => { + const condition = `notification.${field} LIKE :search`; + + if (index === 0) { + qb.where(condition, { search: `%${options.search}%` }); + } else { + qb.orWhere(condition, { search: `%${options.search}%` }); + } + }); + }), + ); + } + // Applying filters + let filterIndex = 0; options.filters?.forEach((filter) => { const field = filter.field; const value = filter.value; + const condition = `notification.${field}`; + const paramName = `value${filterIndex}`; // Unique parameter name issue: https://github.com/typeorm/typeorm/issues/3428 switch (filter.operator) { case 'eq': - whereConditions[field] = Equal(value); + queryBuilder.andWhere(`${condition} = :${paramName}`, { [paramName]: value }); break; case 'contains': if (typeof value === 'string') { - whereConditions[field] = Like(`%${value}%`); + queryBuilder.andWhere(`${condition} LIKE :${paramName}`, { [paramName]: `%${value}%` }); } break; case 'gt': - if (this.isDateField(field)) { - whereConditions[field] = MoreThan(new Date(value)); - } else { - whereConditions[field] = MoreThan(value); - } - + queryBuilder.andWhere(`${condition} > :${paramName}`, { + [paramName]: this.isDateField(filter.field) ? new Date(String(value)) : value, + }); break; case 'lt': - if (this.isDateField(field)) { - whereConditions[field] = LessThan(new Date(value)); - } else { - whereConditions[field] = LessThan(value); - } - + queryBuilder.andWhere(`${condition} < :${paramName}`, { + [paramName]: this.isDateField(filter.field) ? new Date(String(value)) : value, + }); break; } + + filterIndex++; }); - const queryOptions: FindManyOptions = { - where: whereConditions, - skip: options.offset, - take: options.limit, - order: options.sortBy - ? { [options.sortBy]: options.sortOrder === SortOrder.ASC ? 'ASC' : 'DESC' } - : undefined, - }; + // Pagination and Sorting + if (options.offset !== undefined) { + queryBuilder.skip(options.offset); + } + + if (options.limit !== undefined) { + queryBuilder.take(options.limit); + } + + if (options.sortBy) { + queryBuilder.addOrderBy( + `notification.${options.sortBy}`, + options.sortOrder === SortOrder.ASC ? 'ASC' : 'DESC', + ); + } - const [notifications, total] = await this.notificationRepository.findAndCount(queryOptions); + const [notifications, total] = await queryBuilder.getManyAndCount(); return { notifications, total, offset: options.offset, limit: options.limit }; } From 2f43b5a9cf16c21500fbb23f589661cabfbe03b3 Mon Sep 17 00:00:00 2001 From: xixas Date: Tue, 16 Jan 2024 11:55:00 +0530 Subject: [PATCH 5/6] feat: add not equal filter --- apps/api/src/modules/notifications/notifications.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/api/src/modules/notifications/notifications.service.ts b/apps/api/src/modules/notifications/notifications.service.ts index ee4aeb3f..a799389a 100644 --- a/apps/api/src/modules/notifications/notifications.service.ts +++ b/apps/api/src/modules/notifications/notifications.service.ts @@ -157,6 +157,9 @@ export class NotificationsService { [paramName]: this.isDateField(filter.field) ? new Date(String(value)) : value, }); break; + case 'ne': + queryBuilder.andWhere(`${condition} != :${paramName}`, { [paramName]: value }); + break; } filterIndex++; From 51e62e799c066c89565bf8f1613df4316925d02e Mon Sep 17 00:00:00 2001 From: xixas Date: Wed, 17 Jan 2024 14:44:29 +0530 Subject: [PATCH 6/6] string to enum --- apps/api/src/modules/notifications/notifications.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/modules/notifications/notifications.service.ts b/apps/api/src/modules/notifications/notifications.service.ts index a799389a..976b05ce 100644 --- a/apps/api/src/modules/notifications/notifications.service.ts +++ b/apps/api/src/modules/notifications/notifications.service.ts @@ -177,7 +177,7 @@ export class NotificationsService { if (options.sortBy) { queryBuilder.addOrderBy( `notification.${options.sortBy}`, - options.sortOrder === SortOrder.ASC ? 'ASC' : 'DESC', + options.sortOrder === SortOrder.ASC ? SortOrder.ASC : SortOrder.DESC, ); }