Files
supabase/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.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

152 lines
5.2 KiB
TypeScript

import { untrustedSql } from '@supabase/pg-meta'
import { type Snapshot } from 'valtio'
import { type ExplorerQueryModel } from '../QueryEditor'
import { type QueryDisplay } from '../types'
import { type ChartConfig, type QueryCell } from '@/data/content/notebooks/notebook-schema'
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
import {
getQuerySourceBinding,
type QuerySourceBinding,
} from '@/data/query-sources/query-source-registry'
/** Row limit a database cell starts with when it has no saved one to carry over. */
export const DEFAULT_CELL_ROW_LIMIT = 100
/**
* Valtio snapshots are deep-readonly. Readonly properties assign to mutable ones, so only
* the array needs rebuilding to turn a snapshot's chart back into a writable config.
*/
type ReadonlyChartConfig = Omit<ChartConfig, 'y_series'> & {
readonly y_series: readonly string[]
}
export const cloneChartConfig = (
chart: ReadonlyChartConfig | undefined
): ChartConfig | undefined => (chart ? { ...chart, y_series: [...chart.y_series] } : undefined)
/** The display state a query cell hands the shared editor. */
// `view` is already defaulted to 'table' by the domain transform, so there is nothing to
// fall back to here — only the chart needs copying out of the snapshot.
export const getCellDisplay = (cell: Snapshot<QueryCell>): QueryDisplay => ({
view: cell.view,
chart: cloneChartConfig(cell.chart),
})
/** Fields every query cell carries, copied out of a snapshot so the result is writable. */
const copyQueryCellBase = (cell: Snapshot<QueryCell>) => ({
_id: cell._id,
title: cell.title,
view: cell.view,
chart: cloneChartConfig(cell.chart),
})
/** A writable copy of a query cell, preserving its backend and every backend-specific field. */
export const cloneQueryCell = (cell: Snapshot<QueryCell>): QueryCell =>
cell._tag === 'log_cell'
? {
...copyQueryCellBase(cell),
_tag: 'log_cell',
unchecked_sql: cell.unchecked_sql,
time_range: cell.time_range,
}
: {
...copyQueryCellBase(cell),
_tag: 'database_cell',
unchecked_sql: cell.unchecked_sql,
row_limit: cell.row_limit,
database_identifier: cell.database_identifier,
}
/**
* Applies a source binding to a query cell, carrying the query text across unchanged and
* rebranding it for the new backend's dialect.
*
* NOTE — carrying the text over is very likely not what a user wants when the backend
* actually changes. Postgres SQL and logs SQL are separate dialects over separate schemas,
* so a carried-over query will almost always fail to run, and the rebrand asserts a
* dialect the text was never written in. We keep it for now because it is the
* least-destructive option and needs no confirmation prompt; revisit once we know whether
* people switch source to port an existing query or to start a fresh one, at which point
* clearing the body (behind a confirmation) is the likely answer.
*/
export function changeCellSource(cell: Snapshot<QueryCell>, source: QuerySourceBinding): QueryCell {
const base = copyQueryCellBase(cell)
if (source._tag === 'logs') {
return {
...base,
_tag: 'log_cell',
unchecked_sql: untrustedLogSql(cell.unchecked_sql),
time_range: source.time_range,
}
}
return {
...base,
_tag: 'database_cell',
unchecked_sql: untrustedSql(cell.unchecked_sql),
row_limit: cell._tag === 'database_cell' ? cell.row_limit : DEFAULT_CELL_ROW_LIMIT,
database_identifier: source.database_identifier,
}
}
/**
* Writes the editor's text back onto a cell, branded for that cell's dialect. Separate
* from `cloneQueryCell` so the brand stays correlated with the cell tag in one narrowing
* rather than being re-derived at each call site.
*/
export function setCellSql(cell: Snapshot<QueryCell>, sql: string): QueryCell {
const base = copyQueryCellBase(cell)
if (cell._tag === 'log_cell') {
return {
...base,
_tag: 'log_cell',
unchecked_sql: untrustedLogSql(sql),
time_range: cell.time_range,
}
}
return {
...base,
_tag: 'database_cell',
unchecked_sql: untrustedSql(sql),
row_limit: cell.row_limit,
database_identifier: cell.database_identifier,
}
}
/**
* Writes a new row limit onto a database cell. A log cell has no row limit concept, so it
* passes through unchanged.
*/
export function setCellRowLimit(cell: Snapshot<QueryCell>, rowLimit: number): QueryCell {
if (cell._tag === 'log_cell') return cloneQueryCell(cell)
return {
...copyQueryCellBase(cell),
_tag: 'database_cell',
unchecked_sql: cell.unchecked_sql,
row_limit: rowLimit,
database_identifier: cell.database_identifier,
}
}
/**
* Builds the editor's query model from a cell and the editor's live text buffer. Branding
* the buffer is the editor boundary the safe-SQL model expects; which brand applies is
* decided by the cell's tag, so the dialect can't drift from the cell it belongs to.
*/
export function toQueryModel(cell: Snapshot<QueryCell>, sql: string): ExplorerQueryModel {
if (cell._tag === 'log_cell') {
return { ...getQuerySourceBinding(cell), uncheckedSql: untrustedLogSql(sql) }
}
return {
...getQuerySourceBinding(cell),
uncheckedSql: untrustedSql(sql),
rowLimit: cell.row_limit,
}
}