diff --git a/.env.develop b/.env.develop index c0a6960..868ed15 100644 --- a/.env.develop +++ b/.env.develop @@ -3,4 +3,9 @@ NODE_PORT=8085 LOCAL_PORT=8085 IMAGE_VERSION=0.0.1 IMAGE_NAME="image-name" -CONTAINER_NAME="container-name" \ No newline at end of file +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 diff --git a/.gitignore b/.gitignore index 6704566..e4913a7 100644 --- a/.gitignore +++ b/.gitignore @@ -90,7 +90,8 @@ dist # vuepress build output .vuepress/dist - + 419 +Page E # Serverless directories .serverless/ diff --git a/docker-compose.yaml b/docker-compose.yaml index 025c0b8..46a132b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,15 +1,33 @@ version: '3.3' services: - grover: + elasticsearch: + image: ${ES_IMAGE_NAME}:${ES_IMAGE_VERSION} + container_name: ${ES_CONTAINER_NAME} + restart: always + ports: + - "${ES_PORT}:${ES_PORT}" + environment: + - xpack.security.enabled=false + - discovery.type=single-node + + + + + + + freeland: image: ${IMAGE_NAME}:${IMAGE_VERSION} build: context: . dockerfile: Dockerfile container_name: ${CONTAINER_NAME} restart: always + links: + - "elasticsearch:localhost" ports: - - ${LOCAL_PORT}:${NODE_PORT} + - "${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 diff --git a/documentation/classes/EnvironmentVariables.html b/documentation/classes/EnvironmentVariables.html new file mode 100644 index 0000000..b5795c3 --- /dev/null +++ b/documentation/classes/EnvironmentVariables.html @@ -0,0 +1,198 @@ + + +
+ + ++
+ src/infrastructure/config/env.validation.ts
+
+
+
env vatiables
+ + + + + + + + + + + + + + + +import { plainToClass } from 'class-transformer';
+import { validateSync } from 'class-validator';
+
+/**
+ * env vatiables
+ */
+class EnvironmentVariables {
+ // /**
+ // * Represents the amount of comission for each transaction
+ // */
+ // @IsOptional()
+ // TRANSACTION_COMMISSION = 0.001;
+
+ // @IsOptional()
+ // WIDRAW_COMMISSION = 0.001;
+
+ // @IsOptional()
+ // DEPOSIT_FEE_PER_MINUTE = 0.0001;
+}
+
+/**
+ * validates the config
+ * @param config congig
+ * @returns validated config
+ */
+export function validate(config: Record<string, unknown>) {
+ const validatedConfig = plainToClass(EnvironmentVariables, config, { enableImplicitConversion: true });
+ const errors = validateSync(validatedConfig, { skipMissingProperties: false });
+
+ if (errors.length > 0) {
+ throw new Error(errors.toString());
+ }
+ return validatedConfig;
+}
+
+ +
+ src/core/domain/dtos/elastic/es-hit.dto.ts
+
+
+
Structure of the document stored and retrieved from Elasticsearch
+ + + + + + +
+ Properties+ |
+
+ + | +
+ + + Optional + _score + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ + | +
+ Hit relevance score + |
+
+ + + _source + + + | +
+ Type : PaperDto
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ + | +
+ Actual document stored in Elasticsearch + |
+
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
+ */
+const allowedProperties = ['sort', '_source', '_score'];
+
+/**
+ * Structure of the document stored and retrieved from Elasticsearch
+ */
+@ApiExtraModels()
+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()
+ @ApiPropertyOptional({
+ description: 'List of objects that represents how the hit was sorted',
+ example: {}
+ })
+ sort?: [];
+
+ /**
+ * Hit relevance score
+ */
+ @IsOptional()
+ @ApiPropertyOptional({
+ description: 'Relevance score',
+ example: 1.2355
+ })
+ _score?: number;
+}
+ +
+ src/core/domain/dtos/elastic/es-query.dto.ts
+
+
+
Elasticsearch query DTO
+ + + + + + +
+ Properties+ |
+
+
|
+
+constructor()
+ |
+
+ + | +
+ Constructs an empty object + |
+
+ + + Optional + pit + + + | +
+ Type : EsPit
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ + | +
+ Object, that stores PIT ID and time alive + |
+
+ + + query + + + | +
+ Type : EsQuery
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ + | +
+ The search query object passed to Elasticsearch + |
+
+ + + Optional + size + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ + | +
+ Maximum number of elements returned by Elasticsearch + |
+
import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { IsArray, IsDefined, IsInt, IsNotEmpty, IsNumber, 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 = ['size', 'query', 'pit', 'sort'];
+
+ /**
+ * Elasticsearch query DTO
+ */
+ @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;
+
+ /**
+ * Object, that stores PIT ID and time alive
+ */
+ @IsOptional()
+ @IsObject()
+ @ApiPropertyOptional({
+ description: 'PIT object',
+ example: {}
+ })
+ pit?: EsPit;
+
+ /**
+ * Sorting info
+ */
+ @IsOptional()
+ @IsArray()
+ @ApiPropertyOptional({
+ description: '',
+ example: []
+ })
+ sort?: unknown[];
+
+ /**
+ * 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;
+ }
+ }
+ +
+ src/core/domain/dtos/elastic/es-response.dto.ts
+
+
+
Elasticsearch response DTO
+ + + + + + +
+ Properties+ |
+
+ + | +
+ + + _shards + + + | +
+ Type : object
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ + | +
+ Contains a number of Elasticsearch shards +used for the request + |
+
+ + + hits + + + | +
+ Type : EsResponseHits
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ + | +
+ Contains returned documents and metadata + |
+
+ + + Optional + pit_id + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsString()
+ |
+
+ + | +
+ ID of the PIT used in the search + |
+
+ + + timed_out + + + | +
+ Type : boolean
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ + | +
+ Status of the request +If 'true' - the request timed out before completion + |
+
+ + + took + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ + | +
+ Number of milliseconds it +took Elasticsearch to execute the request + |
+
import { ApiExtraModels, ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
+import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsObject, IsOptional, IsString } from "class-validator";
+import { EsResponseHits } from "../../interfaces/elastic/es-response-hits.interface";
+
+/**
+ * List of allowed properties in this DTO
+ */
+const allowedProperties = ['took', 'timed_out', '_shards', 'hits', 'pit_id'];
+
+/**
+ * Elasticsearch response DTO
+ */
+@ApiExtraModels()
+export class EsResponseDto {
+ /**
+ * Number of milliseconds it
+ * took Elasticsearch to execute the request
+ */
+ @IsDefined()
+ @IsNotEmpty()
+ @IsNumber()
+ @ApiProperty({
+ description: 'The time that it took Elasticsearch to process the query',
+ example: 5
+ })
+ took: number;
+
+ /**
+ * Status of the request
+ * If 'true' - the request timed out before completion
+ */
+ @IsDefined()
+ @IsNotEmpty()
+ @IsBoolean()
+ @ApiProperty({
+ description: 'Shows if request timed out before completion',
+ example: false,
+ })
+ timed_out: boolean;
+
+ /**
+ * Contains a number of Elasticsearch shards
+ * used for the request
+ */
+ @IsOptional()
+ @IsObject()
+ @ApiProperty({
+ description: 'Contains a count of Elasticsearch shards used to process the request',
+ example: {
+ total: 1,
+ successful: 1,
+ skipped: 0,
+ failed: 0,
+ }
+ })
+ _shards: object;
+
+ /**
+ * Contains returned documents and metadata
+ */
+ @IsOptional()
+ @IsObject()
+ @ApiProperty({
+ description: 'Contains returned documents and metadata',
+ example: {
+ total: {
+ value: 3,
+ relation: 'eq'
+ },
+ max_score: 1.2,
+ hits: [{
+ _index: 'papers',
+ _id: '01002',
+ _score: 1.2,
+ _source: {},
+ fields: {}
+ }],
+ }
+ })
+ hits: EsResponseHits;
+
+ /**
+ * ID of the PIT used in the search
+ */
+ @IsString()
+ @IsOptional()
+ @ApiPropertyOptional({
+ description: 'Contains PIT ID used to search for results',
+ example: '46ToAwMDaWR5BXV1aWQyKwZub2RlXzMAAAAAAAAAACoBYwADaWR4BXV1aWQxAgZub2RlXzEAAAAAAAAAAAEBYQADaWR5BXV1aWQyKgZub2RlXzIAAAAAAAAAAAwBYgACBXV1aWQyAAAFdXVpZDEAAQltYXRjaF9hbGw_gAAAAA=='
+ })
+ pit_id?: string;
+}
+ +
+ src/core/exceptions/http-response.exception.ts
+
+
+
implements http exception with http response from the service of common module
+ + + ++
+ HttpException
+
+constructor(data: HttpResponse)
+ |
+ ||||||||
+ + | +||||||||
+ Http response exception contructor +
+ Parameters :
+
+
|
+
import { HttpException } from '@nestjs/common';
+import { HttpResponse } from '../domain/interfaces';
+
+//==================================================================================================
+/**
+ * implements http exception with http response from the service of common module
+ */
+export class HttpResponseException extends HttpException {
+ /**
+ * Http response exception contructor
+ * @param data Http response
+ */
+ constructor(data: HttpResponse) {
+ super(HttpException.createBody(data, data.description, data.status), data.status);
+ }
+}
+
+//==================================================================================================
+
+ +
+ src/core/domain/dtos/page.dto.ts
+
+
+
Page model for pagination
+ + + + + + +
+ Properties+ |
+
+ + | +
+constructor(data: PaperDto[], meta: PageMeta)
+ |
+ |||||||||
+ Defined in src/core/domain/dtos/page.dto.ts:37
+ |
+ |||||||||
+ Constructs an object with provided parameters +
+ Parameters :
+
+
|
+
+ + + Readonly + data + + + | +
+ Type : PaperDto[]
+
+ |
+
+ Decorators :
+ +
+ @IsArray()
+ |
+
+ Defined in src/core/domain/dtos/page.dto.ts:27
+ |
+
+ Data block of the page + |
+
+ + + Readonly + meta + + + | +
+ Type : PageMetaDto
+
+ |
+
+ Decorators :
+ +
+ @ApiProperty({description: 'Metadata for the page'})
+ |
+
+ Defined in src/core/domain/dtos/page.dto.ts:37
+ |
+
+ Metadata of the page + |
+
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";
+
+/**
+ * List of allowed properties in this DTO
+ */
+const allowedProperties = ['data', 'meta'];
+
+/**
+ * Page model for pagination
+ */
+@ApiExtraModels()
+export class PageDto {
+ /**
+ * Data block of the page
+ */
+ @IsArray()
+ @ApiProperty({
+ description: 'All data (papers) the page contains',
+ isArray: true,
+ type: PaperDto
+ })
+ readonly data: PaperDto[];
+
+ /**
+ * Metadata of the page
+ */
+ @ApiProperty({
+ description: 'Metadata for the page',
+ // example: {},
+
+ })
+ readonly meta: PageMetaDto;
+
+ /**
+ * Constructs an object with provided parameters
+ * @param data
+ * @param meta
+ */
+ constructor(data: PaperDto[], meta: PageMeta) {
+ this.data = data;
+ this.meta = meta;
+ }
+}
+ +
+ src/core/domain/dtos/page-meta.dto.ts
+
+
+
Page model for pagination
+ + + + ++
+ PageMeta
+
+ Properties+ |
+
+ + | +
+ + + hasNext + + + | +
+ Type : boolean
+
+ |
+
+ Decorators :
+ +
+ @ApiProperty({description: 'Flag, that shows if there's a page following the current one', example: true})
+ |
+
+ Defined in src/core/domain/dtos/page-meta.dto.ts:53
+ |
+
+ 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})
+ |
+
+ Defined in src/core/domain/dtos/page-meta.dto.ts:62
+ |
+
+ 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})
+ |
+
+ Defined in src/core/domain/dtos/page-meta.dto.ts:44
+ |
+
+ Order of the elements on the page + |
+
+ + + pagenum + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @ApiProperty({description: 'Current page number', minimum: 1, example: 3})
+ |
+
+ Defined in src/core/domain/dtos/page-meta.dto.ts:35
+ |
+
+ Current page number + |
+
+ + + pagesize + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @ApiProperty({description: 'Maximum number of elements on the page', minimum: 1, example: 20})
+ |
+
+ Defined in src/core/domain/dtos/page-meta.dto.ts:72
+ |
+
+ Maximum number of elements on the page + |
+
+ + + total + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsArray()
+ |
+
+ Defined in src/core/domain/dtos/page-meta.dto.ts:25
+ |
+
+ 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;
+}
+ +
+ src/core/domain/dtos/paper.dto.ts
+
+
+
Structure of the document stored and retrieved from Elasticsearch
+ + + + + + +
+ Properties+ |
+
+ + | +
+ + + authors + + + | +
+ Type : string[]
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:45
+ |
+
+ List of authors of the paper + |
+
+ + + content + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @ApiProperty({description: 'Contents of the paper presented in Markdown (.md) format', example: '...'})
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:87
+ |
+
+ Contents of the paper [Markdown] + |
+
+ + + id + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:23
+ |
+
+ Unique ID of the paper + |
+
+ + + summary + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:67
+ |
+
+ Summary of the paper. May be a short excerpt from the main text. + |
+
+ + + tags + + + | +
+ Type : string[]
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:78
+ |
+
+ List of tags, that show the certain topics/fields of knowledge paper is touching + |
+
+ + + title + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:34
+ |
+
+ Title of the paper + |
+
+ + + topic + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsNotEmpty()
+ |
+
+ Defined in src/core/domain/dtos/paper.dto.ts:56
+ |
+
+ Topic of the paper + |
+
import { ApiExtraModels, ApiProperty } from "@nestjs/swagger";
+import { IsArray, IsDefined, IsIn, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
+
+/**
+ * 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
+ */
+@ApiExtraModels()
+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;
+}
+ +
+ src/core/interceptors/page.interceptor.ts
+
+
+
Previous search data storage
+ + + + + + +
+ Properties+ |
+
+
|
+
+ Methods+ |
+
+
|
+
+ Accessors+ |
+
+
|
+
+constructor()
+ |
+
+ Defined in src/core/interceptors/page.interceptor.ts:16
+ |
+
+ Constructs an uninitialized object + |
+
+ + + Private + pit + + + | +
+ Type : EsPit
+
+ |
+
+ Defined in src/core/interceptors/page.interceptor.ts:29
+ |
+
+ PIT object of the previous search + |
+
+ + + Private + prevPage + + + | +
+ Type : number
+
+ |
+
+ Defined in src/core/interceptors/page.interceptor.ts:51
+ |
+
+ Number of the previous page + |
+
+ + + Private + tiebreaker + + + | +
+ Type : []
+
+ |
+
+ Defined in src/core/interceptors/page.interceptor.ts:40
+ |
+
+ Tiebreaker and sort parameters + |
+
+ + + Public + isSet + + + | +
+
+ isSet()
+ |
+
+ Defined in src/core/interceptors/page.interceptor.ts:63
+ |
+
+ Checks if there was the search before current one +
+
+
+ Returns :
+ boolean
+
+
+
+ true/false, showing whether or not there was another search before + + |
+
+ + _pit + | +||||||
+ get_pit()
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:33
+ |
+ ||||||
+ set_pit(pit: EsPit)
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:30
+ |
+ ||||||
+
+
+ Parameters :
+
+
+
+
+ Returns :
+ void
+
+ |
+
+ + _tiebreaker + | +||||||
+ get_tiebreaker()
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:44
+ |
+ ||||||
+ set_tiebreaker(tiebreaker: [])
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:41
+ |
+ ||||||
+
+
+ Parameters :
+
+
+
+
+ Returns :
+ void
+
+ |
+
+ + _prevPage + | +||||||
+ get_prevPage()
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:55
+ |
+ ||||||
+ set_prevPage(page: number)
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:52
+ |
+ ||||||
+
+
+ Parameters :
+
+
+
+
+ Returns :
+ void
+
+ |
+
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/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/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;
+ }
+}
+
+/**
+ * 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) {
+ // 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);
+ })
+ );
+ }
+
+ /**
+ * Elastichsearch server port-number
+ */
+ 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
+ */
+ 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://${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);
+ }
+ });
+ }
+
+ /**
+ * 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://${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);
+ }
+ })
+ }
+}
+ +
+ src/core/domain/dtos/request.dto.ts
+
+
+
Request object, which contains query parameters and Elasticsearch query object
+ + + + + + +
+ Properties+ |
+
+ + | +
+constructor(query: SearchQueryDto, es_query: EsQueryDto)
+ |
+ |||||||||
+ Defined in src/core/domain/dtos/request.dto.ts:37
+ |
+ |||||||||
+ Constructs an object with provided parameters +
+ Parameters :
+
+
|
+
+ + + Optional + es_query + + + | +
+ Type : EsQueryDto
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ Defined in src/core/domain/dtos/request.dto.ts:37
+ |
+
+ Elasticsearch query object + |
+
+ + + query + + + | +
+ Type : SearchQueryDto
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ Defined in src/core/domain/dtos/request.dto.ts:26
+ |
+
+ Query parameters object + |
+
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;
+ }
+}
+ +
+ src/core/domain/dtos/search-q.dto.ts
+
+
+
Elasticsearch response DTO
+ + + + + + +
+ Properties+ |
+
+ + | +
+constructor(query: string, page: number, limit: number, order: string)
+ |
+
+ Defined in src/core/domain/dtos/search-q.dto.ts:59
+ |
+
+ Constructs an object with provided parameters + |
+
+ + + limit + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ Defined in src/core/domain/dtos/search-q.dto.ts:48
+ |
+
+ Limits the number of displayed elements. + |
+
+ + + order + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsOptional()
+ |
+
+ Defined in src/core/domain/dtos/search-q.dto.ts:59
+ |
+
+ Limits the number of displayed elements. + |
+
+ + + page + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ Defined in src/core/domain/dtos/search-q.dto.ts:37
+ |
+
+ Page number to display. + |
+
+ + + query + + + | +
+ Type : string
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ Defined in src/core/domain/dtos/search-q.dto.ts:25
+ |
+
+ Given query string to perform the +search on. + |
+
import { ApiExtraModels, ApiProperty } from "@nestjs/swagger";
+import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator";
+
+/**
+ * List of allowed properties in this DTO
+ */
+const allowedProperties = ['query', 'pagen', 'limit', 'order'];
+
+/**
+ * Elasticsearch response DTO
+ */
+@ApiExtraModels()
+export class SearchQueryDto {
+ /**
+ * Given query string to perform the
+ * search on.
+ */
+ @IsDefined()
+ @IsNotEmpty()
+ @IsString()
+ @ApiProperty({
+ description: 'query',
+ 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',
+ example: 10,
+ })
+ limit: number;
+
+ /**
+ * Limits the number of displayed elements.
+ */
+ @IsOptional()
+ @IsString()
+ @ApiProperty({
+ description: 'order',
+ 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;
+ }
+}
+ +
+ src/core/domain/dtos/search-result.dto.ts
+
+
+
Elasticsearch response DTO
+ + + + + + +
+ Properties+ |
+
+
|
+
+constructor(code: number, data: EsResponseDto)
+ |
+ |||||||||
+ Defined in src/core/domain/dtos/search-result.dto.ts:42
+ |
+ |||||||||
+ Constructs an object with provided parameters +
+ Parameters :
+
+
|
+
+ + + data + + + | +
+ Type : EsResponseDto
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ Defined in src/core/domain/dtos/search-result.dto.ts:42
+ |
+
+ All the data acquired. + |
+
+ + + statusCode + + + | +
+ Type : number
+
+ |
+
+ Decorators :
+ +
+ @IsDefined()
+ |
+
+ Defined in src/core/domain/dtos/search-result.dto.ts:25
+ |
+
+ Status code + |
+
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;
+ }
+}
+ +
+ src/application/controller/health.controller.ts
+
+
+ health
+
+
+
Health controller class
+ + + + + + +
+ Methods+ |
+
+
|
+
import { Controller, Get } from '@nestjs/common';
+import { HealthCheckService, HttpHealthIndicator, HealthCheck } from '@nestjs/terminus';
+/**
+ * Health controller class
+ */
+@Controller('health')
+export class HealthController {
+ /**
+ * Health check controller class constructor.
+ * @param health health check service
+ * @param http http response
+ */
+ constructor(private health: HealthCheckService, private http: HttpHealthIndicator) {}
+ //======================================================================================================
+ /**
+ * Checks the liveness of the project
+ * @returns http response
+ */
+ @Get()
+ @HealthCheck()
+ check() {
+ return { status: 'ok', info: { alive: { status: 'up' } }, error: {}, details: { alive: { status: 'up' } } };
+ }
+}
+
+ +
+ src/application/controller/papers.controller.ts
+
+
+
/papers/ route controller
+ + + + + + +
+ Methods+ |
+
+
|
+
+ + + getByContext + + + | +||||||
+getByContext(request: RequestDto)
+ |
+ ||||||
+ Decorators :
+ + @ApiTags('Search')
+ |
+ ||||||
+ + | +||||||
+ Request handler for: GET /papers/search +
+ Parameters :
+
+
+
+
+
+ Returns :
+ Promise<EsResponseDto>
+
+
+
+ a response with a set of matching papers + + |
+
+ + + getByID + + + | +||||||
+getByID(uuid: string)
+ |
+ ||||||
+ Decorators :
+ + @ApiTags('Search')
+ |
+ ||||||
+ + | +||||||
+ Request handler for GET /papers/{uuid} +
+ Parameters :
+
+
+
+
+
+ Returns :
+ Promise<PaperDto>
+
+
+
+ a response with a requested object + + |
+
import { Controller, Get, HttpCode, 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";
+import { RequestDto } from "../../core/domain/dtos/request.dto";
+import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain";
+
+/**
+ * /papers/ route controller
+ */
+@Controller({
+ version: '1',
+ path: 'papers',
+})
+@ApiExtraModels(RequestDto, EsHitDto, EsResponseDto)
+export class PapersController {
+ constructor(private searchService: SearchService) {}
+
+ /**
+ * Request handler for: GET /papers/search
+ * @param query
+ * @param response
+ * @returns a response with a set of matching papers
+ */
+ @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() request: RequestDto): Promise<EsResponseDto> {
+ return this.searchService.findByContext(request.es_query).then(
+ (response) => {
+ return response;
+ },
+ (error) => {
+ throw error;
+ }
+ );
+ }
+
+ /**
+ * Request handler for GET /papers/{uuid}
+ * @param uuid
+ * @param response
+ * @returns a response with a requested object
+ */
+ @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')
+ @HttpCode(200)
+ getByID(@Param('uuid', ParseUUIDPipe) uuid: string): Promise<PaperDto> {
+ return this.searchService.findByID(uuid).then(
+ (response: EsResponseDto) => {
+ return response.hits.hits[0]._source;
+ },
+ (error) => {
+ throw error;
+ }
+ );
+ }
+}
+ File | +Type | +Identifier | +Statements | +
---|---|---|---|
+ + src/application/controller/health.controller.ts + | +controller | +HealthController | ++ 100 % + (2/2) + | +
+ + src/application/controller/papers.controller.ts + | +controller | +PapersController | ++ 100 % + (3/3) + | +
+ + src/core/decorators/public.decorator.ts + | +variable | +IS_PUBLIC_KEY | ++ 100 % + (1/1) + | +
+ + src/core/decorators/public.decorator.ts + | +variable | +Public | ++ 100 % + (1/1) + | +
+ + src/core/decorators/roles.decorator.ts + | +variable | +Roles | ++ 100 % + (1/1) + | +
+ + src/core/decorators/roles.decorator.ts + | +variable | +ROLES_KEY | ++ 100 % + (1/1) + | +
+ + src/core/domain/dtos/elastic/es-hit.dto.ts + | +class | +EsHitDto | ++ 100 % + (4/4) + | +
+ + src/core/domain/dtos/elastic/es-hit.dto.ts + | +variable | +allowedProperties | ++ 100 % + (1/1) + | +
+ + src/core/domain/dtos/elastic/es-query.dto.ts + | +class | +EsQueryDto | ++ 100 % + (7/7) + | +
+ + src/core/domain/dtos/elastic/es-query.dto.ts + | +variable | +allowedProperties | ++ 100 % + (1/1) + | +
+ + src/core/domain/dtos/elastic/es-response.dto.ts + | +class | +EsResponseDto | ++ 100 % + (6/6) + | +
+ + 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 | ++ 100 % + (1/1) + | +
+ + src/core/domain/dtos/page.dto.ts + | +class | +PageDto | ++ 100 % + (4/4) + | +
+ + src/core/domain/dtos/page.dto.ts + | +variable | +allowedProperties | ++ 100 % + (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) + | +
+ + src/core/domain/dtos/search-q.dto.ts + | +class | +SearchQueryDto | ++ 100 % + (6/6) + | +
+ + src/core/domain/dtos/search-q.dto.ts + | +variable | +allowedProperties | ++ 100 % + (1/1) + | +
+ + src/core/domain/dtos/search-result.dto.ts + | +class | +SearchResultDto | ++ 100 % + (4/4) + | +
+ + src/core/domain/dtos/search-result.dto.ts + | +variable | +allowedProperties | ++ 100 % + (1/1) + | +
+ + src/core/domain/interfaces/elastic/es-pit.interface.ts + | +interface | +EsPit | ++ 100 % + (3/3) + | +
+ + src/core/domain/interfaces/elastic/es-query-string.interface.ts + | +interface | +EqQueryString | ++ 100 % + (4/4) + | +
+ + src/core/domain/interfaces/elastic/es-query.interface.ts + | +interface | +EsQuery | ++ 100 % + (2/2) + | +
+ + src/core/domain/interfaces/elastic/es-response-hits.interface.ts + | +interface | +EsResponseHits | ++ 100 % + (4/4) + | +
+ + src/core/domain/interfaces/http-response.interface.ts + | +interface | +HttpResponse | ++ 100 % + (6/6) + | +
+ + src/core/domain/interfaces/page-meta.interface.ts + | +interface | +PageMeta | ++ 100 % + (7/7) + | +
+ + src/core/domain/interfaces/search-info.interface.ts + | +interface | +SearchInfo | ++ 100 % + (3/3) + | +
+ + src/core/exceptions/http-response.exception.ts + | +class | +HttpResponseException | ++ 100 % + (2/2) + | +
+ + src/core/guards/roles.guard.ts + | +guard | +RolesGuard | ++ 100 % + (3/3) + | +
+ + src/core/helpers/env.helper.ts + | +function | +expandEnvVariables | ++ 100 % + (1/1) + | +
+ + src/core/helpers/util.helper.ts + | +function | +naiveRound | ++ 100 % + (1/1) + | +
+ + src/core/helpers/util.helper.ts + | +function | +processHttpError | ++ 100 % + (1/1) + | +
+ + src/core/helpers/util.helper.ts + | +function | +processMicroserviceHttpError | ++ 100 % + (1/1) + | +
+ + src/core/helpers/util.helper.ts + | +function | +validateDTO | ++ 100 % + (1/1) + | +
+ + src/core/helpers/util.helper.ts + | +function | +validateOutputDTO | ++ 100 % + (1/1) + | +
+ + src/core/interceptors/logger.interceptor.ts + | +injectable | +LoggerInterceptor | ++ 100 % + (4/4) + | +
+ + src/core/interceptors/page.interceptor.ts + | +class | +PrevSearch | ++ 100 % + (6/6) + | +
+ + src/core/interceptors/page.interceptor.ts + | +injectable | +PageInterceptor | ++ 100 % + (8/8) + | +
+ + src/core/pipes/validation.pipe.ts + | +interface | +ValidationPipeOptions | ++ 100 % + (4/4) + | +
+ + src/core/services/common/http-response.service.ts + | +injectable | +HttpResponseService | ++ 100 % + (5/5) + | +
+ + src/core/services/common/logger.service.ts + | +injectable | +LoggerService | ++ 100 % + (11/11) + | +
+ + src/core/services/common/search.service.ts + | +injectable | +SearchService | ++ 100 % + (6/6) + | +
+ + src/infrastructure/config/env.objects.ts + | +interface | +VirtualBankOptions | ++ 100 % + (4/4) + | +
+ + src/infrastructure/config/env.objects.ts + | +variable | +configuration | ++ 100 % + (1/1) + | +
+ + src/infrastructure/config/env.validation.ts + | +class | +EnvironmentVariables | ++ 100 % + (1/1) + | +
+ + src/infrastructure/config/env.validation.ts + | +function | +validate | ++ 100 % + (1/1) + | +
+ + src/infrastructure/modules/app.module.ts + | +variable | +modulesList | ++ 100 % + (1/1) + | +
+ + src/main.ts + | +function | +bootstrap | ++ 100 % + (1/1) + | +
+
+ src/core/guards/roles.guard.ts
+
+
+
roles guard
+ + + + + + +
+ Methods+ |
+
+
|
+
+constructor(reflector: Reflector)
+ |
+ ||||||||
+ Defined in src/core/guards/roles.guard.ts:9
+ |
+ ||||||||
+ contructs the role guard service +
+ Parameters :
+
+
|
+
+ + + canActivate + + + | +||||||||
+canActivate(context: ExecutionContext)
+ |
+ ||||||||
+ Defined in src/core/guards/roles.guard.ts:23
+ |
+ ||||||||
+ checks if the user has allowed permission (role) +
+ Parameters :
+
+
+
+
+
+ Returns :
+ boolean
+
+
+
+ returns true if the user has appropriate role + + |
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { Roles as Role } from '..//domain/enums';
+import { ROLES_KEY } from '../decorators';
+/**
+ * roles guard
+ */
+@Injectable()
+export class RolesGuard implements CanActivate {
+ //==================================================================================================
+ /**
+ * contructs the role guard service
+ * @param reflector reflector of the guard
+ */
+ constructor(private reflector: Reflector) {}
+
+ //==================================================================================================
+ /**
+ * checks if the user has allowed permission (role)
+ * @param context context of the guard (actual information)
+ * @returns returns true if the user has appropriate role
+ */
+ canActivate(context: ExecutionContext): boolean {
+ const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
+ context.getHandler(),
+ context.getClass(),
+ ]);
+ if (!requiredRoles) {
+ return true;
+ }
+
+ const { user } = context.switchToHttp().getRequest();
+
+ return user.roles.some((role: Role) => requiredRoles.includes(role));
+ }
+
+ //==================================================================================================
+}
+
+ The hexagonal architecture, or ports and adapters architecture, is an architectural pattern used in software design. It aims at creating loosely coupled application components that can be easily connected to their software environment by means of ports and adapters. This makes components exchangeable at any level and facilitates test automation.
+git clone https://github.com/MoeidHeidari/nestjs-boilerplate
+cd monetary-transaction
There are different stages of building the application for this service. Based on the environment you want to deploy we have different ways to build the application. following information may help with building the service.
+npm install
+
+npm run build
+
+npm run test:ci
+
+npm start:{dev || debug || prod}
cd scripts
+
+bash run.sh -h
+
+2022.05.30.14.43
+
+Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-buildDocker] [-runDocker] [-runApp] [-runDoc] [-packageHelm]
+
+This script helps you to run the application in different forms. below you can get the full list of available options.
+
+Available options:
+
+-h, --help Print this help and exit
+
+-buildDocker Build the docker image called "imageName:latest"
+
+-runDocker Build the docker image and run on local machine
+
+-runApp Run application with npm in usual way for development
+
+-runDoc Generate the code documentation
+
+-packageHelm makes a helm package from the helm chart.
with the following instruction you can install the helm chart on an up and running kubernetes cluster.
+cd k8s
+
+helm install {sample-app} {app-0.1.0.tgz} --set service.type=NodePort
Alternativelly you can deploy the application on an up an running kubernetes cluster using provided config files.
+cd k8s/configFiles
+kubectl apply -f app-namespace.yaml, app-configmap.yaml, app-deployment.yaml, app-service.yaml
it should give you following output
+namespace/app created
+configmap/app-config created
+deployment.apps/app created
+service/app created
by calling the following endpoint you can make sure that the application is running and listening to your desired port
+http://localhost:{port_number}/health
most probably you will get a result back as follow
+++Example
+
++{"status":"ok","info":{"alive":{"status":"up"}},"error":{},"details":{"alive":{"status":"up"}}}
+
mertics
+to get the default metrics of the application you can use the following endpoint
+http://localhost:{port_number}/metrics
by calling the following endpoint you can see the Swagger OpenApi documentation and explore all the available apis and schemas.
+http://localhost:{port_number}/api
By running following comman you can generate the full code documentation (Compodoc) and get access to it through port 7000
npm run doc
+
+ src/core/services/common/http-response.service.ts
+
+
+
HTTP response service
+ + + + + +
+ Methods+ |
+
+
|
+
+ + + generate + + + | +|||||||||||||||||||||||||
+generate(status: number, data, message: string, description: string)
+ |
+ |||||||||||||||||||||||||
+ + | +|||||||||||||||||||||||||
+ generates the HTTP response +
+ Parameters :
+
+
+
+
+
+ Returns :
+ HttpResponse
+
+
+
+ response + + |
+
+ + + Private + getDescription + + + | +||||||||
+
+ getDescription(status: number)
+ |
+ ||||||||
+ + | +||||||||
+ gets the description +
+ Parameters :
+
+
+
+
+
+ Returns :
+ string
+
+
+
+ description + + |
+
+ + + Private + getMessage + + + | +||||||||
+
+ getMessage(status: number)
+ |
+ ||||||||
+ + | +||||||||
+ gets the message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ string
+
+
+
+ message + + |
+
+ + + Private + getType + + + | +||||||||
+
+ getType(status: number)
+ |
+ ||||||||
+ + | +||||||||
+ gets the type +
+ Parameters :
+
+
+
+
+
+ Returns :
+ string
+
+
+
+ type + + |
+
import { HttpStatus, Injectable } from '@nestjs/common';
+import {
+ HttpResponseDescriptions,
+ HttpResponseMessages,
+ HttpResponseTypes,
+ HttpResponseTypesCodes,
+} from '../../domain/enums'
+
+import { HttpResponse } from '../../domain/interfaces';
+
+/**
+ * HTTP response service
+ */
+@Injectable()
+export class HttpResponseService {
+ //==================================================================================================
+ /**
+ * gets the message
+ * @param status HTTP status
+ * @returns message
+ */
+ private getMessage(status: number): string {
+ return HttpResponseMessages[HttpStatus[status].toString() as keyof typeof HttpResponseMessages];
+ }
+
+ //==================================================================================================
+ /**
+ * gets the description
+ * @param status HTTP status
+ * @returns description
+ */
+ private getDescription(status: number): string {
+ return HttpResponseDescriptions[HttpStatus[status].toString() as keyof typeof HttpResponseMessages];
+ }
+
+ //==================================================================================================
+ /**
+ * gets the type
+ * @param status HTTP status
+ * @returns type
+ */
+ private getType(status: number): string {
+ return HttpResponseTypes[
+ HttpResponseTypesCodes[Math.floor(status / 100)].toString() as keyof typeof HttpResponseTypes
+ ];
+ }
+
+ //==================================================================================================
+ /**
+ * generates the HTTP response
+ * @param status HTTP status
+ * @param data data
+ * @param message custom message
+ * @param description custom description
+ * @returns response
+ */
+ generate(
+ status: number,
+ data: unknown = {},
+ message: string = this.getMessage(status),
+ description: string = this.getDescription(status)
+ ): HttpResponse {
+ const response: HttpResponse = {
+ type: this.getType(status),
+ status: status,
+ message: message,
+ description: description,
+ data: data,
+ };
+
+ return response;
+ }
+}
+
+ +
+ src/core/interceptors/logger.interceptor.ts
+
+
+
Logs the requests
+ + + + + +
+ Properties+ |
+
+
|
+
+ Methods+ |
+
+
|
+
+ + + Private + logHttpRequest + + + | +||||||||||||
+
+ logHttpRequest(context: ExecutionContext, startTime: number)
+ |
+ ||||||||||||
+ + | +||||||||||||
+ logs the HTTP requests +
+ Parameters :
+
+
+
+
+
+ Returns :
+ void
+
+
+
+ nothing + + |
+
+ + + Private + Readonly + logger + + + | +
+ Type : LoggerService
+
+ |
+
+ Default value : new LoggerService(LoggerInterceptor.name)
+ |
+
+ + | +
+ logs requests for the service + |
+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import { Observable } from 'rxjs';
+import { tap } from 'rxjs/operators';
+import { Request, Response } from 'express';
+import { LoggerService } from '../services/common'
+////////////////////////////////////////////////////////////////////////
+/**
+ * Logs the requests
+ */
+@Injectable()
+export class LoggerInterceptor implements NestInterceptor {
+ //==================================================================================================
+ /**
+ * logs requests for the service
+ */
+ private readonly logger: LoggerService = new LoggerService(LoggerInterceptor.name);
+
+ //==================================================================================================
+ /**
+ * intercept handler
+ * @param context context
+ * @param next next call
+ * @returns handler
+ */
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
+ const startTime = Date.now();
+ const contextType = context.getType();
+
+ return next.handle().pipe(
+ tap(
+ () => {
+ if (contextType === 'http') {
+ this.logHttpRequest(context, startTime);
+ }
+ },
+ (error: Error) => {
+ if (contextType === 'http') {
+ this.logHttpRequest(context, startTime);
+ } else {
+ const reqTime = Date.now() - startTime;
+ this.logger.log(`[${error.name}] ${error.message} ${reqTime}ms`);
+ }
+ }
+ )
+ );
+ }
+
+ //==================================================================================================
+ /**
+ * logs the HTTP requests
+ * @param context context
+ * @param startTime start time
+ * @returns nothing
+ */
+ private logHttpRequest(context: ExecutionContext, startTime: number) {
+ if (context.getType() !== 'http') return;
+ const reqTime = Date.now() - startTime;
+ const controllerName = context.getClass().name;
+ const handlerName = context.getHandler().name;
+ const request = context.switchToHttp().getRequest<Request>();
+ const response = context.switchToHttp().getResponse<Response>();
+ const { url, method } = request;
+ const { statusCode } = response;
+ this.logger.log(
+ `[HTTP] ${method.toUpperCase()} ${url} ${statusCode} [${controllerName}:${handlerName}] ${reqTime}ms`
+ );
+ }
+}
+
+ +
+ src/core/services/common/logger.service.ts
+
+
+
service for logging
+ + + + + +
+ Properties+ |
+
+ + | +
+ Methods+ |
+
+ + | +
+constructor(context: string)
+ |
+ ||||||
+ + | +||||||
+ constructor for the logger +
+ Parameters :
+
+
|
+
+ + + Static + createlogger + + + | +||||||||
+
+ createlogger(context: string)
+ |
+ ||||||||
+ + | +||||||||
+ creates the logger +
+ Parameters :
+
+
+
+
+
+ Returns :
+ LoggerService
+
+
+
+ logger + + |
+
+ + + Public + debug + + + | +||||||||||||
+
+ debug(message: string, ...args: any[])
+ |
+ ||||||||||||
+ + | +||||||||||||
+ logs the debug message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ void
+
+
+
+
+ |
+
+ + + Public + error + + + | +||||||||||||||||
+
+ error(message: string, error?: string | Error, ...args: any[])
+ |
+ ||||||||||||||||
+ + | +||||||||||||||||
+ logs the error message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ void
+
+
+
+
+ |
+
+ + + Private + format + + + | +||||||||||||
+
+ format(message: string, args?: string[])
+ |
+ ||||||||||||
+ + | +||||||||||||
+ formats the message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ any
+
+
+
+ formatted message + + |
+
+ + + Public + log + + + | +||||||||||||
+
+ log(message: string, ...args: any[])
+ |
+ ||||||||||||
+ + | +||||||||||||
+ logs the message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ void
+
+
+
+
+ |
+
+ + + Public + verbose + + + | +||||||||||||
+
+ verbose(message: string, ...args: any[])
+ |
+ ||||||||||||
+ + | +||||||||||||
+ logs the verbose message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ void
+
+
+
+
+ |
+
+ + + Public + warn + + + | +||||||||||||
+
+ warn(message: string, ...args: any[])
+ |
+ ||||||||||||
+ + | +||||||||||||
+ logs the warning message +
+ Parameters :
+
+
+
+
+
+ Returns :
+ void
+
+
+
+
+ |
+
+ + + Private + Readonly + Optional + context + + + | +
+ Type : string
+
+ |
+
+ + | +
+ context + |
+
+ + + Private + Readonly + logger + + + | +
+ Type : Logger
+
+ |
+
+ + | +
+ logger + |
+
import { Injectable, Logger, LoggerService as NestLoggerService } from '@nestjs/common';
+import { formatWithOptions } from 'util';
+
+/**
+ * service for logging
+ */
+@Injectable()
+export class LoggerService implements NestLoggerService {
+ /**
+ * logger
+ */
+ private readonly logger: Logger;
+ /**
+ * context
+ */
+ private readonly context?: string;
+ //=============================================================================================================
+ /**
+ * constructor for the logger
+ * @param context
+ */
+ constructor(context: string) {
+ this.logger = new Logger(context);
+ this.context = context;
+ }
+ //=============================================================================================================
+ /**
+ * creates the logger
+ * @param context context
+ * @returns logger
+ */
+ static createlogger(context: string): LoggerService {
+ return new LoggerService(context);
+ }
+ //=============================================================================================================
+ /**
+ * logs the message
+ * @param message message
+ * @param args arguments
+ */
+ public log(message: string, ...args: any[]) {
+ this.logger.log(this.format(message, args));
+ }
+ //=============================================================================================================
+ /**
+ * logs the error message
+ * @param message message
+ * @param error error
+ * @param args arguments
+ */
+ public error(message: string, error?: string | Error, ...args: any[]) {
+ this.logger.error(this.format(message, args), error instanceof Error ? error.stack : error);
+ }
+ //=============================================================================================================
+ /**
+ * logs the warning message
+ * @param message message
+ * @param args arguments
+ */
+ public warn(message: string, ...args: any[]) {
+ this.logger.warn(this.format(message, args));
+ }
+ //=============================================================================================================
+ /**
+ * logs the debug message
+ * @param message message
+ * @param args arguments
+ */
+ public debug(message: string, ...args: any[]) {
+ this.logger.debug(this.format(message, args));
+ }
+ //=============================================================================================================
+ /**
+ * logs the verbose message
+ * @param message message
+ * @param args arguments
+ */
+ public verbose(message: string, ...args: any[]) {
+ this.logger.verbose(this.format(message, args));
+ }
+ //=============================================================================================================
+ /**
+ * formats the message
+ * @param message message
+ * @param args arguments
+ * @returns formatted message
+ */
+ private format(message: string, args?: string[]) {
+ if (!args || !args.length) return message;
+
+ return formatWithOptions({ colors: true, depth: 5 }, message, ...args);
+ }
+ //=============================================================================================================
+}
+
+ +
+ src/core/interceptors/page.interceptor.ts
+
+
+
Pagination-implementing interceptor
+ + + + + +
+ Properties+ |
+
+
|
+
+ Methods+ |
+
+ + | +
+constructor(httpService: HttpService)
+ |
+ ||||||
+ Defined in src/core/interceptors/page.interceptor.ts:73
+ |
+ ||||||
+ Injects needed dependencies and instantiates the storage object +
+ Parameters :
+
+
|
+
+ + + Async + deletePIT + + + | +||||||||
+
+ deletePIT(pitID: string)
+ |
+ ||||||||
+ + | +||||||||
+ Deletes the PIT specified by provided ID +
+ Parameters :
+
+
+
+
+
+ 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 :
+
+
+
+
+
+ Returns :
+ Promise<EsPit>
+
+
+
+ PIT object |
+
+ + + Async + intercept + + + | +|||||||||
+
+ intercept(context: ExecutionContext, next: CallHandler
+ |
+ |||||||||
+ Defined in src/core/interceptors/page.interceptor.ts:89
+ |
+ |||||||||
+ Override of intercept() method, specified in NestInterceptor interface +
+ Parameters :
+
+
+
+
+
+ Returns :
+ Promise<Observable<PageDto>>
+
+
+
+ Page with content and metadata + + |
+
+ + + Private + Readonly + ES_IP + + + | +
+ Default value : process.env.ES_CONTAINER_NAME
+ |
+
+ + | +
+ Elastichsearch IP address + |
+
+ + + Private + Readonly + ES_PORT + + + | +
+ Default value : process.env.ES_PORT
+ |
+
+ + | +
+ Elastichsearch server port-number + |
+
+ + + Private + prevSearch + + + | +
+ Type : PrevSearch
+
+ |
+
+ + | +
+ Info about previously completed search + |
+
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/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/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;
+ }
+}
+
+/**
+ * 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) {
+ // 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);
+ })
+ );
+ }
+
+ /**
+ * Elastichsearch server port-number
+ */
+ 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
+ */
+ 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://${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);
+ }
+ });
+ }
+
+ /**
+ * 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://${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);
+ }
+ })
+ }
+}
+ +
+ src/core/services/common/search.service.ts
+
+
+
Search service provider
+ + + + + +
+ Properties+ |
+
+ + | +
+ Methods+ |
+
+
|
+
+constructor(httpService: HttpService)
+ |
+ ||||||
+ + | +||||||
+ Constructs the service with injection of +HTTPService instance +
+ Parameters :
+
+
|
+
+ + + Async + findByContext + + + | +||||||
+
+ findByContext(es_query: EsQueryDto)
+ |
+ ||||||
+ + | +||||||
+ Finds relevant documents by context using the given query string +
+ Parameters :
+
+
+
+
+
+ Returns :
+ Promise<EsResponseDto>
+
+
+
+ Elasticsearch hits or an error object + + |
+
+ + + Async + findByID + + + | +||||||
+
+ findByID(uuid: string)
+ |
+ ||||||
+ + | +||||||
+ Finds a paper by its own ID +
+ Parameters :
+
+
+
+
+
+ Returns :
+ Promise<EsResponseDto>
+
+
+
+ Elasticsearch hits or an error object + + |
+
+ + + Private + Readonly + ES_IP + + + | +
+ Default value : process.env.ES_CONTAINER_NAME
+ |
+
+ + | +
+ Elasticsearch IP address + |
+
+ + + Private + Readonly + ES_PORT + + + | +
+ Default value : process.env.ES_PORT
+ |
+
+ + | +
+ Elastichsearch server port-number + |
+
import { HttpService } from "@nestjs/axios";
+import { GatewayTimeoutException, HttpException, Injectable } from "@nestjs/common";
+import { map, take } from "rxjs";
+import { EsResponseDto} from "../../domain/dtos";
+import { EsQueryDto } from "../../domain/dtos/elastic/es-query.dto";
+
+/**
+ * Search service provider
+ */
+@Injectable()
+export class SearchService {
+ /**
+ * Constructs the service with injection of
+ * HTTPService instance
+ * @param httpService
+ */
+ constructor(private readonly httpService: HttpService) {}
+
+ /**
+ * Elastichsearch server port-number
+ */
+ 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<EsResponseDto> { // Should I change 'object' to specific DTO?
+ 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://${this.ES_IP}:${this.ES_PORT}/_search`, {
+ data: ESQ,
+ headers: {'Content-Type': 'application/json'},
+ }))
+ ?.pipe(take(1), map(axiosRes => axiosRes.data))
+ .subscribe((res: EsResponseDto) => {
+ if (res.timed_out) {
+ reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
+ }
+ resolve(res);
+ });
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+
+ /**
+ * Finds relevant documents by context using the given query string
+ * @param query, <EsQueryDto>
+ * @returns Elasticsearch hits or an error object
+ */
+ async findByContext(es_query: EsQueryDto): Promise<EsResponseDto> {
+ return new Promise((resolve, reject) => {
+ try {
+ (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))
+ .subscribe((res: EsResponseDto) => {
+ if (res.timed_out) {
+ reject(new GatewayTimeoutException('Elasticsearch Timed Out'));
+ }
+
+ resolve(res);
+ });
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+}
+ +
+ src/core/domain/interfaces/elastic/es-query-string.interface.ts
+
+
+
Structure of page metadata
+ + + + +
+ Properties+ |
+
+
|
+
+ + default_field + + + + + | +
+ default_field:
+ |
+
+ Type : string
+
+ |
+
+ Optional + | +
+ Default field to perform a search on, when +no field is specified for the query + |
+
+ + fields + + + + + | +
+ fields:
+ |
+
+ Type : string[]
+
+ |
+
+ Optional + | +
+ Specific fields, to perform a search on +Can't be specified with 'default_field' + |
+
+ + query + + + + + | +
+ query:
+ |
+
+ 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[];
+}
+ +
+ src/core/domain/interfaces/elastic/es-pit.interface.ts
+
+
+
Structure of PIT (Point-In-Time) object
+ + + + +
+ Properties+ |
+
+
|
+
+ + id + + + + + | +
+ id:
+ |
+
+ Type : string
+
+ |
+
+ PIT ID + |
+
+ + keep_alive + + + + + | +
+ keep_alive:
+ |
+
+ Type : string
+
+ |
+
+ Time to live of the PIT + |
+
export interface EsPit {
+ /**
+ * PIT ID
+ */
+ id: string;
+
+ /**
+ * Time to live of the PIT
+ */
+ keep_alive: string;
+}
+ +
+ src/core/domain/interfaces/elastic/es-query.interface.ts
+
+
+
Structure of page metadata
+ + + + +
+ Properties+ |
+
+
|
+
+ + query_string + + + + + | +
+ query_string:
+ |
+
+ 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;
+}
+ +
+ src/core/domain/interfaces/elastic/es-response-hits.interface.ts
+
+
+
Structure of 'hits' object of Elasticsearch response
+ + + + +
+ Properties+ |
+
+
|
+
+ + hits + + + + + | +
+ hits:
+ |
+
+ Type : EsHitDto[]
+
+ |
+
+ Array of search results + |
+
+ + max_score + + + + + | +
+ max_score:
+ |
+
+ Type : number
+
+ |
+
+ Optional + | +
+ Maximum score amongst all search results + |
+
+ + total + + + + + | +
+ total:
+ |
+
+ Type : object
+
+ |
+
+ Object containing info about hits + |
+
import { EsHitDto } from "../../dtos/elastic/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[];
+}
+ +
+ src/core/domain/interfaces/http-response.interface.ts
+
+
+
Basic HTTP response interface
+ + + + +
+ Properties+ |
+
+
|
+
+ + data + + + + + | +
+ data:
+ |
+
+ Type : any
+
+ |
+
+ Represents the actual data which is returned by the API. In case of empty response we will have it empty also. + |
+
+ + description + + + + + | +
+ description:
+ |
+
+ Type : string
+
+ |
+
+ Represents a full description about the response (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) + |
+
+ + message + + + + + | +
+ message:
+ |
+
+ Type : string
+
+ |
+
+ Represents a short message about the response status. + |
+
+ + status + + + + + | +
+ status:
+ |
+
+ Type : number
+
+ |
+
+ Represents the status code of the http response(https://en.wikipedia.org/wiki/List_of_HTTP_status_codes). + |
+
+ + type + + + + + | +
+ type:
+ |
+
+ Type : string
+
+ |
+
+ Represents the type of the response + |
+
export interface HttpResponse {
+ /**
+ * Represents the type of the response
+ */
+ type: string;
+ /**
+ * Represents the status code of the http response(https://en.wikipedia.org/wiki/List_of_HTTP_status_codes).
+ */
+ status: number;
+ /**
+ * Represents a short message about the response status.
+ */
+ message: string;
+ /**
+ * Represents a full description about the response (https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
+ */
+ description: string;
+ /**
+ * Represents the actual data which is returned by the API. In case of empty response we will have it empty also.
+ */
+ data: any;
+}
+
+ +
+ src/core/domain/interfaces/page-meta.interface.ts
+
+
+
Structure of page metadata
+ + + + +
+ Properties+ |
+
+
|
+
+ + hasNext + + + + + | +
+ hasNext:
+ |
+
+ Type : boolean
+
+ |
+
+ Flag that indicates presence of the next page + |
+
+ + hasPrev + + + + + | +
+ hasPrev:
+ |
+
+ Type : boolean
+
+ |
+
+ Flag that indicates presence of the previous page + |
+
+ + order + + + + + | +
+ order:
+ |
+
+ Type : Order
+
+ |
+
+ Order of the elements on the page + |
+
+ + pagenum + + + + + | +
+ pagenum:
+ |
+
+ Type : number
+
+ |
+
+ Number of the page + |
+
+ + pagesize + + + + + | +
+ pagesize:
+ |
+
+ Type : number
+
+ |
+
+ Number of elements on the page + |
+
+ + total + + + + + | +
+ total:
+ |
+
+ Type : number
+
+ |
+
+ Total search results + |
+
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;
+}
+ +
+ src/core/domain/interfaces/search-info.interface.ts
+
+
+
Structure of search metadata
+ + + + +
+ Properties+ |
+
+
|
+
+ + pit + + + + + | +
+ pit:
+ |
+
+ Type : EsPit
+
+ |
+
+ Previous search saved PIT + |
+
+ + tiebreaker + + + + + | +
+ tiebreaker:
+ |
+
+ Type : []
+
+ |
+
+ Special tiebreaker used by Elasticsearch. +Indicates the starting point of next search + |
+
import { EsPit } from "./elastic/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[];
+}
+ +
+ src/core/pipes/validation.pipe.ts
+
+
+
env variables validation pipeline
+ + + ++
+ ValidatorOptions
+
+ Properties+ |
+
+
|
+
+ + disableErrorMessages + + + + + | +
+ disableErrorMessages:
+ |
+
+ Type : boolean
+
+ |
+
+ Optional + | +
+ If error messages should be disabled + |
+
+ + exceptionFactory + + + + + | +
+ exceptionFactory:
+ |
+
+ Type : function
+
+ |
+
+ Optional + | +
+ Exception factory + |
+
+ + transform + + + + + | +
+ transform:
+ |
+
+ Type : boolean
+
+ |
+
+ Optional + | +
+ If it should be transformed + |
+
import { ValidationError, ValidatorOptions } from 'class-validator';
+/**
+ * env variables validation pipeline
+ */
+export interface ValidationPipeOptions extends ValidatorOptions {
+ /**
+ * If it should be transformed
+ */
+ transform?: boolean;
+ /**
+ * If error messages should be disabled
+ */
+ disableErrorMessages?: boolean;
+ /**
+ * Exception factory
+ */
+ exceptionFactory?: (errors: ValidationError[]) => any;
+}
+
+ +
+ src/infrastructure/config/env.objects.ts
+
+
+
VirtualBank options
+ + + + +
+ Properties+ |
+
+
|
+
+ + deposit_fee_per_minute + + + + + | +
+ deposit_fee_per_minute:
+ |
+
+ Type : number
+
+ |
+
+ Represents the fee for each minute more if customer keeps the money in our bank + |
+
+ + transaction_commission + + + + + | +
+ transaction_commission:
+ |
+
+ Type : number
+
+ |
+
+ Represents the commision amount defined for each money transaction + |
+
+ + widraw_commission + + + + + | +
+ widraw_commission:
+ |
+
+ Type : number
+
+ |
+
+ Represents the ammount of commission for each widrawal + |
+
import { expandEnvVariables } from '../../core/helpers/env.helper'
+expandEnvVariables();
+
+/**
+ * options enum
+ */
+export enum EnvObjects {
+ TRANSACTION_COMMISSION = 'VirtualBankOptions',
+ WIDRAW_COMMISSION = 'VirtualBankOptions',
+ DEPOSIT_FEE_PER_MINUTE = 'VirtualBankOptions',
+}
+//===================================================================================================
+/**
+ * VirtualBank options
+ */
+export interface VirtualBankOptions {
+ /**
+ * Represents the commision amount defined for each money transaction
+ */
+ transaction_commission: number;
+ /**
+ * Represents the ammount of commission for each widrawal
+ */
+ widraw_commission: number;
+
+ /**
+ * Represents the fee for each minute more if customer keeps the money in our bank
+ */
+ deposit_fee_per_minute: number;
+}
+
+/**
+ * configuration function
+ * @returns configuration taken from env
+ */
+export const configuration = (): any => ({
+ VirtualBankOptions: {
+ transaction_commission: process.env.TRANSACTION_COMMISSION,
+ widraw_commission: process.env.WIDRAW_COMMISSION,
+ deposit_fee_per_minute: process.env.DEPOSIT_FEE_PER_MINUTE,
+ },
+});
+
+