diff --git a/docker-compose.yaml b/docker-compose.yaml index 769d0f3..46a132b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,8 +3,6 @@ version: '3.3' services: elasticsearch: image: ${ES_IMAGE_NAME}:${ES_IMAGE_VERSION} - build: - context: . container_name: ${ES_CONTAINER_NAME} restart: always ports: @@ -12,6 +10,12 @@ services: environment: - xpack.security.enabled=false - discovery.type=single-node + + + + + + freeland: image: ${IMAGE_NAME}:${IMAGE_VERSION} build: @@ -19,6 +23,8 @@ services: dockerfile: Dockerfile container_name: ${CONTAINER_NAME} restart: always + links: + - "elasticsearch:localhost" ports: - "${LOCAL_PORT}:${NODE_PORT}" environment: diff --git a/documentation/classes/EnvironmentVariables.html b/documentation/classes/EnvironmentVariables.html index a89af6a..b5795c3 100644 --- a/documentation/classes/EnvironmentVariables.html +++ b/documentation/classes/EnvironmentVariables.html @@ -92,7 +92,7 @@
import { plainToClass } from 'class-transformer';
-import { validateSync, IsOptional } from 'class-validator';
+import { validateSync } from 'class-validator';
 
 /**
  * env vatiables
diff --git a/documentation/classes/EsHitDto.html b/documentation/classes/EsHitDto.html
index bc158e2..fa7978b 100644
--- a/documentation/classes/EsHitDto.html
+++ b/documentation/classes/EsHitDto.html
@@ -63,7 +63,7 @@
             

File

- src/core/domain/dtos/es-hit.dto.ts + src/core/domain/dtos/elastic/es-hit.dto.ts

@@ -143,13 +143,13 @@ Decorators :
- @IsOptional()
@ApiProperty({description: 'Relevance score', example: 1.2355})
+ @IsOptional()
@ApiPropertyOptional({description: 'Relevance score', example: 1.2355})
- + @@ -190,7 +190,7 @@ - + @@ -226,13 +226,13 @@ Decorators :
- @IsOptional()
@ApiProperty({description: 'List of objects that represents how the hit was sorted', example: undefined})
+ @IsOptional()
@ApiPropertyOptional({description: 'List of objects that represents how the hit was sorted', example: undefined})
- + @@ -257,9 +257,9 @@
-
import { ApiProperty } from "@nestjs/swagger";
-import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
-import { PaperDto } from "./paper.dto";
+        
import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { IsNotEmpty, IsOptional } from "class-validator";
+import { PaperDto } from "../paper.dto";
 
 /**
  * List of allowed properties in this DTO
@@ -269,6 +269,7 @@ const allowedProperties = ['sort', '_source', '_sc
 /**
  * Structure of the document stored and retrieved from Elasticsearch
  */
+@ApiExtraModels()
 export class EsHitDto {
     /**
      * Actual document stored in Elasticsearch
@@ -286,7 +287,7 @@ export class EsHitDto {
      * List of objects that represents how the hit was sorted
      */
     @IsOptional()
-    @ApiProperty({
+    @ApiPropertyOptional({
         description: 'List of objects that represents how the hit was sorted',
         example: {}
     })
@@ -296,7 +297,7 @@ export class EsHitDto {
      * Hit relevance score
      */
     @IsOptional()
-    @ApiProperty({
+    @ApiPropertyOptional({
         description: 'Relevance score',
         example: 1.2355
     })
diff --git a/documentation/classes/EsQueryDto.html b/documentation/classes/EsQueryDto.html
index 60b06b3..3064425 100644
--- a/documentation/classes/EsQueryDto.html
+++ b/documentation/classes/EsQueryDto.html
@@ -63,7 +63,7 @@
             

File

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

@@ -133,7 +133,7 @@ - + @@ -175,13 +175,13 @@ Decorators :
- @IsOptional()
@IsObject()
@ApiProperty({description: 'PIT object', example: undefined})
+ @IsOptional()
@IsObject()
@ApiPropertyOptional({description: 'PIT object', example: undefined})
- + @@ -222,7 +222,7 @@ - + @@ -258,13 +258,13 @@ Decorators :
- @IsOptional()
@IsArray()
@ApiProperty({description: '', example: undefined})
+ @IsOptional()
@IsArray()
@ApiPropertyOptional({description: '', example: undefined})
- + @@ -300,13 +300,13 @@ Decorators :
- @IsOptional()
@IsDefined()
@IsNumber()
@IsInt()
@ApiProperty({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
+ @IsOptional()
@IsDefined()
@IsNumber()
@IsInt()
@ApiPropertyOptional({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
- + @@ -342,13 +342,13 @@ Decorators :
- @IsOptional()
@IsArray()
@ApiProperty({description: '', example: undefined})
+ @IsOptional()
@IsArray()
@ApiPropertyOptional({description: '', example: undefined})
- + @@ -373,10 +373,10 @@
-
import { ApiProperty } from "@nestjs/swagger";
+        
import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
 import { IsArray, IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator";
-import { EsPit } from "../interfaces/es-pit.interface";
-import { EsQuery } from "../interfaces/es-query.interface"
+import { EsPit } from "../../interfaces/elastic/es-pit.interface";
+import { EsQuery } from "../../interfaces/elastic/es-query.interface"
 
 /**
  * List of allowed properties in this DTO
@@ -386,6 +386,7 @@ import { EsQuery } from "../interfaces/es-query.interface"
  /**
   * Elasticsearch query DTO
   */
+ @ApiExtraModels()
  export class EsQueryDto {
      /**
       * Maximum number of elements returned by Elasticsearch
@@ -394,7 +395,7 @@ import { EsQuery } from "../interfaces/es-query.interface"
      @IsDefined()
      @IsNumber()
      @IsInt()
-     @ApiProperty({
+     @ApiPropertyOptional({
          description: 'Maximum number of elements returned by Elasticsearch',
          example: 30
      })
@@ -416,7 +417,7 @@ import { EsQuery } from "../interfaces/es-query.interface"
       */
      @IsOptional()
      @IsObject()
-     @ApiProperty({
+     @ApiPropertyOptional({
         description: 'PIT object',
         example: {}
      })
@@ -427,7 +428,7 @@ import { EsQuery } from "../interfaces/es-query.interface"
       */
      @IsOptional()
      @IsArray()
-     @ApiProperty({
+     @ApiPropertyOptional({
         description: '',
         example: []
      })
@@ -438,7 +439,7 @@ import { EsQuery } from "../interfaces/es-query.interface"
       */
      @IsOptional()
      @IsArray()
-     @ApiProperty({
+     @ApiPropertyOptional({
         description: '',
         example: []
      })
diff --git a/documentation/classes/EsResponseDto.html b/documentation/classes/EsResponseDto.html
index b147225..200f1e3 100644
--- a/documentation/classes/EsResponseDto.html
+++ b/documentation/classes/EsResponseDto.html
@@ -63,7 +63,7 @@
             

File

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

@@ -147,13 +147,13 @@ Decorators :
- @IsOptional()
@IsObject()
@ApiProperty({description: '_shards', example: undefined})
+ @IsOptional()
@IsObject()
@ApiProperty({description: 'Contains a count of Elasticsearch shards used to process the request', example: undefined})
- + @@ -189,13 +189,13 @@ used for the request

Decorators :
- @IsOptional()
@IsObject()
@ApiProperty({description: 'hits', example: undefined})
+ @IsOptional()
@IsObject()
@ApiProperty({description: 'Contains returned documents and metadata', example: undefined})
- + @@ -231,13 +231,13 @@ used for the request

Decorators :
- @IsString()
@IsOptional()
@ApiProperty({description: 'PIT ID used to search for results', example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='})
+ @IsString()
@IsOptional()
@ApiPropertyOptional({description: 'Contains PIT ID used to search for results', example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='})
- + @@ -272,13 +272,13 @@ used for the request

Decorators :
- @IsDefined()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({description: 'timed_out', example: false})
+ @IsDefined()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({description: 'Shows if request timed out before completion', example: false})
- + @@ -314,13 +314,13 @@ If 'true' - the request timed out before completion

Decorators :
- @IsDefined()
@IsNotEmpty()
@IsNumber()
@ApiProperty({description: 'took', example: 5})
+ @IsDefined()
@IsNotEmpty()
@IsNumber()
@ApiProperty({description: 'The time that it took Elasticsearch to process the query', example: 5})
- + @@ -346,9 +346,9 @@ took Elasticsearch to execute the request

-
import { ApiProperty } from "@nestjs/swagger";
+        
import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
 import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator";
-import { EsResponseHits } from "../interfaces/es-response-hits.interface";
+import { EsResponseHits } from "../../interfaces/elastic/es-response-hits.interface";
 
 /**
  * List of allowed properties in this DTO
@@ -358,6 +358,7 @@ const allowedProperties = ['took', 'timed_out', '_
 /**
  * Elasticsearch response DTO
  */
+@ApiExtraModels()
 export class EsResponseDto {
     /**
      * Number of milliseconds it 
@@ -367,7 +368,7 @@ export class EsResponseDto {
     @IsNotEmpty()
     @IsNumber()
     @ApiProperty({
-        description: 'took',
+        description: 'The time that it took Elasticsearch to process the query',
         example: 5
     })
     took: number;
@@ -380,7 +381,7 @@ export class EsResponseDto {
     @IsNotEmpty()
     @IsBoolean()
     @ApiProperty({
-        description: 'timed_out',
+        description: 'Shows if request timed out before completion',
         example: false,
     })
     timed_out: boolean;
@@ -392,7 +393,7 @@ export class EsResponseDto {
     @IsOptional()
     @IsObject()
     @ApiProperty({
-        description: '_shards',
+        description: 'Contains a count of Elasticsearch shards used to process the request',
         example: {
             total: 1,
             successful: 1,
@@ -408,7 +409,7 @@ export class EsResponseDto {
     @IsOptional()
     @IsObject()
     @ApiProperty({
-        description: 'hits',
+        description: 'Contains returned documents and metadata',
         example: {
             total: {
                 value: 3,
@@ -419,12 +420,8 @@ export class EsResponseDto {
                 _index: 'papers',
                 _id: '01002',
                 _score: 1.2,
-                _source: {
-
-                },
-                fields: {
-
-                }
+                _source: {},
+                fields: {}
             }],
         }
     })
@@ -435,8 +432,8 @@ export class EsResponseDto {
      */
     @IsString()
     @IsOptional()
-    @ApiProperty({
-        description: 'PIT ID used to search for results',
+    @ApiPropertyOptional({
+        description: 'Contains PIT ID used to search for results',
         example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='
     })
     pit_id?: string;
diff --git a/documentation/classes/PageDto.html b/documentation/classes/PageDto.html
index 48a5b98..18a1357 100644
--- a/documentation/classes/PageDto.html
+++ b/documentation/classes/PageDto.html
@@ -122,7 +122,7 @@
                 
                         
                             
-                                
+                                
                             
                         
 
@@ -202,13 +202,13 @@
                         Decorators : 
                         
- @IsArray()
@ApiProperty({description: 'All data the page contains', isArray: true})
+ @IsArray()
@ApiProperty({description: 'All data (papers) the page contains', isArray: true, type: PaperDto})
- + @@ -235,7 +235,7 @@ - Type : PageMeta + Type : PageMetaDto @@ -250,7 +250,7 @@ - + @@ -275,9 +275,11 @@
-
import { ApiProperty } from "@nestjs/swagger";
+        
import { ApiExtraModels, ApiProperty, PartialType } from "@nestjs/swagger";
 import { IsArray } from "class-validator";
+import { Order } from "../enums";
 import { PageMeta } from "../interfaces/page-meta.interface";
+import { PageMetaDto } from "./page-meta.dto";
 import { PaperDto } from "./paper.dto";
 
 /**
@@ -288,14 +290,16 @@ const allowedProperties = ['data', 'meta'];
 /**
  * Page model for pagination
  */
+@ApiExtraModels()
 export class PageDto {
     /**
      * Data block of the page
      */
     @IsArray()
     @ApiProperty({
-        description: 'All data the page contains',
+        description: 'All data (papers) the page contains',
         isArray: true,
+        type: PaperDto
     })
     readonly data: PaperDto[];
 
@@ -304,9 +308,10 @@ export class PageDto {
      */
     @ApiProperty({
         description: 'Metadata for the page',
-        // example: [],
+        // example: {},
+        
     })
-    readonly meta: PageMeta;
+    readonly meta: PageMetaDto;
 
     /**
      * Constructs an object with provided parameters
diff --git a/documentation/classes/PageMetaDto.html b/documentation/classes/PageMetaDto.html
new file mode 100644
index 0000000..17aa765
--- /dev/null
+++ b/documentation/classes/PageMetaDto.html
@@ -0,0 +1,536 @@
+
+
+    
+        
+        
+        hometask documentation
+        
+        
+
+        
+	   
+        
+        
+    
+    
+
+        
+
+        
+
+        
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+

+

File

+

+

+ src/core/domain/dtos/page-meta.dto.ts +

+ + +

+

Description

+

+

+

Page model for pagination

+ +

+ + +

+

Implements

+

+

+ PageMeta +

+ + +
+

Index

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

+ Properties +

+ + + + + + + + + + + + + + + + + + + + +
+ + + hasNext + + +
+ Type : boolean + +
+ Decorators : +
+ + @ApiProperty({description: 'Flag, that shows if there's a page following the current one', example: true})
+
+
+ +
+

Flag, that shows if there's a page following the current one

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + hasPrev + + +
+ Type : boolean + +
+ Decorators : +
+ + @ApiProperty({description: 'Flag, that shows if there's a page preceding the current one', example: true})
+
+
+ +
+

Flag, that shows if there's a page preceding the current one

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + order + + +
+ Type : Order + +
+ Decorators : +
+ + @ApiProperty({description: 'Order of the elements on the page', example: undefined})
+
+
+ +
+

Order of the elements on the page

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + pagenum + + +
+ Type : number + +
+ Decorators : +
+ + @ApiProperty({description: 'Current page number', minimum: 1, example: 3})
+
+
+ +
+

Current page number

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + pagesize + + +
+ Type : number + +
+ Decorators : +
+ + @ApiProperty({description: 'Maximum number of elements on the page', minimum: 1, example: 20})
+
+
+ +
+

Maximum number of elements on the page

+
+
+ + + + + + + + + + + + + + + + + + + + +
+ + + total + + +
+ Type : number + +
+ Decorators : +
+ + @IsArray()
@ApiProperty({description: 'Total number of hits (results) acquired from the search', example: 314})
+
+
+ +
+

Total number of hits (results) acquired from the search

+
+
+
+ + + + + + + +
+ + +
+
import { ApiExtraModels, ApiProperty, PartialType } from "@nestjs/swagger";
+import { IsArray } from "class-validator";
+import { Order } from "../enums";
+import { PageMeta } from "../interfaces/page-meta.interface";
+import { PaperDto } from "./paper.dto";
+
+/**
+ * List of allowed properties in this DTO
+ */
+const allowedProperties = ['total', 'pagenum', 'order', 'hasNext', 'hasPrev', 'pagesize'];
+
+/**
+ * Page model for pagination
+ */
+@ApiExtraModels()
+export class PageMetaDto implements PageMeta {
+    /**
+     * Total number of hits (results) acquired from the search
+     */
+    @IsArray()
+    @ApiProperty({
+        description: 'Total number of hits (results) acquired from the search',
+        example: 314
+    })
+    total: number;
+
+    /**
+     * Current page number
+     */
+    @ApiProperty({
+        description: 'Current page number',
+        minimum: 1,
+        example: 3
+    })
+    pagenum: number;
+
+    /**
+     * Order of the elements on the page
+     */
+    @ApiProperty({
+        description: 'Order of the elements on the page',
+        example: Order.DESC
+    })
+    order: Order;
+
+    /**
+     * Flag, that shows if there's a page following the current one
+     */
+    @ApiProperty({
+        description: 'Flag, that shows if there\'s a page following the current one',
+        example: true
+    })
+    hasNext: boolean;
+
+    /**
+     * Flag, that shows if there's a page preceding the current one
+     */
+    @ApiProperty({
+        description: 'Flag, that shows if there\'s a page preceding the current one',
+        example: true
+    })
+    hasPrev: boolean;
+
+    /**
+     * Maximum number of elements on the page
+     */
+    @ApiProperty({
+        description: 'Maximum number of elements on the page',
+        minimum: 1,
+        example: 20
+    })
+    pagesize: number;
+}
+
+
+ + + + + + + + + +
+
+

results matching ""

+
    +
    +
    +

    No results matching ""

    +
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/PaperDto.html b/documentation/classes/PaperDto.html index 0cde5ce..c0b6e85 100644 --- a/documentation/classes/PaperDto.html +++ b/documentation/classes/PaperDto.html @@ -158,7 +158,7 @@ - + @@ -199,7 +199,7 @@ - + @@ -240,7 +240,7 @@ - + @@ -281,7 +281,7 @@ - + @@ -322,7 +322,7 @@ - + @@ -363,7 +363,7 @@ - + @@ -404,7 +404,7 @@ - + @@ -429,10 +429,8 @@
    -
    import { ApiProperty } from "@nestjs/swagger";
    +        
    import { ApiExtraModels, ApiProperty } from "@nestjs/swagger";
     import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
    -import { EsQueryDto } from "./es-query.dto";
    -import { SearchQueryDto } from "./search-q.dto";
     
     /**
      * List of allowed properties in this DTO
    @@ -442,6 +440,7 @@ const allowedProperties = ['id', 'title', 'authors
     /**
      * Structure of the document stored and retrieved from Elasticsearch
      */
    +@ApiExtraModels()
     export class PaperDto {
         /**
          * Unique ID of the paper
    diff --git a/documentation/classes/PrevSearch.html b/documentation/classes/PrevSearch.html
    index 13b2553..c87872c 100644
    --- a/documentation/classes/PrevSearch.html
    +++ b/documentation/classes/PrevSearch.html
    @@ -76,12 +76,6 @@
                 

    -

    -

    Implements

    -

    -

    - SearchInfo -

    @@ -97,12 +91,15 @@ @@ -129,6 +126,26 @@ + + +
    Accessors
    + + + + + + +
    @@ -144,7 +161,7 @@ - + @@ -169,6 +186,7 @@ + Private pit @@ -182,7 +200,7 @@ - + @@ -201,6 +219,7 @@ + Private prevPage @@ -214,7 +233,7 @@ - + @@ -233,6 +252,7 @@ + Private tiebreaker @@ -246,7 +266,7 @@ - + @@ -288,8 +308,8 @@ - + @@ -319,30 +339,238 @@ +
    +

    + Accessors +

    + + + + + + + + + + + + + + + + + + + + + + + +
    + + _pit +
    + get_pit() +
    + +
    + set_pit(pit: EsPit) +
    + +
    + +
    + Parameters : + + + + + + + + + + + + + + + + + + +
    NameTypeOptional
    pit + EsPit + + No +
    +
    +
    +
    +
    + Returns : void + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + _tiebreaker +
    + get_tiebreaker() +
    + +
    + set_tiebreaker(tiebreaker: []) +
    + +
    + +
    + Parameters : + + + + + + + + + + + + + + + + + + +
    NameTypeOptional
    tiebreaker + [] + + No +
    +
    +
    +
    +
    + Returns : void + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    + + _prevPage +
    + get_prevPage() +
    + +
    + set_prevPage(page: number) +
    + +
    + +
    + Parameters : + + + + + + + + + + + + + + + + + + +
    NameTypeOptional
    page + number + + No +
    +
    +
    +
    +
    + Returns : void + +
    +
    +
    import { HttpService } from "@nestjs/axios";
     import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
    -import { reverse } from "dns";
     import { Observable, map, take } from "rxjs";
    -import { EsResponseDto, PageDto } from "../domain/dtos";
    -import { EsQueryDto } from "../domain/dtos/es-query.dto";
    +import { PageDto } from "../domain/dtos";
    +import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto";
     import { RequestDto } from "../domain/dtos/request.dto";
     import { SearchQueryDto } from "../domain/dtos/search-q.dto";
    -import { SearchResultDto } from "../domain/dtos/search-result.dto";
     import { EsTime } from "../domain/enums/es-time.enum";
     import { Order } from "../domain/enums/page-order.enum";
     import { PageMeta } from "../domain/interfaces";
    -import { EsPit } from "../domain/interfaces/es-pit.interface";
    -import { SearchInfo } from "../domain/interfaces/search-info.interface";
    -import { SearchService } from "../services/common/search.service";
    +import { EsPit } from "../domain/interfaces/elastic/es-pit.interface";
     
     /**
      * Previous search data storage
      */
    -class PrevSearch implements SearchInfo {
    +class PrevSearch {
         /**
          * Constructs an uninitialized object
          */
    @@ -355,17 +583,35 @@ class PrevSearch implements SearchInfo {
         /**
          * PIT object of the previous search
          */
    -    pit: EsPit;
    +    private pit: EsPit;
    +    set _pit(pit: EsPit) {
    +        this.pit = pit;
    +    }
    +    get _pit(): EsPit {
    +        return this.pit;
    +    }
     
         /**
          * Tiebreaker and sort parameters
          */
    -    tiebreaker: unknown[];
    +    private tiebreaker: unknown[];
    +    set _tiebreaker(tiebreaker: unknown[]) {
    +        this.tiebreaker = tiebreaker;
    +    }
    +    get _tiebreaker(): unknown[] {
    +        return this.tiebreaker;
    +    }
     
         /**
          * Number of the previous page
          */
    -    prevPage: number;
    +    private prevPage: number;
    +    set _prevPage(page: number) {
    +        this.prevPage = page;
    +    }
    +    get _prevPage(): number {
    +        return this.prevPage;
    +    }
     
         /**
          * Checks if there was the search before current one
    @@ -416,22 +662,26 @@ export class PageInterceptor implements NestInterceptor {
             ];
     
             if (this.prevSearch.isSet()) {
    -            request.es_query.pit = this.prevSearch.pit;
    -            request.es_query.search_after = this.prevSearch.tiebreaker;
    +            request.es_query.pit = this.prevSearch._pit;
    +            request.es_query.search_after = this.prevSearch._tiebreaker;
     
                 let limit = !query?.limit ? 10 : query.limit;
    -            request.es_query.size = limit * Math.abs(query.page - this.prevSearch.prevPage);
    +            request.es_query.size = limit * Math.abs(query.page - this.prevSearch._prevPage);
                 
    -            if (query.page < this.prevSearch.prevPage) {
    +            if (query.page < this.prevSearch._prevPage) {
                     request.es_query.sort = [{ _score: { order: 'asc' } }];
                     request.es_query.size += limit - 1;
                     reverse = true;
    -            } else if (query.page == this.prevSearch.prevPage) {
    -                //...
    +            } else if (query.page == this.prevSearch._prevPage) {
    +                // Caching should be HERE
    +                request.es_query.sort = [{ _score: { order: 'asc' } }];
    +                reverse = true;
                 }
             } else {
    -            this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
    -            request.es_query.size = !query?.limit ? 10 : query.limit;
    +            this.prevSearch._pit = request.es_query.pit = await this.getPIT(1);
    +
    +            let limit = !query?.limit ? 10 : query.limit;
    +            request.es_query.size = limit * query.page;
             }
     
             return next.handle().pipe(
    @@ -439,28 +689,31 @@ export class PageInterceptor implements NestInterceptor {
                     // Setting the page meta-data
                     let meta: PageMeta = {
                         total: res.hits.total.value,
    -                    pagenum: !query?.page ? 1 : query.page,
    +                    pagenum: !query?.page ? 1 : +query.page,
                         order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC,
    -                    hasNext: false,
    -                    hasPrev: false,
                         pagesize: !query?.limit ? 10 : query.limit,
    +                    hasNext: undefined,
    +                    hasPrev: undefined,
                     }; 
    -                // meta.hasNext = res.hits.hits[meta.pagenum * meta.pagesize] ? true : false;
    -                // meta.hasPrev = res.hits.hits[(meta.pagenum - 1) * meta.pagesize - 1] ? true: false;
    +                meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false;
    +                meta.hasPrev = meta.pagenum != 1 ? true : false;
     
                     // Saving the search info
    -                this.prevSearch.pit.id = res.pit_id;
    -                this.prevSearch.tiebreaker = res.hits.hits[res.hits.hits.length - 1].sort;
    -                this.prevSearch.prevPage = query.page;
    +                this.prevSearch._pit.id = res.pit_id;
    +                this.prevSearch._tiebreaker = res.hits.hits[res.hits.hits.length - 1]?.sort;
    +                this.prevSearch._prevPage = query.page;
     
    +                // Check if the performed search is a backwards search
                     let data = res.hits.hits.slice(-meta.pagesize);
                     if (reverse) {
    -                    console.log('REVERSE');
    -                    this.prevSearch.tiebreaker = data[0].sort;
    +                    this.prevSearch._tiebreaker = data[0]?.sort;
                         data.reverse();
                         reverse = false;
                     }
     
    +                // Omitting the redundant info and leaving only the document
    +                data = data.map((el) => el._source);
    +
                     // Return the page
                     return new PageDto(data, meta);
                 })
    @@ -472,6 +725,11 @@ export class PageInterceptor implements NestInterceptor {
          */
         private readonly ES_PORT = process.env.ES_PORT;
     
    +    /**
    +     * Elastichsearch IP address
    +     */
    +     private readonly ES_IP = process.env.ES_CONTAINER_NAME;
    +
         /**
          * Info about previously completed search
          */
    @@ -485,12 +743,12 @@ export class PageInterceptor implements NestInterceptor {
          public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise<EsPit> {
             return new Promise((resolve, reject) => {
                 try {
    -                (this.httpService.post<EsPit>(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`)
    +                this.httpService.post<EsPit>(`http://${this.ES_IP}:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`)
                         .pipe(take(1), map(axiosRes => axiosRes.data))
    -                    .subscribe((res) => {
    +                    .subscribe((res: EsPit) => {
                             res.keep_alive = alive + unit;
                             resolve(res);
    -                    }));
    +                    });
                 } catch (error) {
                     reject(error);
                 }
    @@ -505,7 +763,7 @@ export class PageInterceptor implements NestInterceptor {
          async deletePIT(pitID: string): Promise<boolean> {
             return new Promise((resolve, reject) => {
                 try {
    -                this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, {
    +                this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, {
                         data: { id: pitID },
                         headers: { 'Content-Type': 'application/json' },
                     })
    @@ -518,72 +776,7 @@ export class PageInterceptor implements NestInterceptor {
                 }
             })
         }
    -}
    -/*
    -public saveInfo(pit: EsPit, tiebreaker: unknown[], page: number) {
    -        this.pit.id = pit.id;
    -        this.pit.keep_alive = pit.keep_alive;
    -
    -        this.tiebreaker = tiebreaker.slice();
    -
    -        this.prevPage = page;
    -    }
    -
    -    public clearInfo() {
    -        this.pit = undefined;
    -        this.tiebreaker = undefined;
    -        this.prevPage = -1;
    -    }*/
    -
    -    // getQueryParams(str: string): any {
    -    //     let parameters: object = {};
    -    //     let pairs: string[] = str.split(',');
    -    //     parameters['main'] = pairs[0];
    -    //     pairs.shift();
    -
    -    //     if(!pairs || pairs[0] === '') return parameters;
    -
    -    //     for (const pair of pairs) {
    -    //         const key: string = pair.substring(0, pair.indexOf('='));
    -    //         const value: string = pair.substring(pair.indexOf('=') + 1);
    -    //         parameters[key] = value;
    -    //     }
    -
    -    //     return parameters;
    -    // }
    -
    -
    -    /**
    -     * OLD WAY PAGINATION
    -     *                 // Setting the page data
    -                // const data = res.hits.slice((meta.pagenum - 1) * meta.pagesize, meta.pagenum * meta.pagesize);
    -     */
    -
    -
    -        // if (query.page == 1) {
    -        //     this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
    -        // } else {
    -        //     if (!this.prevSearch.isSet()) {
    -        //         this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
    -
    -        //         request.es_query.size = query.limit * (query.page - 1);
    -        //         this.searchService.findByContext(request.es_query).then((res: SearchResultDto) => {
    -        //             request.es_query.search_after = res.data.hits.hits[res.data.hits.hits.length - 1].sort;
    -        //         });
    -        //     } else {
    -        //         if (query.page == this.prevSearch.prevPage) {
    -        //             return;
    -        //         } else {
    -        //             request.es_query.pit = this.prevSearch.pit;
    -        //             request.es_query.search_after = this.prevSearch.tiebreaker;
    -        //             request.es_query.size = (query.page - this.prevSearch.prevPage);
    -        //         }
    -
    -        //         // request.es_query.pit = this.prevSearch.pit;
    -        //         // request.es_query.search_after = this.prevSearch.tiebreaker;
    -        //     }
    -        // }
    -
    +}
    diff --git a/documentation/classes/RequestDto.html b/documentation/classes/RequestDto.html index 0ea9fc6..15d9429 100644 --- a/documentation/classes/RequestDto.html +++ b/documentation/classes/RequestDto.html @@ -121,7 +121,7 @@ - + @@ -201,13 +201,13 @@ Decorators :
    - @IsOptional()
    @ApiProperty({description: '', example: undefined})
    + @IsOptional()
    @ApiPropertyOptional({type: EsQueryDto, description: 'Elasticsearch query body constructed by pagination mechanism', example: undefined})
    - + @@ -242,13 +242,13 @@ Decorators :
    - @IsDefined()
    @IsNotEmpty()
    @ApiProperty({description: '', example: undefined})
    + @IsDefined()
    @IsNotEmpty()
    @ApiProperty({type: SearchQueryDto, description: 'Actual query with parameters acquired from the request', example: undefined})
    - + @@ -273,9 +273,9 @@
    -
    import { ApiProperty } from "@nestjs/swagger";
    -import { IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
    -import { EsQueryDto } from "./es-query.dto";
    +        
    import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
    +import { IsDefined, IsNotEmpty, IsOptional } from "class-validator";
    +import { EsQueryDto } from "./elastic/es-query.dto";
     import { SearchQueryDto } from "./search-q.dto";
     
     /**
    @@ -286,6 +286,7 @@ const allowedProperties = ['query', 'es_query'];
     /**
      * Request object, which contains query parameters and Elasticsearch query object
      */
    +@ApiExtraModels()
     export class RequestDto {
         /**
          * Query parameters object
    @@ -293,7 +294,8 @@ export class RequestDto {
         @IsDefined()
         @IsNotEmpty()
         @ApiProperty({
    -        description: '',
    +        type: SearchQueryDto,
    +        description: 'Actual query with parameters acquired from the request',
             example: {}
         })
         query: SearchQueryDto;
    @@ -302,8 +304,9 @@ export class RequestDto {
          * Elasticsearch query object
          */
         @IsOptional()
    -    @ApiProperty({
    -        description: '',
    +    @ApiPropertyOptional({
    +        type: EsQueryDto,
    +        description: 'Elasticsearch query body constructed by pagination mechanism',
             example: {},
         })
         es_query?: EsQueryDto;
    @@ -313,10 +316,10 @@ export class RequestDto {
           * @param query
           * @param es_query
           */
    -     constructor(query: SearchQueryDto, es_query: EsQueryDto) {
    +    constructor(query: SearchQueryDto, es_query: EsQueryDto) {
             this.query = query;
             this.es_query = es_query;
    -     }
    +    }
     }
    diff --git a/documentation/classes/SearchQueryDto.html b/documentation/classes/SearchQueryDto.html index 16bb309..e30b687 100644 --- a/documentation/classes/SearchQueryDto.html +++ b/documentation/classes/SearchQueryDto.html @@ -126,7 +126,7 @@ - + @@ -235,7 +235,7 @@ - + @@ -276,7 +276,7 @@ - + @@ -317,7 +317,7 @@ - + @@ -358,7 +358,7 @@ - + @@ -384,8 +384,8 @@ search on.

    -
    import { ApiProperty } from "@nestjs/swagger";
    -import { IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
    +        
    import { ApiExtraModels, ApiProperty } from "@nestjs/swagger";
    +import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
     
     /**
      * List of allowed properties in this DTO
    @@ -395,6 +395,7 @@ const allowedProperties = ['query', 'pagen', 'limi
     /**
      * Elasticsearch response DTO
      */
    +@ApiExtraModels()
     export class SearchQueryDto {
         /**
          * Given query string to perform the
    diff --git a/documentation/classes/SearchResultDto.html b/documentation/classes/SearchResultDto.html
    index 7212a1c..84dcf2c 100644
    --- a/documentation/classes/SearchResultDto.html
    +++ b/documentation/classes/SearchResultDto.html
    @@ -120,7 +120,7 @@
                     
                             
                                 
    -                                
    +                                
                                 
                             
     
    @@ -205,7 +205,7 @@
                     
                         
                             
    -                                
    +                                
                             
                         
     
    @@ -246,7 +246,7 @@
                     
                         
                             
    -                                
    +                                
                             
                         
     
    @@ -271,9 +271,9 @@
     
     
         
    -
    import { ApiProperty } from "@nestjs/swagger";
    -import { IsArray, IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
    -import { EsResponseDto } from "./es-response.dto";
    +        
    import { ApiExtraModels, ApiProperty } from "@nestjs/swagger";
    +import { IsArray, IsDefined, IsInt, IsNotEmpty } from "class-validator";
    +import { EsResponseDto } from "./elastic/es-response.dto";
     
     /**
      * List of allowed properties in this DTO
    @@ -283,6 +283,7 @@ const allowedProperties = ['data', 'status'];
     /**
      * Elasticsearch response DTO
      */
    +@ApiExtraModels()
     export class SearchResultDto {
         /**
          * Status code
    @@ -305,7 +306,10 @@ export class SearchResultDto {
         @ApiProperty({
             description: 'Data acquired from the Elasticsearch',
             example: {
    -            
    +            took: 1,
    +            timed_out: false,
    +            _shards: {},
    +            hits: {}
             },
         })
         data: EsResponseDto;
    diff --git a/documentation/controllers/PapersController.html b/documentation/controllers/PapersController.html
    index 0657b0d..4de01ed 100644
    --- a/documentation/controllers/PapersController.html
    +++ b/documentation/controllers/PapersController.html
    @@ -59,12 +59,6 @@
                 src/application/controller/papers.controller.ts
             

    -

    -

    Prefix

    -

    -

    - papers -

    @@ -127,7 +121,7 @@ -getByContext(query: RequestDto) +getByContext(request: RequestDto) @@ -135,14 +129,14 @@ Decorators :
    - @ApiOperation({summary: 'Finds papers by context based on the query.'})
    @ApiResponse({status: 200, description: 'Returns back acquired papers.', type: SearchResultDto})
    @Get('search')
    @UseInterceptors(PageInterceptor)
    @HttpCode(200)
    + @ApiTags('Search')
    @ApiOperation({summary: 'Finds papers by context based on the query'})
    @ApiResponse({status: 200, description: 'Returns back a page with acquired papers', type: PageDto})
    @ApiGatewayTimeoutResponse({description: 'Elasticsearch request timed out'})
    @Get('search')
    @UseInterceptors(PageInterceptor)
    @HttpCode(200)
    -

    + @@ -165,7 +159,7 @@ - query + request RequestDto @@ -182,7 +176,7 @@
    - Returns : object + Returns : Promise<EsResponseDto>
    @@ -214,14 +208,14 @@ Decorators :
    - @ApiOperation({summary: 'Finds paper by its UUID.'})
    @ApiResponse({status: 200, description: 'Returns back acquired paper.', type: SearchResultDto})
    @Get(':uuid')
    @UseInterceptors(PageInterceptor)
    @HttpCode(200)
    + @ApiTags('Search')
    @ApiOperation({summary: 'Finds paper by its UUID', tags: undefined})
    @ApiResponse({status: 200, description: 'Returns back a paper', type: PaperDto})
    @ApiGatewayTimeoutResponse({description: 'Elasticsearch request timed out'})
    @Get(':uuid')
    @HttpCode(200)
    - + @@ -261,7 +255,7 @@
    - Returns : object + Returns : Promise<PaperDto>
    @@ -276,17 +270,21 @@
    -
    import { Controller, Get, HttpCode, HttpException, Next, Param, ParseUUIDPipe, Put, Query, Req, Res, UseInterceptors } from "@nestjs/common";
    +        
    import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common";
     import { SearchService } from "../../core/services/common/search.service";
    -import { PageInterceptor } from "src/core/interceptors/page.interceptor";
    -import { SearchResultDto } from "src/core/domain/dtos/search-result.dto";
    -import { ApiOperation, ApiResponse } from "@nestjs/swagger";
    -import { RequestDto } from "src/core/domain/dtos/request.dto";
    +import { PageInterceptor } from "../../core/interceptors/page.interceptor";
    +import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from "@nestjs/swagger";
    +import { RequestDto } from "../../core/domain/dtos/request.dto";
    +import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain";
     
     /**
      * /papers/ route controller
      */
    -@Controller('papers')
    +@Controller({
    +    version: '1',
    +    path: 'papers',
    +})
    +@ApiExtraModels(RequestDto, EsHitDto, EsResponseDto)
     export class PapersController {
         constructor(private searchService: SearchService) {}
     
    @@ -296,22 +294,28 @@ export class PapersController {
          * @param response 
          * @returns a response with a set of matching papers
          */
    -     @ApiOperation({ summary: 'Finds papers by context based on the query.' })
    -     @ApiResponse({
    -       status: 200,
    -       description: 'Returns back acquired papers.',
    -       type: SearchResultDto,
    -     })
    +    @ApiTags('Search')
    +    @ApiOperation({ 
    +        summary: 'Finds papers by context based on the query',
    +    })
    +    @ApiResponse({
    +        status: 200,
    +        description: 'Returns back a page with acquired papers',
    +        type: PageDto
    +    })
    +    @ApiGatewayTimeoutResponse({
    +        description: 'Elasticsearch request timed out'
    +    })
         @Get('search')
         @UseInterceptors(PageInterceptor)
         @HttpCode(200)
    -    getByContext(@Req() query: RequestDto): object {
    -        return this.searchService.findByContext(query.es_query).then(
    -            (response: SearchResultDto) => {
    -                return response.data;
    +    getByContext(@Req() request: RequestDto): Promise<EsResponseDto> {
    +        return this.searchService.findByContext(request.es_query).then(
    +            (response) => {
    +                return response;
                 },
    -            (error: SearchResultDto) => {
    -                throw new HttpException(error.data, error.statusCode);
    +            (error) => {
    +                throw error;
                 }
             );
         }
    @@ -322,22 +326,28 @@ export class PapersController {
          * @param response 
          * @returns a response with a requested object
          */
    -     @ApiOperation({ summary: 'Finds paper by its UUID.' })
    -     @ApiResponse({
    -       status: 200,
    -       description: 'Returns back acquired paper.',
    -       type: SearchResultDto,
    -     })
    +    @ApiTags('Search')
    +    @ApiOperation({ 
    +        summary: 'Finds paper by its UUID',
    +        tags: ['Search']
    +    })
    +    @ApiResponse({
    +        status: 200,
    +        description: 'Returns back a paper',
    +        type: PaperDto
    +    })
    +    @ApiGatewayTimeoutResponse({
    +        description: 'Elasticsearch request timed out'
    +    })
         @Get(':uuid')
    -    @UseInterceptors(PageInterceptor)
         @HttpCode(200)
    -    getByID(@Param('uuid', ParseUUIDPipe) uuid: string): object {
    +    getByID(@Param('uuid', ParseUUIDPipe) uuid: string): Promise<PaperDto> {
             return this.searchService.findByID(uuid).then(
    -            (response) => {
    -                return response.data;
    +            (response: EsResponseDto) => {
    +                return response.hits.hits[0]._source;
                 },
                 (error) => {
    -                throw new HttpException(error.data, error.status);
    +                throw error;
                 }
             );
         }
    diff --git a/documentation/coverage.html b/documentation/coverage.html
    index c1e0b91..b75c0b0 100644
    --- a/documentation/coverage.html
    +++ b/documentation/coverage.html
    @@ -143,7 +143,7 @@
             
                 
                     
    -                src/core/domain/dtos/es-hit.dto.ts
    +                src/core/domain/dtos/elastic/es-hit.dto.ts
                 
                 class
                 EsHitDto
    @@ -155,7 +155,7 @@
             
                 
                     
    -                src/core/domain/dtos/es-hit.dto.ts
    +                src/core/domain/dtos/elastic/es-hit.dto.ts
                 
                 variable
                 allowedProperties
    @@ -167,7 +167,7 @@
             
                 
                     
    -                src/core/domain/dtos/es-query.dto.ts
    +                src/core/domain/dtos/elastic/es-query.dto.ts
                 
                 class
                 EsQueryDto
    @@ -179,7 +179,7 @@
             
                 
                     
    -                src/core/domain/dtos/es-query.dto.ts
    +                src/core/domain/dtos/elastic/es-query.dto.ts
                 
                 variable
                 allowedProperties
    @@ -191,7 +191,7 @@
             
                 
                     
    -                src/core/domain/dtos/es-response.dto.ts
    +                src/core/domain/dtos/elastic/es-response.dto.ts
                 
                 class
                 EsResponseDto
    @@ -203,7 +203,31 @@
             
                 
                     
    -                src/core/domain/dtos/es-response.dto.ts
    +                src/core/domain/dtos/elastic/es-response.dto.ts
    +            
    +            variable
    +            allowedProperties
    +            
    +                100 %
    +                (1/1)
    +            
    +        
    +        
    +            
    +                
    +                src/core/domain/dtos/page-meta.dto.ts
    +            
    +            class
    +            PageMetaDto
    +            
    +                100 %
    +                (7/7)
    +            
    +        
    +        
    +            
    +                
    +                src/core/domain/dtos/page-meta.dto.ts
                 
                 variable
                 allowedProperties
    @@ -335,7 +359,7 @@
             
                 
                     
    -                src/core/domain/interfaces/es-pit.interface.ts
    +                src/core/domain/interfaces/elastic/es-pit.interface.ts
                 
                 interface
                 EsPit
    @@ -347,7 +371,7 @@
             
                 
                     
    -                src/core/domain/interfaces/es-query-string.interface.ts
    +                src/core/domain/interfaces/elastic/es-query-string.interface.ts
                 
                 interface
                 EqQueryString
    @@ -359,7 +383,7 @@
             
                 
                     
    -                src/core/domain/interfaces/es-query.interface.ts
    +                src/core/domain/interfaces/elastic/es-query.interface.ts
                 
                 interface
                 EsQuery
    @@ -371,7 +395,7 @@
             
                 
                     
    -                src/core/domain/interfaces/es-response-hits.interface.ts
    +                src/core/domain/interfaces/elastic/es-response-hits.interface.ts
                 
                 interface
                 EsResponseHits
    @@ -545,7 +569,7 @@
                 PageInterceptor
                 
                     100 %
    -                (7/7)
    +                (8/8)
                 
             
             
    @@ -593,7 +617,7 @@
                 SearchService
                 
                     100 %
    -                (5/5)
    +                (6/6)
                 
             
             
    diff --git a/documentation/graph/dependencies.svg b/documentation/graph/dependencies.svg
    index d77ef52..7a7c66e 100644
    --- a/documentation/graph/dependencies.svg
    +++ b/documentation/graph/dependencies.svg
    @@ -4,217 +4,217 @@
     
     
    -
    -
    +
    +
     dependencies
    -
    -dependencies
    -
    -cluster_AppModule
    -
    -
    -
    -cluster_AppModule_imports
    -
    -
    -
    -cluster_CommonModule
    -
    -
    -
    -cluster_CommonModule_imports
    -
    -
    -
    -cluster_CommonModule_exports
    -
    -
    -
    -cluster_HttpResponseModule
    -
    -
    -
    -cluster_HttpResponseModule_exports
    -
    -
    -
    -cluster_HttpResponseModule_providers
    -
    -
    -
    -cluster_LoggerModule
    -
    -
    -
    -cluster_LoggerModule_exports
    -
    -
    -
    -cluster_LoggerModule_providers
    -
    -
    +
    +dependencies
     
     cluster_SearchModule
    -
    +
     
     
     cluster_SearchModule_exports
    -
    +
     
     
     cluster_SearchModule_providers
    -
    +
    +
    +
    +cluster_AppModule
    +
    +
    +
    +cluster_AppModule_imports
    +
    +
    +
    +cluster_CommonModule
    +
    +
    +
    +cluster_CommonModule_imports
    +
    +
    +
    +cluster_CommonModule_exports
    +
    +
    +
    +cluster_HttpResponseModule
    +
    +
    +
    +cluster_HttpResponseModule_exports
    +
    +
    +
    +cluster_HttpResponseModule_providers
    +
    +
    +
    +cluster_LoggerModule
    +
    +
    +
    +cluster_LoggerModule_exports
    +
    +
    +
    +cluster_LoggerModule_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 423a2cb..2a1c737 100644
    --- a/documentation/injectables/PageInterceptor.html
    +++ b/documentation/injectables/PageInterceptor.html
    @@ -85,6 +85,11 @@
                     
      +
    • + Private + Readonly + ES_IP +
    • Private Readonly @@ -142,7 +147,7 @@ - + @@ -209,8 +214,8 @@ - + @@ -289,8 +294,8 @@ - + @@ -388,8 +393,8 @@ - + @@ -458,6 +463,39 @@

      Properties

      + + + + + + + + + + + + + + + + + +
      + + + Private + Readonly + ES_IP + + +
      + Default value : process.env.ES_CONTAINER_NAME +
      + +
      +

      Elastichsearch IP address

      +
      +
      @@ -478,7 +516,7 @@ @@ -511,7 +549,7 @@ @@ -532,24 +570,20 @@
      import { HttpService } from "@nestjs/axios";
       import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
      -import { reverse } from "dns";
       import { Observable, map, take } from "rxjs";
      -import { EsResponseDto, PageDto } from "../domain/dtos";
      -import { EsQueryDto } from "../domain/dtos/es-query.dto";
      +import { PageDto } from "../domain/dtos";
      +import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto";
       import { RequestDto } from "../domain/dtos/request.dto";
       import { SearchQueryDto } from "../domain/dtos/search-q.dto";
      -import { SearchResultDto } from "../domain/dtos/search-result.dto";
       import { EsTime } from "../domain/enums/es-time.enum";
       import { Order } from "../domain/enums/page-order.enum";
       import { PageMeta } from "../domain/interfaces";
      -import { EsPit } from "../domain/interfaces/es-pit.interface";
      -import { SearchInfo } from "../domain/interfaces/search-info.interface";
      -import { SearchService } from "../services/common/search.service";
      +import { EsPit } from "../domain/interfaces/elastic/es-pit.interface";
       
       /**
        * Previous search data storage
        */
      -class PrevSearch implements SearchInfo {
      +class PrevSearch {
           /**
            * Constructs an uninitialized object
            */
      @@ -562,17 +596,35 @@ class PrevSearch implements SearchInfo {
           /**
            * PIT object of the previous search
            */
      -    pit: EsPit;
      +    private pit: EsPit;
      +    set _pit(pit: EsPit) {
      +        this.pit = pit;
      +    }
      +    get _pit(): EsPit {
      +        return this.pit;
      +    }
       
           /**
            * Tiebreaker and sort parameters
            */
      -    tiebreaker: unknown[];
      +    private tiebreaker: unknown[];
      +    set _tiebreaker(tiebreaker: unknown[]) {
      +        this.tiebreaker = tiebreaker;
      +    }
      +    get _tiebreaker(): unknown[] {
      +        return this.tiebreaker;
      +    }
       
           /**
            * Number of the previous page
            */
      -    prevPage: number;
      +    private prevPage: number;
      +    set _prevPage(page: number) {
      +        this.prevPage = page;
      +    }
      +    get _prevPage(): number {
      +        return this.prevPage;
      +    }
       
           /**
            * Checks if there was the search before current one
      @@ -623,22 +675,26 @@ export class PageInterceptor implements NestInterceptor {
               ];
       
               if (this.prevSearch.isSet()) {
      -            request.es_query.pit = this.prevSearch.pit;
      -            request.es_query.search_after = this.prevSearch.tiebreaker;
      +            request.es_query.pit = this.prevSearch._pit;
      +            request.es_query.search_after = this.prevSearch._tiebreaker;
       
                   let limit = !query?.limit ? 10 : query.limit;
      -            request.es_query.size = limit * Math.abs(query.page - this.prevSearch.prevPage);
      +            request.es_query.size = limit * Math.abs(query.page - this.prevSearch._prevPage);
                   
      -            if (query.page < this.prevSearch.prevPage) {
      +            if (query.page < this.prevSearch._prevPage) {
                       request.es_query.sort = [{ _score: { order: 'asc' } }];
                       request.es_query.size += limit - 1;
                       reverse = true;
      -            } else if (query.page == this.prevSearch.prevPage) {
      -                //...
      +            } else if (query.page == this.prevSearch._prevPage) {
      +                // Caching should be HERE
      +                request.es_query.sort = [{ _score: { order: 'asc' } }];
      +                reverse = true;
                   }
               } else {
      -            this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
      -            request.es_query.size = !query?.limit ? 10 : query.limit;
      +            this.prevSearch._pit = request.es_query.pit = await this.getPIT(1);
      +
      +            let limit = !query?.limit ? 10 : query.limit;
      +            request.es_query.size = limit * query.page;
               }
       
               return next.handle().pipe(
      @@ -646,28 +702,31 @@ export class PageInterceptor implements NestInterceptor {
                       // Setting the page meta-data
                       let meta: PageMeta = {
                           total: res.hits.total.value,
      -                    pagenum: !query?.page ? 1 : query.page,
      +                    pagenum: !query?.page ? 1 : +query.page,
                           order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC,
      -                    hasNext: false,
      -                    hasPrev: false,
                           pagesize: !query?.limit ? 10 : query.limit,
      +                    hasNext: undefined,
      +                    hasPrev: undefined,
                       }; 
      -                // meta.hasNext = res.hits.hits[meta.pagenum * meta.pagesize] ? true : false;
      -                // meta.hasPrev = res.hits.hits[(meta.pagenum - 1) * meta.pagesize - 1] ? true: false;
      +                meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false;
      +                meta.hasPrev = meta.pagenum != 1 ? true : false;
       
                       // Saving the search info
      -                this.prevSearch.pit.id = res.pit_id;
      -                this.prevSearch.tiebreaker = res.hits.hits[res.hits.hits.length - 1].sort;
      -                this.prevSearch.prevPage = query.page;
      +                this.prevSearch._pit.id = res.pit_id;
      +                this.prevSearch._tiebreaker = res.hits.hits[res.hits.hits.length - 1]?.sort;
      +                this.prevSearch._prevPage = query.page;
       
      +                // Check if the performed search is a backwards search
                       let data = res.hits.hits.slice(-meta.pagesize);
                       if (reverse) {
      -                    console.log('REVERSE');
      -                    this.prevSearch.tiebreaker = data[0].sort;
      +                    this.prevSearch._tiebreaker = data[0]?.sort;
                           data.reverse();
                           reverse = false;
                       }
       
      +                // Omitting the redundant info and leaving only the document
      +                data = data.map((el) => el._source);
      +
                       // Return the page
                       return new PageDto(data, meta);
                   })
      @@ -679,6 +738,11 @@ export class PageInterceptor implements NestInterceptor {
            */
           private readonly ES_PORT = process.env.ES_PORT;
       
      +    /**
      +     * Elastichsearch IP address
      +     */
      +     private readonly ES_IP = process.env.ES_CONTAINER_NAME;
      +
           /**
            * Info about previously completed search
            */
      @@ -692,12 +756,12 @@ export class PageInterceptor implements NestInterceptor {
            public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise<EsPit> {
               return new Promise((resolve, reject) => {
                   try {
      -                (this.httpService.post<EsPit>(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`)
      +                this.httpService.post<EsPit>(`http://${this.ES_IP}:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`)
                           .pipe(take(1), map(axiosRes => axiosRes.data))
      -                    .subscribe((res) => {
      +                    .subscribe((res: EsPit) => {
                               res.keep_alive = alive + unit;
                               resolve(res);
      -                    }));
      +                    });
                   } catch (error) {
                       reject(error);
                   }
      @@ -712,7 +776,7 @@ export class PageInterceptor implements NestInterceptor {
            async deletePIT(pitID: string): Promise<boolean> {
               return new Promise((resolve, reject) => {
                   try {
      -                this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, {
      +                this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, {
                           data: { id: pitID },
                           headers: { 'Content-Type': 'application/json' },
                       })
      @@ -725,72 +789,7 @@ export class PageInterceptor implements NestInterceptor {
                   }
               })
           }
      -}
      -/*
      -public saveInfo(pit: EsPit, tiebreaker: unknown[], page: number) {
      -        this.pit.id = pit.id;
      -        this.pit.keep_alive = pit.keep_alive;
      -
      -        this.tiebreaker = tiebreaker.slice();
      -
      -        this.prevPage = page;
      -    }
      -
      -    public clearInfo() {
      -        this.pit = undefined;
      -        this.tiebreaker = undefined;
      -        this.prevPage = -1;
      -    }*/
      -
      -    // getQueryParams(str: string): any {
      -    //     let parameters: object = {};
      -    //     let pairs: string[] = str.split(',');
      -    //     parameters['main'] = pairs[0];
      -    //     pairs.shift();
      -
      -    //     if(!pairs || pairs[0] === '') return parameters;
      -
      -    //     for (const pair of pairs) {
      -    //         const key: string = pair.substring(0, pair.indexOf('='));
      -    //         const value: string = pair.substring(pair.indexOf('=') + 1);
      -    //         parameters[key] = value;
      -    //     }
      -
      -    //     return parameters;
      -    // }
      -
      -
      -    /**
      -     * OLD WAY PAGINATION
      -     *                 // Setting the page data
      -                // const data = res.hits.slice((meta.pagenum - 1) * meta.pagesize, meta.pagenum * meta.pagesize);
      -     */
      -
      -
      -        // if (query.page == 1) {
      -        //     this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
      -        // } else {
      -        //     if (!this.prevSearch.isSet()) {
      -        //         this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
      -
      -        //         request.es_query.size = query.limit * (query.page - 1);
      -        //         this.searchService.findByContext(request.es_query).then((res: SearchResultDto) => {
      -        //             request.es_query.search_after = res.data.hits.hits[res.data.hits.hits.length - 1].sort;
      -        //         });
      -        //     } else {
      -        //         if (query.page == this.prevSearch.prevPage) {
      -        //             return;
      -        //         } else {
      -        //             request.es_query.pit = this.prevSearch.pit;
      -        //             request.es_query.search_after = this.prevSearch.tiebreaker;
      -        //             request.es_query.size = (query.page - this.prevSearch.prevPage);
      -        //         }
      -
      -        //         // request.es_query.pit = this.prevSearch.pit;
      -        //         // request.es_query.search_after = this.prevSearch.tiebreaker;
      -        //     }
      -        // }
      -
      +}
      diff --git a/documentation/injectables/SearchService.html b/documentation/injectables/SearchService.html index 20c8e3d..a80d7ea 100644 --- a/documentation/injectables/SearchService.html +++ b/documentation/injectables/SearchService.html @@ -85,6 +85,11 @@ @@ -242,7 +247,7 @@ HTTPService instance

      @@ -275,8 +280,8 @@ HTTPService instance

      @@ -316,7 +321,7 @@ HTTPService instance

      @@ -333,6 +338,39 @@ HTTPService instance

      Properties

      +
      - +
      - +
        +
      • + Private + Readonly + ES_IP +
      • Private Readonly @@ -133,7 +138,7 @@
      - +
      - +
      + + + + + + + + + + + + + + + + +
      + + + Private + Readonly + ES_IP + + +
      + Default value : process.env.ES_CONTAINER_NAME +
      + +
      +

      Elasticsearch IP address

      +
      +
      @@ -353,7 +391,7 @@ HTTPService instance

      @@ -373,13 +411,10 @@ HTTPService instance

      import { HttpService } from "@nestjs/axios";
      -import { GatewayTimeoutException, Injectable } from "@nestjs/common";
      +import { GatewayTimeoutException, HttpException, Injectable } from "@nestjs/common";
       import { map, take } from "rxjs";
      -import { EsResponseDto } from "src/core/domain/dtos";
      -import { EsQueryDto } from "src/core/domain/dtos/es-query.dto";
      -import { SearchResultDto } from "src/core/domain/dtos/search-result.dto";
      -import { EsTime } from "src/core/domain/enums/es-time.enum";
      -import { EsPit } from "src/core/domain/interfaces/es-pit.interface";
      +import { EsResponseDto} from "../../domain/dtos";
      +import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto";
       
       /**
        * Search service provider
      @@ -398,12 +433,17 @@ export class SearchService {
            */
           private readonly ES_PORT = process.env.ES_PORT;
           
      +    /**
      +     * Elasticsearch IP address
      +     */
      +    private readonly ES_IP = process.env.ES_CONTAINER_NAME;
      +    
           /**
            * Finds a paper by its own ID
            * @param uuid 
            * @returns Elasticsearch hits or an error object
            */
      -    async findByID(uuid: string): Promise<SearchResultDto> { // Should I change 'object' to specific DTO?
      +    async findByID(uuid: string): Promise<EsResponseDto> { // Should I change 'object' to specific DTO?
               let ESQ: EsQueryDto = new EsQueryDto;
       
               ESQ.size = 1;
      @@ -415,21 +455,19 @@ export class SearchService {
       
               return new Promise((resolve, reject) => {
                   try {
      -                (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, {
      +                (this.httpService.get<EsResponseDto>(`http://${this.ES_IP}:${this.ES_PORT}/_search`, {
                           data: ESQ,
                           headers: {'Content-Type': 'application/json'},
                       }))
      -                .pipe(take(1), map(axiosRes => axiosRes.data))
      +                ?.pipe(take(1), map(axiosRes => axiosRes.data))
                       .subscribe((res: EsResponseDto) => {
                           if (res.timed_out) {
      -                        throw new GatewayTimeoutException;
      -                        // reject(new SearchResultDto(504, {message: 'Timed Out'}));
      +                        reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
                           }
      -
      -                    resolve(new SearchResultDto(200, res));
      +                    resolve(res);
                       });
                   } catch (error) {
      -                reject(new SearchResultDto(700, error));
      +                reject(error);
                   }
               });
           }
      @@ -439,65 +477,27 @@ export class SearchService {
            * @param query, <EsQueryDto> 
            * @returns Elasticsearch hits or an error object
            */
      -    async findByContext(es_query: EsQueryDto): Promise<SearchResultDto> {
      -        console.log(`SEARCH|SERVICE: ${JSON.stringify(es_query, null, 2)}`);
      +    async findByContext(es_query: EsQueryDto): Promise<EsResponseDto> {
               return new Promise((resolve, reject) => {
                   try {
      -                (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, {
      +                (this.httpService.get<EsResponseDto>(`http://${this.ES_IP}:${this.ES_PORT}/_search`, {
                           data: es_query,
                           headers: {'Content-Type': 'application/json'},
                       }))
      -                .pipe(take(1), map(axiosRes => axiosRes.data))
      +                ?.pipe(take(1), map(axiosRes => axiosRes.data))
                       .subscribe((res: EsResponseDto) => {
                           if (res.timed_out) {
      -                        throw new GatewayTimeoutException;
      -                        // reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'}));
      +                        reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
                           }
       
      -                    resolve(new SearchResultDto(200, res));
      +                    resolve(res);
                       });
                   } catch (error) {
      -                reject(new SearchResultDto(700, error));
      +                reject(error);
                   }
               });
           }
      -}
      -
      -// let ESQ: EsQueryDto = new EsQueryDto;
      -
      -        // if (limit) ESQ.size = limit;
      -        // ESQ.query = {
      -        //     query_string: {
      -        //         query: query_str,
      -        //         default_field: 'content',
      -        //     }
      -        // }
      -        // this.getPIT(1).then((pit) => {
      -        //     ESQ.pit = pit;
      -        // });
      -
      -/**
      - * Context
      - *      // let es_query = { // DTO
      -        //     query: { // Interface
      -        //         query_string: { // Interface
      -        //             query: query_str,
      -        //             default_field: "content"
      -        //         }
      -        //     },
      -        // }
      - */
      -
      -/**
      - * Single
      - *      // let es_query = {
      -        //     query: {
      -        //         query_string: {
      -        //             query: 'id:' + uuid
      -        //         }
      -        //     },
      -        // }
      - */
      +}
      diff --git a/documentation/interfaces/EqQueryString.html b/documentation/interfaces/EqQueryString.html index 7ad3c14..c9b9dab 100644 --- a/documentation/interfaces/EqQueryString.html +++ b/documentation/interfaces/EqQueryString.html @@ -66,7 +66,7 @@

      File

      - src/core/domain/interfaces/es-query-string.interface.ts + src/core/domain/interfaces/elastic/es-query-string.interface.ts

      @@ -273,11 +273,6 @@ Can't be specified with 'default_field'

      * Can't be specified with 'default_field' */ fields?: string[]; - - /** - * - */ - } diff --git a/documentation/interfaces/EsPit.html b/documentation/interfaces/EsPit.html index ad224a2..1d69180 100644 --- a/documentation/interfaces/EsPit.html +++ b/documentation/interfaces/EsPit.html @@ -66,7 +66,7 @@

      File

      - src/core/domain/interfaces/es-pit.interface.ts + src/core/domain/interfaces/elastic/es-pit.interface.ts

      diff --git a/documentation/interfaces/EsQuery.html b/documentation/interfaces/EsQuery.html index 13a5a04..1be216b 100644 --- a/documentation/interfaces/EsQuery.html +++ b/documentation/interfaces/EsQuery.html @@ -66,7 +66,7 @@

      File

      - src/core/domain/interfaces/es-query.interface.ts + src/core/domain/interfaces/elastic/es-query.interface.ts

      diff --git a/documentation/interfaces/EsResponseHits.html b/documentation/interfaces/EsResponseHits.html index 6323b5a..137408a 100644 --- a/documentation/interfaces/EsResponseHits.html +++ b/documentation/interfaces/EsResponseHits.html @@ -66,7 +66,7 @@

      File

      - src/core/domain/interfaces/es-response-hits.interface.ts + src/core/domain/interfaces/elastic/es-response-hits.interface.ts

      @@ -248,7 +248,7 @@
      -
      import { EsHitDto } from "../dtos/es-hit.dto";
      +        
      import { EsHitDto } from "../../dtos/elastic/es-hit.dto";
       
       /**
        * Structure of 'hits' object of Elasticsearch response
      diff --git a/documentation/interfaces/SearchInfo.html b/documentation/interfaces/SearchInfo.html
      index 28775d4..2d4d921 100644
      --- a/documentation/interfaces/SearchInfo.html
      +++ b/documentation/interfaces/SearchInfo.html
      @@ -198,7 +198,7 @@ Indicates the starting point of next search

      -
      import { EsPit } from "./es-pit.interface";
      +        
      import { EsPit } from "./elastic/es-pit.interface";
       
       /**
        * Structure of search metadata
      diff --git a/documentation/js/menu-wc.js b/documentation/js/menu-wc.js
      index 47fe941..a3eb3db 100644
      --- a/documentation/js/menu-wc.js
      +++ b/documentation/js/menu-wc.js
      @@ -121,13 +121,13 @@ customElements.define('compodoc-menu', class extends HTMLElement {
                                       SearchModule
                                           
    • -
    • - +
      -

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

      +

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

      @@ -131,77 +134,7 @@ - - - - - - - -
      - Default value : ['sort', '_source', '_score'] -
      -

      List of allowed properties in this DTO

      -
      -
      -
      -

      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

      -
      -

      - - - - - - - - - @@ -390,6 +323,111 @@
      - - - allowedProperties - - -
      - Type : [] - -
      - Default value : ['took', 'timed_out', '_shards', 'hits', 'pit_id'] + Default value : ['total', 'pagenum', 'order', 'hasNext', 'hasPrev', 'pagesize']
      +

      src/core/domain/dtos/elastic/es-hit.dto.ts

      +
      +

      + + + + + + + + + + + + + + + + +
      + + + allowedProperties + + +
      + Type : [] + +
      + Default value : ['sort', '_source', '_score'] +
      +

      List of allowed properties in this DTO

      +
      +
      +
      +

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

      +
      +

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

      List of allowed properties in this DTO

      +
      +
      +
      +

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

      +
      +

      + + + + + + + + + + + + + + + + +
      + + + allowedProperties + + +
      + Type : [] + +
      + Default value : ['took', 'timed_out', '_shards', 'hits', 'pit_id'] +
      +

      List of allowed properties in this DTO

      +
      +
      +

      src/infrastructure/config/env.objects.ts

      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/HttpResponseModule.html b/documentation/modules/HttpResponseModule.html index f298ffb..a50283e 100644 --- a/documentation/modules/HttpResponseModule.html +++ b/documentation/modules/HttpResponseModule.html @@ -57,14 +57,14 @@ cluster_HttpResponseModule - -cluster_HttpResponseModule_exports - - cluster_HttpResponseModule_providers + +cluster_HttpResponseModule_exports + + HttpResponseService diff --git a/documentation/modules/LoggerModule.html b/documentation/modules/LoggerModule.html index 733b70a..d91a07c 100644 --- a/documentation/modules/LoggerModule.html +++ b/documentation/modules/LoggerModule.html @@ -57,14 +57,14 @@ cluster_LoggerModule - -cluster_LoggerModule_exports - - cluster_LoggerModule_providers + +cluster_LoggerModule_exports + + LoggerService diff --git a/documentation/modules/LoggerModule/dependencies.svg b/documentation/modules/LoggerModule/dependencies.svg index 5c106c0..0321adc 100644 --- a/documentation/modules/LoggerModule/dependencies.svg +++ b/documentation/modules/LoggerModule/dependencies.svg @@ -14,14 +14,14 @@ cluster_LoggerModule - -cluster_LoggerModule_providers - - cluster_LoggerModule_exports + +cluster_LoggerModule_providers + + LoggerService diff --git a/documentation/modules/SearchModule.html b/documentation/modules/SearchModule.html index d6ede70..fbcca7c 100644 --- a/documentation/modules/SearchModule.html +++ b/documentation/modules/SearchModule.html @@ -134,7 +134,7 @@

      Description

      -

      search module

      +

      Search module

      @@ -178,11 +178,11 @@
      import { HttpModule } from "@nestjs/axios";
       import { Module } from "@nestjs/common";
      -import { PapersController } from "src/application";
      +import { PapersController } from "../../application";
       import { SearchService } from "../../core/services/common/search.service";
       
       /**
      - * search module
      + * Search module
        */
       @Module({
           imports: [
      diff --git a/documentation/overview.html b/documentation/overview.html
      index 5138f8f..91e00cb 100644
      --- a/documentation/overview.html
      +++ b/documentation/overview.html
      @@ -301,7 +301,7 @@
                   

      -

      11 Classes

      +

      12 Classes

      diff --git a/package-lock.json b/package-lock.json index fb8019a..a26b6a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4058,7 +4058,8 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true }, "node_modules/ast-transform": { "version": "0.0.0", @@ -5302,6 +5303,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "dev": true, "dependencies": { "asap": "^2.0.0", "wrappy": "1" @@ -7047,6 +7049,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -12861,7 +12864,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/throat": { "version": "6.0.1", @@ -13695,6 +13699,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -16733,7 +16738,8 @@ "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true }, "ast-transform": { "version": "0.0.0", @@ -17692,6 +17698,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", "integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==", + "dev": true, "requires": { "asap": "^2.0.0", "wrappy": "1" @@ -18984,7 +18991,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true }, "inflight": { "version": "1.0.6", @@ -23388,7 +23396,8 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "throat": { "version": "6.0.1", @@ -23985,6 +23994,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz", "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==", + "dev": true, "requires": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" diff --git a/package.json b/package.json index ca94796..10d3a13 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json", + "test:e2e": "jest --config ./src/test/jest-e2e.json", "doc": "./node_modules/.bin/compodoc -p tsconfig.json -w -s -r 7000 --theme 'readthedocs'" }, "dependencies": { @@ -89,6 +89,11 @@ "collectCoverageFrom": [ "**/*.(t|j)s" ], + "coveragePathIgnorePatterns": [ + "/dist/", + "/documentation", + "/.eslintrc.js" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" } diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 4aefece..74efcdf 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,9 +1,9 @@ -import { Controller, Get, HttpCode, HttpException, Next, Param, ParseUUIDPipe, Put, Query, Req, Res, UseInterceptors } from "@nestjs/common"; +import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; -import { PageInterceptor } from "src/core/interceptors/page.interceptor"; -import { SearchResultDto } from "src/core/domain/dtos/search-result.dto"; -import { ApiOperation, ApiResponse } from "@nestjs/swagger"; -import { RequestDto } from "src/core/domain/dtos/request.dto"; +import { PageInterceptor } from "../../core/interceptors/page.interceptor"; +import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from "@nestjs/swagger"; +import { RequestDto } from "../../core/domain/dtos/request.dto"; +import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain"; /** * /papers/ route controller @@ -12,6 +12,7 @@ import { RequestDto } from "src/core/domain/dtos/request.dto"; version: '1', path: 'papers', }) +@ApiExtraModels(RequestDto, EsHitDto, EsResponseDto) export class PapersController { constructor(private searchService: SearchService) {} @@ -21,22 +22,28 @@ export class PapersController { * @param response * @returns a response with a set of matching papers */ - @ApiOperation({ summary: 'Finds papers by context based on the query.' }) - @ApiResponse({ - status: 200, - description: 'Returns back acquired papers.', - type: SearchResultDto, - }) + @ApiTags('Search') + @ApiOperation({ + summary: 'Finds papers by context based on the query', + }) + @ApiResponse({ + status: 200, + description: 'Returns back a page with acquired papers', + type: PageDto + }) + @ApiGatewayTimeoutResponse({ + description: 'Elasticsearch request timed out' + }) @Get('search') @UseInterceptors(PageInterceptor) @HttpCode(200) - getByContext(@Req() query: RequestDto): object { - return this.searchService.findByContext(query.es_query).then( - (response: SearchResultDto) => { - return response.data; + getByContext(@Req() request: RequestDto): Promise { + return this.searchService.findByContext(request.es_query).then( + (response) => { + return response; }, - (error: SearchResultDto) => { - throw new HttpException(error.data, error.statusCode); + (error) => { + throw error; } ); } @@ -47,22 +54,28 @@ export class PapersController { * @param response * @returns a response with a requested object */ - @ApiOperation({ summary: 'Finds paper by its UUID.' }) - @ApiResponse({ - status: 200, - description: 'Returns back acquired paper.', - type: SearchResultDto, - }) + @ApiTags('Search') + @ApiOperation({ + summary: 'Finds paper by its UUID', + tags: ['Search'] + }) + @ApiResponse({ + status: 200, + description: 'Returns back a paper', + type: PaperDto + }) + @ApiGatewayTimeoutResponse({ + description: 'Elasticsearch request timed out' + }) @Get(':uuid') - @UseInterceptors(PageInterceptor) @HttpCode(200) - getByID(@Param('uuid', ParseUUIDPipe) uuid: string): object { + getByID(@Param('uuid', ParseUUIDPipe) uuid: string): Promise { return this.searchService.findByID(uuid).then( - (response) => { - return response.data; + (response: EsResponseDto) => { + return response.hits.hits[0]._source; }, (error) => { - throw new HttpException(error.data, error.status); + throw error; } ); } diff --git a/src/core/domain/dtos/es-hit.dto.ts b/src/core/domain/dtos/elastic/es-hit.dto.ts similarity index 76% rename from src/core/domain/dtos/es-hit.dto.ts rename to src/core/domain/dtos/elastic/es-hit.dto.ts index 1334729..e2d6c28 100644 --- a/src/core/domain/dtos/es-hit.dto.ts +++ b/src/core/domain/dtos/elastic/es-hit.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; -import { PaperDto } from "./paper.dto"; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional } from "class-validator"; +import { PaperDto } from "../paper.dto"; /** * List of allowed properties in this DTO @@ -10,6 +10,7 @@ const allowedProperties = ['sort', '_source', '_score']; /** * Structure of the document stored and retrieved from Elasticsearch */ +@ApiExtraModels() export class EsHitDto { /** * Actual document stored in Elasticsearch @@ -27,7 +28,7 @@ export class EsHitDto { * List of objects that represents how the hit was sorted */ @IsOptional() - @ApiProperty({ + @ApiPropertyOptional({ description: 'List of objects that represents how the hit was sorted', example: {} }) @@ -37,7 +38,7 @@ export class EsHitDto { * Hit relevance score */ @IsOptional() - @ApiProperty({ + @ApiPropertyOptional({ description: 'Relevance score', example: 1.2355 }) diff --git a/src/core/domain/dtos/es-query.dto.ts b/src/core/domain/dtos/elastic/es-query.dto.ts similarity index 81% rename from src/core/domain/dtos/es-query.dto.ts rename to src/core/domain/dtos/elastic/es-query.dto.ts index 5c9fd6b..0572933 100644 --- a/src/core/domain/dtos/es-query.dto.ts +++ b/src/core/domain/dtos/elastic/es-query.dto.ts @@ -1,7 +1,7 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsArray, IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator"; -import { EsPit } from "../interfaces/es-pit.interface"; -import { EsQuery } from "../interfaces/es-query.interface" +import { EsPit } from "../../interfaces/elastic/es-pit.interface"; +import { EsQuery } from "../../interfaces/elastic/es-query.interface" /** * List of allowed properties in this DTO @@ -11,6 +11,7 @@ import { EsQuery } from "../interfaces/es-query.interface" /** * Elasticsearch query DTO */ + @ApiExtraModels() export class EsQueryDto { /** * Maximum number of elements returned by Elasticsearch @@ -19,7 +20,7 @@ import { EsQuery } from "../interfaces/es-query.interface" @IsDefined() @IsNumber() @IsInt() - @ApiProperty({ + @ApiPropertyOptional({ description: 'Maximum number of elements returned by Elasticsearch', example: 30 }) @@ -41,7 +42,7 @@ import { EsQuery } from "../interfaces/es-query.interface" */ @IsOptional() @IsObject() - @ApiProperty({ + @ApiPropertyOptional({ description: 'PIT object', example: {} }) @@ -52,7 +53,7 @@ import { EsQuery } from "../interfaces/es-query.interface" */ @IsOptional() @IsArray() - @ApiProperty({ + @ApiPropertyOptional({ description: '', example: [] }) @@ -63,7 +64,7 @@ import { EsQuery } from "../interfaces/es-query.interface" */ @IsOptional() @IsArray() - @ApiProperty({ + @ApiPropertyOptional({ description: '', example: [] }) diff --git a/src/core/domain/dtos/es-response.dto.ts b/src/core/domain/dtos/elastic/es-response.dto.ts similarity index 73% rename from src/core/domain/dtos/es-response.dto.ts rename to src/core/domain/dtos/elastic/es-response.dto.ts index 84bc76b..9270953 100644 --- a/src/core/domain/dtos/es-response.dto.ts +++ b/src/core/domain/dtos/elastic/es-response.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator"; -import { EsResponseHits } from "../interfaces/es-response-hits.interface"; +import { EsResponseHits } from "../../interfaces/elastic/es-response-hits.interface"; /** * List of allowed properties in this DTO @@ -10,6 +10,7 @@ const allowedProperties = ['took', 'timed_out', '_shards', 'hits', 'pit_id']; /** * Elasticsearch response DTO */ +@ApiExtraModels() export class EsResponseDto { /** * Number of milliseconds it @@ -19,7 +20,7 @@ export class EsResponseDto { @IsNotEmpty() @IsNumber() @ApiProperty({ - description: 'took', + description: 'The time that it took Elasticsearch to process the query', example: 5 }) took: number; @@ -32,7 +33,7 @@ export class EsResponseDto { @IsNotEmpty() @IsBoolean() @ApiProperty({ - description: 'timed_out', + description: 'Shows if request timed out before completion', example: false, }) timed_out: boolean; @@ -44,7 +45,7 @@ export class EsResponseDto { @IsOptional() @IsObject() @ApiProperty({ - description: '_shards', + description: 'Contains a count of Elasticsearch shards used to process the request', example: { total: 1, successful: 1, @@ -60,7 +61,7 @@ export class EsResponseDto { @IsOptional() @IsObject() @ApiProperty({ - description: 'hits', + description: 'Contains returned documents and metadata', example: { total: { value: 3, @@ -71,12 +72,8 @@ export class EsResponseDto { _index: 'papers', _id: '01002', _score: 1.2, - _source: { - - }, - fields: { - - } + _source: {}, + fields: {} }], } }) @@ -87,8 +84,8 @@ export class EsResponseDto { */ @IsString() @IsOptional() - @ApiProperty({ - description: 'PIT ID used to search for results', + @ApiPropertyOptional({ + description: 'Contains PIT ID used to search for results', example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==' }) pit_id?: string; diff --git a/src/core/domain/dtos/index.ts b/src/core/domain/dtos/index.ts index 32eeca0..d24be14 100644 --- a/src/core/domain/dtos/index.ts +++ b/src/core/domain/dtos/index.ts @@ -1,3 +1,8 @@ -export * from './es-response.dto' -export * from './page.dto' -export * from './search-q.dto' \ No newline at end of file +export * from './elastic/es-query.dto'; +export * from './elastic/es-response.dto'; +export * from './elastic/es-hit.dto'; +export * from './page.dto'; +export * from './search-q.dto'; +export * from './search-result.dto'; +export * from './paper.dto'; +export * from './request.dto'; \ No newline at end of file diff --git a/src/core/domain/dtos/page-meta.dto.ts b/src/core/domain/dtos/page-meta.dto.ts new file mode 100644 index 0000000..2a89285 --- /dev/null +++ b/src/core/domain/dtos/page-meta.dto.ts @@ -0,0 +1,73 @@ +import { ApiExtraModels, ApiProperty, PartialType } from "@nestjs/swagger"; +import { IsArray } from "class-validator"; +import { Order } from "../enums"; +import { PageMeta } from "../interfaces/page-meta.interface"; +import { PaperDto } from "./paper.dto"; + +/** + * List of allowed properties in this DTO + */ +const allowedProperties = ['total', 'pagenum', 'order', 'hasNext', 'hasPrev', 'pagesize']; + +/** + * Page model for pagination + */ +@ApiExtraModels() +export class PageMetaDto implements PageMeta { + /** + * Total number of hits (results) acquired from the search + */ + @IsArray() + @ApiProperty({ + description: 'Total number of hits (results) acquired from the search', + example: 314 + }) + total: number; + + /** + * Current page number + */ + @ApiProperty({ + description: 'Current page number', + minimum: 1, + example: 3 + }) + pagenum: number; + + /** + * Order of the elements on the page + */ + @ApiProperty({ + description: 'Order of the elements on the page', + example: Order.DESC + }) + order: Order; + + /** + * Flag, that shows if there's a page following the current one + */ + @ApiProperty({ + description: 'Flag, that shows if there\'s a page following the current one', + example: true + }) + hasNext: boolean; + + /** + * Flag, that shows if there's a page preceding the current one + */ + @ApiProperty({ + description: 'Flag, that shows if there\'s a page preceding the current one', + example: true + }) + hasPrev: boolean; + + /** + * Maximum number of elements on the page + */ + @ApiProperty({ + description: 'Maximum number of elements on the page', + minimum: 1, + example: 20 + }) + pagesize: number; +} \ No newline at end of file diff --git a/src/core/domain/dtos/page.dto.ts b/src/core/domain/dtos/page.dto.ts index 8c0ab24..785b1e0 100644 --- a/src/core/domain/dtos/page.dto.ts +++ b/src/core/domain/dtos/page.dto.ts @@ -1,6 +1,8 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty, PartialType } from "@nestjs/swagger"; import { IsArray } from "class-validator"; +import { Order } from "../enums"; import { PageMeta } from "../interfaces/page-meta.interface"; +import { PageMetaDto } from "./page-meta.dto"; import { PaperDto } from "./paper.dto"; /** @@ -11,14 +13,16 @@ const allowedProperties = ['data', 'meta']; /** * Page model for pagination */ +@ApiExtraModels() export class PageDto { /** * Data block of the page */ @IsArray() @ApiProperty({ - description: 'All data the page contains', + description: 'All data (papers) the page contains', isArray: true, + type: PaperDto }) readonly data: PaperDto[]; @@ -27,9 +31,10 @@ export class PageDto { */ @ApiProperty({ description: 'Metadata for the page', - // example: [], + // example: {}, + }) - readonly meta: PageMeta; + readonly meta: PageMetaDto; /** * Constructs an object with provided parameters diff --git a/src/core/domain/dtos/paper.dto.ts b/src/core/domain/dtos/paper.dto.ts index 5ee8e60..07e1d7d 100644 --- a/src/core/domain/dtos/paper.dto.ts +++ b/src/core/domain/dtos/paper.dto.ts @@ -1,7 +1,5 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; -import { EsQueryDto } from "./es-query.dto"; -import { SearchQueryDto } from "./search-q.dto"; /** * List of allowed properties in this DTO @@ -11,6 +9,7 @@ const allowedProperties = ['id', 'title', 'authors', 'topic', 'summary', 'tags', /** * Structure of the document stored and retrieved from Elasticsearch */ +@ApiExtraModels() export class PaperDto { /** * Unique ID of the paper diff --git a/src/core/domain/dtos/request.dto.ts b/src/core/domain/dtos/request.dto.ts index fffc0ea..973264d 100644 --- a/src/core/domain/dtos/request.dto.ts +++ b/src/core/domain/dtos/request.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; -import { EsQueryDto } from "./es-query.dto"; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsDefined, IsNotEmpty, IsOptional } from "class-validator"; +import { EsQueryDto } from "./elastic/es-query.dto"; import { SearchQueryDto } from "./search-q.dto"; /** @@ -11,6 +11,7 @@ const allowedProperties = ['query', 'es_query']; /** * Request object, which contains query parameters and Elasticsearch query object */ +@ApiExtraModels() export class RequestDto { /** * Query parameters object @@ -18,7 +19,8 @@ export class RequestDto { @IsDefined() @IsNotEmpty() @ApiProperty({ - description: '', + type: SearchQueryDto, + description: 'Actual query with parameters acquired from the request', example: {} }) query: SearchQueryDto; @@ -27,8 +29,9 @@ export class RequestDto { * Elasticsearch query object */ @IsOptional() - @ApiProperty({ - description: '', + @ApiPropertyOptional({ + type: EsQueryDto, + description: 'Elasticsearch query body constructed by pagination mechanism', example: {}, }) es_query?: EsQueryDto; @@ -38,8 +41,8 @@ export class RequestDto { * @param query * @param es_query */ - constructor(query: SearchQueryDto, es_query: EsQueryDto) { + constructor(query: SearchQueryDto, es_query: EsQueryDto) { this.query = query; this.es_query = es_query; - } + } } \ 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 28bf733..8834657 100644 --- a/src/core/domain/dtos/search-q.dto.ts +++ b/src/core/domain/dtos/search-q.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; +import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; /** * List of allowed properties in this DTO @@ -9,6 +9,7 @@ const allowedProperties = ['query', 'pagen', 'limit', 'order']; /** * Elasticsearch response DTO */ +@ApiExtraModels() export class SearchQueryDto { /** * Given query string to perform the diff --git a/src/core/domain/dtos/search-result.dto.ts b/src/core/domain/dtos/search-result.dto.ts index 5097f64..c926308 100644 --- a/src/core/domain/dtos/search-result.dto.ts +++ b/src/core/domain/dtos/search-result.dto.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; -import { EsResponseDto } from "./es-response.dto"; +import { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsDefined, IsInt, IsNotEmpty } from "class-validator"; +import { EsResponseDto } from "./elastic/es-response.dto"; /** * List of allowed properties in this DTO @@ -10,6 +10,7 @@ const allowedProperties = ['data', 'status']; /** * Elasticsearch response DTO */ +@ApiExtraModels() export class SearchResultDto { /** * Status code @@ -32,7 +33,10 @@ export class SearchResultDto { @ApiProperty({ description: 'Data acquired from the Elasticsearch', example: { - + took: 1, + timed_out: false, + _shards: {}, + hits: {} }, }) data: EsResponseDto; diff --git a/src/core/domain/enums/es-time.enum.ts b/src/core/domain/enums/es-time.enum.ts index 7c5a5b6..8429795 100644 --- a/src/core/domain/enums/es-time.enum.ts +++ b/src/core/domain/enums/es-time.enum.ts @@ -2,11 +2,38 @@ * Elasticsearch time-units */ export enum EsTime { + /** + * Days + */ days = 'd', + + /** + * Hours + */ hours = 'h', + + /** + * Minutes + */ min = 'm', + + /** + * Seconds + */ sec = 's', + + /** + * Milliseconds + */ ms = 'ms', + + /** + * Microseconds + */ us = 'micros', + + /** + * Nanoseconds + */ ns = 'nanos' } \ No newline at end of file diff --git a/src/core/domain/enums/index.ts b/src/core/domain/enums/index.ts index dd8d8c9..8cb24ea 100644 --- a/src/core/domain/enums/index.ts +++ b/src/core/domain/enums/index.ts @@ -1,3 +1,4 @@ export * from './httpResponse' export * from './roles.enum' -export * from './page-order.enum' \ No newline at end of file +export * from './page-order.enum' +export * from './es-time.enum' \ No newline at end of file diff --git a/src/core/domain/enums/page-order.enum.ts b/src/core/domain/enums/page-order.enum.ts index 942cd68..ff4a505 100644 --- a/src/core/domain/enums/page-order.enum.ts +++ b/src/core/domain/enums/page-order.enum.ts @@ -2,6 +2,13 @@ * Page display order */ export enum Order { + /** + * Ascending order + */ ASC = 'asc', + + /** + * Descending order + */ DESC = 'desc', } \ No newline at end of file diff --git a/src/core/domain/index.ts b/src/core/domain/index.ts index cea7a31..5806f60 100644 --- a/src/core/domain/index.ts +++ b/src/core/domain/index.ts @@ -1 +1,3 @@ -export * from './enums' \ No newline at end of file +export * from './enums' +export * from './dtos' +export * from './interfaces' \ No newline at end of file diff --git a/src/core/domain/interfaces/es-pit.interface.ts b/src/core/domain/interfaces/elastic/es-pit.interface.ts similarity index 100% rename from src/core/domain/interfaces/es-pit.interface.ts rename to src/core/domain/interfaces/elastic/es-pit.interface.ts diff --git a/src/core/domain/interfaces/es-query-string.interface.ts b/src/core/domain/interfaces/elastic/es-query-string.interface.ts similarity index 94% rename from src/core/domain/interfaces/es-query-string.interface.ts rename to src/core/domain/interfaces/elastic/es-query-string.interface.ts index 55c1ee6..5f6dcc8 100644 --- a/src/core/domain/interfaces/es-query-string.interface.ts +++ b/src/core/domain/interfaces/elastic/es-query-string.interface.ts @@ -18,9 +18,4 @@ * Can't be specified with 'default_field' */ fields?: string[]; - - /** - * - */ - } \ No newline at end of file diff --git a/src/core/domain/interfaces/es-query.interface.ts b/src/core/domain/interfaces/elastic/es-query.interface.ts similarity index 100% rename from src/core/domain/interfaces/es-query.interface.ts rename to src/core/domain/interfaces/elastic/es-query.interface.ts diff --git a/src/core/domain/interfaces/es-response-hits.interface.ts b/src/core/domain/interfaces/elastic/es-response-hits.interface.ts similarity index 84% rename from src/core/domain/interfaces/es-response-hits.interface.ts rename to src/core/domain/interfaces/elastic/es-response-hits.interface.ts index 32db4de..6bca2ef 100644 --- a/src/core/domain/interfaces/es-response-hits.interface.ts +++ b/src/core/domain/interfaces/elastic/es-response-hits.interface.ts @@ -1,4 +1,4 @@ -import { EsHitDto } from "../dtos/es-hit.dto"; +import { EsHitDto } from "../../dtos/elastic/es-hit.dto"; /** * Structure of 'hits' object of Elasticsearch response diff --git a/src/core/domain/interfaces/index.ts b/src/core/domain/interfaces/index.ts index 8c2a188..6295a5f 100644 --- a/src/core/domain/interfaces/index.ts +++ b/src/core/domain/interfaces/index.ts @@ -1,4 +1,7 @@ -export * from './http-response.interface'; +export * from './http-response.interface' export * from './page-meta.interface' -export * from './es-query.interface' -export * from './es-query-string.interface' \ No newline at end of file +export * from './search-info.interface' +export * from './elastic/es-query.interface' +export * from './elastic/es-query-string.interface' +export * from './elastic/es-response-hits.interface' +export * from './elastic/es-pit.interface' \ No newline at end of file diff --git a/src/core/domain/interfaces/search-info.interface.ts b/src/core/domain/interfaces/search-info.interface.ts index 4ad0b9c..1e1c995 100644 --- a/src/core/domain/interfaces/search-info.interface.ts +++ b/src/core/domain/interfaces/search-info.interface.ts @@ -1,4 +1,4 @@ -import { EsPit } from "./es-pit.interface"; +import { EsPit } from "./elastic/es-pit.interface"; /** * Structure of search metadata diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index adb895e..76e1934 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -2,20 +2,18 @@ import { HttpService } from "@nestjs/axios"; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; import { Observable, map, take } from "rxjs"; import { PageDto } from "../domain/dtos"; -import { EsHitDto } from "../domain/dtos/es-hit.dto"; -import { EsQueryDto } from "../domain/dtos/es-query.dto"; +import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto"; import { RequestDto } from "../domain/dtos/request.dto"; import { SearchQueryDto } from "../domain/dtos/search-q.dto"; import { EsTime } from "../domain/enums/es-time.enum"; import { Order } from "../domain/enums/page-order.enum"; import { PageMeta } from "../domain/interfaces"; -import { EsPit } from "../domain/interfaces/es-pit.interface"; -import { SearchInfo } from "../domain/interfaces/search-info.interface"; +import { EsPit } from "../domain/interfaces/elastic/es-pit.interface"; /** * Previous search data storage */ -class PrevSearch implements SearchInfo { +class PrevSearch { /** * Constructs an uninitialized object */ @@ -28,17 +26,35 @@ class PrevSearch implements SearchInfo { /** * PIT object of the previous search */ - pit: EsPit; + private pit: EsPit; + set _pit(pit: EsPit) { + this.pit = pit; + } + get _pit(): EsPit { + return this.pit; + } /** * Tiebreaker and sort parameters */ - tiebreaker: unknown[]; + private tiebreaker: unknown[]; + set _tiebreaker(tiebreaker: unknown[]) { + this.tiebreaker = tiebreaker; + } + get _tiebreaker(): unknown[] { + return this.tiebreaker; + } /** * Number of the previous page */ - prevPage: number; + private prevPage: number; + set _prevPage(page: number) { + this.prevPage = page; + } + get _prevPage(): number { + return this.prevPage; + } /** * Checks if there was the search before current one @@ -89,21 +105,23 @@ export class PageInterceptor implements NestInterceptor { ]; if (this.prevSearch.isSet()) { - request.es_query.pit = this.prevSearch.pit; - request.es_query.search_after = this.prevSearch.tiebreaker; + request.es_query.pit = this.prevSearch._pit; + request.es_query.search_after = this.prevSearch._tiebreaker; let limit = !query?.limit ? 10 : query.limit; - request.es_query.size = limit * Math.abs(query.page - this.prevSearch.prevPage); + request.es_query.size = limit * Math.abs(query.page - this.prevSearch._prevPage); - if (query.page < this.prevSearch.prevPage) { + if (query.page < this.prevSearch._prevPage) { request.es_query.sort = [{ _score: { order: 'asc' } }]; request.es_query.size += limit - 1; reverse = true; - } else if (query.page == this.prevSearch.prevPage) { - //... + } else if (query.page == this.prevSearch._prevPage) { + // Caching should be HERE + request.es_query.sort = [{ _score: { order: 'asc' } }]; + reverse = true; } } else { - this.prevSearch.pit = request.es_query.pit = await this.getPIT(1); + this.prevSearch._pit = request.es_query.pit = await this.getPIT(1); let limit = !query?.limit ? 10 : query.limit; request.es_query.size = limit * query.page; @@ -114,24 +132,24 @@ export class PageInterceptor implements NestInterceptor { // Setting the page meta-data let meta: PageMeta = { total: res.hits.total.value, - pagenum: !query?.page ? 1 : query.page, + pagenum: !query?.page ? 1 : +query.page, order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC, + pagesize: !query?.limit ? 10 : query.limit, hasNext: undefined, hasPrev: undefined, - pagesize: !query?.limit ? 10 : query.limit, }; meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false; meta.hasPrev = meta.pagenum != 1 ? true : false; // Saving the search info - this.prevSearch.pit.id = res.pit_id; - this.prevSearch.tiebreaker = res.hits.hits[res.hits.hits.length - 1]?.sort; - this.prevSearch.prevPage = query.page; + this.prevSearch._pit.id = res.pit_id; + this.prevSearch._tiebreaker = res.hits.hits[res.hits.hits.length - 1]?.sort; + this.prevSearch._prevPage = query.page; - // Check if the performed search is a backward search + // Check if the performed search is a backwards search let data = res.hits.hits.slice(-meta.pagesize); if (reverse) { - this.prevSearch.tiebreaker = data[0]?.sort; + this.prevSearch._tiebreaker = data[0]?.sort; data.reverse(); reverse = false; } @@ -150,6 +168,11 @@ export class PageInterceptor implements NestInterceptor { */ private readonly ES_PORT = process.env.ES_PORT; + /** + * Elastichsearch IP address + */ + private readonly ES_IP = process.env.ES_CONTAINER_NAME; + /** * Info about previously completed search */ @@ -163,12 +186,12 @@ export class PageInterceptor implements NestInterceptor { public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise { return new Promise((resolve, reject) => { try { - (this.httpService.post(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`) + this.httpService.post(`http://${this.ES_IP}:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`) .pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res) => { + .subscribe((res: EsPit) => { res.keep_alive = alive + unit; resolve(res); - })); + }); } catch (error) { reject(error); } @@ -183,7 +206,7 @@ export class PageInterceptor implements NestInterceptor { async deletePIT(pitID: string): Promise { return new Promise((resolve, reject) => { try { - this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, { + this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, { data: { id: pitID }, headers: { 'Content-Type': 'application/json' }, }) diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index bcb3278..609ce0d 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,11 +1,8 @@ import { HttpService } from "@nestjs/axios"; -import { GatewayTimeoutException, Injectable } from "@nestjs/common"; +import { GatewayTimeoutException, HttpException, Injectable } from "@nestjs/common"; import { map, take } from "rxjs"; -import { EsResponseDto } from "src/core/domain/dtos"; -import { EsQueryDto } from "src/core/domain/dtos/es-query.dto"; -import { SearchResultDto } from "src/core/domain/dtos/search-result.dto"; -import { EsTime } from "src/core/domain/enums/es-time.enum"; -import { EsPit } from "src/core/domain/interfaces/es-pit.interface"; +import { EsResponseDto} from "../../domain/dtos"; +import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto"; /** * Search service provider @@ -24,12 +21,17 @@ export class SearchService { */ private readonly ES_PORT = process.env.ES_PORT; + /** + * Elasticsearch IP address + */ + private readonly ES_IP = process.env.ES_CONTAINER_NAME; + /** * Finds a paper by its own ID * @param uuid * @returns Elasticsearch hits or an error object */ - async findByID(uuid: string): Promise { // Should I change 'object' to specific DTO? + async findByID(uuid: string): Promise { // Should I change 'object' to specific DTO? let ESQ: EsQueryDto = new EsQueryDto; ESQ.size = 1; @@ -41,21 +43,19 @@ export class SearchService { return new Promise((resolve, reject) => { try { - (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { + (this.httpService.get(`http://${this.ES_IP}:${this.ES_PORT}/_search`, { data: ESQ, headers: {'Content-Type': 'application/json'}, })) - .pipe(take(1), map(axiosRes => axiosRes.data)) + ?.pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsResponseDto) => { if (res.timed_out) { - throw new GatewayTimeoutException; - // reject(new SearchResultDto(504, {message: 'Timed Out'})); + reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } - - resolve(new SearchResultDto(200, res)); + resolve(res); }); } catch (error) { - reject(new SearchResultDto(700, error)); + reject(error); } }); } @@ -65,62 +65,24 @@ export class SearchService { * @param query, * @returns Elasticsearch hits or an error object */ - async findByContext(es_query: EsQueryDto): Promise { - console.log(`SEARCH|SERVICE: ${JSON.stringify(es_query, null, 2)}`); + async findByContext(es_query: EsQueryDto): Promise { return new Promise((resolve, reject) => { try { - (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { + (this.httpService.get(`http://${this.ES_IP}:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) - .pipe(take(1), map(axiosRes => axiosRes.data)) + ?.pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsResponseDto) => { if (res.timed_out) { - throw new GatewayTimeoutException; - // reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'})); + reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } - resolve(new SearchResultDto(200, res)); + resolve(res); }); } catch (error) { - reject(new SearchResultDto(700, error)); + reject(error); } }); } -} - -// let ESQ: EsQueryDto = new EsQueryDto; - - // if (limit) ESQ.size = limit; - // ESQ.query = { - // query_string: { - // query: query_str, - // default_field: 'content', - // } - // } - // this.getPIT(1).then((pit) => { - // ESQ.pit = pit; - // }); - -/** - * Context - * // let es_query = { // DTO - // query: { // Interface - // query_string: { // Interface - // query: query_str, - // default_field: "content" - // } - // }, - // } - */ - -/** - * Single - * // let es_query = { - // query: { - // query_string: { - // query: 'id:' + uuid - // } - // }, - // } - */ \ No newline at end of file +} \ No newline at end of file diff --git a/src/infrastructure/config/env.validation.ts b/src/infrastructure/config/env.validation.ts index c56d8d9..c92753c 100644 --- a/src/infrastructure/config/env.validation.ts +++ b/src/infrastructure/config/env.validation.ts @@ -1,5 +1,5 @@ import { plainToClass } from 'class-transformer'; -import { validateSync, IsOptional } from 'class-validator'; +import { validateSync } from 'class-validator'; /** * env vatiables diff --git a/src/infrastructure/modules/index.ts b/src/infrastructure/modules/index.ts index da53f6a..c80e102 100644 --- a/src/infrastructure/modules/index.ts +++ b/src/infrastructure/modules/index.ts @@ -1 +1,2 @@ export * from './app.module'; +export * from './search.module' \ No newline at end of file diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index bccc4e6..526c7d7 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,10 +1,10 @@ import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; -import { PapersController } from "src/application"; +import { PapersController } from "../../application"; import { SearchService } from "../../core/services/common/search.service"; /** - * search module + * Search module */ @Module({ imports: [ diff --git a/src/main.ts b/src/main.ts index 4d8d5c2..c3efc9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,9 @@ async function bootstrap() { }) ); + /** + * Enabling URI-type versioning of the API + */ app.enableVersioning({ type: VersioningType.URI, }); @@ -27,12 +30,14 @@ async function bootstrap() { * Configuration of the Swagger document */ const config = new DocumentBuilder() - .setTitle('Nestjs boilerplate') - .setDescription('This is a nest clean architecture boilerplate') + .setTitle('Freeland') + .setDescription('Freeland open library API') .setVersion('0.0.1') .build(); - const document = SwaggerModule.createDocument(app, config); + const document = SwaggerModule.createDocument(app, config, { + deepScanRoutes: true, + }); SwaggerModule.setup('api', app, document); try { diff --git a/src/test/e2e/papers.controller.e2e.spec.ts b/src/test/e2e/papers.controller.e2e.spec.ts new file mode 100644 index 0000000..922bdc0 --- /dev/null +++ b/src/test/e2e/papers.controller.e2e.spec.ts @@ -0,0 +1,227 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication } from "@nestjs/common"; +import request from 'supertest' +import { AppModule } from "../../infrastructure/modules"; +import { ConfigModule } from "@nestjs/config"; +import { HttpService } from "@nestjs/axios"; +import { of } from "rxjs"; +import { Order } from "../../core/domain"; + +describe('E2E Testing of /papers', () => { + let app: INestApplication; + let httpService: HttpService; + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [ + AppModule, + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + expandVariables: true + }) + ], + providers: [ + { + provide: HttpService, + useValue: { + get: jest.fn(), + post: jest.fn(), + delete: jest.fn() + } + } + ] + }).compile(); + + app = moduleRef.createNestApplication(); + httpService = moduleRef.get(HttpService) + await app.init(); + }); + + it('Should be defined', () => { + expect(app).toBeDefined(); + expect(httpService).toBeDefined(); + }); + + it('GET /papers/{uuid} | Should return one exact item', async () => { + const axiosRes = of({ + status: 200, + statusText: 'statText', + headers: null, + config: null, + data: { + took: 5, + timed_out: false, + _shards: {}, + hits: { + total: {}, + hits: [ + { + _source: { + id: 'thisIsIDofTheFirstObject', + title: 'thisIsTitle', + authors: ['A1', 'A2', 'A3'], + topic: 'thisIsTopic', + summary: 'thisIsSummary', + tags: ['T1', 'T2', 'T3'], + content: 'thisIsContent' + } + } + ] + } + } + }); + + let httpGetSpy = jest.spyOn(httpService, 'get').mockReturnValueOnce(axiosRes); + + const test = await request(app.getHttpServer()) + .get('/papers/2d3dc418-7778-abab-b33f-3d63aa25db41') // ??? Fetch a random object from DB + .expect(200); + + // Expect HttpService.get() method to be touched + expect(httpGetSpy).toHaveBeenCalled(); + expect(httpGetSpy).toHaveReturnedWith(axiosRes); + + // Checking received data + expect(test.body).toBeDefined(); + expect(test.body.id).toBeDefined(); + expect(test.body.id).toBe('thisIsIDofTheFirstObject'); + + expect(test.body.title).toBeDefined(); + expect(test.body.title).toBe('thisIsTitle'); + + expect(test.body.authors).toBeDefined(); + expect(test.body.authors).toEqual(['A1', 'A2', 'A3']); + + expect(test.body.topic).toBeDefined(); + expect(test.body.topic).toBe('thisIsTopic'); + + expect(test.body.summary).toBeDefined(); + expect(test.body.summary).toBe('thisIsSummary'); + + expect(test.body.tags).toBeDefined(); + expect(test.body.tags).toEqual(['T1', 'T2', 'T3']); + + expect(test.body.content).toBeDefined(); + expect(test.body.content).toBe('thisIsContent'); + }); + + it('GET /papers/search? | Should return multiple items on the page and correct meta', async () => { + const axiosResGet = of({ + status: 200, + statusText: 'statText', + headers: null, + config: null, + data: { + took: 5, + timed_out: false, + _shards: {}, + hits: { + total: { + value: 2, + }, + hits: [ + { + _source: { + id: 'thisIsIDofTheFirstObject', + title: 'thisIsTitle1', + authors: ['A1', 'A2', 'A3'], + topic: 'thisIsTopic1', + summary: 'thisIsSummary1', + tags: ['T1', 'T2', 'T3'], + content: 'thisIsContent1' + } + }, + { + _source: { + id: 'thisIsIDofTheSecondObject', + title: 'thisIsTitle2', + authors: ['A4', 'A5', 'A6'], + topic: 'thisIsTopic2', + summary: 'thisIsSummary2', + tags: ['T11', 'T2', 'T8'], + content: 'thisIsContent2' + } + } + ] + } + } + }); + + let httpGetSpy = jest.spyOn(httpService, 'get').mockReturnValueOnce(axiosResGet); + let httpPostSpy = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + data: {}, + status: 200, + statusText: 'statText', + headers: null, + config: null, + })); + + const test = await request(app.getHttpServer()) + .get('/papers/search?query=at&page=1') + .expect(200); + + // Expect HttpService.get() method to be touched + expect(httpGetSpy).toHaveBeenCalled(); + expect(httpGetSpy).toHaveReturnedWith(axiosResGet); + + // Checking received data + expect(test.body.data).toBeDefined(); + expect(test.body.data.length).toBe(2); + + for (const paper of test.body.data) { + expect(paper.id).toBeDefined(); + expect(paper.title).toBeDefined(); + expect(paper.authors).toBeDefined(); + expect(paper.topic).toBeDefined(); + expect(paper.summary).toBeDefined(); + expect(paper.tags).toBeDefined(); + expect(paper.content).toBeDefined(); + } + + expect(test.body.data[0]).toEqual({ + id: 'thisIsIDofTheFirstObject', + title: 'thisIsTitle1', + authors: ['A1', 'A2', 'A3'], + topic: 'thisIsTopic1', + summary: 'thisIsSummary1', + tags: ['T1', 'T2', 'T3'], + content: 'thisIsContent1' + }); + + expect(test.body.data[1]).toEqual({ + id: 'thisIsIDofTheSecondObject', + title: 'thisIsTitle2', + authors: ['A4', 'A5', 'A6'], + topic: 'thisIsTopic2', + summary: 'thisIsSummary2', + tags: ['T11', 'T2', 'T8'], + content: 'thisIsContent2' + }); + + // // Checking received meta + expect(test.body.meta).toBeDefined(); + + expect(test.body.meta.total).toBeDefined(); + expect(test.body.meta.total).toBe(2); + + expect(test.body.meta.pagenum).toBeDefined(); + expect(test.body.meta.pagenum).toBe(1); + + expect(test.body.meta.order).toBeDefined(); + expect(test.body.meta.order).toBe(Order.DESC); + + expect(test.body.meta.pagesize).toBeDefined(); + expect(test.body.meta.pagesize).toBe(10); + + expect(test.body.meta.hasNext).toBeDefined(); + expect(test.body.meta.hasNext).toBe(false); + + expect(test.body.meta.hasPrev).toBeDefined(); + expect(test.body.meta.hasPrev).toBe(false); + }); + + afterAll(async () => { + await app.close(); + }) +}); \ No newline at end of file diff --git a/src/test/jest-e2e.json b/src/test/jest-e2e.json new file mode 100644 index 0000000..c27d4a0 --- /dev/null +++ b/src/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": "e2e.spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} \ No newline at end of file diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index 057ec1c..e3fda46 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,112 +1,318 @@ -// // import { CallHandler, ExecutionContext } from "@nestjs/common"; -// import { HttpModule } from "@nestjs/axios"; -// import { Test } from "@nestjs/testing"; -// import { Observable, of } from "rxjs"; -// import { PapersController } from "src/application"; -// import { Order } from "src/core/domain"; -// import { PageDto, SearchQueryDto } from "src/core/domain/dtos"; -// import { PageInterceptor } from "src/core/interceptors/page.interceptor"; -// import { SearchService } from "src/core/services/common/search.service"; +import { HttpService } from "@nestjs/axios"; +import { ConfigModule } from "@nestjs/config"; + import { Test } from "@nestjs/testing"; +import { Observable, of } from "rxjs"; +import { EsTime, Order } from "src/core/domain"; +import { PageDto } from "src/core/domain/dtos"; +import { HttpResponseException } from "src/core/exceptions"; +import { PageInterceptor } from "src/core/interceptors/page.interceptor"; -// const executionContext = { -// switchToHttp: jest.fn().mockReturnThis(), -// getRequest: jest.fn().mockReturnThis(), -// getHandler: jest.fn().mockReturnThis(), -// getArgs: jest.fn().mockReturnThis(), -// getArgByIndex: jest.fn().mockReturnThis(), -// switchToRpc: jest.fn().mockReturnThis(), -// switchToWs: jest.fn().mockReturnThis(), -// getType: jest.fn().mockReturnThis(), -// getClass: jest.fn().mockReturnThis(), -// }; +const execCtxMock = { + switchToHttp: jest.fn().mockReturnThis(), + getRequest: jest.fn().mockReturnThis(), + getHandler: jest.fn().mockReturnThis(), + getArgs: jest.fn().mockReturnThis(), + getArgByIndex: jest.fn().mockReturnThis(), + switchToRpc: jest.fn().mockReturnThis(), + switchToWs: jest.fn().mockReturnThis(), + getType: jest.fn().mockReturnThis(), + getClass: jest.fn().mockReturnThis(), + }; -// const callHandler = { -// handle: jest.fn(), -// }; +const callHandlerMock = { + handle: jest.fn(), +}; -// describe('Testing PageInterceptor', () => { -// let pageInter: PageInterceptor; -// let moduleRef; +describe('Unit tests for PageInterceptor', () => { + let pageInter: PageInterceptor; + let httpService: HttpService; -// beforeEach(async () => { -// moduleRef = await Test.createTestingModule({ -// imports: [HttpModule], -// controllers: [PapersController], -// providers: [SearchService, PageInterceptor], -// }).compile(); + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + { + provide: HttpService, + useValue: { + post: jest.fn(), + delete: jest.fn() + }, + }, + PageInterceptor, + ], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + expandVariables: true, + }) + ], + }).compile(); -// pageInter = moduleRef.get(PageInterceptor); -// }); + pageInter = moduleRef.get(PageInterceptor); + httpService = moduleRef.get(HttpService); -// describe('intercept()', () => { -// it('Should be defined', () => { -// expect(pageInter).toBeDefined(); -// }); + execCtxMock.getRequest.mockReturnValue({ + query: { + query: 'thisIsMyQuery', + page: 1, + limit: 5, + order: Order.DESC + } + }); -// it('Should return an Observable with a page of type PageDto', (done) => { -// executionContext.getRequest.mockReturnValue( { query: new SearchQueryDto('someQuery', 1, 10, 'desc') }); -// callHandler.handle.mockReturnValue( of({ -// total: { value: 1 }, -// hits: [{},], -// })); + callHandlerMock.handle.mockReturnValue( + of({ + hits: { + total: { value: 1 }, + hits: [{}] + } + }) + ); + }); + + it('Should be defined', () => { + expect(pageInter).toBeDefined(); + expect(httpService).toBeDefined(); + }); + + describe('intercept()', () => { + let tmp; + beforeAll(() => { + tmp = pageInter.getPIT; + pageInter.getPIT = jest.fn().mockReturnValue({}); + }); + + afterAll(() => { + pageInter.getPIT = tmp; + }); + + it('Should return a Promise', () => { + expect(pageInter.intercept(execCtxMock, callHandlerMock)).toBeInstanceOf(Promise); + }); + + it('Should return a Promise with Observable and PageDto inside', () => { + pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { + expect(res).toBeInstanceOf(Observable); + res.subscribe((data) => { + expect(data).toBeInstanceOf(PageDto); + }); + }); + }); + + it('Should touch CallHandler.handle() method', () => { + let chHandleSpy = jest.spyOn(callHandlerMock, 'handle'); + pageInter.intercept(execCtxMock, callHandlerMock); + expect(chHandleSpy).toBeCalled(); + }); + + it('Should construct a page with proper data on it', () => { + callHandlerMock.handle.mockReturnValueOnce( + of({ + hits: { + total: { value: 1 }, + hits: [{ + _source: { + dummy: 'dum' + } + }] + } + }) + ); + + pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { + res.subscribe((page) => { + expect(page.data.length).toBe(1); + expect(page.data[0]).toEqual({ dummy: 'dum' }); + }); + }); + }); + + it('Should construct correct meta-data of the page', () => { + execCtxMock.getRequest.mockReturnValueOnce({ + query: { + page: 5, + order: 'desc', + limit: 100, + } + }); + + callHandlerMock.handle.mockReturnValueOnce( + of({ + hits: { + total: { value: 921 }, + hits: [] + } + }) + ); + + pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { + res.subscribe((page) => { + expect(page.meta).toEqual({ + total: 921, + pagenum: 5, + order: 'desc', + hasNext: true, + hasPrev: true, + pagesize: 100 + }); + }); + }); + }); + + it('Should reverse the search results', () => { + execCtxMock.getRequest.mockReturnValueOnce({ + query: { + page: 1, + order: 'desc', + limit: 3 + } + }); + + pageInter['prevSearch']._prevPage = 3; + pageInter['prevSearch'].isSet = jest.fn().mockImplementationOnce(() => { + return true; + }) + + callHandlerMock.handle.mockReturnValueOnce( + of({ + hits: { + total: { value: 1 }, + hits: [ + { sort: ['1', 'less relevant'], _source: '1' }, + { sort: ['2', 'average'], _source: '2' }, + { sort: ['3', 'most relevant'], _source: '3' } + ] + } + }) + ); + + pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { + res.subscribe((page) => { + expect(pageInter['prevSearch']._tiebreaker).toEqual(['1', 'less relevant']); + expect(page.data).toEqual(['3', '2', '1']); + }); + }); + }); + }); + + describe('getPIT()', () => { + it('Should touch HttpService.post() method', () => { + let httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + pageInter.getPIT(1); + expect(httpPostMock).toHaveBeenCalled(); + }); + + it('Should contain correct port in the URI from .env', () => { + let httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + pageInter.getPIT(1); + expect(httpPostMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); + }); + + it('Should touch HttpService with correct URI when time alive and time-unit are set', () => { + let httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + let time = 2; + let unit = EsTime.sec; - -// expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Observable); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data).toBeInstanceOf(PageDto); -// done(); -// }); -// }) + pageInter.getPIT(time, unit); + expect(httpPostMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); + }); -// it('Should hold content on the returned page', (done) => { -// executionContext.getRequest.mockReturnValueOnce( { query: new SearchQueryDto('someQuery', 1, 10, 'desc') }); -// callHandler.handle.mockReturnValueOnce(of({ -// total: { value: 1 }, -// hits: [{dummy: 'dum'}], -// })); + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'post').mockImplementationOnce(() => { + throw HttpResponseException; + }); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data).toEqual({ -// data: expect.anything(), -// meta: expect.anything(), -// }); -// done(); -// }); -// }); + expect(pageInter.getPIT(1)).rejects.toEqual(HttpResponseException); + }); -// it('Should have next page', (done) => { -// executionContext.getRequest.mockReturnValue({ query: new SearchQueryDto('someQuery', 1, 5, 'desc') }); -// callHandler.handle.mockReturnValue(of({ -// total: { value: 10 }, -// hits: Array(10).fill({dummy: 'dum'}, 0, 10), -// })); + it('Should return a non-empty string when HttpService request succeedes', () => { + jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + data: {id: '2567', keep_alive: '1m'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data.meta.hasNext).toEqual(true); -// expect(data.meta.hasPrev).toEqual(false); -// done(); -// }); -// }); + expect(pageInter.getPIT(1)).resolves.toEqual({ + id: '2567', + keep_alive: '1m', + }); + }); + }); -// it('Should have correct meta-data', (done) => { -// executionContext.getRequest.mockReturnValue({ query: new SearchQueryDto('someQuery', 2, 5, 'asc') }); -// callHandler.handle.mockReturnValue(of({ -// total: { value: 15 }, -// hits: Array(15).fill({dummy: 'dum'}, 0, 15), -// })); + describe('deletePIT()', () => { + it('Should touch HttpService.delete() method', () => { + let httpDeleteMock = jest.spyOn(httpService, 'delete').mockReturnValueOnce( + of({ + data: {succeeded: true}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data.meta).toEqual({ -// total: 15, -// pagenum: 2, -// order: Order.ASC, -// hasNext: true, -// hasPrev: true, -// pagesize: 5 -// }); -// done(); -// }); -// }); -// }); + pageInter.deletePIT(''); + expect(httpDeleteMock).toHaveBeenCalled(); + }); -// }); \ No newline at end of file + it('Should contain correct port in the URI from .env and passed PIT ID in the request body', () => { + let httpDeleteMock = jest.spyOn(httpService, 'delete').mockReturnValueOnce( + of({ + data: { succeeded: true }, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + pageInter.deletePIT('thisIsIDSpecified'); + expect(httpDeleteMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_pit`, { + data: { id: 'thisIsIDSpecified' }, + headers: { 'Content-Type': 'application/json' } + }); + }); + + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'delete').mockImplementationOnce(() => { + throw HttpResponseException; + }); + + expect(pageInter.deletePIT('')).rejects.toEqual(HttpResponseException); + }); + + it('Should return true when Elasticsearch successfully removed PIT', () => { + jest.spyOn(httpService, 'delete').mockReturnValueOnce( + of({ + data: { succeeded: true }, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + expect(pageInter.deletePIT('')).resolves.toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/test/papers.controller.spec.ts b/src/test/papers.controller.spec.ts new file mode 100644 index 0000000..5a1ff79 --- /dev/null +++ b/src/test/papers.controller.spec.ts @@ -0,0 +1,140 @@ +import { HttpModule } from "@nestjs/axios"; +import { NotFoundException } from "@nestjs/common"; +import { Test } from "@nestjs/testing"; +import { PapersController } from "src/application"; +import { SearchService } from "src/core/services/common/search.service"; + + +describe('Unit tests for PapersController', () => { + let searchService: SearchService; + let papersController: PapersController; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + PapersController, + { + provide: SearchService, + useValue: { + findByContext: jest.fn(), + findByID: jest.fn() + } + } + ], + imports: [HttpModule] + }).compile(); + + papersController = moduleRef.get(PapersController); + searchService = moduleRef.get(SearchService); + }); + + it('Should be defined', () => { + expect(papersController).toBeDefined(); + expect(searchService).toBeDefined(); + }); + + describe('getByContext()', () => { + it('Should touch SearchService.findByContext() method', () => { + let findCtxMock = jest.spyOn(searchService, 'findByContext') + .mockResolvedValueOnce({ + took: undefined, + timed_out: undefined, + hits: undefined, + _shards: undefined, + }); + + papersController.getByContext({ query: undefined }); + expect(findCtxMock).toHaveBeenCalled(); + }); + + it('Should resolve, when searched successfully', () => { + const searchResultMock = { + took: 1, + timed_out: false, + hits: { + total: {}, + hits: [ + { + _source: { + id: 'thisIsID', + title: 'andThisIsTheTitle', + authors: ['alsoAuthors'], + topic: 'andThatIsTheTopic', + summary: 'someSummaries', + tags: ['tag1', 'tag2'], + content: 'finallyContent!' + } + } + ], + }, + _shards: undefined, + }; + + jest.spyOn(searchService, 'findByContext') + .mockResolvedValueOnce(searchResultMock); + + expect(papersController.getByContext({ query: undefined })).resolves.toEqual(searchResultMock); + }); + + it('Should throw, when search was unsuccessful', () => { + searchService.findByContext = jest.fn() + .mockRejectedValueOnce(new NotFoundException); + + expect(papersController.getByContext({ query: undefined })) + .rejects.toThrow(NotFoundException) + }); + }); + + describe('getByID()', () => { + it('Should touch SearchService.findByID() method', () => { + let findIDMock = jest.spyOn(searchService, 'findByID') + .mockResolvedValueOnce({ + took: undefined, + timed_out: undefined, + hits: { total: {}, hits:[{ _source: undefined }] }, + _shards: undefined, + }); + + papersController.getByID(''); + expect(findIDMock).toHaveBeenCalled(); + }); + + it('Should resolve the document, when searched successfully', () => { + const searchResultMock = { + took: 1, + timed_out: false, + hits: { + total: {}, + hits: [ + { + _source: { + id: 'thisIsID', + title: 'andThisIsTheTitle', + authors: ['alsoAuthors'], + topic: 'andThatIsTheTopic', + summary: 'someSummaries', + tags: ['tag1', 'tag2'], + content: 'finallyContent!' + } + } + ], + }, + _shards: undefined, + }; + + jest.spyOn(searchService, 'findByID') + .mockResolvedValueOnce(searchResultMock); + + expect(papersController.getByID('')) + .resolves.toEqual(searchResultMock.hits.hits[0]._source); + }); + + it('Should throw, when search was unsuccessful', () => { + searchService.findByID = jest.fn() + .mockRejectedValueOnce(new NotFoundException); + + expect(papersController.getByID('')) + .rejects.toThrow(NotFoundException) + }); + }); +}); \ No newline at end of file diff --git a/src/test/papers.endpoint.spec.ts b/src/test/papers.endpoint.spec.ts deleted file mode 100644 index b8f17be..0000000 --- a/src/test/papers.endpoint.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { INestApplication } from "@nestjs/common"; -import { SearchModule } from "src/infrastructure/modules/search.module"; -import request from 'supertest' -import { assert } from "console"; -import { resolve } from "path"; -import { AppModule } from "src/infrastructure/modules"; - -describe('E2E Testing of /papers', () => { - let app: INestApplication; - - beforeAll(async () => { - const moduleRef: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }).compile(); - - app = moduleRef.createNestApplication(); - await app.init(); - }); - - it('Should return one, exact item on page', async () => { - return request(app.getHttpServer()) - .get('/papers/eeeb2d01-8315-454e-b33f-3d6caa25db42') - .expect(200) - .expect((res) => { - res.body.data.length === 1; - }) - .expect((res) => { - res.body.data[0]._source.id === 'eeeb2d01-8315-454e-b33f-3d6caa25db42'; - }); - }); - - it('Should return multiple items', async () => { - return request(app.getHttpServer()) - .get('/papers/search?query=at&page=1') - .expect(200) - .expect((res) => { - res.body.data.length > 0; - }) - .expect((res) => { - for (const value of res.body.data) { - if(Object.keys(value).length === 0) return false; - } - }) - }); - - afterAll(async () => { - await app.close(); - }) -}); \ No newline at end of file diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index d9c5d58..864f6c3 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -1,112 +1,224 @@ -// import { HttpService } from "@nestjs/axios"; -// import { ConfigModule } from "@nestjs/config"; -// import { Test } from "@nestjs/testing"; -// import exp from "constants"; -// import { of } from "rxjs"; -// import { EsTime } from "src/core/domain/enums/es-time.enum"; -// import { HttpResponseException } from "src/core/exceptions"; -// import { SearchService } from "src/core/services/common/search.service"; +import { HttpService } from "@nestjs/axios"; +import { GatewayTimeoutException, HttpException } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { Test } from "@nestjs/testing"; +import { of } from "rxjs"; +import { EsQueryDto, EsResponseDto } from "src/core/domain"; +import { SearchService } from "src/core/services/common/search.service"; -// describe('Unit tests for SearchService', () => { -// let searchService: SearchService; -// let httpService: HttpService; +describe('Unit tests for SearchService', () => { + let searchService: SearchService; + let httpService: HttpService; + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + SearchService, + { + provide: HttpService, + useValue: { + get: jest.fn(), + }, + }, + ], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + expandVariables: true, + }) + ], + }).compile(); -// 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); + }); -// searchService = moduleRef.get(SearchService); -// httpService = moduleRef.get(HttpService); -// }); + describe('findByID()', () => { + it('Should touch HttpService.get() method', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); -// 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.findByID(''); + expect(httpGetSpy).toHaveBeenCalled(); + }); -// searchService.getPIT(1); -// 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(1); -// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); -// }); - -// it('Should touch HttpService with correct URI when time alive and time-unit are set', () => { -// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ -// data: {id: '2567'}, -// status: 0, -// statusText: '', -// headers: {}, -// config: {}, -// })); - -// let time = 2; -// let unit = EsTime.sec; + it('Should send correct data via HttpService.get() body parameter', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); -// searchService.getPIT(time, unit); -// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); -// }); + const uuid = 'thisIsUUID_Provided'; + searchService.findByID(uuid); + expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { + data: { + size: 1, + query: { + query_string: { + query: 'id:' + uuid + } + } + }, + headers: { 'Content-Type': 'application/json' } + }); + }); -// it('Should return error exeception when HttpService fails', () => { -// jest.spyOn(httpService, 'post').mockImplementation(() => { -// throw HttpResponseException; -// }); + it('Should call HttpService.get() with correct URI and port number', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); + + searchService.findByID(''); + expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>( + `http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_search`, + expect.anything() + ); + }); -// expect(searchService.getPIT(1)).rejects.toEqual(HttpResponseException); -// }); + it('Should return a Promise', () => { + expect(searchService.findByID('')).toBeInstanceOf(Promise); + }); -// it('Should return a non-empty string when HttpService request succeedes', () => { -// jest.spyOn(httpService, 'post').mockReturnValue(of({ -// data: {id: '2567', keep_alive: '1m'}, -// status: 0, -// statusText: '', -// headers: {}, -// config: {}, -// })); + // it('Should return a Promise with EsResponseDto', () => { + // // Axios response mock + // httpService.get = jest.fn().mockReturnValueOnce( + // of({ + // status: undefined, + // statusText: undefined, + // headers: undefined, + // config: undefined, + // data: { + // took: 1, + // timed_out: false, + // hits: { + // total: {}, + // hits: [{}] + // } + // }, + // }) + // ); + + // expect(searchService.findByID('')).resolves.toBeInstanceOf(EsResponseDto) + // }); -// expect(searchService.getPIT(1)).resolves.toEqual({ -// id: '2567', -// keep_alive: '1m', -// }); -// }); + // Errors + it('Should throw 504 | GatewayTimeoutException', () => { + // Axios response mock + httpService.get = jest.fn().mockReturnValueOnce( + of({ + status: undefined, + statusText: undefined, + headers: undefined, + config: undefined, + data: { + timed_out: true, + dummy: 'dum' + } + }) + ); + searchService.findByID('').catch((err) => { + expect(err).toBeInstanceOf(GatewayTimeoutException); + }); + }); -// }); + it('Should throw an HttpException when HttpService.get() fails and throws', () => { + httpService.get = jest.fn().mockImplementationOnce(() => { + throw new HttpException({ oops: 'sorry' }, 999); + }); -// 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 + searchService.findByID('').catch((err) => { + expect(err).toBeInstanceOf(HttpException); + expect(err.response).toEqual({ oops: 'sorry' }); + expect(err.status).toEqual(999); + }); + }); + }); + + describe('findByContext()', () => { + it('Should touch HttpService.get() method', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); + + searchService.findByContext(null); + expect(httpGetSpy).toHaveBeenCalled(); + }); + + it('Should send correct data via HttpService.get() body parameter', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); + + let es_query = new EsQueryDto(); + es_query = { + query: { + query_string: { + query: 'thisIsTheQuery!' + } + } + } + + searchService.findByContext(es_query); + expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { + data: es_query, + headers: { 'Content-Type': 'application/json' } + }); + }); + + it('Should call HttpService.get() with correct URI and port number', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); + + searchService.findByContext(null); + expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>( + `http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_search`, + expect.anything() + ); + }); + + it('Should return a Promise', () => { + expect(searchService.findByContext(null)).toBeInstanceOf(Promise); + }); + + // it('Should return a Promise with EsResponseDto', () => { + // // Axios response mock + // httpService.get = jest.fn().mockReturnValueOnce( + // of({ + // status: undefined, + // statusText: undefined, + // headers: undefined, + // config: undefined, + // data: { + // dummy: 'dum' + // } + // }) + // ); + + // expect(searchService.findByContext(null)).resolves.toMatchObject(null); + // }); + + // Errors + it('Should throw 504 | GatewayTimeoutException', () => { + // Axios response mock + httpService.get = jest.fn().mockReturnValueOnce( + of({ + status: undefined, + statusText: undefined, + headers: undefined, + config: undefined, + data: { + timed_out: true, + dummy: 'dum' + } + }) + ); + + searchService.findByContext(null).catch((err) => { + expect(err).toBeInstanceOf(GatewayTimeoutException); + }); + }); + + it('Should throw an HttpException when HttpService.get() fails and throws', () => { + httpService.get = jest.fn().mockImplementationOnce(() => { + throw new HttpException({ oops: 'sorry' }, 999); + }); + + searchService.findByContext(null).catch((err) => { + expect(err).toBeInstanceOf(HttpException); + expect(err.response).toEqual({ oops: 'sorry' }); + expect(err.status).toEqual(999); + }); + }); + }); +}); \ No newline at end of file