import { HttpService } from "@nestjs/axios"; import { BadRequestException, CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, InternalServerErrorException, NestInterceptor } from "@nestjs/common"; import { Observable, map, take, switchMap, of } from "rxjs"; import { PageDto } from "../domain/dtos"; import { EsTime } from "../domain/enums/es-time.enum"; import { Order, toOrder } from "../domain/enums/page-order.enum"; import { EsPit } from "../domain/interfaces/elastic/es-pit.interface"; import { Cache } from 'cache-manager' import { PageMetaDto } from "../domain/dtos/page-meta.dto"; /** * Pagination-implementing interceptor */ @Injectable() export class PageInterceptor implements NestInterceptor { /** * Injects needed dependencies and instantiates the storage object * @param httpService * @param searchService */ constructor( private readonly httpService: HttpService, @Inject(CACHE_MANAGER) private cacheManager: Cache ) {} /** * Elastichsearch server port-number */ private readonly ES_PORT = process.env.ES_PORT; /** * Elastichsearch IP address */ private readonly ES_IP = process.env.ES_CONTAINER_NAME; /** * Override of intercept() method, specified in NestInterceptor interface * @param context * @param next * @returns Page with content and metadata */ async intercept(context: ExecutionContext, next: CallHandler): Promise> { const query = context.switchToHttp().getRequest().query; const offset = query.offset; const limit = query.limit; const order = query.order; const query_string = query.query; const prev_page = await this.cacheManager.get('prev_page'); if (prev_page) { if (offset == prev_page[1] && limit == prev_page[2] && order == prev_page[3] && query_string === prev_page[4]) return of(prev_page[0]); } return next.handle().pipe( switchMap(async (res) => { // Setting the page meta-data let meta: PageMetaDto = { total: res.hits.total.value, order: toOrder(order), }; // Check if the performed search is a backwards search let data = res?.hits?.hits; // Omitting the redundant info and leaving only the document data = data.map((el) => el._source); // Change the order if set if (order == Order.ASC) data.reverse(); // Cache and return the page const page: PageDto = new PageDto(data, meta); await this.cacheManager.set('prev_page', [page, offset, limit, order, query_string]); return page; }) ); } /** * Acquires a PIT ID from Elasticsearch, needed for a request * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes. * @returns PIT object containing PIT ID and keep_alive value */ public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise { return new Promise((resolve, reject) => { try { this.httpService.post(`http://${this.ES_IP}:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`) .pipe(take(1), map(axiosRes => axiosRes.data)) .subscribe((res: EsPit) => { res.keep_alive = alive + unit; resolve(res); }); } catch (error) { reject(error); } }); } /** * Deletes the PIT specified by provided ID * @param pitID, ID of the PIT, that would be deleted * @returns true/false, depending on the result of deletion of the PIT */ async deletePIT(pitID: string): Promise { return new Promise((resolve, reject) => { try { this.httpService.delete(`http://${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); } }) } }