From 64d935e402164160814bd32bc3aefe65461d20c2 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Fri, 25 Jul 2025 09:57:14 +1000 Subject: [PATCH] hook up to snippets endpoint --- .../interfaces/Database/Tests/TestsList.tsx | 56 +++---- .../SQLEditorNavV2/SQLEditorNav.tsx | 18 ++- .../database-test-create-mutation.ts | 45 ++++-- .../database-test-delete-mutation.ts | 13 +- .../database-test-update-mutation.ts | 40 +++-- .../database-tests/database-tests-query.ts | 151 +++++++----------- .../database-tests.constants.ts | 1 + .../pages/project/[ref]/database/tests.tsx | 3 + 8 files changed, 168 insertions(+), 159 deletions(-) create mode 100644 apps/studio/data/database-tests/database-tests.constants.ts diff --git a/apps/studio/components/interfaces/Database/Tests/TestsList.tsx b/apps/studio/components/interfaces/Database/Tests/TestsList.tsx index 1f547610993..498824568a4 100644 --- a/apps/studio/components/interfaces/Database/Tests/TestsList.tsx +++ b/apps/studio/components/interfaces/Database/Tests/TestsList.tsx @@ -229,7 +229,7 @@ const DatabaseTestsList = forwardRef( return (
{tests.length === 0 ? ( -

+

No setup tests found. Create tests with names starting with "000" to set up test environment.

@@ -245,17 +245,21 @@ const DatabaseTestsList = forwardRef( // Regular tests view with full TestRow component return (
- {tests.map((test, index) => ( - (testRowRefs.current[test.id] = el)} - key={test.id} - index={index} - test={test} - prependQuery={setupTest} - canRun={true} - onSelectTest={onSelectTest} - /> - ))} + {tests.length === 0 ? ( +

No tests found.

+ ) : ( + tests.map((test, index) => ( + (testRowRefs.current[test.id] = el)} + key={test.id} + index={index} + test={test} + prependQuery={setupTest} + canRun={true} + onSelectTest={onSelectTest} + /> + )) + )}
) } @@ -272,13 +276,6 @@ const DatabaseTestsList = forwardRef( )} {isSuccess && ( <> - {filteredSetupTests.length === 0 && - filteredRegularTests.length === 0 && - setupTests.length === 0 && ( -
No tests found.
- )} - - {/* Setup Tests Group - Always show */}
@@ -289,18 +286,15 @@ const DatabaseTestsList = forwardRef( {renderTestTable(filteredSetupTests, true)} - {/* Regular Tests Group */} - {filteredRegularTests.length > 0 && ( - - -
- - Tests -
-
- {renderTestTable(filteredRegularTests, false)} -
- )} + + +
+ + Tests +
+
+ {renderTestTable(filteredRegularTests, false)} +
)}
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx index 55b8dee51f7..c5a768be28c 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx @@ -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, diff --git a/apps/studio/data/database-tests/database-test-create-mutation.ts b/apps/studio/data/database-tests/database-test-create-mutation.ts index eae7cc90cac..8cc1c4a911c 100644 --- a/apps/studio/data/database-tests/database-test-create-mutation.ts +++ b/apps/studio/data/database-tests/database-test-create-mutation.ts @@ -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(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.`) diff --git a/apps/studio/data/database-tests/database-test-delete-mutation.ts b/apps/studio/data/database-tests/database-test-delete-mutation.ts index b50d41a2ebc..80a2a446de8 100644 --- a/apps/studio/data/database-tests/database-test-delete-mutation.ts +++ b/apps/studio/data/database-tests/database-test-delete-mutation.ts @@ -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(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) => { diff --git a/apps/studio/data/database-tests/database-test-update-mutation.ts b/apps/studio/data/database-tests/database-test-update-mutation.ts index a690a531ac6..244b63b9790 100644 --- a/apps/studio/data/database-tests/database-test-update-mutation.ts +++ b/apps/studio/data/database-tests/database-test-update-mutation.ts @@ -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(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.`) diff --git a/apps/studio/data/database-tests/database-tests-query.ts b/apps/studio/data/database-tests/database-tests-query.ts index 759ab4e1648..12f6d46202b 100644 --- a/apps/studio/data/database-tests/database-tests-query.ts +++ b/apps/studio/data/database-tests/database-tests-query.ts @@ -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> diff --git a/apps/studio/data/database-tests/database-tests.constants.ts b/apps/studio/data/database-tests/database-tests.constants.ts new file mode 100644 index 00000000000..8704d711244 --- /dev/null +++ b/apps/studio/data/database-tests/database-tests.constants.ts @@ -0,0 +1 @@ +export const DATABASE_TESTS_FOLDER_NAME = 'sb_tests' diff --git a/apps/studio/pages/project/[ref]/database/tests.tsx b/apps/studio/pages/project/[ref]/database/tests.tsx index d158557a92c..b759bbf7c9a 100644 --- a/apps/studio/pages/project/[ref]/database/tests.tsx +++ b/apps/studio/pages/project/[ref]/database/tests.tsx @@ -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(null) @@ -65,6 +67,7 @@ const DatabaseTestsPage: NextPageWithLayout = () => { id: test.id, name: test.name, query, + ownerId: profile?.id, }) setEditorPanel({ open: false }) },