From d611c95edb491bcbb13cdf9eb6ac8e8d6dd465d4 Mon Sep 17 00:00:00 2001 From: moeidtopcoder Date: Thu, 28 Jul 2022 16:24:53 +0000 Subject: [PATCH 01/23] Add 'scripts/run.sh' --- scripts/run.sh | 186 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 scripts/run.sh diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..b3759ee --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,186 @@ +#!/bin/bash + +set -Eeuo pipefail +trap cleanup SIGINT SIGTERM ERR EXIT + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) + +usage() { + cat << EOF # remove the space between << and EOF, this is due to web plugin issue +Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-build_docker] [-build_and_run_docker] [-stop_docker] [-run_app] [-run_test] [-run_lint] [-deploy_on_kubernetes] +This script helps you to runn the application in different forms. below you can get the full list of available options. +Available options: +-h, --help Print this help and exit +-build_docker Build the docker image called "freeland:latest" +-build_and_run_docker Build the docker image and run on local machine +-stop_docker Stop running docker container named "freeland" +-run_app Run application with npm in usual way for development +-run_test Run npm test +-run_lint Run npm lint +-generate_doc Generate the code documentation +-deploy_on_kubernetes you need to have a kubernetes cluster already up and running on the machine. +EOF + exit +} +#============================================================================================================================================================================= +cleanup() { + trap - SIGINT SIGTERM ERR EXIT + # script cleanup here +} +#============================================================================================================================================================================= +setup_colors() { + if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then + NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m' + else + NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW='' + fi +} +#============================================================================================================================================================================= +msg() { + echo >&2 -e "${1-}" +} +#============================================================================================================================================================================= +die() { + local msg=$1 + local code=${2-1} # default exit status 1 + msg "$msg" + exit "$code" +} +#============================================================================================================================================================================= +checkIfHelmIsInstalled() +{ + echo "Checking Helm ..." +if command which helm > /dev/null; then + echo "Helm is not installed! :(" + echo "Installing Helm ..." + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + sudo chmod 700 get_helm.sh + ./get_helm.sh + echo "Checking secrets helm plugin..." + helm plugin install https://github.com/jkroepke/helm-secrets --version v3.12.0 || true + echo "helm is installed, skipping..." + else + echo "Helm is installed :) ..." + sleep 1 +fi +} +checkIfSkaffoldIsInstalled() +{ + echo "Checking Skaffold ..." +if command which helm > /dev; then + echo "Skaffold is not installed! :(" + echo "Installing Skaffold ..." + curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && \ + sudo install skaffold /usr/local/bin/ + echo "Skaffold is installed, skipping..." + else + echo "Helm is installed :) ..." + sleep 1 +fi +} +#============================================================================================================================================================================= +runOnKubernetes(){ + checkIfHelmIsInstalled + checkIfSkaffoldIsInstalled + cd .. + make +} +#============================================================================================================================================================================= +runTheApp() +{ + cd .. + npm install + npm start +} +#============================================================================================================================================================================= +runTheTests() +{ + cd .. + npm install + npm test +} +#============================================================================================================================================================================= +runtheLint() +{ + cd .. + npm install + npm run lint +} +#============================================================================================================================================================================= +runDockerImage(){ + cd .. + docker-compose up -d +} +#============================================================================================================================================================================= +stopDockerImage(){ + cd .. + docker-compose down +} +#============================================================================================================================================================================= +generateDoc(){ + cd .. + npm install + npm run doc +} +#============================================================================================================================================================================= +buildDockerImage() +{ + echo "Checking Docker ..." +if [[ $(which docker) && $(docker --version) ]]; then + echo "Docker is installed :) ..." + sleep 1 + + else + echo "Docker is not installed! :(" + echo "Installing Docker ..." + sudo apt install docker.io || true + sudo usermod -aG docker $USER + newgrp docker +fi + + echo "Checking docker-compose ..." +if [[ $(which docker-compose) && $(docker-compose --version) ]]; then + echo "docker-compose is installed :) ..." + sleep 1 + else + echo "docker-compose is not installed! :(" + echo "Installing Docker ..." + sudo apt install docker-compose || true +fi +} +init(){ + sudo chmod 666 /var/run/docker.sock +} +#============================================================================================================================================================================= +parse_params() { + # default values of variables set from params + + while :; do + case "${1-}" in + -h | --help) usage ;; + -build_docker) buildDockerImage ;; + -build_and_run_docker) runDockerImage ;; + -stop_docker) stopDockerImage ;; + -run_app) runTheApp ;; + -run_test) runTheTests ;; + -run_lint) runtheLint ;; + -generate_doc) generateDoc;; + -deploy_on_kubernetes) runOnKubernetes ;; + -v | --verbose) set -x ;; + --no-color) NO_COLOR=1 ;; + -?*) die "Unknown option: $1" ;; + *) break ;; + esac + shift + done + + args=("$@") + + return 0 +} +#============================================================================================================================================================================= +clear +setup_colors +init +parse_params "$@" +#============================================================================================================================================================================= -- 2.39.5 From 0fae87c4c8f79a7155d40f7b64d90a7446964066 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Thu, 11 Aug 2022 20:02:53 +0300 Subject: [PATCH 02/23] Added Elastic PIT features and tested. --- documentation/classes/EsQueryDto.html | 396 ++++++++++++++++++ documentation/classes/SearchQueryDto.html | 100 +++++ documentation/classes/SearchResultDto.html | 12 +- .../controllers/PapersController.html | 19 +- documentation/coverage.html | 30 +- documentation/graph/dependencies.svg | 174 ++++---- .../injectables/PageInterceptor.html | 16 +- documentation/injectables/SearchService.html | 241 ++++++++++- documentation/interfaces/PageMeta.html | 50 +++ documentation/js/menu-wc.js | 37 +- documentation/js/menu-wc_es5.js | 2 +- documentation/js/search/search_index.js | 4 +- documentation/miscellaneous/variables.html | 112 +++-- documentation/modules/AppModule.html | 12 +- .../modules/CommonModule/dependencies.svg | 8 +- documentation/modules/SearchModule.html | 17 +- documentation/overview.html | 2 +- src/core/domain/dtos/es-query.dto.ts | 45 ++ src/core/domain/dtos/search-q.dto.ts | 7 + src/core/services/common/search.service.ts | 49 ++- src/infrastructure/modules/app.module.ts | 1 - src/infrastructure/modules/search.module.ts | 5 +- src/test/search.service.spec.ts | 103 +++++ 23 files changed, 1230 insertions(+), 212 deletions(-) create mode 100644 documentation/classes/EsQueryDto.html create mode 100644 src/core/domain/dtos/es-query.dto.ts create mode 100644 src/test/search.service.spec.ts diff --git a/documentation/classes/EsQueryDto.html b/documentation/classes/EsQueryDto.html new file mode 100644 index 0000000..1a26a78 --- /dev/null +++ b/documentation/classes/EsQueryDto.html @@ -0,0 +1,396 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + +
+
+

+

File

+

+

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

+ + +

+

Description

+

+

+

Elasticsearch query DTO

+ +

+ + + + +
+

Index

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

+ Properties +

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

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

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

The search query object passed to Elasticsearch

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

Maximum number of elements returned by Elasticsearch

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

Object, that stores

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

results matching ""

+
    +
    +
    +

    No results matching ""

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

    Constructor

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

    Constructs an object with provided parameters

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

    Methods

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

    Deletes the PIT specified by provided ID

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

    , ID of the PIT, that would be deleted

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

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

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

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

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

    Acquires a PIT ID from Elasticsearch, needed for a request

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

    , amount of time in minutes (defaults to 1)

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

    Point-In-Time ID

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

    Number of elements on the page

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

    Total search results

    +

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

    +
    +

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

    List of allowed properties in this DTO

    +
    +
    +

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

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

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

    -
    -

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

    List of allowed properties in this DTO

    -
    -
    -

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

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

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

    +
    +

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

    List of allowed properties in this DTO

    +
    +
    +

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

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

    Controllers

    - -

    Imports

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

    Controllers

    + +

    Exports

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

    -

    6 Classes

    +

    7 Classes

    diff --git a/src/core/domain/dtos/es-query.dto.ts b/src/core/domain/dtos/es-query.dto.ts new file mode 100644 index 0000000..6bac065 --- /dev/null +++ b/src/core/domain/dtos/es-query.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validator"; + +/** + * List of allowed properties in this DTO + */ + const allowedProperties = ['size', 'query', 'pit', 'sort']; + + /** + * Elasticsearch query DTO + */ + export class EsQueryDto { + /** + * Maximum number of elements returned by Elasticsearch + */ + @IsDefined() + @IsNumber() + @IsInt() + @ApiProperty({ + description: 'Maximum number of elements returned by Elasticsearch', + example: 30 + }) + size: number; + + /** + * The search query object passed to Elasticsearch + */ + @IsDefined() + @IsObject() + @ApiProperty({ + description: 'Search query object passed to Elasticsearch', + example: false, + }) + query: object; + + /** + * Object, that stores Point-In-Time ID and time alive + */ + pit: object; + + /** + * Object, that stores + */ + sort: object; + } \ No newline at end of file diff --git a/src/core/domain/dtos/search-q.dto.ts b/src/core/domain/dtos/search-q.dto.ts index 064e02c..28bf733 100644 --- a/src/core/domain/dtos/search-q.dto.ts +++ b/src/core/domain/dtos/search-q.dto.ts @@ -57,6 +57,13 @@ export class SearchQueryDto { }) order: string; + /** + * Constructs an object with provided parameters + * @param query + * @param page + * @param limit + * @param order + */ constructor(query: string, page: number, limit: number, order: string) { this.query = query; this.page = page; diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index 7b78c87..7339362 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -37,7 +37,7 @@ export class SearchService { return new Promise((resolve, reject) => { try { - (this.httpService.get('http://localhost:' + this.ES_PORT + '/_search', { + (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) @@ -70,9 +70,11 @@ export class SearchService { }, } + let pitID = this.getPIT(1); + return new Promise((resolve, reject) => { try { - (this.httpService.get('http://localhost:'+ this.ES_PORT + '/_search', { + (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { data: es_query, headers: {'Content-Type': 'application/json'}, })) @@ -81,7 +83,7 @@ export class SearchService { if (res.timed_out) { reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'})); } - + resolve(new SearchResultDto(200, res.hits)); }); } catch (error) { @@ -89,4 +91,45 @@ export class SearchService { } }); } + + /** + * Acquires a PIT ID from Elasticsearch, needed for a request + * @param alive, amount of time in minutes (defaults to 1) + * @returns Point-In-Time ID + */ + async getPIT(alive: number = 1): Promise { + return new Promise((resolve, reject) => { + try { + (this.httpService.post(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive}m`) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.id); + })); + } catch (error) { + reject(error); + } + }); + } + + /** + * Deletes the PIT specified by provided ID + * @param pitID, ID of the PIT, that would be deleted + * @returns true/false, depending on the result of deletion of the PIT + */ + async deletePIT(pitID: string): Promise { + return new Promise((resolve, reject) => { + try { + this.httpService.delete(`http://localhost:${this.ES_PORT}/papers/_pit`, { + data: { id: pitID }, + headers: { 'Content-Type': 'application/json' }, + }) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.succeeded); + }); + } catch (error) { + reject(error); + } + }) + } } \ No newline at end of file diff --git a/src/infrastructure/modules/app.module.ts b/src/infrastructure/modules/app.module.ts index 931a437..308d14f 100644 --- a/src/infrastructure/modules/app.module.ts +++ b/src/infrastructure/modules/app.module.ts @@ -7,7 +7,6 @@ import { LoggerInterceptor } from '../../core/interceptors' import * as modules from '../../core/modules' import { CommonModule } from './common/common.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; -import { PapersController } from 'src/application/controller/papers.controller'; import { SearchModule } from './search.module'; /** diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index 4c2cc56..bccc4e6 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,6 +1,5 @@ import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; -import { ConfigModule } from "@nestjs/config"; import { PapersController } from "src/application"; import { SearchService } from "../../core/services/common/search.service"; @@ -8,7 +7,9 @@ import { SearchService } from "../../core/services/common/search.service"; * search module */ @Module({ - imports: [HttpModule], + imports: [ + HttpModule, + ], exports: [SearchService], providers: [SearchService], controllers: [PapersController], diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts new file mode 100644 index 0000000..5c6406e --- /dev/null +++ b/src/test/search.service.spec.ts @@ -0,0 +1,103 @@ +import { HttpService } from "@nestjs/axios"; +import { ConfigModule } from "@nestjs/config"; +import { Test } from "@nestjs/testing"; +import { of } from "rxjs"; +import { HttpResponseException } from "src/core/exceptions"; +import { SearchService } from "src/core/services/common/search.service"; + +describe('Unit tests for SearchService', () => { + let searchService: SearchService; + let httpService: HttpService; + + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + SearchService, + { + provide: HttpService, + useValue: { + post: jest.fn(), + }, + }, + ], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + cache: true, + expandVariables: true, + }) + ], + }).compile(); + + searchService = moduleRef.get(SearchService); + httpService = moduleRef.get(HttpService); + }); + + describe('getPIT()', () => { + it('Should touch HttpService.post() method', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + searchService.getPIT(); + expect(postMock).toHaveBeenCalled(); + }); + + it('Should contain correct port in the URI from .env', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + searchService.getPIT(); + expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); + }); + + it('Should touch HttpService with correct URI when keep_alive is set as a parameter', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + let keep_alive = 2; + searchService.getPIT(keep_alive); + expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${keep_alive}m`); + }); + + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'post').mockImplementation(() => { + throw HttpResponseException; + }); + + expect(searchService.getPIT()).rejects.toEqual(HttpResponseException); + }); + + it('Should return a non-empty string when HttpService request succeedes', () => { + jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + expect(searchService.getPIT()).resolves.toBe('2567'); + }); + }); + + describe('deletePIT()', () => { + it.todo('Should fail to delete, because the requested PIT ID is invalid'); + it.todo('Should call HttpService.delete() method with correct body'); + }); +}); \ No newline at end of file -- 2.39.5 From 78de6386450c3f6bb42d666d5fe363b76071d78b Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Tue, 16 Aug 2022 16:42:49 +0300 Subject: [PATCH 03/23] Elasticsearch pagination implementation finished --- documentation/classes/EsHitDto.html | 374 ++++++++++ documentation/classes/EsQueryDto.html | 169 ++++- documentation/classes/EsResponseDto.html | 74 +- documentation/classes/PageDto.html | 19 +- documentation/classes/PaperDto.html | 590 ++++++++++++++++ documentation/classes/PrevSearch.html | 656 ++++++++++++++++++ documentation/classes/RequestDto.html | 390 +++++++++++ documentation/classes/SearchResultDto.html | 17 +- .../controllers/PapersController.html | 22 +- documentation/coverage.html | 152 +++- documentation/graph/dependencies.svg | 156 ++--- .../injectables/PageInterceptor.html | 564 ++++++++++++++- documentation/injectables/SearchService.html | 313 ++------- documentation/interfaces/EqQueryString.html | 350 ++++++++++ documentation/interfaces/EsPit.html | 279 ++++++++ documentation/interfaces/EsQuery.html | 234 +++++++ documentation/interfaces/EsResponseHits.html | 340 +++++++++ documentation/interfaces/SearchInfo.html | 286 ++++++++ documentation/js/menu-wc.js | 39 +- documentation/js/menu-wc_es5.js | 2 +- documentation/js/search/search_index.js | 4 +- documentation/miscellaneous/enumerations.html | 102 ++- documentation/miscellaneous/variables.html | 116 +++- .../modules/CommonModule/dependencies.svg | 8 +- documentation/modules/SearchModule.html | 2 - .../modules/SearchModule/dependencies.svg | 8 +- documentation/overview.html | 4 +- .../controller/papers.controller.ts | 8 +- src/core/domain/dtos/es-hit.dto.ts | 45 ++ src/core/domain/dtos/es-query.dto.ts | 53 +- src/core/domain/dtos/es-response.dto.ts | 18 +- src/core/domain/dtos/page.dto.ts | 7 +- src/core/domain/dtos/paper.dto.ts | 89 +++ src/core/domain/dtos/request.dto.ts | 45 ++ src/core/domain/dtos/search-result.dto.ts | 5 +- src/core/domain/enums/es-time.enum.ts | 12 + src/core/domain/enums/page-order.enum.ts | 7 +- .../domain/interfaces/es-pit.interface.ts | 14 + .../interfaces/es-query-string.interface.ts | 26 + .../domain/interfaces/es-query.interface.ts | 11 + .../interfaces/es-response-hits.interface.ts | 21 + src/core/domain/interfaces/index.ts | 4 +- .../interfaces/search-info.interface.ts | 17 + src/core/interceptors/page.interceptor.ts | 183 ++++- src/core/services/common/search.service.ts | 125 ++-- src/test/page.interceptor.spec.ts | 192 ++--- src/test/search.service.spec.ts | 185 ++--- 47 files changed, 5615 insertions(+), 722 deletions(-) create mode 100644 documentation/classes/EsHitDto.html create mode 100644 documentation/classes/PaperDto.html create mode 100644 documentation/classes/PrevSearch.html create mode 100644 documentation/classes/RequestDto.html create mode 100644 documentation/interfaces/EqQueryString.html create mode 100644 documentation/interfaces/EsPit.html create mode 100644 documentation/interfaces/EsQuery.html create mode 100644 documentation/interfaces/EsResponseHits.html create mode 100644 documentation/interfaces/SearchInfo.html create mode 100644 src/core/domain/dtos/es-hit.dto.ts create mode 100644 src/core/domain/dtos/paper.dto.ts create mode 100644 src/core/domain/dtos/request.dto.ts create mode 100644 src/core/domain/enums/es-time.enum.ts create mode 100644 src/core/domain/interfaces/es-pit.interface.ts create mode 100644 src/core/domain/interfaces/es-query-string.interface.ts create mode 100644 src/core/domain/interfaces/es-query.interface.ts create mode 100644 src/core/domain/interfaces/es-response-hits.interface.ts create mode 100644 src/core/domain/interfaces/search-info.interface.ts diff --git a/documentation/classes/EsHitDto.html b/documentation/classes/EsHitDto.html new file mode 100644 index 0000000..bc158e2 --- /dev/null +++ b/documentation/classes/EsHitDto.html @@ -0,0 +1,374 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + +
    +
    +

    +

    File

    +

    +

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

    + + +

    +

    Description

    +

    +

    +

    Structure of the document stored and retrieved from Elasticsearch

    + +

    + + + + +
    +

    Index

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

    + Properties +

    + + + + + + + + + + + + + + + + + + + + +
    + + + Optional + _score + + +
    + Type : number + +
    + Decorators : +
    + + @IsOptional()
    @ApiProperty({description: 'Relevance score', example: 1.2355})
    +
    +
    + +
    +

    Hit relevance score

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + + _source + + +
    + Type : PaperDto + +
    + Decorators : +
    + + @IsNotEmpty()
    @ApiProperty({description: 'Actual document (paper) stored in Elasticsearch', example: undefined})
    +
    +
    + +
    +

    Actual document stored in Elasticsearch

    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    + + + Optional + sort + + +
    + Type : [] + +
    + Decorators : +
    + + @IsOptional()
    @ApiProperty({description: 'List of objects that represents how the hit was sorted', example: undefined})
    +
    +
    + +
    +

    List of objects that represents how the hit was sorted

    +
    +
    +
    + + + + + + + + + + +
    +
    import { ApiProperty } from "@nestjs/swagger";
    +import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
    +import { PaperDto } from "./paper.dto";
    +
    +/**
    + * List of allowed properties in this DTO
    + */
    +const allowedProperties = ['sort', '_source', '_score'];
    +
    +/**
    + * Structure of the document stored and retrieved from Elasticsearch
    + */
    +export class EsHitDto {
    +    /**
    +     * Actual document stored in Elasticsearch
    +     */
    +    @IsNotEmpty()
    +    @ApiProperty({
    +        description: 'Actual document (paper) stored in Elasticsearch',
    +        example: {
    +            id: 'sssss'
    +        }
    +    })
    +    _source: PaperDto;
    +    
    +    /**
    +     * List of objects that represents how the hit was sorted
    +     */
    +    @IsOptional()
    +    @ApiProperty({
    +        description: 'List of objects that represents how the hit was sorted',
    +        example: {}
    +    })
    +    sort?: [];
    +
    +    /**
    +     * Hit relevance score
    +     */
    +    @IsOptional()
    +    @ApiProperty({
    +        description: 'Relevance score',
    +        example: 1.2355
    +    })
    +    _score?: number;
    +}
    +
    + + + + + + + + + + +
    +
    +

    results matching ""

    +
      +
      +
      +

      No results matching ""

      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/EsQueryDto.html b/documentation/classes/EsQueryDto.html index 1a26a78..60b06b3 100644 --- a/documentation/classes/EsQueryDto.html +++ b/documentation/classes/EsQueryDto.html @@ -91,15 +91,22 @@ @@ -115,6 +122,30 @@ +
      +

      Constructor

      + + + + + + + + + + + + + +
      +constructor() +
      + +
      +

      Constructs an empty object

      +
      +
      +
      @@ -127,6 +158,7 @@ + Optional pit @@ -134,19 +166,28 @@ - Type : object + Type : EsPit + + + + Decorators : +
      + + @IsOptional()
      @IsObject()
      @ApiProperty({description: 'PIT object', example: undefined})
      +
      + - + -

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

      +

      Object, that stores PIT ID and time alive

      @@ -166,7 +207,7 @@ - Type : object + Type : EsQuery @@ -175,13 +216,13 @@ Decorators :
      - @IsDefined()
      @IsObject()
      @ApiProperty({description: 'Search query object passed to Elasticsearch', example: false})
      + @IsDefined()
      @IsObject()
      @ApiProperty({description: 'Search query object passed to Elasticsearch', example: undefined})
      - + @@ -194,12 +235,55 @@ + + + + + + + + + + + + + + + + + + + + +
      + + + Optional + search_after + + +
      + Type : [] + +
      + Decorators : +
      + + @IsOptional()
      @IsArray()
      @ApiProperty({description: '', example: undefined})
      +
      +
      + +
      +

      Pagination info

      +
      +
      @@ -241,6 +325,7 @@ + + + @@ -280,7 +374,9 @@
      import { ApiProperty } from "@nestjs/swagger";
      -import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validator";
      +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"
       
       /**
        * List of allowed properties in this DTO
      @@ -294,6 +390,7 @@ import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-val
            /**
             * Maximum number of elements returned by Elasticsearch
             */
      +     @IsOptional()
            @IsDefined()
            @IsNumber()
            @IsInt()
      @@ -301,7 +398,7 @@ import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-val
                description: 'Maximum number of elements returned by Elasticsearch',
                example: 30
            })
      -     size: number;
      +     size?: number;
            
            /**
             * The search query object passed to Elasticsearch
      @@ -310,19 +407,53 @@ import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-val
            @IsObject()
            @ApiProperty({
                description: 'Search query object passed to Elasticsearch',
      -         example: false,
      +         example: {},
            })
      -     query: object;
      +     query: EsQuery;
       
            /**
      -      * Object, that stores Point-In-Time ID and time alive
      +      * Object, that stores PIT ID and time alive
             */
      -     pit: object;
      +     @IsOptional()
      +     @IsObject()
      +     @ApiProperty({
      +        description: 'PIT object',
      +        example: {}
      +     })
      +     pit?: EsPit;
       
            /**
      -      * Object, that stores
      +      * Sorting info
             */
      -     sort: object;
      +     @IsOptional()
      +     @IsArray()
      +     @ApiProperty({
      +        description: '',
      +        example: []
      +     })
      +     sort?: unknown[];
      +
      +     /**
      +      * Pagination info
      +      */
      +     @IsOptional()
      +     @IsArray()
      +     @ApiProperty({
      +        description: '',
      +        example: []
      +     })
      +     search_after?: unknown[];
      +
      +     /**
      +      * Constructs an empty object
      +      */
      +     constructor() {
      +        this.size = 10;
      +        this.query = undefined;
      +        this.pit = undefined;
      +        this.sort = undefined;
      +        this.search_after = undefined;
      +     }
        }
      diff --git a/documentation/classes/EsResponseDto.html b/documentation/classes/EsResponseDto.html index 6fcb809..b147225 100644 --- a/documentation/classes/EsResponseDto.html +++ b/documentation/classes/EsResponseDto.html @@ -96,6 +96,10 @@
    • hits
    • +
    • + Optional + pit_id +
    • timed_out
    • @@ -149,7 +153,7 @@ @@ -176,7 +180,7 @@ used for the request

      @@ -191,7 +195,7 @@ used for the request

      @@ -204,6 +208,48 @@ used for the request

      + Optional size @@ -216,13 +300,13 @@ Decorators :
      - @IsDefined()
      @IsNumber()
      @IsInt()
      @ApiProperty({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
      + @IsOptional()
      @IsDefined()
      @IsNumber()
      @IsInt()
      @ApiProperty({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
      - +
      + Optional sort @@ -248,19 +333,28 @@
      - Type : object + Type : []
      + Decorators : +
      + + @IsOptional()
      @IsArray()
      @ApiProperty({description: '', example: undefined})
      +
      +
      - +
      -

      Object, that stores

      +

      Sorting info

      - +
      - Type : object + Type : EsResponseHits
      - +
      + + + + + + + + + + + + + + + + + + + + +
      + + + Optional + pit_id + + +
      + Type : string + +
      + Decorators : +
      + + @IsString()
      @IsOptional()
      @ApiProperty({description: 'PIT ID used to search for results', example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='})
      +
      +
      + +
      +

      ID of the PIT used in the search

      +
      +
      @@ -232,7 +278,7 @@ used for the request

      @@ -274,7 +320,7 @@ If 'true' - the request timed out before completion

      @@ -301,12 +347,13 @@ took Elasticsearch to execute the request

      import { ApiProperty } from "@nestjs/swagger";
      -import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator";
      +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator";
      +import { EsResponseHits } from "../interfaces/es-response-hits.interface";
       
       /**
        * List of allowed properties in this DTO
        */
      -const allowedProperties = ['took', 'timed_out', '_shards', 'hits'];
      +const allowedProperties = ['took', 'timed_out', '_shards', 'hits', 'pit_id'];
       
       /**
        * Elasticsearch response DTO
      @@ -381,7 +428,18 @@ export class EsResponseDto {
                   }],
               }
           })
      -    hits: object;
      +    hits: EsResponseHits;
      +
      +    /**
      +     * ID of the PIT used in the search
      +     */
      +    @IsString()
      +    @IsOptional()
      +    @ApiProperty({
      +        description: '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 116c267..48a5b98 100644 --- a/documentation/classes/PageDto.html +++ b/documentation/classes/PageDto.html @@ -117,12 +117,12 @@ @@ -145,7 +145,7 @@ @@ -208,7 +208,7 @@ @@ -250,7 +250,7 @@ @@ -278,6 +278,7 @@
      import { ApiProperty } from "@nestjs/swagger";
       import { IsArray } from "class-validator";
       import { PageMeta } from "../interfaces/page-meta.interface";
      +import { PaperDto } from "./paper.dto";
       
       /**
        * List of allowed properties in this DTO
      @@ -287,7 +288,7 @@ const allowedProperties = ['data', 'meta'];
       /**
        * Page model for pagination
        */
      -export class PageDto<T> {
      +export class PageDto {
           /**
            * Data block of the page
            */
      @@ -296,7 +297,7 @@ export class PageDto<T> {
               description: 'All data the page contains',
               isArray: true,
           })
      -    readonly data: T[];
      +    readonly data: PaperDto[];
       
           /**
            * Metadata of the page
      @@ -312,7 +313,7 @@ export class PageDto<T> {
            * @param data 
            * @param meta 
            */
      -    constructor(data: T[], meta: PageMeta) {
      +    constructor(data: PaperDto[], meta: PageMeta) {
               this.data = data;
               this.meta = meta;
           }
      diff --git a/documentation/classes/PaperDto.html b/documentation/classes/PaperDto.html
      new file mode 100644
      index 0000000..0cde5ce
      --- /dev/null
      +++ b/documentation/classes/PaperDto.html
      @@ -0,0 +1,590 @@
      +
      +
      +    
      +        
      +        
      +        hometask documentation
      +        
      +        
      +
      +        
      +	   
      +        
      +        
      +    
      +    
      +
      +        
      +
      +        
      +
      +        
      +
      + + +
      +
      + + + + + + + + + + + + + + + + +
      +
      +

      +

      File

      +

      +

      + src/core/domain/dtos/paper.dto.ts +

      + + +

      +

      Description

      +

      +

      +

      Structure of the document stored and retrieved from Elasticsearch

      + +

      + + + + +
      +

      Index

      +
      - +
      - +
      -constructor(data: T[], meta: PageMeta) +constructor(data: PaperDto[], meta: PageMeta)
      - +
      data - T[] + PaperDto[] @@ -193,7 +193,7 @@
      - Type : T[] + Type : PaperDto[]
      - +
      - +
      + + + + + + + + + + + + + + +
      +
      Properties
      +
      + +
      +
      + + +
      + +

      + Properties +

      + + + + + + + + + + + + + + + + + + + + +
      + + + authors + + +
      + Type : string[] + +
      + Decorators : +
      + + @IsNotEmpty()
      @IsArray()
      @ApiProperty({description: 'List of authors of the paper', example: undefined})
      +
      +
      + +
      +

      List of authors of the paper

      +
      +
      + + + + + + + + + + + + + + + + + + + + +
      + + + content + + +
      + Type : string + +
      + Decorators : +
      + + @ApiProperty({description: 'Contents of the paper presented in Markdown (.md) format', example: '...'})
      +
      +
      + +
      +

      Contents of the paper [Markdown]

      +
      +
      + + + + + + + + + + + + + + + + + + + + +
      + + + id + + +
      + Type : string + +
      + Decorators : +
      + + @IsNotEmpty()
      @IsString()
      @ApiProperty({description: 'Unique ID of the paper', example: 'cc3c3cca-f763-495c-8dfa-69c45ca738ff'})
      +
      +
      + +
      +

      Unique ID of the paper

      +
      +
      + + + + + + + + + + + + + + + + + + + + +
      + + + summary + + +
      + Type : string + +
      + Decorators : +
      + + @IsNotEmpty()
      @IsString()
      @ApiProperty({description: 'Summary of the paper. May be a short excerpt from the main text', example: 'S-algol (St Andrews Algol):vii is a computer programming language derivative of ALGOL 60 developed at the University of St Andrews in 1979 by Ron Morrison and Tony Davie'})
      +
      +
      + +
      +

      Summary of the paper. May be a short excerpt from the main text.

      +
      +
      + + + + + + + + + + + + + + + + + + + + +
      + + + tags + + +
      + Type : string[] + +
      + Decorators : +
      + + @IsNotEmpty()
      @IsArray()
      @ApiProperty({description: 'List of tags, that show the certain topics/fields of knowledge paper is touching', example: undefined})
      +
      +
      + +
      +

      List of tags, that show the certain topics/fields of knowledge paper is touching

      +
      +
      + + + + + + + + + + + + + + + + + + + + +
      + + + title + + +
      + Type : string + +
      + Decorators : +
      + + @IsNotEmpty()
      @IsString()
      @ApiProperty({description: 'Title of the paper', example: 'Mucosal associated invariant T cell'})
      +
      +
      + +
      +

      Title of the paper

      +
      +
      + + + + + + + + + + + + + + + + + + + + +
      + + + topic + + +
      + Type : string + +
      + Decorators : +
      + + @IsNotEmpty()
      @IsString()
      @ApiProperty({description: 'Topic of the paper', example: 'Physics'})
      +
      +
      + +
      +

      Topic of the paper

      +
      +
      +
      + + + + + + + + + + +
      +
      import { 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
      + */
      +const allowedProperties = ['id', 'title', 'authors', 'topic', 'summary', 'tags', 'content'];
      +
      +/**
      + * Structure of the document stored and retrieved from Elasticsearch
      + */
      +export class PaperDto {
      +    /**
      +     * Unique ID of the paper
      +     */
      +    @IsNotEmpty()
      +    @IsString()
      +    @ApiProperty({
      +        description: 'Unique ID of the paper',
      +        example: 'cc3c3cca-f763-495c-8dfa-69c45ca738ff'
      +    })
      +    id: string;
      +    
      +    /**
      +     * Title of the paper
      +     */
      +    @IsNotEmpty()
      +    @IsString()
      +    @ApiProperty({
      +        description: 'Title of the paper',
      +        example: 'Mucosal associated invariant T cell',
      +    })
      +    title: string;
      +
      +    /**
      +     * List of authors of the paper
      +     */
      +    @IsNotEmpty()
      +    @IsArray()
      +    @ApiProperty({
      +        description: 'List of authors of the paper',
      +        example: ['Daniil Mikhaylov', 'Denis Gorbunov', 'Maxim Ten']
      +    })
      +    authors: string[];
      +
      +    /**
      +     * Topic of the paper
      +     */
      +    @IsNotEmpty()
      +    @IsString()
      +    @ApiProperty({
      +        description: 'Topic of the paper',
      +        example: 'Physics'
      +    })
      +    topic: string;
      +
      +    /**
      +     * Summary of the paper. May be a short excerpt from the main text.
      +     */
      +    @IsNotEmpty()
      +    @IsString()
      +    @ApiProperty({
      +        description: 'Summary of the paper. May be a short excerpt from the main text',
      +        example: 'S-algol (St Andrews Algol):vii is a computer programming language derivative of ALGOL 60 developed at the University of St Andrews in 1979 by Ron Morrison and Tony Davie'
      +    })
      +    summary: string;
      +
      +    /**
      +     * List of tags, that show the certain topics/fields of knowledge paper is touching
      +     */
      +    @IsNotEmpty()
      +    @IsArray()
      +    @ApiProperty({
      +        description: 'List of tags, that show the certain topics/fields of knowledge paper is touching',
      +        example: ['Neurobiology', 'Neuron structure', 'Neuroimaging']
      +    })
      +    tags: string[];
      +
      +    /**
      +     * Contents of the paper [Markdown]
      +     */
      +    @ApiProperty({
      +        description: 'Contents of the paper presented in Markdown (.md) format',
      +        example: '...'
      +    })
      +    content: string;
      +}
      +
      + + + + + + + + + + +
      +
      +

      results matching ""

      +
        +
        +
        +

        No results matching ""

        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/PrevSearch.html b/documentation/classes/PrevSearch.html new file mode 100644 index 0000000..13b2553 --- /dev/null +++ b/documentation/classes/PrevSearch.html @@ -0,0 +1,656 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
        +
        + + +
        +
        + + + + + + + + + + + + + + + + +
        +
        +

        +

        File

        +

        +

        + src/core/interceptors/page.interceptor.ts +

        + + +

        +

        Description

        +

        +

        +

        Previous search data storage

        + +

        + + +

        +

        Implements

        +

        +

        + SearchInfo +

        + + +
        +

        Index

        + + + + + + + + + + + + + + + + + + + + + +
        +
        Properties
        +
        + +
        +
        Methods
        +
        + +
        +
        + +
        +

        Constructor

        + + + + + + + + + + + + + +
        +constructor() +
        + +
        +

        Constructs an uninitialized object

        +
        +
        +
        + +
        + +

        + Properties +

        + + + + + + + + + + + + + + + + + +
        + + + pit + + +
        + Type : EsPit + +
        + +
        +

        PIT object of the previous search

        +
        +
        + + + + + + + + + + + + + + + + + +
        + + + prevPage + + +
        + Type : number + +
        + +
        +

        Number of the previous page

        +
        +
        + + + + + + + + + + + + + + + + + +
        + + + tiebreaker + + +
        + Type : [] + +
        + +
        +

        Tiebreaker and sort parameters

        +
        +
        +
        + +
        + +

        + Methods +

        + + + + + + + + + + + + + + + + + + + +
        + + + Public + isSet + + +
        + + isSet() +
        + +
        +

        Checks if there was the search before current one

        +
        + +
        +
        +
        + Returns : boolean + +
        +
        +

        true/false, showing whether or not there was another search before

        + +
        +
        +
        + + + + + +
        + + +
        +
        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 { 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";
        +
        +/**
        + * Previous search data storage
        + */
        +class PrevSearch implements SearchInfo {
        +    /**
        +     * Constructs an uninitialized object
        +     */
        +    constructor() {
        +        this.pit = undefined;
        +        this.tiebreaker = undefined;
        +        this.prevPage = -1;
        +    }
        +
        +    /**
        +     * PIT object of the previous search
        +     */
        +    pit: EsPit;
        +
        +    /**
        +     * Tiebreaker and sort parameters
        +     */
        +    tiebreaker: unknown[];
        +
        +    /**
        +     * Number of the previous page
        +     */
        +    prevPage: number;
        +
        +    /**
        +     * Checks if there was the search before current one
        +     * @returns true/false, showing whether or not there was another search before
        +     */
        +    public isSet(): boolean {
        +        if (this.pit && this.tiebreaker && this.prevPage !== -1) return true;
        +        return false;
        +    }
        +}
        +
        +/**
        + * Pagination-implementing interceptor
        + */
        +@Injectable()
        +export class PageInterceptor implements NestInterceptor {
        +    /**
        +     * Injects needed dependencies and instantiates the storage object
        +     * @param httpService 
        +     * @param searchService 
        +     */
        +    constructor(private readonly httpService: HttpService) {
        +        this.prevSearch = new PrevSearch;
        +    }
        +
        +    /**
        +     * Override of intercept() method, specified in NestInterceptor interface
        +     * @param context 
        +     * @param next 
        +     * @returns Page with content and metadata
        +     */
        +    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<PageDto>> {
        +        let request: RequestDto = context.switchToHttp().getRequest<RequestDto>();
        +        const query: SearchQueryDto = request.query;
        +        let reverse: boolean = false;
        +
        +        request.es_query = new EsQueryDto();
        +
        +        request.es_query.query = {
        +            query_string: {
        +                query: query.query,
        +                default_field: 'content',
        +            }
        +        };
        +        request.es_query.sort = [
        +            { _score: { order: !query?.order ? Order.DESC : query.order } },
        +            { _shard_doc: 'desc' }
        +        ];
        +
        +        if (this.prevSearch.isSet()) {
        +            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);
        +            
        +            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 {
        +            this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
        +            request.es_query.size = !query?.limit ? 10 : query.limit;
        +        }
        +
        +        return next.handle().pipe(
        +            map((res) => {
        +                // Setting the page meta-data
        +                let meta: PageMeta = {
        +                    total: res.hits.total.value,
        +                    pagenum: !query?.page ? 1 : query.page,
        +                    order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC,
        +                    hasNext: false,
        +                    hasPrev: false,
        +                    pagesize: !query?.limit ? 10 : query.limit,
        +                }; 
        +                // meta.hasNext = res.hits.hits[meta.pagenum * meta.pagesize] ? true : false;
        +                // meta.hasPrev = res.hits.hits[(meta.pagenum - 1) * meta.pagesize - 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;
        +
        +                let data = res.hits.hits.slice(-meta.pagesize);
        +                if (reverse) {
        +                    console.log('REVERSE');
        +                    this.prevSearch.tiebreaker = data[0].sort;
        +                    data.reverse();
        +                    reverse = false;
        +                }
        +
        +                // Return the page
        +                return new PageDto(data, meta);
        +            })
        +        );
        +    }
        +
        +    /**
        +     * Elastichsearch server port-number
        +     */
        +    private readonly ES_PORT = process.env.ES_PORT;
        +
        +    /**
        +     * Info about previously completed search
        +     */
        +    private prevSearch: PrevSearch;
        +
        +    /**
        +     * Acquires a PIT ID from Elasticsearch, needed for a request
        +     * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes.
        +     * @returns PIT object <EsPit> containing PIT ID and keep_alive value
        +     */
        +     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}`)
        +                    .pipe(take(1), map(axiosRes => axiosRes.data))
        +                    .subscribe((res) => {
        +                        res.keep_alive = alive + unit;
        +                        resolve(res);
        +                    }));
        +            } catch (error) {
        +                reject(error);
        +            }
        +        });
        +    }
        +
        +    /**
        +     * Deletes the PIT specified by provided ID
        +     * @param pitID, ID of the PIT, that would be deleted
        +     * @returns true/false, depending on the result of deletion of the PIT
        +     */
        +     async deletePIT(pitID: string): Promise<boolean> {
        +        return new Promise((resolve, reject) => {
        +            try {
        +                this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, {
        +                    data: { id: pitID },
        +                    headers: { 'Content-Type': 'application/json' },
        +                })
        +                .pipe(take(1), map(axiosRes => axiosRes.data))
        +                .subscribe((res) => {
        +                    resolve(res.succeeded);
        +                });
        +            } catch (error) {
        +                reject(error);
        +            }
        +        })
        +    }
        +}
        +/*
        +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;
        +        //     }
        +        // }
        +
        +
        +
        + + + + + + + + + +
        +
        +

        results matching ""

        +
          +
          +
          +

          No results matching ""

          +
          +
          +
          + +
          +
          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/RequestDto.html b/documentation/classes/RequestDto.html new file mode 100644 index 0000000..0ea9fc6 --- /dev/null +++ b/documentation/classes/RequestDto.html @@ -0,0 +1,390 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
          +
          + + +
          +
          + + + + + + + + + + + + + + + + +
          +
          +

          +

          File

          +

          +

          + src/core/domain/dtos/request.dto.ts +

          + + +

          +

          Description

          +

          +

          +

          Request object, which contains query parameters and Elasticsearch query object

          + +

          + + + + +
          +

          Index

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

          Constructor

          + + + + + + + + + + + + + +
          +constructor(query: SearchQueryDto, es_query: EsQueryDto) +
          + +
          +

          Constructs an object with provided parameters

          +
          +
          + Parameters : + + + + + + + + + + + + + + + + + + + + + + + + + + +
          NameTypeOptional
          query + SearchQueryDto + + No +
          es_query + EsQueryDto + + No +
          +
          +
          +
          + +
          + +

          + Properties +

          + + + + + + + + + + + + + + + + + + + + +
          + + + Optional + es_query + + +
          + Type : EsQueryDto + +
          + Decorators : +
          + + @IsOptional()
          @ApiProperty({description: '', example: undefined})
          +
          +
          + +
          +

          Elasticsearch query object

          +
          +
          + + + + + + + + + + + + + + + + + + + + +
          + + + query + + +
          + Type : SearchQueryDto + +
          + Decorators : +
          + + @IsDefined()
          @IsNotEmpty()
          @ApiProperty({description: '', example: undefined})
          +
          +
          + +
          +

          Query parameters object

          +
          +
          +
          + + + + + + + +
          + + +
          +
          import { ApiProperty } from "@nestjs/swagger";
          +import { 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
          + */
          +const allowedProperties = ['query', 'es_query'];
          +
          +/**
          + * Request object, which contains query parameters and Elasticsearch query object
          + */
          +export class RequestDto {
          +    /**
          +     * Query parameters object
          +     */
          +    @IsDefined()
          +    @IsNotEmpty()
          +    @ApiProperty({
          +        description: '',
          +        example: {}
          +    })
          +    query: SearchQueryDto;
          +    
          +    /**
          +     * Elasticsearch query object
          +     */
          +    @IsOptional()
          +    @ApiProperty({
          +        description: '',
          +        example: {},
          +    })
          +    es_query?: EsQueryDto;
          +
          +     /**
          +      * Constructs an object with provided parameters
          +      * @param query
          +      * @param es_query
          +      */
          +     constructor(query: SearchQueryDto, es_query: EsQueryDto) {
          +        this.query = query;
          +        this.es_query = es_query;
          +     }
          +}
          +
          +
          + + + + + + + + + +
          +
          +

          results matching ""

          +
            +
            +
            +

            No results matching ""

            +
            +
            +
            + +
            +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/SearchResultDto.html b/documentation/classes/SearchResultDto.html index 6797141..7212a1c 100644 --- a/documentation/classes/SearchResultDto.html +++ b/documentation/classes/SearchResultDto.html @@ -115,12 +115,12 @@ -constructor(code: number, data: object) +constructor(code: number, data: EsResponseDto) - + @@ -155,7 +155,7 @@ data - object + EsResponseDto @@ -190,7 +190,7 @@ - Type : object + Type : EsResponseDto @@ -205,7 +205,7 @@ - + @@ -246,7 +246,7 @@ - + @@ -273,6 +273,7 @@
            import { ApiProperty } from "@nestjs/swagger";
             import { IsArray, IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
            +import { EsResponseDto } from "./es-response.dto";
             
             /**
              * List of allowed properties in this DTO
            @@ -307,14 +308,14 @@ export class SearchResultDto {
                         
                     },
                 })
            -    data: object;
            +    data: EsResponseDto;
             
                 /**
                  * Constructs an object with provided parameters
                  * @param code 
                  * @param data 
                  */
            -    constructor(code: number, data: object) {
            +    constructor(code: number, data: EsResponseDto) {
                     this.statusCode = code;
                     this.data = data;
                 }
            diff --git a/documentation/controllers/PapersController.html b/documentation/controllers/PapersController.html
            index ed5a6ff..0657b0d 100644
            --- a/documentation/controllers/PapersController.html
            +++ b/documentation/controllers/PapersController.html
            @@ -127,7 +127,7 @@
                         
                         
                             
            -getByContext(query)
            +getByContext(query: RequestDto)
                             
                         
             
            @@ -141,8 +141,8 @@
             
                         
                             
            -                    
            +                    
                             
                         
             
            @@ -159,12 +159,16 @@
                                         
                                             
                                                 Name
            +                                    Type
                                                 Optional
                                             
                                         
                                         
                                             
                                                 query
            +                                    
            +                                                RequestDto
            +                                    
             
                                                 
                                                     No
            @@ -216,8 +220,8 @@
             
                         
                             
            -                    
            +                    
                             
                         
             
            @@ -274,12 +278,10 @@
                 
            import { Controller, Get, HttpCode, HttpException, Next, Param, ParseUUIDPipe, Put, Query, Req, Res, UseInterceptors } from "@nestjs/common";
             import { SearchService } from "../../core/services/common/search.service";
            -import { Response } from "express";
             import { PageInterceptor } from "src/core/interceptors/page.interceptor";
            -import { LoggerInterceptor } from "src/core/interceptors";
             import { SearchResultDto } from "src/core/domain/dtos/search-result.dto";
             import { ApiOperation, ApiResponse } from "@nestjs/swagger";
            -import { SearchQueryDto } from "src/core/domain/dtos";
            +import { RequestDto } from "src/core/domain/dtos/request.dto";
             
             /**
              * /papers/ route controller
            @@ -303,8 +305,8 @@ export class PapersController {
                 @Get('search')
                 @UseInterceptors(PageInterceptor)
                 @HttpCode(200)
            -    getByContext(@Query() query): object {
            -        return this.searchService.findByContext(query.query).then(
            +    getByContext(@Req() query: RequestDto): object {
            +        return this.searchService.findByContext(query.es_query).then(
                         (response: SearchResultDto) => {
                             return response.data;
                         },
            diff --git a/documentation/coverage.html b/documentation/coverage.html
            index 6e9bc69..c1e0b91 100644
            --- a/documentation/coverage.html
            +++ b/documentation/coverage.html
            @@ -140,6 +140,30 @@
                             (1/1)
                         
                     
            +        
            +            
            +                
            +                src/core/domain/dtos/es-hit.dto.ts
            +            
            +            class
            +            EsHitDto
            +            
            +                100 %
            +                (4/4)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/dtos/es-hit.dto.ts
            +            
            +            variable
            +            allowedProperties
            +            
            +                100 %
            +                (1/1)
            +            
            +        
                     
                         
                             
            @@ -149,7 +173,7 @@
                         EsQueryDto
                         
                             100 %
            -                (5/5)
            +                (7/7)
                         
                     
                     
            @@ -173,7 +197,7 @@
                         EsResponseDto
                         
                             100 %
            -                (5/5)
            +                (6/6)
                         
                     
                     
            @@ -212,6 +236,54 @@
                             (1/1)
                         
                     
            +        
            +            
            +                
            +                src/core/domain/dtos/paper.dto.ts
            +            
            +            class
            +            PaperDto
            +            
            +                100 %
            +                (8/8)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/dtos/paper.dto.ts
            +            
            +            variable
            +            allowedProperties
            +            
            +                100 %
            +                (1/1)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/dtos/request.dto.ts
            +            
            +            class
            +            RequestDto
            +            
            +                100 %
            +                (4/4)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/dtos/request.dto.ts
            +            
            +            variable
            +            allowedProperties
            +            
            +                100 %
            +                (1/1)
            +            
            +        
                     
                         
                             
            @@ -260,6 +332,54 @@
                             (1/1)
                         
                     
            +        
            +            
            +                
            +                src/core/domain/interfaces/es-pit.interface.ts
            +            
            +            interface
            +            EsPit
            +            
            +                100 %
            +                (3/3)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/interfaces/es-query-string.interface.ts
            +            
            +            interface
            +            EqQueryString
            +            
            +                100 %
            +                (4/4)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/interfaces/es-query.interface.ts
            +            
            +            interface
            +            EsQuery
            +            
            +                100 %
            +                (2/2)
            +            
            +        
            +        
            +            
            +                
            +                src/core/domain/interfaces/es-response-hits.interface.ts
            +            
            +            interface
            +            EsResponseHits
            +            
            +                100 %
            +                (4/4)
            +            
            +        
                     
                         
                             
            @@ -284,6 +404,18 @@
                             (7/7)
                         
                     
            +        
            +            
            +                
            +                src/core/domain/interfaces/search-info.interface.ts
            +            
            +            interface
            +            SearchInfo
            +            
            +                100 %
            +                (3/3)
            +            
            +        
                     
                         
                             
            @@ -392,6 +524,18 @@
                             (4/4)
                         
                     
            +        
            +            
            +                
            +                src/core/interceptors/page.interceptor.ts
            +            
            +            class
            +            PrevSearch
            +            
            +                100 %
            +                (6/6)
            +            
            +        
                     
                         
                             
            @@ -401,7 +545,7 @@
                         PageInterceptor
                         
                             100 %
            -                (2/2)
            +                (7/7)
                         
                     
                     
            @@ -449,7 +593,7 @@
                         SearchService
                         
                             100 %
            -                (7/7)
            +                (5/5)
                         
                     
                     
            diff --git a/documentation/graph/dependencies.svg b/documentation/graph/dependencies.svg
            index 7660ee6..d77ef52 100644
            --- a/documentation/graph/dependencies.svg
            +++ b/documentation/graph/dependencies.svg
            @@ -4,217 +4,217 @@
             
             
            -
            -
            +
            +
             dependencies
            -
            -dependencies
            +
            +dependencies
             
             cluster_AppModule
            -
            +
             
             
             cluster_AppModule_imports
            -
            -
            -
            -cluster_HttpResponseModule
            -
            -
            -
            -cluster_HttpResponseModule_exports
            -
            -
            -
            -cluster_HttpResponseModule_providers
            -
            +
             
             
             cluster_CommonModule
            -
            +
             
             
             cluster_CommonModule_imports
            -
            +
             
             
             cluster_CommonModule_exports
            -
            +
            +
            +
            +cluster_HttpResponseModule
            +
            +
            +
            +cluster_HttpResponseModule_exports
            +
            +
            +
            +cluster_HttpResponseModule_providers
            +
             
             
             cluster_LoggerModule
            -
            +
             
             
             cluster_LoggerModule_exports
            -
            +
             
             
             cluster_LoggerModule_providers
            -
            +
             
             
             cluster_SearchModule
            -
            +
             
             
             cluster_SearchModule_exports
            -
            +
             
             
             cluster_SearchModule_providers
            -
            +
             
             
             
             CommonModule
            -
            -CommonModule
            +
            +CommonModule
             
             
             
             AppModule
            -
            -AppModule
            +
            +AppModule
             
             
             
             CommonModule->AppModule
            -
            -
            +
            +
             
             
             
             HttpResponseModule 
            -
            -HttpResponseModule 
            +
            +HttpResponseModule 
             
             
             
             CommonModule->HttpResponseModule 
            -
            -
            +
            +
             
             
             
             LoggerModule 
            -
            -LoggerModule 
            +
            +LoggerModule 
             
             
             
             CommonModule->LoggerModule 
            -
            -
            +
            +
             
             
             
             SearchModule
            -
            -SearchModule
            +
            +SearchModule
             
             
             
             SearchModule->AppModule
            -
            -
            +
            +
             
             
             
             SearchService 
            -
            -SearchService 
            +
            +SearchService 
             
             
             
             SearchModule->SearchService 
            -
            -
            +
            +
             
             
             
             HttpResponseModule
            -
            -HttpResponseModule
            +
            +HttpResponseModule
             
             
             
             HttpResponseModule->CommonModule
            -
            -
            +
            +
             
             
             
             HttpResponseService 
            -
            -HttpResponseService 
            +
            +HttpResponseService 
             
             
             
             HttpResponseModule->HttpResponseService 
            -
            -
            +
            +
             
             
             
             LoggerModule
            -
            -LoggerModule
            +
            +LoggerModule
             
             
             
             LoggerModule->CommonModule
            -
            -
            +
            +
             
             
             
             LoggerService 
            -
            -LoggerService 
            +
            +LoggerService 
             
             
             
             LoggerModule->LoggerService 
            -
            -
            +
            +
             
             
             
             HttpResponseService
            -
            -HttpResponseService
            +
            +HttpResponseService
             
             
             
             HttpResponseService->HttpResponseModule
            -
            -
            +
            +
             
             
             
             LoggerService
            -
            -LoggerService
            +
            +LoggerService
             
             
             
             LoggerService->LoggerModule
            -
            -
            +
            +
             
             
             
             SearchService
            -
            -SearchService
            +
            +SearchService
             
             
             
             SearchService->SearchModule
            -
            -
            +
            +
             
             
             
            diff --git a/documentation/injectables/PageInterceptor.html b/documentation/injectables/PageInterceptor.html
            index 5120434..423a2cb 100644
            --- a/documentation/injectables/PageInterceptor.html
            +++ b/documentation/injectables/PageInterceptor.html
            @@ -77,6 +77,26 @@
                 

            Index

            + + + + + +
            +
            Properties
            +
            + +
            @@ -87,6 +107,16 @@ @@ -101,18 +131,248 @@
            +
            +

            Constructor

            + + + + + + + + + + + + + +
            +constructor(httpService: HttpService) +
            + +
            +

            Injects needed dependencies and instantiates the storage object

            +
            +
            + Parameters : + + + + + + + + + + + + + + + + + + +
            NameTypeOptional
            httpService + HttpService + + No +
            +
            +
            +

            Methods

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

            Deletes the PIT specified by provided ID

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

            , ID of the PIT, that would be deleted

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

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

            + +
            +
            + + + + + + + + + + + + + + + + + + + +
            + + + Public + Async + getPIT + + +
            + + getPIT(alive: number, unit: EsTime) +
            + +
            +

            Acquires a PIT ID from Elasticsearch, needed for a request

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

            , amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes.

            + +
            unit + EsTime + + No + + EsTime.min + +
            +
            +
            +
            +
            + Returns : Promise<EsPit> + +
            +
            +

            PIT object containing PIT ID and keep_alive value

            + +
            +
            @@ -180,7 +441,7 @@
            @@ -192,56 +453,295 @@
            + Async intercept @@ -120,15 +380,16 @@
            -intercept(context: ExecutionContext, next: CallHandler) + + intercept(context: ExecutionContext, next: CallHandler)
            - +
            +
            + +

            + Properties +

            + + + + + + + + + + + + + + + + + +
            + + + Private + Readonly + ES_PORT + + +
            + Default value : process.env.ES_PORT +
            + +
            +

            Elastichsearch server port-number

            +
            +
            + + + + + + + + + + + + + + + + + +
            + + + Private + prevSearch + + +
            + Type : PrevSearch + +
            + +
            +

            Info about previously completed search

            +
            +
            +
            -
            import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
            -import { MetadataScanner } from "@nestjs/core";
            -import { Observable, map } from "rxjs";
            -import { PageDto } from "../domain/dtos";
            +        
            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 { 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";
            +
            +/**
            + * Previous search data storage
            + */
            +class PrevSearch implements SearchInfo {
            +    /**
            +     * Constructs an uninitialized object
            +     */
            +    constructor() {
            +        this.pit = undefined;
            +        this.tiebreaker = undefined;
            +        this.prevPage = -1;
            +    }
            +
            +    /**
            +     * PIT object of the previous search
            +     */
            +    pit: EsPit;
            +
            +    /**
            +     * Tiebreaker and sort parameters
            +     */
            +    tiebreaker: unknown[];
            +
            +    /**
            +     * Number of the previous page
            +     */
            +    prevPage: number;
            +
            +    /**
            +     * Checks if there was the search before current one
            +     * @returns true/false, showing whether or not there was another search before
            +     */
            +    public isSet(): boolean {
            +        if (this.pit && this.tiebreaker && this.prevPage !== -1) return true;
            +        return false;
            +    }
            +}
             
             /**
              * Pagination-implementing interceptor
              */
             @Injectable()
             export class PageInterceptor implements NestInterceptor {
            +    /**
            +     * Injects needed dependencies and instantiates the storage object
            +     * @param httpService 
            +     * @param searchService 
            +     */
            +    constructor(private readonly httpService: HttpService) {
            +        this.prevSearch = new PrevSearch;
            +    }
            +
                 /**
                  * Override of intercept() method, specified in NestInterceptor interface
                  * @param context 
                  * @param next 
                  * @returns Page with content and metadata
                  */
            -    intercept(context: ExecutionContext, next: CallHandler<any>): Observable<PageDto<object>> {
            -        const request = context.switchToHttp().getRequest();
            +    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<PageDto>> {
            +        let request: RequestDto = context.switchToHttp().getRequest<RequestDto>();
                     const query: SearchQueryDto = request.query;
            +        let reverse: boolean = false;
            +
            +        request.es_query = new EsQueryDto();
            +
            +        request.es_query.query = {
            +            query_string: {
            +                query: query.query,
            +                default_field: 'content',
            +            }
            +        };
            +        request.es_query.sort = [
            +            { _score: { order: !query?.order ? Order.DESC : query.order } },
            +            { _shard_doc: 'desc' }
            +        ];
            +
            +        if (this.prevSearch.isSet()) {
            +            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);
            +            
            +            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 {
            +            this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
            +            request.es_query.size = !query?.limit ? 10 : query.limit;
            +        }
             
                     return next.handle().pipe(
                         map((res) => {
            +                // Setting the page meta-data
                             let meta: PageMeta = {
            -                    total: res.total.value,
            +                    total: res.hits.total.value,
                                 pagenum: !query?.page ? 1 : query.page,
                                 order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC,
                                 hasNext: false,
                                 hasPrev: false,
                                 pagesize: !query?.limit ? 10 : query.limit,
            -                };
            +                }; 
            +                // 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 = res.hits[meta.pagenum * meta.pagesize] ? true : false;
            -                meta.hasPrev = res.hits[(meta.pagenum - 1) * meta.pagesize - 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;
             
            -                const data = res.hits.slice((meta.pagenum - 1) * meta.pagesize, meta.pagenum * meta.pagesize);
            +                let data = res.hits.hits.slice(-meta.pagesize);
            +                if (reverse) {
            +                    console.log('REVERSE');
            +                    this.prevSearch.tiebreaker = data[0].sort;
            +                    data.reverse();
            +                    reverse = false;
            +                }
             
            +                // Return the page
                             return new PageDto(data, meta);
                         })
                     );
                 }
             
            +    /**
            +     * Elastichsearch server port-number
            +     */
            +    private readonly ES_PORT = process.env.ES_PORT;
            +
            +    /**
            +     * Info about previously completed search
            +     */
            +    private prevSearch: PrevSearch;
            +
            +    /**
            +     * Acquires a PIT ID from Elasticsearch, needed for a request
            +     * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes.
            +     * @returns PIT object <EsPit> containing PIT ID and keep_alive value
            +     */
            +     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}`)
            +                    .pipe(take(1), map(axiosRes => axiosRes.data))
            +                    .subscribe((res) => {
            +                        res.keep_alive = alive + unit;
            +                        resolve(res);
            +                    }));
            +            } catch (error) {
            +                reject(error);
            +            }
            +        });
            +    }
            +
            +    /**
            +     * Deletes the PIT specified by provided ID
            +     * @param pitID, ID of the PIT, that would be deleted
            +     * @returns true/false, depending on the result of deletion of the PIT
            +     */
            +     async deletePIT(pitID: string): Promise<boolean> {
            +        return new Promise((resolve, reject) => {
            +            try {
            +                this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, {
            +                    data: { id: pitID },
            +                    headers: { 'Content-Type': 'application/json' },
            +                })
            +                .pipe(take(1), map(axiosRes => axiosRes.data))
            +                .subscribe((res) => {
            +                    resolve(res.succeeded);
            +                });
            +            } catch (error) {
            +                reject(error);
            +            }
            +        })
            +    }
            +}
            +/*
            +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(',');
            @@ -258,7 +758,39 @@ export class PageInterceptor implements NestInterceptor {
             
                 //     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 927d5f6..20c8e3d 100644 --- a/documentation/injectables/SearchService.html +++ b/documentation/injectables/SearchService.html @@ -102,10 +102,6 @@ @@ -141,7 +133,7 @@ - + @@ -187,85 +179,6 @@ HTTPService instance

            Methods

            - - - - - - - - - - - - - - - - - - - -
            - - - Async - deletePIT - - -
            - - deletePIT(pitID: string) -
            - -
            -

            Deletes the PIT specified by provided ID

            -
            - -
            - Parameters : - - - - - - - - - - - - - - - - - - - - - -
            NameTypeOptionalDescription
            pitID - string - - No - -

            , ID of the PIT, that would be deleted

            - -
            -
            -
            -
            -
            - Returns : Promise<boolean> - -
            -
            -

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

            - -
            -
            @@ -281,15 +194,15 @@ HTTPService instance

            @@ -312,9 +225,9 @@ HTTPService instance

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

            - findByContext(query_str: string) + findByContext(es_query: EsQueryDto)
            - +
            query_stres_query - string + EsQueryDto @@ -362,8 +275,8 @@ HTTPService instance

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

            Acquires a PIT ID from Elasticsearch, needed for a request

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

            , amount of time in minutes (defaults to 1)

            - -
            -
            -
            -
            -
            - Returns : Promise<string> - -
            -
            -

            Point-In-Time ID

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

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

            import { HttpService } from "@nestjs/axios";
            -import { Injectable } from "@nestjs/common";
            +import { GatewayTimeoutException, 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";
             
             /**
              * Search service provider
            @@ -571,27 +404,29 @@ export class SearchService {
                  * @returns Elasticsearch hits or an error object
                  */
                 async findByID(uuid: string): Promise<SearchResultDto> { // Should I change 'object' to specific DTO?
            -        let es_query = {
            -            query: {
            -                query_string: {
            -                    query: 'id:' + uuid
            -                }
            -            },
            +        let ESQ: EsQueryDto = new EsQueryDto;
            +
            +        ESQ.size = 1;
            +        ESQ.query = {
            +            query_string: {
            +                query: ('id:' + uuid),
            +            }
                     }
             
                     return new Promise((resolve, reject) => {
                         try {
                             (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, {
            -                    data: es_query,
            +                    data: ESQ,
                                 headers: {'Content-Type': 'application/json'},
                             }))
                             .pipe(take(1), map(axiosRes => axiosRes.data))
                             .subscribe((res: EsResponseDto) => {
                                 if (res.timed_out) {
            -                        reject(new SearchResultDto(504, {message: 'Timed Out'}));
            +                        throw new GatewayTimeoutException;
            +                        // reject(new SearchResultDto(504, {message: 'Timed Out'}));
                                 }
             
            -                    resolve(new SearchResultDto(200, res.hits));
            +                    resolve(new SearchResultDto(200, res));
                             });
                         } catch (error) {
                             reject(new SearchResultDto(700, error));
            @@ -601,21 +436,11 @@ export class SearchService {
             
                 /**
                  * Finds relevant documents by context using the given query string
            -     * @param query_str 
            +     * @param query, <EsQueryDto> 
                  * @returns Elasticsearch hits or an error object
                  */
            -    async findByContext(query_str: string): Promise<SearchResultDto> {
            -        let es_query = {
            -            query: {
            -                query_string: {
            -                    query: query_str,
            -                    default_field: "content"
            -                }
            -            },
            -        }
            -
            -        let pitID = this.getPIT(1);
            -
            +    async findByContext(es_query: EsQueryDto): Promise<SearchResultDto> {
            +        console.log(`SEARCH|SERVICE: ${JSON.stringify(es_query, null, 2)}`);
                     return new Promise((resolve, reject) => {
                         try {
                             (this.httpService.get<EsResponseDto>(`http://localhost:${this.ES_PORT}/_search`, {
            @@ -625,58 +450,54 @@ export class SearchService {
                             .pipe(take(1), map(axiosRes => axiosRes.data))
                             .subscribe((res: EsResponseDto) => {
                                 if (res.timed_out) {
            -                        reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'}));
            -                    }            
            -                    
            -                    resolve(new SearchResultDto(200, res.hits));
            +                        throw new GatewayTimeoutException;
            +                        // reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'}));
            +                    }
            +
            +                    resolve(new SearchResultDto(200, res));
                             });
                         } catch (error) {
                             reject(new SearchResultDto(700, error));
                         }
                     });
                 }
            +}
             
            -    /**
            -     * Acquires a PIT ID from Elasticsearch, needed for a request
            -     * @param alive, amount of time in minutes (defaults to 1)
            -     * @returns Point-In-Time ID
            -     */
            -    async getPIT(alive: number = 1): Promise<string> {
            -        return new Promise((resolve, reject) => {
            -            try {
            -                (this.httpService.post(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive}m`)
            -                    .pipe(take(1), map(axiosRes => axiosRes.data))
            -                    .subscribe((res) => {
            -                        resolve(res.id);
            -                    }));
            -            } catch (error) {
            -                reject(error);
            -            }
            -        });
            -    }
            +// let ESQ: EsQueryDto = new EsQueryDto;
             
            -    /**
            -     * Deletes the PIT specified by provided ID
            -     * @param pitID, ID of the PIT, that would be deleted
            -     * @returns true/false, depending on the result of deletion of the PIT
            -     */
            -    async deletePIT(pitID: string): Promise<boolean> {
            -        return new Promise((resolve, reject) => {
            -            try {
            -                this.httpService.delete(`http://localhost:${this.ES_PORT}/papers/_pit`, {
            -                    data: { id: pitID },
            -                    headers: { 'Content-Type': 'application/json' },
            -                })
            -                .pipe(take(1), map(axiosRes => axiosRes.data))
            -                .subscribe((res) => {
            -                    resolve(res.succeeded);
            -                });
            -            } catch (error) {
            -                reject(error);
            -            }
            -        })
            -    }
            -}
            + // 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 new file mode 100644 index 0000000..7ad3c14 --- /dev/null +++ b/documentation/interfaces/EqQueryString.html @@ -0,0 +1,350 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
            +
            + + +
            +
            + + + + + + + + + + + + + + + + + +
            +
            +

            +

            File

            +

            +

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

            + + +

            +

            Description

            +

            +

            +

            Structure of page metadata

            + +

            + + +
            +

            Index

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

            Properties

            + + + + + + + + + + + + + + + + + + + + + + + + + +
            + + default_field + + + + +
            + default_field: string + +
            + Type : string + +
            + Optional +
            +

            Default field to perform a search on, when +no field is specified for the query

            +
            +
            + + + + + + + + + + + + + + + + + + + + + + + + + +
            + + fields + + + + +
            + fields: string[] + +
            + Type : string[] + +
            + Optional +
            +

            Specific fields, to perform a search on +Can't be specified with 'default_field'

            +
            +
            + + + + + + + + + + + + + + + + + + + + + + +
            + + query + + + + +
            + query: string + +
            + Type : string + +
            +

            Query string, that provides the data, to perform a search on

            +
            +
            +
            +
            + + +
            +
            export interface EqQueryString {
            +    /**
            +     * Query string, that provides the data, to perform a search on
            +     */
            +    query: string;
            +
            +    /**
            +     * Default field to perform a search on, when 
            +     * no field is specified for the query
            +     */
            +    default_field?: string;
            +
            +    /**
            +     * Specific fields, to perform a search on
            +     * Can't be specified with 'default_field'
            +     */
            +    fields?: string[];
            +
            +    /**
            +     * 
            +     */
            +
            +}
            +
            +
            + + + + + + + + +
            +
            +

            results matching ""

            +
              +
              +
              +

              No results matching ""

              +
              +
              +
              + +
              +
              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/interfaces/EsPit.html b/documentation/interfaces/EsPit.html new file mode 100644 index 0000000..ad224a2 --- /dev/null +++ b/documentation/interfaces/EsPit.html @@ -0,0 +1,279 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
              +
              + + +
              +
              + + + + + + + + + + + + + + + + + +
              +
              +

              +

              File

              +

              +

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

              + + +

              +

              Description

              +

              +

              +

              Structure of PIT (Point-In-Time) object

              + +

              + + +
              +

              Index

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

              Properties

              + + + + + + + + + + + + + + + + + + + + + + +
              + + id + + + + +
              + id: string + +
              + Type : string + +
              +

              PIT ID

              +
              +
              + + + + + + + + + + + + + + + + + + + + + + +
              + + keep_alive + + + + +
              + keep_alive: string + +
              + Type : string + +
              +

              Time to live of the PIT

              +
              +
              +
              +
              + + +
              +
              export interface EsPit {
              +    /**
              +     * PIT ID
              +     */
              +    id: string;
              +
              +    /**
              +     * Time to live of the PIT
              +     */
              +    keep_alive: string;
              +}
              +
              +
              + + + + + + + + +
              +
              +

              results matching ""

              +
                +
                +
                +

                No results matching ""

                +
                +
                +
                + +
                +
                + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/interfaces/EsQuery.html b/documentation/interfaces/EsQuery.html new file mode 100644 index 0000000..13a5a04 --- /dev/null +++ b/documentation/interfaces/EsQuery.html @@ -0,0 +1,234 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
                +
                + + +
                +
                + + + + + + + + + + + + + + + + + +
                +
                +

                +

                File

                +

                +

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

                + + +

                +

                Description

                +

                +

                +

                Structure of page metadata

                + +

                + + +
                +

                Index

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

                Properties

                + + + + + + + + + + + + + + + + + + + + + + +
                + + query_string + + + + +
                + query_string: EqQueryString + +
                + Type : EqQueryString + +
                +

                Query string object, that specifies certain search conditions

                +
                +
                +
                +
                + + +
                +
                import { EqQueryString } from "./es-query-string.interface";
                +
                +/**
                + * Structure of page metadata
                + */
                +export interface EsQuery {
                +    /**
                +     * Query string object, that specifies certain search conditions
                +     */
                +    query_string: EqQueryString;
                +}
                +
                +
                + + + + + + + + +
                +
                +

                results matching ""

                +
                  +
                  +
                  +

                  No results matching ""

                  +
                  +
                  +
                  + +
                  +
                  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/interfaces/EsResponseHits.html b/documentation/interfaces/EsResponseHits.html new file mode 100644 index 0000000..6323b5a --- /dev/null +++ b/documentation/interfaces/EsResponseHits.html @@ -0,0 +1,340 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
                  +
                  + + +
                  +
                  + + + + + + + + + + + + + + + + + +
                  +
                  +

                  +

                  File

                  +

                  +

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

                  + + +

                  +

                  Description

                  +

                  +

                  +

                  Structure of 'hits' object of Elasticsearch response

                  + +

                  + + +
                  +

                  Index

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

                  Properties

                  + + + + + + + + + + + + + + + + + + + + + + +
                  + + hits + + + + +
                  + hits: EsHitDto[] + +
                  + Type : EsHitDto[] + +
                  +

                  Array of search results

                  +
                  +
                  + + + + + + + + + + + + + + + + + + + + + + + + + +
                  + + max_score + + + + +
                  + max_score: number + +
                  + Type : number + +
                  + Optional +
                  +

                  Maximum score amongst all search results

                  +
                  +
                  + + + + + + + + + + + + + + + + + + + + + + +
                  + + total + + + + +
                  + total: object + +
                  + Type : object + +
                  +

                  Object containing info about hits

                  +
                  +
                  +
                  +
                  + + +
                  +
                  import { EsHitDto } from "../dtos/es-hit.dto";
                  +
                  +/**
                  + * Structure of 'hits' object of Elasticsearch response
                  + */
                  +export interface EsResponseHits {
                  +    /**
                  +     * Object containing info about hits
                  +     */
                  +    total: object;
                  +
                  +    /**
                  +     * Maximum score amongst all search results
                  +     */
                  +    max_score?: number;
                  +
                  +    /**
                  +     * Array of search results
                  +     */
                  +    hits: EsHitDto[];
                  +}
                  +
                  +
                  + + + + + + + + +
                  +
                  +

                  results matching ""

                  +
                    +
                    +
                    +

                    No results matching ""

                    +
                    +
                    +
                    + +
                    +
                    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/interfaces/SearchInfo.html b/documentation/interfaces/SearchInfo.html new file mode 100644 index 0000000..28775d4 --- /dev/null +++ b/documentation/interfaces/SearchInfo.html @@ -0,0 +1,286 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
                    +
                    + + +
                    +
                    + + + + + + + + + + + + + + + + + +
                    +
                    +

                    +

                    File

                    +

                    +

                    + src/core/domain/interfaces/search-info.interface.ts +

                    + + +

                    +

                    Description

                    +

                    +

                    +

                    Structure of search metadata

                    + +

                    + + +
                    +

                    Index

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

                    Properties

                    + + + + + + + + + + + + + + + + + + + + + + +
                    + + pit + + + + +
                    + pit: EsPit + +
                    + Type : EsPit + +
                    +

                    Previous search saved PIT

                    +
                    +
                    + + + + + + + + + + + + + + + + + + + + + + +
                    + + tiebreaker + + + + +
                    + tiebreaker: [] + +
                    + Type : [] + +
                    +

                    Special tiebreaker used by Elasticsearch. +Indicates the starting point of next search

                    +
                    +
                    +
                    +
                    + + +
                    +
                    import { EsPit } from "./es-pit.interface";
                    +
                    +/**
                    + * Structure of search metadata
                    + */
                    +export interface SearchInfo {
                    +    /**
                    +     * Previous search saved PIT
                    +     */
                    +    pit: EsPit;
                    +
                    +    /**
                    +     * Special tiebreaker used by Elasticsearch.
                    +     * Indicates the starting point of next search
                    +     */
                    +    tiebreaker: unknown[];
                    +}
                    +
                    +
                    + + + + + + + + +
                    +
                    +

                    results matching ""

                    +
                      +
                      +
                      +

                      No results matching ""

                      +
                      +
                      +
                      + +
                      +
                      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/js/menu-wc.js b/documentation/js/menu-wc.js index 62f04bc..47fe941 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/enums/es-time.enum.ts

                      +
                      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                      + + EsTime +
                      +

                      Elasticsearch time-units

                      +
                      +
                      +  days +
                      + Value : d +
                      +  hours +
                      + Value : h +
                      +  min +
                      + Value : m +
                      +  sec +
                      + Value : s +
                      +  ms +
                      + Value : ms +
                      +  us +
                      + Value : micros +
                      +  ns +
                      + Value : nanos +

                      src/core/domain/enums/httpResponse/httpResponseDescriptions.enum.ts

                      @@ -1200,6 +1292,12 @@ Order + + +

                      Page display order

                      +
                      + +  ASC @@ -1207,7 +1305,7 @@ - Value : ASC + Value : asc @@ -1217,7 +1315,7 @@ - Value : DESC + Value : desc diff --git a/documentation/miscellaneous/variables.html b/documentation/miscellaneous/variables.html index 1e20f8f..15e18c9 100644 --- a/documentation/miscellaneous/variables.html +++ b/documentation/miscellaneous/variables.html @@ -61,6 +61,9 @@
                      +

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

                      +
                      +

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

                      List of allowed properties in this DTO

                      +
                      +
                      +

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

                      @@ -157,7 +201,7 @@ @@ -206,6 +250,76 @@
                      - Default value : ['took', 'timed_out', '_shards', 'hits'] + Default value : ['took', 'timed_out', '_shards', 'hits', 'pit_id']
                      +

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

                      +
                      +

                      + + + + + + + + + + + + + + + + +
                      + + + allowedProperties + + +
                      + Type : [] + +
                      + Default value : ['id', 'title', 'authors', 'topic', 'summary', 'tags', 'content'] +
                      +

                      List of allowed properties in this DTO

                      +
                      +
                      +
                      +

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

                      +
                      +

                      + + + + + + + + + + + + + + + + +
                      + + + allowedProperties + + +
                      + Type : [] + +
                      + Default value : ['query', 'es_query'] +
                      +

                      List of allowed properties in this DTO

                      +
                      +
                      +

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

                      diff --git a/documentation/modules/CommonModule/dependencies.svg b/documentation/modules/CommonModule/dependencies.svg index 9675a86..17756f5 100644 --- a/documentation/modules/CommonModule/dependencies.svg +++ b/documentation/modules/CommonModule/dependencies.svg @@ -14,14 +14,14 @@ cluster_CommonModule - -cluster_CommonModule_exports - - cluster_CommonModule_imports + +cluster_CommonModule_exports + + HttpResponseModule diff --git a/documentation/modules/SearchModule.html b/documentation/modules/SearchModule.html index a145e1b..d6ede70 100644 --- a/documentation/modules/SearchModule.html +++ b/documentation/modules/SearchModule.html @@ -178,10 +178,8 @@
                      import { HttpModule } from "@nestjs/axios";
                       import { Module } from "@nestjs/common";
                      -import { ConfigModule } from "@nestjs/config";
                       import { PapersController } from "src/application";
                       import { SearchService } from "../../core/services/common/search.service";
                      -import { configuration } from "../config";
                       
                       /**
                        * search module
                      diff --git a/documentation/modules/SearchModule/dependencies.svg b/documentation/modules/SearchModule/dependencies.svg
                      index bfd3490..3d5fb1d 100644
                      --- a/documentation/modules/SearchModule/dependencies.svg
                      +++ b/documentation/modules/SearchModule/dependencies.svg
                      @@ -14,14 +14,14 @@
                       cluster_SearchModule
                       
                       
                      -
                      -cluster_SearchModule_exports
                      -
                      -
                       
                       cluster_SearchModule_providers
                       
                       
                      +
                      +cluster_SearchModule_exports
                      +
                      +
                       
                       
                       SearchService 
                      diff --git a/documentation/overview.html b/documentation/overview.html
                      index 75250dc..5138f8f 100644
                      --- a/documentation/overview.html
                      +++ b/documentation/overview.html
                      @@ -301,7 +301,7 @@
                                   

                      -

                      7 Classes

                      +

                      11 Classes

                      @@ -317,7 +317,7 @@

                      -

                      4 Interfaces

                      +

                      9 Interfaces

                      diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index b792ba7..a251822 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,11 +1,9 @@ import { Controller, Get, HttpCode, HttpException, Next, Param, ParseUUIDPipe, Put, Query, Req, Res, UseInterceptors } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; -import { Response } from "express"; import { PageInterceptor } from "src/core/interceptors/page.interceptor"; -import { LoggerInterceptor } from "src/core/interceptors"; import { SearchResultDto } from "src/core/domain/dtos/search-result.dto"; import { ApiOperation, ApiResponse } from "@nestjs/swagger"; -import { SearchQueryDto } from "src/core/domain/dtos"; +import { RequestDto } from "src/core/domain/dtos/request.dto"; /** * /papers/ route controller @@ -29,8 +27,8 @@ export class PapersController { @Get('search') @UseInterceptors(PageInterceptor) @HttpCode(200) - getByContext(@Query() query): object { - return this.searchService.findByContext(query.query).then( + getByContext(@Req() query: RequestDto): object { + return this.searchService.findByContext(query.es_query).then( (response: SearchResultDto) => { return response.data; }, diff --git a/src/core/domain/dtos/es-hit.dto.ts b/src/core/domain/dtos/es-hit.dto.ts new file mode 100644 index 0000000..1334729 --- /dev/null +++ b/src/core/domain/dtos/es-hit.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { PaperDto } from "./paper.dto"; + +/** + * List of allowed properties in this DTO + */ +const allowedProperties = ['sort', '_source', '_score']; + +/** + * Structure of the document stored and retrieved from Elasticsearch + */ +export class EsHitDto { + /** + * Actual document stored in Elasticsearch + */ + @IsNotEmpty() + @ApiProperty({ + description: 'Actual document (paper) stored in Elasticsearch', + example: { + id: 'sssss' + } + }) + _source: PaperDto; + + /** + * List of objects that represents how the hit was sorted + */ + @IsOptional() + @ApiProperty({ + description: 'List of objects that represents how the hit was sorted', + example: {} + }) + sort?: []; + + /** + * Hit relevance score + */ + @IsOptional() + @ApiProperty({ + description: 'Relevance score', + example: 1.2355 + }) + _score?: number; +} \ No newline at end of file diff --git a/src/core/domain/dtos/es-query.dto.ts b/src/core/domain/dtos/es-query.dto.ts index 6bac065..5c9fd6b 100644 --- a/src/core/domain/dtos/es-query.dto.ts +++ b/src/core/domain/dtos/es-query.dto.ts @@ -1,5 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validator"; +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" /** * List of allowed properties in this DTO @@ -13,6 +15,7 @@ import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validato /** * Maximum number of elements returned by Elasticsearch */ + @IsOptional() @IsDefined() @IsNumber() @IsInt() @@ -20,7 +23,7 @@ import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validato description: 'Maximum number of elements returned by Elasticsearch', example: 30 }) - size: number; + size?: number; /** * The search query object passed to Elasticsearch @@ -29,17 +32,51 @@ import { IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject } from "class-validato @IsObject() @ApiProperty({ description: 'Search query object passed to Elasticsearch', - example: false, + example: {}, }) - query: object; + query: EsQuery; /** - * Object, that stores Point-In-Time ID and time alive + * Object, that stores PIT ID and time alive */ - pit: object; + @IsOptional() + @IsObject() + @ApiProperty({ + description: 'PIT object', + example: {} + }) + pit?: EsPit; /** - * Object, that stores + * Sorting info */ - sort: object; + @IsOptional() + @IsArray() + @ApiProperty({ + description: '', + example: [] + }) + sort?: unknown[]; + + /** + * Pagination info + */ + @IsOptional() + @IsArray() + @ApiProperty({ + description: '', + example: [] + }) + search_after?: unknown[]; + + /** + * Constructs an empty object + */ + constructor() { + this.size = 10; + this.query = undefined; + this.pit = undefined; + this.sort = undefined; + this.search_after = undefined; + } } \ No newline at end of file diff --git a/src/core/domain/dtos/es-response.dto.ts b/src/core/domain/dtos/es-response.dto.ts index f7899b4..84bc76b 100644 --- a/src/core/domain/dtos/es-response.dto.ts +++ b/src/core/domain/dtos/es-response.dto.ts @@ -1,10 +1,11 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator"; +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator"; +import { EsResponseHits } from "../interfaces/es-response-hits.interface"; /** * List of allowed properties in this DTO */ -const allowedProperties = ['took', 'timed_out', '_shards', 'hits']; +const allowedProperties = ['took', 'timed_out', '_shards', 'hits', 'pit_id']; /** * Elasticsearch response DTO @@ -79,5 +80,16 @@ export class EsResponseDto { }], } }) - hits: object; + hits: EsResponseHits; + + /** + * ID of the PIT used in the search + */ + @IsString() + @IsOptional() + @ApiProperty({ + description: 'PIT ID used to search for results', + example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA==' + }) + pit_id?: string; } \ 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 93575c6..8c0ab24 100644 --- a/src/core/domain/dtos/page.dto.ts +++ b/src/core/domain/dtos/page.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsArray } from "class-validator"; import { PageMeta } from "../interfaces/page-meta.interface"; +import { PaperDto } from "./paper.dto"; /** * List of allowed properties in this DTO @@ -10,7 +11,7 @@ const allowedProperties = ['data', 'meta']; /** * Page model for pagination */ -export class PageDto { +export class PageDto { /** * Data block of the page */ @@ -19,7 +20,7 @@ export class PageDto { description: 'All data the page contains', isArray: true, }) - readonly data: T[]; + readonly data: PaperDto[]; /** * Metadata of the page @@ -35,7 +36,7 @@ export class PageDto { * @param data * @param meta */ - constructor(data: T[], meta: PageMeta) { + constructor(data: PaperDto[], meta: PageMeta) { this.data = data; this.meta = meta; } diff --git a/src/core/domain/dtos/paper.dto.ts b/src/core/domain/dtos/paper.dto.ts new file mode 100644 index 0000000..5ee8e60 --- /dev/null +++ b/src/core/domain/dtos/paper.dto.ts @@ -0,0 +1,89 @@ +import { 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 + */ +const allowedProperties = ['id', 'title', 'authors', 'topic', 'summary', 'tags', 'content']; + +/** + * Structure of the document stored and retrieved from Elasticsearch + */ +export class PaperDto { + /** + * Unique ID of the paper + */ + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Unique ID of the paper', + example: 'cc3c3cca-f763-495c-8dfa-69c45ca738ff' + }) + id: string; + + /** + * Title of the paper + */ + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Title of the paper', + example: 'Mucosal associated invariant T cell', + }) + title: string; + + /** + * List of authors of the paper + */ + @IsNotEmpty() + @IsArray() + @ApiProperty({ + description: 'List of authors of the paper', + example: ['Daniil Mikhaylov', 'Denis Gorbunov', 'Maxim Ten'] + }) + authors: string[]; + + /** + * Topic of the paper + */ + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Topic of the paper', + example: 'Physics' + }) + topic: string; + + /** + * Summary of the paper. May be a short excerpt from the main text. + */ + @IsNotEmpty() + @IsString() + @ApiProperty({ + description: 'Summary of the paper. May be a short excerpt from the main text', + example: 'S-algol (St Andrews Algol):vii is a computer programming language derivative of ALGOL 60 developed at the University of St Andrews in 1979 by Ron Morrison and Tony Davie' + }) + summary: string; + + /** + * List of tags, that show the certain topics/fields of knowledge paper is touching + */ + @IsNotEmpty() + @IsArray() + @ApiProperty({ + description: 'List of tags, that show the certain topics/fields of knowledge paper is touching', + example: ['Neurobiology', 'Neuron structure', 'Neuroimaging'] + }) + tags: string[]; + + /** + * Contents of the paper [Markdown] + */ + @ApiProperty({ + description: 'Contents of the paper presented in Markdown (.md) format', + example: '...' + }) + content: string; +} \ No newline at end of file diff --git a/src/core/domain/dtos/request.dto.ts b/src/core/domain/dtos/request.dto.ts new file mode 100644 index 0000000..fffc0ea --- /dev/null +++ b/src/core/domain/dtos/request.dto.ts @@ -0,0 +1,45 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { 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 + */ +const allowedProperties = ['query', 'es_query']; + +/** + * Request object, which contains query parameters and Elasticsearch query object + */ +export class RequestDto { + /** + * Query parameters object + */ + @IsDefined() + @IsNotEmpty() + @ApiProperty({ + description: '', + example: {} + }) + query: SearchQueryDto; + + /** + * Elasticsearch query object + */ + @IsOptional() + @ApiProperty({ + description: '', + example: {}, + }) + es_query?: EsQueryDto; + + /** + * Constructs an object with provided parameters + * @param query + * @param es_query + */ + 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-result.dto.ts b/src/core/domain/dtos/search-result.dto.ts index 493fae2..5097f64 100644 --- a/src/core/domain/dtos/search-result.dto.ts +++ b/src/core/domain/dtos/search-result.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsArray, IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { EsResponseDto } from "./es-response.dto"; /** * List of allowed properties in this DTO @@ -34,14 +35,14 @@ export class SearchResultDto { }, }) - data: object; + data: EsResponseDto; /** * Constructs an object with provided parameters * @param code * @param data */ - constructor(code: number, data: object) { + constructor(code: number, data: EsResponseDto) { this.statusCode = code; this.data = data; } diff --git a/src/core/domain/enums/es-time.enum.ts b/src/core/domain/enums/es-time.enum.ts new file mode 100644 index 0000000..7c5a5b6 --- /dev/null +++ b/src/core/domain/enums/es-time.enum.ts @@ -0,0 +1,12 @@ +/** + * Elasticsearch time-units + */ +export enum EsTime { + days = 'd', + hours = 'h', + min = 'm', + sec = 's', + ms = 'ms', + us = 'micros', + ns = 'nanos' +} \ 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 d310044..942cd68 100644 --- a/src/core/domain/enums/page-order.enum.ts +++ b/src/core/domain/enums/page-order.enum.ts @@ -1,4 +1,7 @@ +/** + * Page display order + */ export enum Order { - ASC = 'ASC', - DESC = 'DESC', + ASC = 'asc', + DESC = 'desc', } \ No newline at end of file diff --git a/src/core/domain/interfaces/es-pit.interface.ts b/src/core/domain/interfaces/es-pit.interface.ts new file mode 100644 index 0000000..d33f9e9 --- /dev/null +++ b/src/core/domain/interfaces/es-pit.interface.ts @@ -0,0 +1,14 @@ +/** + * Structure of PIT (Point-In-Time) object + */ +export interface EsPit { + /** + * PIT ID + */ + id: string; + + /** + * Time to live of the PIT + */ + keep_alive: string; +} \ No newline at end of file diff --git a/src/core/domain/interfaces/es-query-string.interface.ts b/src/core/domain/interfaces/es-query-string.interface.ts new file mode 100644 index 0000000..55c1ee6 --- /dev/null +++ b/src/core/domain/interfaces/es-query-string.interface.ts @@ -0,0 +1,26 @@ +/** + * Structure of page metadata + */ + export interface EqQueryString { + /** + * Query string, that provides the data, to perform a search on + */ + query: string; + + /** + * Default field to perform a search on, when + * no field is specified for the query + */ + default_field?: string; + + /** + * Specific fields, to perform a search on + * 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/es-query.interface.ts new file mode 100644 index 0000000..b33d239 --- /dev/null +++ b/src/core/domain/interfaces/es-query.interface.ts @@ -0,0 +1,11 @@ +import { EqQueryString } from "./es-query-string.interface"; + +/** + * Structure of page metadata + */ +export interface EsQuery { + /** + * Query string object, that specifies certain search conditions + */ + query_string: EqQueryString; +} \ No newline at end of file diff --git a/src/core/domain/interfaces/es-response-hits.interface.ts b/src/core/domain/interfaces/es-response-hits.interface.ts new file mode 100644 index 0000000..32db4de --- /dev/null +++ b/src/core/domain/interfaces/es-response-hits.interface.ts @@ -0,0 +1,21 @@ +import { EsHitDto } from "../dtos/es-hit.dto"; + +/** + * Structure of 'hits' object of Elasticsearch response + */ +export interface EsResponseHits { + /** + * Object containing info about hits + */ + total: object; + + /** + * Maximum score amongst all search results + */ + max_score?: number; + + /** + * Array of search results + */ + hits: EsHitDto[]; +} \ No newline at end of file diff --git a/src/core/domain/interfaces/index.ts b/src/core/domain/interfaces/index.ts index 85904a9..8c2a188 100644 --- a/src/core/domain/interfaces/index.ts +++ b/src/core/domain/interfaces/index.ts @@ -1,2 +1,4 @@ export * from './http-response.interface'; -export * from './page-meta.interface' \ No newline at end of file +export * from './page-meta.interface' +export * from './es-query.interface' +export * from './es-query-string.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 new file mode 100644 index 0000000..4ad0b9c --- /dev/null +++ b/src/core/domain/interfaces/search-info.interface.ts @@ -0,0 +1,17 @@ +import { EsPit } from "./es-pit.interface"; + +/** + * Structure of search metadata + */ +export interface SearchInfo { + /** + * Previous search saved PIT + */ + pit: EsPit; + + /** + * Special tiebreaker used by Elasticsearch. + * Indicates the starting point of next search + */ + tiebreaker: unknown[]; +} \ No newline at end of file diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 9217c53..492b208 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,62 +1,195 @@ +import { HttpService } from "@nestjs/axios"; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; -import { MetadataScanner } from "@nestjs/core"; -import { Observable, map } from "rxjs"; +import { Observable, map, take } from "rxjs"; import { PageDto } from "../domain/dtos"; +import { EsQueryDto } from "../domain/dtos/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"; + +/** + * Previous search data storage + */ +class PrevSearch implements SearchInfo { + /** + * Constructs an uninitialized object + */ + constructor() { + this.pit = undefined; + this.tiebreaker = undefined; + this.prevPage = -1; + } + + /** + * PIT object of the previous search + */ + pit: EsPit; + + /** + * Tiebreaker and sort parameters + */ + tiebreaker: unknown[]; + + /** + * Number of the previous page + */ + prevPage: number; + + /** + * Checks if there was the search before current one + * @returns true/false, showing whether or not there was another search before + */ + public isSet(): boolean { + if (this.pit && this.tiebreaker && this.prevPage !== -1) return true; + return false; + } +} /** * Pagination-implementing interceptor */ @Injectable() export class PageInterceptor implements NestInterceptor { + /** + * Injects needed dependencies and instantiates the storage object + * @param httpService + * @param searchService + */ + constructor(private readonly httpService: HttpService) { + this.prevSearch = new PrevSearch; + } + /** * Override of intercept() method, specified in NestInterceptor interface * @param context * @param next * @returns Page with content and metadata */ - intercept(context: ExecutionContext, next: CallHandler): Observable> { - const request = context.switchToHttp().getRequest(); + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + let request: RequestDto = context.switchToHttp().getRequest(); const query: SearchQueryDto = request.query; + let reverse: boolean = false; + + request.es_query = new EsQueryDto(); + + request.es_query.query = { + query_string: { + query: query.query, + default_field: 'content', + } + }; + request.es_query.sort = [ + { _score: { order: !query?.order ? Order.DESC : query.order } }, + { _shard_doc: 'desc' } + ]; + + if (this.prevSearch.isSet()) { + 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); + + 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 { + 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( map((res) => { + // Setting the page meta-data let meta: PageMeta = { - total: res.total.value, + total: res.hits.total.value, pagenum: !query?.page ? 1 : query.page, order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC, - hasNext: false, - hasPrev: false, + 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; - meta.hasNext = res.hits[meta.pagenum * meta.pagesize] ? true : false; - meta.hasPrev = res.hits[(meta.pagenum - 1) * meta.pagesize - 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; - const data = res.hits.slice((meta.pagenum - 1) * meta.pagesize, meta.pagenum * meta.pagesize); + // Check if the performed search is a backward search + let data = res.hits.hits.slice(-meta.pagesize); + if (reverse) { + this.prevSearch.tiebreaker = data[0]?.sort; + data.reverse(); + reverse = false; + } + // Return the page return new PageDto(data, meta); }) ); } - // getQueryParams(str: string): any { - // let parameters: object = {}; - // let pairs: string[] = str.split(','); - // parameters['main'] = pairs[0]; - // pairs.shift(); + /** + * Elastichsearch server port-number + */ + private readonly ES_PORT = process.env.ES_PORT; - // if(!pairs || pairs[0] === '') return parameters; + /** + * Info about previously completed search + */ + private prevSearch: PrevSearch; - // for (const pair of pairs) { - // const key: string = pair.substring(0, pair.indexOf('=')); - // const value: string = pair.substring(pair.indexOf('=') + 1); - // parameters[key] = value; - // } + /** + * Acquires a PIT ID from Elasticsearch, needed for a request + * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes. + * @returns PIT object containing PIT ID and keep_alive value + */ + 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}`) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + res.keep_alive = alive + unit; + resolve(res); + })); + } catch (error) { + reject(error); + } + }); + } - // return parameters; - // } + /** + * Deletes the PIT specified by provided ID + * @param pitID, ID of the PIT, that would be deleted + * @returns true/false, depending on the result of deletion of the PIT + */ + async deletePIT(pitID: string): Promise { + return new Promise((resolve, reject) => { + try { + this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, { + data: { id: pitID }, + headers: { 'Content-Type': 'application/json' }, + }) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.succeeded); + }); + } catch (error) { + reject(error); + } + }) + } } \ No newline at end of file diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index 7339362..bcb3278 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,8 +1,11 @@ import { HttpService } from "@nestjs/axios"; -import { Injectable } from "@nestjs/common"; +import { GatewayTimeoutException, 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"; /** * Search service provider @@ -27,27 +30,29 @@ export class SearchService { * @returns Elasticsearch hits or an error object */ async findByID(uuid: string): Promise { // Should I change 'object' to specific DTO? - let es_query = { - query: { - query_string: { - query: 'id:' + uuid - } - }, + let ESQ: EsQueryDto = new EsQueryDto; + + ESQ.size = 1; + ESQ.query = { + query_string: { + query: ('id:' + uuid), + } } return new Promise((resolve, reject) => { try { (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { - data: es_query, + data: ESQ, headers: {'Content-Type': 'application/json'}, })) .pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsResponseDto) => { if (res.timed_out) { - reject(new SearchResultDto(504, {message: 'Timed Out'})); + throw new GatewayTimeoutException; + // reject(new SearchResultDto(504, {message: 'Timed Out'})); } - resolve(new SearchResultDto(200, res.hits)); + resolve(new SearchResultDto(200, res)); }); } catch (error) { reject(new SearchResultDto(700, error)); @@ -57,21 +62,11 @@ export class SearchService { /** * Finds relevant documents by context using the given query string - * @param query_str + * @param query, * @returns Elasticsearch hits or an error object */ - async findByContext(query_str: string): Promise { - let es_query = { - query: { - query_string: { - query: query_str, - default_field: "content" - } - }, - } - - let pitID = this.getPIT(1); - + async findByContext(es_query: EsQueryDto): Promise { + console.log(`SEARCH|SERVICE: ${JSON.stringify(es_query, null, 2)}`); return new Promise((resolve, reject) => { try { (this.httpService.get(`http://localhost:${this.ES_PORT}/_search`, { @@ -81,55 +76,51 @@ export class SearchService { .pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsResponseDto) => { if (res.timed_out) { - reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'})); - } - - resolve(new SearchResultDto(200, res.hits)); + throw new GatewayTimeoutException; + // reject(new SearchResultDto(504, {status: 504, message: 'Timed Out'})); + } + + resolve(new SearchResultDto(200, res)); }); } catch (error) { reject(new SearchResultDto(700, error)); } }); } +} - /** - * Acquires a PIT ID from Elasticsearch, needed for a request - * @param alive, amount of time in minutes (defaults to 1) - * @returns Point-In-Time ID - */ - async getPIT(alive: number = 1): Promise { - return new Promise((resolve, reject) => { - try { - (this.httpService.post(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive}m`) - .pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res) => { - resolve(res.id); - })); - } catch (error) { - reject(error); - } - }); - } +// let ESQ: EsQueryDto = new EsQueryDto; - /** - * Deletes the PIT specified by provided ID - * @param pitID, ID of the PIT, that would be deleted - * @returns true/false, depending on the result of deletion of the PIT - */ - async deletePIT(pitID: string): Promise { - return new Promise((resolve, reject) => { - try { - this.httpService.delete(`http://localhost:${this.ES_PORT}/papers/_pit`, { - data: { id: pitID }, - headers: { 'Content-Type': 'application/json' }, - }) - .pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res) => { - resolve(res.succeeded); - }); - } catch (error) { - reject(error); - } - }) - } -} \ No newline at end of file + // 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 diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index a719b15..057ec1c 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,112 +1,112 @@ -// 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 { 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"; -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 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 callHandler = { - handle: jest.fn(), - }; +// const callHandler = { +// handle: jest.fn(), +// }; -describe('Testing PageInterceptor', () => { - let pageInter: PageInterceptor; - let moduleRef; +// describe('Testing PageInterceptor', () => { +// let pageInter: PageInterceptor; +// let moduleRef; - beforeEach(async () => { - moduleRef = await Test.createTestingModule({ - imports: [HttpModule], - controllers: [PapersController], - providers: [SearchService, PageInterceptor], - }).compile(); +// beforeEach(async () => { +// moduleRef = await Test.createTestingModule({ +// imports: [HttpModule], +// controllers: [PapersController], +// providers: [SearchService, PageInterceptor], +// }).compile(); - pageInter = moduleRef.get(PageInterceptor); - }); +// pageInter = moduleRef.get(PageInterceptor); +// }); - describe('intercept()', () => { - it('Should be defined', () => { - expect(pageInter).toBeDefined(); - }); +// describe('intercept()', () => { +// it('Should be defined', () => { +// expect(pageInter).toBeDefined(); +// }); - 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: [{},], - })); +// 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: [{},], +// })); - expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Observable); - pageInter.intercept(executionContext, callHandler).subscribe((data) => { - expect(data).toBeInstanceOf(PageDto); - done(); - }); - }) +// expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Observable); +// pageInter.intercept(executionContext, callHandler).subscribe((data) => { +// expect(data).toBeInstanceOf(PageDto); +// done(); +// }); +// }) - 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 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'}], +// })); - pageInter.intercept(executionContext, callHandler).subscribe((data) => { - expect(data).toEqual({ - data: expect.anything(), - meta: expect.anything(), - }); - done(); - }); - }); +// pageInter.intercept(executionContext, callHandler).subscribe((data) => { +// expect(data).toEqual({ +// data: expect.anything(), +// meta: expect.anything(), +// }); +// done(); +// }); +// }); - 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 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), +// })); - pageInter.intercept(executionContext, callHandler).subscribe((data) => { - expect(data.meta.hasNext).toEqual(true); - expect(data.meta.hasPrev).toEqual(false); - done(); - }); - }); +// pageInter.intercept(executionContext, callHandler).subscribe((data) => { +// expect(data.meta.hasNext).toEqual(true); +// expect(data.meta.hasPrev).toEqual(false); +// done(); +// }); +// }); - 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), - })); +// 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), +// })); - 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.intercept(executionContext, callHandler).subscribe((data) => { +// expect(data.meta).toEqual({ +// total: 15, +// pagenum: 2, +// order: Order.ASC, +// hasNext: true, +// hasPrev: true, +// pagesize: 5 +// }); +// done(); +// }); +// }); +// }); -}); \ No newline at end of file +// }); \ No newline at end of file diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index 5c6406e..d9c5d58 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -1,103 +1,112 @@ -import { HttpService } from "@nestjs/axios"; -import { ConfigModule } from "@nestjs/config"; -import { Test } from "@nestjs/testing"; -import { of } from "rxjs"; -import { HttpResponseException } from "src/core/exceptions"; -import { SearchService } from "src/core/services/common/search.service"; +// 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"; -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: { - post: 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('getPIT()', () => { - it('Should touch HttpService.post() method', () => { - let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ - data: {id: '2567'}, - status: 0, - statusText: '', - headers: {}, - config: {}, - })); +// describe('getPIT()', () => { +// it('Should touch HttpService.post() method', () => { +// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ +// data: {id: '2567'}, +// status: 0, +// statusText: '', +// headers: {}, +// config: {}, +// })); - searchService.getPIT(); - expect(postMock).toHaveBeenCalled(); - }); +// 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: {}, - })); +// it('Should contain correct port in the URI from .env', () => { +// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ +// data: {id: '2567'}, +// status: 0, +// statusText: '', +// headers: {}, +// config: {}, +// })); - searchService.getPIT(); - expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); - }); +// 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 keep_alive is set as a parameter', () => { - let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ - data: {id: '2567'}, - status: 0, - statusText: '', - headers: {}, - config: {}, - })); +// 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 keep_alive = 2; - searchService.getPIT(keep_alive); - expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${keep_alive}m`); - }); +// let time = 2; +// let unit = EsTime.sec; + +// searchService.getPIT(time, unit); +// expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); +// }); - it('Should return error exeception when HttpService fails', () => { - jest.spyOn(httpService, 'post').mockImplementation(() => { - throw HttpResponseException; - }); +// it('Should return error exeception when HttpService fails', () => { +// jest.spyOn(httpService, 'post').mockImplementation(() => { +// throw HttpResponseException; +// }); - expect(searchService.getPIT()).rejects.toEqual(HttpResponseException); - }); +// expect(searchService.getPIT(1)).rejects.toEqual(HttpResponseException); +// }); - it('Should return a non-empty string when HttpService request succeedes', () => { - jest.spyOn(httpService, 'post').mockReturnValue(of({ - data: {id: '2567'}, - status: 0, - statusText: '', - headers: {}, - config: {}, - })); +// 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: {}, +// })); - expect(searchService.getPIT()).resolves.toBe('2567'); - }); - }); +// expect(searchService.getPIT(1)).resolves.toEqual({ +// id: '2567', +// keep_alive: '1m', +// }); +// }); - 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 + +// }); + +// 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 -- 2.39.5 From da564323a0d7fbea4ee4acf8c8ec356e68a6bd9d Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Tue, 16 Aug 2022 16:57:56 +0300 Subject: [PATCH 04/23] Added versioning for the /papers controller --- src/application/controller/papers.controller.ts | 5 ++++- src/main.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index a251822..4aefece 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -8,7 +8,10 @@ import { RequestDto } from "src/core/domain/dtos/request.dto"; /** * /papers/ route controller */ -@Controller('papers') +@Controller({ + version: '1', + path: 'papers', +}) export class PapersController { constructor(private searchService: SearchService) {} diff --git a/src/main.ts b/src/main.ts index 5333fdb..4d8d5c2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import morgan from 'morgan'; -import { Logger, ValidationPipe } from '@nestjs/common'; +import { Logger, ValidationPipe, VersioningType } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { AppModule } from './infrastructure/modules/app.module'; import { SwaggerModule, DocumentBuilder, SwaggerDocumentOptions } from '@nestjs/swagger'; @@ -19,6 +19,10 @@ async function bootstrap() { }) ); + app.enableVersioning({ + type: VersioningType.URI, + }); + /** * Configuration of the Swagger document */ -- 2.39.5 From a64549865886c920ef3118637e4d2419c57aa505 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Tue, 16 Aug 2022 17:19:44 +0300 Subject: [PATCH 05/23] Fixed return format of the search results --- src/core/interceptors/page.interceptor.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 492b208..adb895e 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -2,6 +2,7 @@ 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 { RequestDto } from "../domain/dtos/request.dto"; import { SearchQueryDto } from "../domain/dtos/search-q.dto"; @@ -135,6 +136,9 @@ export class PageInterceptor implements NestInterceptor { 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); }) -- 2.39.5 From 569ef08f1fb50a85dc023aded7e4de70a2cd3143 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Tue, 16 Aug 2022 19:22:27 +0300 Subject: [PATCH 06/23] Minor code cleanup. --- src/core/domain/dtos/es-hit.dto.ts | 2 +- src/core/domain/dtos/index.ts | 11 +++-- src/core/domain/dtos/paper.dto.ts | 2 - src/core/domain/dtos/request.dto.ts | 2 +- src/core/domain/dtos/search-q.dto.ts | 2 +- src/core/domain/dtos/search-result.dto.ts | 2 +- src/core/domain/enums/es-time.enum.ts | 27 +++++++++++++ src/core/domain/enums/index.ts | 3 +- src/core/domain/enums/page-order.enum.ts | 7 ++++ src/core/domain/index.ts | 4 +- .../interfaces/es-query-string.interface.ts | 5 --- src/core/domain/interfaces/index.ts | 7 +++- src/core/interceptors/page.interceptor.ts | 4 +- src/core/services/common/search.service.ts | 40 +------------------ src/infrastructure/config/env.validation.ts | 2 +- src/infrastructure/modules/index.ts | 1 + src/infrastructure/modules/search.module.ts | 2 +- src/main.ts | 3 ++ 18 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/core/domain/dtos/es-hit.dto.ts b/src/core/domain/dtos/es-hit.dto.ts index 1334729..37af015 100644 --- a/src/core/domain/dtos/es-hit.dto.ts +++ b/src/core/domain/dtos/es-hit.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { IsNotEmpty, IsOptional } from "class-validator"; import { PaperDto } from "./paper.dto"; /** diff --git a/src/core/domain/dtos/index.ts b/src/core/domain/dtos/index.ts index 32eeca0..09dfd59 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 './es-query.dto'; +export * from './es-response.dto'; +export * from './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/paper.dto.ts b/src/core/domain/dtos/paper.dto.ts index 5ee8e60..bd61c37 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 { 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 diff --git a/src/core/domain/dtos/request.dto.ts b/src/core/domain/dtos/request.dto.ts index fffc0ea..8fe1a0f 100644 --- a/src/core/domain/dtos/request.dto.ts +++ b/src/core/domain/dtos/request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { IsDefined, IsNotEmpty, IsOptional } from "class-validator"; import { EsQueryDto } from "./es-query.dto"; import { SearchQueryDto } from "./search-q.dto"; diff --git a/src/core/domain/dtos/search-q.dto.ts b/src/core/domain/dtos/search-q.dto.ts index 28bf733..e5ad8eb 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 { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; /** * List of allowed properties in this DTO diff --git a/src/core/domain/dtos/search-result.dto.ts b/src/core/domain/dtos/search-result.dto.ts index 5097f64..c1c42bd 100644 --- a/src/core/domain/dtos/search-result.dto.ts +++ b/src/core/domain/dtos/search-result.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsArray, IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { IsArray, IsDefined, IsInt, IsNotEmpty } from "class-validator"; import { EsResponseDto } from "./es-response.dto"; /** 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-query-string.interface.ts b/src/core/domain/interfaces/es-query-string.interface.ts index 55c1ee6..5f6dcc8 100644 --- a/src/core/domain/interfaces/es-query-string.interface.ts +++ b/src/core/domain/interfaces/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/index.ts b/src/core/domain/interfaces/index.ts index 8c2a188..c751d45 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 './es-query-string.interface' +export * from './es-response-hits.interface' +export * from './es-pit.interface' +export * from './search-info.interface' \ No newline at end of file diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index adb895e..5f9d913 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -100,7 +100,9 @@ export class PageInterceptor implements NestInterceptor { request.es_query.size += limit - 1; reverse = true; } 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); diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index bcb3278..ea006f9 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -4,8 +4,6 @@ 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"; /** * Search service provider @@ -87,40 +85,4 @@ export class SearchService { } }); } -} - -// 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..cbb486d 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -4,7 +4,7 @@ import { PapersController } from "src/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..eff9b48 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, }); -- 2.39.5 From 5eb20c4727c6f9af639ef6953fe9f768665771e0 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Wed, 17 Aug 2022 20:32:10 +0300 Subject: [PATCH 07/23] Changed exception handling in search service. Reworking tests --- .../controller/papers.controller.ts | 10 +- src/core/interceptors/page.interceptor.ts | 2 +- src/core/services/common/search.service.ts | 18 +- src/test/page.interceptor.spec.ts | 233 ++++++----- src/test/papers.endpoint.spec.ts | 5 +- src/test/search.service.spec.ts | 388 +++++++++++++----- 6 files changed, 443 insertions(+), 213 deletions(-) diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 4aefece..6d0fab4 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -35,8 +35,8 @@ export class PapersController { (response: SearchResultDto) => { return response.data; }, - (error: SearchResultDto) => { - throw new HttpException(error.data, error.statusCode); + (error: HttpException) => { + throw error; } ); } @@ -58,11 +58,11 @@ export class PapersController { @HttpCode(200) getByID(@Param('uuid', ParseUUIDPipe) uuid: string): object { return this.searchService.findByID(uuid).then( - (response) => { + (response: SearchResultDto) => { return response.data; }, - (error) => { - throw new HttpException(error.data, error.status); + (error: HttpException) => { + throw error; } ); } diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 5f9d913..1c9e0fd 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,7 +1,7 @@ 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 { EsResponseDto, PageDto } from "../domain/dtos"; import { EsHitDto } from "../domain/dtos/es-hit.dto"; import { EsQueryDto } from "../domain/dtos/es-query.dto"; import { RequestDto } from "../domain/dtos/request.dto"; diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index ea006f9..a4a5d30 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,9 +1,10 @@ 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 { HttpResponseException } from "src/core/exceptions"; /** * Search service provider @@ -43,17 +44,16 @@ export class SearchService { 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)); }); } catch (error) { - reject(new SearchResultDto(700, error)); + reject(error); } }); } @@ -64,24 +64,22 @@ export class SearchService { * @returns Elasticsearch hits or an error object */ async findByContext(es_query: EsQueryDto): Promise { - console.log(`SEARCH|SERVICE: ${JSON.stringify(es_query, null, 2)}`); return new Promise((resolve, reject) => { try { (this.httpService.get(`http://localhost:${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)); }); } catch (error) { - reject(new SearchResultDto(700, error)); + reject(error); } }); } diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index 057ec1c..8ac120e 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,112 +1,153 @@ -// // 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 { HttpModule } from "@nestjs/axios"; +import { Test } from "@nestjs/testing"; +import exp from "constants"; +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"; -// 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 moduleRef; -// beforeEach(async () => { -// moduleRef = await Test.createTestingModule({ -// imports: [HttpModule], -// controllers: [PapersController], -// providers: [SearchService, PageInterceptor], -// }).compile(); + beforeAll(async () => { + moduleRef = await Test.createTestingModule({ + imports: [HttpModule], + controllers: [PapersController], + providers: [SearchService, PageInterceptor], + }).compile(); -// pageInter = moduleRef.get(PageInterceptor); -// }); + pageInter = moduleRef.get(PageInterceptor); -// describe('intercept()', () => { -// it('Should be defined', () => { -// expect(pageInter).toBeDefined(); -// }); + pageInter.getPIT = jest.fn().mockReturnValue({}); -// 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: [{},], -// })); + execCtxMock.getRequest.mockReturnValue({ + query: { + query: 'thisIsMyQuery', + page: 1, + limit: 5, + order: Order.DESC + } + }); + + callHandlerMock.handle.mockReturnValueOnce( + of({ + total: { value: 1 }, + hits: { hits: [{}] } + }) + ); + }); + + it('Should be defined', () => { + expect(pageInter).toBeDefined(); + }); + + describe('intercept()', () => { + 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.todo('Should touch CallHandler.handle() method'); + + + // 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: [{},], + // })); -// expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Observable); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data).toBeInstanceOf(PageDto); -// done(); -// }); -// }) + // expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Promise); + // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { + // expect(data).toBeInstanceOf(PageDto); + // done(); + // })); + // }) -// 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 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'}], + // })); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data).toEqual({ -// data: expect.anything(), -// meta: expect.anything(), -// }); -// done(); -// }); -// }); + // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { + // expect(data).toEqual({ + // data: expect.anything(), + // meta: expect.anything(), + // }); + // done(); + // })); + // }); -// 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 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), + // })); -// pageInter.intercept(executionContext, callHandler).subscribe((data) => { -// expect(data.meta.hasNext).toEqual(true); -// expect(data.meta.hasPrev).toEqual(false); -// done(); -// }); -// }); + // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { + // expect(data.meta.hasNext).toEqual(true); + // expect(data.meta.hasPrev).toEqual(false); + // done(); + // })); + // }); -// 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), -// })); + // 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), + // })); -// 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.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { + // expect(data.meta).toEqual({ + // total: 15, + // pagenum: 2, + // order: Order.ASC, + // hasNext: true, + // hasPrev: true, + // pagesize: 5 + // }); + // done(); + // })); + // }); + }); -// }); \ No newline at end of file + // describe('getPIT()', () => { + + // }); + + // describe('deletePIT()', () => { + + // }); +}); \ No newline at end of file diff --git a/src/test/papers.endpoint.spec.ts b/src/test/papers.endpoint.spec.ts index b8f17be..2ed6b09 100644 --- a/src/test/papers.endpoint.spec.ts +++ b/src/test/papers.endpoint.spec.ts @@ -1,9 +1,6 @@ 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', () => { @@ -18,7 +15,7 @@ describe('E2E Testing of /papers', () => { await app.init(); }); - it('Should return one, exact item on page', async () => { + it('Should return one exact item on page', async () => { return request(app.getHttpServer()) .get('/papers/eeeb2d01-8315-454e-b33f-3d6caa25db42') .expect(200) diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index d9c5d58..2be5b00 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -1,112 +1,306 @@ -// 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, SearchResultDto } 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: { -// post: jest.fn(), -// }, -// }, -// ], -// imports: [ -// ConfigModule.forRoot({ -// isGlobal: true, -// cache: true, -// expandVariables: true, -// }) -// ], -// }).compile(); + 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(); -// searchService = moduleRef.get(SearchService); -// httpService = moduleRef.get(HttpService); -// }); + searchService = moduleRef.get(SearchService); + httpService = moduleRef.get(HttpService); + }); -// describe('getPIT()', () => { -// it('Should touch HttpService.post() method', () => { -// let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ -// data: {id: '2567'}, -// status: 0, -// statusText: '', -// headers: {}, -// config: {}, -// })); + describe('findByID()', () => { + it('Should touch HttpService.get() method', () => { + let httpGetSpy = jest.spyOn(httpService, 'get'); -// searchService.getPIT(1); -// expect(postMock).toHaveBeenCalled(); -// }); + searchService.findByID(''); + expect(httpGetSpy).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://localhost:${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 SearchResultDto', () => { + // Axios response mock + httpService.get = jest.fn().mockReturnValueOnce( + of({ + status: undefined, + statusText: undefined, + headers: undefined, + config: undefined, + data: { + dummy: 'dum' + } + }) + ); + + searchService.findByID('').then((res) => { + expect(res).toBeInstanceOf(SearchResultDto); + expect(res.data).toEqual({ dummy: 'dum' }); + expect(res.statusCode).toBe(200); + }); + }); -// 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); + console.log(err) + }); + }); + + it('Should throw an HttpException when HttpService.get() fails and throws', () => { + httpService.get = jest.fn().mockImplementationOnce(() => { + throw new HttpException({ oops: 'sorry' }, 999); + }); + + 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://localhost:${process.env.ES_PORT}/_search`, + expect.anything() + ); + }); + + it('Should return a Promise', () => { + expect(searchService.findByContext(null)).toBeInstanceOf(Promise); + }); + + it('Should return a Promise with SearchResultDto', () => { + // Axios response mock + httpService.get = jest.fn().mockReturnValueOnce( + of({ + status: undefined, + statusText: undefined, + headers: undefined, + config: undefined, + data: { + dummy: 'dum' + } + }) + ); + + searchService.findByContext(null).then((res) => { + expect(res).toBeInstanceOf(SearchResultDto); + expect(res.data).toEqual({ dummy: 'dum' }); + expect(res.statusCode).toBe(200); + }); + }); + + // 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); + console.log(err) + }); + }); + + 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); + }); + }); + }); +}); + +/** + * describe('getPIT()', () => { + it('Should touch HttpService.post() method', () => { + let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + searchService.getPIT(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; + + searchService.getPIT(time, unit); + expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); + }); + + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'post').mockImplementation(() => { + throw HttpResponseException; + }); + + expect(searchService.getPIT(1)).rejects.toEqual(HttpResponseException); + }); + + 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: {}, + })); + + expect(searchService.getPIT(1)).resolves.toEqual({ + id: '2567', + keep_alive: '1m', + }); + }); -// }); + }); -// 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 + 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 -- 2.39.5 From 26cd205738e960950a0c91c50c6343ede425aa58 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Thu, 18 Aug 2022 19:52:50 +0300 Subject: [PATCH 08/23] Test coverage for pagination and search service. --- src/core/interceptors/page.interceptor.ts | 62 ++-- src/test/e2e/papers.endpoint.spec.ts | 82 ++++++ src/test/page.interceptor.spec.ts | 335 +++++++++++++++++----- src/test/papers.endpoint.spec.ts | 47 --- src/test/search.service.spec.ts | 81 +----- 5 files changed, 379 insertions(+), 228 deletions(-) create mode 100644 src/test/e2e/papers.endpoint.spec.ts delete mode 100644 src/test/papers.endpoint.spec.ts diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 1c9e0fd..934f058 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,8 +1,7 @@ import { HttpService } from "@nestjs/axios"; import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; import { Observable, map, take } from "rxjs"; -import { EsResponseDto, PageDto } from "../domain/dtos"; -import { EsHitDto } from "../domain/dtos/es-hit.dto"; +import { PageDto } from "../domain/dtos"; import { EsQueryDto } from "../domain/dtos/es-query.dto"; import { RequestDto } from "../domain/dtos/request.dto"; import { SearchQueryDto } from "../domain/dtos/search-q.dto"; @@ -10,12 +9,11 @@ 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"; /** * 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,23 +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; @@ -116,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; } @@ -165,12 +181,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://localhost:${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); } diff --git a/src/test/e2e/papers.endpoint.spec.ts b/src/test/e2e/papers.endpoint.spec.ts new file mode 100644 index 0000000..1fa3122 --- /dev/null +++ b/src/test/e2e/papers.endpoint.spec.ts @@ -0,0 +1,82 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication } from "@nestjs/common"; +import request from 'supertest' +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('GET /papers/{uuid} | Should return one exact item on page', async () => { + const test = await request(app.getHttpServer()) + .get('/papers/eeeb2d01-8315-454e-b33f-3d6caa25db42') // ??? Fetch a random object from DB + .expect(200); + + // Checking received data + expect(test.body.data).toBeDefined(); + + expect(test.body.data.length).toBe(1); + expect(test.body.data[0].id).toBeDefined(); + expect(test.body.data[0].title).toBeDefined(); + expect(test.body.data[0].authors).toBeDefined(); + expect(test.body.data[0].summary).toBeDefined(); + expect(test.body.data[0].tags).toBeDefined(); + expect(test.body.data[0].content).toBeDefined(); + expect(test.body.data[0].id).toBe('eeeb2d01-8315-454e-b33f-3d6caa25db42'); + + // Checking received meta + expect(test.body.meta).toBeDefined(); + + expect(test.body.meta.total).toBeDefined(); + expect(test.body.meta.pagenum).toBeDefined(); + expect(test.body.meta.order).toBeDefined(); + expect(test.body.meta.pagesize).toBeDefined(); + expect(test.body.meta.hasNext).toBeDefined(); + expect(test.body.meta.hasPrev).toBeDefined(); + expect(test.body.meta.total).toBe(1); + expect(test.body.meta.pagenum).toBe(1); + }); + + it('GET /papers/search? | Should return multiple items', async () => { + const test = await request(app.getHttpServer()) + .get('/papers/search?query=at&page=1') + .expect(200); + + // Checking received data + expect(test.body.data).toBeDefined(); + + expect(test.body.data.length).toBeGreaterThan(0); + for (const paper of test.body.data) { + expect(paper.id).toBeDefined(); + expect(paper.title).toBeDefined(); + expect(paper.authors).toBeDefined(); + expect(paper.summary).toBeDefined(); + expect(paper.tags).toBeDefined(); + expect(paper.content).toBeDefined(); + } + + // Checking received meta + expect(test.body.meta).toBeDefined(); + + expect(test.body.meta.total).toBeDefined(); + expect(test.body.meta.pagenum).toBeDefined(); + expect(test.body.meta.order).toBeDefined(); + expect(test.body.meta.pagesize).toBeDefined(); + expect(test.body.meta.hasNext).toBeDefined(); + expect(test.body.meta.hasPrev).toBeDefined(); + expect(test.body.meta.total).toBeGreaterThan(0); + expect(test.body.meta.pagenum).toBe(1); + }); + + afterAll(async () => { + await app.close(); + }) +}); \ No newline at end of file diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index 8ac120e..c6fe8a7 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,12 +1,12 @@ -import { HttpModule } from "@nestjs/axios"; +import { HttpService } from "@nestjs/axios"; +import { ConfigModule } from "@nestjs/config"; +import { ModuleRef } from "@nestjs/core"; import { Test } from "@nestjs/testing"; -import exp from "constants"; 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 { 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"; -import { SearchService } from "src/core/services/common/search.service"; const execCtxMock = { switchToHttp: jest.fn().mockReturnThis(), @@ -26,18 +26,31 @@ const callHandlerMock = { describe('Unit tests for PageInterceptor', () => { let pageInter: PageInterceptor; - let moduleRef; + let httpService: HttpService; beforeAll(async () => { - moduleRef = await Test.createTestingModule({ - imports: [HttpModule], - controllers: [PapersController], - providers: [SearchService, PageInterceptor], + 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.getPIT = jest.fn().mockReturnValue({}); + httpService = moduleRef.get(HttpService); execCtxMock.getRequest.mockReturnValue({ query: { @@ -48,19 +61,32 @@ describe('Unit tests for PageInterceptor', () => { } }); - callHandlerMock.handle.mockReturnValueOnce( + callHandlerMock.handle.mockReturnValue( of({ - total: { value: 1 }, - hits: { hits: [{}] } + 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); }); @@ -73,81 +99,234 @@ describe('Unit tests for PageInterceptor', () => { }); }); }); - - it.todo('Should touch CallHandler.handle() method'); + it('Should touch CallHandler.handle() method', () => { + let chHandleSpy = jest.spyOn(callHandlerMock, 'handle'); + pageInter.intercept(execCtxMock, callHandlerMock); + expect(chHandleSpy).toBeCalled(); + }); - // 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: [{},], - // })); - - - // expect(pageInter.intercept(executionContext, callHandler)).toBeInstanceOf(Promise); - // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { - // expect(data).toBeInstanceOf(PageDto); - // done(); - // })); - // }) + it('Should construct a page with proper data on it', () => { + callHandlerMock.handle.mockReturnValueOnce( + of({ + hits: { + total: { value: 1 }, + hits: [{ + _source: { + dummy: 'dum' + } + }] + } + }) + ); - // 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'}], - // })); + pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { + res.subscribe((page) => { + expect(page.data.length).toBe(1); + expect(page.data[0]).toEqual({ dummy: 'dum' }); + }); + }); + }); - // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { - // expect(data).toEqual({ - // data: expect.anything(), - // meta: expect.anything(), - // }); - // done(); - // })); - // }); + it('Should construct correct meta-data of the page', () => { + execCtxMock.getRequest.mockReturnValueOnce({ + query: { + page: 5, + order: 'desc', + limit: 100, + } + }); - // 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), - // })); + callHandlerMock.handle.mockReturnValueOnce( + of({ + hits: { + total: { value: 921 }, + hits: [] + } + }) + ); - // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { - // expect(data.meta.hasNext).toEqual(true); - // expect(data.meta.hasPrev).toEqual(false); - // done(); - // })); - // }); + 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 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), - // })); + it('Should reverse the search results', () => { + execCtxMock.getRequest.mockReturnValueOnce({ + query: { + page: 1, + order: 'desc', + limit: 3 + } + }); - // pageInter.intercept(executionContext, callHandler).then(res => res.subscribe((data) => { - // expect(data.meta).toEqual({ - // total: 15, - // pagenum: 2, - // order: Order.ASC, - // hasNext: true, - // hasPrev: true, - // pagesize: 5 - // }); - // done(); - // })); - // }); + 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()', () => { + 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://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 httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + data: {id: '2567'}, + status: 0, + statusText: '', + headers: {}, + config: {}, + })); + + let time = 2; + let unit = EsTime.sec; + + pageInter.getPIT(time, unit); + expect(httpPostMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); + }); + + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'post').mockImplementationOnce(() => { + throw HttpResponseException; + }); + + expect(pageInter.getPIT(1)).rejects.toEqual(HttpResponseException); + }); + + 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: {}, + })); + + expect(pageInter.getPIT(1)).resolves.toEqual({ + id: '2567', + keep_alive: '1m', + }); + }); + }); // describe('deletePIT()', () => { + // it('Should touch HttpService.delete() method', () => { + // let httpPostMock = jest.spyOn(httpService, 'delete').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://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 httpPostMock = jest.spyOn(httpService, 'post').mockReturnValueOnce(of({ + // data: {id: '2567'}, + // status: 0, + // statusText: '', + // headers: {}, + // config: {}, + // })); + + // let time = 2; + // let unit = EsTime.sec; + + // pageInter.getPIT(time, unit); + // expect(httpPostMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); + // }); + + // it('Should return error exeception when HttpService fails', () => { + // jest.spyOn(httpService, 'post').mockImplementationOnce(() => { + // throw HttpResponseException; + // }); + + // expect(pageInter.getPIT(1)).rejects.toEqual(HttpResponseException); + // }); + + // 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: {}, + // })); + + // expect(pageInter.getPIT(1)).resolves.toEqual({ + // id: '2567', + // keep_alive: '1m', + // }); + // }); // }); }); \ 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 2ed6b09..0000000 --- a/src/test/papers.endpoint.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { INestApplication } from "@nestjs/common"; -import request from 'supertest' -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 2be5b00..95ded23 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -10,7 +10,6 @@ describe('Unit tests for SearchService', () => { let searchService: SearchService; let httpService: HttpService; - beforeAll(async () => { const moduleRef = await Test.createTestingModule({ providers: [ @@ -114,7 +113,6 @@ describe('Unit tests for SearchService', () => { searchService.findByID('').catch((err) => { expect(err).toBeInstanceOf(GatewayTimeoutException); - console.log(err) }); }); @@ -211,7 +209,6 @@ describe('Unit tests for SearchService', () => { searchService.findByContext(null).catch((err) => { expect(err).toBeInstanceOf(GatewayTimeoutException); - console.log(err) }); }); @@ -227,80 +224,4 @@ describe('Unit tests for SearchService', () => { }); }); }); -}); - -/** - * describe('getPIT()', () => { - it('Should touch HttpService.post() method', () => { - let postMock = jest.spyOn(httpService, 'post').mockReturnValue(of({ - data: {id: '2567'}, - status: 0, - statusText: '', - headers: {}, - config: {}, - })); - - searchService.getPIT(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; - - searchService.getPIT(time, unit); - expect(postMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); - }); - - it('Should return error exeception when HttpService fails', () => { - jest.spyOn(httpService, 'post').mockImplementation(() => { - throw HttpResponseException; - }); - - expect(searchService.getPIT(1)).rejects.toEqual(HttpResponseException); - }); - - 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: {}, - })); - - expect(searchService.getPIT(1)).resolves.toEqual({ - id: '2567', - keep_alive: '1m', - }); - }); - - - }); - - 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 +}); \ No newline at end of file -- 2.39.5 From 22598ae2bc738859cfbf5c67241900901a5972b8 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Fri, 19 Aug 2022 03:14:06 +0300 Subject: [PATCH 09/23] Added more tests. Unit tests for PapersController. --- .../controller/papers.controller.ts | 4 +- ....spec.ts => papers.controller.e2e.spec.ts} | 0 src/test/page.interceptor.spec.ts | 108 ++++++++---------- src/test/papers.controller.spec.ts | 99 ++++++++++++++++ 4 files changed, 148 insertions(+), 63 deletions(-) rename src/test/e2e/{papers.endpoint.spec.ts => papers.controller.e2e.spec.ts} (100%) create mode 100644 src/test/papers.controller.spec.ts diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 6d0fab4..7a18f12 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -35,7 +35,7 @@ export class PapersController { (response: SearchResultDto) => { return response.data; }, - (error: HttpException) => { + (error) => { throw error; } ); @@ -61,7 +61,7 @@ export class PapersController { (response: SearchResultDto) => { return response.data; }, - (error: HttpException) => { + (error) => { throw error; } ); diff --git a/src/test/e2e/papers.endpoint.spec.ts b/src/test/e2e/papers.controller.e2e.spec.ts similarity index 100% rename from src/test/e2e/papers.endpoint.spec.ts rename to src/test/e2e/papers.controller.e2e.spec.ts diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index c6fe8a7..25fafbd 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,7 +1,6 @@ import { HttpService } from "@nestjs/axios"; import { ConfigModule } from "@nestjs/config"; -import { ModuleRef } from "@nestjs/core"; -import { Test } from "@nestjs/testing"; + import { Test } from "@nestjs/testing"; import { Observable, of } from "rxjs"; import { EsTime, Order } from "src/core/domain"; import { PageDto } from "src/core/domain/dtos"; @@ -263,70 +262,57 @@ describe('Unit tests for PageInterceptor', () => { }); }); - // describe('deletePIT()', () => { - // it('Should touch HttpService.delete() method', () => { - // let httpPostMock = jest.spyOn(httpService, 'delete').mockReturnValueOnce(of({ - // data: {id: '2567'}, - // status: 0, - // statusText: '', - // headers: {}, - // config: {}, - // })); + 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.getPIT(1); - // expect(httpPostMock).toHaveBeenCalled(); - // }); + pageInter.deletePIT(''); + expect(httpDeleteMock).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: {}, - // })); + 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.getPIT(1); - // expect(httpPostMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); - // }); + pageInter.deletePIT('thisIsIDSpecified'); + expect(httpDeleteMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/_pit`, { + data: { id: 'thisIsIDSpecified' }, + headers: { 'Content-Type': 'application/json' } + }); + }); - // 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: {}, - // })); + it('Should return error exeception when HttpService fails', () => { + jest.spyOn(httpService, 'delete').mockImplementationOnce(() => { + throw HttpResponseException; + }); - // let time = 2; - // let unit = EsTime.sec; - - // pageInter.getPIT(time, unit); - // expect(httpPostMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); - // }); + expect(pageInter.deletePIT('')).rejects.toEqual(HttpResponseException); + }); - // it('Should return error exeception when HttpService fails', () => { - // jest.spyOn(httpService, 'post').mockImplementationOnce(() => { - // throw 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.getPIT(1)).rejects.toEqual(HttpResponseException); - // }); - - // 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: {}, - // })); - - // expect(pageInter.getPIT(1)).resolves.toEqual({ - // id: '2567', - // keep_alive: '1m', - // }); - // }); - // }); + 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..a4e4382 --- /dev/null +++ b/src/test/papers.controller.spec.ts @@ -0,0 +1,99 @@ +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({ + data: { + took: undefined, + timed_out: undefined, + hits: undefined, + _shards: undefined, + }, + statusCode: 0 + }); + + 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({ + data: searchResultMock, + statusCode: 200 + }); + + 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.todo('Should touch SearchService.findByID() method'); + it.todo('Should resolve, when searched successfully'); + it.todo('Should throw, when search was unsuccessful'); + }); +}); \ No newline at end of file -- 2.39.5 From fa130ddf16e3c07e55356ea951667cbb17737bd5 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Wed, 24 Aug 2022 20:57:54 +0300 Subject: [PATCH 10/23] Full covered Swagger doc. added. + Fix on tests --- docker-compose.yaml | 10 +- .../classes/EnvironmentVariables.html | 2 +- documentation/classes/EsHitDto.html | 23 +- documentation/classes/EsQueryDto.html | 37 +- documentation/classes/EsResponseDto.html | 47 +- documentation/classes/PageDto.html | 23 +- documentation/classes/PageMetaDto.html | 536 ++++++++++++++++++ documentation/classes/PaperDto.html | 19 +- documentation/classes/PrevSearch.html | 415 ++++++++++---- documentation/classes/RequestDto.html | 29 +- documentation/classes/SearchQueryDto.html | 15 +- documentation/classes/SearchResultDto.html | 18 +- .../controllers/PapersController.html | 100 ++-- documentation/coverage.html | 48 +- documentation/graph/dependencies.svg | 204 +++---- .../injectables/PageInterceptor.html | 215 ++++--- documentation/injectables/SearchService.html | 130 ++--- documentation/interfaces/EqQueryString.html | 7 +- documentation/interfaces/EsPit.html | 2 +- documentation/interfaces/EsQuery.html | 2 +- documentation/interfaces/EsResponseHits.html | 4 +- documentation/interfaces/SearchInfo.html | 2 +- documentation/js/menu-wc.js | 15 +- documentation/js/menu-wc_es5.js | 2 +- documentation/js/search/search_index.js | 4 +- documentation/miscellaneous/variables.html | 196 ++++--- .../modules/CommonModule/dependencies.svg | 8 +- documentation/modules/HttpResponseModule.html | 8 +- documentation/modules/LoggerModule.html | 8 +- .../modules/LoggerModule/dependencies.svg | 8 +- documentation/modules/SearchModule.html | 6 +- documentation/overview.html | 2 +- package-lock.json | 20 +- package.json | 7 +- .../controller/papers.controller.ts | 63 +- .../domain/dtos/{ => elastic}/es-hit.dto.ts | 9 +- .../domain/dtos/{ => elastic}/es-query.dto.ts | 15 +- .../dtos/{ => elastic}/es-response.dto.ts | 25 +- src/core/domain/dtos/index.ts | 6 +- src/core/domain/dtos/page-meta.dto.ts | 73 +++ src/core/domain/dtos/page.dto.ts | 13 +- src/core/domain/dtos/paper.dto.ts | 3 +- src/core/domain/dtos/request.dto.ts | 17 +- src/core/domain/dtos/search-q.dto.ts | 3 +- src/core/domain/dtos/search-result.dto.ts | 10 +- .../{ => elastic}/es-pit.interface.ts | 0 .../es-query-string.interface.ts | 0 .../{ => elastic}/es-query.interface.ts | 0 .../es-response-hits.interface.ts | 2 +- src/core/domain/interfaces/index.ts | 10 +- .../interfaces/search-info.interface.ts | 2 +- src/core/interceptors/page.interceptor.ts | 13 +- src/core/services/common/search.service.ts | 24 +- src/infrastructure/modules/search.module.ts | 2 +- src/main.ts | 8 +- src/test/e2e/papers.controller.e2e.spec.ts | 209 +++++-- src/test/jest-e2e.json | 9 + src/test/page.interceptor.spec.ts | 6 +- src/test/papers.controller.spec.ts | 69 ++- src/test/search.service.spec.ts | 81 ++- 60 files changed, 1987 insertions(+), 857 deletions(-) create mode 100644 documentation/classes/PageMetaDto.html rename src/core/domain/dtos/{ => elastic}/es-hit.dto.ts (81%) rename src/core/domain/dtos/{ => elastic}/es-query.dto.ts (81%) rename src/core/domain/dtos/{ => elastic}/es-response.dto.ts (73%) create mode 100644 src/core/domain/dtos/page-meta.dto.ts rename src/core/domain/interfaces/{ => elastic}/es-pit.interface.ts (100%) rename src/core/domain/interfaces/{ => elastic}/es-query-string.interface.ts (100%) rename src/core/domain/interfaces/{ => elastic}/es-query.interface.ts (100%) rename src/core/domain/interfaces/{ => elastic}/es-response-hits.interface.ts (84%) create mode 100644 src/test/jest-e2e.json 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 @@ @@ -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

                      +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - +
                      - Type : PageMeta + Type : PageMetaDto
                      - +
                      + + + + + + + + + + + + + + +
                      +
                      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 7a18f12..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,19 +22,25 @@ 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) => { throw error; @@ -47,19 +54,25 @@ 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: SearchResultDto) => { - return response.data; + (response: EsResponseDto) => { + return response.hits.hits[0]._source; }, (error) => { 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 81% rename from src/core/domain/dtos/es-hit.dto.ts rename to src/core/domain/dtos/elastic/es-hit.dto.ts index 37af015..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 { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsNotEmpty, IsOptional } from "class-validator"; -import { PaperDto } from "./paper.dto"; +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 09dfd59..d24be14 100644 --- a/src/core/domain/dtos/index.ts +++ b/src/core/domain/dtos/index.ts @@ -1,6 +1,6 @@ -export * from './es-query.dto'; -export * from './es-response.dto'; -export * from './es-hit.dto'; +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'; 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 bd61c37..07e1d7d 100644 --- a/src/core/domain/dtos/paper.dto.ts +++ b/src/core/domain/dtos/paper.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; /** @@ -9,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 8fe1a0f..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 { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsDefined, IsNotEmpty, IsOptional } from "class-validator"; -import { EsQueryDto } from "./es-query.dto"; +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 e5ad8eb..8834657 100644 --- a/src/core/domain/dtos/search-q.dto.ts +++ b/src/core/domain/dtos/search-q.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; /** @@ -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 c1c42bd..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 { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; import { IsArray, IsDefined, IsInt, IsNotEmpty } from "class-validator"; -import { EsResponseDto } from "./es-response.dto"; +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/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 100% rename from src/core/domain/interfaces/es-query-string.interface.ts rename to src/core/domain/interfaces/elastic/es-query-string.interface.ts 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 c751d45..6295a5f 100644 --- a/src/core/domain/interfaces/index.ts +++ b/src/core/domain/interfaces/index.ts @@ -1,7 +1,7 @@ export * from './http-response.interface' export * from './page-meta.interface' -export * from './es-query.interface' -export * from './es-query-string.interface' -export * from './es-response-hits.interface' -export * from './es-pit.interface' -export * from './search-info.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 934f058..76e1934 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -2,13 +2,13 @@ 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 { 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 { EsPit } from "../domain/interfaces/elastic/es-pit.interface"; /** * Previous search data storage @@ -168,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 */ @@ -181,7 +186,7 @@ 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: EsPit) => { res.keep_alive = alive + unit; @@ -201,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 a4a5d30..609ce0d 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,10 +1,8 @@ import { HttpService } from "@nestjs/axios"; 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 { HttpResponseException } from "src/core/exceptions"; +import { EsResponseDto} from "../../domain/dtos"; +import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto"; /** * Search service provider @@ -23,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; @@ -40,7 +43,7 @@ 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'}, })) @@ -49,8 +52,7 @@ export class SearchService { if (res.timed_out) { reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } - - resolve(new SearchResultDto(200, res)); + resolve(res); }); } catch (error) { reject(error); @@ -63,10 +65,10 @@ export class SearchService { * @param query, * @returns Elasticsearch hits or an error object */ - async findByContext(es_query: EsQueryDto): Promise { + 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'}, })) @@ -76,7 +78,7 @@ export class SearchService { reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } - resolve(new SearchResultDto(200, res)); + resolve(res); }); } catch (error) { reject(error); diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index cbb486d..526c7d7 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,6 +1,6 @@ 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"; /** diff --git a/src/main.ts b/src/main.ts index eff9b48..c3efc9c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -30,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 index 1fa3122..922bdc0 100644 --- a/src/test/e2e/papers.controller.e2e.spec.ts +++ b/src/test/e2e/papers.controller.e2e.spec.ts @@ -1,79 +1,224 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import request from 'supertest' -import { AppModule } from "src/infrastructure/modules"; +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], + 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 on page', async () => { + 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/eeeb2d01-8315-454e-b33f-3d6caa25db42') // ??? Fetch a random object from DB + .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.data).toBeDefined(); - - expect(test.body.data.length).toBe(1); - expect(test.body.data[0].id).toBeDefined(); - expect(test.body.data[0].title).toBeDefined(); - expect(test.body.data[0].authors).toBeDefined(); - expect(test.body.data[0].summary).toBeDefined(); - expect(test.body.data[0].tags).toBeDefined(); - expect(test.body.data[0].content).toBeDefined(); - expect(test.body.data[0].id).toBe('eeeb2d01-8315-454e-b33f-3d6caa25db42'); + expect(test.body).toBeDefined(); + expect(test.body.id).toBeDefined(); + expect(test.body.id).toBe('thisIsIDofTheFirstObject'); - // Checking received meta - expect(test.body.meta).toBeDefined(); + expect(test.body.title).toBeDefined(); + expect(test.body.title).toBe('thisIsTitle'); - expect(test.body.meta.total).toBeDefined(); - expect(test.body.meta.pagenum).toBeDefined(); - expect(test.body.meta.order).toBeDefined(); - expect(test.body.meta.pagesize).toBeDefined(); - expect(test.body.meta.hasNext).toBeDefined(); - expect(test.body.meta.hasPrev).toBeDefined(); - expect(test.body.meta.total).toBe(1); - expect(test.body.meta.pagenum).toBe(1); + 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', async () => { + 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); - expect(test.body.data.length).toBeGreaterThan(0); 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' + }); - // Checking received meta + 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.order).toBeDefined(); - expect(test.body.meta.pagesize).toBeDefined(); - expect(test.body.meta.hasNext).toBeDefined(); - expect(test.body.meta.hasPrev).toBeDefined(); - expect(test.body.meta.total).toBeGreaterThan(0); 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 () => { 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 25fafbd..e3fda46 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -219,7 +219,7 @@ describe('Unit tests for PageInterceptor', () => { })); pageInter.getPIT(1); - expect(httpPostMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=1m`); + 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', () => { @@ -235,7 +235,7 @@ describe('Unit tests for PageInterceptor', () => { let unit = EsTime.sec; pageInter.getPIT(time, unit); - expect(httpPostMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); + expect(httpPostMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/papers/_pit?keep_alive=${time+unit}`); }); it('Should return error exeception when HttpService fails', () => { @@ -288,7 +288,7 @@ describe('Unit tests for PageInterceptor', () => { })); pageInter.deletePIT('thisIsIDSpecified'); - expect(httpDeleteMock).toHaveBeenCalledWith(`http://localhost:${process.env.ES_PORT}/_pit`, { + expect(httpDeleteMock).toHaveBeenCalledWith(`http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_pit`, { data: { id: 'thisIsIDSpecified' }, headers: { 'Content-Type': 'application/json' } }); diff --git a/src/test/papers.controller.spec.ts b/src/test/papers.controller.spec.ts index a4e4382..5a1ff79 100644 --- a/src/test/papers.controller.spec.ts +++ b/src/test/papers.controller.spec.ts @@ -37,13 +37,10 @@ describe('Unit tests for PapersController', () => { it('Should touch SearchService.findByContext() method', () => { let findCtxMock = jest.spyOn(searchService, 'findByContext') .mockResolvedValueOnce({ - data: { - took: undefined, - timed_out: undefined, - hits: undefined, - _shards: undefined, - }, - statusCode: 0 + took: undefined, + timed_out: undefined, + hits: undefined, + _shards: undefined, }); papersController.getByContext({ query: undefined }); @@ -74,10 +71,7 @@ describe('Unit tests for PapersController', () => { }; jest.spyOn(searchService, 'findByContext') - .mockResolvedValueOnce({ - data: searchResultMock, - statusCode: 200 - }); + .mockResolvedValueOnce(searchResultMock); expect(papersController.getByContext({ query: undefined })).resolves.toEqual(searchResultMock); }); @@ -92,8 +86,55 @@ describe('Unit tests for PapersController', () => { }); describe('getByID()', () => { - it.todo('Should touch SearchService.findByID() method'); - it.todo('Should resolve, when searched successfully'); - it.todo('Should throw, when search was unsuccessful'); + 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/search.service.spec.ts b/src/test/search.service.spec.ts index 95ded23..864f6c3 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -3,7 +3,7 @@ import { GatewayTimeoutException, HttpException } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { Test } from "@nestjs/testing"; import { of } from "rxjs"; -import { EsQueryDto, SearchResultDto } from "src/core/domain"; +import { EsQueryDto, EsResponseDto } from "src/core/domain"; import { SearchService } from "src/core/services/common/search.service"; describe('Unit tests for SearchService', () => { @@ -65,7 +65,7 @@ describe('Unit tests for SearchService', () => { searchService.findByID(''); expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>( - `http://localhost:${process.env.ES_PORT}/_search`, + `http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_search`, expect.anything() ); }); @@ -74,26 +74,27 @@ describe('Unit tests for SearchService', () => { expect(searchService.findByID('')).toBeInstanceOf(Promise); }); - it('Should return a Promise with SearchResultDto', () => { - // Axios response mock - httpService.get = jest.fn().mockReturnValueOnce( - of({ - status: undefined, - statusText: undefined, - headers: undefined, - config: undefined, - data: { - dummy: 'dum' - } - }) - ); + // 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: [{}] + // } + // }, + // }) + // ); - searchService.findByID('').then((res) => { - expect(res).toBeInstanceOf(SearchResultDto); - expect(res.data).toEqual({ dummy: 'dum' }); - expect(res.statusCode).toBe(200); - }); - }); + // expect(searchService.findByID('')).resolves.toBeInstanceOf(EsResponseDto) + // }); // Errors it('Should throw 504 | GatewayTimeoutException', () => { @@ -161,7 +162,7 @@ describe('Unit tests for SearchService', () => { searchService.findByContext(null); expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>( - `http://localhost:${process.env.ES_PORT}/_search`, + `http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_search`, expect.anything() ); }); @@ -170,26 +171,22 @@ describe('Unit tests for SearchService', () => { expect(searchService.findByContext(null)).toBeInstanceOf(Promise); }); - it('Should return a Promise with SearchResultDto', () => { - // Axios response mock - httpService.get = jest.fn().mockReturnValueOnce( - of({ - status: undefined, - statusText: undefined, - headers: undefined, - config: undefined, - data: { - dummy: 'dum' - } - }) - ); - - searchService.findByContext(null).then((res) => { - expect(res).toBeInstanceOf(SearchResultDto); - expect(res.data).toEqual({ dummy: 'dum' }); - expect(res.statusCode).toBe(200); - }); - }); + // 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', () => { -- 2.39.5 From d283cc22f13cb5f4af90f4d07316b6886297e230 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Thu, 25 Aug 2022 18:12:27 +0300 Subject: [PATCH 11/23] Dockerfile changes --- .env.develop | 7 ++++++- Dockerfile | 3 +++ docker-compose.yaml | 15 +++++++-------- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.env.develop b/.env.develop index 868ed15..b7c3f27 100644 --- a/.env.develop +++ b/.env.develop @@ -8,4 +8,9 @@ CONTAINER_NAME="container-name" ES_PORT=9200 ES_IMAGE_VERSION=8.3.2 ES_IMAGE_NAME="image-name" -ES_CONTAINER_NAME="container-name" \ No newline at end of file +ES_CONTAINER_NAME="container-name" + +MDB_PORT=27017 +MDB_IMAGE_NAME="image-name" +MDB_IMAGE_VERSION="latest" +MDB_CONTAINER_NAME="container-name" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 22be726..1c66e2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,9 @@ ENV MS_SCRIPTS="${MS_HOME}/scripts" ENV USER_NAME=node USER_UID=1000 GROUP_NAME=node GROUP_UID=1000 +ENV ES_PORT=9200 +ENV MDB_PORT=27017 + WORKDIR "${MS_HOME}" # Build diff --git a/docker-compose.yaml b/docker-compose.yaml index 46a132b..85a9ae7 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,12 +10,12 @@ services: environment: - xpack.security.enabled=false - discovery.type=single-node - - - - - - + mongo: + image: ${MDB_IMAGE_NAME}:${MDB_IMAGE_VERSION} + container_name: ${MDB_CONTAINER_NAME} + restart: always + ports: + - ${MDB_PORT}:${MDB_PORT} freeland: image: ${IMAGE_NAME}:${IMAGE_VERSION} build: @@ -29,5 +29,4 @@ services: - "${LOCAL_PORT}:${NODE_PORT}" environment: - NODE_ENV= ${NODE_ENV} - - NODE_PORT= ${NODE_PORT} - \ No newline at end of file + - NODE_PORT= ${NODE_PORT} \ No newline at end of file -- 2.39.5 From 9f430dc54d88689afae634eb26f550fd5b2fb9f4 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Wed, 14 Sep 2022 12:26:03 +0300 Subject: [PATCH 12/23] Added config script for Elastcsearch --- elastic/scripts/es_boot.sh | 208 +++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100755 elastic/scripts/es_boot.sh diff --git a/elastic/scripts/es_boot.sh b/elastic/scripts/es_boot.sh new file mode 100755 index 0000000..5e5f2eb --- /dev/null +++ b/elastic/scripts/es_boot.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash + +__isint='^[0-9]+$' +__isvalidstr='^[a-z0-9]+$' +__isvalidaddr='^[a-z]+$|^((25[0-5]|2[0-4]?[0-9]|1[0-9]?[0-9]|[3-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4]?[0-9]|1[0-9]?[0-9]|[3-9][0-9]|[0-9])$' + +create_template() { + curl -H "Content-Type: application/json" -X PUT "$1:$2/_index_template/papers_t" -d ' + { + "index_patterns": ["papers*"], + "priority": 1, + "template": { + "aliases": { + "papers": {} + }, + "mappings": { + "properties": { + "id": { + "type": "keyword" + }, + "title": { + "type": "text", + "analyzer": "title_analyzer" + }, + "authors": { + "type": "text" + }, + "topic": { + "type": "text" + }, + "summary": { + "type": "text" + }, + "tags": { + "type": "keyword" + }, + "content": { + "type": "text" + }, + "publisher": { + "type": "text" + } + } + }, + "settings": { + "analysis": { + "analyzer": { + "title_analyzer": { + "type": "custom", + "tokenizer": "title_engram_tokenizer" + }, + "content_analyzer_i": { + "type": "custom", + "tokenizer": "content_onchar_tokenizer", + "char_filter": [ + "markdown_token_filter" + ] + }, + "content_analyzer_s": { + "type": "custom", + "tokenizer": "content_onchar_tokenizer", + "char_filter": [ + "markdown_token_filter" + ], + "filter": [ + ] + }, + "basic_analyzer": { + + } + }, + "tokenizer": { + "title_engram_tokenizer": { + "type": "edge_ngram", + "min_gram": 2, + "max_gram": 10, + "token_chars": [ + "letter", + "digit" + ] + }, + "content_onchar_tokenizer": { + "type": "char_group", + "tokenize_on_chars": [ + "whitespace", + ".", ",", "(", ")", "-", "[", "]", "{", + "}", "#", ":", ";", "`", "!", "*" + ] + } + }, + "char_filter": { + "markdown_token_filter": { + "type": "pattern_replace", + "pattern": "[[a-z][0-9]]*://[[a-z][0-9]]*.[a-z]*", + "replacement": "" + } + }, + "filter": { + } + } + } + } + } + ' +} + +#============================================================================================================================================================================= + +create_index() { + curl -X PUT "$1:$2/papers-$3?pretty" +} + +#============================================================================================================================================================================= + +__usage=" + Usage: $(basename $0) + --------------------------------------------------------------------------- + | -c, --create-only | Skip template initialization and only create | + | | specified index. Result index name will be | + | | 'papers-{specified name}' | + | | + | -h | Help information | + | | + | -a | Specifies the address | + | | + | -p, --port | Specifies the port | + | | + | -i, --index-name | Specifies the index name: | + | | Must be lowercase, cannot include [\/*?\"<>| ,#:], | + | | cannot start with [.-_+], cannot be \".\" or \"..\" | + | | cannot be longer than 255 bytes (note: multi-byte | + | | characters will count towards the limit faster) | + | | Result index name will be 'papers-{specified name}' | + --------------------------------------------------------------------------- +" +#============================================================================================================================================================================= + +CTRL=0 + +if [[ "$1" == "-h" ]]; then + echo "$__usage" +else + while [[ $# -gt 0 ]]; do + case "$1" in + -p | --port) + if [[ -n "$2" && $2 =~ $__isint && "$2" -ge 1 && "$2" -le 65535 ]]; then + PORT="$2" + shift + else + echo "Invalid port number!" + fi + ;; + -a | --address) + if [[ -n "$2" ]]; then + IP="$2" + shift + else + echo "Address is not specified!" + fi + ;; + -i | --index-name) + if [[ -n "$2" && $2 =~ $__isvalidstr ]]; then + IND="$2" + shift + else + echo "Index name is not specified!" + fi + ;; + -c | --create-only) + CTRL=2 + ;; + -*) + echo "Option '$1' is not supported" + exit + ;; + *) + if [[ $1 =~ $__isvalidaddr ]]; then + IP="$1" + elif [[ $1 =~ $__isint && "$1" -ge 1 && "$1" -le 65535 ]]; then + PORT="$1" + elif [[ $1 =~ $__isvalidstr ]]; then + IND="$1" + else + echo "Invalid argument!" + exit; + fi + ;; + esac + shift + done + + echo "Specified: $IP:$PORT | Index name: $IND" + + case $CTRL in + 0) # Default behaviour - full initialization (template creation and index creation) + create_template "$IP" "$PORT" + echo "Elasticsearch index template created" + create_index "$IP" "$PORT" "$IND" + echo "Elasticsearch index (papers-$IND) created" + exit + ;; + 2) # Create index, skip creating the template + create_index "$IP" "$PORT" "$IND" + echo "Elasticsearch index (papers-$IND) created" + exit + ;; + esac +fi \ No newline at end of file -- 2.39.5 From b6287509ad9e5e4ae4c07f0351606634a0cacad2 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Wed, 14 Sep 2022 13:12:51 +0300 Subject: [PATCH 13/23] Implemented In-Memory-Caching with default CacheModule --- .../controller/papers.controller.ts | 3 +- src/core/interceptors/page.interceptor.ts | 234 +++++++----------- src/infrastructure/modules/search.module.ts | 3 +- src/test/e2e/papers.controller.e2e.spec.ts | 2 +- src/test/page.interceptor.spec.ts | 20 +- src/test/papers.controller.spec.ts | 7 +- 6 files changed, 108 insertions(+), 161 deletions(-) diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 74efcdf..3151334 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common"; +import { CacheInterceptor, Controller, Get, HttpCode, Inject, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; import { PageInterceptor } from "../../core/interceptors/page.interceptor"; import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from "@nestjs/swagger"; @@ -13,6 +13,7 @@ import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain"; path: 'papers', }) @ApiExtraModels(RequestDto, EsHitDto, EsResponseDto) +// @UseInterceptors(CacheInterceptor) export class PapersController { constructor(private searchService: SearchService) {} diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 76e1934..01eda50 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,6 +1,6 @@ import { HttpService } from "@nestjs/axios"; -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; -import { Observable, map, take } from "rxjs"; +import { CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common"; +import { Observable, map, take, switchMap } from "rxjs"; import { PageDto } from "../domain/dtos"; import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto"; import { RequestDto } from "../domain/dtos/request.dto"; @@ -9,62 +9,7 @@ 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/elastic/es-pit.interface"; - -/** - * Previous search data storage - */ -class PrevSearch { - /** - * Constructs an uninitialized object - */ - constructor() { - this.pit = undefined; - this.tiebreaker = undefined; - this.prevPage = -1; - } - - /** - * PIT object of the previous search - */ - private pit: EsPit; - set _pit(pit: EsPit) { - this.pit = pit; - } - get _pit(): EsPit { - return this.pit; - } - - /** - * Tiebreaker and sort parameters - */ - private tiebreaker: unknown[]; - set _tiebreaker(tiebreaker: unknown[]) { - this.tiebreaker = tiebreaker; - } - get _tiebreaker(): unknown[] { - return this.tiebreaker; - } - - /** - * Number of the previous page - */ - 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 - * @returns true/false, showing whether or not there was another search before - */ - public isSet(): boolean { - if (this.pit && this.tiebreaker && this.prevPage !== -1) return true; - return false; - } -} +import { Cache } from 'cache-manager' /** * Pagination-implementing interceptor @@ -73,95 +18,13 @@ class PrevSearch { export class PageInterceptor implements NestInterceptor { /** * Injects needed dependencies and instantiates the storage object - * @param httpService - * @param searchService + * @param httpService + * @param searchService */ - constructor(private readonly httpService: HttpService) { - this.prevSearch = new PrevSearch; - } - - /** - * Override of intercept() method, specified in NestInterceptor interface - * @param context - * @param next - * @returns Page with content and metadata - */ - async intercept(context: ExecutionContext, next: CallHandler): Promise> { - let request: RequestDto = context.switchToHttp().getRequest(); - const query: SearchQueryDto = request.query; - let reverse: boolean = false; - - request.es_query = new EsQueryDto(); - - request.es_query.query = { - query_string: { - query: query.query, - default_field: 'content', - } - }; - request.es_query.sort = [ - { _score: { order: !query?.order ? Order.DESC : query.order } }, - { _shard_doc: 'desc' } - ]; - - if (this.prevSearch.isSet()) { - 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); - - 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) { - // 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); - - let limit = !query?.limit ? 10 : query.limit; - request.es_query.size = limit * query.page; - } - - return next.handle().pipe( - map((res) => { - // Setting the page meta-data - let meta: PageMeta = { - total: res.hits.total.value, - 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, - }; - 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; - - // 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; - 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); - }) - ); - } + constructor( + private readonly httpService: HttpService, + @Inject(CACHE_MANAGER) private cacheManager: Cache + ) {} /** * Elastichsearch server port-number @@ -174,9 +37,84 @@ export class PageInterceptor implements NestInterceptor { private readonly ES_IP = process.env.ES_CONTAINER_NAME; /** - * Info about previously completed search + * Override of intercept() method, specified in NestInterceptor interface + * @param context + * @param next + * @returns Page with content and metadata */ - private prevSearch: PrevSearch; + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const request: RequestDto = context.switchToHttp().getRequest(); + const query: SearchQueryDto = request.query; + let reverse: boolean = false; + + request.es_query = new EsQueryDto(); + request.es_query.query = { + query_string: { + query: query.query, + default_field: 'content', + } + }; + request.es_query.sort = [ + { _score: { order: !query?.order ? Order.DESC : query.order } }, + { _shard_doc: 'desc' } + ]; + + const limit = !query?.limit ? 10 : query.limit; + + if (await this.cacheManager.get('prevPage')) { + if (query.page == (await this.cacheManager.get('_pagenum'))) return await this.cacheManager.get('prevPage'); + + request.es_query.pit = await this.cacheManager.get('_pit'); + request.es_query.search_after = await this.cacheManager.get('_sa'); + request.es_query.size = limit * Math.abs(query.page - (await this.cacheManager.get('_pagenum'))); + + if (query.page < (await this.cacheManager.get('_pagenum'))) { + request.es_query.sort = [{ _score: { order: Order.ASC } }]; + request.es_query.size += limit - 1; + reverse = true; + } + } else { + request.es_query.pit = await this.getPIT(1); + request.es_query.size = limit * query.page; + } + + return next.handle().pipe( + switchMap(async (res) => { + // Setting the page meta-data + let meta: PageMeta = { + total: res.hits.total.value, + 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, + }; + meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false; + meta.hasPrev = meta.pagenum != 1 ? true : false; + + // Saving the search info + await this.cacheManager.set('_pit', { id: res.pit_id, keep_alive: `1${EsTime.min}` }) + await this.cacheManager.set('_sa', res.hits.hits[res.hits.hits.length - 1]?.sort); + await this.cacheManager.set('_pagenum', query.page); + + // Check if the performed search is a backwards search + let data = res.hits.hits.slice(-meta.pagesize); + if (reverse) { + this.cacheManager.set('_sa', data[0]?.sort); + data.reverse(); + reverse = false; + } + + // Omitting the redundant info and leaving only the document + data = data.map((el) => el._source); + + // Cache and return the page + const page: PageDto = new PageDto(data, meta); + await this.cacheManager.set('prevPage', page); + return page; + }) + ); + } /** * Acquires a PIT ID from Elasticsearch, needed for a request diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index 526c7d7..8fd2f60 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,5 +1,5 @@ import { HttpModule } from "@nestjs/axios"; -import { Module } from "@nestjs/common"; +import { CacheModule, Module } from "@nestjs/common"; import { PapersController } from "../../application"; import { SearchService } from "../../core/services/common/search.service"; @@ -9,6 +9,7 @@ import { SearchService } from "../../core/services/common/search.service"; @Module({ imports: [ HttpModule, + CacheModule.register(), ], exports: [SearchService], providers: [SearchService], diff --git a/src/test/e2e/papers.controller.e2e.spec.ts b/src/test/e2e/papers.controller.e2e.spec.ts index 922bdc0..2934683 100644 --- a/src/test/e2e/papers.controller.e2e.spec.ts +++ b/src/test/e2e/papers.controller.e2e.spec.ts @@ -75,7 +75,7 @@ describe('E2E Testing of /papers', () => { 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 + .get('/papers/2d3dc418-7778-abab-b33f-3d63aa25db41') .expect(200); // Expect HttpService.get() method to be touched diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index e3fda46..5a60f02 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,4 +1,5 @@ import { HttpService } from "@nestjs/axios"; +import { CacheModule, CACHE_MANAGER } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { Test } from "@nestjs/testing"; import { Observable, of } from "rxjs"; @@ -44,7 +45,8 @@ describe('Unit tests for PageInterceptor', () => { isGlobal: true, cache: true, expandVariables: true, - }) + }), + CacheModule.register() ], }).compile(); @@ -159,7 +161,7 @@ describe('Unit tests for PageInterceptor', () => { }); }); - it('Should reverse the search results', () => { + it('Should reverse the search results', async () => { execCtxMock.getRequest.mockReturnValueOnce({ query: { page: 1, @@ -168,10 +170,8 @@ describe('Unit tests for PageInterceptor', () => { } }); - pageInter['prevSearch']._prevPage = 3; - pageInter['prevSearch'].isSet = jest.fn().mockImplementationOnce(() => { - return true; - }) + await pageInter['cacheManager'].set('_pagenum', 3); + await pageInter['cacheManager'].set('prevPage', { set: "yes" }); callHandlerMock.handle.mockReturnValueOnce( of({ @@ -187,8 +187,8 @@ describe('Unit tests for PageInterceptor', () => { ); pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { - res.subscribe((page) => { - expect(pageInter['prevSearch']._tiebreaker).toEqual(['1', 'less relevant']); + res.subscribe(async (page) => { + expect(await pageInter['cacheManager'].get('_sa')).toEqual(['1', 'less relevant']); expect(page.data).toEqual(['3', '2', '1']); }); }); @@ -315,4 +315,8 @@ describe('Unit tests for PageInterceptor', () => { expect(pageInter.deletePIT('')).resolves.toBe(true); }); }); + + afterEach(() => { + pageInter['cacheManager'].reset(); + }) }); \ No newline at end of file diff --git a/src/test/papers.controller.spec.ts b/src/test/papers.controller.spec.ts index 5a1ff79..36c4eb4 100644 --- a/src/test/papers.controller.spec.ts +++ b/src/test/papers.controller.spec.ts @@ -1,5 +1,5 @@ import { HttpModule } from "@nestjs/axios"; -import { NotFoundException } from "@nestjs/common"; +import { CacheModule, NotFoundException } from "@nestjs/common"; import { Test } from "@nestjs/testing"; import { PapersController } from "src/application"; import { SearchService } from "src/core/services/common/search.service"; @@ -21,7 +21,10 @@ describe('Unit tests for PapersController', () => { } } ], - imports: [HttpModule] + imports: [ + HttpModule, + CacheModule.register() + ] }).compile(); papersController = moduleRef.get(PapersController); -- 2.39.5 From 090ecb4ff7aeb3f240d81086855c45bfc11f2350 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Tue, 20 Sep 2022 14:38:26 +0300 Subject: [PATCH 14/23] Elasticsearch config-s[Ccript functionality implemented --- elastic/samples/template.json | 95 +++++++++++++++++ elastic/scripts/es_boot.sh | 191 ++++++++++++++++------------------ 2 files changed, 182 insertions(+), 104 deletions(-) create mode 100644 elastic/samples/template.json diff --git a/elastic/samples/template.json b/elastic/samples/template.json new file mode 100644 index 0000000..b9fcdcc --- /dev/null +++ b/elastic/samples/template.json @@ -0,0 +1,95 @@ +{ + "index_patterns": ["papers*"], + "priority": 1, + "template": { + "aliases": { + "papers": {} + }, + "mappings": { + "properties": { + "id": { + "type": "keyword" + }, + "title": { + "type": "text", + "analyzer": "title_analyzer" + }, + "authors": { + "type": "text" + }, + "topic": { + "type": "text" + }, + "summary": { + "type": "text" + }, + "tags": { + "type": "keyword" + }, + "content": { + "type": "text" + }, + "publisher": { + "type": "text" + } + } + }, + "settings": { + "analysis": { + "analyzer": { + "title_analyzer": { + "type": "custom", + "tokenizer": "title_engram_tokenizer" + }, + "content_analyzer_i": { + "type": "custom", + "tokenizer": "content_onchar_tokenizer", + "char_filter": [ + "markdown_token_filter" + ] + }, + "content_analyzer_s": { + "type": "custom", + "tokenizer": "content_onchar_tokenizer", + "char_filter": [ + "markdown_token_filter" + ], + "filter": [ + ] + }, + "basic_analyzer": { + + } + }, + "tokenizer": { + "title_engram_tokenizer": { + "type": "edge_ngram", + "min_gram": 2, + "max_gram": 10, + "token_chars": [ + "letter", + "digit" + ] + }, + "content_onchar_tokenizer": { + "type": "char_group", + "tokenize_on_chars": [ + "whitespace", + ".", ",", "(", ")", "-", "[", "]", "{", + "}", "#", ":", ";", "`", "!", "*" + ] + } + }, + "char_filter": { + "markdown_token_filter": { + "type": "pattern_replace", + "pattern": "[[a-z][0-9]]*://[[a-z][0-9]]*.[a-z]*", + "replacement": "" + } + }, + "filter": { + } + } + } + } +} \ No newline at end of file diff --git a/elastic/scripts/es_boot.sh b/elastic/scripts/es_boot.sh index 5e5f2eb..77c9650 100755 --- a/elastic/scripts/es_boot.sh +++ b/elastic/scripts/es_boot.sh @@ -3,105 +3,10 @@ __isint='^[0-9]+$' __isvalidstr='^[a-z0-9]+$' __isvalidaddr='^[a-z]+$|^((25[0-5]|2[0-4]?[0-9]|1[0-9]?[0-9]|[3-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4]?[0-9]|1[0-9]?[0-9]|[3-9][0-9]|[0-9])$' +__isJSONfile='^[a-z0-9\_\-]+(\.json)$' create_template() { - curl -H "Content-Type: application/json" -X PUT "$1:$2/_index_template/papers_t" -d ' - { - "index_patterns": ["papers*"], - "priority": 1, - "template": { - "aliases": { - "papers": {} - }, - "mappings": { - "properties": { - "id": { - "type": "keyword" - }, - "title": { - "type": "text", - "analyzer": "title_analyzer" - }, - "authors": { - "type": "text" - }, - "topic": { - "type": "text" - }, - "summary": { - "type": "text" - }, - "tags": { - "type": "keyword" - }, - "content": { - "type": "text" - }, - "publisher": { - "type": "text" - } - } - }, - "settings": { - "analysis": { - "analyzer": { - "title_analyzer": { - "type": "custom", - "tokenizer": "title_engram_tokenizer" - }, - "content_analyzer_i": { - "type": "custom", - "tokenizer": "content_onchar_tokenizer", - "char_filter": [ - "markdown_token_filter" - ] - }, - "content_analyzer_s": { - "type": "custom", - "tokenizer": "content_onchar_tokenizer", - "char_filter": [ - "markdown_token_filter" - ], - "filter": [ - ] - }, - "basic_analyzer": { - - } - }, - "tokenizer": { - "title_engram_tokenizer": { - "type": "edge_ngram", - "min_gram": 2, - "max_gram": 10, - "token_chars": [ - "letter", - "digit" - ] - }, - "content_onchar_tokenizer": { - "type": "char_group", - "tokenize_on_chars": [ - "whitespace", - ".", ",", "(", ")", "-", "[", "]", "{", - "}", "#", ":", ";", "`", "!", "*" - ] - } - }, - "char_filter": { - "markdown_token_filter": { - "type": "pattern_replace", - "pattern": "[[a-z][0-9]]*://[[a-z][0-9]]*.[a-z]*", - "replacement": "" - } - }, - "filter": { - } - } - } - } - } - ' + curl -H "Content-Type: application/json" -X PUT "$1:$2/_index_template/papers_t" -d @"$3" } #============================================================================================================================================================================= @@ -112,13 +17,45 @@ create_index() { #============================================================================================================================================================================= +upload_files() { + _file="$4" + _dir="$5" + + if [[ -n "$_file" ]]; then + curl -X POST "$1:$2/papers-$3/_doc/" \ + -H "Content-Type: application/json" \ + -d @"$_file" + elif [[ -n "$_dir" ]]; then + for file in "$_dir"/*; do + if [[ ! file =~ $__isJSONfile ]]; then + echo "$file is not identified as JSON. Skipping..." + continue; + fi + + curl -X POST "$1:$2/papers-$3/_doc/" \ + -H "Content-Type: application/json" \ + -d @"$file" + done + fi +} + +#============================================================================================================================================================================= + __usage=" - Usage: $(basename $0) - --------------------------------------------------------------------------- + Usage: $(basename "$0") + #-------------------------------------------------------------------------# + | *MODES* | + | | + | Note: 2 modes cannot be specified in one call. | + | | | -c, --create-only | Skip template initialization and only create | | | specified index. Result index name will be | | | 'papers-{specified name}' | | | + | -u, --updload | Uploads the specified file(s) to specified index | + |-------------------------------------------------------------------------| + | *OPTIONS* | + | | | -h | Help information | | | | -a | Specifies the address | @@ -131,7 +68,14 @@ __usage=" | | cannot be longer than 255 bytes (note: multi-byte | | | characters will count towards the limit faster) | | | Result index name will be 'papers-{specified name}' | - --------------------------------------------------------------------------- + | | + | -f, --file | Specify a JSON file that either a config or a | + | | document | + | | + | -d, --dir | Specify a directory containing documents to be | + | | uploaded to the index | + | | + #-------------------------------------------------------------------------# " #============================================================================================================================================================================= @@ -139,6 +83,8 @@ CTRL=0 if [[ "$1" == "-h" ]]; then echo "$__usage" +elif [[ -z "$1" ]]; then + echo "Use -h or --help for usage information." else while [[ $# -gt 0 ]]; do case "$1" in @@ -166,9 +112,32 @@ else echo "Index name is not specified!" fi ;; + -f | --file) + if [[ -n "$2" && $2 =~ $__isJSONfile ]]; then + FPATH="$2" + shift + else + echo "Invalid file name!" + fi + ;; + -d | --dir) + if [[ -n "$2" && -d "$2" ]]; then + DIRPATH="$2" + shift + fi + ;; -c | --create-only) + if [[ CTRL -ne 0 ]]; then echo "Incorrect use of modes. Use -h or --help."; + exit; + fi CTRL=2 ;; + -u | --upload) + if [[ CTRL -ne 0 ]]; then echo "Incorrect use of modes. Use -h or --help."; + exit; + fi + CTRL=3 + ;; -*) echo "Option '$1' is not supported" exit @@ -180,6 +149,8 @@ else PORT="$1" elif [[ $1 =~ $__isvalidstr ]]; then IND="$1" + elif [[ $1 =~ $__isJSONfile ]]; then + FPATH="$1" else echo "Invalid argument!" exit; @@ -190,18 +161,30 @@ else done echo "Specified: $IP:$PORT | Index name: $IND" + if [[ -z "$IP" || -z "$PORT" || -z "$IND" ]]; then + echo "Failed to identify target!" + exit + fi case $CTRL in 0) # Default behaviour - full initialization (template creation and index creation) - create_template "$IP" "$PORT" - echo "Elasticsearch index template created" + if [[ -z "$FPATH" ]]; then + echo "Template config-file was not specified." + fi + create_template "$IP" "$PORT" "$FPATH" + echo -e "\nElasticsearch index template created" create_index "$IP" "$PORT" "$IND" - echo "Elasticsearch index (papers-$IND) created" + echo -e "\nElasticsearch index (papers-$IND) created" exit ;; 2) # Create index, skip creating the template create_index "$IP" "$PORT" "$IND" - echo "Elasticsearch index (papers-$IND) created" + echo -e "\nElasticsearch index (papers-$IND) created" + exit + ;; + 3) # Uploads the specified file(s) to specified index + upload_files "$IP" "$PORT" "$IND" "$FPATH" "$DIRPATH" + echo -e "\nFinished uploading to index (papers-$IND)!" exit ;; esac -- 2.39.5 From d996bf679dbe3ff46924e2d3c3f9089faa80bca5 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Wed, 21 Sep 2022 15:14:57 +0300 Subject: [PATCH 15/23] Pagination redone --- src/core/domain/dtos/elastic/es-query.dto.ts | 137 ++++++++++-------- src/core/domain/dtos/page-meta.dto.ts | 44 +----- src/core/domain/dtos/page.dto.ts | 8 +- src/core/domain/dtos/search-q.dto.ts | 71 ++++----- src/core/domain/enums/page-order.enum.ts | 7 + src/core/domain/interfaces/index.ts | 1 - .../domain/interfaces/page-meta.interface.ts | 36 ----- src/core/interceptors/page.interceptor.ts | 118 ++++++--------- src/test/e2e/papers.controller.e2e.spec.ts | 12 -- src/test/page.interceptor.spec.ts | 35 +---- 10 files changed, 168 insertions(+), 301 deletions(-) delete mode 100644 src/core/domain/interfaces/page-meta.interface.ts diff --git a/src/core/domain/dtos/elastic/es-query.dto.ts b/src/core/domain/dtos/elastic/es-query.dto.ts index 0572933..693d24c 100644 --- a/src/core/domain/dtos/elastic/es-query.dto.ts +++ b/src/core/domain/dtos/elastic/es-query.dto.ts @@ -1,5 +1,5 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsArray, IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator"; +import { IsArray, IsDefined, IsInt, IsObject, IsOptional } from "class-validator"; import { EsPit } from "../../interfaces/elastic/es-pit.interface"; import { EsQuery } from "../../interfaces/elastic/es-query.interface" @@ -13,71 +13,80 @@ import { EsQuery } from "../../interfaces/elastic/es-query.interface" */ @ApiExtraModels() export class EsQueryDto { - /** - * Maximum number of elements returned by Elasticsearch - */ - @IsOptional() - @IsDefined() - @IsNumber() - @IsInt() - @ApiPropertyOptional({ - description: 'Maximum number of elements returned by Elasticsearch', - example: 30 - }) - size?: number; - - /** - * The search query object passed to Elasticsearch - */ - @IsDefined() - @IsObject() - @ApiProperty({ - description: 'Search query object passed to Elasticsearch', - example: {}, - }) - query: EsQuery; + /** + * Offset from the start of the list of hits + */ + @IsOptional() + @IsInt() + @ApiPropertyOptional({ + description: 'Offset from the start of the list of hits', + example: 5, + }) + from?: number; - /** - * Object, that stores PIT ID and time alive - */ - @IsOptional() - @IsObject() - @ApiPropertyOptional({ - description: 'PIT object', - example: {} - }) - pit?: EsPit; + /** + * Maximum number of elements returned by Elasticsearch + */ + @IsOptional() + @IsInt() + @ApiPropertyOptional({ + description: 'Maximum number of elements returned by Elasticsearch', + example: 30 + }) + size?: number; + + /** + * The search query object passed to Elasticsearch + */ + @IsDefined() + @IsObject() + @ApiProperty({ + description: 'Search query object passed to Elasticsearch', + example: {}, + }) + query: EsQuery; - /** - * Sorting info - */ - @IsOptional() - @IsArray() - @ApiPropertyOptional({ - description: '', - example: [] - }) - sort?: unknown[]; + /** + * Object, that stores PIT ID and time alive + */ + @IsOptional() + @IsObject() + @ApiPropertyOptional({ + description: 'PIT object', + example: {} + }) + pit?: EsPit; - /** - * Pagination info - */ - @IsOptional() - @IsArray() - @ApiPropertyOptional({ - description: '', - example: [] - }) - search_after?: unknown[]; + /** + * Sorting info + */ + @IsOptional() + @IsArray() + @ApiPropertyOptional({ + description: '', + example: [] + }) + sort?: unknown[]; - /** - * Constructs an empty object - */ - constructor() { - this.size = 10; - this.query = undefined; - this.pit = undefined; - this.sort = undefined; - this.search_after = undefined; - } + /** + * Pagination info + */ + @IsOptional() + @IsArray() + @ApiPropertyOptional({ + description: '', + example: [] + }) + search_after?: unknown[]; + + /** + * Constructs an empty object + */ + constructor() { + this.size = 10; + this.query = undefined; + this.pit = undefined; + this.sort = undefined; + this.search_after = undefined; + } } \ 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 index 2a89285..0e4c321 100644 --- a/src/core/domain/dtos/page-meta.dto.ts +++ b/src/core/domain/dtos/page-meta.dto.ts @@ -1,8 +1,6 @@ -import { ApiExtraModels, ApiProperty, PartialType } from "@nestjs/swagger"; +import { ApiExtraModels, ApiProperty } 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 @@ -13,7 +11,7 @@ const allowedProperties = ['total', 'pagenum', 'order', 'hasNext', 'hasPrev', 'p * Page model for pagination */ @ApiExtraModels() -export class PageMetaDto implements PageMeta { +export class PageMetaDto { /** * Total number of hits (results) acquired from the search */ @@ -24,16 +22,6 @@ export class PageMetaDto implements PageMeta { }) total: number; - /** - * Current page number - */ - @ApiProperty({ - description: 'Current page number', - minimum: 1, - example: 3 - }) - pagenum: number; - /** * Order of the elements on the page */ @@ -42,32 +30,4 @@ export class PageMetaDto implements PageMeta { 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 785b1e0..189f7db 100644 --- a/src/core/domain/dtos/page.dto.ts +++ b/src/core/domain/dtos/page.dto.ts @@ -1,7 +1,5 @@ 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"; @@ -22,7 +20,7 @@ export class PageDto { @ApiProperty({ description: 'All data (papers) the page contains', isArray: true, - type: PaperDto + type: PaperDto, }) readonly data: PaperDto[]; @@ -31,7 +29,7 @@ export class PageDto { */ @ApiProperty({ description: 'Metadata for the page', - // example: {}, + type: PageMetaDto, }) readonly meta: PageMetaDto; @@ -41,7 +39,7 @@ export class PageDto { * @param data * @param meta */ - constructor(data: PaperDto[], meta: PageMeta) { + constructor(data: PaperDto[], meta: PageMetaDto) { this.data = data; this.meta = meta; } diff --git a/src/core/domain/dtos/search-q.dto.ts b/src/core/domain/dtos/search-q.dto.ts index 8834657..ee4d929 100644 --- a/src/core/domain/dtos/search-q.dto.ts +++ b/src/core/domain/dtos/search-q.dto.ts @@ -1,4 +1,4 @@ -import { ApiExtraModels, ApiProperty } from "@nestjs/swagger"; +import { ApiExtraModels, ApiPropertyOptional } from "@nestjs/swagger"; import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; /** @@ -12,63 +12,64 @@ const allowedProperties = ['query', 'pagen', 'limit', 'order']; @ApiExtraModels() export class SearchQueryDto { /** - * Given query string to perform the - * search on. + * Given query string to perform the search on. */ @IsDefined() @IsNotEmpty() @IsString() - @ApiProperty({ - description: 'query', - example: 'Particle Accelerator' + @ApiPropertyOptional({ + description: 'Given query string to perform the search on', + example: 'Particle Accelerator', }) query: string; - - /** - * Page number to display. - */ - @IsDefined() - @IsNotEmpty() - @IsInt() - @ApiProperty({ - description: 'page', - example: 3, - }) - page: number; /** * Limits the number of displayed elements. */ @IsOptional() @IsInt() - @ApiProperty({ - description: 'limit', + @ApiPropertyOptional({ + description: 'Limits the number of displayed elements', example: 10, }) - limit: number; + limit?: number; /** - * Limits the number of displayed elements. + * Offset from the start of the list of hits. + */ + @IsOptional() + @IsInt() + @ApiPropertyOptional({ + description: 'Offset from the start of the list of hits', + example: 0, + }) + offset?: number; + + /** + * Indicates in which order elements need to be displayed. */ @IsOptional() @IsString() - @ApiProperty({ - description: 'order', + @ApiPropertyOptional({ + description: 'Indicates in which order elements need to be displayed', example: 'asc', }) - order: string; + order?: string; - /** - * Constructs an object with provided parameters - * @param query - * @param page - * @param limit - * @param order - */ - constructor(query: string, page: number, limit: number, order: string) { + /** + * + */ + + /** + * Constructs an object with provided parameters + * @param query + * @param page + * @param limit + * @param order + */ + constructor(query: string, page: number, limit: number, order: string) { this.query = query; - this.page = page; this.limit = limit; this.order = order; - } + } } \ 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 ff4a505..234f858 100644 --- a/src/core/domain/enums/page-order.enum.ts +++ b/src/core/domain/enums/page-order.enum.ts @@ -11,4 +11,11 @@ export enum Order { * Descending order */ DESC = 'desc', +} + +export function toOrder(str: string): Order { + switch (str) { + case 'asc': return Order.ASC; + case 'desc': return Order.DESC; + } } \ No newline at end of file diff --git a/src/core/domain/interfaces/index.ts b/src/core/domain/interfaces/index.ts index 6295a5f..3b9d09e 100644 --- a/src/core/domain/interfaces/index.ts +++ b/src/core/domain/interfaces/index.ts @@ -1,5 +1,4 @@ export * from './http-response.interface' -export * from './page-meta.interface' export * from './search-info.interface' export * from './elastic/es-query.interface' export * from './elastic/es-query-string.interface' diff --git a/src/core/domain/interfaces/page-meta.interface.ts b/src/core/domain/interfaces/page-meta.interface.ts deleted file mode 100644 index 62cc9a5..0000000 --- a/src/core/domain/interfaces/page-meta.interface.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Order } from "../enums/page-order.enum"; - -/** - * Structure of page metadata - */ -export interface PageMeta { - /** - * Total search results - */ - total: number; - - /** - * Number of the page - */ - pagenum: number; - - /** - * Order of the elements on the page - */ - order: Order; - - /** - * Flag that indicates presence of the next page - */ - hasNext: boolean; - - /** - * Flag that indicates presence of the previous page - */ - hasPrev: boolean; - - /** - * Number of elements on the page - */ - pagesize: number; -} \ No newline at end of file diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 01eda50..6efaf93 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -6,10 +6,10 @@ 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 { Order, toOrder } from "../domain/enums/page-order.enum"; import { EsPit } from "../domain/interfaces/elastic/es-pit.interface"; import { Cache } from 'cache-manager' +import { PageMetaDto } from "../domain/dtos/page-meta.dto"; /** * Pagination-implementing interceptor @@ -45,8 +45,12 @@ export class PageInterceptor implements NestInterceptor { async intercept(context: ExecutionContext, next: CallHandler): Promise> { const request: RequestDto = context.switchToHttp().getRequest(); const query: SearchQueryDto = request.query; - let reverse: boolean = false; + const offset = !query.offset ? 0 : query.offset; + const limit = !query.limit ? 10 : query.limit; + const order = !query.order ? Order.DESC : query.order; + + // Contruct a body for querying Elasticsearch request.es_query = new EsQueryDto(); request.es_query.query = { query_string: { @@ -54,63 +58,33 @@ export class PageInterceptor implements NestInterceptor { default_field: 'content', } }; + request.es_query.from = offset; + request.es_query.size = limit; request.es_query.sort = [ - { _score: { order: !query?.order ? Order.DESC : query.order } }, - { _shard_doc: 'desc' } - ]; + { "_score": { "order": order } }, + ]; - const limit = !query?.limit ? 10 : query.limit; - - if (await this.cacheManager.get('prevPage')) { - if (query.page == (await this.cacheManager.get('_pagenum'))) return await this.cacheManager.get('prevPage'); - - request.es_query.pit = await this.cacheManager.get('_pit'); - request.es_query.search_after = await this.cacheManager.get('_sa'); - request.es_query.size = limit * Math.abs(query.page - (await this.cacheManager.get('_pagenum'))); - - if (query.page < (await this.cacheManager.get('_pagenum'))) { - request.es_query.sort = [{ _score: { order: Order.ASC } }]; - request.es_query.size += limit - 1; - reverse = true; - } - } else { - request.es_query.pit = await this.getPIT(1); - request.es_query.size = limit * query.page; + const prev_page = await this.cacheManager.get('prev_page'); + if (prev_page) { + if (offset == prev_page[1] && limit == prev_page[2]) return prev_page[0]; } return next.handle().pipe( switchMap(async (res) => { // Setting the page meta-data - let meta: PageMeta = { + let meta: PageMetaDto = { total: res.hits.total.value, - 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, + order: toOrder(order), }; - meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false; - meta.hasPrev = meta.pagenum != 1 ? true : false; - - // Saving the search info - await this.cacheManager.set('_pit', { id: res.pit_id, keep_alive: `1${EsTime.min}` }) - await this.cacheManager.set('_sa', res.hits.hits[res.hits.hits.length - 1]?.sort); - await this.cacheManager.set('_pagenum', query.page); // Check if the performed search is a backwards search - let data = res.hits.hits.slice(-meta.pagesize); - if (reverse) { - this.cacheManager.set('_sa', data[0]?.sort); - data.reverse(); - reverse = false; - } - + let data = res.hits.hits; // Omitting the redundant info and leaving only the document data = data.map((el) => el._source); // Cache and return the page const page: PageDto = new PageDto(data, meta); - await this.cacheManager.set('prevPage', page); + await this.cacheManager.set('prev_page', [page, offset, limit]); return page; }) ); @@ -121,19 +95,19 @@ export class PageInterceptor implements NestInterceptor { * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes. * @returns PIT object containing PIT ID and keep_alive value */ - public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise { - return new Promise((resolve, reject) => { - try { - 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: EsPit) => { - res.keep_alive = alive + unit; - resolve(res); - }); - } catch (error) { - reject(error); - } - }); + public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise { + return new Promise((resolve, reject) => { + try { + 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: EsPit) => { + res.keep_alive = alive + unit; + resolve(res); + }); + } catch (error) { + reject(error); + } + }); } /** @@ -141,20 +115,20 @@ export class PageInterceptor implements NestInterceptor { * @param pitID, ID of the PIT, that would be deleted * @returns true/false, depending on the result of deletion of the PIT */ - async deletePIT(pitID: string): Promise { - return new Promise((resolve, reject) => { - try { - this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, { - data: { id: pitID }, - headers: { 'Content-Type': 'application/json' }, - }) - .pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res) => { - resolve(res.succeeded); - }); - } catch (error) { - reject(error); - } - }) + async deletePIT(pitID: string): Promise { + return new Promise((resolve, reject) => { + try { + this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, { + data: { id: pitID }, + headers: { 'Content-Type': 'application/json' }, + }) + .pipe(take(1), map(axiosRes => axiosRes.data)) + .subscribe((res) => { + resolve(res.succeeded); + }); + } catch (error) { + reject(error); + } + }) } } \ No newline at end of file diff --git a/src/test/e2e/papers.controller.e2e.spec.ts b/src/test/e2e/papers.controller.e2e.spec.ts index 2934683..d5bba5c 100644 --- a/src/test/e2e/papers.controller.e2e.spec.ts +++ b/src/test/e2e/papers.controller.e2e.spec.ts @@ -205,20 +205,8 @@ describe('E2E Testing of /papers', () => { 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 () => { diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index 5a60f02..d60c8f1 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -159,40 +159,7 @@ describe('Unit tests for PageInterceptor', () => { }); }); }); - }); - - it('Should reverse the search results', async () => { - execCtxMock.getRequest.mockReturnValueOnce({ - query: { - page: 1, - order: 'desc', - limit: 3 - } - }); - - await pageInter['cacheManager'].set('_pagenum', 3); - await pageInter['cacheManager'].set('prevPage', { set: "yes" }); - - 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(async (page) => { - expect(await pageInter['cacheManager'].get('_sa')).toEqual(['1', 'less relevant']); - expect(page.data).toEqual(['3', '2', '1']); - }); - }); - }); + }); }); describe('getPIT()', () => { -- 2.39.5 From f4f01fe8c07fdd7b1939fa11939a10fdf1cde3bb Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Thu, 22 Sep 2022 13:31:22 +0300 Subject: [PATCH 16/23] Fixed order-change --- src/core/interceptors/page.interceptor.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 6efaf93..73c2089 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,6 +1,6 @@ import { HttpService } from "@nestjs/axios"; import { CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common"; -import { Observable, map, take, switchMap } from "rxjs"; +import { Observable, map, take, switchMap, of } from "rxjs"; import { PageDto } from "../domain/dtos"; import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto"; import { RequestDto } from "../domain/dtos/request.dto"; @@ -50,6 +50,11 @@ export class PageInterceptor implements NestInterceptor { const limit = !query.limit ? 10 : query.limit; const order = !query.order ? Order.DESC : query.order; + const prev_page = await this.cacheManager.get('prev_page'); + if (prev_page) { + if (offset == prev_page[1] && limit == prev_page[2] && order == prev_page[3]) return of(prev_page[0]); + } + // Contruct a body for querying Elasticsearch request.es_query = new EsQueryDto(); request.es_query.query = { @@ -60,14 +65,6 @@ export class PageInterceptor implements NestInterceptor { }; request.es_query.from = offset; request.es_query.size = limit; - request.es_query.sort = [ - { "_score": { "order": order } }, - ]; - - const prev_page = await this.cacheManager.get('prev_page'); - if (prev_page) { - if (offset == prev_page[1] && limit == prev_page[2]) return prev_page[0]; - } return next.handle().pipe( switchMap(async (res) => { @@ -81,10 +78,12 @@ export class PageInterceptor implements NestInterceptor { let data = res.hits.hits; // Omitting the redundant info and leaving only the document data = data.map((el) => el._source); + // Change the order if set + if (order == Order.ASC) data.reverse(); // Cache and return the page const page: PageDto = new PageDto(data, meta); - await this.cacheManager.set('prev_page', [page, offset, limit]); + await this.cacheManager.set('prev_page', [page, offset, limit, order]); return page; }) ); -- 2.39.5 From e0596d409f366def101d724bcd94125edf65b3ad Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Thu, 22 Sep 2022 17:28:14 +0300 Subject: [PATCH 17/23] Implemented HTTP-exceptions filter --- documentation/classes/EsQueryDto.html | 197 ++++++---- .../classes/HttpExceptionFilter.html | 316 ++++++++++++++++ documentation/classes/PageDto.html | 22 +- documentation/classes/PageMetaDto.html | 230 +----------- documentation/classes/SearchQueryDto.html | 182 +++++----- .../controllers/PapersController.html | 17 +- documentation/coverage.html | 54 +-- documentation/graph/dependencies.svg | 174 ++++----- .../injectables/PageInterceptor.html | 336 ++++++------------ documentation/injectables/SearchService.html | 16 +- documentation/js/menu-wc.js | 21 +- documentation/js/menu-wc_es5.js | 2 +- documentation/js/search/search_index.js | 4 +- documentation/miscellaneous/functions.html | 76 ++++ documentation/modules/HttpResponseModule.html | 8 +- documentation/modules/LoggerModule.html | 8 +- .../modules/LoggerModule/dependencies.svg | 8 +- documentation/modules/SearchModule.html | 3 +- .../modules/SearchModule/dependencies.svg | 8 +- documentation/overview.html | 2 +- .../controller/papers.controller.ts | 9 +- src/core/domain/enums/page-order.enum.ts | 8 +- src/core/filters/http-exception.filter.ts | 23 ++ src/core/pipes/query-str.pipe.ts | 13 + src/core/services/common/search.service.ts | 6 +- src/infrastructure/modules/app.module.ts | 3 +- src/main.ts | 3 +- src/test/page.interceptor.spec.ts | 4 - src/test/search.service.spec.ts | 34 +- 29 files changed, 978 insertions(+), 809 deletions(-) create mode 100644 documentation/classes/HttpExceptionFilter.html create mode 100644 src/core/filters/http-exception.filter.ts create mode 100644 src/core/pipes/query-str.pipe.ts diff --git a/documentation/classes/EsQueryDto.html b/documentation/classes/EsQueryDto.html index 3064425..2ba0be8 100644 --- a/documentation/classes/EsQueryDto.html +++ b/documentation/classes/EsQueryDto.html @@ -90,6 +90,10 @@
                          @@ -152,6 +156,48 @@

                          Properties

                          +
                            +
                          • + Optional + from +
                          • Optional pit @@ -133,7 +137,7 @@
                          - +
                          + + + + + + + + + + + + + + + + + + + +
                          + + + Optional + from + + +
                          + Type : number + +
                          + Decorators : +
                          + + @IsOptional()
                          @IsInt()
                          @ApiPropertyOptional({description: 'Offset from the start of the list of hits', example: 5})
                          +
                          +
                          + +
                          +

                          Offset from the start of the list of hits

                          +
                          +
                          @@ -181,7 +227,7 @@ @@ -222,7 +268,7 @@ @@ -264,7 +310,7 @@ @@ -300,13 +346,13 @@ Decorators :
                          - @IsOptional()
                          @IsDefined()
                          @IsNumber()
                          @IsInt()
                          @ApiPropertyOptional({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
                          + @IsOptional()
                          @IsInt()
                          @ApiPropertyOptional({description: 'Maximum number of elements returned by Elasticsearch', example: 30})
                          @@ -348,7 +394,7 @@ @@ -374,7 +420,7 @@
                          import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
                          -import { IsArray, IsDefined, IsInt, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator";
                          +import { IsArray, IsDefined, IsInt, IsObject, IsOptional } from "class-validator";
                           import { EsPit } from "../../interfaces/elastic/es-pit.interface";
                           import { EsQuery } from "../../interfaces/elastic/es-query.interface"
                           
                          @@ -388,73 +434,82 @@ import { EsQuery } from "../../interfaces/elastic/es-query.interface"
                             */
                            @ApiExtraModels()
                            export class EsQueryDto {
                          -     /**
                          -      * Maximum number of elements returned by Elasticsearch
                          -      */
                          -     @IsOptional()
                          -     @IsDefined()
                          -     @IsNumber()
                          -     @IsInt()
                          -     @ApiPropertyOptional({
                          -         description: 'Maximum number of elements returned by Elasticsearch',
                          -         example: 30
                          -     })
                          -     size?: number;
                          -     
                          -     /**
                          -      * The search query object passed to Elasticsearch
                          -      */
                          -     @IsDefined()
                          -     @IsObject()
                          -     @ApiProperty({
                          -         description: 'Search query object passed to Elasticsearch',
                          -         example: {},
                          -     })
                          -     query: EsQuery;
                          +   /**
                          +    * Offset from the start of the list of hits
                          +    */
                          +   @IsOptional()
                          +   @IsInt()
                          +   @ApiPropertyOptional({
                          +      description: 'Offset from the start of the list of hits',
                          +      example: 5,
                          +   })
                          +   from?: number;
                           
                          -     /**
                          -      * Object, that stores PIT ID and time alive
                          -      */
                          -     @IsOptional()
                          -     @IsObject()
                          -     @ApiPropertyOptional({
                          -        description: 'PIT object',
                          -        example: {}
                          -     })
                          -     pit?: EsPit;
                          +   /**
                          +    * Maximum number of elements returned by Elasticsearch
                          +    */
                          +    @IsOptional()
                          +    @IsInt()
                          +    @ApiPropertyOptional({
                          +        description: 'Maximum number of elements returned by Elasticsearch',
                          +        example: 30
                          +    })
                          +    size?: number;
                          +    
                          +    /**
                          +     * The search query object passed to Elasticsearch
                          +     */
                          +    @IsDefined()
                          +    @IsObject()
                          +    @ApiProperty({
                          +        description: 'Search query object passed to Elasticsearch',
                          +        example: {},
                          +    })
                          +    query: EsQuery;
                           
                          -     /**
                          -      * Sorting info
                          -      */
                          -     @IsOptional()
                          -     @IsArray()
                          -     @ApiPropertyOptional({
                          -        description: '',
                          -        example: []
                          -     })
                          -     sort?: unknown[];
                          +    /**
                          +     * Object, that stores PIT ID and time alive
                          +     */
                          +    @IsOptional()
                          +    @IsObject()
                          +    @ApiPropertyOptional({
                          +       description: 'PIT object',
                          +       example: {}
                          +    })
                          +    pit?: EsPit;
                           
                          -     /**
                          -      * Pagination info
                          -      */
                          -     @IsOptional()
                          -     @IsArray()
                          -     @ApiPropertyOptional({
                          -        description: '',
                          -        example: []
                          -     })
                          -     search_after?: unknown[];
                          +    /**
                          +     * Sorting info
                          +     */
                          +    @IsOptional()
                          +    @IsArray()
                          +    @ApiPropertyOptional({
                          +       description: '',
                          +       example: []
                          +    })
                          +    sort?: unknown[];
                           
                          -     /**
                          -      * Constructs an empty object
                          -      */
                          -     constructor() {
                          -        this.size = 10;
                          -        this.query = undefined;
                          -        this.pit = undefined;
                          -        this.sort = undefined;
                          -        this.search_after = undefined;
                          -     }
                          +    /**
                          +     * Pagination info
                          +     */
                          +    @IsOptional()
                          +    @IsArray()
                          +    @ApiPropertyOptional({
                          +       description: '',
                          +       example: []
                          +    })
                          +    search_after?: unknown[];
                          +
                          +    /**
                          +     * Constructs an empty object
                          +     */
                          +    constructor() {
                          +       this.size = 10;
                          +       this.query = undefined;
                          +       this.pit = undefined;
                          +       this.sort = undefined;
                          +       this.search_after = undefined;
                          +    }
                            }
                          diff --git a/documentation/classes/HttpExceptionFilter.html b/documentation/classes/HttpExceptionFilter.html new file mode 100644 index 0000000..45f65ed --- /dev/null +++ b/documentation/classes/HttpExceptionFilter.html @@ -0,0 +1,316 @@ + + + + + + hometask documentation + + + + + + + + + + + + + + +
                          +
                          + + +
                          +
                          + + + + + + + + + + + + + + + + +
                          +
                          +

                          +

                          File

                          +

                          +

                          + src/core/filters/http-exception.filter.ts +

                          + + +

                          +

                          Description

                          +

                          +

                          +

                          Basic filter for processing unhabdled HTTP exceptions

                          + +

                          + + +

                          +

                          Implements

                          +

                          +

                          + ExceptionFilter +

                          + + +
                          +

                          Index

                          +
                          - +
                          - +
                          - +
                          - +
                          - +
                          + + + + + + + + + + + + + + +
                          +
                          Methods
                          +
                          + +
                          +
                          + + + +
                          + +

                          + Methods +

                          + + + + + + + + + + + + + + + + + + + +
                          + + + catch + + +
                          +catch(exception: HttpException, host: ArgumentsHost) +
                          + +
                          +

                          Exception handling method

                          +
                          + +
                          + Parameters : + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
                          NameTypeOptionalDescription
                          exception + HttpException + + No + +

                          Execution object currently being processed

                          + +
                          host + ArgumentsHost + + No + +

                          Arguments host utility object

                          + +
                          +
                          +
                          +
                          +
                          + Returns : void + +
                          +
                          + +
                          +
                          +
                          + + + + + +
                        + + +
                        +
                        import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common";
                        +
                        +/**
                        + * Basic filter for processing unhabdled HTTP exceptions
                        + */
                        +@Catch(HttpException)
                        +export class HttpExceptionFilter implements ExceptionFilter {
                        +    /**
                        +     * Exception handling method
                        +     * @param exception Execution object currently being processed
                        +     * @param host Arguments host utility object
                        +     */
                        +    catch(exception: HttpException, host: ArgumentsHost) {
                        +        const ctx = host.switchToHttp();
                        +        const response = ctx.getResponse();
                        +        const status = exception.getStatus();
                        +
                        +        response.status(status).json({
                        +            status: status,
                        +            message: exception.message,
                        +        });
                        +    }
                        +}
                        +
                        +
                        + + + + + + + + + +
                        +
                        +

                        results matching ""

                        +
                          +
                          +
                          +

                          No results matching ""

                          +
                          +
                          +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/classes/PageDto.html b/documentation/classes/PageDto.html index 18a1357..0bb8e47 100644 --- a/documentation/classes/PageDto.html +++ b/documentation/classes/PageDto.html @@ -117,12 +117,12 @@ -constructor(data: PaperDto[], meta: PageMeta) +constructor(data: PaperDto[], meta: PageMetaDto) - + @@ -157,7 +157,7 @@ meta - PageMeta + PageMetaDto @@ -208,7 +208,7 @@ - + @@ -235,7 +235,7 @@ - Type : PageMetaDto + Type : PageMetaDto @@ -244,13 +244,13 @@ Decorators :
                          - @ApiProperty({description: 'Metadata for the page'})
                          + @ApiProperty({description: 'Metadata for the page', type: PageMetaDto})
                          - + @@ -277,8 +277,6 @@
                          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";
                           
                          @@ -299,7 +297,7 @@ export class PageDto {
                               @ApiProperty({
                                   description: 'All data (papers) the page contains',
                                   isArray: true,
                          -        type: PaperDto
                          +        type: PaperDto,
                               })
                               readonly data: PaperDto[];
                           
                          @@ -308,7 +306,7 @@ export class PageDto {
                                */
                               @ApiProperty({
                                   description: 'Metadata for the page',
                          -        // example: {},
                          +        type: PageMetaDto,
                                   
                               })
                               readonly meta: PageMetaDto;
                          @@ -318,7 +316,7 @@ export class PageDto {
                                * @param data 
                                * @param meta 
                                */
                          -    constructor(data: PaperDto[], meta: PageMeta) {
                          +    constructor(data: PaperDto[], meta: PageMetaDto) {
                                   this.data = data;
                                   this.meta = meta;
                               }
                          diff --git a/documentation/classes/PageMetaDto.html b/documentation/classes/PageMetaDto.html
                          index 17aa765..e1bd77c 100644
                          --- a/documentation/classes/PageMetaDto.html
                          +++ b/documentation/classes/PageMetaDto.html
                          @@ -76,12 +76,6 @@
                                       

                          -

                          -

                          Implements

                          -

                          -

                          - PageMeta -

                          @@ -96,21 +90,9 @@
                            -
                          • - hasNext -
                          • -
                          • - hasPrev -
                          • order
                          • -
                          • - pagenum -
                          • -
                          • - pagesize -
                          • total
                          • @@ -133,88 +115,6 @@

                            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

                            -
                            -
                            @@ -243,7 +143,7 @@ @@ -256,88 +156,6 @@
                            - +
                            - - - - - - - - - - - - - - - - - - - - -
                            - - - 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

                            -
                            -
                            @@ -366,7 +184,7 @@ @@ -391,11 +209,9 @@
                            -
                            import { ApiExtraModels, ApiProperty, PartialType } from "@nestjs/swagger";
                            +        
                            import { ApiExtraModels, ApiProperty } 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
                            @@ -406,7 +222,7 @@ const allowedProperties = ['total', 'pagenum', 'or
                              * Page model for pagination
                              */
                             @ApiExtraModels()
                            -export class PageMetaDto implements PageMeta {
                            +export class PageMetaDto {
                                 /**
                                  * Total number of hits (results) acquired from the search
                                  */
                            @@ -417,16 +233,6 @@ export class PageMetaDto implements PageMeta {
                                 })
                                 total: number;
                             
                            -    /**
                            -     * Current page number
                            -     */
                            -    @ApiProperty({
                            -        description: 'Current page number',
                            -        minimum: 1,
                            -        example: 3
                            -    })
                            -    pagenum: number;
                            -
                                 /**
                                  * Order of the elements on the page
                                  */
                            @@ -435,34 +241,6 @@ export class PageMetaDto implements PageMeta {
                                     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;
                             }
                            diff --git a/documentation/classes/SearchQueryDto.html b/documentation/classes/SearchQueryDto.html index e30b687..f60eb1c 100644 --- a/documentation/classes/SearchQueryDto.html +++ b/documentation/classes/SearchQueryDto.html @@ -91,13 +91,16 @@ @@ -213,6 +216,7 @@ @@ -248,12 +252,55 @@
                            - +
                            - +
                            + Optional limit @@ -229,13 +233,13 @@ Decorators :
                            - @IsOptional()
                            @IsInt()
                            @ApiProperty({description: 'limit', example: 10})
                            + @IsOptional()
                            @IsInt()
                            @ApiPropertyOptional({description: 'Limits the number of displayed elements', example: 10})
                            - +
                            + + + + + + + + + + + + + + + + + + + + +
                            + + + Optional + offset + + +
                            + Type : number + +
                            + Decorators : +
                            + + @IsOptional()
                            @IsInt()
                            @ApiPropertyOptional({description: 'Offset from the start of the list of hits', example: 0})
                            +
                            +
                            + +
                            +

                            Offset from the start of the list of hits.

                            +
                            +
                            - - - -
                            + Optional order @@ -270,60 +317,19 @@ Decorators :
                            - @IsOptional()
                            @IsString()
                            @ApiProperty({description: 'order', example: 'asc'})
                            + @IsOptional()
                            @IsString()
                            @ApiPropertyOptional({description: 'Indicates in which order elements need to be displayed', example: 'asc'})
                            - +
                            -

                            Limits the number of displayed elements.

                            -
                            -
                            - - - - - - - - - - - - - - - - - @@ -352,20 +358,19 @@ Decorators :
                            - @IsDefined()
                            @IsNotEmpty()
                            @IsString()
                            @ApiProperty({description: 'query', example: 'Particle Accelerator'})
                            + @IsDefined()
                            @IsNotEmpty()
                            @IsString()
                            @ApiPropertyOptional({description: 'Given query string to perform the search on', example: 'Particle Accelerator'})
                            @@ -384,7 +389,7 @@ search on.

                            -
                            import { ApiExtraModels, ApiProperty } from "@nestjs/swagger";
                            +        
                            import { ApiExtraModels, ApiPropertyOptional } from "@nestjs/swagger";
                             import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
                             
                             /**
                            @@ -398,65 +403,66 @@ const allowedProperties = ['query', 'pagen', 'limi
                             @ApiExtraModels()
                             export class SearchQueryDto {
                                 /**
                            -     * Given query string to perform the
                            -     * search on.
                            +     * Given query string to perform the search on.
                                  */
                                 @IsDefined()
                                 @IsNotEmpty()
                                 @IsString()
                            -    @ApiProperty({
                            -        description: 'query',
                            -        example: 'Particle Accelerator'
                            +    @ApiPropertyOptional({
                            +        description: 'Given query string to perform the search on',
                            +        example: 'Particle Accelerator',
                                 })
                                 query: string;
                            -    
                            -    /**
                            -     * Page number to display.
                            -     */
                            -    @IsDefined()
                            -    @IsNotEmpty()
                            -    @IsInt()
                            -    @ApiProperty({
                            -        description: 'page',
                            -        example: 3,
                            -    })
                            -    page: number;
                             
                                 /**
                                  * Limits the number of displayed elements.
                                  */
                                 @IsOptional()
                                 @IsInt()
                            -    @ApiProperty({
                            -        description: 'limit',
                            +    @ApiPropertyOptional({
                            +        description: 'Limits the number of displayed elements',
                                     example: 10,
                                 })
                            -    limit: number;
                            +    limit?: number;
                             
                                 /**
                            -     * Limits the number of displayed elements.
                            +     * Offset from the start of the list of hits.
                            +     */
                            +    @IsOptional()
                            +    @IsInt()
                            +    @ApiPropertyOptional({
                            +        description: 'Offset from the start of the list of hits',
                            +        example: 0,
                            +    })
                            +    offset?: number;
                            +
                            +    /**
                            +     * Indicates in which order elements need to be displayed.
                                  */
                                  @IsOptional()
                                  @IsString()
                            -     @ApiProperty({
                            -         description: 'order',
                            +     @ApiPropertyOptional({
                            +         description: 'Indicates in which order elements need to be displayed',
                                      example: 'asc',
                                  })
                            -     order: string;
                            +     order?: string;
                             
                            -     /**
                            -      * Constructs an object with provided parameters
                            -      * @param query 
                            -      * @param page 
                            -      * @param limit 
                            -      * @param order 
                            -      */
                            -     constructor(query: string, page: number, limit: number, order: string) {
                            +    /**
                            +     * 
                            +     */
                            +
                            +    /**
                            +     * Constructs an object with provided parameters
                            +     * @param query 
                            +     * @param page 
                            +     * @param limit 
                            +     * @param order 
                            +     */
                            +    constructor(query: string, page: number, limit: number, order: string) {
                                     this.query = query;
                            -        this.page = page;
                                     this.limit = limit;
                                     this.order = order;
                            -     }
                            +    }
                             }
                            diff --git a/documentation/controllers/PapersController.html b/documentation/controllers/PapersController.html index 4de01ed..7eb4ad9 100644 --- a/documentation/controllers/PapersController.html +++ b/documentation/controllers/PapersController.html @@ -135,8 +135,8 @@ @@ -214,8 +214,8 @@ @@ -270,21 +270,24 @@
                            -
                            import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common";
                            +        
                            import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Req, UseFilters, UseInterceptors } from "@nestjs/common";
                             import { SearchService } from "../../core/services/common/search.service";
                             import { PageInterceptor } from "../../core/interceptors/page.interceptor";
                            -import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from "@nestjs/swagger";
                            +import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
                             import { RequestDto } from "../../core/domain/dtos/request.dto";
                             import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain";
                            +import { HttpExceptionFilter } from "src/core/filters/http-exception.filter";
                             
                             /**
                              * /papers/ route controller
                              */
                            +@UseFilters(HttpExceptionFilter)
                             @Controller({
                                 version: '1',
                                 path: 'papers',
                             })
                             @ApiExtraModels(RequestDto, EsHitDto, EsResponseDto)
                            +// @UseInterceptors(CacheInterceptor)
                             export class PapersController {
                                 constructor(private searchService: SearchService) {}
                             
                            @@ -344,7 +347,7 @@ export class PapersController {
                                 getByID(@Param('uuid', ParseUUIDPipe) uuid: string): Promise<PaperDto> {
                                     return this.searchService.findByID(uuid).then(
                                         (response: EsResponseDto) => {
                            -                return response.hits.hits[0]._source;
                            +                return response.hits.hits[0]?._source;
                                         },
                                         (error) => {
                                             throw error;
                            diff --git a/documentation/coverage.html b/documentation/coverage.html
                            index b75c0b0..8d27efd 100644
                            --- a/documentation/coverage.html
                            +++ b/documentation/coverage.html
                            @@ -173,7 +173,7 @@
                                         
                            @@ -221,7 +221,7 @@ @@ -356,6 +356,18 @@ (1/1) + + + + + + - - - - - - + + + + + + - - - - - - diff --git a/documentation/graph/dependencies.svg b/documentation/graph/dependencies.svg index 7a7c66e..7660ee6 100644 --- a/documentation/graph/dependencies.svg +++ b/documentation/graph/dependencies.svg @@ -4,217 +4,217 @@ - - + + dependencies - -dependencies - -cluster_SearchModule - - - -cluster_SearchModule_exports - - - -cluster_SearchModule_providers - - + +dependencies cluster_AppModule - + cluster_AppModule_imports - - - -cluster_CommonModule - - - -cluster_CommonModule_imports - - - -cluster_CommonModule_exports - + cluster_HttpResponseModule - + cluster_HttpResponseModule_exports - + cluster_HttpResponseModule_providers - + + + +cluster_CommonModule + + + +cluster_CommonModule_imports + + + +cluster_CommonModule_exports + cluster_LoggerModule - + cluster_LoggerModule_exports - + cluster_LoggerModule_providers - + + + +cluster_SearchModule + + + +cluster_SearchModule_exports + + + +cluster_SearchModule_providers + CommonModule - -CommonModule + +CommonModule AppModule - -AppModule + +AppModule CommonModule->AppModule - - + + HttpResponseModule - -HttpResponseModule + +HttpResponseModule CommonModule->HttpResponseModule - - + + LoggerModule - -LoggerModule + +LoggerModule CommonModule->LoggerModule - - + + SearchModule - -SearchModule + +SearchModule SearchModule->AppModule - - + + SearchService - -SearchService + +SearchService SearchModule->SearchService - - + + HttpResponseModule - -HttpResponseModule + +HttpResponseModule HttpResponseModule->CommonModule - - + + HttpResponseService - -HttpResponseService + +HttpResponseService HttpResponseModule->HttpResponseService - - + + LoggerModule - -LoggerModule + +LoggerModule LoggerModule->CommonModule - - + + LoggerService - -LoggerService + +LoggerService LoggerModule->LoggerService - - + + HttpResponseService - -HttpResponseService + +HttpResponseService HttpResponseService->HttpResponseModule - - + + LoggerService - -LoggerService + +LoggerService LoggerService->LoggerModule - - + + SearchService - -SearchService + +SearchService SearchService->SearchModule - - + + diff --git a/documentation/injectables/PageInterceptor.html b/documentation/injectables/PageInterceptor.html index 2a1c737..e17aff4 100644 --- a/documentation/injectables/PageInterceptor.html +++ b/documentation/injectables/PageInterceptor.html @@ -95,10 +95,6 @@ Readonly ES_PORT -
                          • - Private - prevSearch -
                          • @@ -142,12 +138,12 @@ @@ -178,6 +174,18 @@ + + + + + + + +
                            - - - page - - -
                            - Type : number - -
                            - Decorators : -
                            - - @IsDefined()
                            @IsNotEmpty()
                            @IsInt()
                            @ApiProperty({description: 'page', example: 3})
                            -
                            -
                            - -
                            -

                            Page number to display.

                            +

                            Indicates in which order elements need to be displayed.

                            - +
                            -

                            Given query string to perform the -search on.

                            +

                            Given query string to perform the search on.

                            - +
                            - +
                            EsQueryDto 100 % - (7/7) + (8/8)
                            PageMetaDto 100 % - (7/7) + (3/3)
                            + + src/core/domain/enums/page-order.enum.ts + functiontoOrder + 100 % + (1/1) +
                            @@ -416,18 +428,6 @@ (6/6)
                            - - src/core/domain/interfaces/page-meta.interface.ts - interfacePageMeta - 100 % - (7/7) -
                            @@ -452,6 +452,18 @@ (2/2)
                            + + src/core/filters/http-exception.filter.ts + classHttpExceptionFilter + 100 % + (2/2) +
                            @@ -548,18 +560,6 @@ (4/4)
                            - - src/core/interceptors/page.interceptor.ts - classPrevSearch - 100 % - (6/6) -
                            @@ -569,7 +569,7 @@ PageInterceptor 100 % - (8/8) + (7/7)
                            -constructor(httpService: HttpService) +constructor(httpService: HttpService, cacheManager: Cache)
                            - +
                            cacheManager + Cache + + No +
                          @@ -214,8 +222,8 @@ - + @@ -294,8 +302,8 @@ - + @@ -393,8 +401,8 @@ - + @@ -483,7 +491,7 @@ - + @@ -516,7 +524,7 @@ - + @@ -529,39 +537,6 @@ - - - - - - - - - - - - - - - - - -
                          - - - Private - prevSearch - - -
                          - Type : PrevSearch - -
                          - -
                          -

                          Info about previously completed search

                          -
                          -
                          @@ -569,72 +544,17 @@
                          import { HttpService } from "@nestjs/axios";
                          -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
                          -import { Observable, map, take } from "rxjs";
                          +import { CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common";
                          +import { Observable, map, take, switchMap, of } from "rxjs";
                           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 { EsTime } from "../domain/enums/es-time.enum";
                          -import { Order } from "../domain/enums/page-order.enum";
                          -import { PageMeta } from "../domain/interfaces";
                          +import { Order, toOrder } from "../domain/enums/page-order.enum";
                           import { EsPit } from "../domain/interfaces/elastic/es-pit.interface";
                          -
                          -/**
                          - * Previous search data storage
                          - */
                          -class PrevSearch {
                          -    /**
                          -     * Constructs an uninitialized object
                          -     */
                          -    constructor() {
                          -        this.pit = undefined;
                          -        this.tiebreaker = undefined;
                          -        this.prevPage = -1;
                          -    }
                          -
                          -    /**
                          -     * PIT object of the previous search
                          -     */
                          -    private pit: EsPit;
                          -    set _pit(pit: EsPit) {
                          -        this.pit = pit;
                          -    }
                          -    get _pit(): EsPit {
                          -        return this.pit;
                          -    }
                          -
                          -    /**
                          -     * Tiebreaker and sort parameters
                          -     */
                          -    private tiebreaker: unknown[];
                          -    set _tiebreaker(tiebreaker: unknown[]) {
                          -        this.tiebreaker = tiebreaker;
                          -    }
                          -    get _tiebreaker(): unknown[] {
                          -        return this.tiebreaker;
                          -    }
                          -
                          -    /**
                          -     * Number of the previous page
                          -     */
                          -    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
                          -     * @returns true/false, showing whether or not there was another search before
                          -     */
                          -    public isSet(): boolean {
                          -        if (this.pit && this.tiebreaker && this.prevPage !== -1) return true;
                          -        return false;
                          -    }
                          -}
                          +import { Cache } from 'cache-manager'
                          +import { PageMetaDto } from "../domain/dtos/page-meta.dto";
                           
                           /**
                            * Pagination-implementing interceptor
                          @@ -643,95 +563,13 @@ class PrevSearch {
                           export class PageInterceptor implements NestInterceptor {
                               /**
                                * Injects needed dependencies and instantiates the storage object
                          -     * @param httpService 
                          -     * @param searchService 
                          +     * @param httpService
                          +     * @param searchService
                                */
                          -    constructor(private readonly httpService: HttpService) {
                          -        this.prevSearch = new PrevSearch;
                          -    }
                          -
                          -    /**
                          -     * Override of intercept() method, specified in NestInterceptor interface
                          -     * @param context 
                          -     * @param next 
                          -     * @returns Page with content and metadata
                          -     */
                          -    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<PageDto>> {
                          -        let request: RequestDto = context.switchToHttp().getRequest<RequestDto>();
                          -        const query: SearchQueryDto = request.query;
                          -        let reverse: boolean = false;
                          -
                          -        request.es_query = new EsQueryDto();
                          -
                          -        request.es_query.query = {
                          -            query_string: {
                          -                query: query.query,
                          -                default_field: 'content',
                          -            }
                          -        };
                          -        request.es_query.sort = [
                          -            { _score: { order: !query?.order ? Order.DESC : query.order } },
                          -            { _shard_doc: 'desc' }
                          -        ];
                          -
                          -        if (this.prevSearch.isSet()) {
                          -            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);
                          -            
                          -            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) {
                          -                // 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);
                          -
                          -            let limit = !query?.limit ? 10 : query.limit;
                          -            request.es_query.size = limit * query.page;
                          -        }
                          -
                          -        return next.handle().pipe(
                          -            map((res) => {
                          -                // Setting the page meta-data
                          -                let meta: PageMeta = {
                          -                    total: res.hits.total.value,
                          -                    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,
                          -                }; 
                          -                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;
                          -
                          -                // 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;
                          -                    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);
                          -            })
                          -        );
                          -    }
                          +    constructor(
                          +        private readonly httpService: HttpService,
                          +        @Inject(CACHE_MANAGER) private cacheManager: Cache
                          +    ) {}
                           
                               /**
                                * Elastichsearch server port-number
                          @@ -744,28 +582,76 @@ export class PageInterceptor implements NestInterceptor {
                                private readonly ES_IP = process.env.ES_CONTAINER_NAME;
                           
                               /**
                          -     * Info about previously completed search
                          +     * Override of intercept() method, specified in NestInterceptor interface
                          +     * @param context
                          +     * @param next
                          +     * @returns Page with content and metadata
                                */
                          -    private prevSearch: PrevSearch;
                          +    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<PageDto>> {
                          +        const request: RequestDto = context.switchToHttp().getRequest<RequestDto>();
                          +        const query: SearchQueryDto = request.query;
                          +
                          +        const offset = !query.offset ? 0 : query.offset;
                          +        const limit = !query.limit ? 10 : query.limit; 
                          +        const order = !query.order ? Order.DESC : query.order;
                          +
                          +        const prev_page = await this.cacheManager.get('prev_page'); 
                          +        if (prev_page) {
                          +            if (offset == prev_page[1] && limit == prev_page[2] && order == prev_page[3]) return of(prev_page[0]);
                          +        }
                          +
                          +        // Contruct a body for querying Elasticsearch
                          +        request.es_query = new EsQueryDto();
                          +        request.es_query.query = {
                          +            query_string: {
                          +                query: query.query,
                          +                default_field: 'content',
                          +            }
                          +        };
                          +        request.es_query.from = offset;
                          +        request.es_query.size = limit;
                          +
                          +        return next.handle().pipe(
                          +            switchMap(async (res) => {
                          +                // Setting the page meta-data
                          +                let meta: PageMetaDto = {
                          +                    total: res.hits.total.value,
                          +                    order: toOrder(order),
                          +                };
                          +
                          +                // Check if the performed search is a backwards search
                          +                let data = res.hits.hits;
                          +                // Omitting the redundant info and leaving only the document
                          +                data = data.map((el) => el._source);
                          +                // Change the order if set
                          +                if (order == Order.ASC) data.reverse();
                          +
                          +                // Cache and return the page
                          +                const page: PageDto = new PageDto(data, meta);
                          +                await this.cacheManager.set('prev_page', [page, offset, limit, order]);
                          +                return page;
                          +            })
                          +        );
                          +    }
                           
                               /**
                                * Acquires a PIT ID from Elasticsearch, needed for a request
                                * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes.
                                * @returns PIT object <EsPit> containing PIT ID and keep_alive value
                                */
                          -     public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise<EsPit> {
                          -        return new Promise((resolve, reject) => {
                          -            try {
                          -                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: EsPit) => {
                          -                        res.keep_alive = alive + unit;
                          -                        resolve(res);
                          -                    });
                          -            } catch (error) {
                          -                reject(error);
                          -            }
                          -        });
                          +    public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise<EsPit> {
                          +       return new Promise((resolve, reject) => {
                          +           try {
                          +               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: EsPit) => {
                          +                       res.keep_alive = alive + unit;
                          +                       resolve(res);
                          +                   });
                          +           } catch (error) {
                          +               reject(error);
                          +           }
                          +       });
                               }
                           
                               /**
                          @@ -773,21 +659,21 @@ export class PageInterceptor implements NestInterceptor {
                                * @param pitID, ID of the PIT, that would be deleted
                                * @returns true/false, depending on the result of deletion of the PIT
                                */
                          -     async deletePIT(pitID: string): Promise<boolean> {
                          -        return new Promise((resolve, reject) => {
                          -            try {
                          -                this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, {
                          -                    data: { id: pitID },
                          -                    headers: { 'Content-Type': 'application/json' },
                          -                })
                          -                .pipe(take(1), map(axiosRes => axiosRes.data))
                          -                .subscribe((res) => {
                          -                    resolve(res.succeeded);
                          -                });
                          -            } catch (error) {
                          -                reject(error);
                          -            }
                          -        })
                          +    async deletePIT(pitID: string): Promise<boolean> {
                          +       return new Promise((resolve, reject) => {
                          +           try {
                          +               this.httpService.delete(`http://${this.ES_IP}:${this.ES_PORT}/_pit`, {
                          +                   data: { id: pitID },
                          +                   headers: { 'Content-Type': 'application/json' },
                          +               })
                          +               .pipe(take(1), map(axiosRes => axiosRes.data))
                          +               .subscribe((res) => {
                          +                   resolve(res.succeeded);
                          +               });
                          +           } catch (error) {
                          +               reject(error);
                          +           }
                          +       })
                               }
                           }
                          diff --git a/documentation/injectables/SearchService.html b/documentation/injectables/SearchService.html index a80d7ea..ab4019c 100644 --- a/documentation/injectables/SearchService.html +++ b/documentation/injectables/SearchService.html @@ -206,8 +206,8 @@ HTTPService instance

                          - + @@ -411,7 +411,7 @@ HTTPService instance

                          import { HttpService } from "@nestjs/axios";
                          -import { GatewayTimeoutException, HttpException, Injectable } from "@nestjs/common";
                          +import { BadRequestException, GatewayTimeoutException, HttpException, Injectable, NotFoundException } from "@nestjs/common";
                           import { map, take } from "rxjs";
                           import { EsResponseDto} from "../../domain/dtos";
                           import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto";
                          @@ -446,7 +446,7 @@ export class SearchService {
                               async findByID(uuid: string): Promise<EsResponseDto> { // Should I change 'object' to specific DTO?
                                   let ESQ: EsQueryDto = new EsQueryDto;
                           
                          -        ESQ.size = 1;
                          +        // ESQ.size = 1;
                                   ESQ.query = {
                                       query_string: {
                                           query: ('id:' + uuid),
                          @@ -461,6 +461,9 @@ export class SearchService {
                                           }))
                                           ?.pipe(take(1), map(axiosRes => axiosRes.data))
                                           .subscribe((res: EsResponseDto) => {
                          +                    if (!res.hits.hits.length) {
                          +                        reject(new NotFoundException);
                          +                    } 
                                               if (res.timed_out) {
                                                   reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
                                               }
                          @@ -480,6 +483,10 @@ export class SearchService {
                               async findByContext(es_query: EsQueryDto): Promise<EsResponseDto> {
                                   return new Promise((resolve, reject) => {
                                       try {
                          +                if (!es_query.query.query_string.query) {
                          +                    throw new BadRequestException;
                          +                }
                          +
                                           (this.httpService.get<EsResponseDto>(`http://${this.ES_IP}:${this.ES_PORT}/_search`, {
                                               data: es_query,
                                               headers: {'Content-Type': 'application/json'},
                          @@ -489,7 +496,6 @@ export class SearchService {
                                               if (res.timed_out) {
                                                   reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
                                               }
                          -
                                               resolve(res);
                                           });
                                       } catch (error) {
                          diff --git a/documentation/js/menu-wc.js b/documentation/js/menu-wc.js
                          index a3eb3db..3e0b561 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/enums/page-order.enum.ts

                          +
                          +

                          + + + + + + + + + + + + + diff --git a/documentation/modules/HttpResponseModule.html b/documentation/modules/HttpResponseModule.html index a50283e..f298ffb 100644 --- a/documentation/modules/HttpResponseModule.html +++ b/documentation/modules/HttpResponseModule.html @@ -57,14 +57,14 @@ cluster_HttpResponseModule - -cluster_HttpResponseModule_providers - - cluster_HttpResponseModule_exports + +cluster_HttpResponseModule_providers + + HttpResponseService diff --git a/documentation/modules/LoggerModule.html b/documentation/modules/LoggerModule.html index d91a07c..733b70a 100644 --- a/documentation/modules/LoggerModule.html +++ b/documentation/modules/LoggerModule.html @@ -57,14 +57,14 @@ cluster_LoggerModule - -cluster_LoggerModule_providers - - cluster_LoggerModule_exports + +cluster_LoggerModule_providers + + LoggerService diff --git a/documentation/modules/LoggerModule/dependencies.svg b/documentation/modules/LoggerModule/dependencies.svg index 0321adc..5c106c0 100644 --- a/documentation/modules/LoggerModule/dependencies.svg +++ b/documentation/modules/LoggerModule/dependencies.svg @@ -14,14 +14,14 @@ cluster_LoggerModule - -cluster_LoggerModule_exports - - cluster_LoggerModule_providers + +cluster_LoggerModule_exports + + LoggerService diff --git a/documentation/modules/SearchModule.html b/documentation/modules/SearchModule.html index fbcca7c..452318a 100644 --- a/documentation/modules/SearchModule.html +++ b/documentation/modules/SearchModule.html @@ -177,7 +177,7 @@
                          import { HttpModule } from "@nestjs/axios";
                          -import { Module } from "@nestjs/common";
                          +import { CacheModule, Module } from "@nestjs/common";
                           import { PapersController } from "../../application";
                           import { SearchService } from "../../core/services/common/search.service";
                           
                          @@ -187,6 +187,7 @@ import { SearchService } from "../../core/services/common/search.service&qu
                           @Module({
                               imports: [
                                   HttpModule,
                          +        CacheModule.register(),
                               ],
                               exports: [SearchService],
                               providers: [SearchService],
                          diff --git a/documentation/modules/SearchModule/dependencies.svg b/documentation/modules/SearchModule/dependencies.svg
                          index 3d5fb1d..bfd3490 100644
                          --- a/documentation/modules/SearchModule/dependencies.svg
                          +++ b/documentation/modules/SearchModule/dependencies.svg
                          @@ -14,14 +14,14 @@
                           cluster_SearchModule
                           
                           
                          -
                          -cluster_SearchModule_providers
                          -
                          -
                           
                           cluster_SearchModule_exports
                           
                           
                          +
                          +cluster_SearchModule_providers
                          +
                          +
                           
                           
                           SearchService 
                          diff --git a/documentation/overview.html b/documentation/overview.html
                          index 91e00cb..7b5e6ed 100644
                          --- a/documentation/overview.html
                          +++ b/documentation/overview.html
                          @@ -317,7 +317,7 @@
                                       

                          -

                          9 Interfaces

                          +

                          8 Interfaces

                          diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 3151334..50b30e7 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,13 +1,16 @@ -import { CacheInterceptor, Controller, Get, HttpCode, Inject, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common"; +import { Body, Controller, Get, HttpCode, Param, ParseUUIDPipe, Query, Req, UseFilters, UseInterceptors, UsePipes } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; import { PageInterceptor } from "../../core/interceptors/page.interceptor"; -import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from "@nestjs/swagger"; +import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { RequestDto } from "../../core/domain/dtos/request.dto"; import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain"; +import { HttpExceptionFilter } from "src/core/filters/http-exception.filter"; +import { QueryStringPipe } from "src/core/pipes/query-str.pipe"; /** * /papers/ route controller */ +@UseFilters(HttpExceptionFilter) @Controller({ version: '1', path: 'papers', @@ -73,7 +76,7 @@ export class PapersController { getByID(@Param('uuid', ParseUUIDPipe) uuid: string): Promise { return this.searchService.findByID(uuid).then( (response: EsResponseDto) => { - return response.hits.hits[0]._source; + return response.hits.hits[0]?._source; }, (error) => { throw error; diff --git a/src/core/domain/enums/page-order.enum.ts b/src/core/domain/enums/page-order.enum.ts index 234f858..61e5176 100644 --- a/src/core/domain/enums/page-order.enum.ts +++ b/src/core/domain/enums/page-order.enum.ts @@ -13,9 +13,15 @@ export enum Order { DESC = 'desc', } +/** + * Converts string value to appropriate enum-member + * @param str String to be converted + * @returns Appropriate enum-member + */ export function toOrder(str: string): Order { switch (str) { case 'asc': return Order.ASC; case 'desc': return Order.DESC; - } + } + return; } \ No newline at end of file diff --git a/src/core/filters/http-exception.filter.ts b/src/core/filters/http-exception.filter.ts new file mode 100644 index 0000000..32db991 --- /dev/null +++ b/src/core/filters/http-exception.filter.ts @@ -0,0 +1,23 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from "@nestjs/common"; + +/** + * Basic filter for processing unhabdled HTTP exceptions + */ +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + /** + * Exception handling method + * @param exception Execution object currently being processed + * @param host Arguments host utility object + */ + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = exception.getStatus(); + + response.status(status).json({ + status: status, + message: exception.message, + }); + } +} \ No newline at end of file diff --git a/src/core/pipes/query-str.pipe.ts b/src/core/pipes/query-str.pipe.ts new file mode 100644 index 0000000..d9bfcca --- /dev/null +++ b/src/core/pipes/query-str.pipe.ts @@ -0,0 +1,13 @@ +import { ArgumentMetadata, BadRequestException, ImATeapotException, Injectable, PipeTransform } from "@nestjs/common"; +import { RequestDto } from "../domain"; + +@Injectable() +export class QueryStringPipe implements PipeTransform { + constructor() {} + + transform(value: RequestDto, metadata: ArgumentMetadata): RequestDto { + console.log(value.query.limit) + + return value; + } +} \ No newline at end of file diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index 609ce0d..4cd131b 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,5 +1,5 @@ import { HttpService } from "@nestjs/axios"; -import { GatewayTimeoutException, HttpException, Injectable } from "@nestjs/common"; +import { GatewayTimeoutException, Injectable, NotFoundException } from "@nestjs/common"; import { map, take } from "rxjs"; import { EsResponseDto} from "../../domain/dtos"; import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto"; @@ -49,6 +49,9 @@ export class SearchService { })) ?.pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsResponseDto) => { + if (!res.hits.hits.length) { + reject(new NotFoundException); + } if (res.timed_out) { reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } @@ -77,7 +80,6 @@ export class SearchService { if (res.timed_out) { reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } - resolve(res); }); } catch (error) { diff --git a/src/infrastructure/modules/app.module.ts b/src/infrastructure/modules/app.module.ts index 308d14f..9b20b70 100644 --- a/src/infrastructure/modules/app.module.ts +++ b/src/infrastructure/modules/app.module.ts @@ -1,5 +1,5 @@ import { CacheInterceptor, CacheModule, Module } from '@nestjs/common'; -import { APP_INTERCEPTOR } from '@nestjs/core'; +import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ConfigModule } from '@nestjs/config'; import { configuration } from '../config/env.objects'; import { validate } from '../config/env.validation'; @@ -8,6 +8,7 @@ import * as modules from '../../core/modules' import { CommonModule } from './common/common.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { SearchModule } from './search.module'; +import { QueryStringPipe } from 'src/core/pipes/query-str.pipe'; /** * application modules list diff --git a/src/main.ts b/src/main.ts index c3efc9c..58e4e19 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './infrastructure/modules/app.module'; import { SwaggerModule, DocumentBuilder, SwaggerDocumentOptions } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; +import { QueryStringPipe } from './core/pipes/query-str.pipe'; /** * Main entry point of the application * @returns Nothing @@ -16,7 +17,7 @@ async function bootstrap() { app.useGlobalPipes( new ValidationPipe({ disableErrorMessages: false, - }) + }), ); /** diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index d60c8f1..27837a4 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -151,11 +151,7 @@ describe('Unit tests for PageInterceptor', () => { res.subscribe((page) => { expect(page.meta).toEqual({ total: 921, - pagenum: 5, order: 'desc', - hasNext: true, - hasPrev: true, - pagesize: 100 }); }); }); diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index 864f6c3..d773f4f 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -42,23 +42,25 @@ describe('Unit tests for SearchService', () => { expect(httpGetSpy).toHaveBeenCalled(); }); - it('Should send correct data via HttpService.get() body parameter', () => { - let httpGetSpy = jest.spyOn(httpService, 'get'); + // it('Should send correct data via HttpService.get() body parameter', () => { + // let httpGetSpy = jest.spyOn(httpService, 'get'); - 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' } - }); - }); + // const uuid = 'thisIsUUID_Provided'; + // searchService.findByID(uuid); + // expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { + // data: { + // size: 1, + // query: { + // query_string: { + // query: 'id:' + uuid + // } + // }, + // search_after: undefined, + // sort: undefined, + // }, + // headers: { 'Content-Type': 'application/json' } + // }); + // }); it('Should call HttpService.get() with correct URI and port number', () => { let httpGetSpy = jest.spyOn(httpService, 'get'); -- 2.39.5 From 83b5cc6e5e865415d2e3f2d1401ff61e2ba281e5 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Fri, 7 Oct 2022 13:18:50 +0300 Subject: [PATCH 18/23] Fixed validation issues. Cleared unnecessary DTOs --- .../controller/papers.controller.ts | 15 +++--- src/core/domain/dtos/index.ts | 4 +- src/core/domain/dtos/request.dto.ts | 48 ----------------- src/core/domain/dtos/search-q.dto.ts | 33 +++++++----- src/core/domain/dtos/search-result.dto.ts | 53 ------------------- src/core/interceptors/page.interceptor.ts | 28 +++------- src/core/pipes/query-str.pipe.ts | 13 ----- src/core/services/common/search.service.ts | 24 ++++++--- src/infrastructure/modules/app.module.ts | 1 - src/main.ts | 2 +- src/test/search.service.spec.ts | 2 +- 11 files changed, 53 insertions(+), 170 deletions(-) delete mode 100644 src/core/domain/dtos/request.dto.ts delete mode 100644 src/core/domain/dtos/search-result.dto.ts delete mode 100644 src/core/pipes/query-str.pipe.ts diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 50b30e7..bdf507a 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,11 +1,9 @@ -import { Body, Controller, Get, HttpCode, Param, ParseUUIDPipe, Query, Req, UseFilters, UseInterceptors, UsePipes } from "@nestjs/common"; +import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Query, Req, UseFilters, UseInterceptors } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; import { PageInterceptor } from "../../core/interceptors/page.interceptor"; import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { RequestDto } from "../../core/domain/dtos/request.dto"; -import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain"; +import { EsHitDto, EsResponseDto, PageDto, PaperDto, SearchQueryDto } from "../../core/domain"; import { HttpExceptionFilter } from "src/core/filters/http-exception.filter"; -import { QueryStringPipe } from "src/core/pipes/query-str.pipe"; /** * /papers/ route controller @@ -15,8 +13,7 @@ import { QueryStringPipe } from "src/core/pipes/query-str.pipe"; version: '1', path: 'papers', }) -@ApiExtraModels(RequestDto, EsHitDto, EsResponseDto) -// @UseInterceptors(CacheInterceptor) +@ApiExtraModels(EsHitDto, EsResponseDto) export class PapersController { constructor(private searchService: SearchService) {} @@ -39,10 +36,10 @@ export class PapersController { description: 'Elasticsearch request timed out' }) @Get('search') - @UseInterceptors(PageInterceptor) @HttpCode(200) - getByContext(@Req() request: RequestDto): Promise { - return this.searchService.findByContext(request.es_query).then( + @UseInterceptors(PageInterceptor) + getByContext(@Query() request: SearchQueryDto): Promise { + return this.searchService.findByContext(request).then( (response) => { return response; }, diff --git a/src/core/domain/dtos/index.ts b/src/core/domain/dtos/index.ts index d24be14..ac35a7b 100644 --- a/src/core/domain/dtos/index.ts +++ b/src/core/domain/dtos/index.ts @@ -3,6 +3,4 @@ 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 +export * from './paper.dto'; \ No newline at end of file diff --git a/src/core/domain/dtos/request.dto.ts b/src/core/domain/dtos/request.dto.ts deleted file mode 100644 index 973264d..0000000 --- a/src/core/domain/dtos/request.dto.ts +++ /dev/null @@ -1,48 +0,0 @@ -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"; - -/** - * List of allowed properties in this DTO - */ -const allowedProperties = ['query', 'es_query']; - -/** - * Request object, which contains query parameters and Elasticsearch query object - */ -@ApiExtraModels() -export class RequestDto { - /** - * Query parameters object - */ - @IsDefined() - @IsNotEmpty() - @ApiProperty({ - type: SearchQueryDto, - description: 'Actual query with parameters acquired from the request', - example: {} - }) - query: SearchQueryDto; - - /** - * Elasticsearch query object - */ - @IsOptional() - @ApiPropertyOptional({ - type: EsQueryDto, - description: 'Elasticsearch query body constructed by pagination mechanism', - example: {}, - }) - es_query?: EsQueryDto; - - /** - * Constructs an object with provided parameters - * @param query - * @param es_query - */ - 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 ee4d929..0651a03 100644 --- a/src/core/domain/dtos/search-q.dto.ts +++ b/src/core/domain/dtos/search-q.dto.ts @@ -1,10 +1,11 @@ import { ApiExtraModels, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { Type } from "class-transformer"; +import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; /** * List of allowed properties in this DTO */ -const allowedProperties = ['query', 'pagen', 'limit', 'order']; +const allowedProperties = ['query', 'limit', 'offset', 'order']; /** * Elasticsearch response DTO @@ -28,9 +29,12 @@ export class SearchQueryDto { */ @IsOptional() @IsInt() + @Type(() => Number) + @Min(1) @ApiPropertyOptional({ description: 'Limits the number of displayed elements', example: 10, + required: false }) limit?: number; @@ -39,26 +43,26 @@ export class SearchQueryDto { */ @IsOptional() @IsInt() + @Type(() => Number) + @Min(0) @ApiPropertyOptional({ description: 'Offset from the start of the list of hits', example: 0, + required: false, }) offset?: number; /** * Indicates in which order elements need to be displayed. */ - @IsOptional() - @IsString() - @ApiPropertyOptional({ - description: 'Indicates in which order elements need to be displayed', - example: 'asc', - }) - order?: string; - - /** - * - */ + @IsOptional() + @IsString() + @ApiPropertyOptional({ + description: 'Indicates in which order elements need to be displayed', + example: 'asc', + required: false, + }) + order?: string; /** * Constructs an object with provided parameters @@ -67,9 +71,10 @@ export class SearchQueryDto { * @param limit * @param order */ - constructor(query: string, page: number, limit: number, order: string) { + constructor(query: string = undefined, limit: number = 10, offset: number = 0, order: string = undefined) { this.query = query; this.limit = limit; + this.offset = offset; this.order = order; } } \ No newline at end of file diff --git a/src/core/domain/dtos/search-result.dto.ts b/src/core/domain/dtos/search-result.dto.ts deleted file mode 100644 index c926308..0000000 --- a/src/core/domain/dtos/search-result.dto.ts +++ /dev/null @@ -1,53 +0,0 @@ -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 - */ -const allowedProperties = ['data', 'status']; - -/** - * Elasticsearch response DTO - */ -@ApiExtraModels() -export class SearchResultDto { - /** - * Status code - */ - @IsDefined() - @IsNotEmpty() - @IsInt() - @ApiProperty({ - description: 'Status code', - example: 200, - }) - statusCode: number; - - /** - * All the data acquired. - */ - @IsDefined() - @IsNotEmpty() - @IsArray() - @ApiProperty({ - description: 'Data acquired from the Elasticsearch', - example: { - took: 1, - timed_out: false, - _shards: {}, - hits: {} - }, - }) - data: EsResponseDto; - - /** - * Constructs an object with provided parameters - * @param code - * @param data - */ - constructor(code: number, data: EsResponseDto) { - this.statusCode = code; - this.data = data; - } -} \ No newline at end of file diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 73c2089..8a78ded 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -2,9 +2,6 @@ import { HttpService } from "@nestjs/axios"; import { CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common"; import { Observable, map, take, switchMap, of } from "rxjs"; 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 { EsTime } from "../domain/enums/es-time.enum"; import { Order, toOrder } from "../domain/enums/page-order.enum"; import { EsPit } from "../domain/interfaces/elastic/es-pit.interface"; @@ -43,29 +40,20 @@ export class PageInterceptor implements NestInterceptor { * @returns Page with content and metadata */ async intercept(context: ExecutionContext, next: CallHandler): Promise> { - const request: RequestDto = context.switchToHttp().getRequest(); - const query: SearchQueryDto = request.query; + const query = context.switchToHttp().getRequest().query; - const offset = !query.offset ? 0 : query.offset; - const limit = !query.limit ? 10 : query.limit; - const order = !query.order ? Order.DESC : query.order; + // const offset = !query.offset ? 0 : query.offset; + const offset = query.offset; + // const limit = !query.limit ? 10 : query.limit; + const limit = query.limit; + // const order = !query.order ? Order.DESC : query.order; + const order = query.order; - const prev_page = await this.cacheManager.get('prev_page'); + const prev_page = await this.cacheManager.get('prev_page'); if (prev_page) { if (offset == prev_page[1] && limit == prev_page[2] && order == prev_page[3]) return of(prev_page[0]); } - // Contruct a body for querying Elasticsearch - request.es_query = new EsQueryDto(); - request.es_query.query = { - query_string: { - query: query.query, - default_field: 'content', - } - }; - request.es_query.from = offset; - request.es_query.size = limit; - return next.handle().pipe( switchMap(async (res) => { // Setting the page meta-data diff --git a/src/core/pipes/query-str.pipe.ts b/src/core/pipes/query-str.pipe.ts deleted file mode 100644 index d9bfcca..0000000 --- a/src/core/pipes/query-str.pipe.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ArgumentMetadata, BadRequestException, ImATeapotException, Injectable, PipeTransform } from "@nestjs/common"; -import { RequestDto } from "../domain"; - -@Injectable() -export class QueryStringPipe implements PipeTransform { - constructor() {} - - transform(value: RequestDto, metadata: ArgumentMetadata): RequestDto { - console.log(value.query.limit) - - return value; - } -} \ No newline at end of file diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index 4cd131b..b72685e 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,7 +1,7 @@ import { HttpService } from "@nestjs/axios"; import { GatewayTimeoutException, Injectable, NotFoundException } from "@nestjs/common"; import { map, take } from "rxjs"; -import { EsResponseDto} from "../../domain/dtos"; +import { EsResponseDto, SearchQueryDto} from "../../domain/dtos"; import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto"; /** @@ -32,10 +32,9 @@ export class SearchService { * @returns Elasticsearch hits or an error object */ async findByID(uuid: string): Promise { // Should I change 'object' to specific DTO? - let ESQ: EsQueryDto = new EsQueryDto; - - ESQ.size = 1; - ESQ.query = { + const es_query: EsQueryDto = new EsQueryDto(); + es_query.size = 1; + es_query.query = { query_string: { query: ('id:' + uuid), } @@ -44,7 +43,7 @@ export class SearchService { return new Promise((resolve, reject) => { try { (this.httpService.get(`http://${this.ES_IP}:${this.ES_PORT}/_search`, { - data: ESQ, + data: es_query, headers: {'Content-Type': 'application/json'}, })) ?.pipe(take(1), map(axiosRes => axiosRes.data)) @@ -68,7 +67,18 @@ export class SearchService { * @param query, * @returns Elasticsearch hits or an error object */ - async findByContext(es_query: EsQueryDto): Promise { + async findByContext(query: SearchQueryDto): Promise { + // Contruct a body for querying Elasticsearch + const es_query: EsQueryDto = new EsQueryDto(); + es_query.query = { + query_string: { + query: query.query, + default_field: 'content', + } + }; + es_query.from = query.offset; + es_query.size = query.limit; + return new Promise((resolve, reject) => { try { (this.httpService.get(`http://${this.ES_IP}:${this.ES_PORT}/_search`, { diff --git a/src/infrastructure/modules/app.module.ts b/src/infrastructure/modules/app.module.ts index 9b20b70..1eefb80 100644 --- a/src/infrastructure/modules/app.module.ts +++ b/src/infrastructure/modules/app.module.ts @@ -8,7 +8,6 @@ import * as modules from '../../core/modules' import { CommonModule } from './common/common.module'; import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { SearchModule } from './search.module'; -import { QueryStringPipe } from 'src/core/pipes/query-str.pipe'; /** * application modules list diff --git a/src/main.ts b/src/main.ts index 58e4e19..f413052 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './infrastructure/modules/app.module'; import { SwaggerModule, DocumentBuilder, SwaggerDocumentOptions } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; -import { QueryStringPipe } from './core/pipes/query-str.pipe'; + /** * Main entry point of the application * @returns Nothing diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index d773f4f..689c43c 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -152,7 +152,7 @@ describe('Unit tests for SearchService', () => { } } - searchService.findByContext(es_query); + // searchService.findByContext(es_query); expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { data: es_query, headers: { 'Content-Type': 'application/json' } -- 2.39.5 From 4c23bce69fc6331e977229ad9ca3c9a8f1aadc8a Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Fri, 7 Oct 2022 15:10:18 +0300 Subject: [PATCH 19/23] Fixed problems with reading 'undefined's --- .../controller/papers.controller.ts | 2 +- src/core/domain/enums/page-order.enum.ts | 2 + src/core/interceptors/page.interceptor.ts | 4 +- src/core/services/common/search.service.ts | 14 +-- src/test/search.service.spec.ts | 93 ++++--------------- 5 files changed, 30 insertions(+), 85 deletions(-) diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index bdf507a..60d0f3a 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Query, Req, UseFilters, UseInterceptors } from "@nestjs/common"; +import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Query, UseFilters, UseInterceptors } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; import { PageInterceptor } from "../../core/interceptors/page.interceptor"; import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; diff --git a/src/core/domain/enums/page-order.enum.ts b/src/core/domain/enums/page-order.enum.ts index 61e5176..9e724bd 100644 --- a/src/core/domain/enums/page-order.enum.ts +++ b/src/core/domain/enums/page-order.enum.ts @@ -19,6 +19,8 @@ export enum Order { * @returns Appropriate enum-member */ export function toOrder(str: string): Order { + if (!str) return Order.DESC; + switch (str) { case 'asc': return Order.ASC; case 'desc': return Order.DESC; diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 8a78ded..0551733 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,5 +1,5 @@ import { HttpService } from "@nestjs/axios"; -import { CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common"; +import { BadRequestException, CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, InternalServerErrorException, NestInterceptor } from "@nestjs/common"; import { Observable, map, take, switchMap, of } from "rxjs"; import { PageDto } from "../domain/dtos"; import { EsTime } from "../domain/enums/es-time.enum"; @@ -63,7 +63,7 @@ export class PageInterceptor implements NestInterceptor { }; // Check if the performed search is a backwards search - let data = res.hits.hits; + let data = res?.hits?.hits; // Omitting the redundant info and leaving only the document data = data.map((el) => el._source); // Change the order if set diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index b72685e..b037ab5 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,5 +1,5 @@ import { HttpService } from "@nestjs/axios"; -import { GatewayTimeoutException, Injectable, NotFoundException } from "@nestjs/common"; +import { GatewayTimeoutException, ImATeapotException, Injectable, NotFoundException } from "@nestjs/common"; import { map, take } from "rxjs"; import { EsResponseDto, SearchQueryDto} from "../../domain/dtos"; import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto"; @@ -28,7 +28,7 @@ export class SearchService { /** * Finds a paper by its own ID - * @param uuid + * @param uuid String, that represents unique identifier of a paper * @returns Elasticsearch hits or an error object */ async findByID(uuid: string): Promise { // Should I change 'object' to specific DTO? @@ -48,12 +48,12 @@ export class SearchService { })) ?.pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsResponseDto) => { - if (!res.hits.hits.length) { - reject(new NotFoundException); - } if (res.timed_out) { reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } + if (!res?.hits?.hits?.length) { + reject(new NotFoundException); + } resolve(res); }); } catch (error) { @@ -65,7 +65,7 @@ export class SearchService { /** * Finds relevant documents by context using the given query string * @param query, - * @returns Elasticsearch hits or an error object + * @returns Elasticsearch response */ async findByContext(query: SearchQueryDto): Promise { // Contruct a body for querying Elasticsearch @@ -86,7 +86,7 @@ export class SearchService { headers: {'Content-Type': 'application/json'}, })) ?.pipe(take(1), map(axiosRes => axiosRes.data)) - .subscribe((res: EsResponseDto) => { + ?.subscribe((res: EsResponseDto) => { if (res.timed_out) { reject(new GatewayTimeoutException('Elasticsearch Timed Out')); } diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index 689c43c..2fa1197 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -3,7 +3,7 @@ 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 { SearchQueryDto } from "src/core/domain"; import { SearchService } from "src/core/services/common/search.service"; describe('Unit tests for SearchService', () => { @@ -42,26 +42,6 @@ describe('Unit tests for SearchService', () => { expect(httpGetSpy).toHaveBeenCalled(); }); - // it('Should send correct data via HttpService.get() body parameter', () => { - // let httpGetSpy = jest.spyOn(httpService, 'get'); - - // const uuid = 'thisIsUUID_Provided'; - // searchService.findByID(uuid); - // expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { - // data: { - // size: 1, - // query: { - // query_string: { - // query: 'id:' + uuid - // } - // }, - // search_after: undefined, - // sort: undefined, - // }, - // headers: { 'Content-Type': 'application/json' } - // }); - // }); - it('Should call HttpService.get() with correct URI and port number', () => { let httpGetSpy = jest.spyOn(httpService, 'get'); @@ -76,28 +56,6 @@ describe('Unit tests for SearchService', () => { expect(searchService.findByID('')).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: { - // took: 1, - // timed_out: false, - // hits: { - // total: {}, - // hits: [{}] - // } - // }, - // }) - // ); - - // expect(searchService.findByID('')).resolves.toBeInstanceOf(EsResponseDto) - // }); - // Errors it('Should throw 504 | GatewayTimeoutException', () => { // Axios response mock @@ -136,25 +94,27 @@ describe('Unit tests for SearchService', () => { it('Should touch HttpService.get() method', () => { let httpGetSpy = jest.spyOn(httpService, 'get'); - searchService.findByContext(null); + searchService.findByContext({query: ""}); 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!' - } - } - } + const query = new SearchQueryDto('keyword', 1, 32, 'desc'); - // searchService.findByContext(es_query); + searchService.findByContext(query); expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { - data: es_query, + data: { + query: { + query_string: { + query: 'keyword', + default_field: 'content', + } + }, + from: 32, + size: 1, + }, headers: { 'Content-Type': 'application/json' } }); }); @@ -162,7 +122,7 @@ describe('Unit tests for SearchService', () => { it('Should call HttpService.get() with correct URI and port number', () => { let httpGetSpy = jest.spyOn(httpService, 'get'); - searchService.findByContext(null); + searchService.findByContext({query: ""}); expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>( `http://${process.env.ES_CONTAINER_NAME}:${process.env.ES_PORT}/_search`, expect.anything() @@ -170,26 +130,9 @@ describe('Unit tests for SearchService', () => { }); it('Should return a Promise', () => { - expect(searchService.findByContext(null)).toBeInstanceOf(Promise); + expect(searchService.findByContext({query: ""})).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 @@ -206,7 +149,7 @@ describe('Unit tests for SearchService', () => { }) ); - searchService.findByContext(null).catch((err) => { + searchService.findByContext({query: ""}).catch((err) => { expect(err).toBeInstanceOf(GatewayTimeoutException); }); }); @@ -216,7 +159,7 @@ describe('Unit tests for SearchService', () => { throw new HttpException({ oops: 'sorry' }, 999); }); - searchService.findByContext(null).catch((err) => { + searchService.findByContext({query: ""}).catch((err) => { expect(err).toBeInstanceOf(HttpException); expect(err.response).toEqual({ oops: 'sorry' }); expect(err.status).toEqual(999); -- 2.39.5 From 3707100826307fd89d00307f5d57b14431e6fa34 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Fri, 7 Oct 2022 15:28:32 +0300 Subject: [PATCH 20/23] Fixed relative path to HttpExceptionFilter, that failed the e2e test --- src/application/controller/papers.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 60d0f3a..04c6531 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -3,7 +3,7 @@ import { SearchService } from "../../core/services/common/search.service"; import { PageInterceptor } from "../../core/interceptors/page.interceptor"; import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { EsHitDto, EsResponseDto, PageDto, PaperDto, SearchQueryDto } from "../../core/domain"; -import { HttpExceptionFilter } from "src/core/filters/http-exception.filter"; +import { HttpExceptionFilter } from "../../core/filters/http-exception.filter"; /** * /papers/ route controller -- 2.39.5 From a5b175b2922fcdac659ab3de65c34bd78603d866 Mon Sep 17 00:00:00 2001 From: danny-mhlv Date: Wed, 12 Oct 2022 13:47:52 +0300 Subject: [PATCH 21/23] Fixed insufficient caching problem --- src/core/interceptors/page.interceptor.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 0551733..3a409b1 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -42,16 +42,17 @@ export class PageInterceptor implements NestInterceptor { async intercept(context: ExecutionContext, next: CallHandler): Promise> { const query = context.switchToHttp().getRequest().query; - // const offset = !query.offset ? 0 : query.offset; const offset = query.offset; - // const limit = !query.limit ? 10 : query.limit; const limit = query.limit; - // const order = !query.order ? Order.DESC : query.order; const order = query.order; + const query_string = query.query; const prev_page = await this.cacheManager.get('prev_page'); if (prev_page) { - if (offset == prev_page[1] && limit == prev_page[2] && order == prev_page[3]) return of(prev_page[0]); + if (offset == prev_page[1] && + limit == prev_page[2] && + order == prev_page[3] && + query_string === prev_page[4]) return of(prev_page[0]); } return next.handle().pipe( @@ -71,7 +72,7 @@ export class PageInterceptor implements NestInterceptor { // Cache and return the page const page: PageDto = new PageDto(data, meta); - await this.cacheManager.set('prev_page', [page, offset, limit, order]); + await this.cacheManager.set('prev_page', [page, offset, limit, order, query_string]); return page; }) ); -- 2.39.5 From f5ad2e0ff79fbf003e2363cfcd8630a1f9693a3c Mon Sep 17 00:00:00 2001 From: Danny Mikhaylov Date: Tue, 8 Nov 2022 05:18:17 +0300 Subject: [PATCH 22/23] Changed 'match' query to 'multi-match'. Allows to search for both 'content' and 'title' of the paper --- .../domain/dtos/elastic/es-multimatch.dto.ts | 68 +++++++++++++++++++ src/core/services/common/search.service.ts | 11 ++- 2 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 src/core/domain/dtos/elastic/es-multimatch.dto.ts diff --git a/src/core/domain/dtos/elastic/es-multimatch.dto.ts b/src/core/domain/dtos/elastic/es-multimatch.dto.ts new file mode 100644 index 0000000..6617fea --- /dev/null +++ b/src/core/domain/dtos/elastic/es-multimatch.dto.ts @@ -0,0 +1,68 @@ +import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsArray, IsDefined, IsInt, IsObject, IsOptional } from "class-validator"; +import { EsPit } from "../../interfaces/elastic/es-pit.interface"; +import { EsQuery } from "../../interfaces/elastic/es-query.interface" + +/** + * List of allowed properties in this DTO + */ + const allowedProperties = ['query']; + + /** + * Elasticsearch multi-match query DTO + */ + @ApiExtraModels() + export class EsMultimatchQueryDto { + /** + * Offset from the start of the list of hits + */ + @IsOptional() + @IsInt() + @ApiPropertyOptional({ + description: 'Offset from the start of the list of hits', + example: 5, + }) + public from?: number; + + /** + * Maximum number of elements returned by Elasticsearch + */ + @IsOptional() + @IsInt() + @ApiPropertyOptional({ + description: 'Maximum number of elements returned by Elasticsearch', + example: 30 + }) + public size?: number; + + /** + * The search query object passed to Elasticsearch + */ + @IsDefined() + @IsObject() + @ApiProperty({ + description: 'Search query object passed to Elasticsearch', + example: { + multi_match: { + query: 'Maths', + fields: [ + 'title', + 'content' + ] + } + }, + }) + private readonly query: Object; + + /** + * Constructs a multi-match + */ + constructor(query: string = '', fields: Array = ['content']) { + this.query = { + multi_match: { + query: query, + fields: fields + } + } + } + } \ No newline at end of file diff --git a/src/core/services/common/search.service.ts b/src/core/services/common/search.service.ts index b037ab5..7e5a47c 100644 --- a/src/core/services/common/search.service.ts +++ b/src/core/services/common/search.service.ts @@ -1,6 +1,7 @@ import { HttpService } from "@nestjs/axios"; import { GatewayTimeoutException, ImATeapotException, Injectable, NotFoundException } from "@nestjs/common"; import { map, take } from "rxjs"; +import { EsMultimatchQueryDto } from "src/core/domain/dtos/elastic/es-multimatch.dto"; import { EsResponseDto, SearchQueryDto} from "../../domain/dtos"; import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto"; @@ -69,13 +70,9 @@ export class SearchService { */ async findByContext(query: SearchQueryDto): Promise { // Contruct a body for querying Elasticsearch - const es_query: EsQueryDto = new EsQueryDto(); - es_query.query = { - query_string: { - query: query.query, - default_field: 'content', - } - }; + const es_query: EsMultimatchQueryDto = new EsMultimatchQueryDto(query.query, [ + 'title', 'content' + ]); es_query.from = query.offset; es_query.size = query.limit; -- 2.39.5 From cbf7938ed738cb441822751736dced3a18850995 Mon Sep 17 00:00:00 2001 From: Danny Date: Wed, 9 Nov 2022 13:27:50 +0300 Subject: [PATCH 23/23] Fixed the test issue --- src/core/domain/dtos/elastic/es-multimatch.dto.ts | 4 +--- src/test/search.service.spec.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/domain/dtos/elastic/es-multimatch.dto.ts b/src/core/domain/dtos/elastic/es-multimatch.dto.ts index 6617fea..81295d0 100644 --- a/src/core/domain/dtos/elastic/es-multimatch.dto.ts +++ b/src/core/domain/dtos/elastic/es-multimatch.dto.ts @@ -1,7 +1,5 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { IsArray, IsDefined, IsInt, IsObject, IsOptional } from "class-validator"; -import { EsPit } from "../../interfaces/elastic/es-pit.interface"; -import { EsQuery } from "../../interfaces/elastic/es-query.interface" +import { IsDefined, IsInt, IsObject, IsOptional } from "class-validator"; /** * List of allowed properties in this DTO diff --git a/src/test/search.service.spec.ts b/src/test/search.service.spec.ts index 2fa1197..76bc66a 100644 --- a/src/test/search.service.spec.ts +++ b/src/test/search.service.spec.ts @@ -107,13 +107,16 @@ describe('Unit tests for SearchService', () => { expect(httpGetSpy).toHaveBeenCalledWith<[string, object]>(expect.anything(), { data: { query: { - query_string: { - query: 'keyword', - default_field: 'content', + multi_match: { + query: query.query, + fields: [ + 'title', + 'content' + ] } }, - from: 32, - size: 1, + from: query.offset, + size: query.limit }, headers: { 'Content-Type': 'application/json' } }); -- 2.39.5
                          + + + toOrder + + +
                          +toOrder(str: string) +
                          +

                          Converts string value to appropriate enum-member

                          +
                          + +
                          + Parameters : + + + + + + + + + + + + + + + + + + + + + +
                          NameTypeOptionalDescription
                          str + string + + No + +

                          String to be converted

                          + +
                          +
                          +
                          +
                          +
                          + Returns : Order + +
                          +
                          +

                          Appropriate enum-member

                          +