mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
hook up to snippets endpoint
This commit is contained in:
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 })
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user