Files
supabase/apps/studio/components/interfaces/Settings/Logs/useRecentLogSqlSnippets.ts
Charis 7743fee3ab feat(studio): log_sql content shape + remap content.sql to unchecked_sql (#48305)
## What

PR **2 of 9** in the SQL-editor query-source (Database vs Logs) stack.

**Base:** `charislam/snippet-source-typing` (#48301) — this is a stacked
PR; review/merge that one first.

Client-side rename only — **the wire format is unchanged** (the platform
API still stores and returns `content.sql`). This moves the frontend
`LogSqlSnippets.Content` field to the branded `unchecked_sql`, matching
`SqlSnippets.Content`, and hardens the remap boundary so the rename
can't silently drop saved query text.

## Changes

- **`types/userContent.ts`** — `LogSqlSnippets.Content`'s plain `sql:
string` becomes `unchecked_sql: UntrustedLogSqlFragment` (the brand
added in PR 1). Shape kept minimal: `{ content_id, unchecked_sql,
schema_version }`.
- **`data/content/content-remap.ts`** — extend
`remapSqlContentField`/`unmapSqlContentField` to `log_sql`, branding
**per type** (`untrustedLogSql` for logs, `untrustedSql` for database)
and never mixing brands. **Defensive unmap**: content missing
`unchecked_sql` is never clobbered with `sql: undefined`; a residual raw
`sql` field (a missed save-path rename) throws in development to surface
the bug loudly, while production no-ops safely.
- **Legacy Logs Explorer consumers** updated to the branded field: the
explorer save/update paths, `SavedQueriesItem`, `RecentQueriesItem`, and
the recent-queries page.
- **Two db-only write sites** that leaned on
`LogSqlSnippets.Content.sql`: `EditorPanel` now saves `unchecked_sql`,
and `MoveQueryModal` switches to the SQL-editor-specific
`getSqlSnippetById` so its content is typed as `SqlSnippets.Content` —
no narrowing or casting.

## Tests

- **content-remap**: `log_sql` remap/unmap round-trip with the logs
brand; the defensive-unmap no-op (prod) and dev throw.
- **content-upsert-mutation**: a `log_sql` payload reaches the wire as a
plain `content.sql` and the response remaps back to `unchecked_sql` (the
data-loss-critical round-trip shared by both explorer save-new and
`SavedQueriesItem` update).

## Verification

- `pnpm --filter studio run typecheck` ✓
- `pnpm --filter studio run lint:ratchet` ✓ (no new warnings)
- `pnpm test:studio` for `data/content` + `Settings/Logs` — 139 passing
✓
- Prettier ✓

Nothing is user-visible yet — logs snippet entry points arrive later in
the stack behind the `sqlEditorLogsSource` flag.

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

- **Bug Fixes**
- Improved handling of saved and recent log queries across the SQL
editor and Logs Explorer.
- Log SQL now uses `unchecked_sql` (branded as untrusted) consistently
when creating, editing, moving, and reopening queries, with correct
remapping to/from the API boundary.
- Fixed saved-query update payloads to preserve the right query content
and omit legacy fields.

- **Tests**
- Added/expanded Vitest coverage for saved log query editing, recent-log
normalization, and `log_sql` remap/upsert request/response behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 10:47:43 -04:00

48 lines
1.8 KiB
TypeScript

import { useMemo } from 'react'
import { untrustedLogSql, type UntrustedLogSqlFragment } from '@/data/logs/safe-analytics-sql'
import { useLocalStorage } from '@/hooks/misc/useLocalStorage'
import type { LogSqlSnippets } from '@/types'
/**
* Shape of a recent-log-sql entry as it may exist in localStorage. Entries written before the
* `sql` → `unchecked_sql` rename carry the raw `sql` field; entries written after carry the
* branded `unchecked_sql`. This tolerant type lets us read both and normalize to the branded
* frontend shape, so no consumer ever sees an unbranded `sql`.
*/
type StoredRecentLogSqlSnippet = Omit<LogSqlSnippets.Content, 'unchecked_sql'> & {
unchecked_sql?: UntrustedLogSqlFragment
sql?: string
}
/**
* Normalizes a stored recent-log-sql entry into the current `LogSqlSnippets.Content` shape,
* branding a legacy raw `sql` value with `untrustedLogSql`. Prefers an already-branded
* `unchecked_sql` when present.
*/
export function normalizeRecentLogSqlSnippet(
entry: StoredRecentLogSqlSnippet
): LogSqlSnippets.Content {
const { sql, unchecked_sql, ...rest } = entry
return { ...rest, unchecked_sql: unchecked_sql ?? untrustedLogSql(sql ?? '') }
}
/**
* localStorage-backed list of recent Logs Explorer queries, always surfaced with the branded
* `unchecked_sql` field. Reads tolerate the pre-rename `{ sql }` shape and normalize it; writing
* the normalized value back heals the stored entries over time.
*/
export function useRecentLogSqlSnippets(projectRef?: string) {
const [stored, setStored] = useLocalStorage<StoredRecentLogSqlSnippet[]>(
`project-content-${projectRef}-recent-log-sql`,
[]
)
const snippets = useMemo<LogSqlSnippets.Content[]>(
() => stored.map(normalizeRecentLogSqlSnippet),
[stored]
)
return [snippets, setStored] as const
}