Logs.utils.test.ts325 lines · main
1import dayjs from 'dayjs'
2import utc from 'dayjs/plugin/utc'
3import { describe, expect, test } from 'vitest'
4
5import {
6 checkForILIKEClause,
7 checkForWildcard,
8 checkForWithClause,
9 fillTimeseries,
10} from '@/components/interfaces/Settings/Logs/Logs.utils'
11
12dayjs.extend(utc)
13
14describe('fillTimeseries', () => {
15 test('should return empty array for empty data without min/max', () => {
16 const result = fillTimeseries([], 'timestamp', 'value', 0)
17 expect(result).toEqual([])
18 })
19
20 test('should return empty array for empty data with min/max', () => {
21 const min = '2023-01-01T00:00:00.000Z'
22 const max = '2023-01-01T01:00:00.000Z'
23 const result = fillTimeseries([], 'timestamp', 'value', 0, min, max)
24
25 // When min/max are provided, the function fills the time range with default values
26 // This creates 61 data points (one for each minute from 00:00 to 01:00)
27 expect(result).toHaveLength(61)
28 expect(result[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', value: 0 })
29 expect(result[60]).toEqual({ timestamp: '2023-01-01T01:00:00.000Z', value: 0 })
30 })
31
32 test('should normalize timestamps when data exceeds minPointsToFill', () => {
33 const data = [
34 { timestamp: '2023-01-01T00:00:00.000Z', value: 1 },
35 { timestamp: '2023-01-01T00:01:00.000Z', value: 2 },
36 { timestamp: '2023-01-01T00:02:00.000Z', value: 3 },
37 { timestamp: '2023-01-01T00:03:00.000Z', value: 4 },
38 { timestamp: '2023-01-01T00:04:00.000Z', value: 5 },
39 { timestamp: '2023-01-01T00:05:00.000Z', value: 6 },
40 { timestamp: '2023-01-01T00:06:00.000Z', value: 7 },
41 { timestamp: '2023-01-01T00:07:00.000Z', value: 8 },
42 { timestamp: '2023-01-01T00:08:00.000Z', value: 9 },
43 { timestamp: '2023-01-01T00:09:00.000Z', value: 10 },
44 { timestamp: '2023-01-01T00:10:00.000Z', value: 11 },
45 { timestamp: '2023-01-01T00:11:00.000Z', value: 12 },
46 { timestamp: '2023-01-01T00:12:00.000Z', value: 13 },
47 { timestamp: '2023-01-01T00:13:00.000Z', value: 14 },
48 { timestamp: '2023-01-01T00:14:00.000Z', value: 15 },
49 { timestamp: '2023-01-01T00:15:00.000Z', value: 16 },
50 { timestamp: '2023-01-01T00:16:00.000Z', value: 17 },
51 { timestamp: '2023-01-01T00:17:00.000Z', value: 18 },
52 { timestamp: '2023-01-01T00:18:00.000Z', value: 19 },
53 { timestamp: '2023-01-01T00:19:00.000Z', value: 20 },
54 { timestamp: '2023-01-01T00:20:00.000Z', value: 21 },
55 ]
56 const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 20)
57
58 // Should return normalized data without filling gaps
59 expect(result).toHaveLength(21)
60 result.forEach((item) => {
61 expect(item.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
62 })
63 })
64
65 test('should fill gaps in sparse data with 1-minute intervals', () => {
66 const min = '2023-01-01T00:00:00.000Z'
67 const max = '2023-01-01T00:04:00.000Z'
68 const data = [
69 { timestamp: '2023-01-01T00:01:00.000Z', value: 10 },
70 { timestamp: '2023-01-01T00:03:00.000Z', value: 30 },
71 ]
72
73 const result = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '2m')
74
75 expect(result).toHaveLength(5)
76 const sortedResult = result.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
77
78 expect(sortedResult[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', value: 0 })
79 expect(sortedResult[1]).toEqual({ timestamp: '2023-01-01T00:01:00.000Z', value: 10 })
80 expect(sortedResult[2]).toEqual({ timestamp: '2023-01-01T00:02:00.000Z', value: 0 })
81 expect(sortedResult[3]).toEqual({ timestamp: '2023-01-01T00:03:00.000Z', value: 30 })
82 expect(sortedResult[4]).toEqual({ timestamp: '2023-01-01T00:04:00.000Z', value: 0 })
83 })
84
85 test('should handle multiple value keys', () => {
86 const min = '2023-01-01T00:00:00.000Z'
87 const max = '2023-01-01T00:02:00.000Z'
88 const data = [{ timestamp: '2023-01-01T00:01:00.000Z', count1: 10, count2: 100 }]
89
90 const result = fillTimeseries(data, 'timestamp', ['count1', 'count2'], 5, min, max, 20, '1m')
91
92 expect(result).toHaveLength(3)
93 const sortedResult = result.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
94
95 expect(sortedResult[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', count1: 5, count2: 5 })
96 expect(sortedResult[1]).toEqual({
97 timestamp: '2023-01-01T00:01:00.000Z',
98 count1: 10,
99 count2: 100,
100 })
101 expect(sortedResult[2]).toEqual({ timestamp: '2023-01-01T00:02:00.000Z', count1: 5, count2: 5 })
102 })
103
104 test('should handle different interval formats', () => {
105 const min = '2023-01-01T00:00:00.000Z'
106 const max = '2023-01-01T00:10:00.000Z'
107 const data = [{ timestamp: '2023-01-01T00:05:00.000Z', value: 50 }]
108
109 // Test 5-minute intervals: 00:00, 00:05, 00:10 = 3 points
110 const result5m = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '5m')
111 expect(result5m).toHaveLength(3)
112
113 // Test 2-minute intervals: 00:00, 00:02, 00:04, 00:05, 00:06, 00:08, 00:10 = 7 points
114 const result2m = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '2m')
115 console.log(
116 '2m intervals:',
117 result2m.map((r) => r.timestamp)
118 )
119 expect(result2m).toHaveLength(7)
120
121 // Test 1-hour intervals: 00:00, 01:00, 02:00 with existing data at 00:05 = 4 points
122 const maxHour = '2023-01-01T02:00:00.000Z'
123 const result1h = fillTimeseries(data, 'timestamp', 'value', 0, min, maxHour, 20, '1h')
124 expect(result1h).toHaveLength(4)
125 })
126
127 test('should handle microsecond timestamps correctly', () => {
128 const now = dayjs.utc('2023-01-01T00:00:00.000Z')
129 const data = [
130 { timestamp: now.valueOf() * 1000, value: 1 },
131 { timestamp: now.add(1, 'minute').valueOf() * 1000, value: 2 },
132 ]
133
134 const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 1)
135
136 expect(result).toHaveLength(2)
137 result.forEach((item) => {
138 expect(item.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
139 })
140 })
141
142 test('should handle mixed timestamp formats', () => {
143 const data = [
144 { timestamp: '2023-01-01T00:00:00.000Z', value: 1 },
145 { timestamp: dayjs.utc('2023-01-01T00:01:00.000Z').valueOf() * 1000, value: 2 },
146 { timestamp: '2023-01-01T00:02:00.000Z', value: 3 },
147 ]
148
149 const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 1)
150
151 expect(result).toHaveLength(3)
152 result.forEach((item) => {
153 expect(item.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
154 })
155 })
156
157 test('should not fill gaps when data is dense enough', () => {
158 const data = Array.from({ length: 25 }, (_, i) => ({
159 timestamp: dayjs.utc('2023-01-01T00:00:00.000Z').add(i, 'minute').toISOString(),
160 value: i,
161 }))
162
163 const result = fillTimeseries(data, 'timestamp', 'value', 0, undefined, undefined, 20)
164
165 expect(result).toHaveLength(25)
166 expect(result).toEqual(data)
167 })
168
169 test('should handle edge case with single data point', () => {
170 const data = [{ timestamp: '2023-01-01T00:00:00.000Z', value: 1 }]
171
172 const result = fillTimeseries(data, 'timestamp', 'value', 0)
173
174 expect(result).toEqual(data)
175 })
176
177 test('should handle edge case with single data point and min/max', () => {
178 const min = '2023-01-01T00:00:00.000Z'
179 const max = '2023-01-01T00:02:00.000Z'
180 const data = [{ timestamp: '2023-01-01T00:01:00.000Z', value: 1 }]
181
182 const result = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, '1m')
183
184 expect(result).toHaveLength(3)
185 const sortedResult = result.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
186
187 expect(sortedResult[0]).toEqual({ timestamp: '2023-01-01T00:00:00.000Z', value: 0 })
188 expect(sortedResult[1]).toEqual({ timestamp: '2023-01-01T00:01:00.000Z', value: 1 })
189 expect(sortedResult[2]).toEqual({ timestamp: '2023-01-01T00:02:00.000Z', value: 0 })
190 })
191
192 test('should handle invalid interval format gracefully', () => {
193 const min = '2023-01-01T00:00:00.000Z'
194 const max = '2023-01-01T00:02:00.000Z'
195 const data = [{ timestamp: '2023-01-01T00:01:00.000Z', value: 1 }]
196
197 const result = fillTimeseries(data, 'timestamp', 'value', 0, min, max, 20, 'invalid')
198
199 // Should fall back to default behavior
200 expect(result.length).toBeGreaterThan(1)
201 })
202
203 test('should preserve all properties of original data', () => {
204 const data = [
205 {
206 timestamp: '2023-01-01T00:00:00.000Z',
207 value: 1,
208 extra: 'data',
209 nested: { prop: 'value' },
210 },
211 ]
212
213 const result = fillTimeseries(data, 'timestamp', 'value', 0)
214
215 expect(result[0]).toEqual(data[0])
216 expect(result[0].extra).toBe('data')
217 expect(result[0].nested).toEqual({ prop: 'value' })
218 })
219
220 test('should handle empty value keys array', () => {
221 const min = '2023-01-01T00:00:00.000Z'
222 const max = '2023-01-01T00:01:00.000Z'
223 const data = [{ timestamp: '2023-01-01T00:00:30.000Z', value: 1 }]
224
225 const result = fillTimeseries(data, 'timestamp', [], 0, min, max, 20, '30s')
226
227 expect(result).toHaveLength(3)
228 result.forEach((item) => {
229 expect(item).toHaveProperty('timestamp')
230 expect(item).not.toHaveProperty('value')
231 })
232 })
233})
234
235describe('checkForWithClause', () => {
236 test('basic queries', () => {
237 expect(checkForWithClause('SELECT * FROM table')).toBe(false)
238 expect(checkForWithClause('SELECT * FROM table WITH clause')).toBe(true)
239 expect(checkForWithClause('WITH test AS (SELECT * FROM table) SELECT * FROM test')).toBe(true)
240 expect(checkForWithClause('SELECT * FROM withsomething')).toBe(false)
241 })
242
243 test('case sensitivity', () => {
244 expect(checkForWithClause('with test AS (SELECT * FROM table) SELECT * FROM test')).toBe(true)
245 expect(checkForWithClause('WiTh test AS (SELECT * FROM table) SELECT * FROM test')).toBe(true)
246 })
247
248 test('comments', () => {
249 expect(checkForWithClause('SELECT * FROM table -- WITH clause')).toBe(false)
250 expect(checkForWithClause('SELECT * FROM table /* WITH clause */')).toBe(false)
251 expect(checkForWithClause('-- WITH clause\nSELECT * FROM table')).toBe(false)
252 expect(checkForWithClause('/* WITH clause */\nSELECT * FROM table')).toBe(false)
253 })
254
255 test('string literals', () => {
256 expect(checkForWithClause("SELECT 'WITH' FROM table")).toBe(false)
257 expect(checkForWithClause("SELECT * FROM table WHERE column = 'WITH clause'")).toBe(false)
258 })
259
260 test('subqueries', () => {
261 expect(
262 checkForWithClause('SELECT * FROM (WITH subquery AS (SELECT 1) SELECT * FROM subquery)')
263 ).toBe(true)
264 expect(
265 checkForWithClause(
266 'SELECT * FROM table WHERE column IN (WITH subquery AS (SELECT 1) SELECT * FROM subquery)'
267 )
268 ).toBe(true)
269 })
270})
271
272describe('checkForILIKEClause', () => {
273 test('basic queries', () => {
274 expect(checkForILIKEClause('SELECT * FROM table')).toBe(false)
275 expect(checkForILIKEClause('SELECT * FROM table WHERE column ILIKE "%value%"')).toBe(true)
276 expect(checkForILIKEClause('SELECT * FROM table WHERE column LIKE "%value%"')).toBe(false)
277 expect(checkForILIKEClause('SELECT * FROM ilikesomething')).toBe(false)
278 })
279
280 test('case sensitivity', () => {
281 expect(checkForILIKEClause('SELECT * FROM table WHERE column ilike "%value%"')).toBe(true)
282 expect(checkForILIKEClause('SELECT * FROM table WHERE column IlIkE "%value%"')).toBe(true)
283 })
284
285 test('comments', () => {
286 expect(checkForILIKEClause('SELECT * FROM table -- ILIKE clause')).toBe(false)
287 expect(checkForILIKEClause('SELECT * FROM table /* ILIKE clause */')).toBe(false)
288 expect(checkForILIKEClause('-- ILIKE clause\nSELECT * FROM table')).toBe(false)
289 expect(checkForILIKEClause('/* ILIKE clause */\nSELECT * FROM table')).toBe(false)
290 })
291
292 test('string literals', () => {
293 expect(checkForILIKEClause("SELECT 'ILIKE' FROM table")).toBe(false)
294 expect(checkForILIKEClause("SELECT * FROM table WHERE column = 'ILIKE clause'")).toBe(false)
295 })
296
297 test('subqueries', () => {
298 expect(
299 checkForILIKEClause('SELECT * FROM (SELECT * FROM table WHERE column ILIKE "%value%")')
300 ).toBe(true)
301 expect(
302 checkForILIKEClause(
303 'SELECT * FROM table WHERE column IN (SELECT * FROM subtable WHERE column ILIKE "%value%")'
304 )
305 ).toBe(true)
306 })
307})
308
309describe('checkForWildcard', () => {
310 test('basic queries', () => {
311 expect(checkForWildcard('SELECT * FROM table')).toBe(true)
312 expect(checkForWildcard('SELECT column FROM table')).toBe(false)
313 })
314
315 test('comments', () => {
316 expect(checkForWildcard('SELECT column FROM table -- *')).toBe(false)
317 expect(checkForWildcard('SELECT column FROM table /* * */')).toBe(false)
318 expect(checkForWildcard('-- *\nSELECT column FROM table')).toBe(false)
319 expect(checkForWildcard('/* * */\nSELECT column FROM table')).toBe(false)
320 })
321
322 test('count(*)', () => {
323 expect(checkForWildcard('SELECT count(*) FROM table')).toBe(false)
324 })
325})