mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature (+ a small refactor and a docs/convention note). PR 4 of the stacked SQL-editor query-source series (Database vs Logs). ## What is the current behavior? The SQL editor has no representation of a logs query's time range: `querySource.ts` only knows how to map a snippet type to a source (`getSnippetSource`), and session state (`sql-editor-session-state.ts`) tracks results and the row limit but not a per-snippet time range. The Logs date picker's pure range helpers (`parseCustomInput`, `generateDynamicHelper`, the `Unit` type) are trapped inside the `Logs.DatePickers.tsx` React component. ## What is the new behavior? - **Logs time-range domain** in `querySource.ts`: branded `IsoDateTimeString` + `isoDateTimeString()`, `RelativeTimeUnit`, a `LogDateRange` discriminated union (relative/absolute), `DEFAULT_LOG_DATE_RANGE`, a single date-picker parser (`datePickerValueToLogDateRange` / `logDateRangeToDatePickerValue` — handles the five presets *and* dynamic `2h`/`30m` helpers; `calcTo === ''` means "now"; unparseable helpers degrade to absolute), and `resolveLogRunRange` which re-resolves relative ranges against `now` at run time (reusing the existing `ResolvedLogDateRange` shape). - **Session state**: per-snippet `logRange` + `setLogRange` — session-only, never written to snippet content, so it works on read-only shared snippets and is cleaned up in `clearForSnippet`. - **Refactor**: extracted the picker's framework-free helpers into a new pure `Logs.datePickerHelpers.ts`; the logs domain now shares the `Unit` type and reuses `generateDynamicHelper` instead of duplicating them. Importers point at the new module directly (no re-export shim). Hardened the amount parse against `NaN`. - **Full unit coverage** in `querySource.test.ts`. Recorded the no-shim refactoring convention in the `studio-best-practices` skill. Verification: `pnpm typecheck` clean, lint ratchet improved, 43 tests pass (querySource + Logs.Datepickers), Prettier clean. ## Additional context <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added robust Logs date-range modeling with support for relative (e.g., last N units) and absolute time periods. - SQL Editor sessions now remember log date ranges per snippet. - **Bug Fixes** - Safer handling of invalid or missing date inputs, with sensible fallback to default/current time. - **Tests** - Added/expanded automated coverage for date-range conversion, helper parsing, and resolution behavior. - **Refactor** - Centralized date-picker helper utilities for reuse across the Logs and SQL query experience. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
231 lines
7.6 KiB
TypeScript
231 lines
7.6 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
datePickerValueToLogDateRange,
|
|
DEFAULT_LOG_DATE_RANGE,
|
|
getSnippetSource,
|
|
isoDateTimeString,
|
|
logDateRangeToDatePickerValue,
|
|
resolveLogRunRange,
|
|
type LogDateRange,
|
|
} from './querySource'
|
|
import {
|
|
EXPLORER_DATEPICKER_HELPERS,
|
|
getDefaultHelper,
|
|
} from '@/components/interfaces/Settings/Logs/Logs.constants'
|
|
import { generateHelpersFromInput } from '@/components/interfaces/Settings/Logs/Logs.datePickerHelpers'
|
|
import type { DatePickerValue } from '@/components/interfaces/Settings/Logs/Logs.DatePickers'
|
|
import type { DatetimeHelper } from '@/components/interfaces/Settings/Logs/Logs.types'
|
|
|
|
/** Build the `DatePickerValue` the Logs picker submits when a helper is selected. */
|
|
const valueFromHelper = (helper: DatetimeHelper): DatePickerValue => ({
|
|
from: helper.calcFrom(),
|
|
to: helper.calcTo(),
|
|
isHelper: true,
|
|
text: helper.text,
|
|
})
|
|
|
|
/** The single dynamic helper produced from typed input like "2h" / "30m". */
|
|
const dynamicHelper = (input: string): DatetimeHelper => {
|
|
const generated = generateHelpersFromInput(input)
|
|
if (!generated || generated.length !== 1) {
|
|
throw new Error(`Expected a single dynamic helper for "${input}"`)
|
|
}
|
|
return generated[0]
|
|
}
|
|
|
|
describe('querySource.ts:getSnippetSource', () => {
|
|
it('maps log_sql to the logs source', () => {
|
|
expect(getSnippetSource({ type: 'log_sql' })).toBe('logs')
|
|
})
|
|
|
|
it('maps sql to the database source', () => {
|
|
expect(getSnippetSource({ type: 'sql' })).toBe('database')
|
|
})
|
|
|
|
it('maps report to the database source', () => {
|
|
expect(getSnippetSource({ type: 'report' })).toBe('database')
|
|
})
|
|
})
|
|
|
|
describe('querySource.ts:isoDateTimeString', () => {
|
|
it('accepts a valid ISO datetime', () => {
|
|
const raw = '2025-01-01T12:00:00.000Z'
|
|
expect(isoDateTimeString(raw)).toBe(raw)
|
|
})
|
|
|
|
it('rejects an empty string', () => {
|
|
expect(isoDateTimeString('')).toBeNull()
|
|
})
|
|
|
|
it('rejects junk', () => {
|
|
expect(isoDateTimeString('not-a-date')).toBeNull()
|
|
expect(isoDateTimeString('2025-13-45T99:99:99Z')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('querySource.ts:datePickerValueToLogDateRange', () => {
|
|
it('parses every static preset into a relative range', () => {
|
|
const cases: Array<[string, { amount: number; unit: 'minute' | 'hour' | 'day' }]> = [
|
|
['Last hour', { amount: 1, unit: 'hour' }],
|
|
['Last 3 hours', { amount: 3, unit: 'hour' }],
|
|
['Last 24 hours', { amount: 24, unit: 'hour' }],
|
|
['Last 3 days', { amount: 3, unit: 'day' }],
|
|
['Last 7 days', { amount: 7, unit: 'day' }],
|
|
]
|
|
|
|
for (const [text, last] of cases) {
|
|
const helper = EXPLORER_DATEPICKER_HELPERS.find((h) => h.text === text)
|
|
expect(helper, `preset "${text}" exists`).toBeDefined()
|
|
expect(datePickerValueToLogDateRange(valueFromHelper(helper!))).toEqual({
|
|
kind: 'relative',
|
|
last,
|
|
})
|
|
}
|
|
})
|
|
|
|
it('parses dynamic helpers ("2h", "30m", "7d") into relative ranges', () => {
|
|
expect(datePickerValueToLogDateRange(valueFromHelper(dynamicHelper('2h')))).toEqual({
|
|
kind: 'relative',
|
|
last: { amount: 2, unit: 'hour' },
|
|
})
|
|
expect(datePickerValueToLogDateRange(valueFromHelper(dynamicHelper('30m')))).toEqual({
|
|
kind: 'relative',
|
|
last: { amount: 30, unit: 'minute' },
|
|
})
|
|
expect(datePickerValueToLogDateRange(valueFromHelper(dynamicHelper('7d')))).toEqual({
|
|
kind: 'relative',
|
|
last: { amount: 7, unit: 'day' },
|
|
})
|
|
})
|
|
|
|
it('treats a preset (calcTo() === "") as relative — the empty end means "now"', () => {
|
|
const lastHour = getDefaultHelper(EXPLORER_DATEPICKER_HELPERS)
|
|
const value = valueFromHelper(lastHour)
|
|
expect(value.to).toBe('')
|
|
expect(datePickerValueToLogDateRange(value)).toEqual({
|
|
kind: 'relative',
|
|
last: { amount: 1, unit: 'hour' },
|
|
})
|
|
})
|
|
|
|
it('degrades an unparseable helper to an absolute range using from/now (never empty)', () => {
|
|
vi.useFakeTimers()
|
|
const now = new Date('2025-01-01T12:00:00.000Z')
|
|
vi.setSystemTime(now)
|
|
|
|
const from = '2024-12-31T00:00:00.000Z'
|
|
const result = datePickerValueToLogDateRange({
|
|
from,
|
|
to: '',
|
|
isHelper: true,
|
|
text: 'Some custom label',
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
kind: 'absolute',
|
|
from,
|
|
to: dayjs(now).toISOString(),
|
|
})
|
|
})
|
|
|
|
it('maps a custom (non-helper) pick to an absolute range with both endpoints', () => {
|
|
const from = '2025-01-01T00:00:00.000Z'
|
|
const to = '2025-01-02T00:00:00.000Z'
|
|
expect(datePickerValueToLogDateRange({ from, to, isHelper: false })).toEqual({
|
|
kind: 'absolute',
|
|
from,
|
|
to,
|
|
})
|
|
})
|
|
|
|
it('falls back to the default range when the value has no usable from', () => {
|
|
expect(datePickerValueToLogDateRange({ from: '', to: '', isHelper: false })).toEqual(
|
|
DEFAULT_LOG_DATE_RANGE
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('querySource.ts:logDateRangeToDatePickerValue', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2025-01-01T12:00:00.000Z'))
|
|
})
|
|
|
|
it('renders a relative range exactly as the picker would emit the helper', () => {
|
|
const value = logDateRangeToDatePickerValue({
|
|
kind: 'relative',
|
|
last: { amount: 1, unit: 'hour' },
|
|
})
|
|
expect(value).toEqual({
|
|
from: dayjs().subtract(1, 'hour').toISOString(),
|
|
to: dayjs().toISOString(),
|
|
isHelper: true,
|
|
text: 'Last 1 hour',
|
|
})
|
|
})
|
|
|
|
it('pluralizes the label for amounts greater than one', () => {
|
|
expect(
|
|
logDateRangeToDatePickerValue({ kind: 'relative', last: { amount: 3, unit: 'day' } }).text
|
|
).toBe('Last 3 days')
|
|
})
|
|
|
|
it('round-trips through datePickerValueToLogDateRange', () => {
|
|
const range: LogDateRange = { kind: 'relative', last: { amount: 30, unit: 'minute' } }
|
|
expect(datePickerValueToLogDateRange(logDateRangeToDatePickerValue(range))).toEqual(range)
|
|
})
|
|
|
|
it('passes an absolute range through as a non-helper value', () => {
|
|
const range: LogDateRange = {
|
|
kind: 'absolute',
|
|
from: isoDateTimeString('2025-01-01T00:00:00.000Z')!,
|
|
to: isoDateTimeString('2025-01-02T00:00:00.000Z')!,
|
|
}
|
|
expect(logDateRangeToDatePickerValue(range)).toEqual({
|
|
from: '2025-01-01T00:00:00.000Z',
|
|
to: '2025-01-02T00:00:00.000Z',
|
|
isHelper: false,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('querySource.ts:resolveLogRunRange', () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('re-resolves a relative range against the current time', () => {
|
|
vi.useFakeTimers()
|
|
const now = new Date('2025-06-15T08:30:00.000Z')
|
|
vi.setSystemTime(now)
|
|
|
|
expect(resolveLogRunRange({ kind: 'relative', last: { amount: 2, unit: 'hour' } })).toEqual({
|
|
from: dayjs(now).subtract(2, 'hour').toISOString(),
|
|
to: dayjs(now).toISOString(),
|
|
})
|
|
})
|
|
|
|
it('re-resolves the same relative range differently as time advances', () => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2025-06-15T08:00:00.000Z'))
|
|
const first = resolveLogRunRange({ kind: 'relative', last: { amount: 1, unit: 'hour' } })
|
|
|
|
vi.setSystemTime(new Date('2025-06-15T10:00:00.000Z'))
|
|
const second = resolveLogRunRange({ kind: 'relative', last: { amount: 1, unit: 'hour' } })
|
|
|
|
expect(first).not.toEqual(second)
|
|
expect(second.to).toBe(dayjs('2025-06-15T10:00:00.000Z').toISOString())
|
|
})
|
|
|
|
it('passes an absolute range through unchanged', () => {
|
|
const from = isoDateTimeString('2025-01-01T00:00:00.000Z')!
|
|
const to = isoDateTimeString('2025-01-02T00:00:00.000Z')!
|
|
expect(resolveLogRunRange({ kind: 'absolute', from, to })).toEqual({
|
|
from: '2025-01-01T00:00:00.000Z',
|
|
to: '2025-01-02T00:00:00.000Z',
|
|
})
|
|
})
|
|
})
|