File

src/core/interceptors/page.interceptor.ts

Description

Pagination-implementing interceptor

Index

Properties
Methods

Constructor

constructor(httpService: HttpService)

Injects needed dependencies and instantiates the storage object

Parameters :
Name Type Optional
httpService HttpService No

Methods

Async deletePIT
deletePIT(pitID: string)

Deletes the PIT specified by provided ID

Parameters :
Name Type Optional Description
pitID string No

, ID of the PIT, that would be deleted

Returns : Promise<boolean>

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

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

Acquires a PIT ID from Elasticsearch, needed for a request

Parameters :
Name Type Optional Default value Description
alive number No

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

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

PIT object containing PIT ID and keep_alive value

Async intercept
intercept(context: ExecutionContext, next: CallHandler)

Override of intercept() method, specified in NestInterceptor interface

Parameters :
Name Type Optional
context ExecutionContext No
next CallHandler<any> No

Page with content and metadata

Properties

Private Readonly ES_PORT
Default value : process.env.ES_PORT

Elastichsearch server port-number

Private prevSearch
Type : PrevSearch

Info about previously completed search

import { HttpService } from "@nestjs/axios";
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import { reverse } from "dns";
import { Observable, map, take } from "rxjs";
import { EsResponseDto, PageDto } from "../domain/dtos";
import { EsQueryDto } from "../domain/dtos/es-query.dto";
import { RequestDto } from "../domain/dtos/request.dto";
import { SearchQueryDto } from "../domain/dtos/search-q.dto";
import { SearchResultDto } from "../domain/dtos/search-result.dto";
import { EsTime } from "../domain/enums/es-time.enum";
import { Order } from "../domain/enums/page-order.enum";
import { PageMeta } from "../domain/interfaces";
import { EsPit } from "../domain/interfaces/es-pit.interface";
import { SearchInfo } from "../domain/interfaces/search-info.interface";
import { SearchService } from "../services/common/search.service";

/**
 * Previous search data storage
 */
class PrevSearch implements SearchInfo {
    /**
     * Constructs an uninitialized object
     */
    constructor() {
        this.pit = undefined;
        this.tiebreaker = undefined;
        this.prevPage = -1;
    }

    /**
     * PIT object of the previous search
     */
    pit: EsPit;

    /**
     * Tiebreaker and sort parameters
     */
    tiebreaker: unknown[];

    /**
     * Number of the previous page
     */
    prevPage: number;

    /**
     * Checks if there was the search before current one
     * @returns true/false, showing whether or not there was another search before
     */
    public isSet(): boolean {
        if (this.pit && this.tiebreaker && this.prevPage !== -1) return true;
        return false;
    }
}

/**
 * Pagination-implementing interceptor
 */
@Injectable()
export class PageInterceptor implements NestInterceptor {
    /**
     * Injects needed dependencies and instantiates the storage object
     * @param httpService 
     * @param searchService 
     */
    constructor(private readonly httpService: HttpService) {
        this.prevSearch = new PrevSearch;
    }

    /**
     * Override of intercept() method, specified in NestInterceptor interface
     * @param context 
     * @param next 
     * @returns Page with content and metadata
     */
    async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<PageDto>> {
        let request: RequestDto = context.switchToHttp().getRequest<RequestDto>();
        const query: SearchQueryDto = request.query;
        let reverse: boolean = false;

        request.es_query = new EsQueryDto();

        request.es_query.query = {
            query_string: {
                query: query.query,
                default_field: 'content',
            }
        };
        request.es_query.sort = [
            { _score: { order: !query?.order ? Order.DESC : query.order } },
            { _shard_doc: 'desc' }
        ];

        if (this.prevSearch.isSet()) {
            request.es_query.pit = this.prevSearch.pit;
            request.es_query.search_after = this.prevSearch.tiebreaker;

            let limit = !query?.limit ? 10 : query.limit;
            request.es_query.size = limit * Math.abs(query.page - this.prevSearch.prevPage);
            
            if (query.page < this.prevSearch.prevPage) {
                request.es_query.sort = [{ _score: { order: 'asc' } }];
                request.es_query.size += limit - 1;
                reverse = true;
            } else if (query.page == this.prevSearch.prevPage) {
                //...
            }
        } else {
            this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
            request.es_query.size = !query?.limit ? 10 : query.limit;
        }

        return next.handle().pipe(
            map((res) => {
                // Setting the page meta-data
                let meta: PageMeta = {
                    total: res.hits.total.value,
                    pagenum: !query?.page ? 1 : query.page,
                    order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC,
                    hasNext: false,
                    hasPrev: false,
                    pagesize: !query?.limit ? 10 : query.limit,
                }; 
                // meta.hasNext = res.hits.hits[meta.pagenum * meta.pagesize] ? true : false;
                // meta.hasPrev = res.hits.hits[(meta.pagenum - 1) * meta.pagesize - 1] ? true: false;

                // Saving the search info
                this.prevSearch.pit.id = res.pit_id;
                this.prevSearch.tiebreaker = res.hits.hits[res.hits.hits.length - 1].sort;
                this.prevSearch.prevPage = query.page;

                let data = res.hits.hits.slice(-meta.pagesize);
                if (reverse) {
                    console.log('REVERSE');
                    this.prevSearch.tiebreaker = data[0].sort;
                    data.reverse();
                    reverse = false;
                }

                // Return the page
                return new PageDto(data, meta);
            })
        );
    }

    /**
     * Elastichsearch server port-number
     */
    private readonly ES_PORT = process.env.ES_PORT;

    /**
     * Info about previously completed search
     */
    private prevSearch: PrevSearch;

    /**
     * Acquires a PIT ID from Elasticsearch, needed for a request
     * @param alive, amount of time in minutes (defaults to 1). If time unit is not specified - defaults to minutes.
     * @returns PIT object <EsPit> containing PIT ID and keep_alive value
     */
     public async getPIT(alive: number, unit: EsTime = EsTime.min): Promise<EsPit> {
        return new Promise((resolve, reject) => {
            try {
                (this.httpService.post<EsPit>(`http://localhost:${this.ES_PORT}/papers/_pit?keep_alive=${alive+unit}`)
                    .pipe(take(1), map(axiosRes => axiosRes.data))
                    .subscribe((res) => {
                        res.keep_alive = alive + unit;
                        resolve(res);
                    }));
            } catch (error) {
                reject(error);
            }
        });
    }

    /**
     * Deletes the PIT specified by provided ID
     * @param pitID, ID of the PIT, that would be deleted
     * @returns true/false, depending on the result of deletion of the PIT
     */
     async deletePIT(pitID: string): Promise<boolean> {
        return new Promise((resolve, reject) => {
            try {
                this.httpService.delete(`http://localhost:${this.ES_PORT}/_pit`, {
                    data: { id: pitID },
                    headers: { 'Content-Type': 'application/json' },
                })
                .pipe(take(1), map(axiosRes => axiosRes.data))
                .subscribe((res) => {
                    resolve(res.succeeded);
                });
            } catch (error) {
                reject(error);
            }
        })
    }
}
/*
public saveInfo(pit: EsPit, tiebreaker: unknown[], page: number) {
        this.pit.id = pit.id;
        this.pit.keep_alive = pit.keep_alive;

        this.tiebreaker = tiebreaker.slice();

        this.prevPage = page;
    }

    public clearInfo() {
        this.pit = undefined;
        this.tiebreaker = undefined;
        this.prevPage = -1;
    }*/

    // getQueryParams(str: string): any {
    //     let parameters: object = {};
    //     let pairs: string[] = str.split(',');
    //     parameters['main'] = pairs[0];
    //     pairs.shift();

    //     if(!pairs || pairs[0] === '') return parameters;

    //     for (const pair of pairs) {
    //         const key: string = pair.substring(0, pair.indexOf('='));
    //         const value: string = pair.substring(pair.indexOf('=') + 1);
    //         parameters[key] = value;
    //     }

    //     return parameters;
    // }


    /**
     * OLD WAY PAGINATION
     *                 // Setting the page data
                // const data = res.hits.slice((meta.pagenum - 1) * meta.pagesize, meta.pagenum * meta.pagesize);
     */


        // if (query.page == 1) {
        //     this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);
        // } else {
        //     if (!this.prevSearch.isSet()) {
        //         this.prevSearch.pit = request.es_query.pit = await this.getPIT(1);

        //         request.es_query.size = query.limit * (query.page - 1);
        //         this.searchService.findByContext(request.es_query).then((res: SearchResultDto) => {
        //             request.es_query.search_after = res.data.hits.hits[res.data.hits.hits.length - 1].sort;
        //         });
        //     } else {
        //         if (query.page == this.prevSearch.prevPage) {
        //             return;
        //         } else {
        //             request.es_query.pit = this.prevSearch.pit;
        //             request.es_query.search_after = this.prevSearch.tiebreaker;
        //             request.es_query.size = (query.page - this.prevSearch.prevPage);
        //         }

        //         // request.es_query.pit = this.prevSearch.pit;
        //         // request.es_query.search_after = this.prevSearch.tiebreaker;
        //     }
        // }

results matching ""

    No results matching ""