Files
supabase/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.test.ts
Charis 8bdfe03fe7 refactor(studio): drop notebook type widening now that the API supports it (#49272)
## Summary

- Regenerates `packages/api-types` for the content endpoints now that
the Platform API's `notebook` content type has landed (list/get/upsert
`type` enums, plus `UpsertContentBody`'s notebook cell shape with
`_id`/`y_series`). Unrelated schema drift from the same regen
(Warehouse, SSO, notification exceptions, etc.) is excluded — only the
content-endpoint hunks are applied.
- Removes every local widening cast added while the API support was
pending (`content-query.ts`, `content-infinite-query.ts`,
`notebook-query.ts`, `notebook-upsert-mutation.ts`,
`sql-folders-query.ts`).
- What remains is scoped and renamed to match: draft ids
(`generateDraftId`/`isDraftId`), used only for cells created client-side
in the editor before their first save, dropped before they'd ever reach
the backend as a fake `_id`.

## Test plan

- [x] `pnpm typecheck` — clean
- [x] `pnpm --filter studio test` — full suite passes (518 files / 5471
tests)
- [x] `pnpm --filter studio run lint:ratchet` — no new warnings
- [x] `pnpm format` / prettier — clean

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved notebook cell tracking during editing, reordering, insertion,
and deletion.
* Preserved existing cell identifiers while removing temporary draft
identifiers before saving.
* Improved chart configuration for selecting and displaying multiple
Y-axis series.
  * Strengthened notebook validation and content persistence behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-20 13:06:41 +08:00

179 lines
5.2 KiB
TypeScript

import { untrustedSql } from '@supabase/pg-meta'
import { describe, expect, it } from 'vitest'
import {
changeCellSource,
cloneQueryCell,
DEFAULT_CELL_ROW_LIMIT,
getCellDisplay,
setCellRowLimit,
setCellSql,
toQueryModel,
} from './QueryCell.utils'
import { type ChartConfig, type QueryCell } from '@/data/content/notebooks/notebook-schema'
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
const CHART: ChartConfig = {
type: 'bar',
x_column: 'day',
y_series: ['signups'],
cumulative: false,
scale: 'linear',
show_labels: true,
}
const DATABASE_CELL: QueryCell = {
_tag: 'database_cell',
_id: 'cell-1',
title: 'Signups',
view: 'chart',
chart: CHART,
unchecked_sql: untrustedSql('select * from auth.users'),
row_limit: 50,
database_identifier: 'replica-1',
}
const LOG_CELL: QueryCell = {
_tag: 'log_cell',
_id: 'cell-2',
title: 'Edge errors',
view: 'table',
chart: CHART,
unchecked_sql: untrustedLogSql('select timestamp from logs'),
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 },
}
describe('changeCellSource', () => {
it('keeps the query when only the database changes', () => {
const next = changeCellSource(DATABASE_CELL, {
_tag: 'database',
database_identifier: 'replica-2',
})
expect(next).toEqual({ ...DATABASE_CELL, database_identifier: 'replica-2' })
})
it('keeps the query when only the log time range changes', () => {
const time_range = { _tag: 'relative_time_range', unit: 'day', amount: 7 } as const
const next = changeCellSource(LOG_CELL, { _tag: 'logs', time_range })
expect(next).toEqual({ ...LOG_CELL, time_range })
})
it('carries the query text over when moving from the database to logs', () => {
const time_range = { _tag: 'relative_time_range', unit: 'hour', amount: 1 } as const
const next = changeCellSource(DATABASE_CELL, { _tag: 'logs', time_range })
expect(next).toEqual({
_tag: 'log_cell',
_id: 'cell-1',
title: 'Signups',
view: 'chart',
chart: CHART,
unchecked_sql: 'select * from auth.users',
time_range,
})
})
it('carries the query text over and restores a default row limit when moving from logs to the database', () => {
const next = changeCellSource(LOG_CELL, { _tag: 'database', database_identifier: undefined })
expect(next).toEqual({
_tag: 'database_cell',
_id: 'cell-2',
title: 'Edge errors',
view: 'table',
chart: CHART,
unchecked_sql: 'select timestamp from logs',
row_limit: DEFAULT_CELL_ROW_LIMIT,
database_identifier: undefined,
})
})
it('applies the selected database when moving from logs to the database', () => {
const next = changeCellSource(LOG_CELL, {
_tag: 'database',
database_identifier: 'replica-2',
})
expect(next).toMatchObject({ _tag: 'database_cell', database_identifier: 'replica-2' })
})
it('preserves the chart across a backend change so display settings survive', () => {
const next = changeCellSource(DATABASE_CELL, {
_tag: 'logs',
time_range: { _tag: 'relative_time_range', unit: 'hour', amount: 1 },
})
expect(next.chart).toEqual(CHART)
expect(next.chart).not.toBe(DATABASE_CELL.chart)
})
})
describe('setCellSql', () => {
it('writes the text back onto a database cell without touching its source', () => {
expect(setCellSql(DATABASE_CELL, 'select 1')).toEqual({
...DATABASE_CELL,
unchecked_sql: 'select 1',
})
})
it('writes the text back onto a log cell without touching its time range', () => {
expect(setCellSql(LOG_CELL, 'select 2')).toEqual({ ...LOG_CELL, unchecked_sql: 'select 2' })
})
})
describe('setCellRowLimit', () => {
it('writes the row limit onto a database cell without touching its query', () => {
expect(setCellRowLimit(DATABASE_CELL, 500)).toEqual({ ...DATABASE_CELL, row_limit: 500 })
})
it('leaves a log cell unchanged, since it has no row limit concept', () => {
expect(setCellRowLimit(LOG_CELL, 500)).toEqual(LOG_CELL)
})
})
describe('cloneQueryCell', () => {
it('copies the chart series array rather than aliasing it', () => {
const clone = cloneQueryCell(DATABASE_CELL)
expect(clone).toEqual(DATABASE_CELL)
expect(clone.chart?.y_series).not.toBe(DATABASE_CELL.chart?.y_series)
})
})
describe('getCellDisplay', () => {
it('keeps a configured chart while the table view is selected', () => {
expect(getCellDisplay({ ...DATABASE_CELL, view: 'table' })).toEqual({
view: 'table',
chart: CHART,
})
})
it('reports no chart when a cell has never configured one', () => {
expect(getCellDisplay({ ...DATABASE_CELL, view: 'table', chart: undefined })).toEqual({
view: 'table',
chart: undefined,
})
})
})
describe('toQueryModel', () => {
it('tags a database cell with its row limit and database', () => {
expect(toQueryModel(DATABASE_CELL, 'select 3')).toEqual({
_tag: 'database',
uncheckedSql: 'select 3',
database_identifier: 'replica-1',
rowLimit: 50,
})
})
it('tags a log cell with its time range and no row limit', () => {
expect(toQueryModel(LOG_CELL, 'select 4')).toEqual({
_tag: 'logs',
uncheckedSql: 'select 4',
time_range: LOG_CELL.time_range,
})
})
})