From 8be8d09cf49e625cbd11af17eabb39a8837dadbb Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Fri, 21 Jul 2023 00:45:09 +0530 Subject: [PATCH 1/2] Fix : Clicks_count published on RMQ --- apps/api/src/app/app.controller.ts | 132 ++++++++++-------- apps/api/src/app/app.service.ts | 216 ++++++++++++++++------------- apps/api/src/main.ts | 54 ++++---- 3 files changed, 220 insertions(+), 182 deletions(-) diff --git a/apps/api/src/app/app.controller.ts b/apps/api/src/app/app.controller.ts index 2167d75b..3dc15b8d 100644 --- a/apps/api/src/app/app.controller.ts +++ b/apps/api/src/app/app.controller.ts @@ -10,33 +10,37 @@ import { Put, Res, UseInterceptors, -} from '@nestjs/common'; +} from "@nestjs/common"; import { ClientProxy, Ctx, MessagePattern, Payload, RmqContext, -} from '@nestjs/microservices'; +} from "@nestjs/microservices"; -import { HealthCheckService, HttpHealthIndicator, HealthCheck } from '@nestjs/terminus'; -import { PrismaHealthIndicator } from './prisma/prisma.health'; -import { RedisService } from '@liaoliaots/nestjs-redis'; -import { RedisHealthIndicator } from '@liaoliaots/nestjs-redis/health'; -import Redis from 'ioredis'; -import { Link } from './app.interface'; +import { + HealthCheckService, + HttpHealthIndicator, + HealthCheck, +} from "@nestjs/terminus"; +import { PrismaHealthIndicator } from "./prisma/prisma.health"; +import { RedisService } from "@liaoliaots/nestjs-redis"; +import { RedisHealthIndicator } from "@liaoliaots/nestjs-redis/health"; +import Redis from "ioredis"; +import { Link } from "./app.interface"; -import { AppService } from './app.service'; -import { RouterService } from './router/router.service'; -import { link as LinkModel } from '@prisma/client'; -import { AddROToResponseInterceptor } from './interceptors/addROToResponseInterceptor'; -import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ConfigService } from '@nestjs/config'; +import { AppService } from "./app.service"; +import { RouterService } from "./router/router.service"; +import { link as LinkModel } from "@prisma/client"; +import { AddROToResponseInterceptor } from "./interceptors/addROToResponseInterceptor"; +import { ApiBody, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ConfigService } from "@nestjs/config"; @Controller() @UseInterceptors(AddROToResponseInterceptor) export class AppController { - private readonly redis: Redis + private readonly redis: Redis; constructor( private readonly appService: AppService, @@ -47,39 +51,54 @@ export class AppController { private prismaIndicator: PrismaHealthIndicator, private readonly configService: ConfigService, private readonly redisService: RedisService, - @Inject('CLICK_SERVICE') private clickServiceClient: ClientProxy + @Inject("CLICK_SERVICE") private clickServiceClient: ClientProxy ) { - this.redis = redisService.getClient(configService.get('REDIS_NAME')); + this.redis = redisService.getClient(configService.get("REDIS_NAME")); } - - @Get('/health') + @Get("/health") @HealthCheck() - @ApiOperation({ summary: 'Get Health Check Status' }) - @ApiResponse({ status: 200, description: 'Result Report for All the Health Check Services' }) + @ApiOperation({ summary: "Get Health Check Status" }) + @ApiResponse({ + status: 200, + description: "Result Report for All the Health Check Services", + }) async checkHealth() { return this.healthCheckService.check([ - async () => this.http.pingCheck('RabbitMQ', this.configService.get('RABBITMQ_HEALTH_URL')), - async () => this.http.pingCheck('Basic Check', this.configService.get('BASE_URL')), - async () => this.redisIndicator.checkHealth('Redis', { type: 'redis', client: this.redis, timeout: 500 }), - async () => this.prismaIndicator.isHealthy('Db'), - ]) + async () => + this.http.pingCheck( + "RabbitMQ", + this.configService.get("RABBITMQ_HEALTH_URL") + ), + async () => + this.http.pingCheck("Basic Check", this.configService.get("BASE_URL")), + async () => + this.redisIndicator.checkHealth("Redis", { + type: "redis", + client: this.redis, + timeout: 500, + }), + async () => this.prismaIndicator.isHealthy("Db"), + ]); } - -/* + + /* @Deprecated */ -@Get('/sr/:code') -@ApiOperation({ summary: 'Redirect with encoded parameters' }) -@ApiResponse({ status: 301, description: 'will be redirected to the specified encoded link'}) - async handler(@Param('code') code: string, @Res() res) { - const resp = await this.routerService.decodeAndRedirect(code) + @Get("/sr/:code") + @ApiOperation({ summary: "Redirect with encoded parameters" }) + @ApiResponse({ + status: 301, + description: "will be redirected to the specified encoded link", + }) + async handler(@Param("code") code: string, @Res() res) { + const resp = await this.routerService.decodeAndRedirect(code); this.clickServiceClient - .send('onClick', { + .send("onClick", { hashid: resp.hashid, }) .subscribe(); - if (resp.url !== '') { + if (resp.url !== "") { return res.redirect(resp.url); } else { throw new NotFoundException(); @@ -87,42 +106,46 @@ export class AppController { } //http://localhost:3333/api/redirect/208 - @Get('/:hashid') - @ApiOperation({ summary: 'Redirect Links' }) - @ApiResponse({ status: 301, description: 'will be redirected to the specified link'}) - async redirect(@Param('hashid') hashid: string, @Res() res) { + @Get("/:hashid") + @ApiOperation({ summary: "Redirect Links" }) + @ApiResponse({ + status: 301, + description: "will be redirected to the specified link", + }) + async redirect(@Param("hashid") hashid: string, @Res() res) { const reRouteURL: string = await this.appService.redirect(hashid); this.clickServiceClient - .send('onClick', { + .send("onClick", { hashid: hashid, }) .subscribe(); - if (reRouteURL !== '') { - console.log({reRouteURL}); + if (reRouteURL !== "") { + console.log({ reRouteURL }); return res.redirect(302, reRouteURL); } else { throw new NotFoundException(); } } - - @Post('/register') - @ApiOperation({ summary: 'Create New Links' }) + @Post("/register") + @ApiOperation({ summary: "Create New Links" }) @ApiBody({ type: Link }) - @ApiResponse({ type: Link, status: 200}) + @ApiResponse({ type: Link, status: 200 }) async register(@Body() link: Link): Promise { return this.appService.createLink(link); } - - @Patch('update/:id') - @ApiOperation({ summary: 'Update Existing Links' }) + @Patch("update/:id") + @ApiOperation({ summary: "Update Existing Links" }) @ApiBody({ type: Link }) - @ApiResponse({ type: Link, status: 200}) - async update(@Param('id') id: string, @Body() link: Link ): Promise { + @ApiResponse({ type: Link, status: 200 }) + async update( + @Param("id") id: string, + @Body() link: Link + ): Promise { return this.appService.updateLink({ where: { customHashId: id }, - data: { + data: { userID: link.user || null, tags: link.tags || null, clicks: link.clicks || null, @@ -130,11 +153,11 @@ export class AppController { hashid: link.hashid || null, project: link.project || null, customHashId: link.customHashId || null, - }, + }, }); } - @MessagePattern('onClick') + @MessagePattern("onClick") async getNotifications( @Payload() data: number[], @Ctx() context: RmqContext @@ -145,5 +168,4 @@ export class AppController { console.log(`Message: ${originalMsg}`); await this.appService.updateClicks(JSON.parse(originalMsg).data.hashid); } - } diff --git a/apps/api/src/app/app.service.ts b/apps/api/src/app/app.service.ts index c389dab0..ea11f21b 100644 --- a/apps/api/src/app/app.service.ts +++ b/apps/api/src/app/app.service.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; -import { RedisService } from '@liaoliaots/nestjs-redis'; -import { PrismaService } from './prisma.service'; -import { link, Prisma } from '@prisma/client'; -import { ConfigService } from '@nestjs/config' -import { TelemetryService } from './telemetry/telemetry.service'; +import { Injectable } from "@nestjs/common"; +import { RedisService } from "@liaoliaots/nestjs-redis"; +import { PrismaService } from "./prisma.service"; +import { link, Prisma } from "@prisma/client"; +import { ConfigService } from "@nestjs/config"; +import { TelemetryService } from "./telemetry/telemetry.service"; @Injectable() export class AppService { @@ -11,29 +11,37 @@ export class AppService { private configService: ConfigService, private readonly redisService: RedisService, private prisma: PrismaService, - private telemetryService: TelemetryService, - ) {} + private telemetryService: TelemetryService + ) {} async setKey(hashid: string): Promise { - const client = await this.redisService.getClient(this.configService.get('REDIS_NAME')); + const client = await this.redisService.getClient( + this.configService.get("REDIS_NAME") + ); client.set(hashid, 0); } - + async updateClicks(urlId: string): Promise { - const client = await this.redisService.getClient(this.configService.get('REDIS_NAME')); + const client = await this.redisService.getClient( + this.configService.get("REDIS_NAME") + ); client.incr(urlId); } async fetchAllKeys(): Promise { - const client = await this.redisService.getClient(this.configService.get('REDIS_NAME')); - const keys: string[] = await client.keys('*'); - return keys + const client = await this.redisService.getClient( + this.configService.get("REDIS_NAME") + ); + const keys: string[] = await client.keys("*"); + return keys; } async updateClicksInDb(): Promise { - const client = await this.redisService.getClient(this.configService.get('REDIS_NAME')); - const keys: string[] = await this.fetchAllKeys() - for(const key of keys) { + const client = await this.redisService.getClient( + this.configService.get("REDIS_NAME") + ); + const keys: string[] = await this.fetchAllKeys(); + for (const key of keys) { client.get(key).then(async (value: string) => { const updateClick = await this.prisma.link.updateMany({ where: { @@ -54,92 +62,100 @@ export class AppService { } } - async link(linkWhereUniqueInput: Prisma.linkWhereUniqueInput, - ): Promise { - return this.prisma.link.findUnique({ - where: linkWhereUniqueInput, - }); - } + async link( + linkWhereUniqueInput: Prisma.linkWhereUniqueInput + ): Promise { + return this.prisma.link.findUnique({ + where: linkWhereUniqueInput, + }); + } - async links(params: { - skip?: number; - take?: number; - cursor?: Prisma.linkWhereUniqueInput; - where?: Prisma.linkWhereInput; - orderBy?: Prisma.linkOrderByWithRelationInput; - }): Promise { - const { skip, take, cursor, where, orderBy } = params; - return this.prisma.link.findMany({ - skip, - take, - cursor, - where, - orderBy, - }); - } - - async createLink(data: Prisma.linkCreateInput): Promise { - const link = await this.prisma.link.create({ - data, - }); + async links(params: { + skip?: number; + take?: number; + cursor?: Prisma.linkWhereUniqueInput; + where?: Prisma.linkWhereInput; + orderBy?: Prisma.linkOrderByWithRelationInput; + }): Promise { + const { skip, take, cursor, where, orderBy } = params; + return this.prisma.link.findMany({ + skip, + take, + cursor, + where, + orderBy, + }); + } - this.setKey(link.hashid.toString()); - return link; - } + async createLink(data: Prisma.linkCreateInput): Promise { + const link = await this.prisma.link.create({ + data, + }); - async updateLink(params: { - where: Prisma.linkWhereUniqueInput; - data: Prisma.linkUpdateInput; - }): Promise { - const { where, data } = params; - return this.prisma.link.update({ - data, - where, - }); - } - - async deleteLink(where: Prisma.linkWhereUniqueInput): Promise { - return this.prisma.link.delete({ - where, - }); - } + this.setKey(link.hashid.toString()); + return link; + } - async redirect(hashid: string): Promise { - return this.prisma.link.findMany({ - where: { - OR: [ - { - hashid: Number.isNaN(Number(hashid))? -1:parseInt(hashid), - }, - { customHashId: hashid }, - ], - }, - select: { - url: true, - params: true, - hashid: true, - }, - take: 1 - }) - .then(response => { - const url = response[0].url - const params = response[0].params - const ret = []; - - this.updateClicks(response[0].hashid.toString()); + async updateLink(params: { + where: Prisma.linkWhereUniqueInput; + data: Prisma.linkUpdateInput; + }): Promise { + const { where, data } = params; + return this.prisma.link.update({ + data, + where, + }); + } - if(params == null){ - return url; - }else { - Object.keys(params).forEach(function(d) { - ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(params[d])); - }) - return `${url}?${ret.join('&')}` || ''; - } - }) - .catch(err => { - this.telemetryService.sendEvent(this.configService.get('POSTHOG_DISTINCT_KEY'), "Exception in getLinkFromHashIdOrCustomHashId query", {error: err.message}) - return ''; - }); - } + async deleteLink(where: Prisma.linkWhereUniqueInput): Promise { + return this.prisma.link.delete({ + where, + }); + } + + async redirect(hashid: string): Promise { + return this.prisma.link + .findMany({ + where: { + OR: [ + { + hashid: Number.isNaN(Number(hashid)) ? -1 : parseInt(hashid), + }, + { customHashId: hashid }, + ], + }, + select: { + url: true, + params: true, + hashid: true, + }, + take: 1, + }) + .then((response) => { + const url = response[0].url; + const params = response[0].params; + const ret = []; + + // this.updateClicks(response[0].hashid.toString()); + + if (params == null) { + return url; + } else { + Object.keys(params).forEach(function (d) { + ret.push( + encodeURIComponent(d) + "=" + encodeURIComponent(params[d]) + ); + }); + return `${url}?${ret.join("&")}` || ""; + } + }) + .catch((err) => { + this.telemetryService.sendEvent( + this.configService.get("POSTHOG_DISTINCT_KEY"), + "Exception in getLinkFromHashIdOrCustomHashId query", + { error: err.message } + ); + return ""; + }); + } } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index dbfe4f7a..5325a2ee 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -3,16 +3,16 @@ * This is only a minimal backend to get started. */ -import { Logger } from '@nestjs/common'; -import { NestFactory } from '@nestjs/core'; -import { MicroserviceOptions, Transport } from '@nestjs/microservices'; -import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { Logger } from "@nestjs/common"; +import { NestFactory } from "@nestjs/core"; +import { MicroserviceOptions, Transport } from "@nestjs/microservices"; +import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import { FastifyAdapter, NestFastifyApplication, -} from '@nestjs/platform-fastify'; +} from "@nestjs/platform-fastify"; -import { AppModule } from './app/app.module'; +import { AppModule } from "./app/app.module"; async function bootstrap() { // API for YAUS @@ -20,35 +20,35 @@ async function bootstrap() { AppModule, new FastifyAdapter() ); - app.enableCors() - - // // MS for managing side-effects - // app.connectMicroservice({ - // transport: Transport.RMQ, - // options: { - // urls: ['amqp://username:password@localhost:5672'], - // queue: 'clicks', - // queueOptions: { - // durable: false, - // }, - // }, - // }); - - const globalPrefix = process.env.APP_GLOBAL_PREFIX || ''; + app.enableCors(); + + // MS for managing side-effects + app.connectMicroservice({ + transport: Transport.RMQ, + options: { + urls: ["amqp://username:password@localhost:5672"], + queue: "clicks", + queueOptions: { + durable: false, // was false + }, + }, + }); + + const globalPrefix = process.env.APP_GLOBAL_PREFIX || ""; app.setGlobalPrefix(globalPrefix); const config = new DocumentBuilder() - .setTitle('YAUS - Yet Another URL Shortener') - .setDescription('YAUS APIS') - .setVersion('1.0') - .addTag('yaus') + .setTitle("YAUS - Yet Another URL Shortener") + .setDescription("YAUS APIS") + .setVersion("1.0") + .addTag("yaus") .build(); const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('api', app, document); + SwaggerModule.setup("api", app, document); const port = process.env.PORT || 3333; await app.startAllMicroservices(); - await app.listen(port, '0.0.0.0'); + await app.listen(port, "0.0.0.0"); Logger.log( `🚀 Application is running on: http://localhost:${port}/${globalPrefix}` ); From c8288a266108695bc5be18d223eb289664ea3922 Mon Sep 17 00:00:00 2001 From: Rishabh Jain Date: Thu, 27 Jul 2023 00:51:51 +0530 Subject: [PATCH 2/2] Fix : RMQ crashing --- .../addROToResponseInterceptor.ts | 103 +++++++++--------- apps/api/src/main.ts | 2 +- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/apps/api/src/app/interceptors/addROToResponseInterceptor.ts b/apps/api/src/app/interceptors/addROToResponseInterceptor.ts index e7be9514..481b723b 100644 --- a/apps/api/src/app/interceptors/addROToResponseInterceptor.ts +++ b/apps/api/src/app/interceptors/addROToResponseInterceptor.ts @@ -1,59 +1,58 @@ import { - CallHandler, - ExecutionContext, - Injectable, - NestInterceptor, - } from '@nestjs/common'; + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; - import { map, Observable } from 'rxjs'; +import { map, Observable } from 'rxjs'; import { TelemetryService } from '../telemetry/telemetry.service'; import { URLSearchParams } from 'url'; - - // Nestjs Lifecyle - https://i.stack.imgur.com/2lFhd.jpg - - export interface Response { - data: T; - } - - /** - * @description - * Adds response object created in addResponseObject.interceptor.ts to the response body. - */ - @Injectable() - export class AddROToResponseInterceptor - implements NestInterceptor> - { - constructor( - private readonly telemetryService: TelemetryService, - private configService: ConfigService){} - intercept( - context: ExecutionContext, - next: CallHandler, - ): Observable> { - const now = Date.now(); - const req = context.switchToHttp().getRequest();; - return next.handle().pipe( - map((data) => { - let name: string; - console.log(`Execution Time: ${Date.now() - now}ms`) - const rawUrl = decodeURIComponent(req.raw.url); - const url = rawUrl.split("?")?.[0]; - const urlSearchParams = new URLSearchParams(rawUrl.split("?")?.[1]); +// Nestjs Lifecyle - https://i.stack.imgur.com/2lFhd.jpg + +export interface Response { + data: T; +} + +/** + * @description + * Adds response object created in addResponseObject.interceptor.ts to the response body. + */ +@Injectable() +export class AddROToResponseInterceptor + implements NestInterceptor> +{ + constructor( + private readonly telemetryService: TelemetryService, + private configService: ConfigService) { } + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable> { + const now = Date.now(); + const req = context.switchToHttp().getRequest();; + return next.handle().pipe( + map((data) => { + let name: string; + console.log(`Execution Time: ${Date.now() - now}ms`) + + const rawUrl = decodeURIComponent(req?.raw?.url); + const url = rawUrl.split("?")?.[0]; + const urlSearchParams = new URLSearchParams(rawUrl.split("?")?.[1]); - this.telemetryService.sendEvent( - this.configService.get("POSTHOG_DISTINCT_KEY"), - `${url} Execution Time`, - { - routeName: name, - executionTime: `${Date.now() - now}ms`, - url: req.url, - queryParams: Object.fromEntries(urlSearchParams.entries()), - } - ); - return data; - }), - ); - } + this.telemetryService.sendEvent( + this.configService.get("POSTHOG_DISTINCT_KEY"), + `${url} Execution Time`, + { + routeName: name, + executionTime: `${Date.now() - now}ms`, + url: req.url, + queryParams: Object.fromEntries(urlSearchParams.entries()), + } + ); + return data; + }), + ); } - \ No newline at end of file +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 5325a2ee..9de1ae4c 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -29,7 +29,7 @@ async function bootstrap() { urls: ["amqp://username:password@localhost:5672"], queue: "clicks", queueOptions: { - durable: false, // was false + durable: true, }, }, });