// (C) 2007-2020 GoodData Corporation
import md5 from "md5";
import invariant from "invariant";
import cloneDeep from "lodash/cloneDeep";
import compact from "lodash/compact";
import filter from "lodash/filter";
import first from "lodash/first";
import find from "lodash/find";
import map from "lodash/map";
import merge from "lodash/merge";
import every from "lodash/every";
import get from "lodash/get";
import isEmpty from "lodash/isEmpty";
import negate from "lodash/negate";
import partial from "lodash/partial";
import flatten from "lodash/flatten";
import set from "lodash/set";
import { Rules } from "../utils/rules";
import { sortDefinitions } from "../utils/definitions";
import { getMissingUrisInAttributesMap } from "../utils/attributesMapLoader";
import {
getAttributes,
getAttributesDisplayForms,
getDefinition,
getMeasureFilters,
getMeasures,
isAttributeMeasureFilter,
} from "../utils/visualizationObjectHelper";
import { IMeasure } from "../interfaces";
import { XhrModule } from "../xhr";
const notEmpty = negate(isEmpty);
function findHeaderForMappingFn(mapping: any, header: any) {
return (
(mapping.element === header.id || mapping.element === header.uri) && header.measureIndex === undefined
);
}
function wrapMeasureIndexesFromMappings(metricMappings: any[], headers: any[]) {
if (metricMappings) {
metricMappings.forEach(mapping => {
const header = find(headers, partial(findHeaderForMappingFn, mapping));
if (header) {
header.measureIndex = mapping.measureIndex;
header.isPoP = mapping.isPoP;
}
});
}
return headers;
}
const emptyResult = {
extendedTabularDataResult: {
values: [],
warnings: [],
},
};
const MAX_TITLE_LENGTH = 1000;
function getMetricTitle(suffix: string, title: string) {
const maxLength = MAX_TITLE_LENGTH - suffix.length;
if (title && title.length > maxLength) {
if (title[title.length - 1] === ")") {
return `${title.substring(0, maxLength - 2)}…)${suffix}`;
}
return `${title.substring(0, maxLength - 1)}…${suffix}`;
}
return `${title}${suffix}`;
}
const getBaseMetricTitle = partial(getMetricTitle, "");
const CONTRIBUTION_METRIC_FORMAT = "#,##0.00%";
function getPoPDefinition(measure: IMeasure) {
return get(measure, ["definition", "popMeasureDefinition"], {});
}
function getAggregation(measure: IMeasure) {
return get(getDefinition(measure), "aggregation", "").toLowerCase();
}
function isEmptyFilter(metricFilter: any) {
if (get(metricFilter, "positiveAttributeFilter")) {
return isEmpty(get(metricFilter, ["positiveAttributeFilter", "in"]));
}
if (get(metricFilter, "negativeAttributeFilter")) {
return isEmpty(get(metricFilter, ["negativeAttributeFilter", "notIn"]));
}
if (get(metricFilter, "absoluteDateFilter")) {
return (
get(metricFilter, ["absoluteDateFilter", "from"]) === undefined &&
get(metricFilter, ["absoluteDateFilter", "to"]) === undefined
);
}
return (
get(metricFilter, ["relativeDateFilter", "from"]) === undefined &&
get(metricFilter, ["relativeDateFilter", "to"]) === undefined
);
}
function allFiltersEmpty(item: any) {
return every(map(getMeasureFilters(item), f => isEmptyFilter(f)));
}
function isDerived(measure: any) {
const aggregation = getAggregation(measure);
return aggregation !== "" || !allFiltersEmpty(measure);
}
function getAttrTypeFromMap(dfUri: string, attributesMap: any) {
return get(get(attributesMap, [dfUri], {}), ["attribute", "content", "type"]);
}
function getAttrUriFromMap(dfUri: string, attributesMap: any) {
return get(get(attributesMap, [dfUri], {}), ["attribute", "meta", "uri"]);
}
function isAttrFilterNegative(attributeFilter: any) {
return get(attributeFilter, "negativeAttributeFilter") !== undefined;
}
function getAttrFilterElements(attributeFilter: any) {
const isNegative = isAttrFilterNegative(attributeFilter);
const pathToElements = isNegative
? ["negativeAttributeFilter", "notIn"]
: ["positiveAttributeFilter", "in"];
return get(attributeFilter, pathToElements, []);
}
function getAttrFilterExpression(measureFilter: any, attributesMap: any) {
const isNegative = get(measureFilter, "negativeAttributeFilter", false);
const detailPath = isNegative ? "negativeAttributeFilter" : "positiveAttributeFilter";
const attributeUri = getAttrUriFromMap(
get(measureFilter, [detailPath, "displayForm", "uri"]),
attributesMap,
);
const elements = getAttrFilterElements(measureFilter);
if (isEmpty(elements)) {
return null;
}
const elementsForQuery = map(elements, e => `[${e}]`);
const negative = isNegative ? "NOT " : "";
return `[${attributeUri}] ${negative}IN (${elementsForQuery.join(",")})`;
}
function getDateFilterExpression() {
// measure date filter was never supported
return "";
}
function getFilterExpression(attributesMap: any, measureFilter: any) {
if (isAttributeMeasureFilter(measureFilter)) {
return getAttrFilterExpression(measureFilter, attributesMap);
}
return getDateFilterExpression();
}
function getGeneratedMetricExpression(item: any, attributesMap: any) {
const aggregation = getAggregation(item).toUpperCase();
const objectUri = get(getDefinition(item), "item.uri");
const where = filter(map(getMeasureFilters(item), partial(getFilterExpression, attributesMap)), e => !!e);
return `SELECT ${aggregation ? `${aggregation}([${objectUri}])` : `[${objectUri}]`}${
notEmpty(where) ? ` WHERE ${where.join(" AND ")}` : ""
}`;
}
function getPercentMetricExpression(category: any, attributesMap: any, measure: any) {
let metricExpressionWithoutFilters = `SELECT [${get(getDefinition(measure), "item.uri")}]`;
if (isDerived(measure)) {
metricExpressionWithoutFilters = getGeneratedMetricExpression(
set(cloneDeep(measure), ["definition", "measureDefinition", "filters"], []),
attributesMap,
);
}
const attributeUri = getAttrUriFromMap(get(category, "displayForm.uri"), attributesMap);
const whereFilters = filter(
map(getMeasureFilters(measure), partial(getFilterExpression, attributesMap)),
e => !!e,
);
const whereExpression = notEmpty(whereFilters) ? ` WHERE ${whereFilters.join(" AND ")}` : "";
// tslint:disable-next-line:max-line-length
return `SELECT (${metricExpressionWithoutFilters}${whereExpression}) / (${metricExpressionWithoutFilters} BY ALL [${attributeUri}]${whereExpression})`;
}
function getPoPExpression(attributeUri: string, metricExpression: string) {
return `SELECT ${metricExpression} FOR PREVIOUS ([${attributeUri}])`;
}
function getGeneratedMetricHash(title: string, format: string, expression: string) {
return md5(`${expression}#${title}#${format}`);
}
function getMeasureType(measure: any) {
const aggregation = getAggregation(measure);
if (aggregation === "") {
return "metric";
} else if (aggregation === "count") {
return "attribute";
}
return "fact";
}
function getGeneratedMetricIdentifier(
item: any,
aggregation: string,
expressionCreator: (item: any, attributesMap: any) => string,
hasher: any,
attributesMap: any,
) {
const [, , , prjId, , id] = get(getDefinition(item), "item.uri", "").split("/");
const identifier = `${prjId}_${id}`;
const hash = hasher(expressionCreator(item, attributesMap));
const hasNoFilters = isEmpty(getMeasureFilters(item));
const type = getMeasureType(item);
const prefix = hasNoFilters || allFiltersEmpty(item) ? "" : "_filtered";
return `${type}_${identifier}.generated.${hash}${prefix}_${aggregation}`;
}
function isDateAttribute(attribute: any, attributesMap = {}) {
return getAttrTypeFromMap(get(attribute, ["displayForm", "uri"]), attributesMap) !== undefined;
}
function getMeasureSorting(measure?: any, mdObj?: any) {
const sorting = get(mdObj, ["properties", "sortItems"], []);
const matchedSorting = sorting.find((sortItem: any) => {
const measureSortItem = get(sortItem, ["measureSortItem"]);
if (measureSortItem) {
// only one item now, we support only 2d data
const identifier = get(measureSortItem, [
"locators",
0,
"measureLocatorItem",
"measureIdentifier",
]);
return identifier === get(measure, "localIdentifier");
}
return false;
});
if (matchedSorting) {
return get(matchedSorting, ["measureSortItem", "direction"], null);
}
return null;
}
function getCategorySorting(category: any, mdObj: any) {
const sorting = get(mdObj, ["properties", "sortItems"], []);
const matchedSorting = sorting.find((sortItem: any) => {
const attributeSortItem = get(sortItem, ["attributeSortItem"]);
if (attributeSortItem) {
const identifier = get(attributeSortItem, ["attributeIdentifier"]);
return identifier === get(category, "localIdentifier");
}
return false;
});
if (matchedSorting) {
return get(matchedSorting, ["attributeSortItem", "direction"], null);
}
return null;
}
const createPureMetric = (measure: any, mdObj: any, measureIndex: number) => ({
element: get(measure, ["definition", "measureDefinition", "item", "uri"]),
sort: getMeasureSorting(measure, mdObj),
meta: { measureIndex },
});
function createDerivedMetric(measure: any, mdObj: any, measureIndex: number, attributesMap: any) {
const { format } = measure;
const sort = getMeasureSorting(measure, mdObj);
const title = getBaseMetricTitle(measure.title);
const hasher = partial(getGeneratedMetricHash, title, format);
const aggregation = getAggregation(measure);
const element = getGeneratedMetricIdentifier(
measure,
aggregation.length ? aggregation : "base",
getGeneratedMetricExpression,
hasher,
attributesMap,
);
const definition = {
metricDefinition: {
identifier: element,
expression: getGeneratedMetricExpression(measure, attributesMap),
title,
format,
},
};
return {
element,
definition,
sort,
meta: {
measureIndex,
},
};
}
function createContributionMetric(measure: any, mdObj: any, measureIndex: number, attributesMap: any) {
const attribute = first(getAttributes(mdObj));
const getMetricExpression = partial(getPercentMetricExpression, attribute, attributesMap);
const title = getBaseMetricTitle(get(measure, "title"));
const hasher = partial(getGeneratedMetricHash, title, CONTRIBUTION_METRIC_FORMAT);
const identifier = getGeneratedMetricIdentifier(
measure,
"percent",
getMetricExpression,
hasher,
attributesMap,
);
return {
element: identifier,
definition: {
metricDefinition: {
identifier,
expression: getMetricExpression(measure),
title,
format: CONTRIBUTION_METRIC_FORMAT,
},
},
sort: getMeasureSorting(measure, mdObj),
meta: {
measureIndex,
},
};
}
function getOriginalMeasureForPoP(popMeasure: any, mdObj: any) {
return getMeasures(mdObj).find(
(measure: any) =>
get(measure, "localIdentifier") === get(getPoPDefinition(popMeasure), ["measureIdentifier"]),
);
}
function createPoPMetric(popMeasure: any, mdObj: any, measureIndex: number, attributesMap: any) {
const title = getBaseMetricTitle(get(popMeasure, "title"));
const format = get(popMeasure, "format");
const hasher = partial(getGeneratedMetricHash, title, format);
const attributeUri = get(popMeasure, "definition.popMeasureDefinition.popAttribute.uri");
const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj);
const originalMeasureExpression = `[${get(getDefinition(originalMeasure), ["item", "uri"])}]`;
let metricExpression = getPoPExpression(attributeUri, originalMeasureExpression);
if (isDerived(originalMeasure)) {
const generated = createDerivedMetric(originalMeasure, mdObj, measureIndex, attributesMap);
const generatedMeasureExpression = `(${get(generated, [
"definition",
"metricDefinition",
"expression",
])})`;
metricExpression = getPoPExpression(attributeUri, generatedMeasureExpression);
}
const identifier = getGeneratedMetricIdentifier(
originalMeasure,
"pop",
() => metricExpression,
hasher,
attributesMap,
);
return {
element: identifier,
definition: {
metricDefinition: {
identifier,
expression: metricExpression,
title,
format,
},
},
sort: getMeasureSorting(popMeasure, mdObj),
meta: {
measureIndex,
isPoP: true,
},
};
}
function createContributionPoPMetric(popMeasure: any, mdObj: any, measureIndex: number, attributesMap: any) {
const attributeUri = get(popMeasure, ["definition", "popMeasureDefinition", "popAttribute", "uri"]);
const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj);
const generated = createContributionMetric(originalMeasure, mdObj, measureIndex, attributesMap);
const title = getBaseMetricTitle(get(popMeasure, "title"));
const format = CONTRIBUTION_METRIC_FORMAT;
const hasher = partial(getGeneratedMetricHash, title, format);
const generatedMeasureExpression = `(${get(generated, [
"definition",
"metricDefinition",
"expression",
])})`;
const metricExpression = getPoPExpression(attributeUri, generatedMeasureExpression);
const identifier = getGeneratedMetricIdentifier(
originalMeasure,
"pop",
() => metricExpression,
hasher,
attributesMap,
);
return {
element: identifier,
definition: {
metricDefinition: {
identifier,
expression: metricExpression,
title,
format,
},
},
sort: getMeasureSorting(),
meta: {
measureIndex,
isPoP: true,
},
};
}
function categoryToElement(attributesMap: any, mdObj: any, category: any) {
const element = getAttrUriFromMap(get(category, ["displayForm", "uri"]), attributesMap);
return {
element,
sort: getCategorySorting(category, mdObj),
};
}
function isPoP({ definition }: any) {
return get(definition, "popMeasureDefinition") !== undefined;
}
function isContribution({ definition }: any) {
return get(definition, ["measureDefinition", "computeRatio"]);
}
function isPoPContribution(popMeasure: any, mdObj: any) {
if (isPoP(popMeasure)) {
const originalMeasure = getOriginalMeasureForPoP(popMeasure, mdObj);
return isContribution(originalMeasure);
}
return false;
}
function isCalculatedMeasure({ definition }: any) {
return get(definition, ["measureDefinition", "aggregation"]) === undefined;
}
const rules = new Rules();
rules.addRule([isPoPContribution], createContributionPoPMetric);
rules.addRule([isPoP], createPoPMetric);
rules.addRule([isContribution], createContributionMetric);
rules.addRule([isDerived], createDerivedMetric);
rules.addRule([isCalculatedMeasure], createPureMetric);
function getMetricFactory(measure: any, mdObj: any) {
const factory = rules.match(measure, mdObj);
invariant(factory, `Unknown factory for: ${measure}`);
return factory;
}
function getExecutionDefinitionsAndColumns(mdObj: any, options: any, attributesMap: any) {
const measures = getMeasures(mdObj);
let attributes = getAttributes(mdObj);
const metrics = flatten(
map(measures, (measure, index) =>
getMetricFactory(measure, mdObj)(measure, mdObj, index, attributesMap),
),
);
if (options.removeDateItems) {
attributes = filter(attributes, attribute => !isDateAttribute(attribute, attributesMap));
}
attributes = map(attributes, partial(categoryToElement, attributesMap, mdObj));
const columns = compact(map([...attributes, ...metrics], "element"));
return {
columns,
definitions: sortDefinitions(compact(map(metrics, "definition"))),
};
}
/**
* Module for execution on experimental execution resource
*
* @class execution
* @module execution
* @deprecated The module is in maintenance mode only (just the the compilation issues are being fixed when
* referenced utilities and interfaces are being changed) and is not being extended when AFM executor
* have new functionality added.
*/
export class ExperimentalExecutionsModule {
constructor(private xhr: XhrModule, private loadAttributesMap: any) {}
/**
* For the given projectId it returns table structure with the given
* elements in column headers.
*
* @method getData
* @param {String} projectId - GD project identifier
* @param {Array} columns - An array of attribute or metric identifiers.
* @param {Object} executionConfiguration - Execution configuration - can contain for example
* property "where" containing query-like filters
* property "orderBy" contains array of sorted properties to order in form
* [{column: 'identifier', direction: 'asc|desc'}]
* @param {Object} settings - Supports additional settings accepted by the underlying
* xhr.ajax() calls
*
* @return {Object} Structure with `headers` and `rawData` keys filled with values from execution.
*/
public getData(projectId: string, columns: any[], executionConfiguration: any = {}, settings: any = {}) {
if (process.env.NODE_ENV !== "test") {
// tslint:disable-next-line:no-console
console.warn(
"ExperimentalExecutionsModule is deprecated and is no longer being maintained. " +
"Please migrate to the ExecuteAfmModule.",
);
}
const executedReport: any = {
isLoaded: false,
};
// Create request and result structures
const request: any = {
execution: { columns },
};
// enrich configuration with supported properties such as
// where clause with query-like filters
["where", "orderBy", "definitions"].forEach(property => {
if (executionConfiguration[property]) {
request.execution[property] = executionConfiguration[property];
}
});
// Execute request
return this.xhr
.post(`/gdc/internal/projects/${projectId}/experimental/executions`, {
...settings,
body: JSON.stringify(request),
})
.then(r => r.getData())
.then(response => {
executedReport.headers = wrapMeasureIndexesFromMappings(
get(executionConfiguration, "metricMappings"),
get(response, ["executionResult", "headers"], []),
);
// Start polling on url returned in the executionResult for tabularData
return this.loadExtendedDataResults(
response.executionResult.extendedTabularDataResult,
settings,
);
})
.then((r: any) => {
const { result, status } = r;
return {
...executedReport,
rawData: get(result, "extendedTabularDataResult.values", []),
warnings: get(result, "extendedTabularDataResult.warnings", []),
isLoaded: true,
isEmpty: status === 204,
};
});
}
public mdToExecutionDefinitionsAndColumns(projectId: string, mdObj: any, options = {}) {
const allDfUris = getAttributesDisplayForms(mdObj);
const attributesMapPromise = this.getAttributesMap(options, allDfUris, projectId);
return attributesMapPromise.then((attributesMap: any) => {
return getExecutionDefinitionsAndColumns(mdObj, options, attributesMap);
});
}
private getAttributesMap(options: any, displayFormUris: string[], projectId: string) {
const attributesMap = get(options, "attributesMap", {});
const missingUris = getMissingUrisInAttributesMap(displayFormUris, attributesMap);
return this.loadAttributesMap(projectId, missingUris).then((result: any) => {
return {
...attributesMap,
...result,
};
});
}
private loadExtendedDataResults(uri: string, settings: any, prevResult = emptyResult) {
return new Promise((resolve, reject) => {
this.xhr
.ajax(uri, settings)
.then(r => {
const { response } = r;
if (response.status === 204) {
return {
status: response.status,
result: "",
};
}
return {
status: response.status,
result: r.getData(),
};
})
.then(({ status, result }) => {
const values = [
...get(prevResult, "extendedTabularDataResult.values", []),
...get(result, "extendedTabularDataResult.values", []),
];
const warnings = [
...get(prevResult, "extendedTabularDataResult.warnings", []),
...get(result, "extendedTabularDataResult.warnings", []),
];
const updatedResult = merge({}, prevResult, {
extendedTabularDataResult: {
values,
warnings,
},
});
const nextUri = get(result, "extendedTabularDataResult.paging.next");
if (nextUri) {
resolve(this.loadExtendedDataResults(nextUri, settings, updatedResult));
} else {
resolve({ status, result: updatedResult });
}
}, reject);
});
}
}