diff --git a/apps/web-api/prisma/fixtures/members.ts b/apps/web-api/prisma/fixtures/members.ts index 8413b96c1..a247b7d04 100644 --- a/apps/web-api/prisma/fixtures/members.ts +++ b/apps/web-api/prisma/fixtures/members.ts @@ -41,6 +41,7 @@ const membersFactory = Factory.define>( twitterHandler: faker.internet.userName(name), linkedinHandler: faker.internet.userName(name), telegramHandler: faker.internet.userName(name), + telegramUid: faker.helpers.slugify(`uid-tele-${name.toLowerCase()}`), officeHours: faker.helpers.arrayElement([null, faker.internet.url()]), moreDetails: faker.helpers.arrayElement([null, faker.lorem.paragraph()]), plnFriend: faker.datatype.boolean(), diff --git a/apps/web-api/prisma/migrations/20241219130952_add_telegram_uid_unique/migration.sql b/apps/web-api/prisma/migrations/20241219130952_add_telegram_uid_unique/migration.sql new file mode 100644 index 000000000..05ef391ed --- /dev/null +++ b/apps/web-api/prisma/migrations/20241219130952_add_telegram_uid_unique/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - A unique constraint covering the columns `[telegramHandler]` on the table `Member` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[telegramUid]` on the table `Member` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "Member" ADD COLUMN "telegramUid" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "Member_telegramHandler_key" ON "Member"("telegramHandler"); + +-- CreateIndex +CREATE UNIQUE INDEX "Member_telegramUid_key" ON "Member"("telegramUid"); diff --git a/apps/web-api/prisma/schema.prisma b/apps/web-api/prisma/schema.prisma index f78db7311..b2c7e6cf6 100644 --- a/apps/web-api/prisma/schema.prisma +++ b/apps/web-api/prisma/schema.prisma @@ -62,7 +62,8 @@ model Member { discordHandler String? twitterHandler String? linkedinHandler String? - telegramHandler String? + telegramHandler String? @unique + telegramUid String? @unique officeHours String? moreDetails String? bio String? diff --git a/apps/web-api/src/internals/internals.module.ts b/apps/web-api/src/internals/internals.module.ts index 06a0623ce..a74cb2381 100644 --- a/apps/web-api/src/internals/internals.module.ts +++ b/apps/web-api/src/internals/internals.module.ts @@ -2,11 +2,13 @@ import { Module } from '@nestjs/common'; import { PLEventsModule } from '../pl-events/pl-events.module'; import { PLEventsInternalController } from './pl-events.controller'; import { AuthModule } from '../auth/auth.module' +import { MembersController } from './members.controller'; +import { MembersModule } from '../members/members.module'; @Module({ - controllers: [PLEventsInternalController], + controllers: [PLEventsInternalController, MembersController], providers: [], exports: [], - imports:[PLEventsModule, AuthModule] + imports:[PLEventsModule, AuthModule, MembersModule] }) export class InternalsModule {} diff --git a/apps/web-api/src/internals/members.controller.ts b/apps/web-api/src/internals/members.controller.ts new file mode 100644 index 000000000..52532d6bf --- /dev/null +++ b/apps/web-api/src/internals/members.controller.ts @@ -0,0 +1,35 @@ +import { Controller, UseGuards, Body, BadRequestException, NotFoundException } from '@nestjs/common'; +import { Api, initNestServer, ApiDecorator } from '@ts-rest/nest'; +import { apiInternals } from 'libs/contracts/src/lib/contract-internals'; +import { InternalUpdateMemberDto, ResponseMemberSchema } from 'libs/contracts/src/schema'; +import { ApiOkResponseFromZod } from '../decorators/api-response-from-zod'; +import { InternalAuthGuard } from '../guards/auth.guard'; +import { MembersService } from '../members/members.service'; + +const server = initNestServer(apiInternals); +type RouteShape = typeof server.routeShapes; + +@Controller("") +@UseGuards(InternalAuthGuard) +export class MembersController { + constructor( + private readonly membersService: MembersService + ) {} + + @Api(server.route.updateTelagramUid) + @ApiOkResponseFromZod(ResponseMemberSchema) + async updateTelegramUid( + @Body() updateRequestDto: InternalUpdateMemberDto + ) { + if(updateRequestDto.telegramHandler) { + const member = await this.membersService.findUnique({telegramHandler: {equals: updateRequestDto.telegramHandler, mode: 'insensitive'}}); + if(member) { + return await this.membersService.updateMemberByUid(member.uid, {telegramUid: updateRequestDto.telegramUid}); + } + throw new NotFoundException(`Member with telegram handle ${updateRequestDto.telegramHandler} not found`); + + } else { + throw new BadRequestException('Telegram handle cannot be empty'); + } + } +} diff --git a/apps/web-api/src/members/members.controller.ts b/apps/web-api/src/members/members.controller.ts index 37e39eb6d..c1a712b64 100644 --- a/apps/web-api/src/members/members.controller.ts +++ b/apps/web-api/src/members/members.controller.ts @@ -69,6 +69,14 @@ export class MemberController { this.membersService.buildParticipationTypeFilter(queryParams) ], }; + // Check for the office hours blank when OH not null is passed + if (request.query['officeHours__not'] === 'null') { + builtQuery.where.AND.push({ + officeHours: { + not: '', + }, + }); + } return await this.membersService.findAll(builtQuery); } diff --git a/apps/web-api/src/members/members.service.ts b/apps/web-api/src/members/members.service.ts index 68e4c4214..903817200 100644 --- a/apps/web-api/src/members/members.service.ts +++ b/apps/web-api/src/members/members.service.ts @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import { BadRequestException, ConflictException, @@ -85,6 +84,16 @@ export class MembersService { } } + /** + * Retrieves a member based on unique query options + * @param queryOptions - Object containing unique field value pairs + * @returns A promise resolving to member if found else null + */ + async findUnique(queryOptions: Prisma.MemberWhereInput): Promise { + //Ideally this should be findUnique but to handle case insensitive we have done this, we should habdle this with lower case handles when saving + return await this.prisma.member.findFirst({ where: queryOptions }); + } + /** * This method retrieves the default(Founder, CEO, CTO and COO) and user selected(memberRoles) role's count * @param defaultAndUserSelectedRoles An array of role name(default & user selected roles) @@ -204,10 +213,12 @@ export class MembersService { tx: Prisma.TransactionClient = this.prisma, ): Promise { try { - return await tx.member.update({ + const result = await tx.member.update({ where: { uid }, data: member, }); + await this.cacheService.reset({ service: 'members' }); + return result; } catch (error) { return this.handleErrors(error); } diff --git a/apps/web-api/src/teams/teams.controller.ts b/apps/web-api/src/teams/teams.controller.ts index 0e7895979..f60ba0996 100644 --- a/apps/web-api/src/teams/teams.controller.ts +++ b/apps/web-api/src/teams/teams.controller.ts @@ -67,7 +67,15 @@ export class TeamsController { this.teamsService.buildFocusAreaFilters(focusAreas), this.teamsService.buildRecentTeamsFilter(request.query), this.teamsService.buildParticipationTypeFilter(request.query) - ] + ], + }; + // Check for the office hours blank when OH not null is passed + if (request.query['officeHours__not'] === 'null') { + builtQuery.where.AND.push({ + officeHours: { + not: '', + }, + }); } return this.teamsService.findAll(builtQuery); } diff --git a/apps/web-api/src/utils/cache/cache.service.ts b/apps/web-api/src/utils/cache/cache.service.ts index 9d137a31e..e0256ccad 100644 --- a/apps/web-api/src/utils/cache/cache.service.ts +++ b/apps/web-api/src/utils/cache/cache.service.ts @@ -8,14 +8,15 @@ export class CacheService { constructor( @Inject(CACHE_MANAGER) private cache: Cache, private logService: LogService - ) {} + ) { } // Mapping service names to tags private serviceTagsMap = { - members: ['member-filters', 'member-list', 'members-roles'], - projects: ['project-list', 'focus-areas'], - teams: ['team-filters', 'team-list', 'focus-areas'], - 'participants-requests': ['member-filters', 'member-list','team-filters', 'team-list', 'focus-areas'] + members: ['member-filters', 'member-list', 'members-roles', "featured", "member-airtable", "member-repositories", "member-detail","team-list"], + projects: ['project-list', 'focus-areas', "project-detail", "team-detail", "featured", "project-oso"], + teams: ['team-filters', 'team-list', 'focus-areas', "team-detail", "featured", "team-airtable"], + 'participants-requests': ['member-filters', 'member-list', 'team-filters', 'team-list', 'focus-areas'], + PLEventGuest: ["irl-locations", "irl-guests", "irl-locations-topic", "irl-guest-events"] }; // Reset cache and call API based on service diff --git a/apps/web-api/src/utils/notification/notification.service.ts b/apps/web-api/src/utils/notification/notification.service.ts index a6895db7c..6bbb5f1ef 100644 --- a/apps/web-api/src/utils/notification/notification.service.ts +++ b/apps/web-api/src/utils/notification/notification.service.ts @@ -1,4 +1,3 @@ -/* eslint-disable prettier/prettier */ import { Injectable } from '@nestjs/common'; import { AwsService } from '../aws/aws.service'; import { SlackService } from '../slack/slack.service'; diff --git a/libs/contracts/src/lib/contract-internals.ts b/libs/contracts/src/lib/contract-internals.ts index 90e1d7c57..41f965244 100644 --- a/libs/contracts/src/lib/contract-internals.ts +++ b/libs/contracts/src/lib/contract-internals.ts @@ -1,6 +1,8 @@ import { initContract } from '@ts-rest/core'; import { + InternalUpdateMemberDto, PLEventGuestQueryParams, + ResponseMemberSchema, ResponsePLEventGuestSchemaWithRelationsSchema } from '../schema'; import { getAPIVersionAsPath } from '../utils/versioned-path'; @@ -17,4 +19,13 @@ export const apiInternals = contract.router({ }, summary: 'Get a pl event with guests by location', }, + updateTelagramUid: { + method: 'PATCH', + path: `${getAPIVersionAsPath('1')}/internals/members`, + body: InternalUpdateMemberDto, + responses: { + 200: ResponseMemberSchema + }, + summary: 'Update the telegram uid for a member' + } }); \ No newline at end of file diff --git a/libs/contracts/src/schema/member.ts b/libs/contracts/src/schema/member.ts index 4cbcf8084..d7ecdb7d5 100644 --- a/libs/contracts/src/schema/member.ts +++ b/libs/contracts/src/schema/member.ts @@ -37,6 +37,7 @@ export const MemberSchema = z.object({ discordHandler: z.string().nullish(), twitterHandler: z.string().nullish(), telegramHandler: z.string().nullish(), + telegramUid: z.string().nullable(), officeHours: z.string().nullish(), airtableRecId: z.string().nullish(), plnFriend: z.boolean().nullish(), @@ -61,7 +62,7 @@ export const MemberSchema = z.object({ -export const ResponseMemberSchema = MemberSchema.omit({ id: true }).strict(); +export const ResponseMemberSchema = MemberSchema.omit({ id: true, telegramUid: true }).strict(); export const ResponseMemberWithRelationsSchema = ResponseMemberSchema.extend({ image: ResponseImageWithRelationsSchema.optional(), @@ -115,7 +116,8 @@ export const MemberDetailQueryParams = MemberQueryParams.unwrap() .pick(RETRIEVAL_QUERY_FILTERS) .optional(); -export class MemberDto extends createZodDto(MemberSchema) {} +export class MemberDto extends createZodDto(MemberSchema.omit({telegramUid: true})) {} +export class InternalUpdateMemberDto extends createZodDto(MemberSchema.pick({telegramUid: true, telegramHandler: true})) {} export class CreateMemberSchemaDto extends createZodDto(CreateMemberSchema) {}