@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.
@@ -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
- const DEFAULT_GRANULARITY = 'day';
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 (!TIME_SERIES[timeDimension.granularity]) {
566
- throw new Error(`Unsupported time granularity: ${timeDimension.granularity}`);
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 TIME_SERIES[timeDimension.granularity](padToDay ? range.snapTo('d') : range);
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