Spaces:
Runtime error
Runtime error
Akshar2325 commited on
Commit ·
2f0bbdc
1
Parent(s): bcd6afe
feat(short-url): implement Shlink URL shortener integration with CRUD operations
Browse files- .env.example +6 -0
- prisma/schema.prisma +20 -0
- src/app.module.ts +2 -0
- src/core/short-url-core/dto/short-url-core.dto.ts +10 -0
- src/core/short-url-core/short-url-core.module.ts +9 -0
- src/core/short-url-core/short-url-core.service.ts +29 -0
- src/shared/keys/short-url.keys.ts +5 -0
- src/shared/modules/short-url/shlink/dto/shlink.dto.ts +21 -0
- src/shared/modules/short-url/shlink/shlink.config.ts +6 -0
- src/shared/modules/short-url/shlink/shlink.controller.ts +56 -0
- src/shared/modules/short-url/shlink/shlink.module.ts +14 -0
- src/shared/modules/short-url/shlink/shlink.service.ts +193 -0
- src/shared/modules/short-url/short-url.module.ts +8 -0
.env.example
CHANGED
|
@@ -211,3 +211,9 @@ N8N_DISCORD_LOOGING_WEBHOOK_SUCCESS_URL=https://your-n8n-instance.com/webhook/ap
|
|
| 211 |
|
| 212 |
# Webhook for error API calls (status 400+)
|
| 213 |
N8N_DISCORD_LOOGING_WEBHOOK_ERROR_URL=https://your-n8n-instance.com/webhook/api-error-log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
# Webhook for error API calls (status 400+)
|
| 213 |
N8N_DISCORD_LOOGING_WEBHOOK_ERROR_URL=https://your-n8n-instance.com/webhook/api-error-log
|
| 214 |
+
|
| 215 |
+
# Shlink URL Shortener Configuration
|
| 216 |
+
# ===========================
|
| 217 |
+
# Shlink instance URL and API key for URL shortening service
|
| 218 |
+
SHLINK_URL=https://sasuke734-shlink-shortener.hf.space
|
| 219 |
+
SHLINK_API_KEY=33707705d3fc4449b5b468fe4829d6c95cee1e70889a45f39d265a1b654c860a7927ea0b6d234181b46f1cb8232d791f611c35bf681c415895e
|
prisma/schema.prisma
CHANGED
|
@@ -81,6 +81,10 @@ enum PLATFORM_TYPE {
|
|
| 81 |
WEB
|
| 82 |
}
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
// ============ USER MANAGEMENT ============
|
| 85 |
|
| 86 |
model User {
|
|
@@ -929,3 +933,19 @@ model SeriesTag {
|
|
| 929 |
@@index([tagId])
|
| 930 |
@@map("series_tags")
|
| 931 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
WEB
|
| 82 |
}
|
| 83 |
|
| 84 |
+
enum URL_SHORTENER_PROVIDER {
|
| 85 |
+
SHLINK
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
// ============ USER MANAGEMENT ============
|
| 89 |
|
| 90 |
model User {
|
|
|
|
| 933 |
@@index([tagId])
|
| 934 |
@@map("series_tags")
|
| 935 |
}
|
| 936 |
+
|
| 937 |
+
// ============ SHORT URL ============
|
| 938 |
+
|
| 939 |
+
model ShortUrl {
|
| 940 |
+
id String @id @default(uuid())
|
| 941 |
+
longUrl String @db.Text
|
| 942 |
+
shortUrl String
|
| 943 |
+
shortCode String?
|
| 944 |
+
provider URL_SHORTENER_PROVIDER
|
| 945 |
+
createdAt DateTime @default(now()) @db.Timestamp(3)
|
| 946 |
+
updatedAt DateTime @updatedAt @db.Timestamp(3)
|
| 947 |
+
isDeleted Boolean @default(false)
|
| 948 |
+
|
| 949 |
+
@@index([provider])
|
| 950 |
+
@@map("short_urls")
|
| 951 |
+
}
|
src/app.module.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { CommonModule } from './shared/modules/common/common.module';
|
|
| 9 |
import { DiscordLoggerModule } from './shared/modules/discord-logger/discord-logger.module';
|
| 10 |
import { BaseQueryCoreModule } from './core/base-query-core/base-query-core.module';
|
| 11 |
import { UploadModule } from './shared/modules/upload/upload.module';
|
|
|
|
| 12 |
import { AppController } from './app.controller';
|
| 13 |
import { ApiTokenGuard } from './shared/guards/api-token.guard';
|
| 14 |
import { DiscordLoggerInterceptor } from './shared/modules/discord-logger/discord-logger.interceptor';
|
|
@@ -31,6 +32,7 @@ import { DiscordExceptionFilter } from './shared/modules/discord-logger/discord-
|
|
| 31 |
DiscordLoggerModule,
|
| 32 |
BaseQueryCoreModule,
|
| 33 |
UploadModule,
|
|
|
|
| 34 |
UserModule,
|
| 35 |
SuperAdminModule,
|
| 36 |
],
|
|
|
|
| 9 |
import { DiscordLoggerModule } from './shared/modules/discord-logger/discord-logger.module';
|
| 10 |
import { BaseQueryCoreModule } from './core/base-query-core/base-query-core.module';
|
| 11 |
import { UploadModule } from './shared/modules/upload/upload.module';
|
| 12 |
+
import { ShortUrlModule } from './shared/modules/short-url/short-url.module';
|
| 13 |
import { AppController } from './app.controller';
|
| 14 |
import { ApiTokenGuard } from './shared/guards/api-token.guard';
|
| 15 |
import { DiscordLoggerInterceptor } from './shared/modules/discord-logger/discord-logger.interceptor';
|
|
|
|
| 32 |
DiscordLoggerModule,
|
| 33 |
BaseQueryCoreModule,
|
| 34 |
UploadModule,
|
| 35 |
+
ShortUrlModule,
|
| 36 |
UserModule,
|
| 37 |
SuperAdminModule,
|
| 38 |
],
|
src/core/short-url-core/dto/short-url-core.dto.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { ShortUrl } from '@prisma/client';
|
| 3 |
+
import { IsArray } from 'class-validator';
|
| 4 |
+
import { CorePaginateDto } from 'src/core/base-query-core/dto/base-query-core.dto';
|
| 5 |
+
|
| 6 |
+
export class ShortUrlCorePaginateDto extends CorePaginateDto {
|
| 7 |
+
@ApiProperty({ required: true })
|
| 8 |
+
@IsArray()
|
| 9 |
+
list?: ShortUrl[];
|
| 10 |
+
}
|
src/core/short-url-core/short-url-core.module.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 3 |
+
import { ShortUrlCoreService } from './short-url-core.service';
|
| 4 |
+
|
| 5 |
+
@Module({
|
| 6 |
+
providers: [PrismaService, ShortUrlCoreService],
|
| 7 |
+
exports: [ShortUrlCoreService, PrismaService],
|
| 8 |
+
})
|
| 9 |
+
export class ShortUrlCoreModule {}
|
src/core/short-url-core/short-url-core.service.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Injectable } from '@nestjs/common';
|
| 2 |
+
import { ShortUrl, Prisma } from '@prisma/client';
|
| 3 |
+
import { PrismaService } from 'src/shared/modules/prisma/prisma.service';
|
| 4 |
+
import { ShortUrlCorePaginateDto } from './dto/short-url-core.dto';
|
| 5 |
+
import { PrismaBaseRepository } from 'src/shared/libs/prisma-base.repository';
|
| 6 |
+
import { ShortUrlMessages } from 'src/shared/keys/short-url.keys';
|
| 7 |
+
|
| 8 |
+
@Injectable()
|
| 9 |
+
export class ShortUrlCoreService extends PrismaBaseRepository<
|
| 10 |
+
ShortUrl,
|
| 11 |
+
ShortUrlCorePaginateDto,
|
| 12 |
+
Prisma.ShortUrlCreateArgs,
|
| 13 |
+
Prisma.ShortUrlUpdateArgs,
|
| 14 |
+
Prisma.ShortUrlUpdateManyArgs,
|
| 15 |
+
Prisma.ShortUrlFindUniqueArgs,
|
| 16 |
+
Prisma.ShortUrlFindFirstArgs,
|
| 17 |
+
Prisma.ShortUrlFindManyArgs,
|
| 18 |
+
Prisma.ShortUrlDeleteArgs,
|
| 19 |
+
Prisma.ShortUrlDeleteManyArgs,
|
| 20 |
+
Prisma.ShortUrlCountArgs,
|
| 21 |
+
Prisma.ShortUrlUpsertArgs
|
| 22 |
+
> {
|
| 23 |
+
constructor(private prismaService: PrismaService) {
|
| 24 |
+
super(prismaService.prisma.shortUrl, {
|
| 25 |
+
NOT_FOUND: ShortUrlMessages.NOT_FOUND,
|
| 26 |
+
DELETED: ShortUrlMessages.DELETED,
|
| 27 |
+
});
|
| 28 |
+
}
|
| 29 |
+
}
|
src/shared/keys/short-url.keys.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const ShortUrlMessages = {
|
| 2 |
+
NOT_FOUND: 'Short URL not found',
|
| 3 |
+
DELETED: 'Short URL deleted successfully',
|
| 4 |
+
CREATED: 'Short URL created successfully',
|
| 5 |
+
};
|
src/shared/modules/short-url/shlink/dto/shlink.dto.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ApiProperty } from '@nestjs/swagger';
|
| 2 |
+
import { IsString, IsUrl, IsOptional } from 'class-validator';
|
| 3 |
+
|
| 4 |
+
export class CreateShortUrlDto {
|
| 5 |
+
@ApiProperty({
|
| 6 |
+
description: 'Long URL to be shortened',
|
| 7 |
+
example: 'https://example.com/very/long/url',
|
| 8 |
+
})
|
| 9 |
+
@IsUrl()
|
| 10 |
+
@IsString()
|
| 11 |
+
longUrl: string;
|
| 12 |
+
|
| 13 |
+
@ApiProperty({
|
| 14 |
+
description: 'Optional custom short code',
|
| 15 |
+
required: false,
|
| 16 |
+
example: 'my-custom-code',
|
| 17 |
+
})
|
| 18 |
+
@IsString()
|
| 19 |
+
@IsOptional()
|
| 20 |
+
customSlug?: string;
|
| 21 |
+
}
|
src/shared/modules/short-url/shlink/shlink.config.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { registerAs } from '@nestjs/config';
|
| 2 |
+
|
| 3 |
+
export default registerAs('shlink', () => ({
|
| 4 |
+
url: process.env.SHLINK_URL,
|
| 5 |
+
apiKey: process.env.SHLINK_API_KEY,
|
| 6 |
+
}));
|
src/shared/modules/short-url/shlink/shlink.controller.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Controller,
|
| 3 |
+
Get,
|
| 4 |
+
Post,
|
| 5 |
+
Delete,
|
| 6 |
+
Body,
|
| 7 |
+
Param,
|
| 8 |
+
Query,
|
| 9 |
+
HttpCode,
|
| 10 |
+
HttpStatus,
|
| 11 |
+
} from '@nestjs/common';
|
| 12 |
+
import { ApiTags, ApiOperation, ApiQuery, ApiParam } from '@nestjs/swagger';
|
| 13 |
+
import { ShlinkService } from './shlink.service';
|
| 14 |
+
import { CreateShortUrlDto } from './dto/shlink.dto';
|
| 15 |
+
import { URL_SHORTENER_PROVIDER } from '@prisma/client';
|
| 16 |
+
import { LogToDiscord } from 'src/shared/decorators/log-to-discord.decorator';
|
| 17 |
+
|
| 18 |
+
@ApiTags('Short URL: Shlink')
|
| 19 |
+
@Controller('short-url/shlink')
|
| 20 |
+
@LogToDiscord()
|
| 21 |
+
export class ShlinkController {
|
| 22 |
+
constructor(private readonly shlinkService: ShlinkService) {}
|
| 23 |
+
|
| 24 |
+
@Post()
|
| 25 |
+
@ApiOperation({ summary: 'Create a new short URL' })
|
| 26 |
+
async createShortUrl(@Body() createDto: CreateShortUrlDto) {
|
| 27 |
+
return this.shlinkService.createShortUrl(createDto);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@Get()
|
| 31 |
+
@ApiOperation({ summary: 'Get all short URLs by provider' })
|
| 32 |
+
@ApiQuery({
|
| 33 |
+
name: 'provider',
|
| 34 |
+
enum: URL_SHORTENER_PROVIDER,
|
| 35 |
+
required: false,
|
| 36 |
+
description: 'Filter by provider (default: SHLINK)',
|
| 37 |
+
})
|
| 38 |
+
async getAllShortUrls(@Query('provider') provider?: URL_SHORTENER_PROVIDER) {
|
| 39 |
+
return this.shlinkService.getAllShortUrls(provider);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
@Get(':id')
|
| 43 |
+
@ApiOperation({ summary: 'Get a short URL by ID' })
|
| 44 |
+
@ApiParam({ name: 'id', description: 'Short URL ID' })
|
| 45 |
+
async getShortUrlById(@Param('id') id: string) {
|
| 46 |
+
return this.shlinkService.getShortUrlById(id);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@Delete(':id')
|
| 50 |
+
@HttpCode(HttpStatus.OK)
|
| 51 |
+
@ApiOperation({ summary: 'Delete a short URL' })
|
| 52 |
+
@ApiParam({ name: 'id', description: 'Short URL ID' })
|
| 53 |
+
async deleteShortUrl(@Param('id') id: string) {
|
| 54 |
+
return this.shlinkService.deleteShortUrl(id);
|
| 55 |
+
}
|
| 56 |
+
}
|
src/shared/modules/short-url/shlink/shlink.module.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { ConfigModule } from '@nestjs/config';
|
| 3 |
+
import { ShlinkController } from './shlink.controller';
|
| 4 |
+
import { ShlinkService } from './shlink.service';
|
| 5 |
+
import shlinkConfig from './shlink.config';
|
| 6 |
+
import { ShortUrlCoreModule } from 'src/core/short-url-core/short-url-core.module';
|
| 7 |
+
|
| 8 |
+
@Module({
|
| 9 |
+
imports: [ConfigModule.forFeature(shlinkConfig), ShortUrlCoreModule],
|
| 10 |
+
controllers: [ShlinkController],
|
| 11 |
+
providers: [ShlinkService],
|
| 12 |
+
exports: [ShlinkService],
|
| 13 |
+
})
|
| 14 |
+
export class ShlinkModule {}
|
src/shared/modules/short-url/shlink/shlink.service.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
Injectable,
|
| 3 |
+
Logger,
|
| 4 |
+
BadRequestException,
|
| 5 |
+
NotFoundException,
|
| 6 |
+
} from '@nestjs/common';
|
| 7 |
+
import { ConfigService } from '@nestjs/config';
|
| 8 |
+
import { URL_SHORTENER_PROVIDER } from '@prisma/client';
|
| 9 |
+
import { ShortUrlCoreService } from 'src/core/short-url-core/short-url-core.service';
|
| 10 |
+
import { CreateShortUrlDto } from './dto/shlink.dto';
|
| 11 |
+
|
| 12 |
+
@Injectable()
|
| 13 |
+
export class ShlinkService {
|
| 14 |
+
private readonly logger = new Logger(ShlinkService.name);
|
| 15 |
+
private readonly shlinkUrl: string;
|
| 16 |
+
private readonly shlinkApiKey: string;
|
| 17 |
+
|
| 18 |
+
constructor(
|
| 19 |
+
private configService: ConfigService,
|
| 20 |
+
private shortUrlCoreService: ShortUrlCoreService,
|
| 21 |
+
) {
|
| 22 |
+
this.shlinkUrl = this.configService.get<string>('shlink.url') || '';
|
| 23 |
+
this.shlinkApiKey = this.configService.get<string>('shlink.apiKey') || '';
|
| 24 |
+
|
| 25 |
+
if (!this.shlinkUrl || !this.shlinkApiKey) {
|
| 26 |
+
this.logger.warn('Shlink URL or API Key is not configured');
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Create a short URL using Shlink API
|
| 32 |
+
*/
|
| 33 |
+
async createShortUrl(createDto: CreateShortUrlDto) {
|
| 34 |
+
try {
|
| 35 |
+
// Prepare request payload
|
| 36 |
+
const payload: any = {
|
| 37 |
+
longUrl: createDto.longUrl,
|
| 38 |
+
findIfExists: true,
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
if (createDto.customSlug) {
|
| 42 |
+
payload.customSlug = createDto.customSlug;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Call Shlink REST API
|
| 46 |
+
const response = await fetch(`${this.shlinkUrl}/rest/v3/short-urls`, {
|
| 47 |
+
method: 'POST',
|
| 48 |
+
headers: {
|
| 49 |
+
'Content-Type': 'application/json',
|
| 50 |
+
'X-Api-Key': this.shlinkApiKey,
|
| 51 |
+
},
|
| 52 |
+
body: JSON.stringify(payload),
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
if (!response.ok) {
|
| 56 |
+
const errorData = await response.json().catch(() => ({}));
|
| 57 |
+
this.logger.error('Failed to create short URL:', errorData);
|
| 58 |
+
throw new BadRequestException(
|
| 59 |
+
errorData.detail || 'Failed to create short URL',
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const data = await response.json();
|
| 64 |
+
|
| 65 |
+
// Extract short code from the shortUrl
|
| 66 |
+
const shortCode = data.shortCode || data.shortUrl?.split('/').pop();
|
| 67 |
+
|
| 68 |
+
// Save to database
|
| 69 |
+
const shortUrl = await this.shortUrlCoreService.create({
|
| 70 |
+
data: {
|
| 71 |
+
longUrl: data.longUrl,
|
| 72 |
+
shortUrl: data.shortUrl,
|
| 73 |
+
shortCode: shortCode,
|
| 74 |
+
provider: URL_SHORTENER_PROVIDER.SHLINK,
|
| 75 |
+
},
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
this.logger.log(
|
| 79 |
+
`Created short URL: ${shortUrl.shortUrl} for ${shortUrl.longUrl}`,
|
| 80 |
+
);
|
| 81 |
+
|
| 82 |
+
return shortUrl;
|
| 83 |
+
} catch (error) {
|
| 84 |
+
this.logger.error('Error creating short URL:', error);
|
| 85 |
+
throw new BadRequestException('Failed to create short URL');
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
/**
|
| 90 |
+
* Get all short URLs by provider
|
| 91 |
+
*/
|
| 92 |
+
async getAllShortUrls(
|
| 93 |
+
provider: URL_SHORTENER_PROVIDER = URL_SHORTENER_PROVIDER.SHLINK,
|
| 94 |
+
) {
|
| 95 |
+
try {
|
| 96 |
+
const shortUrls = await this.shortUrlCoreService.findMany({
|
| 97 |
+
where: {
|
| 98 |
+
provider: provider,
|
| 99 |
+
isDeleted: false,
|
| 100 |
+
},
|
| 101 |
+
orderBy: {
|
| 102 |
+
createdAt: 'desc',
|
| 103 |
+
},
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
return shortUrls;
|
| 107 |
+
} catch (error) {
|
| 108 |
+
this.logger.error('Error fetching short URLs:', error);
|
| 109 |
+
throw error;
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/**
|
| 114 |
+
* Delete a short URL (soft delete from database and delete from Shlink)
|
| 115 |
+
*/
|
| 116 |
+
async deleteShortUrl(id: string) {
|
| 117 |
+
try {
|
| 118 |
+
// Find the short URL in database
|
| 119 |
+
const shortUrl = await this.shortUrlCoreService.findUnique({
|
| 120 |
+
where: { id },
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
if (!shortUrl) {
|
| 124 |
+
throw new NotFoundException('Short URL not found');
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// Delete from Shlink REST API
|
| 128 |
+
if (shortUrl.shortCode) {
|
| 129 |
+
try {
|
| 130 |
+
const response = await fetch(
|
| 131 |
+
`${this.shlinkUrl}/rest/v3/short-urls/${shortUrl.shortCode}`,
|
| 132 |
+
{
|
| 133 |
+
method: 'DELETE',
|
| 134 |
+
headers: {
|
| 135 |
+
'X-Api-Key': this.shlinkApiKey,
|
| 136 |
+
},
|
| 137 |
+
},
|
| 138 |
+
);
|
| 139 |
+
|
| 140 |
+
if (!response.ok && response.status !== 404) {
|
| 141 |
+
this.logger.warn(
|
| 142 |
+
`Failed to delete from Shlink: ${response.status}`,
|
| 143 |
+
);
|
| 144 |
+
} else {
|
| 145 |
+
this.logger.log(
|
| 146 |
+
`Deleted short URL from Shlink: ${shortUrl.shortCode}`,
|
| 147 |
+
);
|
| 148 |
+
}
|
| 149 |
+
} catch (error) {
|
| 150 |
+
this.logger.warn(
|
| 151 |
+
'Error deleting from Shlink, continuing with DB deletion:',
|
| 152 |
+
error,
|
| 153 |
+
);
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// Hard delete from database
|
| 158 |
+
await this.shortUrlCoreService.delete({
|
| 159 |
+
where: { id },
|
| 160 |
+
});
|
| 161 |
+
|
| 162 |
+
this.logger.log(`Deleted short URL: ${id}`);
|
| 163 |
+
|
| 164 |
+
return {
|
| 165 |
+
message: 'Short URL deleted successfully',
|
| 166 |
+
id: shortUrl.id,
|
| 167 |
+
};
|
| 168 |
+
} catch (error) {
|
| 169 |
+
this.logger.error('Error deleting short URL:', error);
|
| 170 |
+
throw error;
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/**
|
| 175 |
+
* Get a single short URL by ID
|
| 176 |
+
*/
|
| 177 |
+
async getShortUrlById(id: string) {
|
| 178 |
+
try {
|
| 179 |
+
const shortUrl = await this.shortUrlCoreService.findUnique({
|
| 180 |
+
where: { id },
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
if (!shortUrl) {
|
| 184 |
+
throw new NotFoundException('Short URL not found');
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
return shortUrl;
|
| 188 |
+
} catch (error) {
|
| 189 |
+
this.logger.error('Error fetching short URL:', error);
|
| 190 |
+
throw error;
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
src/shared/modules/short-url/short-url.module.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Module } from '@nestjs/common';
|
| 2 |
+
import { ShlinkModule } from './shlink/shlink.module';
|
| 3 |
+
|
| 4 |
+
@Module({
|
| 5 |
+
imports: [ShlinkModule],
|
| 6 |
+
exports: [ShlinkModule],
|
| 7 |
+
})
|
| 8 |
+
export class ShortUrlModule {}
|