Endpoints finalized. Test covered.

This commit is contained in:
danny-mhlv 2022-08-02 02:06:37 +03:00
parent 8d4e09f0f6
commit a6e20cdfe3
12 changed files with 240 additions and 55 deletions

View File

@ -17,8 +17,10 @@ services:
image: ${ES_IMAGE_NAME}:${ES_IMAGE_VERSION} image: ${ES_IMAGE_NAME}:${ES_IMAGE_VERSION}
build: build:
context: . context: .
dockerfile: Dockerfile
container_name: ${ES_CONTAINER_NAME} container_name: ${ES_CONTAINER_NAME}
restart: always
ports:
- ${ES_PORT}:${ES_PORT}
environment: environment:
- xpack.security.enabled=false - xpack.security.enabled=false
- discovery.type=single-node - discovery.type=single-node

2
package-lock.json generated
View File

@ -38,7 +38,7 @@
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "27.5.1", "@types/jest": "27.5.1",
"@types/node": "^17.0.38", "@types/node": "^17.0.38",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1", "eslint": "^8.0.1",

View File

@ -51,7 +51,7 @@
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "27.5.1", "@types/jest": "27.5.1",
"@types/node": "^17.0.38", "@types/node": "^17.0.38",
"@types/supertest": "^2.0.11", "@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0", "@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1", "eslint": "^8.0.1",
@ -75,7 +75,8 @@
"json", "json",
"ts" "ts"
], ],
"rootDir": "src", "modulePaths": ["<rootDir>"],
"rootDir": "./",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"

View File

@ -1,22 +1,45 @@
import { Controller, Get, Param, Put, Query, Res } from "@nestjs/common"; import { Controller, Get, Next, Param, ParseUUIDPipe, Put, Query, Req, Res } from "@nestjs/common";
import { SearchService } from "src/core/services/common/search.service"; import { EsResponseDto } from "src/core/domain/dtos";
import { Response } from "express"; import { SearchService } from "../../core/services/common/search.service";
import { response, Response } from "express";
@Controller('papers') @Controller('papers')
export class PapersController { export class PapersController {
constructor(private searchService: SearchService) {} constructor(private searchService: SearchService) {}
/**
*
* @param query
* @param response
* @returns a response with a set of matching papers
*/
@Get('search') @Get('search')
getByContext(@Query('query') query: string/*, @Query('page') page, @Query('limit') limit*/): object { getByContext(@Query('query') query: string, @Res() response: Response/*, @Query('page') page, @Query('limit') limit*/) {
return this.searchService.findByContext(query); return this.searchService.findByContext(query).then(
(res) => {
response.status(200).send(res);
},
(err) => {
response.status(err).send();
}
);
} }
@Get(':id') /**
getByID(@Param('id') id: string): object { *
if(!id) { * @param uuid
//response.status(400).send({msg: "fff"}); * @param response
} else { * @returns a response with a requested object
return this.searchService.findByID(id); */
@Get(':uuid')
getByID(@Param('uuid', ParseUUIDPipe) uuid: string, @Res() response: Response) {
return this.searchService.findByID(uuid).then(
(res) => {
response.status(200).send(res);
},
(err) => {
response.status(err).send();
} }
);
} }
} }

View File

@ -0,0 +1,83 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional } from "class-validator";
/**
* List of allowed properties in this DTO
*/
const allowedProperties = ['took', 'timed_out', '_shards', 'hits'];
/**
* Elasticsearch response DTO
*/
export class EsResponseDto {
/**
* Number of milliseconds it
* took Elasticsearch to execute the request
*/
@IsDefined()
@IsNotEmpty()
@IsNumber()
@ApiProperty({
description: 'took',
example: 5
})
took: number;
/**
* Status of the request
* If 'true' - the request timed out before completion
*/
@IsDefined()
@IsNotEmpty()
@IsBoolean()
@ApiProperty({
description: 'timed_out',
example: false,
})
timed_out: boolean;
/**
* Contains a number of Elasticsearch shards
* used for the request
*/
@IsOptional()
@IsObject()
@ApiProperty({
description: '_shards',
example: {
total: 1,
successful: 1,
skipped: 0,
failed: 0,
}
})
_shards: object;
/**
* Contains returned documents and metadata
*/
@IsOptional()
@IsObject()
@ApiProperty({
description: 'hits',
example: {
total: {
value: 3,
relation: 'eq'
},
max_score: 1.2,
hits: [{
_index: 'papers',
_id: '01002',
_score: 1.2,
_source: {
},
fields: {
}
}],
}
})
hits: object;
}

View File

@ -0,0 +1 @@
export * from './es-response.dto'

View File

@ -1,6 +1,7 @@
import { HttpService } from "@nestjs/axios"; import { HttpService } from "@nestjs/axios";
import { Injectable } from "@nestjs/common"; import { HttpException, HttpStatus, Injectable, NotAcceptableException, NotFoundException } from "@nestjs/common";
import { map, Observable } from "rxjs"; import { map, NotFoundError, take } from "rxjs";
import { EsResponseDto } from "src/core/domain/dtos";
/** /**
* Search service provider * Search service provider
@ -9,26 +10,51 @@ import { map, Observable } from "rxjs";
export class SearchService { export class SearchService {
constructor(private readonly httpService: HttpService) {} constructor(private readonly httpService: HttpService) {}
// Find paper by its ID /**
findByID(id: string): object { * Finds a paper by its own ID
* @param uuid
* @returns Elasticsearch hits or an error object
*/
async findByID(uuid: string): Promise<any> { // Should I change 'object' to specific DTO?
let es_query = { let es_query = {
query: { query: {
query_string: { query_string: {
query: 'id:' + id query: 'id:' + uuid
} }
} }
} }
return this.httpService.get('http://localhost:9200/_search', { // Specify ES_PORT env!
return new Promise((resolve, reject) => {
try {
(this.httpService.get<EsResponseDto>('http://localhost:9200/_search', {
data: es_query, data: es_query,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
}).pipe( }))
map(response=>response.data) .pipe(take(1), map(axiosRes => axiosRes.data))
); .subscribe((res: any) => {
if (res.timed_out) {
reject(504);
} }
// Find paper by context if (!res.hits.hits.length) {
findByContext(query_str: string): object { reject(404);
}
resolve(res.hits);
});
} catch (error) {
reject(error);
}
});
}
/**
* Finds relevant documents by context using the given query string
* @param query_str
* @returns Elasticsearch hits or an error object
*/
findByContext(query_str: string): Promise<EsResponseDto> {
let es_query = { let es_query = {
query: { query: {
query_string: { query_string: {
@ -38,11 +64,27 @@ export class SearchService {
} }
} }
return this.httpService.get('http://localhost:9200/_search', { return new Promise((resolve, reject) => {
try {
(this.httpService.get<EsResponseDto>('http://localhost:9200/_search', {
data: es_query, data: es_query,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
}).pipe( }))
map(response=>response.data) .pipe(take(1), map(axiosRes => axiosRes.data))
); .subscribe((res: any) => {
if (res.timed_out) {
reject(504);
}
if (!res.hits.hits.length) {
reject(404);
}
resolve(res.hits);
});
} catch (error) {
reject(error);
}
});
} }
} }

View File

@ -7,7 +7,7 @@ import { LoggerInterceptor } from '../../core/interceptors'
import * as modules from '../../core/modules' import * as modules from '../../core/modules'
import { CommonModule } from './common/common.module'; import { CommonModule } from './common/common.module';
import { PrometheusModule } from '@willsoto/nestjs-prometheus'; import { PrometheusModule } from '@willsoto/nestjs-prometheus';
import { PapersController } from 'src/application'; import { PapersController } from 'src/application/controller/papers.controller';
import { SearchModule } from './search.module'; import { SearchModule } from './search.module';
/** /**

View File

@ -1,6 +1,6 @@
import { HttpModule } from "@nestjs/axios"; import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { SearchService } from "src/core/services/common/search.service"; import { SearchService } from "../../core/services/common/search.service";
/** /**
* search module * search module

View File

@ -0,0 +1,51 @@
import { Test, TestingModule } from "@nestjs/testing";
import request from 'supertest'
import { INestApplication } from "@nestjs/common";
import { AppModule } from "../infrastructure/modules";
describe('PapersController', () => {
let app: INestApplication;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
await app.init();
});
/**
* Getting paper by its ID
*/
it('Should be 404', () => {
let uuid = 'eeeb2d01-8315-454e-b33f-3d6caa25db43';
return request(app.getHttpServer())
.get('/papers/' + uuid)
.expect(404);
});
it('Should be 200', () => {
let uuid = 'eeeb2d01-8315-454e-b33f-3d6caa25db42';
return request(app.getHttpServer())
.get('/papers/' + uuid)
.expect(200);
});
/**
* Getting paper by the given context
*/
it('Should be 404', () => {
let q_str = 'Explosion'; // Non-existing word in the test-paper
return request(app.getHttpServer())
.get('/papers/search?query=' + q_str)
.expect(404);
});
it('Should be 200', () => {
let q_str = 'Docker'; // Existing word in the test-paper
return request(app.getHttpServer())
.get('/papers/search?query=' + q_str)
.expect(200);
});
});

View File

View File

@ -1,18 +0,0 @@
import { Test, TestingModule } from "@nestjs/testing";
import { PapersController } from "../src/application/controller/papers.controller";
describe('PapersController', () => {
let controller: PapersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PapersController],
}).compile();
controller = module.get<PapersController>(PapersController);
});
it('Should be defined', () => {
expect(controller).toBeDefined();
});
});