From 614d768d2ae615ba9df8413b86e2b33d53993d2a Mon Sep 17 00:00:00 2001 From: Myrotvorets Date: Tue, 26 Sep 2023 08:40:27 +0300 Subject: [PATCH] Refactoring --- src/controllers/search.mts | 59 +++++++++++++++++++++--------- src/lib/environment.mts | 4 +- src/services/search.mts | 14 +++++-- test/unit/lib/environment.test.mts | 5 +++ test/unit/services/search.test.mts | 9 +++-- 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/controllers/search.mts b/src/controllers/search.mts index 82548ecd..d9ece22f 100644 --- a/src/controllers/search.mts +++ b/src/controllers/search.mts @@ -1,26 +1,49 @@ -import { type NextFunction, type Request, type Response, Router } from 'express'; +import { type NextFunction, type Request, RequestHandler, type Response, Router } from 'express'; import { asyncWrapperMiddleware } from '@myrotvorets/express-async-middleware-wrapper'; -import { SearchService } from '../services/search.mjs'; +import { type SearchItem, SearchService } from '../services/search.mjs'; +import { environment } from '../lib/environment.mjs'; -async function searchHandler(req: Request, res: Response, next: NextFunction): Promise { - const result = await SearchService.search(req.query.s as string); - if (result === null) { - next({ - success: false, - status: 400, - code: 'BAD_SEARCH_TERM', - message: 'Both surname and name are required', - }); - } else { - res.json({ - success: true, - items: result, - }); - } +type DefaultParams = Record; + +interface SearchRequestParams { + s: string; +} + +interface SearchResponse { + success: true; + items: SearchItem[]; +} + +function searchHandler( + service: SearchService, +): RequestHandler { + return asyncWrapperMiddleware(async function _searchHandler( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + const result = await service.search(req.query.s); + if (result === null) { + next({ + success: false, + status: 400, + code: 'BAD_SEARCH_TERM', + message: 'Both surname and name are required', + }); + } else { + res.json({ + success: true, + items: result, + }); + } + }); } export function searchController(): Router { const router = Router(); - router.get('/search', asyncWrapperMiddleware(searchHandler)); + const env = environment(); + const service = new SearchService(env.IMAGE_CDN_PREFIX); + + router.get('/search', searchHandler(service)); return router; } diff --git a/src/lib/environment.mts b/src/lib/environment.mts index 4d40d9c0..b0378e52 100644 --- a/src/lib/environment.mts +++ b/src/lib/environment.mts @@ -1,8 +1,9 @@ -import { cleanEnv, port, str } from 'envalid'; +import { cleanEnv, port, str, url } from 'envalid'; export interface Environment { NODE_ENV: string; PORT: number; + IMAGE_CDN_PREFIX: string; } let environ: Environment | null = null; @@ -12,6 +13,7 @@ export function environment(reset = false): Environment { environ = cleanEnv(process.env, { NODE_ENV: str({ default: 'development' }), PORT: port({ default: 3000 }), + IMAGE_CDN_PREFIX: url({ default: 'https://cdn.myrotvorets.center/m/' }), }); } diff --git a/src/services/search.mts b/src/services/search.mts index 5e4d348c..ab16e1fd 100644 --- a/src/services/search.mts +++ b/src/services/search.mts @@ -16,7 +16,13 @@ export interface SearchItem { } export class SearchService { - public static async search(name: string): Promise { + private readonly _cdnPrefix: string; + + public constructor(cdnPrefix: string) { + this._cdnPrefix = cdnPrefix; + } + + public async search(name: string): Promise { const n = SearchService.prepareName(name); if (!n) { return null; @@ -35,13 +41,13 @@ export class SearchService { if (rows) { const thumbs = SearchService.getThumbnails(atts); - return SearchService.prepareResult(rows, thumbs); + return this.prepareResult(rows, thumbs); } return []; } - private static prepareResult(criminals: Criminal[], thumbs: Record): SearchItem[] { + private prepareResult(criminals: Criminal[], thumbs: Record): SearchItem[] { return criminals.map((item) => { const entry: SearchItem = { id: item.id, @@ -61,7 +67,7 @@ export class SearchService { } if (typeof thumbs[item.id] !== 'undefined') { - entry.thumbnail = `https://cdn.myrotvorets.center/m/${thumbs[item.id]}`; + entry.thumbnail = `${this._cdnPrefix}${thumbs[item.id]}`; } return entry; diff --git a/test/unit/lib/environment.test.mts b/test/unit/lib/environment.test.mts index 6082b920..8c9e64d6 100644 --- a/test/unit/lib/environment.test.mts +++ b/test/unit/lib/environment.test.mts @@ -16,11 +16,13 @@ describe('environment', function () { const expected: Environment = { NODE_ENV: 'development', PORT: 3000, + IMAGE_CDN_PREFIX: 'https://cdn.example.com/', }; process.env = { NODE_ENV: `${expected.NODE_ENV}`, PORT: `${expected.PORT}`, + IMAGE_CDN_PREFIX: `${expected.IMAGE_CDN_PREFIX}`, EXTRA: 'xxx', }; @@ -32,11 +34,13 @@ describe('environment', function () { const expected: Environment = { NODE_ENV: 'staging', PORT: 3030, + IMAGE_CDN_PREFIX: 'https://cdn.example.com/', }; process.env = { NODE_ENV: `${expected.NODE_ENV}`, PORT: `${expected.PORT}`, + IMAGE_CDN_PREFIX: `${expected.IMAGE_CDN_PREFIX}`, }; let actual = { ...environment(true) }; @@ -45,6 +49,7 @@ describe('environment', function () { process.env = { NODE_ENV: `${expected.NODE_ENV}${expected.NODE_ENV}`, PORT: `1${expected.PORT}`, + IMAGE_CDN_PREFIX: `${expected.IMAGE_CDN_PREFIX}`, }; actual = { ...environment() }; diff --git a/test/unit/services/search.test.mts b/test/unit/services/search.test.mts index 3ccc8be3..21a05979 100644 --- a/test/unit/services/search.test.mts +++ b/test/unit/services/search.test.mts @@ -139,7 +139,8 @@ describe('SearchService', function () { // eslint-disable-next-line mocha/no-setup-in-describe table1.forEach((name) => it(`should return null when prepareName returns falsy value ('${name}')`, function () { - return expect(SearchService.search(name)).to.eventually.be.null; + const svc = new SearchService('https://cdn.example.com/'); + return expect(svc.search(name)).to.eventually.be.null; }), ); @@ -167,7 +168,8 @@ describe('SearchService', function () { }); tracker.install(); - return expect(SearchService.search('Путин Владимир')).to.become([]); + const svc = new SearchService('https://cdn.example.com/'); + return expect(svc.search('Путин Владимир')).to.become([]); }); it('should return the expected results', function () { @@ -201,7 +203,8 @@ describe('SearchService', function () { }); tracker.install(); - return expect(SearchService.search('Our mock will find everything')).to.become(resultItems); + const svc = new SearchService('https://cdn.myrotvorets.center/m/'); + return expect(svc.search('Our mock will find everything')).to.become(resultItems); }); }); });