-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: re-org StatisticsController and its dependencies (#185)
- Loading branch information
1 parent
2615f34
commit 77c0d0f
Showing
16 changed files
with
193 additions
and
108 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,88 +1,17 @@ | ||
import Express from 'express' | ||
import { statClient } from '../redis' | ||
import { logger, statisticsExpiry } from '../config' | ||
import { DependencyIds } from '../constants' | ||
import { container } from '../util/inversify' | ||
import { UrlRepositoryInterface } from '../repositories/interfaces/UrlRepositoryInterface' | ||
import { UserRepositoryInterface } from '../repositories/interfaces/UserRepositoryInterface' | ||
import { StatisticsControllerInterface } from '../controllers/interfaces/StatisticsControllerInterface' | ||
|
||
const router = Express.Router() | ||
const urlRepository = container.get<UrlRepositoryInterface>( | ||
DependencyIds.urlRepository, | ||
) | ||
const userRepository = container.get<UserRepositoryInterface>( | ||
DependencyIds.userRepository, | ||
|
||
const statisticsController = container.get<StatisticsControllerInterface>( | ||
DependencyIds.statisticsController, | ||
) | ||
|
||
/** | ||
* Endpoint to retrieve total user, link, and click counts. | ||
*/ | ||
router.get('/', async (_: Express.Request, res: Express.Response) => { | ||
// Check if cache contains value | ||
statClient.mget( | ||
['userCount', 'linkCount', 'clickCount'], | ||
async (cacheError, results: Array<string | null>) => { | ||
if (cacheError) { | ||
// log and fallback to database | ||
logger.error( | ||
`Access to statistics cache failed unexpectedly:\t${cacheError}`, | ||
) | ||
} | ||
|
||
// Since the expiry in Redis of the values are the same, | ||
// all 3 should be present (or absent) from Redis together | ||
// If the data is not in Redis, results will be [null, null, null] | ||
if (!cacheError && !results.includes(null)) { | ||
// Turn each value into an integer | ||
const [userCount, linkCount, clickCount] = results.map((x) => Number(x)) | ||
|
||
res.json({ userCount, linkCount, clickCount }) | ||
return | ||
} | ||
|
||
// If the values are not found in the cache, we read from the DB | ||
const [userCount, linkCount, clickCountUntrusted] = await Promise.all([ | ||
userRepository.getNumUsers(), | ||
urlRepository.getNumUrls(), | ||
urlRepository.getTotalLinkClicks(), | ||
]) | ||
|
||
// Cater to the edge case where clickCount is NaN because there are no links | ||
const clickCount = Number.isNaN(clickCountUntrusted) | ||
? 0 | ||
: clickCountUntrusted | ||
|
||
res.json({ userCount, linkCount, clickCount }) | ||
|
||
// Store values into Redis | ||
const callback = (err: Error | null) => { | ||
if (err) { | ||
logger.error(`Cache write failed:\t${err}`) | ||
} | ||
} | ||
statClient.set( | ||
'userCount', | ||
`${userCount}`, | ||
'EX', | ||
statisticsExpiry, | ||
callback, | ||
) | ||
statClient.set( | ||
'linkCount', | ||
`${linkCount}`, | ||
'EX', | ||
statisticsExpiry, | ||
callback, | ||
) | ||
statClient.set( | ||
'clickCount', | ||
`${clickCount}`, | ||
'EX', | ||
statisticsExpiry, | ||
callback, | ||
) | ||
}, | ||
) | ||
}) | ||
router.get('/', statisticsController.getGlobalStatistics) | ||
|
||
export = router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import Express from 'express' | ||
import { inject, injectable } from 'inversify' | ||
import { StatisticsControllerInterface } from './interfaces/StatisticsControllerInterface' | ||
import { StatisticsServiceInterface } from '../services/interfaces/StatisticsServiceInterface' | ||
import { DependencyIds } from '../constants' | ||
|
||
@injectable() | ||
export class StatisticsController implements StatisticsControllerInterface { | ||
private statisticsService: StatisticsServiceInterface | ||
|
||
public constructor( | ||
@inject(DependencyIds.statisticsService) | ||
statisticsService: StatisticsServiceInterface, | ||
) { | ||
this.statisticsService = statisticsService | ||
} | ||
|
||
public getGlobalStatistics: ( | ||
req: Express.Request, | ||
res: Express.Response, | ||
) => Promise<void> = async (_, res) => { | ||
res.json(await this.statisticsService.getGlobalStatistics()) | ||
} | ||
} | ||
|
||
export default StatisticsController |
8 changes: 8 additions & 0 deletions
8
src/server/controllers/interfaces/StatisticsControllerInterface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import Express from 'express' | ||
|
||
export interface StatisticsControllerInterface { | ||
getGlobalStatistics( | ||
req: Express.Request, | ||
res: Express.Response, | ||
): Promise<void> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* eslint-disable class-methods-use-this */ | ||
|
||
import { injectable } from 'inversify' | ||
import { User } from '../models/user' | ||
import { Url } from '../models/url' | ||
import { statClient } from '../redis' | ||
import { logger, statisticsExpiry } from '../config' | ||
import { StatisticsRepositoryInterface } from './interfaces/StatisticsRepositoryInterface' | ||
import { GlobalStatistics } from './types' | ||
|
||
const USER_COUNT_KEY = 'userCount' | ||
const CLICK_COUNT_KEY = 'clickCount' | ||
const LINK_COUNT_KEY = 'linkCount' | ||
|
||
@injectable() | ||
export class StatisticsRepository implements StatisticsRepositoryInterface { | ||
public getGlobalStatistics: () => Promise<GlobalStatistics> = async () => { | ||
const counts = await this.tryGetFromCache([ | ||
USER_COUNT_KEY, | ||
CLICK_COUNT_KEY, | ||
LINK_COUNT_KEY, | ||
]) | ||
|
||
let [userCount, clickCount, linkCount] = counts.map((count) => | ||
count != null ? Number(count) : null, | ||
) | ||
|
||
if (userCount == null) { | ||
userCount = await User.count() | ||
this.trySetCache(USER_COUNT_KEY, userCount.toString()) | ||
} | ||
|
||
if (clickCount == null) { | ||
clickCount = await Url.sum('clicks') | ||
this.trySetCache(CLICK_COUNT_KEY, clickCount.toString()) | ||
} | ||
|
||
if (linkCount == null) { | ||
linkCount = await Url.count() | ||
this.trySetCache(LINK_COUNT_KEY, linkCount.toString()) | ||
} | ||
|
||
return { linkCount, clickCount, userCount } | ||
} | ||
|
||
private tryGetFromCache(keys: string[]): Promise<(string | null)[]> { | ||
return new Promise((resolve) => | ||
statClient.mget(keys, (cacheError, result) => { | ||
if (cacheError) { | ||
logger.error( | ||
`Access to statistics cache failed unexpectedly:\t${cacheError}`, | ||
) | ||
resolve(keys.map(() => null)) | ||
} | ||
resolve(result) | ||
}), | ||
) | ||
} | ||
|
||
private trySetCache(key: string, value: string): void { | ||
statClient.set(key, value, 'EX', statisticsExpiry, (err: Error | null) => { | ||
if (err) { | ||
logger.error(`Cache write failed:\t${err}`) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
export default StatisticsRepository |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
src/server/repositories/interfaces/StatisticsRepositoryInterface.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { GlobalStatistics } from '../types' | ||
|
||
export interface StatisticsRepositoryInterface { | ||
/** | ||
* Retrieves the global statistics from the store. | ||
* These include total click, link and user count. | ||
* @returns Promise that resolves to an object that encapsulates statistics. | ||
*/ | ||
getGlobalStatistics(): Promise<GlobalStatistics> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { inject, injectable } from 'inversify' | ||
import { StatisticsServiceInterface } from './interfaces/StatisticsServiceInterface' | ||
import { DependencyIds } from '../constants' | ||
import { StatisticsRepositoryInterface } from '../repositories/interfaces/StatisticsRepositoryInterface' | ||
import { GlobalStatistics } from '../repositories/types' | ||
|
||
@injectable() | ||
export class StatisticsService implements StatisticsServiceInterface { | ||
private statisticsRepository: StatisticsRepositoryInterface | ||
|
||
public constructor( | ||
@inject(DependencyIds.statisticsRepository) | ||
statisticsRepository: StatisticsRepositoryInterface, | ||
) { | ||
this.statisticsRepository = statisticsRepository | ||
} | ||
|
||
getGlobalStatistics: () => Promise<GlobalStatistics> = async () => { | ||
return this.statisticsRepository.getGlobalStatistics() | ||
} | ||
} | ||
|
||
export default StatisticsService |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { GlobalStatistics } from '../../repositories/types' | ||
|
||
export interface StatisticsServiceInterface { | ||
getGlobalStatistics(): Promise<GlobalStatistics> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* eslint-disable class-methods-use-this */ | ||
|
||
import { injectable } from 'inversify' | ||
import { StatisticsRepositoryInterface } from '../../../../src/server/repositories/interfaces/StatisticsRepositoryInterface' | ||
import { GlobalStatistics } from '../../../../src/server/repositories/types' | ||
|
||
@injectable() | ||
export class MockStatisticsRepository implements StatisticsRepositoryInterface { | ||
getGlobalStatistics(): Promise<GlobalStatistics> { | ||
return Promise.resolve({ userCount: 1, clickCount: 2, linkCount: 3 }) | ||
} | ||
} | ||
|
||
export default MockStatisticsRepository |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { StatisticsService } from '../../../src/server/services/StatisticsService' | ||
import { MockStatisticsRepository } from '../mocks/repositories/StatisticsRepository' | ||
|
||
/** | ||
* Unit tests for StatisticService. | ||
*/ | ||
describe('StatisticService tests', () => { | ||
describe('getGlobalStatistics tests', () => { | ||
it('Should return statistics from repository', async () => { | ||
const service = new StatisticsService(new MockStatisticsRepository()) | ||
await expect(service.getGlobalStatistics()).resolves.toStrictEqual({ | ||
userCount: 1, | ||
clickCount: 2, | ||
linkCount: 3, | ||
}) | ||
}) | ||
}) | ||
}) |