MDX API

API to Manipulate MDX Queries

The MdxApi library contains all the functions to extract information from an MDX query string or update it.

Design #

Usage #

Parsing #

In order to read or modify an MDX statement, the statement string must be first parsed. The parsing consists of converting the Mdx into a tree of JavaScript objects. If the input MDX string is not syntactically valid, trying to parse it will raise a parsing error and the MDX API will not be able to process this string.

Each node of the tree produced by the parsing is called a MdxNode. This includes the root node, so the result of the parsing of an MDX string is a MdxNode. There are multiple types of nodes depending for each part of the MDX tree, but the MDX API hides this complexity from you.

Warning

We ask that you not directly mutate MdxNode objects. This constraint allows us to cache MdxNodes produced by the MDX API, speeding up later processing of similar MDX.

If you want to modify an MdxNode, create a clone of the object first. Some third-party libraries provide functions to make this more convenient, such as Lodash’s _.cloneDeep/_.clone and immutability-helper’s update.

How To Transform a String #

Since we often need to make consecutive calls to different MDX API methods and since parsing is an expensive operation, all the methods of the MDX API reading or updating an MDX statement take as input a MdxNode and return a MdxNode.

Additionally, most of MDX API functions require the server Discovery information along the statement, to be able to know which part of the MDX query corresponds to a hierarchy or level, what are the default members and so on… This is why most of the functions will take as input an object of type StatementAndDiscovery and return also a StatementAndDiscovery so that calls can be chained.

Each modification of an MDX string requires then to first parse it, then call the wanted MDX API method with a StatementAndDiscovery, then retrieve the statement part of the resulting StatementAndDiscovery and convert it back to a string. This is done automatically by the MdxTransformApi.

Filters #

The MDX filtering API is very extensive as this the part of the API that is more likely to be used in projects.

ActiveFilter #

The main object used to manipulate the filters of an MDX statement is called ActiveFilter. Active here means that this filter is currently in use in a statement, as opposed to the class Filter which describes a type of filter, outside of any statement context.

The function mdxApi.filters.selectFilters retrieves the list of active filters from an MDX.

Filter selectors #

Filter selectors are objects that can be used to filter out some active filters from the list of filters returned in selectFilters. They are Plugins because their ultimate goal is to store them in filter bookmarks so that one can display in a dashboard a Widget filtering a given hierarchy only. Selectors represent the persisted information about a filter, so that one can find again a filter even after most of the MDX changed.

The list of important core implementations is:

Name Parameters Role Usage
AllSelector None Keeps all active filters This is the selector the Wizard uses by default
UserSelector None Filters out server filters With this selector one sees the filters contained in the MDX
KeySelector key Keeps only filters of a given type If one wants to list only TopCount filters for instance
HierarchySelector hierarchy Keeps only filters filtering a given hierarchy If one wants to list only filters on Measures for instance
HierarchyKeyIndexSelector hierarchy, key, index Keeps only one active filter with the given parameters Track an active filter even if the MDX is heavily modified

ActiveFilter interface #

The result of an MDX query is filtered either:

  • by filters in the statement itself, which are called user filters because the user wrote the statement.
  • by members of slicing hierarchies not expressed on the main axes of the select statement. These filters are called server filters because they are applied by the server implicitly. The method remove is not available on those type of filter.

Both filters share the same type AugmentedActiveFilter.

Filters plugins #

Each filter plugin describe a type of filter that can be used in a query:

  • how to recognize their type of filters in the MDX
  • how to change the MDX when an active filter is being created with this key
  • how to render the UI to edit the filter in the Edit filter popup
  • how to render the UI to see/edit the filter in the Wizard

Each filter plugin is responsible of choosing the value that will be contained in the created ActiveFilter based on the MDX. This value is a JavaScript Object that has a different shape depending on the plugin used.

The list of core filters is:

Name Description Example of handled MDX Value shape
Explicit List selected members explicitly {[D1].[H1].[AllMember].[M1], [D1].[H1].[AllMember].[M1]} The array of selected members: ['[AllMember].[M1]', '[AllMember].[M2]']
TopCount TopCount, BottomPercent etc… TopCount(Filter(*, NOT IsEmpty(measure)), n, measure) {measure: 'pnl.SUM', order: 'TOP', mode: 'COUNT', amount: 10, byParent: false, levelDepth: 1}
NumericBinary Compare measure with value Filter(...[L1].Members, [Measures].[pnl.SUM] > 0) {measure: 'pnl.SUM', level: 'L1', constant: 0, op: '>', yoda: false}
NumericBetween Compare measure with two values Filter(...[L1].Members, [Measures].[a] > 0 AND [Measures].[a] < 10 {measure: 'a', level: 'L1', low: 0, high: 10, inside: true}
StringBinary Compare member label with String Filter(...[L1].Members, [D1].[H1].CurrentMember.MEMBER_CAPTION > "a" {level: 'L1', constant: 0, op: '>', yoda: false}
StringBetween Compare member label with two Strings Combine NumericBetween with StringBinary Combine also the values
Substring Check if member label contains or not a String Filter(...[L].Members, Left([D].[H].CurrentMember.member_caption length) <> str) {level: 'L1', mode: Mode.Left, inverted: true}
Emptiness Check if measure is empty or not Filter(...[L].Members, NOT IsEmpty([Measures].[pnl.SUM])) {measure: 'pnl.SUM', level: 'L', empty: false}

Basic filter manipulation examples #

activeui.jsscript.jsbody.htmlstyle.css
import ActiveUI from 'activeui';

const activeUI = ActiveUI.initialize();

activeUI.queries.serversPool.addActivePivotServer({url: 'http://url/of/your/activepivot/server'});

export default activeUI;

import activeUI from './activeui';

const mdxEl = document.getElementById('before');
const positionEl = document.getElementById('position');
const keyEl = document.getElementById('key');
const valueEl = document.getElementById('value');
const hierarchyEl = document.getElementById('hierarchy');
const afterEl = document.getElementById('after');

const activePivotServer = activeUI.queries.serversPool.getActivePivotServer('http://url/of/your/activepivot/server');

// Write in 'after' the query of 'before' with an added filter.
const getMdxAfter = (mdxBefore, position, key, hierarchy, value) =>
  activePivotServer
    .getDiscovery()
    .get()
    .then(discovery => {
      try {
        return activeUI.mdx.transform(mdxBefore, discovery, snd =>
          activeUI.mdx.filters.addFilter(snd, position, key, hierarchy, value)
        );
      } catch (e) {
        return 'Invalid MDX';
      }
    });

const addFilter = () => {
  const mdxBefore = mdxEl.value;
  const position = positionEl.value;
  const key = keyEl.value;
  const hierarchy = hierarchyEl.value;
  let value;
  try {
    value = JSON.parse(valueEl.value);
  } catch (e) {
    afterEl.innerHTML = 'Invalid value';
    return;
  }
  getMdxAfter(mdxBefore, position, key, hierarchy, value).then(mdx => (afterEl.innerHTML = mdx));
};

// Listen to changes in input
mdxEl.onkeyup = addFilter;
positionEl.onkeyup = addFilter;
keyEl.onkeyup = addFilter;
valueEl.onkeyup = addFilter;
hierarchyEl.onkeyup = addFilter;
// Run once on the initial MDX
addFilter();

<h1>Input</h1>
<label for="before" class="textarealabel">MDX</label><textarea id="before" cols="50" rows="10">
SELECT [Measures].[contributors.COUNT] ON COLUMNS FROM [EquityDerivativesCube]
</textarea><br/>
<label for="key">Key</label><input id="key" type="text" value="topcount">
<label for="position">Position</label><input id="position" type="text" value="0" style="width:2em"/><br/>
<label for="hierarchy">Hierarchy</label><input id="hierarchy" type="text" value="[Underlyings].[Products]"/><br/>
<label for="value" class="textarealabel">Value</label><textarea cols="50" rows="8" id="value">
{
    "measure": "contributors.COUNT",
    "order": "TOP",
    "mode": "COUNT",
    "amount": 10,
    "byParent": false,
    "levelDepth": 1
}
</textarea><br/>
<h1>Output</h1>
<pre id="after"></pre>

#after {
  width: 300px;
  /* http://stackoverflow.com/questions/248011/how-do-i-wrap-text-in-a-pre-tag */
  white-space: pre-wrap; /* CSS 3 */
  white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
  white-space: -pre-wrap; /* Opera 4-6 */
  white-space: -o-pre-wrap; /* Opera 7 */
  word-wrap: break-word; /* Internet Explorer 5.5+ */
}

label {
  display: inline-block;
  width: 100px;
}

input {
  width: 200px;
}

.textarealabel {
  vertical-align: top;
}