From 0fae87c4c8f79a7155d40f7b64d90a7446964066 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Thu, 11 Aug 2022 20:02:53 +0300 Subject: [PATCH] Added Elastic PIT features and tested. --- documentation/classes/EsQueryDto.html | 396 ++++++++++++++++++ documentation/classes/SearchQueryDto.html | 100 +++++ documentation/classes/SearchResultDto.html | 12 +- .../controllers/PapersController.html | 19 +- documentation/coverage.html | 30 +- documentation/graph/dependencies.svg | 174 ++++---- .../injectables/PageInterceptor.html | 16 +- documentation/injectables/SearchService.html | 241 ++++++++++- documentation/interfaces/PageMeta.html | 50 +++ documentation/js/menu-wc.js | 37 +- documentation/js/menu-wc_es5.js | 2 +- documentation/js/search/search_index.js | 4 +- documentation/miscellaneous/variables.html | 112 +++-- documentation/modules/AppModule.html | 12 +- .../modules/CommonModule/dependencies.svg | 8 +- documentation/modules/SearchModule.html | 17 +- documentation/overview.html | 2 +- src/core/domain/dtos/es-query.dto.ts | 45 ++ src/core/domain/dtos/search-q.dto.ts | 7 + src/core/services/common/search.service.ts | 49 ++- src/infrastructure/modules/app.module.ts | 1 - src/infrastructure/modules/search.module.ts | 5 +- src/test/search.service.spec.ts | 103 +++++ 23 files changed, 1230 insertions(+), 212 deletions(-) create mode 100644 documentation/classes/EsQueryDto.html create mode 100644 src/core/domain/dtos/es-query.dto.ts create mode 100644 src/test/search.service.spec.ts diff --git a/documentation/classes/EsQueryDto.html b/documentation/classes/EsQueryDto.html new file mode 100644 index 0000000..1a26a78 --- /dev/null +++ b/documentation/classes/EsQueryDto.html @@ -0,0 +1,396 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+

+

File

+

+

+ src/core/domain/dtos/es-query.dto.ts +

+ + +

+

Description

+

+

+

Elasticsearch query DTO

+ +

+ + + + +
+

Index

+ + + + + + + + + + + + + + + +
+
Properties
+
+ +
+
+ + +
+ +

+ Properties +

+ + + + + + + + + + + + + + + + + +
+ + + pit + + +
+ Type : object + +
+ +
+

Object, that stores Point-In-Time ID and time alive

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + query + + +
+ Type : object + +
+ Decorators : +
+ + @IsDefined()
@IsObject()
@ApiProperty({description: 'Search query object passed to Elasticsearch', example: false})
+
+
+ +
+

The search query object passed to Elasticsearch

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + size + + +
+ Type : number + +
+ Decorators : +
+ + @IsDefined()
@IsNumber()
@IsInt()
@ApiProperty({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
+
+
+ +
+

Maximum number of elements returned by Elasticsearch

+
+
+ + + + + + + + + + + + + + + + + +
+ + + sort + + +
+ Type : object + +
+ +
+

Object, that stores

+
+
+
+ + + + + + + +
+ + +
+
import { ApiProperty } from "@nestjs/swagger";
+import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validator";
+
+/**
+ * List of allowed properties in this DTO
+ */
+ const allowedProperties = ['size', 'query', 'pit', 'sort'];
+
+ /**
+  * Elasticsearch query DTO
+  */
+ export class EsQueryDto {
+     /**
+      * Maximum number of elements returned by Elasticsearch
+      */
+     @IsDefined()
+     @IsNumber()
+     @IsInt()
+     @ApiProperty({
+         description: 'Maximum number of elements returned by Elasticsearch',
+         example: 30
+     })
+     size: number;
+     
+     /**
+      * The search query object passed to Elasticsearch
+      */
+     @IsDefined()
+     @IsObject()
+     @ApiProperty({
+         description: 'Search query object passed to Elasticsearch',
+         example: false,
+     })
+     query: object;
+
+     /**
+      * Object, that stores Point-In-Time ID and time alive
+      */
+     pit: object;
+
+     /**
+      * Object, that stores
+      */
+     sort: object;
+ }
+
+
+ + + + + + + + + +
+
+

results matching ""

+
    +
    +
    +

    No results matching ""

    +
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/SearchQueryDto.html b/documentation/classes/SearchQueryDto.html index 048aaa5..16bb309 100644 --- a/documentation/classes/SearchQueryDto.html +++ b/documentation/classes/SearchQueryDto.html @@ -115,6 +115,92 @@ +
    +

    Constructor

    + + + + + + + + + + + + + +
    +constructor(query: string, page: number, limit: number, order: string) +
    + +
    +

    Constructs an object with provided parameters

    +
    +
    + Parameters : + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeOptional
    query + string + + No +
    page + number + + No +
    limit + number + + No +
    order + string + + No +
    +
    +
    +
    @@ -356,6 +442,20 @@ export class SearchQueryDto { example: 'asc', }) order: string; + + /** + * Constructs an object with provided parameters + * @param query + * @param page + * @param limit + * @param order + */ + constructor(query: string, page: number, limit: number, order: string) { + this.query = query; + this.page = page; + this.limit = limit; + this.order = order; + } } diff --git a/documentation/classes/SearchResultDto.html b/documentation/classes/SearchResultDto.html index 37f35ca..6797141 100644 --- a/documentation/classes/SearchResultDto.html +++ b/documentation/classes/SearchResultDto.html @@ -94,7 +94,7 @@ data
  • - status + statusCode
  • @@ -222,10 +222,10 @@ - + - status - + statusCode + @@ -293,7 +293,7 @@ export class SearchResultDto { description: 'Status code', example: 200, }) - status: number; + statusCode: number; /** * All the data acquired. @@ -315,7 +315,7 @@ export class SearchResultDto { * @param data */ constructor(code: number, data: object) { - this.status = code; + this.statusCode = code; this.data = data; } } diff --git a/documentation/controllers/PapersController.html b/documentation/controllers/PapersController.html index e667872..ed5a6ff 100644 --- a/documentation/controllers/PapersController.html +++ b/documentation/controllers/PapersController.html @@ -141,8 +141,8 @@ - + @@ -210,14 +210,14 @@ Decorators :
    - @ApiOperation({summary: 'Finds paper by its UUID.'})
    @ApiResponse({status: 200, description: 'Returns back acquired paper.', type: SearchResultDto})
    @Get(':uuid')
    + @ApiOperation({summary: 'Finds paper by its UUID.'})
    @ApiResponse({status: 200, description: 'Returns back acquired paper.', type: SearchResultDto})
    @Get(':uuid')
    @UseInterceptors(PageInterceptor)
    @HttpCode(200)
    - + @@ -279,6 +279,7 @@ import { PageInterceptor } from "src/core/interceptors/page.interceptor&quo import { LoggerInterceptor } from "src/core/interceptors"; import { SearchResultDto } from "src/core/domain/dtos/search-result.dto"; import { ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { SearchQueryDto } from "src/core/domain/dtos"; /** * /papers/ route controller @@ -305,11 +306,10 @@ export class PapersController { getByContext(@Query() query): object { return this.searchService.findByContext(query.query).then( (response: SearchResultDto) => { - // console.log(JSON.stringify(response.data, null, 2)); return response.data; }, (error: SearchResultDto) => { - throw new HttpException(error.data, error.status); + throw new HttpException(error.data, error.statusCode); } ); } @@ -326,11 +326,12 @@ export class PapersController { description: 'Returns back acquired paper.', type: SearchResultDto, }) - @Get(':uuid') + @Get(':uuid') + @UseInterceptors(PageInterceptor) + @HttpCode(200) getByID(@Param('uuid', ParseUUIDPipe) uuid: string): object { return this.searchService.findByID(uuid).then( (response) => { - // console.log(JSON.stringify(response.data, null, 2)); return response.data; }, (error) => { diff --git a/documentation/coverage.html b/documentation/coverage.html index f168902..6e9bc69 100644 --- a/documentation/coverage.html +++ b/documentation/coverage.html @@ -140,6 +140,30 @@ (1/1) + + + + src/core/domain/dtos/es-query.dto.ts + + class + EsQueryDto + + 100 % + (5/5) + + + + + + src/core/domain/dtos/es-query.dto.ts + + variable + allowedProperties + + 100 % + (1/1) + + @@ -197,7 +221,7 @@ SearchQueryDto 100 % - (5/5) + (6/6) @@ -257,7 +281,7 @@ PageMeta 100 % - (6/6) + (7/7) @@ -425,7 +449,7 @@ SearchService 100 % - (5/5) + (7/7) diff --git a/documentation/graph/dependencies.svg b/documentation/graph/dependencies.svg index c2b88aa..7660ee6 100644 --- a/documentation/graph/dependencies.svg +++ b/documentation/graph/dependencies.svg @@ -4,217 +4,217 @@ - - + + dependencies - -dependencies - -cluster_LoggerModule - - - -cluster_LoggerModule_exports - - - -cluster_LoggerModule_providers - - - -cluster_CommonModule - - - -cluster_CommonModule_imports - - - -cluster_CommonModule_exports - - + +dependencies cluster_AppModule - + cluster_AppModule_imports - + cluster_HttpResponseModule - + cluster_HttpResponseModule_exports - + cluster_HttpResponseModule_providers - + + + +cluster_CommonModule + + + +cluster_CommonModule_imports + + + +cluster_CommonModule_exports + + + +cluster_LoggerModule + + + +cluster_LoggerModule_exports + + + +cluster_LoggerModule_providers + cluster_SearchModule - + cluster_SearchModule_exports - + cluster_SearchModule_providers - + CommonModule - -CommonModule + +CommonModule AppModule - -AppModule + +AppModule CommonModule->AppModule - - + + HttpResponseModule - -HttpResponseModule + +HttpResponseModule CommonModule->HttpResponseModule - - + + LoggerModule - -LoggerModule + +LoggerModule CommonModule->LoggerModule - - + + SearchModule - -SearchModule + +SearchModule SearchModule->AppModule - - + + SearchService - -SearchService + +SearchService SearchModule->SearchService - - + + HttpResponseModule - -HttpResponseModule + +HttpResponseModule HttpResponseModule->CommonModule - - + + HttpResponseService - -HttpResponseService + +HttpResponseService HttpResponseModule->HttpResponseService - - + + LoggerModule - -LoggerModule + +LoggerModule LoggerModule->CommonModule - - + + LoggerService - -LoggerService + +LoggerService LoggerModule->LoggerService - - + + HttpResponseService - -HttpResponseService + +HttpResponseService HttpResponseService->HttpResponseModule - - + + LoggerService - -LoggerService + +LoggerService LoggerService->LoggerModule - - + + SearchService - -SearchService + +SearchService SearchService->SearchModule - - + + diff --git a/documentation/injectables/PageInterceptor.html b/documentation/injectables/PageInterceptor.html index 7d68907..5120434 100644 --- a/documentation/injectables/PageInterceptor.html +++ b/documentation/injectables/PageInterceptor.html @@ -127,8 +127,8 @@
    -
    + @@ -180,7 +180,7 @@
    - Returns : Observable | Promise + Returns : Observable<PageDto<object>>
    @@ -202,6 +202,7 @@ import { MetadataScanner } from "@nestjs/core"; import { Observable, map } from "rxjs"; import { PageDto } from "../domain/dtos"; import { SearchQueryDto } from "../domain/dtos/search-q.dto"; +import { SearchResultDto } from "../domain/dtos/search-result.dto"; import { Order } from "../domain/enums/page-order.enum"; import { PageMeta } from "../domain/interfaces"; @@ -216,20 +217,19 @@ export class PageInterceptor implements NestInterceptor { * @param next * @returns Page with content and metadata */ - intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> { + intercept(context: ExecutionContext, next: CallHandler<any>): Observable<PageDto<object>> { const request = context.switchToHttp().getRequest(); const query: SearchQueryDto = request.query; return next.handle().pipe( map((res) => { - if (!res.hits) return res; - let meta: PageMeta = { - pagenum: +query?.page, + total: res.total.value, + pagenum: !query?.page ? 1 : query.page, order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC, hasNext: false, hasPrev: false, - pagesize: !query?.limit ? 1 : query.limit, + pagesize: !query?.limit ? 10 : query.limit, }; meta.hasNext = res.hits[meta.pagenum * meta.pagesize] ? true : false; diff --git a/documentation/injectables/SearchService.html b/documentation/injectables/SearchService.html index 5ee35c5..927d5f6 100644 --- a/documentation/injectables/SearchService.html +++ b/documentation/injectables/SearchService.html @@ -102,6 +102,10 @@ @@ -179,6 +187,85 @@ HTTPService instance

    Methods

    + + + + + + + + + + + + + + + + + + + +
    + + + Async + deletePIT + + +
    + + deletePIT(pitID: string) +
    + +
    +

    Deletes the PIT specified by provided ID

    +
    + +
    + Parameters : + + + + + + + + + + + + + + + + + + + + + +
    NameTypeOptionalDescription
    pitID + string + + No + +

    , ID of the PIT, that would be deleted

    + +
    +
    +
    +
    +
    + Returns : Promise<boolean> + +
    +
    +

    true/false, depending on the result of deletion of the PIT

    + +
    +
    @@ -201,8 +288,8 @@ HTTPService instance

    @@ -327,6 +414,89 @@ HTTPService instance

    - +
    + + + + + + + + + + + + + + + + + + + +
    + + + Async + getPIT + + +
    + + getPIT(alive: number) +
    + +
    +

    Acquires a PIT ID from Elasticsearch, needed for a request

    +
    + +
    + Parameters : + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeOptionalDefault valueDescription
    alive + number + + No + + 1 + +

    , amount of time in minutes (defaults to 1)

    + +
    +
    +
    +
    +
    + Returns : Promise<string> + +
    +
    +

    Point-In-Time ID

    + +
    +
    @@ -406,25 +576,21 @@ export class SearchService { query_string: { query: 'id:' + uuid } - } + }, } return new Promise((resolve, reject) => { try { - (this.httpService.get<EsResponseDto>('http://localhost:' + this.ES_PORT + '/_search', { + (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) .pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res: any) => { + .subscribe((res: EsResponseDto) => { if (res.timed_out) { reject(new SearchResultDto(504, {message: 'Timed Out'})); } - if (!res.hits.hits.length) { - reject(new SearchResultDto(404, {message: 'Not Found'})); - } - resolve(new SearchResultDto(200, res.hits)); }); } catch (error) { @@ -445,25 +611,23 @@ export class SearchService { query: query_str, default_field: "content" } - } + }, } + let pitID = this.getPIT(1); + return new Promise((resolve, reject) => { try { - (this.httpService.get<EsResponseDto>('http://localhost:'+ this.ES_PORT + '/_search', { + (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) .pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res: any) => { + .subscribe((res: EsResponseDto) => { if (res.timed_out) { reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'})); - } - - if (!res.hits.hits.length) { - reject(new SearchResultDto(404, {status: 404, message: 'Not Found'})); - } - + } + resolve(new SearchResultDto(200, res.hits)); }); } catch (error) { @@ -471,6 +635,47 @@ export class SearchService { } }); } + + /** + * Acquires a PIT ID from Elasticsearch, needed for a request + * @param alive, amount of time in minutes (defaults to 1) + * @returns Point-In-Time ID + */ + async getPIT(alive: number = 1): Promise<string> { + return new Promise((resolve, reject) => { + try { + (this.httpService.post(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive}m`) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.id); + })); + } catch (error) { + reject(error); + } + }); + } + + /** + * Deletes the PIT specified by provided ID + * @param pitID, ID of the PIT, that would be deleted + * @returns true/false, depending on the result of deletion of the PIT + */ + async deletePIT(pitID: string): Promise<boolean> { + return new Promise((resolve, reject) => { + try { + this.httpService.delete(`http://localhost:${this.ES_PORT}/papers/_pit`, { + data: { id: pitID }, + headers: { 'Content-Type': 'application/json' }, + }) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.succeeded); + }); + } catch (error) { + reject(error); + } + }) + } } diff --git a/documentation/interfaces/PageMeta.html b/documentation/interfaces/PageMeta.html index eeb55c2..3495c3d 100644 --- a/documentation/interfaces/PageMeta.html +++ b/documentation/interfaces/PageMeta.html @@ -121,6 +121,12 @@ pagesize +
  • + + total + +
  • @@ -322,6 +328,45 @@

    Number of elements on the page

    +
    + + + + + + + + + + + + + + + + + + + + + + + + @@ -338,6 +383,11 @@ * Structure of page metadata */ export interface PageMeta { + /** + * Total search results + */ + total: number; + /** * Number of the page */ diff --git a/documentation/js/menu-wc.js b/documentation/js/menu-wc.js index 6a8b651..62f04bc 100644 --- a/documentation/js/menu-wc.js +++ b/documentation/js/menu-wc.js @@ -62,20 +62,6 @@ customElements.define('compodoc-menu', class extends HTMLElement {
    + + total + + + + +
    + total: number + +
    + Type : number + +
    +

    Total search results

    +

    src/core/domain/dtos/es-query.dto.ts

    +
    +

    + + + + + + + + + + + + + + + + +
    + + + allowedProperties + + +
    + Type : [] + +
    + Default value : ['size', 'query', 'pit', 'sort'] +
    +

    List of allowed properties in this DTO

    +
    +
    +

    src/core/domain/dtos/es-response.dto.ts

    @@ -133,41 +171,6 @@
    -

    src/core/domain/dtos/search-q.dto.ts

    -
    -

    - - - - - - - - - - - - - - - - -
    - - - allowedProperties - - -
    - Type : [] - -
    - Default value : ['query', 'pagen', 'limit', 'order'] -
    -

    List of allowed properties in this DTO

    -
    -
    -

    src/core/domain/dtos/page.dto.ts

    @@ -203,6 +206,41 @@
    +

    src/core/domain/dtos/search-q.dto.ts

    +
    +

    + + + + + + + + + + + + + + + + +
    + + + allowedProperties + + +
    + Type : [] + +
    + Default value : ['query', 'pagen', 'limit', 'order'] +
    +

    List of allowed properties in this DTO

    +
    +
    +

    src/core/domain/dtos/search-result.dto.ts

    diff --git a/documentation/modules/AppModule.html b/documentation/modules/AppModule.html index 967cdbb..550a0c7 100644 --- a/documentation/modules/AppModule.html +++ b/documentation/modules/AppModule.html @@ -137,15 +137,6 @@
    -
    -

    Controllers

    - -

    Imports

    @@ -175,7 +166,6 @@ import { LoggerInterceptor } from '../../core/interceptors' import * as modules from '../../core/modules' import { CommonModule } from './common/common.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; -import { PapersController } from 'src/application/controller/papers.controller'; import { SearchModule } from './search.module'; /** @@ -211,7 +201,7 @@ const modulesList = Object.keys(modules).map(moduleIndex => modules useClass: LoggerInterceptor, }, ], - controllers: [PapersController], + controllers: [], }) export class AppModule {} diff --git a/documentation/modules/CommonModule/dependencies.svg b/documentation/modules/CommonModule/dependencies.svg index 17756f5..9675a86 100644 --- a/documentation/modules/CommonModule/dependencies.svg +++ b/documentation/modules/CommonModule/dependencies.svg @@ -14,14 +14,14 @@ cluster_CommonModule - -cluster_CommonModule_imports - - cluster_CommonModule_exports + +cluster_CommonModule_imports + + HttpResponseModule diff --git a/documentation/modules/SearchModule.html b/documentation/modules/SearchModule.html index ec446a6..a145e1b 100644 --- a/documentation/modules/SearchModule.html +++ b/documentation/modules/SearchModule.html @@ -150,6 +150,15 @@
    +
    +

    Controllers

    + +

    Exports

    @@ -170,16 +179,20 @@
    import { HttpModule } from "@nestjs/axios";
     import { Module } from "@nestjs/common";
     import { ConfigModule } from "@nestjs/config";
    +import { PapersController } from "src/application";
     import { SearchService } from "../../core/services/common/search.service";
    +import { configuration } from "../config";
     
     /**
      * search module
      */
     @Module({
    -    imports: [HttpModule],
    +    imports: [
    +        HttpModule,
    +    ],
         exports: [SearchService],
         providers: [SearchService],
    -    controllers: [],
    +    controllers: [PapersController],
     })
     export class SearchModule {}
    diff --git a/documentation/overview.html b/documentation/overview.html index 5fbdc93..75250dc 100644 --- a/documentation/overview.html +++ b/documentation/overview.html @@ -301,7 +301,7 @@

    -

    6 Classes

    +

    7 Classes

    diff --git a/src/core/domain/dtos/es-query.dto.ts b/src/core/domain/dtos/es-query.dto.ts new file mode 100644 index 0000000..6bac065 --- /dev/null +++ b/src/core/domain/dtos/es-query.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validator"; + +/** + * List of allowed properties in this DTO + */ + const allowedProperties = ['size', 'query', 'pit', 'sort']; + + /** + * Elasticsearch query DTO + */ + export class EsQueryDto { + /** + * Maximum number of elements returned by Elasticsearch + */ + @IsDefined() + @IsNumber() + @IsInt() + @ApiProperty({ + description: 'Maximum number of elements returned by Elasticsearch', + example: 30 + }) + size: number; + + /** + * The search query object passed to Elasticsearch + */ + @IsDefined() + @IsObject() + @ApiProperty({ + description: 'Search query object passed to Elasticsearch', + example: false, + }) + query: object; + + /** + * Object, that stores Point-In-Time ID and time alive + */ + pit: object; + + /** + * Object, that stores + */ + sort: object; + } \ No newline at end of file diff --git a/src/core/domain/dtos/search-q.dto.ts b/src/core/domain/dtos/search-q.dto.ts index 064e02c..28bf733 100644 --- a/src/core/domain/dtos/search-q.dto.ts +++ b/src/core/domain/dtos/search-q.dto.ts @@ -57,6 +57,13 @@ export class SearchQueryDto { }) order: string; + /** + * Constructs an object with provided parameters + * @param query + * @param page + * @param limit + * @param order + */ constructor(query: string, page: number, limit: number, order: string) { this.query = query; this.page = page; diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index 7b78c87..7339362 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -37,7 +37,7 @@ export class SearchService { return new Promise((resolve, reject) => { try { - (this.httpService.get('http://localhost:' + this.ES_PORT + '/_search', { + (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) @@ -70,9 +70,11 @@ export class SearchService { }, } + let pitID = this.getPIT(1); + return new Promise((resolve, reject) => { try { - (this.httpService.get('http://localhost:'+ this.ES_PORT + '/_search', { + (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) @@ -81,7 +83,7 @@ export class SearchService { if (res.timed_out) { reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'})); } - + resolve(new SearchResultDto(200, res.hits)); }); } catch (error) { @@ -89,4 +91,45 @@ export class SearchService { } }); } + + /** + * Acquires a PIT ID from Elasticsearch, needed for a request + * @param alive, amount of time in minutes (defaults to 1) + * @returns Point-In-Time ID + */ + async getPIT(alive: number = 1): Promise { + return new Promise((resolve, reject) => { + try { + (this.httpService.post(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive}m`) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.id); + })); + } catch (error) { + reject(error); + } + }); + } + + /** + * Deletes the PIT specified by provided ID + * @param pitID, ID of the PIT, that would be deleted + * @returns true/false, depending on the result of deletion of the PIT + */ + async deletePIT(pitID: string): Promise { + return new Promise((resolve, reject) => { + try { + this.httpService.delete(`http://localhost:${this.ES_PORT}/papers/_pit`, { + data: { id: pitID }, + headers: { 'Content-Type': 'application/json' }, + }) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.succeeded); + }); + } catch (error) { + reject(error); + } + }) + } } \ No newline at end of file diff --git a/src/infrastructure/modules/app.module.ts b/src/infrastructure/modules/app.module.ts index 931a437..308d14f 100644 --- a/src/infrastructure/modules/app.module.ts +++ b/src/infrastructure/modules/app.module.ts @@ -7,7 +7,6 @@ import { LoggerInterceptor } from '../../core/interceptors' import * as modules from '../../core/modules' import { CommonModule } from './common/common.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; -import { PapersController } from 'src/application/controller/papers.controller'; import { SearchModule } from './search.module'; /** diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index 4c2cc56..bccc4e6 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,6 +1,5 @@ import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; import { PapersController } from "src/application"; import { SearchService } from "../../core/services/common/search.service"; @@ -8,7 +7,9 @@ import { SearchService } from "../../core/services/common/search.service"; * search module */ @Module({ - imports: [HttpModule], + imports: [ + HttpModule, + ], exports: [SearchService], providers: [SearchService], controllers: [PapersController], diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts new file mode 100644 index 0000000..5c6406e --- /dev/null +++ b/src/test/search.service.spec.ts @@ -0,0 +1,103 @@ +import { HttpService } from "@nestjs/axios"; +import { ConfigModule } from "@nestjs/config"; +import { Test } from "@nestjs/testing"; +import { of } from "rxjs"; +import { HttpResponseException } from "src/core/exceptions"; +import { SearchService } from "src/core/services/common/search.service"; + +describe('Unit tests for SearchService', () => { + let searchService: SearchService; + let httpService: HttpService; + + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + SearchService, + { + provide: HttpService, + useValue: { + post: jest.fn(), + }, + }, + ], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + expandVariables: true, + }) + ], + }).compile(); + + searchService = moduleRef.get(SearchService); + httpService = moduleRef.get(HttpService); + }); + + describe('getPIT()', () => { + it('Should touch HttpService.post() method', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + searchService.getPIT(); + expect(postMock).toHaveBeenCalled(); + }); + + it('Should contain correct port in the URI from .env', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + searchService.getPIT(); + expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); + }); + + it('Should touch HttpService with correct URI when keep_alive is set as a parameter', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + let keep_alive = 2; + searchService.getPIT(keep_alive); + expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${keep_alive}m`); + }); + + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'post').mockImplementation(() => { + throw HttpResponseException; + }); + + expect(searchService.getPIT()).rejects.toEqual(HttpResponseException); + }); + + it('Should return a non-empty string when HttpService request succeedes', () => { + jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + expect(searchService.getPIT()).resolves.toBe('2567'); + }); + }); + + describe('deletePIT()', () => { + it.todo('Should fail to delete, because the requested PIT ID is invalid'); + it.todo('Should call HttpService.delete() method with correct body'); + }); +}); \ No newline at end of file