@cubejs-client/core 0.36.0 → 0.36.4
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.
- package/dist/cubejs-client-core.esm.js +228 -43
- package/dist/cubejs-client-core.esm.js.map +1 -1
- package/dist/cubejs-client-core.js +296 -83
- package/dist/cubejs-client-core.js.map +1 -1
- package/dist/cubejs-client-core.umd.js +515 -203
- package/dist/cubejs-client-core.umd.js.map +1 -1
- package/index.d.ts +20 -2
- package/package.json +2 -2
- package/src/ResultSet.js +28 -52
- package/src/index.js +6 -5
- package/src/tests/ResultSet.test.js +301 -4
- package/src/tests/granularity.test.js +2 -0
- package/src/tests/utils.test.js +1 -1
- package/src/time.js +296 -0
- package/src/utils.js +5 -18
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { v4 } from 'uuid';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
|
+
import { fromPairs, toPairs, equals, clone, indexBy, prop, mergeDeepLeft, pipe, map, filter, reduce, minBy, maxBy, flatten, pluck, mergeAll, uniq, dropLast, groupBy, unnest as unnest$1 } from 'ramda';
|
|
3
4
|
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
|
|
5
|
+
import duration from 'dayjs/plugin/duration';
|
|
6
|
+
import isoWeek from 'dayjs/plugin/isoWeek';
|
|
4
7
|
import en from 'dayjs/locale/en';
|
|
5
|
-
import { fromPairs, toPairs, equals, clone, indexBy, prop, mergeDeepLeft, pipe, map, filter, reduce, minBy, maxBy, flatten, pluck, mergeAll, uniq, dropLast, groupBy, unnest as unnest$1 } from 'ramda';
|
|
6
8
|
import fetch from 'cross-fetch';
|
|
7
9
|
import 'url-search-params-polyfill';
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
dayjs.extend(quarterOfYear);
|
|
12
|
+
dayjs.extend(duration);
|
|
13
|
+
dayjs.extend(isoWeek);
|
|
10
14
|
const GRANULARITIES = [{
|
|
11
15
|
name: undefined,
|
|
12
16
|
title: 'w/o grouping'
|
|
@@ -35,6 +39,219 @@ const GRANULARITIES = [{
|
|
|
35
39
|
name: 'year',
|
|
36
40
|
title: 'Year'
|
|
37
41
|
}];
|
|
42
|
+
const DEFAULT_GRANULARITY = 'day';
|
|
43
|
+
|
|
44
|
+
// When granularity is week, weekStart Value must be 1. However, since the client can change it globally
|
|
45
|
+
// (https://day.js.org/docs/en/i18n/changing-locale) So the function below has been added.
|
|
46
|
+
const internalDayjs = (...args) => dayjs(...args).locale({
|
|
47
|
+
...en,
|
|
48
|
+
weekStart: 1
|
|
49
|
+
});
|
|
50
|
+
const TIME_SERIES = {
|
|
51
|
+
day: range => range.by('d').map(d => d.format('YYYY-MM-DDT00:00:00.000')),
|
|
52
|
+
month: range => range.snapTo('month').by('M').map(d => d.format('YYYY-MM-01T00:00:00.000')),
|
|
53
|
+
year: range => range.snapTo('year').by('y').map(d => d.format('YYYY-01-01T00:00:00.000')),
|
|
54
|
+
hour: range => range.by('h').map(d => d.format('YYYY-MM-DDTHH:00:00.000')),
|
|
55
|
+
minute: range => range.by('m').map(d => d.format('YYYY-MM-DDTHH:mm:00.000')),
|
|
56
|
+
second: range => range.by('s').map(d => d.format('YYYY-MM-DDTHH:mm:ss.000')),
|
|
57
|
+
week: range => range.snapTo('week').by('w').map(d => d.startOf('week').format('YYYY-MM-DDT00:00:00.000')),
|
|
58
|
+
quarter: range => range.snapTo('quarter').by('quarter').map(d => d.startOf('quarter').format('YYYY-MM-DDT00:00:00.000'))
|
|
59
|
+
};
|
|
60
|
+
const isPredefinedGranularity = granularity => !!TIME_SERIES[granularity];
|
|
61
|
+
const DateRegex = /^\d\d\d\d-\d\d-\d\d$/;
|
|
62
|
+
const LocalDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z?$/;
|
|
63
|
+
const dayRange = (from, to) => ({
|
|
64
|
+
by: value => {
|
|
65
|
+
const results = [];
|
|
66
|
+
let start = internalDayjs(from);
|
|
67
|
+
const end = internalDayjs(to);
|
|
68
|
+
while (start.isBefore(end) || start.isSame(end)) {
|
|
69
|
+
results.push(start);
|
|
70
|
+
start = start.add(1, value);
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
},
|
|
74
|
+
snapTo: value => dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value)),
|
|
75
|
+
start: internalDayjs(from),
|
|
76
|
+
end: internalDayjs(to)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse PostgreSQL-like interval string into object
|
|
81
|
+
* E.g. '2 years 15 months 100 weeks 99 hours 15 seconds'
|
|
82
|
+
* Negative units are also supported
|
|
83
|
+
* E.g. '-2 months 5 days -10 hours'
|
|
84
|
+
*
|
|
85
|
+
* TODO: It's copy/paste of parseSqlInterval from @cubejs-backend/shared [time.ts]
|
|
86
|
+
* It's not referenced to omit imports of moment.js staff.
|
|
87
|
+
* Probably one day we should choose one implementation and reuse it in other places.
|
|
88
|
+
*/
|
|
89
|
+
function parseSqlInterval(intervalStr) {
|
|
90
|
+
const interval = {};
|
|
91
|
+
const parts = intervalStr.split(/\s+/);
|
|
92
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
93
|
+
const value = parseInt(parts[i], 10);
|
|
94
|
+
const unit = parts[i + 1];
|
|
95
|
+
|
|
96
|
+
// Remove ending 's' (e.g., 'days' -> 'day')
|
|
97
|
+
const singularUnit = unit.endsWith('s') ? unit.slice(0, -1) : unit;
|
|
98
|
+
interval[singularUnit] = value;
|
|
99
|
+
}
|
|
100
|
+
return interval;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Adds interval to provided date.
|
|
105
|
+
* TODO: It's copy/paste of addInterval from @cubejs-backend/shared [time.ts]
|
|
106
|
+
* but operates with dayjs instead of moment.js
|
|
107
|
+
* @param {dayjs} date
|
|
108
|
+
* @param interval
|
|
109
|
+
* @returns {dayjs}
|
|
110
|
+
*/
|
|
111
|
+
function addInterval(date, interval) {
|
|
112
|
+
let res = date.clone();
|
|
113
|
+
Object.entries(interval).forEach(([key, value]) => {
|
|
114
|
+
res = res.add(value, key);
|
|
115
|
+
});
|
|
116
|
+
return res;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Adds interval to provided date.
|
|
121
|
+
* TODO: It's copy/paste of subtractInterval from @cubejs-backend/shared [time.ts]
|
|
122
|
+
* but operates with dayjs instead of moment.js
|
|
123
|
+
* @param {dayjs} date
|
|
124
|
+
* @param interval
|
|
125
|
+
* @returns {dayjs}
|
|
126
|
+
*/
|
|
127
|
+
function subtractInterval(date, interval) {
|
|
128
|
+
let res = date.clone();
|
|
129
|
+
Object.entries(interval).forEach(([key, value]) => {
|
|
130
|
+
res = res.subtract(value, key);
|
|
131
|
+
});
|
|
132
|
+
return res;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Returns the closest date prior to date parameter aligned with the origin point
|
|
137
|
+
* TODO: It's copy/paste of alignToOrigin from @cubejs-backend/shared [time.ts]
|
|
138
|
+
* but operates with dayjs instead of moment.js
|
|
139
|
+
*/
|
|
140
|
+
function alignToOrigin(startDate, interval, origin) {
|
|
141
|
+
let alignedDate = startDate.clone();
|
|
142
|
+
let intervalOp;
|
|
143
|
+
let isIntervalNegative = false;
|
|
144
|
+
let offsetDate = addInterval(origin, interval);
|
|
145
|
+
|
|
146
|
+
// The easiest way to check the interval sign
|
|
147
|
+
if (offsetDate.isBefore(origin)) {
|
|
148
|
+
isIntervalNegative = true;
|
|
149
|
+
}
|
|
150
|
+
offsetDate = origin.clone();
|
|
151
|
+
if (startDate.isBefore(origin)) {
|
|
152
|
+
intervalOp = isIntervalNegative ? addInterval : subtractInterval;
|
|
153
|
+
while (offsetDate.isAfter(startDate)) {
|
|
154
|
+
offsetDate = intervalOp(offsetDate, interval);
|
|
155
|
+
}
|
|
156
|
+
alignedDate = offsetDate;
|
|
157
|
+
} else {
|
|
158
|
+
intervalOp = isIntervalNegative ? subtractInterval : addInterval;
|
|
159
|
+
while (offsetDate.isBefore(startDate)) {
|
|
160
|
+
alignedDate = offsetDate.clone();
|
|
161
|
+
offsetDate = intervalOp(offsetDate, interval);
|
|
162
|
+
}
|
|
163
|
+
if (offsetDate.isSame(startDate)) {
|
|
164
|
+
alignedDate = offsetDate;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return alignedDate;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns the time series points for the custom interval
|
|
172
|
+
* TODO: It's almost a copy/paste of timeSeriesFromCustomInterval from
|
|
173
|
+
* @cubejs-backend/shared [time.ts] but operates with dayjs instead of moment.js
|
|
174
|
+
*/
|
|
175
|
+
const timeSeriesFromCustomInterval = (from, to, granularity) => {
|
|
176
|
+
const intervalParsed = parseSqlInterval(granularity.interval);
|
|
177
|
+
const start = internalDayjs(from);
|
|
178
|
+
const end = internalDayjs(to);
|
|
179
|
+
let origin = granularity.origin ? internalDayjs(granularity.origin) : internalDayjs().startOf('year');
|
|
180
|
+
if (granularity.offset) {
|
|
181
|
+
origin = addInterval(origin, parseSqlInterval(granularity.offset));
|
|
182
|
+
}
|
|
183
|
+
let alignedStart = alignToOrigin(start, intervalParsed, origin);
|
|
184
|
+
const dates = [];
|
|
185
|
+
while (alignedStart.isBefore(end) || alignedStart.isSame(end)) {
|
|
186
|
+
dates.push(alignedStart.format('YYYY-MM-DDTHH:mm:ss.000'));
|
|
187
|
+
alignedStart = addInterval(alignedStart, intervalParsed);
|
|
188
|
+
}
|
|
189
|
+
return dates;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Returns the lowest time unit for the interval
|
|
194
|
+
* @protected
|
|
195
|
+
* @param {string} interval
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
const diffTimeUnitForInterval = interval => {
|
|
199
|
+
if (/second/i.test(interval)) {
|
|
200
|
+
return 'second';
|
|
201
|
+
} else if (/minute/i.test(interval)) {
|
|
202
|
+
return 'minute';
|
|
203
|
+
} else if (/hour/i.test(interval)) {
|
|
204
|
+
return 'hour';
|
|
205
|
+
} else if (/day/i.test(interval)) {
|
|
206
|
+
return 'day';
|
|
207
|
+
} else if (/week/i.test(interval)) {
|
|
208
|
+
return 'day';
|
|
209
|
+
} else if (/month/i.test(interval)) {
|
|
210
|
+
return 'month';
|
|
211
|
+
} else if (/quarter/i.test(interval)) {
|
|
212
|
+
return 'month';
|
|
213
|
+
} else /* if (/year/i.test(interval)) */{
|
|
214
|
+
return 'year';
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const granularityOrder = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
|
218
|
+
const minGranularityForIntervals = (i1, i2) => {
|
|
219
|
+
const g1 = diffTimeUnitForInterval(i1);
|
|
220
|
+
const g2 = diffTimeUnitForInterval(i2);
|
|
221
|
+
const g1pos = granularityOrder.indexOf(g1);
|
|
222
|
+
const g2pos = granularityOrder.indexOf(g2);
|
|
223
|
+
if (g1pos > g2pos) {
|
|
224
|
+
return g1;
|
|
225
|
+
}
|
|
226
|
+
return g2;
|
|
227
|
+
};
|
|
228
|
+
const granularityFor = dateStr => {
|
|
229
|
+
const dayjsDate = internalDayjs(dateStr);
|
|
230
|
+
const month = dayjsDate.month();
|
|
231
|
+
const date = dayjsDate.date();
|
|
232
|
+
const hours = dayjsDate.hour();
|
|
233
|
+
const minutes = dayjsDate.minute();
|
|
234
|
+
const seconds = dayjsDate.second();
|
|
235
|
+
const milliseconds = dayjsDate.millisecond();
|
|
236
|
+
const weekDay = dayjsDate.isoWeekday();
|
|
237
|
+
if (month === 0 && date === 1 && hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) {
|
|
238
|
+
return 'year';
|
|
239
|
+
} else if (date === 1 && hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) {
|
|
240
|
+
return 'month';
|
|
241
|
+
} else if (weekDay === 1 && hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) {
|
|
242
|
+
return 'week';
|
|
243
|
+
} else if (hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) {
|
|
244
|
+
return 'day';
|
|
245
|
+
} else if (minutes === 0 && seconds === 0 && milliseconds === 0) {
|
|
246
|
+
return 'hour';
|
|
247
|
+
} else if (seconds === 0 && milliseconds === 0) {
|
|
248
|
+
return 'minute';
|
|
249
|
+
} else if (milliseconds === 0) {
|
|
250
|
+
return 'second';
|
|
251
|
+
}
|
|
252
|
+
return 'second'; // TODO return 'millisecond';
|
|
253
|
+
};
|
|
254
|
+
|
|
38
255
|
function removeEmptyQueryFields(_query) {
|
|
39
256
|
const query = _query || {};
|
|
40
257
|
return fromPairs(toPairs(query).map(([key, value]) => {
|
|
@@ -291,26 +508,6 @@ function aliasSeries(values, index, pivotConfig, duplicateMeasures) {
|
|
|
291
508
|
return nonNullValues;
|
|
292
509
|
}
|
|
293
510
|
|
|
294
|
-
dayjs.extend(quarterOfYear);
|
|
295
|
-
|
|
296
|
-
// When granularity is week, weekStart Value must be 1. However, since the client can change it globally (https://day.js.org/docs/en/i18n/changing-locale)
|
|
297
|
-
// So the function below has been added.
|
|
298
|
-
const internalDayjs = (...args) => dayjs(...args).locale({
|
|
299
|
-
...en,
|
|
300
|
-
weekStart: 1
|
|
301
|
-
});
|
|
302
|
-
const TIME_SERIES = {
|
|
303
|
-
day: range => range.by('d').map(d => d.format('YYYY-MM-DDT00:00:00.000')),
|
|
304
|
-
month: range => range.snapTo('month').by('M').map(d => d.format('YYYY-MM-01T00:00:00.000')),
|
|
305
|
-
year: range => range.snapTo('year').by('y').map(d => d.format('YYYY-01-01T00:00:00.000')),
|
|
306
|
-
hour: range => range.by('h').map(d => d.format('YYYY-MM-DDTHH:00:00.000')),
|
|
307
|
-
minute: range => range.by('m').map(d => d.format('YYYY-MM-DDTHH:mm:00.000')),
|
|
308
|
-
second: range => range.by('s').map(d => d.format('YYYY-MM-DDTHH:mm:ss.000')),
|
|
309
|
-
week: range => range.snapTo('week').by('w').map(d => d.startOf('week').format('YYYY-MM-DDT00:00:00.000')),
|
|
310
|
-
quarter: range => range.snapTo('quarter').by('quarter').map(d => d.startOf('quarter').format('YYYY-MM-DDT00:00:00.000'))
|
|
311
|
-
};
|
|
312
|
-
const DateRegex = /^\d\d\d\d-\d\d-\d\d$/;
|
|
313
|
-
const LocalDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z?$/;
|
|
314
511
|
const groupByToPairs = keyFn => {
|
|
315
512
|
const acc = new Map();
|
|
316
513
|
return data => {
|
|
@@ -331,21 +528,6 @@ const unnest = arr => {
|
|
|
331
528
|
});
|
|
332
529
|
return res;
|
|
333
530
|
};
|
|
334
|
-
const dayRange = (from, to) => ({
|
|
335
|
-
by: value => {
|
|
336
|
-
const results = [];
|
|
337
|
-
let start = internalDayjs(from);
|
|
338
|
-
const end = internalDayjs(to);
|
|
339
|
-
while (start.isBefore(end) || start.isSame(end)) {
|
|
340
|
-
results.push(start);
|
|
341
|
-
start = start.add(1, value);
|
|
342
|
-
}
|
|
343
|
-
return results;
|
|
344
|
-
},
|
|
345
|
-
snapTo: value => dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value)),
|
|
346
|
-
start: internalDayjs(from),
|
|
347
|
-
end: internalDayjs(to)
|
|
348
|
-
});
|
|
349
531
|
const QUERY_TYPE = {
|
|
350
532
|
REGULAR_QUERY: 'regularQuery',
|
|
351
533
|
COMPARE_DATE_RANGE_QUERY: 'compareDateRangeQuery',
|
|
@@ -544,7 +726,7 @@ class ResultSet {
|
|
|
544
726
|
normalizePivotConfig(pivotConfig) {
|
|
545
727
|
return ResultSet.getNormalizedPivotConfig(this.loadResponse.pivotQuery, pivotConfig);
|
|
546
728
|
}
|
|
547
|
-
timeSeries(timeDimension, resultIndex) {
|
|
729
|
+
timeSeries(timeDimension, resultIndex, annotations) {
|
|
548
730
|
if (!timeDimension.granularity) {
|
|
549
731
|
return null;
|
|
550
732
|
}
|
|
@@ -562,10 +744,13 @@ class ResultSet {
|
|
|
562
744
|
const padToDay = timeDimension.dateRange ? timeDimension.dateRange.find(d => d.match(DateRegex)) : !['hour', 'minute', 'second'].includes(timeDimension.granularity);
|
|
563
745
|
const [start, end] = dateRange;
|
|
564
746
|
const range = dayRange(start, end);
|
|
565
|
-
if (
|
|
566
|
-
|
|
747
|
+
if (isPredefinedGranularity(timeDimension.granularity)) {
|
|
748
|
+
return TIME_SERIES[timeDimension.granularity](padToDay ? range.snapTo('d') : range);
|
|
749
|
+
}
|
|
750
|
+
if (!annotations[`${timeDimension.dimension}.${timeDimension.granularity}`]) {
|
|
751
|
+
throw new Error(`Granularity "${timeDimension.granularity}" not found in time dimension "${timeDimension.dimension}"`);
|
|
567
752
|
}
|
|
568
|
-
return
|
|
753
|
+
return timeSeriesFromCustomInterval(start, end, annotations[`${timeDimension.dimension}.${timeDimension.granularity}`].granularity);
|
|
569
754
|
}
|
|
570
755
|
pivot(pivotConfig) {
|
|
571
756
|
pivotConfig = this.normalizePivotConfig(pivotConfig);
|
|
@@ -578,7 +763,7 @@ class ResultSet {
|
|
|
578
763
|
}) => this.axisValuesString(xValues));
|
|
579
764
|
const measureValue = (row, measure) => row[measure] || 0;
|
|
580
765
|
if (pivotConfig.fillMissingDates && pivotConfig.x.length === 1 && equals(pivotConfig.x, (query.timeDimensions || []).filter(td => Boolean(td.granularity)).map(td => ResultSet.timeDimensionMember(td)))) {
|
|
581
|
-
const series = this.loadResponses.map(loadResponse => this.timeSeries(loadResponse.query.timeDimensions[0], resultIndex));
|
|
766
|
+
const series = this.loadResponses.map(loadResponse => this.timeSeries(loadResponse.query.timeDimensions[0], resultIndex, loadResponse.annotation.timeDimensions));
|
|
582
767
|
if (series[0]) {
|
|
583
768
|
groupByXAxis = rows => {
|
|
584
769
|
const byXValues = groupBy(({
|
|
@@ -1445,5 +1630,5 @@ class CubeApi {
|
|
|
1445
1630
|
var index = ((apiToken, options) => new CubeApi(apiToken, options));
|
|
1446
1631
|
|
|
1447
1632
|
export default index;
|
|
1448
|
-
export { CubeApi, DEFAULT_GRANULARITY, GRANULARITIES, HttpTransport, Meta, RequestError, ResultSet, aliasSeries, areQueriesEqual, defaultHeuristics, defaultOrder, flattenFilters, getOrderMembersFromOrder, getQueryMembers, isQueryPresent, moveItemInArray, movePivotItem, removeEmptyQueryFields, validateQuery };
|
|
1633
|
+
export { CubeApi, DEFAULT_GRANULARITY, DateRegex, GRANULARITIES, HttpTransport, LocalDateRegex, Meta, RequestError, ResultSet, TIME_SERIES, addInterval, aliasSeries, areQueriesEqual, dayRange, defaultHeuristics, defaultOrder, diffTimeUnitForInterval, flattenFilters, getOrderMembersFromOrder, getQueryMembers, granularityFor, internalDayjs, isPredefinedGranularity, isQueryPresent, minGranularityForIntervals, moveItemInArray, movePivotItem, parseSqlInterval, removeEmptyQueryFields, subtractInterval, timeSeriesFromCustomInterval, validateQuery };
|
|
1449
1634
|
//# sourceMappingURL=cubejs-client-core.esm.js.map
|