'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: Dashboard * (C) Copyright IBM Corp. 2017, 2019 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. * */ define(['./QueryDefinition', '../../../../lib/@waca/dashboard-common/dist/core/Model', '../../../../features/content/dataQueryExecution/DataQueryResult', '../QueryResultData', './QueryManagerGeoHelper', '../../../../lib/@waca/dashboard-common/dist/query/FacetDataObject', '../QueryResultObject', '../../../../features/content/dataQueryExecution/QueryResults', '../../../../features/content/dataQueryExecution/api/QueryResultsAPI', 'underscore'], function (QueryDefinition, Model, DataQueryResult, DeprecatedQueryResultData, Helper, FacetData, QueryResultObject, QueryResults, QueryResultsAPI, _) { 'use strict'; /** * The QueryManager is responsible for hiding the details of querying for data, * filtering data and providing results to widgets. * * Widgets understand data as rows in 'slot order'. * The QueryManager does things like: * - Allowing widgets to form a query in a field order they specify (corresponding to slots and mappings) * to hide the fact that queries are made in 'dimension first' order * - returning the data back to the widget as rows in the same order (and as indexed columns or ranges) * - filtering, sorting. * - presenting a subset of rows */ var QueryManager = Model.extend({ init: function init(options) { QueryManager.inherited('init', this, arguments); this.dashboardAPI = options.dashboardAPI; this.logger = options.dashboardAPI && options.dashboardAPI.getGlassCoreSvc('.Logger'); this.queryDefinition = new QueryDefinition({ dashboardAPI: this.dashboardAPI, logger: this.logger }); //Latlong this.queryDefinitions = []; // the map used to cache refreshing queries this._refreshQueriesMap = {}; }, getVesion: function getVesion() { return 'endor'; }, clearCachedResponses: function clearCachedResponses() { this._topBottomQueryCache.clear(); }, /** * The following routine is used to send the needed queries as ajax calls * and return their promises in an array. We can wait for all the queries to * complete using this promises array. * @param {Array} queryOptionsArray object holding the querySpec and topBottomQuery * @param {String} sender (Optional) query sender * @return {Promise} */ _sendQueriesAndGetPromises: function _sendQueriesAndGetPromises(queryOptionsArray, sender, useAPI) { var _this = this; var queryPromises = []; this.queryService.updateRequestId(sender); _.each(queryOptionsArray, function (queryOptions) { queryOptions.senderId = _this.visAPI.ownerWidget.id; queryOptions.sender = sender; var sourceIdOrModule = useAPI ? queryOptions.sourceIdOrModule.getSourceId() : queryOptions.sourceIdOrModule; var queryService = useAPI ? _this.dashboardAPI.getFeature('QueryService') : _this.queryService; // Run the main query Spec (if one is defined) if (queryOptions.querySpec) { var queryPromise = useAPI ? queryService.executeQuery(sourceIdOrModule, queryOptions.querySpec, sender) : queryService.runQuery(queryOptions); queryPromises.push({ layerId: queryOptions.layerId, promise: queryPromise }); } // Run the top and bottom query if it exists if (queryOptions.topBottomQuery) { var topbottomQueryPromise = useAPI ? queryService.executeQuery(sourceIdOrModule, queryOptions.topBottomQuery, sender) : _this.queryService.runQuery({ querySpec: queryOptions.topBottomQuery, sourceIdOrModule: queryOptions.sourceIdOrModule }); queryPromises.push({ layerId: queryOptions.layerId, promise: topbottomQueryPromise, type: 'topbottom' }); } }); return queryPromises; }, _handleSuccessfulCompletionOfQueries: function _handleSuccessfulCompletionOfQueries(renderContext, queryPromiseResults, queryOptions, oriRequests) { var _this2 = this; var mainQueryResults; var completedTopBottomQuery; var promiseResults = _.toArray(queryPromiseResults); var oQueryResultObject = new QueryResultObject(renderContext); var sLayerId; _.each(promiseResults, function (result, index) { var bIsMainQuery = false; sLayerId = oriRequests[index].layerId; if (!oriRequests[index].type) { bIsMainQuery = true; mainQueryResults = result.data; } //We assume currently one layer has only one query request if (oriRequests[index + 1] && oriRequests[index + 1].layerId === sLayerId && oriRequests[index + 1].type === 'topbottom') { completedTopBottomQuery = promiseResults[index + 1].data; } var oQueryOption = _.find(queryOptions, function (entry) { return entry.layerId === sLayerId; }); var mainQueryThreshold = oQueryOption.querySpec.limit; var topBottomByAggregateId = {}; if (oQueryOption.topBottomQuery) { _this2._updateTopBottomByAggregateId(topBottomByAggregateId, completedTopBottomQuery); } if (bIsMainQuery) { // Should be able to just pass mapped data items, but due to DSS defect 274285, temporarily pass slots to build result. var slots = _this2.visAPI.ownerWidget.getAPI().getFeature('Visualization').getSlots(); var deprecatedQueryResult = new DeprecatedQueryResultData(mainQueryResults); var queryResult = new DataQueryResult(mainQueryResults, slots); // Make the query spec and raw response available as properties in the query result data object if (queryOptions[index] && queryOptions[index].querySpec) { queryResult.setPropertyValue('QuerySpec.internal', JSON.stringify(queryOptions[index].querySpec)); } // Ideally we should access the raw data from the ajax response as raw string, // but currentlt we don't have access to it because we query using modelling which we should mve away from queryResult.setPropertyValue('RawData.internal', JSON.stringify(mainQueryResults)); var queryResultAPI = queryResult.getAPI(); _this2._callUnprocessedResultDataHandlers && _this2._callUnprocessedResultDataHandlers(mainQueryResults, { layerId: sLayerId }, queryResultAPI); _this2.postProcessCallback(mainQueryResults); oQueryResultObject.addQueryResultObject(sLayerId, deprecatedQueryResult, mainQueryThreshold, { topBottomMappings: topBottomByAggregateId }, queryResultAPI); } }); return oQueryResultObject; }, _handleSucceededQueries: function _handleSucceededQueries(queryPromiseResults, oriRequests) { var queryResults = new QueryResults(); var type = void 0; _.each(_.toArray(queryPromiseResults), function (result, index) { if (!oriRequests[index].type) { type = QueryResultsAPI.QUERY_RESULT_TYPE.MAIN; } else if (oriRequests[index].type === QueryResultsAPI.QUERY_RESULT_TYPE.TOPBOTTOM) { type = QueryResultsAPI.QUERY_RESULT_TYPE.TOPBOTTOM; } queryResults.addResult(result, oriRequests[index].layerId, type); }); return queryResults.getAPI(); }, /** * Update a map of top bottom results mapped by their 'aggregateId'. * An aggregate id is a key which is the minimum needed to uniquely identify an aggregate (ie: its columnId + aggregate type). * eg:{ 3sum: { topResult: 100, bottomResult: -500 }, * 7avg: { topResult: 10000, bottomResult: 600 }, * 7sum: { topResult: 100000, bottomResult: 60 }} * * @param topBottomByAggregateId - The object to be updated which maps one or more AggregateIds to the top and bottom result rows. * @param completedTopBottomQuery - a query object that includes the descriptor of what was executed and the query response. * (as returned from queryCache.getQueryPromise() when it resolves). * @returns the updated topBottomByAgggregateId map */ _updateTopBottomByAggregateId: function _updateTopBottomByAggregateId(topBottomByAggregateId, completedTopBottomQuery) { if (!completedTopBottomQuery || !completedTopBottomQuery.dataItems || completedTopBottomQuery.dataItems.length < 1) { return topBottomByAggregateId; } //min and max are stored in result pairs 2 per corresponding field (eg: result for field1, field2 is min1 max1 min2 max2) var data = completedTopBottomQuery.data[0].pt; var dataItems = completedTopBottomQuery.dataItems; var numDataItems = dataItems.length; for (var dataItemIndex = 0; dataItemIndex < numDataItems; dataItemIndex += 2) { // Need the min, max and aggregate type for the data item. var dataItemMin = dataItems[dataItemIndex]; var dataItemAggType = dataItemMin.itemClass.h[0].aggregate; var areDataItemsAggregates = !!dataItemAggType; // Only do top and bottom for aggregates if (areDataItemsAggregates) { var dataItemMinValue = data[dataItemIndex].v; // First data item is the min var dataItemMaxValue = data[dataItemIndex + 1].v; // Second data item is the max var facetDataItemMin = new FacetData({ u: dataItemMinValue }); // Create Min and Max facet data items for legacy purposes. var facetDataItemMax = new FacetData({ u: dataItemMaxValue }); // TODO This can be cleaned up further when the legacy query code is updated. var columnName = dataItemMin.itemClass.h[0].u.split('_MIN')[0]; this._updateTopBottom(topBottomByAggregateId, columnName + dataItemAggType, facetDataItemMin, facetDataItemMax); } } return topBottomByAggregateId; }, /** * If the passed in arguments for bottom/top are less/greater than the current top/bottom values for this aggregate, widen the range. * * @param topBottomByAggregateId a map of aggregates keyed by their id (which is columnId + aggregation type). * @param aggregateId - the aggregateId to update the top and bottom for. * @param bottom - the bottom value to process * @param top - the top value to process */ _updateTopBottom: function _updateTopBottom(topBottomByAggregateId, aggregateId, bottom, top) { if (!topBottomByAggregateId[aggregateId]) { topBottomByAggregateId[aggregateId] = { bottomResult: bottom, topResult: top }; return; } if (bottom.useValue < topBottomByAggregateId[aggregateId].bottomResult.useValue) { topBottomByAggregateId[aggregateId].bottomResult = bottom; } if (top.useValue > topBottomByAggregateId[aggregateId].topResult.useValue) { topBottomByAggregateId[aggregateId].topResult = top; } }, /** * Run the query and, when ready, process the queryResponses * queryResponses is an array of one or more query responses. The format is different for 'main queries' and 'minmax' queries.. * [ * //index 1: main query in queryResponses array is the result * {results: [['data', 'row' 1],['data', 'row' 2]], hasNext: false, hasPrev: false }, * //index 2, 3 form: minmax query. * {queryDescriptor: {}, response: [['min1', 'max1', 'min2', 'max2'...]] }, * {minmaxresponse2} * ] * @param renderContext - information about the current render (including the deferred to be resolved when the query is complete) * @param queryOptionsArray - any options to be put on the query (such as whether geoJson should be processed, a rowLimit should be imposed etc). * @param querySender (optional) - unique string identifier of the query sender (defaults to 'QueryManager') * @param isAuxQuery true if is an auxiliary query * @returns a promise. */ whenQueryResultsReady: function whenQueryResultsReady() { var renderContext = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var queryOptionsArray = arguments[1]; var querySender = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'QueryManager'; var _this3 = this; var useAPI = arguments[3]; var isAuxQuery = arguments[4]; var aOrigPromises = renderContext.noDataQuery ? this._getEmptyData(queryOptionsArray) : this._sendQueriesAndGetPromises(queryOptionsArray, querySender, useAPI); var queryPromises = _.pluck(aOrigPromises, 'promise'); return Promise.all(queryPromises).then(function (results) { var ETags = []; _.each(results, function (result, index) { if (!aOrigPromises[index].type && !useAPI) { ETags.push(result.ETag); //Main Query } }); // Check the last modified for the query data change. if (renderContext.extraInfo && renderContext.extraInfo.sender === 'realtimeRefresh') { // Use querySpec as the key to check if the query is already executed. var bAtLeastOneQueryChanged = false; _.each(queryOptionsArray, function (queryOptions, index) { var url = JSON.stringify(queryOptions.querySpec); if (!_this3._refreshQueriesMap[url] || _this3._refreshQueriesMap[url] !== ETags[index]) { bAtLeastOneQueryChanged = true; _this3._refreshQueriesMap[url] = ETags[index]; } }); if (!bAtLeastOneQueryChanged) { // Returns empty result when the query data did not change for real time refreshing. renderContext.sameQueryData = true; } } var result = void 0; if (!useAPI) { result = _this3._handleSuccessfulCompletionOfQueries(renderContext, results, queryOptionsArray, aOrigPromises); } else { result = _this3._handleSucceededQueries(results, aOrigPromises); } if (!isAuxQuery) { // Cache result if is not summary queries. _this3._queryResults = result; } return result; }).catch(function (jqXHR) { return Promise.reject(jqXHR); }); }, _getEmptyData: function _getEmptyData(queryOptions) { var _this4 = this; return _.map(queryOptions, function (option, index) { var dataItems = _this4.queryDefinitions[index].getQueryDataItems(); return { layerId: option.layerId, promise: Promise.resolve({ data: { dataItems: _.map(dataItems, function (dataItem) { var emptyDataItem = { itemClass: { h: [{ u: dataItem.itemId, d: _this4.visAPI.getMetadataColumn(dataItem.itemId).getLabel() }], id: dataItem.id } }; // ordinal dataitems requires an aggregation // while categorical dataitems requires an empty items array if (dataItem.aggregate) { emptyDataItem.itemClass.h[0].aggregate = dataItem.aggregate; } else { emptyDataItem.items = []; } return emptyDataItem; }), data: [] } }) }; }); }, _shouldRejectGeoQueryFail: function _shouldRejectGeoQueryFail(geoResponse) { if (geoResponse && geoResponse.status === 'Fail') { if (!geoResponse.mapboxData) { return true; } if (geoResponse.mapboxData.length === 0) { return true; } return false; } return false; }, /* * load the field values for the field in use */ _getResultFieldValuesAtFieldIndex: function _getResultFieldValuesAtFieldIndex(queryResults, fieldIndex) { var values = []; var oDataItem; for (var i = 0; i < queryResults.getDataRows().length; i++) { oDataItem = queryResults.getDataItemByIndex(i, fieldIndex); values.push(oDataItem.displayValue); } return values; }, getQueryResults: function getQueryResults() { return this._queryResults; }, getQueryDefinition: function getQueryDefinition(layerId) { if (!layerId) { if (!this.queryDefinitions[0]) { this.queryDefinitions.push(new QueryDefinition({ dashboardAPI: this.dashboardAPI, logger: this.logger })); return this.queryDefinitions[0]; } } var oResult = _.find(this.queryDefinitions, function (def) { return def.layerId === layerId; }); if (!oResult) { oResult = this.queryDefinitions.push(new QueryDefinition({ layerId: layerId, dashboardAPI: this.dashboardAPI, logger: this.logger })); } return oResult; }, getQueryDefinitions: function getQueryDefinitions() { return this.queryDefinitions; } }); return QueryManager; }); //# sourceMappingURL=QueryManager.js.map