UNPKG

flexmonster-mongo-connector

Version:

MongoDB connector for Flexmonster Pivot Table and Charts

346 lines (298 loc) 16.2 kB
import { IRequestArgument } from "../IRequestArgument"; import { QueryBuilder } from "../../../query/builder/QueryBuilder"; import { MongoResponseParser } from "../../../parsers/MongoResponseParser"; import { AbstractApiRequest } from "./AbstractApiRequest"; import { APISchema } from "../../../schema/APISchema"; import { IQuery } from "../../../query/IQuery"; import { MongoQueryExecutor } from "../../../query/MongoQueryExecutor"; import { LoggingManager } from "../../../logging/LoggingManager"; import { ArrayDataObject } from "../../../cache/dataObject/impl/ArrayDataObject"; export class AggregationApiRequest extends AbstractApiRequest { private readonly GROUPING_LIMITATION: number = 100000; //the limit for MongoDB 1 time response, to avoid 16 MB document limitation private _isPaginationEnabled: boolean = false; private _templateQuery: IQuery = null; private _isFinished: boolean = false; private _currentTimer: NodeJS.Timeout = undefined; constructor(requestArgument: IRequestArgument) { super(requestArgument); this._loggingTemplate = "aggregations"; this._templateQuery = this._splitedQueries.shift(); this._isPaginationEnabled = this.isPaginationNecessary(this._splitedQueries); if (this._isPaginationEnabled) { this._splitedQueries.sort((first: IQuery, second: IQuery) => { return first.queryStats.expectedNumberOfRecords - second.queryStats.expectedNumberOfRecords; }); } } public async getData(queryBuilder: QueryBuilder, queryExecutor: MongoQueryExecutor): Promise < any > { const dataObject: ArrayDataObject = await this._getData(queryBuilder, queryExecutor); this.loadDataAsync(queryBuilder, queryExecutor, dataObject); return dataObject; } public async _getData(queryBuilder: QueryBuilder, queryExecutor: MongoQueryExecutor): Promise < any > { if (this._isPaginationEnabled) this.applyPaginationStrategie(this._splitedQueries); const preFilteredQueries: IQuery[] = this._isPaginationEnabled ? this.preFilterQueries(this._splitedQueries) : this._splitedQueries; const mongoQuery: any = this.buildMongoQuery(queryBuilder, this._schema, preFilteredQueries); LoggingManager.log(`Getting ${this.loggingTemplate} data`); LoggingManager.log(`Generated pipeline query to MongoDB ${JSON.stringify(mongoQuery)}`); const startDate = new Date(); const queryResultCursor: Promise < any > = this.executeQuery(queryExecutor, mongoQuery); //console.log(">>>>>>promise", new Date().getTime() - startDate.getTime()); return this.parseQueryResult(queryResultCursor, startDate, preFilteredQueries); } private loadDataAsync(queryBuilder: QueryBuilder, queryExecutor: MongoQueryExecutor, dataObject: ArrayDataObject): void { if (this._isPaginationEnabled) { this._isFinished = this.isEverythingLoaded(this._splitedQueries); if (this._currentTimer !== undefined) clearTimeout(this._currentTimer); if (!this._isFinished) { dataObject.isCompleted = false; this._currentTimer = setTimeout(async () => { const data: ArrayDataObject = await this.getData(queryBuilder, queryExecutor); dataObject.push(data); this.loadDataAsync(queryBuilder, queryExecutor, dataObject); }, 100); } else { this._isFinished = true; dataObject.isCompleted = true; } } else { this._isFinished = true; dataObject.isCompleted = true; } return; } public buildMongoQuery(queryBuilder: QueryBuilder, schema: APISchema, preFilteredQueries?: IQuery[]) { if (queryBuilder == null) throw new Error("Illegal argument exception"); if (preFilteredQueries === undefined) preFilteredQueries = this._splitedQueries; const mongoQuery: any = queryBuilder.buildAggregationPipelineFacet(preFilteredQueries, schema, this._templateQuery, this._isPaginationEnabled); //TODO: rename "buildPipeline" //queryBuilder.applyPaging(mongoQuery, {skipNumber: this._currentPageIndex, limitNumber: this.CHUNK_SIZE}); //this._currentPageIndex += this.CHUNK_SIZE; return mongoQuery; } private preFilterQueries(queries: IQuery[]): IQuery[] { if (!this._isPaginationEnabled) return queries; const paginatedQueries: IQuery[] = []; for (let i: number = 0; i < queries.length; i++) { if (!queries[i].queryStats.isAllQueryDataLoaded && queries[i].queryStats.chunkToLoad > 0) { paginatedQueries.push(queries[i]); } } return paginatedQueries; } public parseQueryResult = (queryResult: Promise < any > , date: Date = null, preFilteredQueries?: IQuery[]) => MongoResponseParser.getInstance().parseCalculationsFromCursor(queryResult, preFilteredQueries, this.CHUNK_SIZE, date, this); public updateLoadingStatus(data: any): void { if (data === undefined) return; for (let i: number = 0; i < this._splitedQueries.length; i++) { const dataChunk: any[] = data[this._splitedQueries[i].definition]; if (dataChunk !== undefined) { this._splitedQueries[i].queryStats.loadedNumberOfRecords = dataChunk.length; this.updateQueryStats(this._splitedQueries[i]); } } } public toJSON(response: any, nextPageToken? : string) { const jsonResponse: any = { "aggs": response }; if (nextPageToken != null) jsonResponse["nextPageToken"] = nextPageToken; return jsonResponse; } private isAllQueryDataLoaded(query: IQuery): boolean { return query.queryStats.loadedNumberOfRecords < query.queryStats.chunkToLoad || query.queryStats.sumOfLoadedRecords + query.queryStats.loadedNumberOfRecords >= query.queryStats.expectedNumberOfRecords; } private updateQueryStats(query: IQuery): void { //console.log(">>>>>>query", query); if (this.isAllQueryDataLoaded(query)) { query.queryStats.isAllQueryDataLoaded = true; } else { query.queryStats.sumOfLoadedRecords += query.queryStats.loadedNumberOfRecords; query.queryStats.chunkToLoad = 0; query.queryStats.loadedNumberOfRecords = 0; } } private isEverythingLoaded(quries: IQuery[]): boolean { for (let i: number = 0; i < quries.length; i++) { if (!quries[i].queryStats.isAllQueryDataLoaded) { return false; } } return true; } protected _splitQuery(query: any): any[] { if (query == null) throw new Error("Illegal argument exception"); const aggregationQueries: Map <string, IQuery> = new Map(); const expectedNumberOfRecords: number = this.areSubTotalsAvailable(query) ? this.getExpectedNumberOfRecords(query["aggs"]["by"]["rows"], query["aggs"]["values"]) + this.getExpectedNumberOfRecords(query["aggs"]["by"]["cols"], query["aggs"]["values"]) : 0; aggregationQueries.set("intersection", { definition: "intersection", clientQuery: query, queryStats: { expectedNumberOfRecords: expectedNumberOfRecords, chunkToLoad: 0, isAllQueryDataLoaded: false, loadedNumberOfRecords: 0, sumOfLoadedRecords: 0 } }); //the first query is to obtain intersections this._splitIntersectionQuery(query, aggregationQueries); if (this.areValuesAvailable(query)) { this._splitSubTotalQuery(query, aggregationQueries); this._splitGrandTotalQuery(query, aggregationQueries); } const uniqueAggregationQueries: IQuery[] = Array.from(aggregationQueries.values()); return uniqueAggregationQueries; } private applyPaginationStrategie(queries: IQuery[]): void { //console.log(">>>>> before", JSON.stringify(queries, null, ' ')); let currentSizeChunk: number = 0; const coeficient: number = 0.25; while (currentSizeChunk < this.GROUPING_LIMITATION) { currentSizeChunk = this.balanceGroupingLimitation(queries, currentSizeChunk, coeficient); } } private balanceGroupingLimitation(queries: IQuery[], currentSizeChunk: number, coeficient: number): number { let queryNumber: number = 0; while (currentSizeChunk < this.GROUPING_LIMITATION && queryNumber < queries.length) { if (queries[queryNumber].queryStats.isAllQueryDataLoaded || queries[queryNumber].queryStats.chunkToLoad === queries[queryNumber].queryStats.expectedNumberOfRecords) { queryNumber++; continue; } const query: IQuery = queries[queryNumber]; let expectedChunkToLoad: number = (this.GROUPING_LIMITATION * coeficient > query.queryStats.expectedNumberOfRecords - query.queryStats.sumOfLoadedRecords) ? query.queryStats.expectedNumberOfRecords - query.queryStats.sumOfLoadedRecords : this.GROUPING_LIMITATION * coeficient; if (currentSizeChunk + expectedChunkToLoad <= this.GROUPING_LIMITATION) { query.queryStats.chunkToLoad += expectedChunkToLoad; currentSizeChunk += expectedChunkToLoad; } else { expectedChunkToLoad = this.GROUPING_LIMITATION - currentSizeChunk; currentSizeChunk += expectedChunkToLoad; } queryNumber++; } return currentSizeChunk; } private isPaginationNecessary(queryList: IQuery[]): boolean { let sumOfExpectedRecords: number = 0; for (let i: number = 0; i < queryList.length; i++) { if (queryList[i].queryStats === undefined) { continue; } sumOfExpectedRecords+= queryList[i].queryStats.expectedNumberOfRecords; } //console.log(">>>>>>sum of expected", sumOfExpectedRecords, JSON.stringify(queryList, null, ' ')); return this.GROUPING_LIMITATION < sumOfExpectedRecords; } private _splitIntersectionQuery(query: any, aggregationQueries: Map <string, IQuery> ) { if (typeof query["aggs"] === "undefined" || typeof query["aggs"]["by"] === "undefined" || typeof query["aggs"]["by"]["rows"] === "undefined" || typeof query["aggs"]["by"]["cols"] === "undefined") return; const rowsList: any[] = query["aggs"]["by"]["rows"]; const colsList: any[] = query["aggs"]["by"]["cols"]; let rowsItems = []; let colsItems = []; let intersectionQuery = JSON.parse(JSON.stringify(query)); delete intersectionQuery["aggs"]["by"]["rows"]; delete intersectionQuery["aggs"]["by"]["cols"]; for (let i = 0; i < rowsList.length; i++) { rowsItems.push(rowsList[i]); colsItems = rowsItems.slice(0); for (let j = 0; j < colsList.length; j++) { colsItems = colsItems.slice(0); colsItems.push(colsList[j]); intersectionQuery = JSON.parse(JSON.stringify(intersectionQuery)); intersectionQuery["aggs"]["by"] = { "cols": colsItems }; aggregationQueries.set(JSON.stringify(colsItems), { definition: "intersection" + i + j, clientQuery: intersectionQuery, queryStats: { expectedNumberOfRecords: this.getExpectedNumberOfRecords(colsItems, intersectionQuery["aggs"]["values"]), chunkToLoad: 0, isAllQueryDataLoaded: false, loadedNumberOfRecords: 0, sumOfLoadedRecords: 0 } }) } } return; } private getExpectedNumberOfRecords(colsItems: any[], valuesList: any[]): number { if (colsItems === undefined) return 0; let expectedNumberOfRecords: number = this._schema.fields.get(colsItems[0]["uniqueName"]).fieldStats.distinctMembersNumber; let numberOfMeasures: number = valuesList === undefined ? 1 : valuesList.length; for (let i = 1; i < colsItems.length; i++) { expectedNumberOfRecords*= this._schema.fields.get(colsItems[i]["uniqueName"]).fieldStats.distinctMembersNumber; } expectedNumberOfRecords*= numberOfMeasures; return expectedNumberOfRecords; } private _splitSubTotalQuery(query: any, aggregationQueries: Map <string, IQuery> ): void { if (!this.areSubTotalsAvailable(query)) return; const rowByQuery: any = JSON.parse(JSON.stringify(query)); //a full copy of original query delete rowByQuery["aggs"]["by"]["cols"]; const colsByQuery: any = JSON.parse(JSON.stringify(query)); //a full copy of original query delete colsByQuery["aggs"]["by"]["rows"]; if (rowByQuery["aggs"]["by"]["rows"] != null) { this._generateAllSubtotalsCombinations(rowByQuery, aggregationQueries, "rows", "totalRows"); } if (colsByQuery["aggs"]["by"]["cols"] != null) { this._generateAllSubtotalsCombinations(colsByQuery, aggregationQueries, "cols", "totalColumns"); } return; } private _generateAllSubtotalsCombinations(query: any, aggregationQueries: Map <string, IQuery> , axisName: string, definitionLabel: string): void { const rowsColumnsList: any[] = query["aggs"]["by"][axisName]; let axisItemsList: any[] = []; let subTotalQuery = null; for (let i = 0; i < rowsColumnsList.length; i++) { subTotalQuery = JSON.parse(JSON.stringify(query)); axisItemsList = axisItemsList.slice(0); axisItemsList.push(rowsColumnsList[i]); subTotalQuery["aggs"]["by"][axisName] = axisItemsList; aggregationQueries.set(JSON.stringify(axisItemsList), { definition: definitionLabel + i, clientQuery: subTotalQuery, queryStats: { expectedNumberOfRecords: this.getExpectedNumberOfRecords(axisItemsList, subTotalQuery["aggs"]["values"]), chunkToLoad: 0, isAllQueryDataLoaded: false, loadedNumberOfRecords: 0, sumOfLoadedRecords: 0 } }); } return; } private areSubTotalsAvailable(query: any): boolean { return (query["aggs"] != null && query["aggs"]["by"] != null); } private _splitGrandTotalQuery(query: any, aggregationQueries: Map <string, IQuery> ): void { const grandTotalQuery: any = JSON.parse(JSON.stringify(query)); delete grandTotalQuery["aggs"]["by"]; const definitionLabel: string = "grandTotal"; aggregationQueries.set(definitionLabel, { definition: definitionLabel, clientQuery: grandTotalQuery, queryStats: { expectedNumberOfRecords: grandTotalQuery["aggs"]["values"].length, chunkToLoad: 0, isAllQueryDataLoaded: false, loadedNumberOfRecords: 0, sumOfLoadedRecords: 0 } }); return; } private areValuesAvailable(query: any): boolean { return (query["aggs"] != null && query["aggs"]["values"] != null && query["aggs"]["values"].length != 0) } }