Skip to content

Commit b74ec4a

Browse files
authored
Get original url from database and do not create new short URL if it exist. Use constructor parameter injection for url shortener service (#21)
1 parent d91bf4f commit b74ec4a

5 files changed

Lines changed: 88 additions & 16 deletions

File tree

src/controllers/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Request, Response } from 'express';
1+
import { KeyGenerationServiceImpl } from './../services/impl/keyGeneration';
2+
import { Response } from 'express';
23
import {
34
ValidatedRequest,
45
// Extend from this to define a valid schema type/interface
@@ -13,6 +14,7 @@ import { UrlShortenerServiceImpl } from '../services/impl/urlShortener';
1314

1415
import { UrlDaoImpl } from '../db/postgres/dao/urlDao';
1516
import { Redis } from '../db/redis';
17+
import { throwError, ErrorCode } from '../utils/error';
1618
import logger from '../utils/logger';
1719

1820
export const healthCheck = async (
@@ -48,7 +50,7 @@ export const shortenUrl = async (
4850
res: Response
4951
) => {
5052
const { url, alias }: ShortenUrlRequest = req.body;
51-
const shortenUrlService = new UrlShortenerServiceImpl();
53+
const shortenUrlService = UrlShortenerServiceImpl.defaultImpl();
5254
const shortenedUrl = await shortenUrlService.shortenUrl(url, alias);
5355
res.json(shortenedUrl);
5456
};
@@ -58,7 +60,14 @@ export const redirectUrl = async (
5860
res: Response
5961
) => {
6062
const { urlKey } = req.params;
61-
const shortenUrlService = new UrlShortenerServiceImpl();
63+
const shortenUrlService = UrlShortenerServiceImpl.defaultImpl();
6264
const originalUrl = await shortenUrlService.getOriginalUrl(urlKey);
65+
66+
if (!originalUrl) {
67+
throwError({
68+
status: ErrorCode.NOT_FOUND,
69+
message: `Short URL ${urlKey} does not exist`,
70+
});
71+
}
6372
res.redirect(307, originalUrl);
6473
};

src/db/postgres/dao/urlDao.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface UrlDao {
66
insert(id: string, originalUrl: string): Promise<void>;
77
findById(id: string): Promise<Url>;
88
findByIds(ids: string[]): Promise<Url[]>;
9+
findByOriginalUrl(originalUrl: string): Promise<Url[]>;
910
}
1011

1112
export class UrlDaoImpl implements UrlDao {
@@ -26,4 +27,10 @@ export class UrlDaoImpl implements UrlDao {
2627
public async findByIds(ids: string[]): Promise<Url[]> {
2728
return Url.query().findByIds(ids);
2829
}
30+
31+
public async findByOriginalUrl(originalUrl: string): Promise<Url[]> {
32+
return Url.query().where({
33+
originalUrl,
34+
});
35+
}
2936
}

src/services/impl/urlShortener.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { UrlShortenerService } from '../urlShortener';
1+
import { UrlShortenerService } from './../urlShortener';
22
import { KeyGenerationServiceImpl } from './keyGeneration';
33
import { KeyGeneration } from '../keyGeneration';
44
import config from '../../config';
@@ -13,10 +13,22 @@ export class UrlShortenerServiceImpl implements UrlShortenerService {
1313
private urlDao: UrlDao;
1414
private redis: MemoryStore;
1515

16-
constructor() {
17-
this.redis = Redis.getInstance();
18-
this.keyGenerationService = new KeyGenerationServiceImpl();
19-
this.urlDao = new UrlDaoImpl();
16+
constructor(
17+
urlDao: UrlDao,
18+
keyGenerationService: KeyGeneration,
19+
redis: MemoryStore
20+
) {
21+
this.redis = redis;
22+
this.keyGenerationService = keyGenerationService;
23+
this.urlDao = urlDao;
24+
}
25+
26+
public static defaultImpl(): UrlShortenerService {
27+
return new UrlShortenerServiceImpl(
28+
new UrlDaoImpl(),
29+
new KeyGenerationServiceImpl(),
30+
Redis.getInstance()
31+
);
2032
}
2133

2234
public async shortenUrl(
@@ -28,6 +40,11 @@ export class UrlShortenerServiceImpl implements UrlShortenerService {
2840
await this.ensureAliasDoesNotExist(alias);
2941
key = urlSafe(alias);
3042
} else {
43+
// If key exists, return it, don't store additional records
44+
const urlKey = await this.getUrlKeyFromDb(originalUrl);
45+
if (urlKey) {
46+
return this.constructShortenedUrl(urlKey);
47+
}
3148
key = await this.getAvailableKey();
3249
}
3350

@@ -39,11 +56,28 @@ export class UrlShortenerServiceImpl implements UrlShortenerService {
3956
return this.constructShortenedUrl(key);
4057
}
4158

59+
public async getUrlKeyFromDb(originalUrl: string): Promise<string> {
60+
const urls = await this.urlDao.findByOriginalUrl(originalUrl);
61+
if (!urls) {
62+
logger.info(
63+
`Original url ${originalUrl} is not found in database. Returning undefined`
64+
);
65+
return undefined;
66+
}
67+
if (urls && urls.length) {
68+
const urlKey = urls[0].id;
69+
logger.info(
70+
`Original url ${originalUrl} is found in database, returning url key ${urlKey}`
71+
);
72+
return urlKey;
73+
}
74+
}
75+
4276
public async getOriginalUrl(urlKey: string): Promise<string> {
4377
const cachedData = await this.redis.getString(urlKey);
4478
// TODO: Test. Log cache hit or miss
4579
if (cachedData) {
46-
logger.info(`Found short URL ${urlKey} from cache`);
80+
logger.info(`Found url key ${urlKey} from cache. Returning original url`);
4781
return cachedData;
4882
}
4983

@@ -52,11 +86,11 @@ export class UrlShortenerServiceImpl implements UrlShortenerService {
5286
);
5387
const urlInDb = await this.urlDao.findById(urlKey);
5488
if (!urlInDb) {
55-
throwError({
56-
status: ErrorCode.NOT_FOUND,
57-
message: `/${urlKey} does not exist`,
58-
});
89+
return undefined;
5990
}
91+
logger.info(
92+
`Setting url key ${urlKey} and original url ${urlInDb.originalUrl} in cache`
93+
);
6094
await this.redis.setString(urlKey, urlInDb.originalUrl);
6195
return urlInDb.originalUrl;
6296
}

test/integration/redirectUrl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ describe('Redirect Url integration test', () => {
3030
const urlKey = 'unknown';
3131
const res = await sendRequest(server, 'get', `/${urlKey}`);
3232
expect(res.status).to.equal(ErrorCode.NOT_FOUND);
33-
expect(res.body.error.message).to.equal(`/${urlKey} does not exist`);
33+
expect(res.body.error.message).to.equal(
34+
`Short URL ${urlKey} does not exist`
35+
);
3436
});
3537

3638
it('should redirect if url key exist', async () => {

test/unit/services/urlShortener.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { UrlShortenerServiceImpl } from './../../../src/services/impl/urlShortener';
12
import { expect } from 'chai';
23
import Sinon from 'sinon';
34

4-
import { UrlShortenerServiceImpl } from '../../../src/services/impl/urlShortener';
55
import { UrlDaoImpl } from '../../../src/db/postgres/dao/urlDao';
66
import { Redis } from '../../../src/db/redis';
77

@@ -18,11 +18,31 @@ describe('UrlShortenerService unit tests', () => {
1818
UrlShortenerServiceImpl.prototype,
1919
'ensureAliasDoesNotExist' as any
2020
).resolves();
21+
Sinon.stub(UrlShortenerServiceImpl.prototype, 'getUrlKeyFromDb').resolves(
22+
undefined
23+
);
2124
Sinon.stub(UrlDaoImpl.prototype, 'insert').resolves();
2225
Sinon.stub(Redis, 'getInstance' as any).resolves();
2326

24-
const urlShortenerService = new UrlShortenerServiceImpl();
27+
const urlShortenerService = UrlShortenerServiceImpl.defaultImpl();
2528
const shortenedUrl = await urlShortenerService.shortenUrl(url, alias);
2629
expect(shortenedUrl).to.eq(`http://localhost:3000/${alias}`);
2730
});
31+
32+
it('Should get shortened URL from DB and do not insert into DB', async () => {
33+
const url = 'www.google.com';
34+
Sinon.stub(
35+
UrlShortenerServiceImpl.prototype,
36+
'ensureAliasDoesNotExist' as any
37+
).resolves();
38+
Sinon.stub(UrlShortenerServiceImpl.prototype, 'getUrlKeyFromDb').resolves(
39+
'urlKey'
40+
);
41+
Sinon.stub(UrlDaoImpl.prototype, 'insert').resolves();
42+
Sinon.stub(Redis, 'getInstance' as any).resolves();
43+
44+
const urlShortenerService = UrlShortenerServiceImpl.defaultImpl();
45+
const shortenedUrl = await urlShortenerService.shortenUrl(url);
46+
expect(shortenedUrl).to.eq(`http://localhost:3000/urlKey`);
47+
});
2848
});

0 commit comments

Comments
 (0)