From 1d5ba2df40d702367e823ac1c66b8258482b1b39 Mon Sep 17 00:00:00 2001 From: selimyanat Date: Wed, 19 Mar 2025 19:30:22 +0100 Subject: [PATCH 1/6] get original url from shortened url --- .../repository/in-memory-url.repository.ts | 9 ++++- .../repository/redis-url.repository.ts | 27 +++++++++++-- .../repository/repository.provider.ts | 1 - .../get-original-url-controller.spec.ts | 40 +++++++++++++++++++ .../get-original-url.controller.ts | 19 +++++++++ api/src/shorten-url/get-original-url.dto.ts | 6 +++ .../shorten-url/get-original-url.usecase.ts | 14 +++++++ api/src/shorten-url/shorten-url.module.ts | 5 ++- api/src/shorten-url/shorten-url.repository.ts | 4 +- api/src/shorten-url/shorten-url.usecase.ts | 5 +-- 10 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 api/src/shorten-url/get-original-url-controller.spec.ts create mode 100644 api/src/shorten-url/get-original-url.controller.ts create mode 100644 api/src/shorten-url/get-original-url.dto.ts create mode 100644 api/src/shorten-url/get-original-url.usecase.ts diff --git a/api/src/infrastructure/repository/in-memory-url.repository.ts b/api/src/infrastructure/repository/in-memory-url.repository.ts index 0a2c5e7..93c69e7 100644 --- a/api/src/infrastructure/repository/in-memory-url.repository.ts +++ b/api/src/infrastructure/repository/in-memory-url.repository.ts @@ -10,8 +10,15 @@ export class InMemoryUrlRepository implements ShortenUrlRepository { return Promise.resolve(undefined); } - findURL(url: string): Promise { + findShortenedURL(url: string): Promise { const shortenedUrl = this.urls.get(url); return Promise.resolve(shortenedUrl); } + + findOriginalURL(shortenedUrl: string): Promise { + const originalUrl = Array.from(this.urls.keys()).find( + (key) => this.urls.get(key) === shortenedUrl, + ); + return Promise.resolve(originalUrl || null); + } } diff --git a/api/src/infrastructure/repository/redis-url.repository.ts b/api/src/infrastructure/repository/redis-url.repository.ts index c177a5f..0b36bf7 100644 --- a/api/src/infrastructure/repository/redis-url.repository.ts +++ b/api/src/infrastructure/repository/redis-url.repository.ts @@ -31,11 +31,32 @@ export class RedisUrlRepository } async create(url: string, shortenedUrl: string): Promise { - await this.redisClient.set(url, shortenedUrl, { EX: this.redisTTL }); + // Use two keys to allow both forward and reverse lookup because: + // Efficient retrieval (O(1) time complexity) + // No need to scan all keys + // Reduces query complexity + await this.redisClient.set( + `originalUrl:${url}`, + `shortenedUrl: ${shortenedUrl}`, + { + EX: this.redisTTL, + }, + ); // 1 day expiry + await this.redisClient.set( + `shortenedUrl: ${shortenedUrl}`, + `originalUrl: ${url}`, + { + EX: this.redisTTL, + }, + ); + } + + async findShortenedURL(shortenedUrl: string): Promise { + return await this.redisClient.get(`shortenedUrl:${shortenedUrl}`); } - async findURL(url: string): Promise { - return await this.redisClient.get(url); + async findOriginalURL(originalUrl: string): Promise { + return await this.redisClient.get(`originalUrl:${originalUrl}`); } async onModuleInit() { diff --git a/api/src/infrastructure/repository/repository.provider.ts b/api/src/infrastructure/repository/repository.provider.ts index 5f583e4..918f2f0 100644 --- a/api/src/infrastructure/repository/repository.provider.ts +++ b/api/src/infrastructure/repository/repository.provider.ts @@ -9,7 +9,6 @@ import { RedisUrlRepository } from './redis-url.repository'; */ export const RepositoryProvider: Provider = { // TODO: Use a constant for the key - //provide: 'ShortenUrlRepository', provide: ShortenUrlRepository, useFactory: (configService: ConfigService) => { const persistenceType = configService.get( diff --git a/api/src/shorten-url/get-original-url-controller.spec.ts b/api/src/shorten-url/get-original-url-controller.spec.ts new file mode 100644 index 0000000..1729e24 --- /dev/null +++ b/api/src/shorten-url/get-original-url-controller.spec.ts @@ -0,0 +1,40 @@ +import { ShortenUrlController } from './shorten-url.controller'; +import { GetOriginalUrlController } from './get-original-url.controller'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule } from '@nestjs/config'; +import { ShortenUrlModule } from './shorten-url.module'; + +describe('ShortenUrl controller', () => { + let shortenUrlController: ShortenUrlController; + let underTest: GetOriginalUrlController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + imports: [await ConfigModule.forRoot(), ShortenUrlModule], + }).compile(); + + shortenUrlController = app.get(ShortenUrlController); + underTest = app.get(GetOriginalUrlController); + }); + + describe('getOriginalUrl', () => { + it('should return the original URL for a given shortened URL', async () => { + const longUrl = + 'https://zapper.xyz/very-long-url/very-long-url/very-long-url'; + + // Use the shorten URL controller to create the shortened URL + const shortenedResponse = await shortenUrlController.shortenUrl({ + url: longUrl, + }); + const shortenedUrl = shortenedResponse.shortenedUrl; + + // Now retrieve the original URL using GetOriginalUrlController + const response = await underTest.getOriginalUrl({ + shortenedUrl: shortenedUrl, + }); + + expect(response).not.toBeNull(); + expect(response.originalUrl).toBe(longUrl); + }); + }); +}); diff --git a/api/src/shorten-url/get-original-url.controller.ts b/api/src/shorten-url/get-original-url.controller.ts new file mode 100644 index 0000000..27adb0f --- /dev/null +++ b/api/src/shorten-url/get-original-url.controller.ts @@ -0,0 +1,19 @@ +import { Body, Controller, Get } from '@nestjs/common'; +import { GetOriginalUrlDto } from './get-original-url.dto'; +import { ShortenUrlUsecase } from './shorten-url.usecase'; +import { GetOriginalUrlUsecase } from './get-original-url.usecase'; + +@Controller('/shorten-url') +export class GetOriginalUrlController { + constructor(private readonly getOriginalUrlUsecase: GetOriginalUrlUsecase) {} + + @Get() + async getOriginalUrl( + @Body() request: GetOriginalUrlDto, + ): Promise<{ originalUrl: string }> { + const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl( + request.shortenedUrl, + ); + return { originalUrl }; + } +} diff --git a/api/src/shorten-url/get-original-url.dto.ts b/api/src/shorten-url/get-original-url.dto.ts new file mode 100644 index 0000000..735c408 --- /dev/null +++ b/api/src/shorten-url/get-original-url.dto.ts @@ -0,0 +1,6 @@ +import { IsString, IsUrl } from 'class-validator'; + +export class GetOriginalUrlDto { + @IsUrl() + shortenedUrl: string; +} diff --git a/api/src/shorten-url/get-original-url.usecase.ts b/api/src/shorten-url/get-original-url.usecase.ts new file mode 100644 index 0000000..0893a4b --- /dev/null +++ b/api/src/shorten-url/get-original-url.usecase.ts @@ -0,0 +1,14 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ShortenUrlRepository } from './shorten-url.repository'; + +@Injectable() +export class GetOriginalUrlUsecase { + constructor( + @Inject('ShortenUrlRepository') + private readonly shortenUrlRepository: ShortenUrlRepository, + ) {} + + async getOriginalUrl(shortenedUrl: string): Promise { + return await this.shortenUrlRepository.findOriginalURL(shortenedUrl); + } +} diff --git a/api/src/shorten-url/shorten-url.module.ts b/api/src/shorten-url/shorten-url.module.ts index 28167d8..35804a8 100644 --- a/api/src/shorten-url/shorten-url.module.ts +++ b/api/src/shorten-url/shorten-url.module.ts @@ -5,13 +5,15 @@ import { ShortenUrlUsecase } from './shorten-url.usecase'; import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service'; import { InfrastructureModule } from '../infrastructure/infrastructure.module'; import { ShortenUrlRepository } from './shorten-url.repository'; +import { GetOriginalUrlUsecase } from './get-original-url.usecase'; +import { GetOriginalUrlController } from './get-original-url.controller'; @Module({ imports: [ ConfigModule, // ✅ Ensure ConfigModule is available InfrastructureModule.register(), // ✅ Register InfrastructureModule dynamically ], - controllers: [ShortenUrlController], + controllers: [ShortenUrlController, GetOriginalUrlController], providers: [ { provide: 'ShortenUrlRepository', @@ -19,6 +21,7 @@ import { ShortenUrlRepository } from './shorten-url.repository'; }, ShortenUrlIdGeneratorService, ShortenUrlUsecase, + GetOriginalUrlUsecase, ], }) export class ShortenUrlModule {} diff --git a/api/src/shorten-url/shorten-url.repository.ts b/api/src/shorten-url/shorten-url.repository.ts index efa6921..5000340 100644 --- a/api/src/shorten-url/shorten-url.repository.ts +++ b/api/src/shorten-url/shorten-url.repository.ts @@ -1,5 +1,7 @@ export abstract class ShortenUrlRepository { - abstract findURL(url: string): Promise; + abstract findShortenedURL(url: string): Promise; abstract create(url: string, shortenedUrl: string): Promise; + + abstract findOriginalURL(shortenedUrl: string): Promise; } diff --git a/api/src/shorten-url/shorten-url.usecase.ts b/api/src/shorten-url/shorten-url.usecase.ts index 4fcdf4e..40753d4 100644 --- a/api/src/shorten-url/shorten-url.usecase.ts +++ b/api/src/shorten-url/shorten-url.usecase.ts @@ -27,9 +27,8 @@ export class ShortenUrlUsecase { } async shortenUrl(originalURL: string): Promise { - const existingShortenedUrl = await this.shortenUrlRepository.findURL( - originalURL, - ); + const existingShortenedUrl = + await this.shortenUrlRepository.findShortenedURL(originalURL); if (existingShortenedUrl) { return existingShortenedUrl; From d44cc9b75002d633d4b2b6d664c5b0bd5a0e3c05 Mon Sep 17 00:00:00 2001 From: selimyanat Date: Wed, 19 Mar 2025 19:30:40 +0100 Subject: [PATCH 2/6] get original url from shortened url --- webapp/app/api/url-shortener/route.ts | 54 ++++++++++----------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/webapp/app/api/url-shortener/route.ts b/webapp/app/api/url-shortener/route.ts index b419887..780c3ab 100644 --- a/webapp/app/api/url-shortener/route.ts +++ b/webapp/app/api/url-shortener/route.ts @@ -1,43 +1,27 @@ import { NextRequest, NextResponse } from 'next/server'; -// Function to handle POST requests export async function POST(request: NextRequest) { - try { - const { url } = await request.json(); - if (!url || typeof url !== 'string') { - throw new Error(`Invalid URL format ${url}`); - } + const { url } = await request.json().catch(() => ({})); // ✅ Simplified JSON parsing - const shortenedUrl = await shortenUrl(url); - return NextResponse.json({ shortenedUrl }, { status: 200 }); - } catch (error) { - console.error('Failed to shorten URL:', error); - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Invalid request' }, - { status: 400 } - ); + if (typeof url !== 'string' || !url.trim()) { + return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 }); } -} -// Function to call the backend API -async function shortenUrl(url: string): Promise { - try { - const requestBody = { url }; - const response = await fetch('http://localhost:3000/shorten-url', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody), + return fetch('http://localhost:3000/shorten-url', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + }) + .then(async (res) => { + if (!res.ok) + throw new Error((await res.json()).error || 'Failed to shorten URL'); + return res.json(); + }) + .then((data) => + NextResponse.json({ shortenedUrl: data.shortenedUrl }, { status: 200 }) + ) + .catch((error) => { + console.error('Error shortening URL:', error); + return NextResponse.json({ error: error.message }, { status: 400 }); }); - - if (!response.ok) { - const errorResponse = await response.json(); - throw new Error(errorResponse.error || 'Failed to shorten URL'); - } - - const data = await response.json(); - return data; - } catch (error) { - console.error('Error shortening URL:', error); - throw error; - } } From 40505c5021dd5027fe14896aef5672b9c12c8d8d Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 21 Mar 2025 11:35:57 +0100 Subject: [PATCH 3/6] redirect request with shorted url to original url --- .../repository/in-memory-url.repository.ts | 5 ---- .../repository/redis-url.repository.ts | 19 ++---------- .../get-original-url-controller.spec.ts | 23 +++++++++++---- .../get-original-url.controller.ts | 29 +++++++++++-------- .../shorten-url/get-original-url.usecase.ts | 4 ++- api/src/shorten-url/shorten-url.repository.ts | 2 -- api/src/shorten-url/shorten-url.usecase.ts | 15 ---------- 7 files changed, 39 insertions(+), 58 deletions(-) diff --git a/api/src/infrastructure/repository/in-memory-url.repository.ts b/api/src/infrastructure/repository/in-memory-url.repository.ts index 93c69e7..9541270 100644 --- a/api/src/infrastructure/repository/in-memory-url.repository.ts +++ b/api/src/infrastructure/repository/in-memory-url.repository.ts @@ -10,11 +10,6 @@ export class InMemoryUrlRepository implements ShortenUrlRepository { return Promise.resolve(undefined); } - findShortenedURL(url: string): Promise { - const shortenedUrl = this.urls.get(url); - return Promise.resolve(shortenedUrl); - } - findOriginalURL(shortenedUrl: string): Promise { const originalUrl = Array.from(this.urls.keys()).find( (key) => this.urls.get(key) === shortenedUrl, diff --git a/api/src/infrastructure/repository/redis-url.repository.ts b/api/src/infrastructure/repository/redis-url.repository.ts index 0b36bf7..ded6ed2 100644 --- a/api/src/infrastructure/repository/redis-url.repository.ts +++ b/api/src/infrastructure/repository/redis-url.repository.ts @@ -14,9 +14,9 @@ export class RedisUrlRepository { private readonly DEFAULT_TTL = 100; - private redisClient: RedisClientType; + private readonly redisTTL: number; - private redisTTL: number; + private redisClient: RedisClientType; constructor(private readonly configService: ConfigService) { const redisUrl = this.configService.get('REDIS_URL'); @@ -31,17 +31,6 @@ export class RedisUrlRepository } async create(url: string, shortenedUrl: string): Promise { - // Use two keys to allow both forward and reverse lookup because: - // Efficient retrieval (O(1) time complexity) - // No need to scan all keys - // Reduces query complexity - await this.redisClient.set( - `originalUrl:${url}`, - `shortenedUrl: ${shortenedUrl}`, - { - EX: this.redisTTL, - }, - ); // 1 day expiry await this.redisClient.set( `shortenedUrl: ${shortenedUrl}`, `originalUrl: ${url}`, @@ -51,10 +40,6 @@ export class RedisUrlRepository ); } - async findShortenedURL(shortenedUrl: string): Promise { - return await this.redisClient.get(`shortenedUrl:${shortenedUrl}`); - } - async findOriginalURL(originalUrl: string): Promise { return await this.redisClient.get(`originalUrl:${originalUrl}`); } diff --git a/api/src/shorten-url/get-original-url-controller.spec.ts b/api/src/shorten-url/get-original-url-controller.spec.ts index 1729e24..d15e9a6 100644 --- a/api/src/shorten-url/get-original-url-controller.spec.ts +++ b/api/src/shorten-url/get-original-url-controller.spec.ts @@ -17,7 +17,7 @@ describe('ShortenUrl controller', () => { underTest = app.get(GetOriginalUrlController); }); - describe('getOriginalUrl', () => { + describe('Redirect to original url', () => { it('should return the original URL for a given shortened URL', async () => { const longUrl = 'https://zapper.xyz/very-long-url/very-long-url/very-long-url'; @@ -26,15 +26,26 @@ describe('ShortenUrl controller', () => { const shortenedResponse = await shortenUrlController.shortenUrl({ url: longUrl, }); + + // Extract the slug from the shortened URL const shortenedUrl = shortenedResponse.shortenedUrl; + const slug = shortenedUrl.split('/').pop(); // Now retrieve the original URL using GetOriginalUrlController - const response = await underTest.getOriginalUrl({ - shortenedUrl: shortenedUrl, - }); + const response = await underTest.getOriginalUrl(slug); - expect(response).not.toBeNull(); - expect(response.originalUrl).toBe(longUrl); + // TODO check with an integration / e2e test the redirection code (302) + expect(response).toEqual({ url: longUrl }); }); }); + + it('should return 404 if the shortened URL is not found', async () => { + const redirect = jest.fn(); + const res = { redirect } as any; + + const slug = 'does-not-exist'; + await expect(underTest.getOriginalUrl(slug)).rejects.toThrow( + 'Shortened URL "does-not-exist" not found', + ); + }); }); diff --git a/api/src/shorten-url/get-original-url.controller.ts b/api/src/shorten-url/get-original-url.controller.ts index 27adb0f..ea71373 100644 --- a/api/src/shorten-url/get-original-url.controller.ts +++ b/api/src/shorten-url/get-original-url.controller.ts @@ -1,19 +1,24 @@ -import { Body, Controller, Get } from '@nestjs/common'; -import { GetOriginalUrlDto } from './get-original-url.dto'; -import { ShortenUrlUsecase } from './shorten-url.usecase'; +import { + Controller, + Get, + NotFoundException, + Param, + Redirect, +} from '@nestjs/common'; import { GetOriginalUrlUsecase } from './get-original-url.usecase'; -@Controller('/shorten-url') +@Controller() export class GetOriginalUrlController { constructor(private readonly getOriginalUrlUsecase: GetOriginalUrlUsecase) {} - @Get() - async getOriginalUrl( - @Body() request: GetOriginalUrlDto, - ): Promise<{ originalUrl: string }> { - const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl( - request.shortenedUrl, - ); - return { originalUrl }; + @Get(':slug') + @Redirect(undefined, 302) + async getOriginalUrl(@Param('slug') slug: string) { + const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl(slug); + if (!originalUrl) { + throw new NotFoundException(`Shortened URL "${slug}" not found`); + } + // Use temporary redirect to original URL so that we can capture analytics + return { url: originalUrl }; } } diff --git a/api/src/shorten-url/get-original-url.usecase.ts b/api/src/shorten-url/get-original-url.usecase.ts index 0893a4b..e5eaabe 100644 --- a/api/src/shorten-url/get-original-url.usecase.ts +++ b/api/src/shorten-url/get-original-url.usecase.ts @@ -9,6 +9,8 @@ export class GetOriginalUrlUsecase { ) {} async getOriginalUrl(shortenedUrl: string): Promise { - return await this.shortenUrlRepository.findOriginalURL(shortenedUrl); + // TODO: reconstruct the URL ? or store only the shorted path ?? + const url = 'http://localhost:3000/' + shortenedUrl; + return await this.shortenUrlRepository.findOriginalURL(url); } } diff --git a/api/src/shorten-url/shorten-url.repository.ts b/api/src/shorten-url/shorten-url.repository.ts index 5000340..4dfe256 100644 --- a/api/src/shorten-url/shorten-url.repository.ts +++ b/api/src/shorten-url/shorten-url.repository.ts @@ -1,6 +1,4 @@ export abstract class ShortenUrlRepository { - abstract findShortenedURL(url: string): Promise; - abstract create(url: string, shortenedUrl: string): Promise; abstract findOriginalURL(shortenedUrl: string): Promise; diff --git a/api/src/shorten-url/shorten-url.usecase.ts b/api/src/shorten-url/shorten-url.usecase.ts index 40753d4..cd39734 100644 --- a/api/src/shorten-url/shorten-url.usecase.ts +++ b/api/src/shorten-url/shorten-url.usecase.ts @@ -27,12 +27,6 @@ export class ShortenUrlUsecase { } async shortenUrl(originalURL: string): Promise { - const existingShortenedUrl = - await this.shortenUrlRepository.findShortenedURL(originalURL); - - if (existingShortenedUrl) { - return existingShortenedUrl; - } const id = this.idGenerator.generateId(); const encodedId = this.encodeBase62(Number(id)); const shortenedUrl = this.shortenedBaseUrl + '/' + encodedId; @@ -51,13 +45,4 @@ export class ShortenUrlUsecase { } return encoded; } - - private getBaseUrl(url: string): string { - try { - const parsedUrl = new URL(url); - return `${parsedUrl.protocol}//${parsedUrl.hostname}`; - } catch (error) { - throw new Error(`Invalid URL: ${url}`); - } - } } From 6dbc62f3be220827c5fae463b68ebf7103c63560 Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 21 Mar 2025 11:46:27 +0100 Subject: [PATCH 4/6] files re-org --- .../create-shorten-url.dto.ts | 0 .../shorten-url.controller.spec.ts | 2 +- .../shorten-url.controller.ts | 0 .../shorten-url.id-generator.service.ts | 0 .../shorten-url.usecase.ts | 2 +- api/src/shorten-url/get-original-url.dto.ts | 6 ------ ...edirect-to-original-url-controller.spec.ts} | 18 ++++++++++-------- .../redirect-to-original-url.controller.ts} | 10 ++++++---- .../redirect-to-original-url.usecase.ts} | 4 ++-- api/src/shorten-url/shorten-url.module.ts | 14 +++++++------- 10 files changed, 27 insertions(+), 29 deletions(-) rename api/src/shorten-url/{ => create-shorten-url}/create-shorten-url.dto.ts (100%) rename api/src/shorten-url/{ => create-shorten-url}/shorten-url.controller.spec.ts (95%) rename api/src/shorten-url/{ => create-shorten-url}/shorten-url.controller.ts (100%) rename api/src/shorten-url/{ => create-shorten-url}/shorten-url.id-generator.service.ts (100%) rename api/src/shorten-url/{ => create-shorten-url}/shorten-url.usecase.ts (96%) delete mode 100644 api/src/shorten-url/get-original-url.dto.ts rename api/src/shorten-url/{get-original-url-controller.spec.ts => redirect-to-original-url/redirect-to-original-url-controller.spec.ts} (71%) rename api/src/shorten-url/{get-original-url.controller.ts => redirect-to-original-url/redirect-to-original-url.controller.ts} (61%) rename api/src/shorten-url/{get-original-url.usecase.ts => redirect-to-original-url/redirect-to-original-url.usecase.ts} (80%) diff --git a/api/src/shorten-url/create-shorten-url.dto.ts b/api/src/shorten-url/create-shorten-url/create-shorten-url.dto.ts similarity index 100% rename from api/src/shorten-url/create-shorten-url.dto.ts rename to api/src/shorten-url/create-shorten-url/create-shorten-url.dto.ts diff --git a/api/src/shorten-url/shorten-url.controller.spec.ts b/api/src/shorten-url/create-shorten-url/shorten-url.controller.spec.ts similarity index 95% rename from api/src/shorten-url/shorten-url.controller.spec.ts rename to api/src/shorten-url/create-shorten-url/shorten-url.controller.spec.ts index f9b67ff..2dc02ca 100644 --- a/api/src/shorten-url/shorten-url.controller.spec.ts +++ b/api/src/shorten-url/create-shorten-url/shorten-url.controller.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ShortenUrlController } from './shorten-url.controller'; import { ConfigModule } from '@nestjs/config'; -import { ShortenUrlModule } from './shorten-url.module'; +import { ShortenUrlModule } from '../shorten-url.module'; describe('ShortenUrl controller', () => { let underTest: ShortenUrlController; diff --git a/api/src/shorten-url/shorten-url.controller.ts b/api/src/shorten-url/create-shorten-url/shorten-url.controller.ts similarity index 100% rename from api/src/shorten-url/shorten-url.controller.ts rename to api/src/shorten-url/create-shorten-url/shorten-url.controller.ts diff --git a/api/src/shorten-url/shorten-url.id-generator.service.ts b/api/src/shorten-url/create-shorten-url/shorten-url.id-generator.service.ts similarity index 100% rename from api/src/shorten-url/shorten-url.id-generator.service.ts rename to api/src/shorten-url/create-shorten-url/shorten-url.id-generator.service.ts diff --git a/api/src/shorten-url/shorten-url.usecase.ts b/api/src/shorten-url/create-shorten-url/shorten-url.usecase.ts similarity index 96% rename from api/src/shorten-url/shorten-url.usecase.ts rename to api/src/shorten-url/create-shorten-url/shorten-url.usecase.ts index cd39734..398f270 100644 --- a/api/src/shorten-url/shorten-url.usecase.ts +++ b/api/src/shorten-url/create-shorten-url/shorten-url.usecase.ts @@ -1,6 +1,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service'; -import { ShortenUrlRepository } from './shorten-url.repository'; +import { ShortenUrlRepository } from '../shorten-url.repository'; import { ConfigService } from '@nestjs/config'; @Injectable() diff --git a/api/src/shorten-url/get-original-url.dto.ts b/api/src/shorten-url/get-original-url.dto.ts deleted file mode 100644 index 735c408..0000000 --- a/api/src/shorten-url/get-original-url.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IsString, IsUrl } from 'class-validator'; - -export class GetOriginalUrlDto { - @IsUrl() - shortenedUrl: string; -} diff --git a/api/src/shorten-url/get-original-url-controller.spec.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts similarity index 71% rename from api/src/shorten-url/get-original-url-controller.spec.ts rename to api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts index d15e9a6..f829cae 100644 --- a/api/src/shorten-url/get-original-url-controller.spec.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts @@ -1,12 +1,12 @@ -import { ShortenUrlController } from './shorten-url.controller'; -import { GetOriginalUrlController } from './get-original-url.controller'; +import { ShortenUrlController } from '../create-shorten-url/shorten-url.controller'; +import { RedirectToOriginalUrlController } from './redirect-to-original-url.controller'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigModule } from '@nestjs/config'; -import { ShortenUrlModule } from './shorten-url.module'; +import { ShortenUrlModule } from '../shorten-url.module'; -describe('ShortenUrl controller', () => { +describe('Redirect to original url controller', () => { let shortenUrlController: ShortenUrlController; - let underTest: GetOriginalUrlController; + let underTest: RedirectToOriginalUrlController; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ @@ -14,7 +14,9 @@ describe('ShortenUrl controller', () => { }).compile(); shortenUrlController = app.get(ShortenUrlController); - underTest = app.get(GetOriginalUrlController); + underTest = app.get( + RedirectToOriginalUrlController, + ); }); describe('Redirect to original url', () => { @@ -32,7 +34,7 @@ describe('ShortenUrl controller', () => { const slug = shortenedUrl.split('/').pop(); // Now retrieve the original URL using GetOriginalUrlController - const response = await underTest.getOriginalUrl(slug); + const response = await underTest.redirectToOriginalUrl(slug); // TODO check with an integration / e2e test the redirection code (302) expect(response).toEqual({ url: longUrl }); @@ -44,7 +46,7 @@ describe('ShortenUrl controller', () => { const res = { redirect } as any; const slug = 'does-not-exist'; - await expect(underTest.getOriginalUrl(slug)).rejects.toThrow( + await expect(underTest.redirectToOriginalUrl(slug)).rejects.toThrow( 'Shortened URL "does-not-exist" not found', ); }); diff --git a/api/src/shorten-url/get-original-url.controller.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts similarity index 61% rename from api/src/shorten-url/get-original-url.controller.ts rename to api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts index ea71373..da0ee59 100644 --- a/api/src/shorten-url/get-original-url.controller.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts @@ -5,15 +5,17 @@ import { Param, Redirect, } from '@nestjs/common'; -import { GetOriginalUrlUsecase } from './get-original-url.usecase'; +import { RedirectToOriginalUrlUsecase } from './redirect-to-original-url.usecase'; @Controller() -export class GetOriginalUrlController { - constructor(private readonly getOriginalUrlUsecase: GetOriginalUrlUsecase) {} +export class RedirectToOriginalUrlController { + constructor( + private readonly getOriginalUrlUsecase: RedirectToOriginalUrlUsecase, + ) {} @Get(':slug') @Redirect(undefined, 302) - async getOriginalUrl(@Param('slug') slug: string) { + async redirectToOriginalUrl(@Param('slug') slug: string) { const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl(slug); if (!originalUrl) { throw new NotFoundException(`Shortened URL "${slug}" not found`); diff --git a/api/src/shorten-url/get-original-url.usecase.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts similarity index 80% rename from api/src/shorten-url/get-original-url.usecase.ts rename to api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts index e5eaabe..34aa9e0 100644 --- a/api/src/shorten-url/get-original-url.usecase.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; -import { ShortenUrlRepository } from './shorten-url.repository'; +import { ShortenUrlRepository } from '../shorten-url.repository'; @Injectable() -export class GetOriginalUrlUsecase { +export class RedirectToOriginalUrlUsecase { constructor( @Inject('ShortenUrlRepository') private readonly shortenUrlRepository: ShortenUrlRepository, diff --git a/api/src/shorten-url/shorten-url.module.ts b/api/src/shorten-url/shorten-url.module.ts index 35804a8..bac29ee 100644 --- a/api/src/shorten-url/shorten-url.module.ts +++ b/api/src/shorten-url/shorten-url.module.ts @@ -1,19 +1,19 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { ShortenUrlController } from './shorten-url.controller'; -import { ShortenUrlUsecase } from './shorten-url.usecase'; -import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service'; +import { ShortenUrlController } from './create-shorten-url/shorten-url.controller'; +import { ShortenUrlUsecase } from './create-shorten-url/shorten-url.usecase'; +import { ShortenUrlIdGeneratorService } from './create-shorten-url/shorten-url.id-generator.service'; import { InfrastructureModule } from '../infrastructure/infrastructure.module'; import { ShortenUrlRepository } from './shorten-url.repository'; -import { GetOriginalUrlUsecase } from './get-original-url.usecase'; -import { GetOriginalUrlController } from './get-original-url.controller'; +import { RedirectToOriginalUrlUsecase } from './redirect-to-original-url/redirect-to-original-url.usecase'; +import { RedirectToOriginalUrlController } from './redirect-to-original-url/redirect-to-original-url.controller'; @Module({ imports: [ ConfigModule, // ✅ Ensure ConfigModule is available InfrastructureModule.register(), // ✅ Register InfrastructureModule dynamically ], - controllers: [ShortenUrlController, GetOriginalUrlController], + controllers: [ShortenUrlController, RedirectToOriginalUrlController], providers: [ { provide: 'ShortenUrlRepository', @@ -21,7 +21,7 @@ import { GetOriginalUrlController } from './get-original-url.controller'; }, ShortenUrlIdGeneratorService, ShortenUrlUsecase, - GetOriginalUrlUsecase, + RedirectToOriginalUrlUsecase, ], }) export class ShortenUrlModule {} From 40402da69f3b88a4480f2b8e201d26dc15796bc8 Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 21 Mar 2025 11:55:30 +0100 Subject: [PATCH 5/6] rename shorten-url with create-shorten-url --- ...spec.ts => create-shorten-url.controller.spec.ts} | 6 +++--- ...ontroller.ts => create-shorten-url.controller.ts} | 7 +++---- ...-url.usecase.ts => create-shorten-url.usecase.ts} | 12 ++++++------ .../redirect-to-original-url-controller.spec.ts | 8 +++++--- .../redirect-to-original-url.controller.ts | 4 +++- .../redirect-to-original-url.usecase.ts | 4 ++-- api/src/shorten-url/shorten-url.module.ts | 8 ++++---- 7 files changed, 26 insertions(+), 23 deletions(-) rename api/src/shorten-url/create-shorten-url/{shorten-url.controller.spec.ts => create-shorten-url.controller.spec.ts} (83%) rename api/src/shorten-url/create-shorten-url/{shorten-url.controller.ts => create-shorten-url.controller.ts} (67%) rename api/src/shorten-url/create-shorten-url/{shorten-url.usecase.ts => create-shorten-url.usecase.ts} (77%) diff --git a/api/src/shorten-url/create-shorten-url/shorten-url.controller.spec.ts b/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.spec.ts similarity index 83% rename from api/src/shorten-url/create-shorten-url/shorten-url.controller.spec.ts rename to api/src/shorten-url/create-shorten-url/create-shorten-url.controller.spec.ts index 2dc02ca..5c558a3 100644 --- a/api/src/shorten-url/create-shorten-url/shorten-url.controller.spec.ts +++ b/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.spec.ts @@ -1,17 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { ShortenUrlController } from './shorten-url.controller'; +import { CreateShortenUrlController } from './create-shorten-url.controller'; import { ConfigModule } from '@nestjs/config'; import { ShortenUrlModule } from '../shorten-url.module'; describe('ShortenUrl controller', () => { - let underTest: ShortenUrlController; + let underTest: CreateShortenUrlController; beforeEach(async () => { const app: TestingModule = await Test.createTestingModule({ imports: [await ConfigModule.forRoot(), ShortenUrlModule], }).compile(); - underTest = app.get(ShortenUrlController); + underTest = app.get(CreateShortenUrlController); }); describe('shortenUrl', () => { diff --git a/api/src/shorten-url/create-shorten-url/shorten-url.controller.ts b/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts similarity index 67% rename from api/src/shorten-url/create-shorten-url/shorten-url.controller.ts rename to api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts index 0a45113..3031aa6 100644 --- a/api/src/shorten-url/create-shorten-url/shorten-url.controller.ts +++ b/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts @@ -1,16 +1,15 @@ import { Body, Controller, Logger, Post } from '@nestjs/common'; -import { ShortenUrlUsecase } from './shorten-url.usecase'; +import { CreateShortenUrlUsecase } from './create-shorten-url.usecase'; import { CreateShortenUrlDto } from './create-shorten-url.dto'; @Controller('/shorten-url') -export class ShortenUrlController { - constructor(private readonly shortenUrlUsecase: ShortenUrlUsecase) {} +export class CreateShortenUrlController { + constructor(private readonly shortenUrlUsecase: CreateShortenUrlUsecase) {} @Post() async shortenUrl( @Body() request: CreateShortenUrlDto, ): Promise<{ shortenedUrl: string }> { - Logger.log(`Received url ${request.url} to shorten`); // TODO perhaps it is worth converting the URL from string to URL object const shortenedUrl = await this.shortenUrlUsecase.shortenUrl(request.url); return { shortenedUrl }; diff --git a/api/src/shorten-url/create-shorten-url/shorten-url.usecase.ts b/api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts similarity index 77% rename from api/src/shorten-url/create-shorten-url/shorten-url.usecase.ts rename to api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts index 398f270..7a0d121 100644 --- a/api/src/shorten-url/create-shorten-url/shorten-url.usecase.ts +++ b/api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts @@ -4,11 +4,11 @@ import { ShortenUrlRepository } from '../shorten-url.repository'; import { ConfigService } from '@nestjs/config'; @Injectable() -export class ShortenUrlUsecase { +export class CreateShortenUrlUsecase { private static BASE62_CHARACTERS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - private static BASE62 = ShortenUrlUsecase.BASE62_CHARACTERS.length; + private static BASE62 = CreateShortenUrlUsecase.BASE62_CHARACTERS.length; private readonly shortenedBaseUrl: string; @@ -35,13 +35,13 @@ export class ShortenUrlUsecase { } private encodeBase62(id: number): string { - if (id === 0) return ShortenUrlUsecase.BASE62_CHARACTERS[0]; + if (id === 0) return CreateShortenUrlUsecase.BASE62_CHARACTERS[0]; let encoded = ''; while (id > 0) { - const remainder = id % ShortenUrlUsecase.BASE62; - encoded = ShortenUrlUsecase.BASE62_CHARACTERS[remainder] + encoded; - id = Math.floor(id / ShortenUrlUsecase.BASE62); + const remainder = id % CreateShortenUrlUsecase.BASE62; + encoded = CreateShortenUrlUsecase.BASE62_CHARACTERS[remainder] + encoded; + id = Math.floor(id / CreateShortenUrlUsecase.BASE62); } return encoded; } diff --git a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts index f829cae..cfb32c3 100644 --- a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts @@ -1,11 +1,11 @@ -import { ShortenUrlController } from '../create-shorten-url/shorten-url.controller'; +import { CreateShortenUrlController } from '../create-shorten-url/create-shorten-url.controller'; import { RedirectToOriginalUrlController } from './redirect-to-original-url.controller'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigModule } from '@nestjs/config'; import { ShortenUrlModule } from '../shorten-url.module'; describe('Redirect to original url controller', () => { - let shortenUrlController: ShortenUrlController; + let shortenUrlController: CreateShortenUrlController; let underTest: RedirectToOriginalUrlController; beforeEach(async () => { @@ -13,7 +13,9 @@ describe('Redirect to original url controller', () => { imports: [await ConfigModule.forRoot(), ShortenUrlModule], }).compile(); - shortenUrlController = app.get(ShortenUrlController); + shortenUrlController = app.get( + CreateShortenUrlController, + ); underTest = app.get( RedirectToOriginalUrlController, ); diff --git a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts index da0ee59..1c39313 100644 --- a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts @@ -16,7 +16,9 @@ export class RedirectToOriginalUrlController { @Get(':slug') @Redirect(undefined, 302) async redirectToOriginalUrl(@Param('slug') slug: string) { - const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl(slug); + const originalUrl = await this.getOriginalUrlUsecase.redirectToOriginalUrl( + slug, + ); if (!originalUrl) { throw new NotFoundException(`Shortened URL "${slug}" not found`); } diff --git a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts index 34aa9e0..77336c2 100644 --- a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts @@ -8,9 +8,9 @@ export class RedirectToOriginalUrlUsecase { private readonly shortenUrlRepository: ShortenUrlRepository, ) {} - async getOriginalUrl(shortenedUrl: string): Promise { + async redirectToOriginalUrl(slug: string): Promise { // TODO: reconstruct the URL ? or store only the shorted path ?? - const url = 'http://localhost:3000/' + shortenedUrl; + const url = 'http://localhost:3000/' + slug; return await this.shortenUrlRepository.findOriginalURL(url); } } diff --git a/api/src/shorten-url/shorten-url.module.ts b/api/src/shorten-url/shorten-url.module.ts index bac29ee..387bb03 100644 --- a/api/src/shorten-url/shorten-url.module.ts +++ b/api/src/shorten-url/shorten-url.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { ShortenUrlController } from './create-shorten-url/shorten-url.controller'; -import { ShortenUrlUsecase } from './create-shorten-url/shorten-url.usecase'; +import { CreateShortenUrlController } from './create-shorten-url/create-shorten-url.controller'; +import { CreateShortenUrlUsecase } from './create-shorten-url/create-shorten-url.usecase'; import { ShortenUrlIdGeneratorService } from './create-shorten-url/shorten-url.id-generator.service'; import { InfrastructureModule } from '../infrastructure/infrastructure.module'; import { ShortenUrlRepository } from './shorten-url.repository'; @@ -13,14 +13,14 @@ import { RedirectToOriginalUrlController } from './redirect-to-original-url/redi ConfigModule, // ✅ Ensure ConfigModule is available InfrastructureModule.register(), // ✅ Register InfrastructureModule dynamically ], - controllers: [ShortenUrlController, RedirectToOriginalUrlController], + controllers: [CreateShortenUrlController, RedirectToOriginalUrlController], providers: [ { provide: 'ShortenUrlRepository', useExisting: ShortenUrlRepository, // ✅ Use injected provider }, ShortenUrlIdGeneratorService, - ShortenUrlUsecase, + CreateShortenUrlUsecase, RedirectToOriginalUrlUsecase, ], }) From 564e5aa55c9a0201d3be66aa79db48315846909c Mon Sep 17 00:00:00 2001 From: selimyanat Date: Fri, 21 Mar 2025 14:53:46 +0100 Subject: [PATCH 6/6] encapsulate the url in an URL object --- .../infrastructure/repository/in-memory-url.repository.ts | 8 ++++---- api/src/infrastructure/repository/redis-url.repository.ts | 8 ++++---- .../create-shorten-url/create-shorten-url.controller.ts | 5 +++-- .../create-shorten-url/create-shorten-url.usecase.ts | 8 ++++---- .../redirect-to-original-url-controller.spec.ts | 3 --- .../redirect-to-original-url.controller.ts | 2 +- .../redirect-to-original-url.usecase.ts | 7 +++---- api/src/shorten-url/shorten-url.repository.ts | 4 ++-- 8 files changed, 21 insertions(+), 24 deletions(-) diff --git a/api/src/infrastructure/repository/in-memory-url.repository.ts b/api/src/infrastructure/repository/in-memory-url.repository.ts index 9541270..009dfa0 100644 --- a/api/src/infrastructure/repository/in-memory-url.repository.ts +++ b/api/src/infrastructure/repository/in-memory-url.repository.ts @@ -5,14 +5,14 @@ import { Injectable, Logger } from '@nestjs/common'; export class InMemoryUrlRepository implements ShortenUrlRepository { private urls: Map = new Map(); - create(url: string, shortenedUrl: string): Promise { - this.urls.set(url, shortenedUrl); + create(url: string, encodedUrl: string): Promise { + this.urls.set(url, encodedUrl); return Promise.resolve(undefined); } - findOriginalURL(shortenedUrl: string): Promise { + findOriginalURL(encodedUrl: string): Promise { const originalUrl = Array.from(this.urls.keys()).find( - (key) => this.urls.get(key) === shortenedUrl, + (key) => this.urls.get(key) === encodedUrl, ); return Promise.resolve(originalUrl || null); } diff --git a/api/src/infrastructure/repository/redis-url.repository.ts b/api/src/infrastructure/repository/redis-url.repository.ts index ded6ed2..7ebc2ba 100644 --- a/api/src/infrastructure/repository/redis-url.repository.ts +++ b/api/src/infrastructure/repository/redis-url.repository.ts @@ -30,9 +30,9 @@ export class RedisUrlRepository ); } - async create(url: string, shortenedUrl: string): Promise { + async create(url: string, encodedUrl: string): Promise { await this.redisClient.set( - `shortenedUrl: ${shortenedUrl}`, + `encodedUrl: ${encodedUrl}`, `originalUrl: ${url}`, { EX: this.redisTTL, @@ -40,8 +40,8 @@ export class RedisUrlRepository ); } - async findOriginalURL(originalUrl: string): Promise { - return await this.redisClient.get(`originalUrl:${originalUrl}`); + async findOriginalURL(encodedUrl: string): Promise { + return await this.redisClient.get(`encodedUrl:${encodedUrl}`); } async onModuleInit() { diff --git a/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts b/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts index 3031aa6..9c19a90 100644 --- a/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts +++ b/api/src/shorten-url/create-shorten-url/create-shorten-url.controller.ts @@ -11,7 +11,8 @@ export class CreateShortenUrlController { @Body() request: CreateShortenUrlDto, ): Promise<{ shortenedUrl: string }> { // TODO perhaps it is worth converting the URL from string to URL object - const shortenedUrl = await this.shortenUrlUsecase.shortenUrl(request.url); - return { shortenedUrl }; + const url = new URL(request.url); + const shortenedUrl = await this.shortenUrlUsecase.shortenUrl(url); + return { shortenedUrl: shortenedUrl.toString() }; } } diff --git a/api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts b/api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts index 7a0d121..c9e2585 100644 --- a/api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts +++ b/api/src/shorten-url/create-shorten-url/create-shorten-url.usecase.ts @@ -26,11 +26,11 @@ export class CreateShortenUrlUsecase { this.configService.get('SHORTENED_BASE_URL'); } - async shortenUrl(originalURL: string): Promise { + async shortenUrl(originalURL: URL): Promise { const id = this.idGenerator.generateId(); - const encodedId = this.encodeBase62(Number(id)); - const shortenedUrl = this.shortenedBaseUrl + '/' + encodedId; - await this.shortenUrlRepository.create(originalURL, shortenedUrl); + const encodedUrl = this.encodeBase62(Number(id)); + await this.shortenUrlRepository.create(originalURL.toString(), encodedUrl); + const shortenedUrl = new URL(encodedUrl, this.shortenedBaseUrl); return shortenedUrl; } diff --git a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts index cfb32c3..46043e5 100644 --- a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url-controller.spec.ts @@ -44,9 +44,6 @@ describe('Redirect to original url controller', () => { }); it('should return 404 if the shortened URL is not found', async () => { - const redirect = jest.fn(); - const res = { redirect } as any; - const slug = 'does-not-exist'; await expect(underTest.redirectToOriginalUrl(slug)).rejects.toThrow( 'Shortened URL "does-not-exist" not found', diff --git a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts index 1c39313..f1af2bf 100644 --- a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.controller.ts @@ -23,6 +23,6 @@ export class RedirectToOriginalUrlController { throw new NotFoundException(`Shortened URL "${slug}" not found`); } // Use temporary redirect to original URL so that we can capture analytics - return { url: originalUrl }; + return { url: originalUrl.toString() }; } } diff --git a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts index 77336c2..9c2856b 100644 --- a/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts +++ b/api/src/shorten-url/redirect-to-original-url/redirect-to-original-url.usecase.ts @@ -8,9 +8,8 @@ export class RedirectToOriginalUrlUsecase { private readonly shortenUrlRepository: ShortenUrlRepository, ) {} - async redirectToOriginalUrl(slug: string): Promise { - // TODO: reconstruct the URL ? or store only the shorted path ?? - const url = 'http://localhost:3000/' + slug; - return await this.shortenUrlRepository.findOriginalURL(url); + async redirectToOriginalUrl(slug: string): Promise { + const originalUrl = await this.shortenUrlRepository.findOriginalURL(slug); + return originalUrl ? new URL(originalUrl) : null; } } diff --git a/api/src/shorten-url/shorten-url.repository.ts b/api/src/shorten-url/shorten-url.repository.ts index 4dfe256..332fd05 100644 --- a/api/src/shorten-url/shorten-url.repository.ts +++ b/api/src/shorten-url/shorten-url.repository.ts @@ -1,5 +1,5 @@ export abstract class ShortenUrlRepository { - abstract create(url: string, shortenedUrl: string): Promise; + abstract create(url: string, encodedUrl: string): Promise; - abstract findOriginalURL(shortenedUrl: string): Promise; + abstract findOriginalURL(encodedUrl: string): Promise; }