Chore/refactor db extensions to rq (#19622)

* Init db extensions to RQ

* Shift all extensions to RQ

* Deprecate extensions meta store

* Address comments

* Address comment

* Address comment

* Update apps/studio/components/interfaces/Database/Extensions/EnableExtensionModal.tsx

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>

* Remove connection string check for entity-types-query and database-triggers-query

* Remove connection string checks

---------

Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
This commit is contained in:
Joshen LimandAlaister Young authored and GitHub committed 2023-12-13 12:02:20 +07:00
1 parent f27bcb808e
commit 04c19a0d62
21 files changed
+438 -239

No files matched your search

@@ -1,9 +1,11 @@
import type { PostgresExtension } from '@supabase/postgres-meta'
import { useEffect, useState } from 'react'
import toast from 'react-hot-toast'
import { Button, Form, IconDatabase, IconPlus, Input, Listbox, Modal } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useStore } from 'hooks'
@@ -15,7 +17,7 @@ interface EnableExtensionModalProps {
const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionModalProps) => {
const { project } = useProjectContext()
const { ui, meta } = useStore()
const { meta } = useStore()
const [defaultSchema, setDefaultSchema] = useState()
const [fetchingSchemaInfo, setFetchingSchemaInfo] = useState(false)
@@ -23,6 +25,15 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { mutate: enableExtension, isLoading: isEnabling } = useDatabaseExtensionEnableMutation({
onSuccess: () => {
toast.success(`${extension.name} is on.`)
onCancel()
},
onError: (error) => {
toast.error(`Failed to enable ${extension.name}: ${error.message}`)
},
})
// [Joshen] Worth checking in with users - whether having this schema selection
// might be confusing, and if we should have a tooltip to explain that schemas
@@ -58,8 +69,8 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
return errors
}
const onSubmit = async (values: any, { setSubmitting }: any) => {
setSubmitting(true)
const onSubmit = async (values: any) => {
if (project === undefined) return console.error('Project is required')
const schema =
defaultSchema !== undefined && defaultSchema !== null
@@ -68,45 +79,19 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
? values.name
: values.schema
if (!schema.startsWith('pg_')) {
const { error: createSchemaError } = await meta.query(`create schema if not exists ${schema}`)
if (createSchemaError) {
return ui.setNotification({
error: createSchemaError,
category: 'error',
message: `Failed to create schema: ${createSchemaError.message}`,
})
}
}
const { error: createExtensionError } = await meta.extensions.create({
enableExtension({
projectRef: project.ref,
connectionString: project?.connectionString,
schema,
name: extension.name,
version: extension.default_version,
cascade: true,
createSchema: !schema.startsWith('pg_'),
})
if (createExtensionError) {
ui.setNotification({
error: createExtensionError,
category: 'error',
message: `Failed to toggle ${extension.name.toUpperCase()}: ${
createExtensionError.message
}`,
})
} else {
ui.setNotification({
category: 'success',
message: `${extension.name.toUpperCase()} is on.`,
})
}
setSubmitting(false)
onCancel()
}
return (
<Modal
closable
hideFooter
visible={visible}
onCancel={onCancel}
@@ -126,7 +111,7 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
validate={validate}
onSubmit={onSubmit}
>
{({ isSubmitting, values }: any) => {
{({ values }: any) => {
return (
<div className="space-y-4 py-4">
<Modal.Content>
@@ -188,10 +173,10 @@ const EnableExtensionModal = ({ visible, extension, onCancel }: EnableExtensionM
<Modal.Separator />
<Modal.Content>
<div className="flex items-center justify-end space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onCancel()}>
<Button type="default" disabled={isEnabling} onClick={() => onCancel()}>
Cancel
</Button>
<Button htmlType="submit" disabled={isSubmitting} loading={isSubmitting}>
<Button htmlType="submit" disabled={isEnabling} loading={isEnabling}>
Enable extension
</Button>
</div>
@@ -1,13 +1,15 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { observer } from 'mobx-react-lite'
import { useState } from 'react'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useCheckPermissions, useStore } from 'hooks'
import { isResponseOk } from 'lib/common/fetch'
import Link from 'next/link'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { extensions } from 'shared-data'
import { Badge, IconExternalLink, IconLoader, Modal, Toggle } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmationModal from 'components/ui/ConfirmationModal'
import { useDatabaseExtensionDisableMutation } from 'data/database-extensions/database-extension-disable-mutation'
import { useCheckPermissions } from 'hooks'
import EnableExtensionModal from './EnableExtensionModal'
interface ExtensionCardProps {
@@ -15,13 +17,18 @@ interface ExtensionCardProps {
}
const ExtensionCard = ({ extension }: ExtensionCardProps) => {
const { ui, meta } = useStore()
const { project } = useProjectContext()
const isOn = extension.installed_version !== null
const [loading, setLoading] = useState(false)
const [showConfirmEnableModal, setShowConfirmEnableModal] = useState(false)
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || ''
const { mutate: disableExtension, isLoading: isDisabling } = useDatabaseExtensionDisableMutation({
onSuccess: () => {
toast.success(`${extension.name} is off.`)
setIsDisableModalOpen(false)
},
})
const canUpdateExtensions = useCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'extensions'
@@ -36,30 +43,14 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
setIsDisableModalOpen(true)
}
async function disableExtension() {
try {
setLoading(true)
const response = await meta.extensions.del(extension.name)
if (!isResponseOk(response)) {
throw response.error
}
const onConfirmDisable = () => {
if (project === undefined) return console.error('Project is required')
ui.setNotification({
category: 'success',
message: `${extension.name.toUpperCase()} is off.`,
})
setIsDisableModalOpen(false)
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Toggle ${extension.name.toUpperCase()} failed: ${error.message}`,
})
} finally {
// Need to reload them because the delete function
// removes the extension from the store
meta.extensions.load()
setLoading(false)
}
disableExtension({
projectRef: project.ref,
connectionString: project.connectionString,
id: extension.name,
})
}
return (
@@ -111,7 +102,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
</Link>
) : null}
</div>
{loading ? (
{isDisabling ? (
<IconLoader className="animate-spin" size={16} />
) : (
<Toggle
@@ -151,14 +142,13 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
visible={isDisableModalOpen}
header="Confirm to disable extension"
buttonLabel="Disable"
buttonLoadingLabel="Disabling"
onSelectCancel={() => setIsDisableModalOpen(false)}
onSelectConfirm={() => {
disableExtension()
}}
onSelectConfirm={() => onConfirmDisable()}
>
<Modal.Content>
<p className="py-4 text-sm text-foreground-light">
Are you sure you want to turn OFF "{extension.name}" extension?
Are you sure you want to turn OFF the "{extension.name}" extension?
</p>
</Modal.Content>
</ConfirmationModal>
@@ -1,29 +1,35 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { isNull, partition } from 'lodash'
import { observer } from 'mobx-react-lite'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { Button, IconAlertCircle, IconExternalLink, IconSearch, Input } from 'ui'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import InformationBox from 'components/ui/InformationBox'
import NoSearchResults from 'components/ui/NoSearchResults'
import { useCheckPermissions, usePermissionsLoaded, useStore } from 'hooks'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks'
import ExtensionCard from './ExtensionCard'
import { HIDDEN_EXTENSIONS } from './Extensions.constants'
import ShimmeringLoader, { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import ExtensionCardSkeleton from './ExtensionCardSkeleton'
import { HIDDEN_EXTENSIONS } from './Extensions.constants'
const Extensions = () => {
const { meta } = useStore()
const { filter } = useParams()
const { project } = useProjectContext()
const [filterString, setFilterString] = useState<string>('')
const { data, isLoading } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const extensions =
filterString.length === 0
? meta.extensions.list()
: meta.extensions.list((ext: any) => ext.name.includes(filterString))
const isLoading = meta.extensions.isLoading
? data ?? []
: (data ?? []).filter((ext) => ext.name.includes(filterString))
const extensionsWithoutHidden = extensions.filter(
(ext: any) => !HIDDEN_EXTENSIONS.includes(ext.name)
)
@@ -17,6 +17,7 @@ import { isResponseOk } from 'lib/common/fetch'
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
import { SupaResponse } from 'types'
import { convertArgumentTypes, convertConfigParams, hasWhitespace } from './Functions.utils'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
// [Refactor] Remove local state, just use the Form component
@@ -755,13 +756,15 @@ const SelectSchema = observer(({}) => {
})
const SelectLanguage = observer(({}) => {
const { meta } = useStore()
const _localState = useContext(CreateFunctionContext)
const { project } = useProjectContext()
const [enabledExtensions] = partition(
meta.extensions.list(),
(ext: any) => !isNull(ext.installed_version)
)
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [enabledExtensions] = partition(data ?? [], (ext: any) => !isNull(ext.installed_version))
return (
<Listbox
@@ -6,8 +6,8 @@ import { Button, IconExternalLink } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { FDW, useFDWsQuery } from 'data/fdw/fdws-query'
import { useStore } from 'hooks'
import DeleteWrapperModal from './DeleteWrapperModal'
import WrapperRow from './WrapperRow'
import { WRAPPERS } from './Wrappers.constants'
@@ -15,12 +15,17 @@ import WrappersDisabledState from './WrappersDisabledState'
import WrappersDropdown from './WrappersDropdown'
const Wrappers = () => {
const { meta } = useStore()
const { project } = useProjectContext()
const { data, isLoading } = useFDWsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: exts, isLoading: isLoadingExtensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const wrappersExtension = (exts ?? []).find((ext) => ext.name === 'wrappers')
const vaultExtension = (exts ?? []).find((ext) => ext.name === 'supabase_vault')
const [open, setOpen] = useState<string>('')
const [selectedWrapperToDelete, setSelectedWrapperToDelete] = useState<FDW>()
@@ -28,10 +33,6 @@ const Wrappers = () => {
const wrappers = data?.result ?? []
const groupedWrappers = groupBy(wrappers, 'handler')
const wrappersExtension = meta.extensions.byId('wrappers')
const vaultExtension = meta.extensions.byId('supabase_vault')
const isLoadingExtensions = meta.extensions.isLoading
const isWrappersEnabled =
wrappersExtension !== undefined &&
wrappersExtension.installed_version !== null &&
@@ -1,21 +1,30 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { observer } from 'mobx-react-lite'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { Button, IconExternalLink } from 'ui'
import { useTheme } from 'next-themes'
import { useCheckPermissions, useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useCheckPermissions } from 'hooks'
import { BASE_PATH } from 'lib/constants'
const WrappersDisabledState = () => {
const { ui, meta } = useStore()
const { ref } = useParams()
const { resolvedTheme } = useTheme()
const wrappersExtension = meta.extensions.byId('wrappers')
const vaultExtension = meta.extensions.byId('supabase_vault')
const { project } = useProjectContext()
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const wrappersExtension = (data ?? []).find((ext) => ext.name === 'wrappers')
const vaultExtension = (data ?? []).find((ext) => ext.name === 'supabase_vault')
const isNotAvailable = wrappersExtension === undefined || vaultExtension === undefined
const [isEnabling, setIsEnabling] = useState<boolean>(false)
@@ -24,44 +33,43 @@ const WrappersDisabledState = () => {
'extensions'
)
const { mutateAsync: enableExtension } = useDatabaseExtensionEnableMutation({ onError: () => {} })
const onEnableWrappers = async () => {
if (wrappersExtension === undefined || vaultExtension === undefined) return
setIsEnabling(true)
if (project === undefined) return console.error('Project is required')
const requiredExtensions = await Promise.all([
await meta.extensions.create({
schema: wrappersExtension.schema ?? 'extensions',
name: wrappersExtension.name,
version: wrappersExtension.default_version,
cascade: true,
}),
await meta.extensions.create({
schema: vaultExtension.schema ?? 'vault',
name: vaultExtension.name,
version: vaultExtension.default_version,
cascade: true,
}),
])
const errors = requiredExtensions.filter(
(res) => res.error && !res.error.message.includes('already exists')
)
let hasError = false
if (errors.length > 0) {
ui.setNotification({
error: errors,
category: 'error',
message: `Failed to enable Wrappers for your project: ${errors
.map((x) => x.message)
.join(', ')}`,
})
} else {
ui.setNotification({
category: 'success',
message: 'Wrappers is now enabled for your project!',
})
try {
setIsEnabling(true)
await Promise.all([
enableExtension({
projectRef: project.ref,
connectionString: project.connectionString,
schema: wrappersExtension.schema ?? 'extensions',
name: wrappersExtension.name,
version: wrappersExtension.default_version,
cascade: true,
}),
enableExtension({
projectRef: project.ref,
connectionString: project.connectionString,
schema: vaultExtension.schema ?? 'vault',
name: vaultExtension.name,
version: vaultExtension.default_version,
cascade: true,
}),
])
} catch (error: any) {
if (!error.message.includes('already exists')) {
hasError = true
toast.error(`Failed to enable Wrappers: ${error.message}`)
}
} finally {
setIsEnabling(false)
if (!hasError) toast.success('Wrappers is now enabled!')
}
setIsEnabling(false)
}
return (
@@ -1,27 +1,48 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useTheme } from 'next-themes'
import { observer } from 'mobx-react-lite'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { Button, IconExternalLink } from 'ui'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useCheckPermissions, useStore } from 'hooks'
import { BASE_PATH } from 'lib/constants'
const VaultToggle = () => {
const { meta, ui } = useStore()
const { ref } = useParams()
const { meta, ui } = useStore()
const { resolvedTheme } = useTheme()
const { project } = useProjectContext()
const [isEnabling, setIsEnabling] = useState(false)
const canToggleVault = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'extensions')
const vaultExtension = meta.extensions.byId('supabase_vault')
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const vaultExtension = (data ?? []).find((ext) => ext.name === 'supabase_vault')
const isNotAvailable = vaultExtension === undefined
const { mutate: enableExtension } = useDatabaseExtensionEnableMutation({
onSuccess: () => {
toast.success(`Vault is now enabled for your project!`)
},
onError: (error) => {
toast.error(`Failed to enable Vault for your project: ${error.message}`)
setIsEnabling(false)
},
})
const onEnableVault = async () => {
if (vaultExtension === undefined) return
if (project === undefined) return console.error('Project is required')
setIsEnabling(true)
const { error: createSchemaError } = await meta.query(
@@ -36,25 +57,14 @@ const VaultToggle = () => {
})
}
const { error: createExtensionError } = await meta.extensions.create({
enableExtension({
projectRef: project.ref,
connectionString: project.connectionString,
schema: vaultExtension.schema ?? 'vault',
name: vaultExtension.name,
version: vaultExtension.default_version,
cascade: true,
})
if (createExtensionError) {
ui.setNotification({
error: createExtensionError,
category: 'error',
message: `Failed to enable Vault for your project: ${createExtensionError.message}`,
})
setIsEnabling(false)
} else {
ui.setNotification({
category: 'success',
message: 'Vault is now enabled for your project!',
})
}
}
return (
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import ProductMenu from 'components/ui/ProductMenu'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSelectedProject, useStore, withAuth } from 'hooks'
import ProjectLayout from '../'
import { generateDatabaseMenu } from './DatabaseMenu.utils'
@@ -18,15 +19,18 @@ const DatabaseLayout = ({ children }: PropsWithChildren<DatabaseLayoutProps>) =>
const router = useRouter()
const page = router.pathname.split('/')[4]
const vaultExtension = meta.extensions.byId('supabase_vault')
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const vaultExtension = (data ?? []).find((ext) => ext.name === 'supabase_vault')
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
const pgNetExtensionExists = meta.extensions.byId('pg_net') !== undefined
const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined
useEffect(() => {
if (ui.selectedProjectRef) {
meta.roles.load()
meta.triggers.load()
meta.extensions.load()
meta.publications.load()
}
}, [ui.selectedProjectRef])
@@ -1,19 +1,12 @@
import {
useFlag,
useIsFeatureEnabled,
useSelectedOrganization,
useSelectedProject,
useStore,
withAuth,
} from 'hooks'
import { useIsFeatureEnabled, useSelectedOrganization, useSelectedProject, withAuth } from 'hooks'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { PropsWithChildren } from 'react'
import { generateSettingsMenu } from './SettingsMenu.utils'
import { useParams } from 'common'
import ProductMenu from 'components/ui/ProductMenu'
import ProjectLayout from '..'
import { useParams } from 'common'
interface SettingsLayoutProps {
title?: string
@@ -22,7 +15,6 @@ interface SettingsLayoutProps {
const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutProps>) => {
const router = useRouter()
const { ref } = useParams()
const { ui, meta } = useStore()
const project = useSelectedProject()
const organization = useSelectedOrganization()
@@ -51,12 +43,6 @@ const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutPro
invoices: invoicesEnabled,
})
useEffect(() => {
if (ui.selectedProjectRef) {
meta.extensions.load()
}
}, [ui.selectedProjectRef])
return (
<ProjectLayout
title={title || 'Settings'}
@@ -3,24 +3,29 @@ import { observer } from 'mobx-react-lite'
import { PropsWithChildren, useEffect, useMemo } from 'react'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, usePermissionsLoaded, useStore } from 'hooks'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useCheckPermissions, usePermissionsLoaded, useSelectedProject, useStore } from 'hooks'
import { ProjectLayoutWithAuth } from '../'
import TableEditorMenu from './TableEditorMenu'
const TableEditorLayout = ({ children }: PropsWithChildren<{}>) => {
const { vault, meta, ui } = useStore()
const project = useSelectedProject()
const canReadTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'tables')
const isPermissionsLoaded = usePermissionsLoaded()
const vaultExtension = meta.extensions.byId('supabase_vault')
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const vaultExtension = (data ?? []).find((ext) => ext.name === 'supabase_vault')
const isVaultEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
useEffect(() => {
if (ui.selectedProjectRef) {
meta.policies.load()
meta.publications.load()
meta.extensions.load()
}
}, [ui.selectedProjectRef])
@@ -0,0 +1,70 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { del } from 'data/fetchers'
import { ResponseError } from 'types'
import { databaseExtensionsKeys } from './keys'
export type DatabaseExtensionDisableVariables = {
projectRef: string
connectionString?: string
id: string
}
export async function disableDatabaseExtension({
projectRef,
connectionString,
id,
}: DatabaseExtensionDisableVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await del('/platform/pg-meta/{ref}/extensions', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
query: { id },
},
headers,
})
if (error) throw error
return data
}
type DatabaseExtensionDisableData = Awaited<ReturnType<typeof disableDatabaseExtension>>
export const useDatabaseExtensionDisableMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<
DatabaseExtensionDisableData,
ResponseError,
DatabaseExtensionDisableVariables
>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<
DatabaseExtensionDisableData,
ResponseError,
DatabaseExtensionDisableVariables
>((vars) => disableDatabaseExtension(vars), {
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseExtensionsKeys.list(projectRef))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to disable database extension: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
})
}
@@ -0,0 +1,86 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
import { databaseExtensionsKeys } from './keys'
import { executeSql } from 'data/sql/execute-sql-query'
export type DatabaseExtensionEnableVariables = {
projectRef: string
connectionString?: string
schema: string
name: string
version: string
cascade?: boolean
createSchema?: boolean
}
export async function enableDatabaseExtension({
projectRef,
connectionString,
schema,
name,
version,
cascade = false,
createSchema = false,
}: DatabaseExtensionEnableVariables) {
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
if (createSchema) {
try {
await executeSql({
projectRef,
connectionString,
sql: `create schema if not exists ${schema}`,
})
} catch (error) {
throw error
}
}
const { data, error } = await post('/platform/pg-meta/{ref}/extensions', {
params: {
header: { 'x-connection-encrypted': connectionString! },
path: { ref: projectRef },
},
body: { schema, name, version, cascade },
headers,
})
if (error) throw error
return data
}
type DatabaseExtensionEnableData = Awaited<ReturnType<typeof enableDatabaseExtension>>
export const useDatabaseExtensionEnableMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<DatabaseExtensionEnableData, ResponseError, DatabaseExtensionEnableVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<DatabaseExtensionEnableData, ResponseError, DatabaseExtensionEnableVariables>(
(vars) => enableDatabaseExtension(vars),
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseExtensionsKeys.list(projectRef))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to enable database extension: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,67 @@
import { UseQueryOptions, useQuery, useQueryClient } from '@tanstack/react-query'
import { get } from 'data/fetchers'
import { useCallback } from 'react'
import { ResponseError } from 'types'
import { databaseExtensionsKeys } from './keys'
export type DatabaseExtensionsVariables = {
projectRef?: string
connectionString?: string
}
export async function getDatabaseExtensions(
{ projectRef, connectionString }: DatabaseExtensionsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
let headers = new Headers()
if (connectionString) headers.set('x-connection-encrypted', connectionString)
const { data, error } = await get('/platform/pg-meta/{ref}/extensions', {
params: {
header: {
'x-connection-encrypted': connectionString!,
},
path: {
ref: projectRef,
},
},
headers,
signal,
})
if (error) throw error
return data
}
export type DatabaseExtensionsData = Awaited<ReturnType<typeof getDatabaseExtensions>>
export type DatabaseExtensionsError = ResponseError
export const useDatabaseExtensionsQuery = <TData = DatabaseExtensionsData>(
{ projectRef, connectionString }: DatabaseExtensionsVariables,
{
enabled = true,
...options
}: UseQueryOptions<DatabaseExtensionsData, DatabaseExtensionsError, TData> = {}
) =>
useQuery<DatabaseExtensionsData, DatabaseExtensionsError, TData>(
databaseExtensionsKeys.list(projectRef),
({ signal }) => getDatabaseExtensions({ projectRef, connectionString }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
export const useDatabaseExtensionsPrefetch = ({ projectRef }: DatabaseExtensionsVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (projectRef) {
client.prefetchQuery(databaseExtensionsKeys.list(projectRef), ({ signal }) =>
getDatabaseExtensions({ projectRef }, signal)
)
}
}, [projectRef])
}
@@ -0,0 +1,4 @@
export const databaseExtensionsKeys = {
list: (projectRef: string | undefined) =>
['projects', projectRef, 'database-extensions'] as const,
}
@@ -51,8 +51,7 @@ export const useDatabaseHooks = <TData = DatabaseTriggersData>(
(trigger.schema !== 'net' || trigger.function_args.length === 0)
)
},
enabled:
enabled && typeof projectRef !== 'undefined' && typeof connectionString !== 'undefined',
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
@@ -68,8 +67,7 @@ export const useDatabaseTriggers = <TData = DatabaseTriggersData>(
databaseTriggerKeys.list(projectRef),
({ signal }) => getDatabaseTriggers({ projectRef, connectionString }, signal),
{
enabled:
enabled && typeof projectRef !== 'undefined' && typeof connectionString !== 'undefined',
enabled: enabled && typeof projectRef !== 'undefined',
...options,
}
)
@@ -125,8 +125,7 @@ export const useEntityTypesQuery = <TData = EntityTypesData>(
signal
),
{
enabled:
enabled && typeof projectRef !== 'undefined' && typeof connectionString !== 'undefined',
enabled: enabled && typeof projectRef !== 'undefined',
getNextPageParam(lastPage, pages) {
const page = pages.length
const currentTotalCount = page * limit
@@ -3,29 +3,33 @@ import { createGraphiQLFetcher, Fetcher } from '@graphiql/toolkit'
import { useParams } from 'common'
import { observer } from 'mobx-react-lite'
import { useTheme } from 'next-themes'
import { useEffect, useMemo } from 'react'
import { useMemo } from 'react'
import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard'
import GraphiQL from 'components/interfaces/GraphQL/GraphiQL'
import { DocsLayout } from 'components/layouts'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import Connecting from 'components/ui/Loading/Loading'
import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
import { useProjectApiQuery } from 'data/config/project-api-query'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useStore } from 'hooks'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { getRoleImpersonationJWT } from 'lib/role-impersonation'
import { useGetImpersonatedRole } from 'state/role-impersonation-state'
import { NextPageWithLayout } from 'types'
const GraphiQLPage: NextPageWithLayout = () => {
const { ui, meta } = useStore()
const { resolvedTheme } = useTheme()
const { ref: projectRef } = useParams()
const { project } = useProjectContext()
const currentTheme = resolvedTheme?.includes('dark') ? 'dark' : 'light'
const isExtensionsLoading = meta.extensions.isLoading
const pgGraphqlExtension = meta.extensions.byId('pg_graphql')
const { data, isLoading: isExtensionsLoading } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const pgGraphqlExtension = (data ?? []).find((ext) => ext.name === 'pg_graphql')
const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
const { data: settings, isFetched } = useProjectApiQuery({ projectRef })
@@ -38,13 +42,6 @@ const GraphiQLPage: NextPageWithLayout = () => {
const { data: config } = useProjectPostgrestConfigQuery({ projectRef })
const jwtSecret = config?.jwt_secret
useEffect(() => {
if (ui.selectedProjectRef) {
// Schemas may be needed when enabling the GraphQL extension
meta.extensions.load()
}
}, [ui.selectedProjectRef])
const getImpersonatedRole = useGetImpersonatedRole()
const fetcher = useMemo(() => {
@@ -1,21 +1,29 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { useParams } from 'common'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { Tabs } from 'ui'
import { useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { NextPageWithLayout } from 'types'
import { EncryptionKeysManagement, VaultToggle } from 'components/interfaces/Settings/Vault'
import { SettingsLayout } from 'components/layouts'
import { VaultToggle, EncryptionKeysManagement } from 'components/interfaces/Settings/Vault'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormHeader } from 'components/ui/Forms'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
const VaultSettingsSecrets: NextPageWithLayout = () => {
const router = useRouter()
const { meta, vault } = useStore()
const { ref } = useParams()
const { vault } = useStore()
const { project } = useProjectContext()
const vaultExtension = meta.extensions.byId('supabase_vault')
const { data } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const vaultExtension = (data ?? []).find((ext) => ext.name === 'supabase_vault')
const isEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
useEffect(() => {
@@ -1,23 +1,30 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { observer } from 'mobx-react-lite'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { Tabs } from 'ui'
import { useStore } from 'hooks'
import { useParams } from 'common/hooks'
import { NextPageWithLayout } from 'types'
import { SecretsManagement, VaultToggle } from 'components/interfaces/Settings/Vault'
import { SettingsLayout } from 'components/layouts'
import { VaultToggle, SecretsManagement } from 'components/interfaces/Settings/Vault'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormHeader } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useStore } from 'hooks'
import { NextPageWithLayout } from 'types'
const VaultSettingsSecrets: NextPageWithLayout = () => {
const router = useRouter()
const { meta, vault } = useStore()
const { vault } = useStore()
const { ref } = useParams()
const { project } = useProjectContext()
const vaultExtension = meta.extensions.byId('supabase_vault')
const isLoading = meta.extensions.isLoading
const { data, isLoading } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const vaultExtension = (data ?? []).find((ext) => ext.name === 'supabase_vault')
const isEnabled = vaultExtension !== undefined && vaultExtension.installed_version !== null
useEffect(() => {
@@ -1,21 +0,0 @@
import PostgresMetaInterface from '../common/PostgresMetaInterface'
import type { PostgresColumn } from '@supabase/postgres-meta'
import { IRootStore } from '../RootStore'
export default class ExtensionsStore extends PostgresMetaInterface<PostgresColumn> {
constructor(
rootStore: IRootStore,
dataUrl: string,
headers?: {
[prop: string]: any
},
options?: { identifier: string }
) {
super(rootStore, dataUrl, headers, options)
}
// loadBySchema is not supported in this store
async loadBySchema(schema: string) {
return []
}
}
-14
View File
@@ -38,7 +38,6 @@ import { getQueryClient } from 'data/query-client'
import { tableKeys } from 'data/tables/keys'
import { getTable } from 'data/tables/table-query'
import { getTables } from 'data/tables/tables-query'
import ExtensionsStore from './ExtensionsStore'
import ForeignTableStore, { IForeignTableStore } from './ForeignTableStore'
import FunctionsStore from './FunctionsStore'
import MaterializedViewStore, { IMaterializedViewStore } from './MaterializedViewStore'
@@ -63,7 +62,6 @@ export interface IMetaStore {
policies: IPostgresMetaInterface<any>
triggers: IPostgresMetaInterface<any>
functions: IPostgresMetaInterface<any>
extensions: IPostgresMetaInterface<any>
publications: IPostgresMetaInterface<any>
projectRef?: string
@@ -153,7 +151,6 @@ export default class MetaStore implements IMetaStore {
policies: PoliciesStore
triggers: TriggersStore
functions: FunctionsStore
extensions: ExtensionsStore
publications: PublicationStore
projectRef?: string
@@ -195,14 +192,6 @@ export default class MetaStore implements IMetaStore {
this.policies = new PoliciesStore(this.rootStore, `${this.baseUrl}/policies`, this.headers)
this.triggers = new TriggersStore(this.rootStore, `${this.baseUrl}/triggers`, this.headers)
this.functions = new FunctionsStore(this.rootStore, `${this.baseUrl}/functions`, this.headers)
this.extensions = new ExtensionsStore(
this.rootStore,
`${this.baseUrl}/extensions`,
this.headers,
{
identifier: 'name',
}
)
this.publications = new PublicationStore(
this.rootStore,
`${this.baseUrl}/publications`,
@@ -1003,9 +992,6 @@ export default class MetaStore implements IMetaStore {
this.functions.setUrl(`${this.baseUrl}/functions`)
this.functions.setHeaders(this.headers)
this.extensions.setUrl(`${this.baseUrl}/extensions`)
this.extensions.setHeaders(this.headers)
this.publications.setUrl(`${this.baseUrl}/publications`)
this.publications.setHeaders(this.headers)
}