Skip to content

Commit 1d5ba2d

Browse files
committed
get original url from shortened url
1 parent 572cd4b commit 1d5ba2d

10 files changed

+120
-10
lines changed

api/src/infrastructure/repository/in-memory-url.repository.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,15 @@ export class InMemoryUrlRepository implements ShortenUrlRepository {
1010
return Promise.resolve(undefined);
1111
}
1212

13-
findURL(url: string): Promise<string | null> {
13+
findShortenedURL(url: string): Promise<string | null> {
1414
const shortenedUrl = this.urls.get(url);
1515
return Promise.resolve(shortenedUrl);
1616
}
17+
18+
findOriginalURL(shortenedUrl: string): Promise<string | null> {
19+
const originalUrl = Array.from(this.urls.keys()).find(
20+
(key) => this.urls.get(key) === shortenedUrl,
21+
);
22+
return Promise.resolve(originalUrl || null);
23+
}
1724
}

api/src/infrastructure/repository/redis-url.repository.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,32 @@ export class RedisUrlRepository
3131
}
3232

3333
async create(url: string, shortenedUrl: string): Promise<void> {
34-
await this.redisClient.set(url, shortenedUrl, { EX: this.redisTTL });
34+
// Use two keys to allow both forward and reverse lookup because:
35+
// Efficient retrieval (O(1) time complexity)
36+
// No need to scan all keys
37+
// Reduces query complexity
38+
await this.redisClient.set(
39+
`originalUrl:${url}`,
40+
`shortenedUrl: ${shortenedUrl}`,
41+
{
42+
EX: this.redisTTL,
43+
},
44+
); // 1 day expiry
45+
await this.redisClient.set(
46+
`shortenedUrl: ${shortenedUrl}`,
47+
`originalUrl: ${url}`,
48+
{
49+
EX: this.redisTTL,
50+
},
51+
);
52+
}
53+
54+
async findShortenedURL(shortenedUrl: string): Promise<string | null> {
55+
return await this.redisClient.get(`shortenedUrl:${shortenedUrl}`);
3556
}
3657

37-
async findURL(url: string): Promise<string | null> {
38-
return await this.redisClient.get(url);
58+
async findOriginalURL(originalUrl: string): Promise<string | null> {
59+
return await this.redisClient.get(`originalUrl:${originalUrl}`);
3960
}
4061

4162
async onModuleInit() {

api/src/infrastructure/repository/repository.provider.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { RedisUrlRepository } from './redis-url.repository';
99
*/
1010
export const RepositoryProvider: Provider = {
1111
// TODO: Use a constant for the key
12-
//provide: 'ShortenUrlRepository',
1312
provide: ShortenUrlRepository,
1413
useFactory: (configService: ConfigService) => {
1514
const persistenceType = configService.get<string>(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ShortenUrlController } from './shorten-url.controller';
2+
import { GetOriginalUrlController } from './get-original-url.controller';
3+
import { Test, TestingModule } from '@nestjs/testing';
4+
import { ConfigModule } from '@nestjs/config';
5+
import { ShortenUrlModule } from './shorten-url.module';
6+
7+
describe('ShortenUrl controller', () => {
8+
let shortenUrlController: ShortenUrlController;
9+
let underTest: GetOriginalUrlController;
10+
11+
beforeEach(async () => {
12+
const app: TestingModule = await Test.createTestingModule({
13+
imports: [await ConfigModule.forRoot(), ShortenUrlModule],
14+
}).compile();
15+
16+
shortenUrlController = app.get<ShortenUrlController>(ShortenUrlController);
17+
underTest = app.get<GetOriginalUrlController>(GetOriginalUrlController);
18+
});
19+
20+
describe('getOriginalUrl', () => {
21+
it('should return the original URL for a given shortened URL', async () => {
22+
const longUrl =
23+
'https://zapper.xyz/very-long-url/very-long-url/very-long-url';
24+
25+
// Use the shorten URL controller to create the shortened URL
26+
const shortenedResponse = await shortenUrlController.shortenUrl({
27+
url: longUrl,
28+
});
29+
const shortenedUrl = shortenedResponse.shortenedUrl;
30+
31+
// Now retrieve the original URL using GetOriginalUrlController
32+
const response = await underTest.getOriginalUrl({
33+
shortenedUrl: shortenedUrl,
34+
});
35+
36+
expect(response).not.toBeNull();
37+
expect(response.originalUrl).toBe(longUrl);
38+
});
39+
});
40+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Body, Controller, Get } from '@nestjs/common';
2+
import { GetOriginalUrlDto } from './get-original-url.dto';
3+
import { ShortenUrlUsecase } from './shorten-url.usecase';
4+
import { GetOriginalUrlUsecase } from './get-original-url.usecase';
5+
6+
@Controller('/shorten-url')
7+
export class GetOriginalUrlController {
8+
constructor(private readonly getOriginalUrlUsecase: GetOriginalUrlUsecase) {}
9+
10+
@Get()
11+
async getOriginalUrl(
12+
@Body() request: GetOriginalUrlDto,
13+
): Promise<{ originalUrl: string }> {
14+
const originalUrl = await this.getOriginalUrlUsecase.getOriginalUrl(
15+
request.shortenedUrl,
16+
);
17+
return { originalUrl };
18+
}
19+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { IsString, IsUrl } from 'class-validator';
2+
3+
export class GetOriginalUrlDto {
4+
@IsUrl()
5+
shortenedUrl: string;
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Inject, Injectable } from '@nestjs/common';
2+
import { ShortenUrlRepository } from './shorten-url.repository';
3+
4+
@Injectable()
5+
export class GetOriginalUrlUsecase {
6+
constructor(
7+
@Inject('ShortenUrlRepository')
8+
private readonly shortenUrlRepository: ShortenUrlRepository,
9+
) {}
10+
11+
async getOriginalUrl(shortenedUrl: string): Promise<string | null> {
12+
return await this.shortenUrlRepository.findOriginalURL(shortenedUrl);
13+
}
14+
}

api/src/shorten-url/shorten-url.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@ import { ShortenUrlUsecase } from './shorten-url.usecase';
55
import { ShortenUrlIdGeneratorService } from './shorten-url.id-generator.service';
66
import { InfrastructureModule } from '../infrastructure/infrastructure.module';
77
import { ShortenUrlRepository } from './shorten-url.repository';
8+
import { GetOriginalUrlUsecase } from './get-original-url.usecase';
9+
import { GetOriginalUrlController } from './get-original-url.controller';
810

911
@Module({
1012
imports: [
1113
ConfigModule, // ✅ Ensure ConfigModule is available
1214
InfrastructureModule.register(), // ✅ Register InfrastructureModule dynamically
1315
],
14-
controllers: [ShortenUrlController],
16+
controllers: [ShortenUrlController, GetOriginalUrlController],
1517
providers: [
1618
{
1719
provide: 'ShortenUrlRepository',
1820
useExisting: ShortenUrlRepository, // ✅ Use injected provider
1921
},
2022
ShortenUrlIdGeneratorService,
2123
ShortenUrlUsecase,
24+
GetOriginalUrlUsecase,
2225
],
2326
})
2427
export class ShortenUrlModule {}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export abstract class ShortenUrlRepository {
2-
abstract findURL(url: string): Promise<string | null>;
2+
abstract findShortenedURL(url: string): Promise<string | null>;
33

44
abstract create(url: string, shortenedUrl: string): Promise<void>;
5+
6+
abstract findOriginalURL(shortenedUrl: string): Promise<string | null>;
57
}

api/src/shorten-url/shorten-url.usecase.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,8 @@ export class ShortenUrlUsecase {
2727
}
2828

2929
async shortenUrl(originalURL: string): Promise<string> {
30-
const existingShortenedUrl = await this.shortenUrlRepository.findURL(
31-
originalURL,
32-
);
30+
const existingShortenedUrl =
31+
await this.shortenUrlRepository.findShortenedURL(originalURL);
3332

3433
if (existingShortenedUrl) {
3534
return existingShortenedUrl;

0 commit comments

Comments
 (0)