@cubejs-client/core 1.3.15 → 1.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/{cubejs-client-core.js → cubejs-client-core.cjs.js} +1016 -411
  2. package/dist/cubejs-client-core.cjs.js.map +1 -0
  3. package/dist/cubejs-client-core.umd.js +2901 -12088
  4. package/dist/cubejs-client-core.umd.js.map +1 -1
  5. package/dist/src/HttpTransport.d.ts +54 -0
  6. package/dist/src/HttpTransport.d.ts.map +1 -0
  7. package/dist/src/HttpTransport.js +55 -0
  8. package/dist/src/Meta.d.ts +62 -0
  9. package/dist/src/Meta.d.ts.map +1 -0
  10. package/dist/src/Meta.js +150 -0
  11. package/dist/src/ProgressResult.d.ts +8 -0
  12. package/dist/src/ProgressResult.d.ts.map +1 -0
  13. package/dist/src/ProgressResult.js +11 -0
  14. package/dist/src/RequestError.d.ts +6 -0
  15. package/dist/src/RequestError.d.ts.map +1 -0
  16. package/dist/src/RequestError.js +7 -0
  17. package/dist/src/ResultSet.d.ts +430 -0
  18. package/dist/src/ResultSet.d.ts.map +1 -0
  19. package/dist/src/ResultSet.js +952 -0
  20. package/dist/src/SqlQuery.d.ts +17 -0
  21. package/dist/src/SqlQuery.d.ts.map +1 -0
  22. package/dist/src/SqlQuery.js +11 -0
  23. package/dist/src/index.d.ts +194 -0
  24. package/dist/src/index.d.ts.map +1 -0
  25. package/dist/src/index.js +411 -0
  26. package/dist/src/index.umd.d.ts +3 -0
  27. package/dist/src/index.umd.d.ts.map +1 -0
  28. package/dist/src/index.umd.js +6 -0
  29. package/dist/src/time.d.ts +70 -0
  30. package/dist/src/time.d.ts.map +1 -0
  31. package/dist/src/time.js +249 -0
  32. package/dist/src/types.d.ts +424 -0
  33. package/dist/src/types.d.ts.map +1 -0
  34. package/dist/src/types.js +1 -0
  35. package/dist/src/utils.d.ts +19 -0
  36. package/dist/src/utils.d.ts.map +1 -0
  37. package/dist/src/utils.js +294 -0
  38. package/dist/test/CubeApi.test.d.ts +7 -0
  39. package/dist/test/CubeApi.test.d.ts.map +1 -0
  40. package/dist/test/CubeApi.test.js +279 -0
  41. package/dist/test/HttpTransport.test.d.ts +2 -0
  42. package/dist/test/HttpTransport.test.d.ts.map +1 -0
  43. package/dist/test/HttpTransport.test.js +244 -0
  44. package/dist/test/ResultSet.test.d.ts +7 -0
  45. package/dist/test/ResultSet.test.d.ts.map +1 -0
  46. package/dist/test/ResultSet.test.js +1725 -0
  47. package/dist/test/compare-date-range.test.d.ts +2 -0
  48. package/dist/test/compare-date-range.test.d.ts.map +1 -0
  49. package/dist/test/compare-date-range.test.js +742 -0
  50. package/dist/test/data-blending.test.d.ts +2 -0
  51. package/dist/test/data-blending.test.d.ts.map +1 -0
  52. package/dist/test/data-blending.test.js +423 -0
  53. package/dist/test/default-heuristics.test.d.ts +2 -0
  54. package/dist/test/default-heuristics.test.d.ts.map +1 -0
  55. package/dist/test/default-heuristics.test.js +108 -0
  56. package/dist/test/drill-down.test.d.ts +2 -0
  57. package/dist/test/drill-down.test.d.ts.map +1 -0
  58. package/dist/test/drill-down.test.js +373 -0
  59. package/dist/test/fixtures/datablending/load-responses.json +261 -0
  60. package/dist/test/granularity.test.d.ts +2 -0
  61. package/dist/test/granularity.test.d.ts.map +1 -0
  62. package/dist/test/granularity.test.js +218 -0
  63. package/dist/test/helpers.d.ts +283 -0
  64. package/dist/test/helpers.d.ts.map +1 -0
  65. package/dist/test/helpers.js +974 -0
  66. package/dist/test/index.test.d.ts +7 -0
  67. package/dist/test/index.test.d.ts.map +1 -0
  68. package/dist/test/index.test.js +370 -0
  69. package/dist/test/table.test.d.ts +2 -0
  70. package/dist/test/table.test.d.ts.map +1 -0
  71. package/dist/test/table.test.js +757 -0
  72. package/dist/test/utils.test.d.ts +2 -0
  73. package/dist/test/utils.test.d.ts.map +1 -0
  74. package/dist/test/utils.test.js +32 -0
  75. package/package.json +26 -21
  76. package/dist/cubejs-client-core.esm.js +0 -1639
  77. package/dist/cubejs-client-core.esm.js.map +0 -1
  78. package/dist/cubejs-client-core.js.map +0 -1
  79. package/index.d.ts +0 -1338
  80. package/src/HttpTransport.js +0 -60
  81. package/src/HttpTransport.test.js +0 -117
  82. package/src/Meta.js +0 -142
  83. package/src/ProgressResult.js +0 -13
  84. package/src/RequestError.js +0 -7
  85. package/src/ResultSet.js +0 -746
  86. package/src/SqlQuery.js +0 -13
  87. package/src/index.js +0 -398
  88. package/src/index.test.js +0 -454
  89. package/src/index.umd.js +0 -8
  90. package/src/tests/ResultSet.test.js +0 -1655
  91. package/src/tests/compare-date-range.test.js +0 -753
  92. package/src/tests/data-blending.test.js +0 -432
  93. package/src/tests/default-heuristics.test.js +0 -118
  94. package/src/tests/drill-down.test.js +0 -402
  95. package/src/tests/fixtures/datablending/load-responses.json +0 -261
  96. package/src/tests/granularity.test.js +0 -225
  97. package/src/tests/table.test.js +0 -791
  98. package/src/tests/utils.test.js +0 -35
  99. package/src/time.js +0 -296
  100. package/src/utils.js +0 -368
@@ -0,0 +1,952 @@
1
+ import dayjs from 'dayjs';
2
+ import { groupBy, pipe, fromPairs, uniq, map, dropLast, equals, reduce, minBy, maxBy, clone, mergeDeepLeft, flatten, } from 'ramda';
3
+ import { aliasSeries } from './utils';
4
+ import { DateRegex, dayRange, internalDayjs, isPredefinedGranularity, LocalDateRegex, TIME_SERIES, timeSeriesFromCustomInterval } from './time';
5
+ const groupByToPairs = function groupByToPairsImpl(keyFn) {
6
+ const acc = new Map();
7
+ return (data) => {
8
+ data.forEach((row) => {
9
+ const key = keyFn(row);
10
+ if (!acc.has(key)) {
11
+ acc.set(key, []);
12
+ }
13
+ acc.get(key).push(row);
14
+ });
15
+ return Array.from(acc.entries());
16
+ };
17
+ };
18
+ const unnest = (arr) => {
19
+ const res = [];
20
+ arr.forEach((subArr) => {
21
+ subArr.forEach(element => res.push(element));
22
+ });
23
+ return res;
24
+ };
25
+ export const QUERY_TYPE = {
26
+ REGULAR_QUERY: 'regularQuery',
27
+ COMPARE_DATE_RANGE_QUERY: 'compareDateRangeQuery',
28
+ BLENDING_QUERY: 'blendingQuery',
29
+ };
30
+ /**
31
+ * Provides a convenient interface for data manipulation.
32
+ */
33
+ export default class ResultSet {
34
+ static measureFromAxis(axisValues) {
35
+ return axisValues[axisValues.length - 1];
36
+ }
37
+ static timeDimensionMember(td) {
38
+ return `${td.dimension}.${td.granularity}`;
39
+ }
40
+ /**
41
+ * ```js
42
+ * import { ResultSet } from '@cubejs-client/core';
43
+ *
44
+ * const resultSet = await cubeApi.load(query);
45
+ * // You can store the result somewhere
46
+ * const tmp = resultSet.serialize();
47
+ *
48
+ * // and restore it later
49
+ * const resultSet = ResultSet.deserialize(tmp);
50
+ * ```
51
+ * @param data the result of [serialize](#result-set-serialize)
52
+ * @param options
53
+ */
54
+ static deserialize(data, options) {
55
+ return new ResultSet(data.loadResponse, options);
56
+ }
57
+ constructor(loadResponse, options = {}) {
58
+ if ('queryType' in loadResponse && loadResponse.queryType != null) {
59
+ this.loadResponse = loadResponse;
60
+ this.queryType = loadResponse.queryType;
61
+ this.loadResponses = loadResponse.results;
62
+ }
63
+ else {
64
+ this.queryType = QUERY_TYPE.REGULAR_QUERY;
65
+ this.loadResponse = {
66
+ ...loadResponse,
67
+ pivotQuery: {
68
+ ...loadResponse.query,
69
+ queryType: this.queryType
70
+ }
71
+ };
72
+ this.loadResponses = [loadResponse];
73
+ }
74
+ if (!Object.values(QUERY_TYPE).includes(this.queryType)) {
75
+ throw new Error('Unknown query type');
76
+ }
77
+ this.parseDateMeasures = options.parseDateMeasures;
78
+ this.options = options;
79
+ this.backwardCompatibleData = [];
80
+ }
81
+ /**
82
+ * Returns a measure drill down query.
83
+ *
84
+ * Provided you have a measure with the defined `drillMembers` on the `Orders` cube
85
+ * ```js
86
+ * measures: {
87
+ * count: {
88
+ * type: `count`,
89
+ * drillMembers: [Orders.status, Users.city, count],
90
+ * },
91
+ * // ...
92
+ * }
93
+ * ```
94
+ *
95
+ * Then you can use the `drillDown` method to see the rows that contribute to that metric
96
+ * ```js
97
+ * resultSet.drillDown(
98
+ * {
99
+ * xValues,
100
+ * yValues,
101
+ * },
102
+ * // you should pass the `pivotConfig` if you have used it for axes manipulation
103
+ * pivotConfig
104
+ * )
105
+ * ```
106
+ *
107
+ * the result will be a query with the required filters applied and the dimensions/measures filled out
108
+ * ```js
109
+ * {
110
+ * measures: ['Orders.count'],
111
+ * dimensions: ['Orders.status', 'Users.city'],
112
+ * filters: [
113
+ * // dimension and measure filters
114
+ * ],
115
+ * timeDimensions: [
116
+ * //...
117
+ * ]
118
+ * }
119
+ * ```
120
+ *
121
+ * In case when you want to add `order` or `limit` to the query, you can simply spread it
122
+ *
123
+ * ```js
124
+ * // An example for React
125
+ * const drillDownResponse = useCubeQuery(
126
+ * {
127
+ * ...drillDownQuery,
128
+ * limit: 30,
129
+ * order: {
130
+ * 'Orders.ts': 'desc'
131
+ * }
132
+ * },
133
+ * {
134
+ * skip: !drillDownQuery
135
+ * }
136
+ * );
137
+ * ```
138
+ * @returns Drill down query
139
+ */
140
+ drillDown(drillDownLocator, pivotConfig) {
141
+ if (this.queryType === QUERY_TYPE.COMPARE_DATE_RANGE_QUERY) {
142
+ throw new Error('compareDateRange drillDown query is not currently supported');
143
+ }
144
+ if (this.queryType === QUERY_TYPE.BLENDING_QUERY) {
145
+ throw new Error('Data blending drillDown query is not currently supported');
146
+ }
147
+ const { query } = this.loadResponses[0];
148
+ const xValues = drillDownLocator?.xValues ?? [];
149
+ const yValues = drillDownLocator?.yValues ?? [];
150
+ const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig);
151
+ const values = [];
152
+ normalizedPivotConfig?.x.forEach((member, currentIndex) => values.push([member, xValues[currentIndex]]));
153
+ normalizedPivotConfig?.y.forEach((member, currentIndex) => values.push([member, yValues[currentIndex]]));
154
+ const { filters: parentFilters = [], segments = [] } = this.query();
155
+ const { measures } = this.loadResponses[0].annotation;
156
+ let [, measureName] = values.find(([member]) => member === 'measures') || [];
157
+ if (measureName === undefined) {
158
+ [measureName] = Object.keys(measures);
159
+ }
160
+ if (!(measures[measureName]?.drillMembers?.length ?? 0)) {
161
+ return null;
162
+ }
163
+ const filters = [
164
+ {
165
+ member: measureName,
166
+ operator: 'measureFilter',
167
+ },
168
+ ...parentFilters
169
+ ];
170
+ const timeDimensions = [];
171
+ values.filter(([member]) => member !== 'measures')
172
+ .forEach(([member, value]) => {
173
+ const [cubeName, dimension, granularity] = member.split('.');
174
+ if (granularity !== undefined) {
175
+ const range = dayRange(value, value).snapTo(granularity);
176
+ const originalTimeDimension = query.timeDimensions?.find((td) => td.dimension);
177
+ let dateRange = [
178
+ range.start,
179
+ range.end
180
+ ];
181
+ if (originalTimeDimension?.dateRange) {
182
+ const [originalStart, originalEnd] = originalTimeDimension.dateRange;
183
+ dateRange = [
184
+ dayjs(originalStart) > range.start ? dayjs(originalStart) : range.start,
185
+ dayjs(originalEnd) < range.end ? dayjs(originalEnd) : range.end,
186
+ ];
187
+ }
188
+ timeDimensions.push({
189
+ dimension: [cubeName, dimension].join('.'),
190
+ dateRange: dateRange.map((dt) => dt.format('YYYY-MM-DDTHH:mm:ss.SSS')),
191
+ });
192
+ }
193
+ else if (value == null) {
194
+ filters.push({
195
+ member,
196
+ operator: 'notSet',
197
+ });
198
+ }
199
+ else {
200
+ filters.push({
201
+ member,
202
+ operator: 'equals',
203
+ values: [value.toString()],
204
+ });
205
+ }
206
+ });
207
+ if (timeDimensions.length === 0 &&
208
+ Array.isArray(query.timeDimensions) &&
209
+ query.timeDimensions.length > 0 &&
210
+ query.timeDimensions[0].granularity == null) {
211
+ timeDimensions.push(query.timeDimensions[0]);
212
+ }
213
+ return {
214
+ ...measures[measureName].drillMembersGrouped,
215
+ filters,
216
+ ...(segments.length > 0 ? { segments } : {}),
217
+ timeDimensions,
218
+ segments,
219
+ timezone: query.timezone
220
+ };
221
+ }
222
+ /**
223
+ * Returns an array of series with key, title and series data.
224
+ * ```js
225
+ * // For the query
226
+ * {
227
+ * measures: ['Stories.count'],
228
+ * timeDimensions: [{
229
+ * dimension: 'Stories.time',
230
+ * dateRange: ['2015-01-01', '2015-12-31'],
231
+ * granularity: 'month'
232
+ * }]
233
+ * }
234
+ *
235
+ * // ResultSet.series() will return
236
+ * [
237
+ * {
238
+ * key: 'Stories.count',
239
+ * title: 'Stories Count',
240
+ * shortTitle: 'Count',
241
+ * series: [
242
+ * { x: '2015-01-01T00:00:00', value: 27120 },
243
+ * { x: '2015-02-01T00:00:00', value: 25861 },
244
+ * { x: '2015-03-01T00:00:00', value: 29661 },
245
+ * //...
246
+ * ],
247
+ * },
248
+ * ]
249
+ * ```
250
+ */
251
+ series(pivotConfig) {
252
+ return this.seriesNames(pivotConfig).map(({ title, shortTitle, key }) => ({
253
+ title,
254
+ shortTitle,
255
+ key,
256
+ series: this.chartPivot(pivotConfig).map(({ x, ...obj }) => ({ value: obj[key], x }))
257
+ }));
258
+ }
259
+ axisValues(axis, resultIndex = 0) {
260
+ const { query } = this.loadResponses[resultIndex];
261
+ return (row) => {
262
+ const value = (measure) => axis
263
+ .filter(d => d !== 'measures')
264
+ .map((d) => {
265
+ const val = row[d];
266
+ return val != null ? val : null;
267
+ })
268
+ .concat(measure ? [measure] : []);
269
+ if (axis.find(d => d === 'measures') && (query.measures || []).length) {
270
+ return (query.measures || []).map(value);
271
+ }
272
+ return [value()];
273
+ };
274
+ }
275
+ axisValuesString(axisValues, delimiter = ', ') {
276
+ const formatValue = (v) => {
277
+ if (v == null) {
278
+ return '∅';
279
+ }
280
+ else if (v === '') {
281
+ return '[Empty string]';
282
+ }
283
+ else {
284
+ return v;
285
+ }
286
+ };
287
+ return axisValues.map(formatValue).join(delimiter);
288
+ }
289
+ static getNormalizedPivotConfig(query, pivotConfig) {
290
+ const defaultPivotConfig = {
291
+ x: [],
292
+ y: [],
293
+ fillMissingDates: true,
294
+ joinDateRange: false
295
+ };
296
+ const { measures = [], dimensions = [] } = query || {};
297
+ const timeDimensions = (query?.timeDimensions || []).filter(td => !!td.granularity);
298
+ pivotConfig = pivotConfig || (timeDimensions.length ? {
299
+ x: timeDimensions.map(td => ResultSet.timeDimensionMember(td)),
300
+ y: dimensions
301
+ } : {
302
+ x: dimensions,
303
+ y: []
304
+ });
305
+ const normalizedPivotConfig = mergeDeepLeft(pivotConfig, defaultPivotConfig);
306
+ const substituteTimeDimensionMembers = (axis) => axis.map(subDim => ((timeDimensions.find(td => td.dimension === subDim) &&
307
+ !dimensions.find(d => d === subDim)) ?
308
+ ResultSet.timeDimensionMember((query?.timeDimensions || []).find(td => td.dimension === subDim)) :
309
+ subDim));
310
+ normalizedPivotConfig.x = substituteTimeDimensionMembers(normalizedPivotConfig.x);
311
+ normalizedPivotConfig.y = substituteTimeDimensionMembers(normalizedPivotConfig.y);
312
+ const allIncludedDimensions = normalizedPivotConfig.x.concat(normalizedPivotConfig.y);
313
+ const allDimensions = timeDimensions.map(td => ResultSet.timeDimensionMember(td)).concat(dimensions);
314
+ const dimensionFilter = (key) => allDimensions.includes(key) || key === 'measures';
315
+ normalizedPivotConfig.x = normalizedPivotConfig.x.concat(allDimensions.filter(d => !allIncludedDimensions.includes(d) && d !== 'compareDateRange'))
316
+ .filter(dimensionFilter);
317
+ normalizedPivotConfig.y = normalizedPivotConfig.y.filter(dimensionFilter);
318
+ if (!normalizedPivotConfig.x.concat(normalizedPivotConfig.y).find(d => d === 'measures')) {
319
+ normalizedPivotConfig.y.push('measures');
320
+ }
321
+ if (dimensions.includes('compareDateRange') && !normalizedPivotConfig.y.concat(normalizedPivotConfig.x).includes('compareDateRange')) {
322
+ normalizedPivotConfig.y.unshift('compareDateRange');
323
+ }
324
+ if (!measures.length) {
325
+ normalizedPivotConfig.x = normalizedPivotConfig.x.filter(d => d !== 'measures');
326
+ normalizedPivotConfig.y = normalizedPivotConfig.y.filter(d => d !== 'measures');
327
+ }
328
+ return normalizedPivotConfig;
329
+ }
330
+ normalizePivotConfig(pivotConfig) {
331
+ return ResultSet.getNormalizedPivotConfig(this.loadResponse.pivotQuery, pivotConfig);
332
+ }
333
+ timeSeries(timeDimension, resultIndex, annotations) {
334
+ if (!timeDimension.granularity) {
335
+ return null;
336
+ }
337
+ let dateRange;
338
+ dateRange = timeDimension.dateRange;
339
+ if (!dateRange) {
340
+ const member = ResultSet.timeDimensionMember(timeDimension);
341
+ const rawRows = this.timeDimensionBackwardCompatibleData(resultIndex || 0);
342
+ const dates = rawRows
343
+ .map(row => {
344
+ const value = row[member];
345
+ return value ? internalDayjs(value) : null;
346
+ })
347
+ .filter((d) => Boolean(d));
348
+ dateRange = dates.length && [
349
+ (reduce(minBy((d) => d.toDate()), dates[0], dates)).toString(),
350
+ (reduce(maxBy((d) => d.toDate()), dates[0], dates)).toString(),
351
+ ] || null;
352
+ }
353
+ if (!dateRange) {
354
+ return null;
355
+ }
356
+ const padToDay = timeDimension.dateRange ?
357
+ timeDimension.dateRange.find(d => d.match(DateRegex)) :
358
+ !['hour', 'minute', 'second'].includes(timeDimension.granularity);
359
+ const [start, end] = dateRange;
360
+ const range = dayRange(start, end);
361
+ if (isPredefinedGranularity(timeDimension.granularity)) {
362
+ return TIME_SERIES[timeDimension.granularity](padToDay ? range.snapTo('d') : range);
363
+ }
364
+ if (!annotations?.[`${timeDimension.dimension}.${timeDimension.granularity}`]) {
365
+ throw new Error(`Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}"`);
366
+ }
367
+ return timeSeriesFromCustomInterval(start, end, annotations[`${timeDimension.dimension}.${timeDimension.granularity}`].granularity);
368
+ }
369
+ /**
370
+ * Base method for pivoting [ResultSet](#result-set) data.
371
+ * Most of the time shouldn't be used directly and [chartPivot](#result-set-chart-pivot)
372
+ * or [tablePivot](#table-pivot) should be used instead.
373
+ *
374
+ * You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
375
+ * ```js
376
+ * // For query
377
+ * {
378
+ * measures: ['Stories.count'],
379
+ * timeDimensions: [{
380
+ * dimension: 'Stories.time',
381
+ * dateRange: ['2015-01-01', '2015-03-31'],
382
+ * granularity: 'month'
383
+ * }]
384
+ * }
385
+ *
386
+ * // ResultSet.pivot({ x: ['Stories.time'], y: ['measures'] }) will return
387
+ * [
388
+ * {
389
+ * xValues: ["2015-01-01T00:00:00"],
390
+ * yValuesArray: [
391
+ * [['Stories.count'], 27120]
392
+ * ]
393
+ * },
394
+ * {
395
+ * xValues: ["2015-02-01T00:00:00"],
396
+ * yValuesArray: [
397
+ * [['Stories.count'], 25861]
398
+ * ]
399
+ * },
400
+ * {
401
+ * xValues: ["2015-03-01T00:00:00"],
402
+ * yValuesArray: [
403
+ * [['Stories.count'], 29661]
404
+ * ]
405
+ * }
406
+ * ]
407
+ * ```
408
+ * @returns An array of pivoted rows.
409
+ */
410
+ pivot(pivotConfig) {
411
+ const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig);
412
+ const { pivotQuery: query } = this.loadResponse;
413
+ const pivotImpl = (resultIndex = 0) => {
414
+ let groupByXAxis = groupByToPairs(({ xValues }) => this.axisValuesString(xValues));
415
+ const measureValue = (row, measure) => row[measure] || normalizedPivotConfig.fillWithValue || 0;
416
+ if (normalizedPivotConfig.fillMissingDates &&
417
+ normalizedPivotConfig.x.length === 1 &&
418
+ (equals(normalizedPivotConfig.x, (query.timeDimensions || [])
419
+ .filter(td => Boolean(td.granularity))
420
+ .map(td => ResultSet.timeDimensionMember(td))))) {
421
+ const series = this.loadResponses.map((loadResponse) => this.timeSeries(loadResponse.query.timeDimensions[0], resultIndex, loadResponse.annotation.timeDimensions));
422
+ if (series[0]) {
423
+ groupByXAxis = (rows) => {
424
+ const byXValues = groupBy(({ xValues }) => xValues[0], rows);
425
+ return series[resultIndex]?.map(d => [d, byXValues[d] || [{ xValues: [d], row: {} }]]) ?? [];
426
+ };
427
+ }
428
+ }
429
+ const xGrouped = pipe(map((row) => this.axisValues(normalizedPivotConfig.x, resultIndex)(row).map(xValues => ({ xValues, row }))), unnest, groupByXAxis)(this.timeDimensionBackwardCompatibleData(resultIndex));
430
+ const yValuesMap = {};
431
+ xGrouped.forEach(([, rows]) => {
432
+ rows.forEach(({ row }) => {
433
+ this.axisValues(normalizedPivotConfig.y, resultIndex)(row).forEach((values) => {
434
+ if (Object.keys(row).length > 0) {
435
+ yValuesMap[values.join()] = values;
436
+ }
437
+ });
438
+ });
439
+ });
440
+ const allYValues = Object.values(yValuesMap);
441
+ const measureOnX = Boolean((normalizedPivotConfig.x).find(d => d === 'measures'));
442
+ return xGrouped.map(([, rows]) => {
443
+ const { xValues } = rows[0];
444
+ const yGrouped = {};
445
+ rows.forEach(({ row }) => {
446
+ const arr = this.axisValues(normalizedPivotConfig.y, resultIndex)(row).map(yValues => ({ yValues, row }));
447
+ arr.forEach((res) => {
448
+ yGrouped[this.axisValuesString(res.yValues)] = res;
449
+ });
450
+ });
451
+ return {
452
+ xValues,
453
+ yValuesArray: unnest(allYValues.map(yValues => {
454
+ const measure = measureOnX ?
455
+ ResultSet.measureFromAxis(xValues) :
456
+ ResultSet.measureFromAxis(yValues);
457
+ return [[yValues, measureValue((yGrouped[this.axisValuesString(yValues)] ||
458
+ ({ row: {} })).row, measure)]];
459
+ }))
460
+ };
461
+ });
462
+ };
463
+ const pivots = this.loadResponses.length > 1
464
+ ? this.loadResponses.map((_, index) => pivotImpl(index))
465
+ : [];
466
+ return pivots.length
467
+ ? this.mergePivots(pivots, normalizedPivotConfig.joinDateRange || false)
468
+ : pivotImpl();
469
+ }
470
+ mergePivots(pivots, joinDateRange) {
471
+ const minLengthPivot = pivots.reduce((memo, current) => (memo != null && current.length >= memo.length ? memo : current), null) || [];
472
+ return minLengthPivot.map((_, index) => {
473
+ const xValues = joinDateRange
474
+ ? [pivots.map((pivot) => pivot[index]?.xValues || []).join(', ')]
475
+ : minLengthPivot[index].xValues;
476
+ return {
477
+ xValues,
478
+ yValuesArray: unnest(pivots.map((pivot) => pivot[index].yValuesArray))
479
+ };
480
+ });
481
+ }
482
+ /**
483
+ * Returns normalized query result data in the following format.
484
+ *
485
+ * You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
486
+ * ```js
487
+ * // For the query
488
+ * {
489
+ * measures: ['Stories.count'],
490
+ * timeDimensions: [{
491
+ * dimension: 'Stories.time',
492
+ * dateRange: ['2015-01-01', '2015-12-31'],
493
+ * granularity: 'month'
494
+ * }]
495
+ * }
496
+ *
497
+ * // ResultSet.chartPivot() will return
498
+ * [
499
+ * { "x":"2015-01-01T00:00:00", "Stories.count": 27120, "xValues": ["2015-01-01T00:00:00"] },
500
+ * { "x":"2015-02-01T00:00:00", "Stories.count": 25861, "xValues": ["2015-02-01T00:00:00"] },
501
+ * { "x":"2015-03-01T00:00:00", "Stories.count": 29661, "xValues": ["2015-03-01T00:00:00"] },
502
+ * //...
503
+ * ]
504
+ *
505
+ * ```
506
+ * When using `chartPivot()` or `seriesNames()`, you can pass `aliasSeries` in the [pivotConfig](#types-pivot-config)
507
+ * to give each series a unique prefix. This is useful for `blending queries` which use the same measure multiple times.
508
+ *
509
+ * ```js
510
+ * // For the queries
511
+ * {
512
+ * measures: ['Stories.count'],
513
+ * timeDimensions: [
514
+ * {
515
+ * dimension: 'Stories.time',
516
+ * dateRange: ['2015-01-01', '2015-12-31'],
517
+ * granularity: 'month',
518
+ * },
519
+ * ],
520
+ * },
521
+ * {
522
+ * measures: ['Stories.count'],
523
+ * timeDimensions: [
524
+ * {
525
+ * dimension: 'Stories.time',
526
+ * dateRange: ['2015-01-01', '2015-12-31'],
527
+ * granularity: 'month',
528
+ * },
529
+ * ],
530
+ * filters: [
531
+ * {
532
+ * member: 'Stores.read',
533
+ * operator: 'equals',
534
+ * value: ['true'],
535
+ * },
536
+ * ],
537
+ * },
538
+ *
539
+ * // ResultSet.chartPivot({ aliasSeries: ['one', 'two'] }) will return
540
+ * [
541
+ * {
542
+ * x: '2015-01-01T00:00:00',
543
+ * 'one,Stories.count': 27120,
544
+ * 'two,Stories.count': 8933,
545
+ * xValues: ['2015-01-01T00:00:00'],
546
+ * },
547
+ * {
548
+ * x: '2015-02-01T00:00:00',
549
+ * 'one,Stories.count': 25861,
550
+ * 'two,Stories.count': 8344,
551
+ * xValues: ['2015-02-01T00:00:00'],
552
+ * },
553
+ * {
554
+ * x: '2015-03-01T00:00:00',
555
+ * 'one,Stories.count': 29661,
556
+ * 'two,Stories.count': 9023,
557
+ * xValues: ['2015-03-01T00:00:00'],
558
+ * },
559
+ * //...
560
+ * ]
561
+ * ```
562
+ */
563
+ chartPivot(pivotConfig) {
564
+ const validate = (value) => {
565
+ if (this.parseDateMeasures && LocalDateRegex.test(value)) {
566
+ return new Date(value);
567
+ }
568
+ else if (!Number.isNaN(Number.parseFloat(value))) {
569
+ return Number.parseFloat(value);
570
+ }
571
+ return value;
572
+ };
573
+ const duplicateMeasures = new Set();
574
+ if (this.queryType === QUERY_TYPE.BLENDING_QUERY) {
575
+ const allMeasures = flatten(this.loadResponses.map(({ query }) => query.measures ?? []));
576
+ allMeasures.filter((e, i, a) => a.indexOf(e) !== i).forEach(m => duplicateMeasures.add(m));
577
+ }
578
+ return this.pivot(pivotConfig).map(({ xValues, yValuesArray }) => {
579
+ const yValuesMap = {};
580
+ yValuesArray
581
+ .forEach(([yValues, m], i) => {
582
+ yValuesMap[this.axisValuesString(aliasSeries(yValues, i, pivotConfig, duplicateMeasures), ',')] = m && validate(m);
583
+ });
584
+ return {
585
+ x: this.axisValuesString(xValues, ','),
586
+ xValues,
587
+ ...yValuesMap
588
+ };
589
+ });
590
+ }
591
+ /**
592
+ * Returns normalized query result data prepared for visualization in the table format.
593
+ *
594
+ * You can find the examples of using the `pivotConfig` [here](#types-pivot-config)
595
+ *
596
+ * For example:
597
+ * ```js
598
+ * // For the query
599
+ * {
600
+ * measures: ['Stories.count'],
601
+ * timeDimensions: [{
602
+ * dimension: 'Stories.time',
603
+ * dateRange: ['2015-01-01', '2015-12-31'],
604
+ * granularity: 'month'
605
+ * }]
606
+ * }
607
+ *
608
+ * // ResultSet.tablePivot() will return
609
+ * [
610
+ * { "Stories.time": "2015-01-01T00:00:00", "Stories.count": 27120 },
611
+ * { "Stories.time": "2015-02-01T00:00:00", "Stories.count": 25861 },
612
+ * { "Stories.time": "2015-03-01T00:00:00", "Stories.count": 29661 },
613
+ * //...
614
+ * ]
615
+ * ```
616
+ * @returns An array of pivoted rows
617
+ */
618
+ tablePivot(pivotConfig) {
619
+ const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig || {});
620
+ const isMeasuresPresent = normalizedPivotConfig.x.concat(normalizedPivotConfig.y).includes('measures');
621
+ return this.pivot(normalizedPivotConfig).map(({ xValues, yValuesArray }) => fromPairs([
622
+ ...(normalizedPivotConfig.x).map((key, index) => [
623
+ key,
624
+ xValues[index] ?? ''
625
+ ]),
626
+ ...(isMeasuresPresent
627
+ ? yValuesArray.map(([yValues, measure]) => [
628
+ yValues.length ? yValues.join() : 'value',
629
+ measure
630
+ ])
631
+ : [])
632
+ ]));
633
+ }
634
+ /**
635
+ * Returns an array of column definitions for `tablePivot`.
636
+ *
637
+ * For example:
638
+ * ```js
639
+ * // For the query
640
+ * {
641
+ * measures: ['Stories.count'],
642
+ * timeDimensions: [{
643
+ * dimension: 'Stories.time',
644
+ * dateRange: ['2015-01-01', '2015-12-31'],
645
+ * granularity: 'month'
646
+ * }]
647
+ * }
648
+ *
649
+ * // ResultSet.tableColumns() will return
650
+ * [
651
+ * {
652
+ * key: 'Stories.time',
653
+ * dataIndex: 'Stories.time',
654
+ * title: 'Stories Time',
655
+ * shortTitle: 'Time',
656
+ * type: 'time',
657
+ * format: undefined,
658
+ * },
659
+ * {
660
+ * key: 'Stories.count',
661
+ * dataIndex: 'Stories.count',
662
+ * title: 'Stories Count',
663
+ * shortTitle: 'Count',
664
+ * type: 'count',
665
+ * format: undefined,
666
+ * },
667
+ * //...
668
+ * ]
669
+ * ```
670
+ *
671
+ * In case we want to pivot the table axes
672
+ * ```js
673
+ * // Let's take this query as an example
674
+ * {
675
+ * measures: ['Orders.count'],
676
+ * dimensions: ['Users.country', 'Users.gender']
677
+ * }
678
+ *
679
+ * // and put the dimensions on `y` axis
680
+ * resultSet.tableColumns({
681
+ * x: [],
682
+ * y: ['Users.country', 'Users.gender', 'measures']
683
+ * })
684
+ * ```
685
+ *
686
+ * then `tableColumns` will group the table head and return
687
+ * ```js
688
+ * {
689
+ * key: 'Germany',
690
+ * type: 'string',
691
+ * title: 'Users Country Germany',
692
+ * shortTitle: 'Germany',
693
+ * meta: undefined,
694
+ * format: undefined,
695
+ * children: [
696
+ * {
697
+ * key: 'male',
698
+ * type: 'string',
699
+ * title: 'Users Gender male',
700
+ * shortTitle: 'male',
701
+ * meta: undefined,
702
+ * format: undefined,
703
+ * children: [
704
+ * {
705
+ * // ...
706
+ * dataIndex: 'Germany.male.Orders.count',
707
+ * shortTitle: 'Count',
708
+ * },
709
+ * ],
710
+ * },
711
+ * {
712
+ * // ...
713
+ * shortTitle: 'female',
714
+ * children: [
715
+ * {
716
+ * // ...
717
+ * dataIndex: 'Germany.female.Orders.count',
718
+ * shortTitle: 'Count',
719
+ * },
720
+ * ],
721
+ * },
722
+ * ],
723
+ * },
724
+ * // ...
725
+ * ```
726
+ * @returns An array of columns
727
+ */
728
+ tableColumns(pivotConfig) {
729
+ const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig || {});
730
+ const annotations = this.loadResponses
731
+ .map((r) => r.annotation)
732
+ .reduce((acc, annotation) => mergeDeepLeft(acc, annotation), {
733
+ dimensions: {},
734
+ measures: {},
735
+ timeDimensions: {},
736
+ segments: {},
737
+ });
738
+ const flatMeta = Object.values(annotations).reduce((a, b) => ({ ...a, ...b }), {});
739
+ const schema = {};
740
+ const extractFields = (key) => {
741
+ const { title, shortTitle, type, format, meta } = flatMeta[key] || {};
742
+ return {
743
+ key,
744
+ title,
745
+ shortTitle,
746
+ type,
747
+ format,
748
+ meta
749
+ };
750
+ };
751
+ const pivot = this.pivot(normalizedPivotConfig);
752
+ (pivot[0]?.yValuesArray || []).forEach(([yValues]) => {
753
+ if (yValues.length > 0) {
754
+ let currentItem = schema;
755
+ yValues.forEach((value, index) => {
756
+ currentItem[`_${value}`] = {
757
+ key: value,
758
+ memberId: normalizedPivotConfig.y[index] === 'measures'
759
+ ? value
760
+ : normalizedPivotConfig.y[index],
761
+ children: currentItem[`_${value}`]?.children || {}
762
+ };
763
+ currentItem = currentItem[`_${value}`].children;
764
+ });
765
+ }
766
+ });
767
+ const toColumns = (item = {}, path = []) => {
768
+ if (Object.keys(item).length === 0) {
769
+ return [];
770
+ }
771
+ return Object.values(item).map(({ key, ...currentItem }) => {
772
+ const children = toColumns(currentItem.children, [
773
+ ...path,
774
+ key
775
+ ]);
776
+ const { title, shortTitle, ...fields } = extractFields(currentItem.memberId);
777
+ const dimensionValue = key !== currentItem.memberId || title == null ? key : '';
778
+ if (!children.length) {
779
+ return {
780
+ ...fields,
781
+ key,
782
+ dataIndex: [...path, key].join(),
783
+ title: [title, dimensionValue].join(' ').trim(),
784
+ shortTitle: dimensionValue || shortTitle,
785
+ };
786
+ }
787
+ return {
788
+ ...fields,
789
+ key,
790
+ title: [title, dimensionValue].join(' ').trim(),
791
+ shortTitle: dimensionValue || shortTitle,
792
+ children,
793
+ };
794
+ });
795
+ };
796
+ let otherColumns = [];
797
+ if (!pivot.length && normalizedPivotConfig.y.includes('measures')) {
798
+ otherColumns = (this.loadResponses[0].query.measures || []).map((key) => ({ ...extractFields(key), dataIndex: key }));
799
+ }
800
+ // Synthetic column to display the measure value
801
+ if (!normalizedPivotConfig.y.length && normalizedPivotConfig.x.includes('measures')) {
802
+ otherColumns.push({
803
+ key: 'value',
804
+ dataIndex: 'value',
805
+ title: 'Value',
806
+ shortTitle: 'Value',
807
+ type: 'string',
808
+ });
809
+ }
810
+ return (normalizedPivotConfig.x).map((key) => {
811
+ if (key === 'measures') {
812
+ return {
813
+ key: 'measures',
814
+ dataIndex: 'measures',
815
+ title: 'Measures',
816
+ shortTitle: 'Measures',
817
+ type: 'string',
818
+ };
819
+ }
820
+ return ({ ...extractFields(key), dataIndex: key });
821
+ })
822
+ .concat(toColumns(schema))
823
+ .concat(otherColumns);
824
+ }
825
+ totalRow(pivotConfig) {
826
+ return this.chartPivot(pivotConfig)[0];
827
+ }
828
+ categories(pivotConfig) {
829
+ return this.chartPivot(pivotConfig);
830
+ }
831
+ /**
832
+ * Returns an array of series objects, containing `key` and `title` parameters.
833
+ * ```js
834
+ * // For query
835
+ * {
836
+ * measures: ['Stories.count'],
837
+ * timeDimensions: [{
838
+ * dimension: 'Stories.time',
839
+ * dateRange: ['2015-01-01', '2015-12-31'],
840
+ * granularity: 'month'
841
+ * }]
842
+ * }
843
+ *
844
+ * // ResultSet.seriesNames() will return
845
+ * [
846
+ * {
847
+ * key: 'Stories.count',
848
+ * title: 'Stories Count',
849
+ * shortTitle: 'Count',
850
+ * yValues: ['Stories.count'],
851
+ * },
852
+ * ]
853
+ * ```
854
+ * @returns An array of series names
855
+ */
856
+ seriesNames(pivotConfig) {
857
+ const normalizedPivotConfig = this.normalizePivotConfig(pivotConfig);
858
+ const measures = this.loadResponses
859
+ .map(r => r.annotation.measures)
860
+ .reduce((acc, m) => ({ ...acc, ...m }), {});
861
+ const seriesNames = unnest(this.loadResponses.map((_, index) => pipe(map(this.axisValues(normalizedPivotConfig.y, index)), unnest, uniq)(this.timeDimensionBackwardCompatibleData(index))));
862
+ const duplicateMeasures = new Set();
863
+ if (this.queryType === QUERY_TYPE.BLENDING_QUERY) {
864
+ const allMeasures = flatten(this.loadResponses.map(({ query }) => query.measures ?? []));
865
+ allMeasures.filter((e, i, a) => a.indexOf(e) !== i).forEach(m => duplicateMeasures.add(m));
866
+ }
867
+ return seriesNames.map((axisValues, i) => {
868
+ const aliasedAxis = aliasSeries(axisValues, i, normalizedPivotConfig, duplicateMeasures);
869
+ return {
870
+ title: this.axisValuesString(normalizedPivotConfig.y.find(d => d === 'measures') ?
871
+ dropLast(1, aliasedAxis).concat(measures[ResultSet.measureFromAxis(axisValues)].title) :
872
+ aliasedAxis, ', '),
873
+ shortTitle: this.axisValuesString(normalizedPivotConfig.y.find(d => d === 'measures') ?
874
+ dropLast(1, aliasedAxis).concat(measures[ResultSet.measureFromAxis(axisValues)].shortTitle) :
875
+ aliasedAxis, ', '),
876
+ key: this.axisValuesString(aliasedAxis, ','),
877
+ yValues: axisValues
878
+ };
879
+ });
880
+ }
881
+ query() {
882
+ if (this.queryType !== QUERY_TYPE.REGULAR_QUERY) {
883
+ throw new Error(`Method is not supported for a '${this.queryType}' query type. Please use decompose`);
884
+ }
885
+ return this.loadResponses[0].query;
886
+ }
887
+ pivotQuery() {
888
+ return this.loadResponse.pivotQuery || null;
889
+ }
890
+ /**
891
+ * @return the total number of rows if the `total` option was set, when sending the query
892
+ */
893
+ totalRows() {
894
+ return this.loadResponses[0].total;
895
+ }
896
+ rawData() {
897
+ if (this.queryType !== QUERY_TYPE.REGULAR_QUERY) {
898
+ throw new Error(`Method is not supported for a '${this.queryType}' query type. Please use decompose`);
899
+ }
900
+ return this.loadResponses[0].data;
901
+ }
902
+ annotation() {
903
+ if (this.queryType !== QUERY_TYPE.REGULAR_QUERY) {
904
+ throw new Error(`Method is not supported for a '${this.queryType}' query type. Please use decompose`);
905
+ }
906
+ return this.loadResponses[0].annotation;
907
+ }
908
+ timeDimensionBackwardCompatibleData(resultIndex) {
909
+ if (resultIndex === undefined) {
910
+ throw new Error('resultIndex is required');
911
+ }
912
+ if (!this.backwardCompatibleData[resultIndex]) {
913
+ const { data, query } = this.loadResponses[resultIndex];
914
+ const timeDimensions = (query.timeDimensions || []).filter(td => Boolean(td.granularity));
915
+ this.backwardCompatibleData[resultIndex] = data.map(row => ({
916
+ ...row,
917
+ ...(fromPairs(Object.keys(row)
918
+ .filter(field => {
919
+ const foundTd = timeDimensions.find(d => d.dimension === field);
920
+ return foundTd && !row[ResultSet.timeDimensionMember(foundTd)];
921
+ }).map(field => ([ResultSet.timeDimensionMember(timeDimensions.find(d => d.dimension === field)), row[field]]))))
922
+ }));
923
+ }
924
+ return this.backwardCompatibleData[resultIndex];
925
+ }
926
+ /**
927
+ * Can be used when you need access to the methods that can't be used with some query types (eg `compareDateRangeQuery` or `blendingQuery`)
928
+ * ```js
929
+ * resultSet.decompose().forEach((currentResultSet) => {
930
+ * console.log(currentResultSet.rawData());
931
+ * });
932
+ * ```
933
+ */
934
+ decompose() {
935
+ return this.loadResponses.map((result) => new ResultSet({
936
+ queryType: QUERY_TYPE.REGULAR_QUERY,
937
+ pivotQuery: {
938
+ ...result.query,
939
+ queryType: QUERY_TYPE.REGULAR_QUERY,
940
+ },
941
+ results: [result]
942
+ }, this.options));
943
+ }
944
+ /**
945
+ * Can be used to stash the `ResultSet` in a storage and restored later with [deserialize](#result-set-deserialize)
946
+ */
947
+ serialize() {
948
+ return {
949
+ loadResponse: clone(this.loadResponse)
950
+ };
951
+ }
952
+ }