hook up to snippets endpoint

This commit is contained in:
Saxon Fletcher committed 2025-07-25 09:57:14 +10:00
1 parent d2a310aa88
commit 64d935e402
8 files changed
+168 -159

No files matched your search

@@ -229,7 +229,7 @@ const DatabaseTestsList = forwardRef<TestsListHandle, DatabaseTestsListProps>(
return (
<div className="w-full">
{tests.length === 0 ? (
<p>
<p className="text-sm text-foreground-light p-4">
No setup tests found. Create tests with names starting with "000" to set up test
environment.
</p>
@@ -245,17 +245,21 @@ const DatabaseTestsList = forwardRef<TestsListHandle, DatabaseTestsListProps>(
// Regular tests view with full TestRow component
return (
<div>
{tests.map((test, index) => (
<TestRow
ref={(el) => (testRowRefs.current[test.id] = el)}
key={test.id}
index={index}
test={test}
prependQuery={setupTest}
canRun={true}
onSelectTest={onSelectTest}
/>
))}
{tests.length === 0 ? (
<p className="text-sm text-foreground-light p-4">No tests found.</p>
) : (
tests.map((test, index) => (
<TestRow
ref={(el) => (testRowRefs.current[test.id] = el)}
key={test.id}
index={index}
test={test}
prependQuery={setupTest}
canRun={true}
onSelectTest={onSelectTest}
/>
))
)}
</div>
)
}
@@ -272,13 +276,6 @@ const DatabaseTestsList = forwardRef<TestsListHandle, DatabaseTestsListProps>(
)}
{isSuccess && (
<>
{filteredSetupTests.length === 0 &&
filteredRegularTests.length === 0 &&
setupTests.length === 0 && (
<div className="p-4 text-center text-sm text-foreground-light">No tests found.</div>
)}
{/* Setup Tests Group - Always show */}
<Card>
<CardHeader className="px-4">
<div className="flex items-center gap-2">
@@ -289,18 +286,15 @@ const DatabaseTestsList = forwardRef<TestsListHandle, DatabaseTestsListProps>(
{renderTestTable(filteredSetupTests, true)}
</Card>
{/* Regular Tests Group */}
{filteredRegularTests.length > 0 && (
<Card>
<CardHeader className="px-4">
<div className="flex items-center gap-2">
<FlaskConical strokeWidth={1.5} size={16} className="text-foreground-lighter" />
<CardTitle>Tests</CardTitle>
</div>
</CardHeader>
{renderTestTable(filteredRegularTests, false)}
</Card>
)}
<Card>
<CardHeader className="px-4">
<div className="flex items-center gap-2">
<FlaskConical strokeWidth={1.5} size={16} className="text-foreground-lighter" />
<CardTitle>Tests</CardTitle>
</div>
</CardHeader>
{renderTestTable(filteredRegularTests, false)}
</Card>
</>
)}
</div>
@@ -31,6 +31,7 @@ import {
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
import { SqlSnippets } from 'types'
import { Separator, TreeView } from 'ui'
import { DATABASE_TESTS_FOLDER_NAME } from 'data/database-tests/database-tests.constants'
import {
InnerSideBarEmptyPanel,
InnerSideMenuCollapsible,
@@ -141,17 +142,28 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
return snippetInfo
}, [privateSnippetsPages?.pages, subResults, isLoading, isPreviousData, isFetching, snippet])
// Exclude special tests folder from SQL Editor view
const allFolders = useSnippetFolders(projectRef as string)
const hiddenFolderIds = allFolders
.filter((f) => f.name?.toLowerCase() === DATABASE_TESTS_FOLDER_NAME.toLowerCase())
.map((f) => f.id)
const folders = allFolders.filter((f) => !hiddenFolderIds.includes(f.id))
const privateSnippets = useMemo(
() =>
filteredSnippets.snippets
?.filter((snippet) => snippet.visibility === 'user')
?.filter(
(snippet) =>
snippet.visibility === 'user' &&
!(snippet.folder_id && hiddenFolderIds.includes(snippet.folder_id))
)
.sort((a, b) => {
if (sort === 'name') return a.name.localeCompare(b.name)
else return new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
}) ?? [],
[filteredSnippets.snippets, sort]
[filteredSnippets.snippets, sort, hiddenFolderIds]
)
const folders = useSnippetFolders(projectRef as string)
const { data: snippetCountData } = useContentCountQuery({
projectRef,
@@ -2,13 +2,18 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { uuidv4 } from 'lib/helpers'
// Content helpers
import { insertContent, InsertContentPayload } from 'data/content/content-insert-mutation'
import { getSQLSnippetFolders } from 'data/content/sql-folders-query'
import { createSQLSnippetFolder } from 'data/content/sql-folder-create-mutation'
import { DATABASE_TESTS_FOLDER_NAME } from './database-tests.constants'
import { databaseTestsKeys } from './database-tests-key'
import { DatabaseTest } from './database-tests-query'
type DatabaseTestCreateVariables = {
projectRef: string
connectionString?: string
query: string
name: string
connectionString?: string // preserved for API compatibility, currently unused
}
export const useDatabaseTestCreateMutation = ({
@@ -20,17 +25,37 @@ export const useDatabaseTestCreateMutation = ({
return useMutation({
mutationFn: async ({ projectRef, query, name }: DatabaseTestCreateVariables) => {
// This is a mock implementation.
// In a real scenario, you would save the test to a file or a database table.
const key = databaseTestsKeys.list(projectRef)
const currentTests = queryClient.getQueryData<DatabaseTest[]>(key) ?? []
const newTest: DatabaseTest = {
// 1. Ensure "test" folder exists
const folderResp = await getSQLSnippetFolders({
projectRef,
name: DATABASE_TESTS_FOLDER_NAME,
})
let testFolder = folderResp.folders.find(
(f) => f.name.toLowerCase() === DATABASE_TESTS_FOLDER_NAME.toLowerCase()
)
if (!testFolder) {
// create the folder if it doesn't exist
testFolder = await createSQLSnippetFolder({ projectRef, name: DATABASE_TESTS_FOLDER_NAME })
}
// 2. Insert the content as a SQL snippet under the test folder
const payload: InsertContentPayload = {
id: uuidv4(),
name,
query,
description: '',
type: 'sql',
visibility: 'user',
folder_id: testFolder.id,
content: { sql: query },
}
queryClient.setQueryData(key, [...currentTests, newTest])
return newTest
const data = await insertContent({ projectRef, payload })
// Invalidate cached lists so UI refreshes
await queryClient.invalidateQueries(databaseTestsKeys.list(projectRef))
return { id: data.id, name: data.name, query }
},
onSuccess: (newTest) => {
toast.success(`Test "${newTest.name}" created.`)
@@ -1,8 +1,8 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { deleteContents } from 'data/content/content-delete-mutation'
import { databaseTestsKeys } from './database-tests-key'
import { DatabaseTest } from './database-tests-query'
type DatabaseTestDeleteVariables = {
projectRef: string
@@ -18,12 +18,11 @@ export const useDatabaseTestDeleteMutation = ({
return useMutation({
mutationFn: async ({ projectRef, id }: DatabaseTestDeleteVariables) => {
// This is a mock implementation.
// In a real scenario, you would delete the test file or remove it from a database table.
const key = databaseTestsKeys.list(projectRef)
const currentTests = queryClient.getQueryData<DatabaseTest[]>(key) ?? []
const updatedTests = currentTests.filter((test) => test.id !== id)
queryClient.setQueryData(key, updatedTests)
await deleteContents({ projectRef, ids: [id] })
// Invalidate test list cache
await queryClient.invalidateQueries(databaseTestsKeys.list(projectRef))
return id
},
onSuccess: (id) => {
@@ -1,14 +1,16 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { upsertContent } from 'data/content/content-upsert-mutation'
import { databaseTestsKeys } from './database-tests-key'
import { DatabaseTest } from './database-tests-query'
type DatabaseTestUpdateVariables = {
projectRef: string
id: string
query: string
name?: string
name: string
folderId?: string
ownerId: number
}
export const useDatabaseTestUpdateMutation = ({
@@ -19,16 +21,32 @@ export const useDatabaseTestUpdateMutation = ({
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ projectRef, id, query, name }: DatabaseTestUpdateVariables) => {
const key = databaseTestsKeys.list(projectRef)
const currentTests = queryClient.getQueryData<DatabaseTest[]>(key) ?? []
const updatedTests = currentTests.map((test) =>
test.id === id ? { ...test, name, query } : test
)
queryClient.setQueryData(key, updatedTests)
mutationFn: async ({
projectRef,
id,
query,
name,
folderId,
ownerId,
}: DatabaseTestUpdateVariables) => {
// Prepare payload for upsert
const payload = {
id,
name,
description: '',
type: 'sql' as const,
visibility: 'user' as const,
folder_id: folderId ?? undefined,
owner_id: ownerId,
content: { sql: query },
}
const updatedTest = updatedTests.find((test) => test.id === id)
return updatedTest!
await upsertContent({ projectRef, payload })
// Invalidate cache so that refetch happens
await queryClient.invalidateQueries(databaseTestsKeys.list(projectRef))
return { id, name, query }
},
onSuccess: (updatedTest) => {
toast.success(`Test "${updatedTest.name}" updated.`)
@@ -1,5 +1,9 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { databaseTestsKeys } from './database-tests-key'
import { getSQLSnippetFolders } from 'data/content/sql-folders-query'
import { getSQLSnippetFolderContents } from 'data/content/sql-folder-contents-query'
import { DATABASE_TESTS_FOLDER_NAME } from './database-tests.constants'
import { getContentById } from 'data/content/content-id-query'
export type DatabaseTest = {
id: string
@@ -12,110 +16,63 @@ export type DatabaseTestsVariables = {
connectionString?: string
}
// This is a placeholder. Replace with your actual data fetching logic.
const MOCK_TESTS: DatabaseTest[] = [
{
id: '0',
name: '000-setup-tests-hooks',
query: `
-- install tests utilities
-- install pgtap extension for testing
create extension if not exists pgtap with schema extensions;
/*
---------------------
---- install dbdev ----
----------------------
Requires:
- pg_tle: https://github.com/aws/pg_tle
- pgsql-http: https://github.com/pramsey/pgsql-http
*/
create extension if not exists http with schema extensions;
create extension if not exists pg_tle;
drop extension if exists "supabase-dbdev";
select pgtle.uninstall_extension_if_exists('supabase-dbdev');
select
pgtle.install_extension(
'supabase-dbdev',
resp.contents ->> 'version',
'PostgreSQL package manager',
resp.contents ->> 'sql'
)
from http(
(
'GET',
'https://api.database.dev/rest/v1/'
|| 'package_versions?select=sql,version'
|| '&package_name=eq.supabase-dbdev'
|| '&order=version.desc'
|| '&limit=1',
array[
('apiKey', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhtdXB0cHBsZnZpaWZyYndtbXR2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODAxMDczNzIsImV4cCI6MTk5NTY4MzM3Mn0.z2CN0mvO2No8wSi46Gw59DFGCTJrzM0AQKsu_5k134s')::http_header
],
null,
null
)
) x,
lateral (
select
((row_to_json(x) -> 'content') #>> '{}')::json -> 0
) resp(contents);
create extension "supabase-dbdev";
select dbdev.install('supabase-dbdev');
drop extension if exists "supabase-dbdev";
create extension "supabase-dbdev";
-- Install test helpers
select dbdev.install('basejump-supabase_test_helpers');
create extension if not exists "basejump-supabase_test_helpers" version '0.0.6';
`,
},
{
id: '1',
name: '01_test_users_table',
query: `
BEGIN;
SELECT plan(1);
SELECT has_table('users');
SELECT * FROM finish();
ROLLBACK;
`.trim(),
},
{
id: '2',
name: '02_test_rls_policy',
query: `
BEGIN;
SELECT plan(1);
SELECT policies_are('public', 'profiles', ARRAY['Profiles are public']);
SELECT * FROM finish();
ROLLBACK;
`.trim(),
},
{
id: '3',
name: '03_test_rls_public',
query: `
begin;
select plan(1);
-- Verify RLS is enabled on all tables in the public schema
select tests.rls_enabled('public');
select * from finish();
rollback;
`,
},
]
* Fetches SQL snippets that live inside a folder named "test".
* These snippets are considered database tests for a project.
* The function first retrieves (or verifies the existence of) the
* "test" folder via the `/content/folders` endpoint, then fetches
* the snippets that belong to that folder, and finally resolves each
* snippet's SQL text via `getContentById` so that callers receive the
* full `query` string required to run the tests.
*/
export async function getDatabaseTests(
{ projectRef, connectionString }: DatabaseTestsVariables,
{ projectRef }: DatabaseTestsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
// In a real implementation, you would fetch this from a database or file system.
// For now, we return mock data.
// The timeout is to simulate network latency.
await new Promise((resolve) => setTimeout(resolve, 500))
/*
* 1. Locate the folder named "test" (case-insensitive)
*/
const folderResp = await getSQLSnippetFolders(
{ projectRef, name: DATABASE_TESTS_FOLDER_NAME },
signal
)
return MOCK_TESTS
const testFolder = folderResp.folders.find(
(f) => f.name.toLowerCase() === DATABASE_TESTS_FOLDER_NAME.toLowerCase()
)
if (!testFolder) return []
/*
* 2. Fetch list of contents within that folder via folder contents endpoint
*/
const folderContentsResp = await getSQLSnippetFolderContents(
{ projectRef, folderId: testFolder.id, sort: 'name' },
signal
)
const contents = (folderContentsResp.contents ?? []) as Array<{
id: string
name: string
}>
/*
* 3. Resolve SQL text for each snippet in parallel
*/
const tests: DatabaseTest[] = await Promise.all(
contents.map(async (item) => {
const { content } = await getContentById({ projectRef, id: item.id })
const sql = (content as any)?.sql ?? ''
return {
id: item.id,
name: item.name,
query: sql,
}
})
)
return tests
}
export type DatabaseTestsData = Awaited<ReturnType<typeof getDatabaseTests>>
@@ -0,0 +1 @@
export const DATABASE_TESTS_FOLDER_NAME = 'sb_tests'
@@ -19,6 +19,7 @@ import { useAppStateSnapshot } from 'state/app-state'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import type { NextPageWithLayout } from 'types'
import { toast } from 'sonner'
import { useProfile } from 'lib/profile'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useDatabaseTestCreateMutation } from 'data/database-tests/database-test-create-mutation'
import { useDatabaseTestUpdateMutation } from 'data/database-tests/database-test-update-mutation'
@@ -26,6 +27,7 @@ import { useDatabaseTestUpdateMutation } from 'data/database-tests/database-test
const DatabaseTestsPage: NextPageWithLayout = () => {
const { setEditorPanel } = useAppStateSnapshot()
const aiSnap = useAiAssistantStateSnapshot()
const { profile } = useProfile()
const project = useSelectedProject()
const [showRunAllTestsModal, setShowRunAllTestsModal] = useState(false)
const testsListRef = useRef<TestsListHandle>(null)
@@ -65,6 +67,7 @@ const DatabaseTestsPage: NextPageWithLayout = () => {
id: test.id,
name: test.name,
query,
ownerId: profile?.id,
})
setEditorPanel({ open: false })
},