@cubejs-client/core 0.36.2 → 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 +6 -0
- 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
package/src/time.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import quarterOfYear from 'dayjs/plugin/quarterOfYear';
|
|
3
|
+
import duration from 'dayjs/plugin/duration';
|
|
4
|
+
import isoWeek from 'dayjs/plugin/isoWeek';
|
|
5
|
+
import en from 'dayjs/locale/en';
|
|
6
|
+
|
|
7
|
+
dayjs.extend(quarterOfYear);
|
|
8
|
+
dayjs.extend(duration);
|
|
9
|
+
dayjs.extend(isoWeek);
|
|
10
|
+
|
|
11
|
+
export const GRANULARITIES = [
|
|
12
|
+
{ name: undefined, title: 'w/o grouping' },
|
|
13
|
+
{ name: 'second', title: 'Second' },
|
|
14
|
+
{ name: 'minute', title: 'Minute' },
|
|
15
|
+
{ name: 'hour', title: 'Hour' },
|
|
16
|
+
{ name: 'day', title: 'Day' },
|
|
17
|
+
{ name: 'week', title: 'Week' },
|
|
18
|
+
{ name: 'month', title: 'Month' },
|
|
19
|
+
{ name: 'quarter', title: 'Quarter' },
|
|
20
|
+
{ name: 'year', title: 'Year' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_GRANULARITY = 'day';
|
|
24
|
+
|
|
25
|
+
// When granularity is week, weekStart Value must be 1. However, since the client can change it globally
|
|
26
|
+
// (https://day.js.org/docs/en/i18n/changing-locale) So the function below has been added.
|
|
27
|
+
export const internalDayjs = (...args) => dayjs(...args).locale({ ...en, weekStart: 1 });
|
|
28
|
+
|
|
29
|
+
export const TIME_SERIES = {
|
|
30
|
+
day: (range) => range.by('d').map(d => d.format('YYYY-MM-DDT00:00:00.000')),
|
|
31
|
+
month: (range) => range.snapTo('month').by('M').map(d => d.format('YYYY-MM-01T00:00:00.000')),
|
|
32
|
+
year: (range) => range.snapTo('year').by('y').map(d => d.format('YYYY-01-01T00:00:00.000')),
|
|
33
|
+
hour: (range) => range.by('h').map(d => d.format('YYYY-MM-DDTHH:00:00.000')),
|
|
34
|
+
minute: (range) => range.by('m').map(d => d.format('YYYY-MM-DDTHH:mm:00.000')),
|
|
35
|
+
second: (range) => range.by('s').map(d => d.format('YYYY-MM-DDTHH:mm:ss.000')),
|
|
36
|
+
week: (range) => range.snapTo('week').by('w').map(d => d.startOf('week').format('YYYY-MM-DDT00:00:00.000')),
|
|
37
|
+
quarter: (range) => range.snapTo('quarter').by('quarter').map(d => d.startOf('quarter').format(
|
|
38
|
+
'YYYY-MM-DDT00:00:00.000'
|
|
39
|
+
)),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const isPredefinedGranularity = (granularity) => !!TIME_SERIES[granularity];
|
|
43
|
+
|
|
44
|
+
export const DateRegex = /^\d\d\d\d-\d\d-\d\d$/;
|
|
45
|
+
export const LocalDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z?$/;
|
|
46
|
+
|
|
47
|
+
export const dayRange = (from, to) => ({
|
|
48
|
+
by: (value) => {
|
|
49
|
+
const results = [];
|
|
50
|
+
|
|
51
|
+
let start = internalDayjs(from);
|
|
52
|
+
const end = internalDayjs(to);
|
|
53
|
+
|
|
54
|
+
while (start.isBefore(end) || start.isSame(end)) {
|
|
55
|
+
results.push(start);
|
|
56
|
+
start = start.add(1, value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return results;
|
|
60
|
+
},
|
|
61
|
+
snapTo: (value) => dayRange(internalDayjs(from).startOf(value), internalDayjs(to).endOf(value)),
|
|
62
|
+
start: internalDayjs(from),
|
|
63
|
+
end: internalDayjs(to),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse PostgreSQL-like interval string into object
|
|
68
|
+
* E.g. '2 years 15 months 100 weeks 99 hours 15 seconds'
|
|
69
|
+
* Negative units are also supported
|
|
70
|
+
* E.g. '-2 months 5 days -10 hours'
|
|
71
|
+
*
|
|
72
|
+
* TODO: It's copy/paste of parseSqlInterval from @cubejs-backend/shared [time.ts]
|
|
73
|
+
* It's not referenced to omit imports of moment.js staff.
|
|
74
|
+
* Probably one day we should choose one implementation and reuse it in other places.
|
|
75
|
+
*/
|
|
76
|
+
export function parseSqlInterval(intervalStr) {
|
|
77
|
+
const interval = {};
|
|
78
|
+
const parts = intervalStr.split(/\s+/);
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
81
|
+
const value = parseInt(parts[i], 10);
|
|
82
|
+
const unit = parts[i + 1];
|
|
83
|
+
|
|
84
|
+
// Remove ending 's' (e.g., 'days' -> 'day')
|
|
85
|
+
const singularUnit = unit.endsWith('s') ? unit.slice(0, -1) : unit;
|
|
86
|
+
interval[singularUnit] = value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return interval;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Adds interval to provided date.
|
|
94
|
+
* TODO: It's copy/paste of addInterval from @cubejs-backend/shared [time.ts]
|
|
95
|
+
* but operates with dayjs instead of moment.js
|
|
96
|
+
* @param {dayjs} date
|
|
97
|
+
* @param interval
|
|
98
|
+
* @returns {dayjs}
|
|
99
|
+
*/
|
|
100
|
+
export function addInterval(date, interval) {
|
|
101
|
+
let res = date.clone();
|
|
102
|
+
|
|
103
|
+
Object.entries(interval).forEach(([key, value]) => {
|
|
104
|
+
res = res.add(value, key);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return res;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Adds interval to provided date.
|
|
112
|
+
* TODO: It's copy/paste of subtractInterval from @cubejs-backend/shared [time.ts]
|
|
113
|
+
* but operates with dayjs instead of moment.js
|
|
114
|
+
* @param {dayjs} date
|
|
115
|
+
* @param interval
|
|
116
|
+
* @returns {dayjs}
|
|
117
|
+
*/
|
|
118
|
+
export function subtractInterval(date, interval) {
|
|
119
|
+
let res = date.clone();
|
|
120
|
+
|
|
121
|
+
Object.entries(interval).forEach(([key, value]) => {
|
|
122
|
+
res = res.subtract(value, key);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return res;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Returns the closest date prior to date parameter aligned with the origin point
|
|
130
|
+
* TODO: It's copy/paste of alignToOrigin from @cubejs-backend/shared [time.ts]
|
|
131
|
+
* but operates with dayjs instead of moment.js
|
|
132
|
+
*/
|
|
133
|
+
function alignToOrigin(startDate, interval, origin) {
|
|
134
|
+
let alignedDate = startDate.clone();
|
|
135
|
+
let intervalOp;
|
|
136
|
+
let isIntervalNegative = false;
|
|
137
|
+
|
|
138
|
+
let offsetDate = addInterval(origin, interval);
|
|
139
|
+
|
|
140
|
+
// The easiest way to check the interval sign
|
|
141
|
+
if (offsetDate.isBefore(origin)) {
|
|
142
|
+
isIntervalNegative = true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
offsetDate = origin.clone();
|
|
146
|
+
|
|
147
|
+
if (startDate.isBefore(origin)) {
|
|
148
|
+
intervalOp = isIntervalNegative ? addInterval : subtractInterval;
|
|
149
|
+
|
|
150
|
+
while (offsetDate.isAfter(startDate)) {
|
|
151
|
+
offsetDate = intervalOp(offsetDate, interval);
|
|
152
|
+
}
|
|
153
|
+
alignedDate = offsetDate;
|
|
154
|
+
} else {
|
|
155
|
+
intervalOp = isIntervalNegative ? subtractInterval : addInterval;
|
|
156
|
+
|
|
157
|
+
while (offsetDate.isBefore(startDate)) {
|
|
158
|
+
alignedDate = offsetDate.clone();
|
|
159
|
+
offsetDate = intervalOp(offsetDate, interval);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (offsetDate.isSame(startDate)) {
|
|
163
|
+
alignedDate = offsetDate;
|
|
164
|
+
}
|
|
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
|
+
export 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
|
+
|
|
185
|
+
const dates = [];
|
|
186
|
+
|
|
187
|
+
while (alignedStart.isBefore(end) || alignedStart.isSame(end)) {
|
|
188
|
+
dates.push(alignedStart.format('YYYY-MM-DDTHH:mm:ss.000'));
|
|
189
|
+
alignedStart = addInterval(alignedStart, intervalParsed);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return dates;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns the lowest time unit for the interval
|
|
197
|
+
* @protected
|
|
198
|
+
* @param {string} interval
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
export const diffTimeUnitForInterval = (interval) => {
|
|
202
|
+
if (/second/i.test(interval)) {
|
|
203
|
+
return 'second';
|
|
204
|
+
} else if (/minute/i.test(interval)) {
|
|
205
|
+
return 'minute';
|
|
206
|
+
} else if (/hour/i.test(interval)) {
|
|
207
|
+
return 'hour';
|
|
208
|
+
} else if (/day/i.test(interval)) {
|
|
209
|
+
return 'day';
|
|
210
|
+
} else if (/week/i.test(interval)) {
|
|
211
|
+
return 'day';
|
|
212
|
+
} else if (/month/i.test(interval)) {
|
|
213
|
+
return 'month';
|
|
214
|
+
} else if (/quarter/i.test(interval)) {
|
|
215
|
+
return 'month';
|
|
216
|
+
} else /* if (/year/i.test(interval)) */ {
|
|
217
|
+
return 'year';
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const granularityOrder = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
|
222
|
+
|
|
223
|
+
export const minGranularityForIntervals = (i1, i2) => {
|
|
224
|
+
const g1 = diffTimeUnitForInterval(i1);
|
|
225
|
+
const g2 = diffTimeUnitForInterval(i2);
|
|
226
|
+
const g1pos = granularityOrder.indexOf(g1);
|
|
227
|
+
const g2pos = granularityOrder.indexOf(g2);
|
|
228
|
+
|
|
229
|
+
if (g1pos > g2pos) {
|
|
230
|
+
return g1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return g2;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const granularityFor = (dateStr) => {
|
|
237
|
+
const dayjsDate = internalDayjs(dateStr);
|
|
238
|
+
const month = dayjsDate.month();
|
|
239
|
+
const date = dayjsDate.date();
|
|
240
|
+
const hours = dayjsDate.hour();
|
|
241
|
+
const minutes = dayjsDate.minute();
|
|
242
|
+
const seconds = dayjsDate.second();
|
|
243
|
+
const milliseconds = dayjsDate.millisecond();
|
|
244
|
+
const weekDay = dayjsDate.isoWeekday();
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
month === 0 &&
|
|
248
|
+
date === 1 &&
|
|
249
|
+
hours === 0 &&
|
|
250
|
+
minutes === 0 &&
|
|
251
|
+
seconds === 0 &&
|
|
252
|
+
milliseconds === 0
|
|
253
|
+
) {
|
|
254
|
+
return 'year';
|
|
255
|
+
} else if (
|
|
256
|
+
date === 1 &&
|
|
257
|
+
hours === 0 &&
|
|
258
|
+
minutes === 0 &&
|
|
259
|
+
seconds === 0 &&
|
|
260
|
+
milliseconds === 0
|
|
261
|
+
) {
|
|
262
|
+
return 'month';
|
|
263
|
+
} else if (
|
|
264
|
+
weekDay === 1 &&
|
|
265
|
+
hours === 0 &&
|
|
266
|
+
minutes === 0 &&
|
|
267
|
+
seconds === 0 &&
|
|
268
|
+
milliseconds === 0
|
|
269
|
+
) {
|
|
270
|
+
return 'week';
|
|
271
|
+
} else if (
|
|
272
|
+
hours === 0 &&
|
|
273
|
+
minutes === 0 &&
|
|
274
|
+
seconds === 0 &&
|
|
275
|
+
milliseconds === 0
|
|
276
|
+
) {
|
|
277
|
+
return 'day';
|
|
278
|
+
} else if (
|
|
279
|
+
minutes === 0 &&
|
|
280
|
+
seconds === 0 &&
|
|
281
|
+
milliseconds === 0
|
|
282
|
+
) {
|
|
283
|
+
return 'hour';
|
|
284
|
+
} else if (
|
|
285
|
+
seconds === 0 &&
|
|
286
|
+
milliseconds === 0
|
|
287
|
+
) {
|
|
288
|
+
return 'minute';
|
|
289
|
+
} else if (
|
|
290
|
+
milliseconds === 0
|
|
291
|
+
) {
|
|
292
|
+
return 'second';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return 'second'; // TODO return 'millisecond';
|
|
296
|
+
};
|
package/src/utils.js
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const DEFAULT_GRANULARITY = 'day';
|
|
4
|
-
|
|
5
|
-
export const GRANULARITIES = [
|
|
6
|
-
{ name: undefined, title: 'w/o grouping' },
|
|
7
|
-
{ name: 'second', title: 'Second' },
|
|
8
|
-
{ name: 'minute', title: 'Minute' },
|
|
9
|
-
{ name: 'hour', title: 'Hour' },
|
|
10
|
-
{ name: 'day', title: 'Day' },
|
|
11
|
-
{ name: 'week', title: 'Week' },
|
|
12
|
-
{ name: 'month', title: 'Month' },
|
|
13
|
-
{ name: 'quarter', title: 'Quarter' },
|
|
14
|
-
{ name: 'year', title: 'Year' },
|
|
15
|
-
];
|
|
1
|
+
import { clone, equals, fromPairs, indexBy, prop, toPairs } from 'ramda';
|
|
2
|
+
import { DEFAULT_GRANULARITY } from './time';
|
|
16
3
|
|
|
17
4
|
export function removeEmptyQueryFields(_query) {
|
|
18
5
|
const query = _query || {};
|
|
19
|
-
|
|
6
|
+
|
|
20
7
|
return fromPairs(
|
|
21
8
|
toPairs(query)
|
|
22
9
|
.map(([key, value]) => {
|
|
@@ -27,7 +14,7 @@ export function removeEmptyQueryFields(_query) {
|
|
|
27
14
|
return null;
|
|
28
15
|
}
|
|
29
16
|
}
|
|
30
|
-
|
|
17
|
+
|
|
31
18
|
if (key === 'order' && value) {
|
|
32
19
|
if (Array.isArray(value) && !value.length) {
|
|
33
20
|
return null;
|
|
@@ -44,7 +31,7 @@ export function removeEmptyQueryFields(_query) {
|
|
|
44
31
|
|
|
45
32
|
export function validateQuery(_query) {
|
|
46
33
|
const query = _query || {};
|
|
47
|
-
|
|
34
|
+
|
|
48
35
|
return removeEmptyQueryFields({
|
|
49
36
|
...query,
|
|
50
37
|
filters: (query.filters || []).filter((f) => f.operator),
|