mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## 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>
85 lines
4.2 KiB
TypeScript
85 lines
4.2 KiB
TypeScript
// Remap `sql` → `unchecked_sql` on SQL snippet content objects as they cross the API boundary.
|
|
// The API stores and returns the field as `sql`; the frontend type uses `unchecked_sql` to make
|
|
// it explicit that this value must never be executed without user confirmation.
|
|
//
|
|
// Both database (`type: 'sql'`) and logs (`type: 'log_sql'`) snippets carry user-authored SQL
|
|
// under the same wire field, but each is branded with its own untrusted brand so Postgres SQL
|
|
// and logs SQL can never cross execution paths. Branding is per-type and never mixed.
|
|
//
|
|
// Notebooks (`type: 'notebook'`) carry the same kind of untrusted SQL, but nested per-cell
|
|
// rather than as a single top-level field, so they go through the dedicated notebook schemas
|
|
// (data/content/notebooks/notebook-schema.ts) instead of the single-field swap below.
|
|
import { untrustedSql } from '@supabase/pg-meta'
|
|
|
|
import type { WritableNotebook } from './notebooks/notebook-schema'
|
|
import { notebookDomainSchema, toWireWritableNotebook } from './notebooks/notebook-schema'
|
|
import type { SnippetStatus } from './snippet-status'
|
|
import type { SnippetWithContent } from './sql-folders-query'
|
|
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
|
|
|
function isSqlContentType(type: string): type is 'sql' | 'log_sql' {
|
|
return type === 'sql' || type === 'log_sql'
|
|
}
|
|
|
|
function isNotebookContentType(type: string): type is 'notebook' {
|
|
return type === 'notebook'
|
|
}
|
|
|
|
export function remapSqlContentField<T extends { type: string }>(item: T): T {
|
|
if (isNotebookContentType(item.type)) {
|
|
if (!('content' in item)) return item
|
|
const content = notebookDomainSchema.parse(item.content)
|
|
return { ...item, content } as T
|
|
}
|
|
if (!isSqlContentType(item.type)) return item
|
|
if (!('content' in item)) return item
|
|
const content = item.content as Record<string, unknown>
|
|
if (!('sql' in content)) return item
|
|
const { sql, ...rest } = content
|
|
const unchecked_sql =
|
|
item.type === 'log_sql' ? untrustedLogSql(sql as string) : untrustedSql(sql as string)
|
|
return { ...item, content: { ...rest, unchecked_sql } } as T
|
|
}
|
|
|
|
export function remapSqlContentFields<T extends { type: string }>(items: Array<T>): Array<T> {
|
|
return items.map(remapSqlContentField)
|
|
}
|
|
|
|
// Wire→domain boundary for a single SQL-editor snippet. The platform API types a
|
|
// content row's `content` as an opaque `{ [key: string]: unknown }` map (and its
|
|
// `type` as the full `'sql' | 'report' | 'log_sql'`), so turning a fetched row
|
|
// into a branded, discriminated `SnippetWithContent` needs exactly one assertion.
|
|
// Concentrating it here — the single place that already owns the sql↔unchecked_sql
|
|
// rename — keeps the query/mutation call sites free of `as unknown as` casts.
|
|
export function remapWireSnippet(row: unknown, status: SnippetStatus): SnippetWithContent {
|
|
const snippet = remapSqlContentField(row as SnippetWithContent)
|
|
return { ...snippet, status }
|
|
}
|
|
|
|
// Reverse remap: `unchecked_sql` → `sql` before sending to the API.
|
|
export function unmapSqlContentField<T extends { type: string }>(item: T): T {
|
|
if (isNotebookContentType(item.type)) {
|
|
if (!('content' in item)) return item
|
|
const content = toWireWritableNotebook(item.content as WritableNotebook)
|
|
return { ...item, content } as T
|
|
}
|
|
if (!isSqlContentType(item.type)) return item
|
|
if (!('content' in item)) return item
|
|
const content = item.content as Record<string, unknown>
|
|
if (!('unchecked_sql' in content)) {
|
|
// Defensive guard against a writer that still submits the pre-rename `{ sql }` shape.
|
|
// Such a payload happens to reach the wire correctly (the API stores `sql`), but it
|
|
// means a save path was missed during the rename — surface it loudly in development.
|
|
// Crucially, we NEVER fabricate `sql: undefined` here: that would clobber the user's
|
|
// saved query text. The no-op below preserves whatever the content already holds.
|
|
if (process.env.NODE_ENV !== 'production' && 'sql' in content) {
|
|
throw new Error(
|
|
`unmapSqlContentField: ${item.type} content is missing 'unchecked_sql' but still carries a raw 'sql' field — a save path was not migrated to the branded field.`
|
|
)
|
|
}
|
|
return item
|
|
}
|
|
const { unchecked_sql, ...rest } = content
|
|
return { ...item, content: { ...rest, sql: unchecked_sql } } as T
|
|
}
|