Skip to content

Commit a35ebfe

Browse files
committed
Don't detect time fields from Unix epoch
Fixes #82
1 parent c5975d7 commit a35ebfe

File tree

5 files changed

+102
-105
lines changed

5 files changed

+102
-105
lines changed

src/datasource.ts

Lines changed: 2 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import _ from 'lodash';
2-
import { isValid, parseISO } from 'date-fns';
32
import { JSONPath } from 'jsonpath-plus';
43

54
import {
@@ -9,13 +8,14 @@ import {
98
DataSourceInstanceSettings,
109
toDataFrame,
1110
MetricFindValue,
12-
FieldType,
1311
ScopedVars,
1412
TimeRange,
1513
} from '@grafana/data';
1614
import { getTemplateSrv } from '@grafana/runtime';
1715

1816
import API from './api';
17+
import { detectFieldType } from './detectFieldType';
18+
import { parseValues } from './parseValues';
1919
import { JsonApiQuery, JsonApiDataSourceOptions, Pair } from './types';
2020

2121
export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
@@ -176,104 +176,3 @@ const replaceMacros = (str: string, range?: TimeRange) => {
176176
.replace(/\$__unixEpochTo\(\)/g, range.to.unix().toString())
177177
: str;
178178
};
179-
180-
/**
181-
* Detects the field type from an array of values.
182-
*/
183-
export const detectFieldType = (values: any[]): FieldType => {
184-
// If all values are null, default to strings.
185-
if (values.every((_) => _ === null)) {
186-
return FieldType.string;
187-
}
188-
189-
// If all values are valid ISO 8601, then assume that it's a time field.
190-
const isValidISO = values
191-
.filter((value) => value !== null)
192-
.every((value) => value.length >= 10 && isValid(parseISO(value)));
193-
if (isValidISO) {
194-
return FieldType.time;
195-
}
196-
197-
if (values.every((value) => typeof value === 'number')) {
198-
const uniqueLengths = Array.from(new Set(values.map((value) => Math.round(value).toString().length)));
199-
const hasSameLength = uniqueLengths.length === 1;
200-
201-
// If all the values have the same length of either 10 (seconds) or 13
202-
// (milliseconds), assume it's a time field. This is not always true, so we
203-
// might need to add an option to disable detection of time fields.
204-
if (hasSameLength) {
205-
if (uniqueLengths[0] === 13) {
206-
return FieldType.time;
207-
}
208-
if (uniqueLengths[0] === 10) {
209-
return FieldType.time;
210-
}
211-
}
212-
213-
return FieldType.number;
214-
}
215-
216-
if (values.every((value) => typeof value === 'boolean')) {
217-
return FieldType.boolean;
218-
}
219-
220-
return FieldType.string;
221-
};
222-
223-
/**
224-
* parseValues converts values to the given field type.
225-
*/
226-
export const parseValues = (values: any[], type: FieldType): any[] => {
227-
switch (type) {
228-
case FieldType.time:
229-
// For time field, values are expected to be numbers representing a Unix
230-
// epoch in milliseconds.
231-
232-
if (values.filter((_) => _).every((value) => typeof value === 'string')) {
233-
return values.map((_) => (_ !== null ? parseISO(_).valueOf() : _));
234-
}
235-
236-
if (values.filter((_) => _).every((value) => typeof value === 'number')) {
237-
const ms = 1_000_000_000_000;
238-
239-
// If there are no "big" numbers, assume seconds.
240-
if (values.filter((_) => _).every((_) => _ < ms)) {
241-
return values.map((_) => (_ !== null ? _ * 1000.0 : _));
242-
}
243-
244-
// ... otherwise assume milliseconds.
245-
return values;
246-
}
247-
248-
throw new Error('Unsupported time property');
249-
case FieldType.string:
250-
return values.every((_) => typeof _ === 'string') ? values : values.map((_) => (_ !== null ? _.toString() : _));
251-
case FieldType.number:
252-
return values.every((_) => typeof _ === 'number') ? values : values.map((_) => (_ !== null ? parseFloat(_) : _));
253-
case FieldType.boolean:
254-
return values.every((_) => typeof _ === 'boolean')
255-
? values
256-
: values.map((_) => {
257-
if (_ === null) {
258-
return _;
259-
}
260-
261-
switch (_.toString()) {
262-
case '0':
263-
case 'false':
264-
case 'FALSE':
265-
case 'False':
266-
return false;
267-
case '1':
268-
case 'true':
269-
case 'TRUE':
270-
case 'True':
271-
return true;
272-
default:
273-
throw new Error('Found non-boolean values in a field of type boolean: ' + _.toString());
274-
}
275-
});
276-
default:
277-
throw new Error('Unsupported field type');
278-
}
279-
};

src/detectFieldType.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { detectFieldType } from './datasource';
1+
import { detectFieldType } from './detectFieldType';
22

33
test('years and months gets parsed as string to reduce false positives', () => {
44
expect(detectFieldType(['2005', '2006'])).toStrictEqual('string');
@@ -9,6 +9,14 @@ test('iso8601 date without time zone gets parsed as time', () => {
99
expect(detectFieldType(['2005-01-02', '2006-01-02'])).toStrictEqual('time');
1010
});
1111

12+
test('unix epoch in seconds gets parsed as number', () => {
13+
expect(detectFieldType([1617774880])).toStrictEqual('number');
14+
});
15+
16+
test('unix epoch in milliseconds gets parsed as number', () => {
17+
expect(detectFieldType([1617774880000])).toStrictEqual('number');
18+
});
19+
1220
test('iso8601 gets parsed as time', () => {
1321
expect(detectFieldType(['2006-01-02T15:06:13Z', '2006-01-02T15:07:13Z'])).toStrictEqual('time');
1422
});

src/detectFieldType.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { isValid, parseISO } from 'date-fns';
2+
import { FieldType } from '@grafana/data';
3+
4+
/**
5+
* Detects the field type from an array of values.
6+
*/
7+
export const detectFieldType = (values: any[]): FieldType => {
8+
// If all values are null, default to strings.
9+
if (values.every((_) => _ === null)) {
10+
return FieldType.string;
11+
}
12+
13+
// If all values are valid ISO 8601, then assume that it's a time field.
14+
const isValidISO = values
15+
.filter((value) => value !== null)
16+
.every((value) => value.length >= 10 && isValid(parseISO(value)));
17+
if (isValidISO) {
18+
return FieldType.time;
19+
}
20+
21+
if (values.every((value) => typeof value === 'number')) {
22+
return FieldType.number;
23+
}
24+
25+
if (values.every((value) => typeof value === 'boolean')) {
26+
return FieldType.boolean;
27+
}
28+
29+
return FieldType.string;
30+
};

src/parseValues.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FieldType } from '@grafana/data';
2-
import { parseValues } from './datasource';
2+
import { parseValues } from './parseValues';
33

44
test('parse numbers', () => {
55
const values = [2005, 2006];

src/parseValues.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { parseISO } from 'date-fns';
2+
import { FieldType } from '@grafana/data';
3+
4+
/**
5+
* parseValues converts values to the given field type.
6+
*/
7+
export const parseValues = (values: any[], type: FieldType): any[] => {
8+
switch (type) {
9+
case FieldType.time:
10+
// For time field, values are expected to be numbers representing a Unix
11+
// epoch in milliseconds.
12+
13+
if (values.filter((_) => _).every((value) => typeof value === 'string')) {
14+
return values.map((_) => (_ !== null ? parseISO(_).valueOf() : _));
15+
}
16+
17+
if (values.filter((_) => _).every((value) => typeof value === 'number')) {
18+
const ms = 1_000_000_000_000;
19+
20+
// If there are no "big" numbers, assume seconds.
21+
if (values.filter((_) => _).every((_) => _ < ms)) {
22+
return values.map((_) => (_ !== null ? _ * 1000.0 : _));
23+
}
24+
25+
// ... otherwise assume milliseconds.
26+
return values;
27+
}
28+
29+
throw new Error('Unsupported time property');
30+
case FieldType.string:
31+
return values.every((_) => typeof _ === 'string') ? values : values.map((_) => (_ !== null ? _.toString() : _));
32+
case FieldType.number:
33+
return values.every((_) => typeof _ === 'number') ? values : values.map((_) => (_ !== null ? parseFloat(_) : _));
34+
case FieldType.boolean:
35+
return values.every((_) => typeof _ === 'boolean')
36+
? values
37+
: values.map((_) => {
38+
if (_ === null) {
39+
return _;
40+
}
41+
42+
switch (_.toString()) {
43+
case '0':
44+
case 'false':
45+
case 'FALSE':
46+
case 'False':
47+
return false;
48+
case '1':
49+
case 'true':
50+
case 'TRUE':
51+
case 'True':
52+
return true;
53+
default:
54+
throw new Error('Found non-boolean values in a field of type boolean: ' + _.toString());
55+
}
56+
});
57+
default:
58+
throw new Error('Unsupported field type');
59+
}
60+
};

0 commit comments

Comments
 (0)