Endpoints finalized. Test covered.
This commit is contained in:
parent
8d4e09f0f6
commit
a6e20cdfe3
@ -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
2
package-lock.json
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
83
src/core/domain/dtos/es-response.dto.ts
Normal file
83
src/core/domain/dtos/es-response.dto.ts
Normal 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;
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
export * from './es-response.dto'
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
51
src/test/papers.endpoint.spec.ts
Normal file
51
src/test/papers.endpoint.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
x
Reference in New Issue
Block a user