mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## 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 -->
138 lines
4.3 KiB
TypeScript
138 lines
4.3 KiB
TypeScript
import { untrustedSql } from '@supabase/pg-meta'
|
|
import { HttpResponse } from 'msw'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { upsertContent } from './content-upsert-mutation'
|
|
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
|
import { addAPIMock } from '@/tests/lib/msw'
|
|
|
|
const SNIPPET_ID = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
|
|
const FOLDER_ID = 'c2eebc99-9c0b-4ef8-bb6d-6bb9bd380a13'
|
|
|
|
const payload = {
|
|
id: SNIPPET_ID,
|
|
type: 'sql' as const,
|
|
name: 'Moved query',
|
|
visibility: 'user' as const,
|
|
project_id: 1,
|
|
owner_id: 1,
|
|
folder_id: FOLDER_ID,
|
|
content: {
|
|
content_id: SNIPPET_ID,
|
|
schema_version: '1.0',
|
|
unchecked_sql: untrustedSql('SELECT 1'),
|
|
},
|
|
}
|
|
|
|
const LOGS_SQL =
|
|
"select timestamp, event_message from edge_logs where source = 'edge_logs' limit 10"
|
|
|
|
const logSqlPayload = {
|
|
id: SNIPPET_ID,
|
|
type: 'log_sql' as const,
|
|
name: 'Saved logs query',
|
|
visibility: 'user' as const,
|
|
project_id: 1,
|
|
owner_id: 1,
|
|
content: {
|
|
content_id: SNIPPET_ID,
|
|
schema_version: '1',
|
|
unchecked_sql: untrustedLogSql(LOGS_SQL),
|
|
},
|
|
}
|
|
|
|
describe('upsertContent', () => {
|
|
it('remaps content.sql to unchecked_sql in the upsert response', async () => {
|
|
// Self-hosted (and the management API) persist + return the SQL body under `content.sql`;
|
|
// the editor reads `content.unchecked_sql`. Move/rename consume this response directly, so it
|
|
// must be remapped — otherwise the editor renders blank until a refetch.
|
|
addAPIMock({
|
|
method: 'put',
|
|
path: '/platform/projects/:ref/content',
|
|
response: async ({ request }) => {
|
|
const body = (await request.json()) as { id: string; name: string; type: string }
|
|
return HttpResponse.json({
|
|
id: SNIPPET_ID,
|
|
type: 'sql',
|
|
name: body.name,
|
|
description: '',
|
|
favorite: false,
|
|
folder_id: FOLDER_ID,
|
|
inserted_at: '2024-01-01T00:00:00.000Z',
|
|
updated_at: '2024-01-01T00:00:00.000Z',
|
|
visibility: 'user',
|
|
owner_id: 1,
|
|
project_id: 1,
|
|
content: {
|
|
sql: 'SELECT 1',
|
|
content_id: SNIPPET_ID,
|
|
schema_version: '1.0',
|
|
},
|
|
})
|
|
},
|
|
})
|
|
|
|
const result = await upsertContent({ projectRef: 'default', payload })
|
|
|
|
expect(result?.status).toBe('saved')
|
|
expect(result?.content?.unchecked_sql).toEqual(untrustedSql('SELECT 1'))
|
|
expect(result?.content).not.toHaveProperty('sql')
|
|
})
|
|
|
|
it('sends log_sql content as a raw content.sql on the wire and remaps the response back', async () => {
|
|
let sentBody: { type: string; content: Record<string, unknown> } | undefined
|
|
addAPIMock({
|
|
method: 'put',
|
|
path: '/platform/projects/:ref/content',
|
|
response: async ({ request }) => {
|
|
sentBody = (await request.json()) as {
|
|
type: string
|
|
content: Record<string, unknown>
|
|
}
|
|
return HttpResponse.json({
|
|
id: SNIPPET_ID,
|
|
type: 'log_sql',
|
|
name: logSqlPayload.name,
|
|
description: '',
|
|
favorite: false,
|
|
folder_id: null,
|
|
inserted_at: '2024-01-01T00:00:00.000Z',
|
|
updated_at: '2024-01-01T00:00:00.000Z',
|
|
visibility: 'user',
|
|
owner_id: 1,
|
|
project_id: 1,
|
|
content: {
|
|
sql: LOGS_SQL,
|
|
content_id: SNIPPET_ID,
|
|
schema_version: '1',
|
|
},
|
|
})
|
|
},
|
|
})
|
|
|
|
const result = await upsertContent({ projectRef: 'default', payload: logSqlPayload })
|
|
|
|
// Wire body must carry the plain `sql` field, never the branded `unchecked_sql`.
|
|
expect(sentBody?.type).toBe('log_sql')
|
|
expect(sentBody?.content.sql).toBe(LOGS_SQL)
|
|
expect(sentBody?.content).not.toHaveProperty('unchecked_sql')
|
|
|
|
// Response is remapped back to the branded field for the editor.
|
|
expect(result?.status).toBe('saved')
|
|
expect(result?.content?.unchecked_sql).toEqual(untrustedLogSql(LOGS_SQL))
|
|
expect(result?.content).not.toHaveProperty('sql')
|
|
})
|
|
|
|
it('returns null when the API responds with no snippet', async () => {
|
|
addAPIMock({
|
|
method: 'put',
|
|
path: '/platform/projects/:ref/content',
|
|
response: () => HttpResponse.json(null),
|
|
})
|
|
|
|
const result = await upsertContent({ projectRef: 'default', payload })
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|