flexmonster-mongo-connector
Version:
MongoDB connector for Flexmonster Pivot Table and Charts
346 lines (298 loc) • 16.2 kB
text/typescript
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)
}
}