feat: Make the protected schemas dynamic, namespace schemas are now protected (#37290)

* Add hooks for async protected schemas.

* Migrate the ProtectedSchemaWarning to support the new implementation.

* sq

* Migrate all uses of protected schemas to the new approach.

* Delete extra file.

* Refactor the import foreign schema dialog to forbid protected and exposed schemas.

* Add the type to the protected schema.

* Revert ImportForeignSchemaDialog, it'll be addressed in another PR.

* Update apps/studio/hooks/useProtectedSchemas.ts

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* Fix a bad commit.

* Minor fixes.

* Fix the FDW delete mutation to handle names with numbers.

* Simplify the logic to skip a fetch.

* Minor fixes.

* Make the useIcebergFdwSchemasQuery work for all iceberg FDWs.

* Fix the tab schemas to always show in the Table Editor.

* Apply suggestion from @joshenlim

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* Fix a minor typo.

* Refactor ProtectedSchemaWarning to use Admonition, and standardise input field for target schema iceberg

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Ivan VasilovandJoshen Lim authored and GitHub committed 2025-07-30 11:38:50 +02:00
1 parent 0a985a5643
commit b3c6992e56
28 files changed
+342 -285

No files matched your search

@@ -10,7 +10,7 @@ import {
PolicyTableRow,
PolicyTableRowProps,
} from 'components/interfaces/Auth/Policies/PolicyTableRow'
import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
@@ -13,7 +13,7 @@ import {
useEnumeratedTypesQuery,
} from 'data/enumerated-types/enumerated-types-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
DropdownMenu,
@@ -22,7 +22,7 @@ import {
DropdownMenuTrigger,
Input,
} from 'ui'
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import CreateEnumeratedTypeSidePanel from './CreateEnumeratedTypeSidePanel'
import DeleteEnumeratedTypeModal from './DeleteEnumeratedTypeModal'
import EditEnumeratedTypeSidePanel from './EditEnumeratedTypeSidePanel'
@@ -52,11 +52,7 @@ const EnumeratedTypes = () => {
)
: enumeratedTypes.filter((x) => x.schema === selectedSchema)
const protectedSchemas = (schemas ?? []).filter((schema) =>
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
return (
<div className="space-y-4">
@@ -81,7 +77,7 @@ const EnumeratedTypes = () => {
<div className="flex items-center gap-2">
<DocsButton href="https://www.postgresql.org/docs/current/datatype-enum.html" />
{!isLocked && (
{!isSchemaLocked && (
<Button
className="ml-auto flex-1"
type="primary"
@@ -93,7 +89,9 @@ const EnumeratedTypes = () => {
</div>
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="enumerated types" />}
{isSchemaLocked && (
<ProtectedSchemaWarning schema={selectedSchema} entity="enumerated types" />
)}
{isLoading && <GenericSkeletonLoader />}
@@ -140,7 +138,7 @@ const EnumeratedTypes = () => {
<Table.td>{type.name}</Table.td>
<Table.td>{type.enums.join(', ')}</Table.td>
<Table.td>
{!isLocked && (
{!isSchemaLocked && (
<div className="flex justify-end items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -13,7 +13,7 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
import { useDatabaseFunctionCreateMutation } from 'data/database-functions/database-functions-create-mutation'
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
import { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import type { FormSchema } from 'types'
import {
Button,
@@ -149,6 +149,8 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
}
}, [visible, func])
const { data: protectedSchemas } = useProtectedSchemas()
return (
<Sheet open={visible} onOpenChange={() => isClosingSidePanel()}>
<SheetContent
@@ -205,7 +207,7 @@ const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => {
<SchemaSelector
portal={false}
selectedSchemaName={field.value}
excludedSchemas={PROTECTED_SCHEMAS}
excludedSchemas={protectedSchemas?.map((s) => s.name)}
size="small"
onSelectSchema={(name) => field.onChange(name)}
/>
@@ -1,6 +1,6 @@
import type { PostgresFunction } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { noop } from 'lodash'
import { Search } from 'lucide-react'
import { useRouter } from 'next/router'
@@ -16,10 +16,10 @@ import { useDatabaseFunctionsQuery } from 'data/database-functions/database-func
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Input } from 'ui'
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import FunctionList from './FunctionList'
interface FunctionsListProps {
@@ -60,11 +60,8 @@ const FunctionsList = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas] = partition(schemas ?? [], (schema) =>
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const foundSchema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === foundSchema?.id)
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const {
data: functions,
@@ -126,7 +123,7 @@ const FunctionsList = ({
</div>
<div className="flex items-center gap-x-2">
{!isLocked && (
{!isSchemaLocked && (
<>
<ButtonTooltip
disabled={!canCreateFunctions}
@@ -169,7 +166,7 @@ const FunctionsList = ({
</div>
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="functions" />}
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="functions" />}
<Table
className="table-fixed overflow-x-auto"
@@ -192,7 +189,7 @@ const FunctionsList = ({
<FunctionList
schema={selectedSchema}
filterString={filterString}
isLocked={isLocked}
isLocked={isSchemaLocked}
editFunction={editFunction}
deleteFunction={deleteFunction}
/>
@@ -1,4 +1,4 @@
import { partition, sortBy } from 'lodash'
import { sortBy } from 'lodash'
import { AlertCircle, Search, Trash } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
@@ -14,10 +14,10 @@ import { useDatabaseIndexDeleteMutation } from 'data/database-indexes/index-dele
import { DatabaseIndex, useIndexesQuery } from 'data/database-indexes/indexes-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { Button, Input, SidePanel } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import CreateIndexSidePanel from './CreateIndexSidePanel'
const Indexes = () => {
@@ -58,11 +58,7 @@ const Indexes = () => {
},
})
const [protectedSchemas] = partition(schemas ?? [], (schema) =>
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const sortedIndexes = sortBy(allIndexes ?? [], (index) => index.name.toLocaleLowerCase())
const indexes =
@@ -122,7 +118,7 @@ const Indexes = () => {
icon={<Search size={14} />}
/>
{!isLocked && (
{!isSchemaLocked && (
<Button
className="ml-auto flex-grow lg:flex-grow-0"
type="primary"
@@ -134,7 +130,7 @@ const Indexes = () => {
)}
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="indexes" />}
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="indexes" />}
{isLoadingIndexes && <GenericSkeletonLoader />}
@@ -190,7 +186,7 @@ const Indexes = () => {
<Button type="default" onClick={() => setSelectedIndex(index)}>
View definition
</Button>
{!isLocked && (
{!isSchemaLocked && (
<Button
type="text"
className="px-1"
@@ -1,37 +1,35 @@
import { useState } from 'react'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Modal } from 'ui'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { AlertCircle } from 'lucide-react'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
cn,
} from 'ui'
export const ProtectedSchemaModal = ({
visible,
onClose,
}: {
visible: boolean
onClose: () => void
}) => {
import { INTERNAL_SCHEMAS, useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { Admonition } from 'ui-patterns'
export const ProtectedSchemaDialog = ({ onClose }: { onClose: () => void }) => {
return (
<Modal
size="medium"
visible={visible}
header="Schemas managed by Supabase"
customFooter={
<div className="flex items-center justify-end space-x-2">
<Button type="default" onClick={() => onClose()}>
Understood
</Button>
</div>
}
onCancel={() => onClose()}
>
<Modal.Content className="space-y-2">
<>
<DialogHeader>
<DialogTitle>Schemas managed by Supabase</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="space-y-2 prose">
<p className="text-sm">
The following schemas are managed by Supabase and are currently protected from write
access through the dashboard.
</p>
<div className="flex flex-wrap gap-1">
{PROTECTED_SCHEMAS.map((schema) => (
{INTERNAL_SCHEMAS.map((schema) => (
<code key={schema} className="text-xs">
{schema}
</code>
@@ -45,32 +43,67 @@ export const ProtectedSchemaModal = ({
You can, however, still interact with those schemas through the SQL Editor although we
advise you only do so if you know what you are doing.
</p>
</Modal.Content>
</Modal>
)
}
const ProtectedSchemaWarning = ({ schema, entity }: { schema: string; entity: string }) => {
const [showModal, setShowModal] = useState(false)
return (
<>
<Alert_Shadcn_>
<AlertCircle strokeWidth={2} />
<AlertTitle_Shadcn_>Currently viewing {entity} from a protected schema</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p className="mb-2">
The <code className="text-xs">{schema}</code> schema is managed by Supabase and is
read-only through the dashboard.
</p>
<Button type="default" size="tiny" onClick={() => setShowModal(true)}>
Learn more
</DialogSection>
<DialogFooter>
<div className="flex items-center justify-end space-x-2">
<Button type="default" onClick={onClose}>
Understood
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
<ProtectedSchemaModal visible={showModal} onClose={() => setShowModal(false)} />
</div>
</DialogFooter>
</>
)
}
export default ProtectedSchemaWarning
export const ProtectedSchemaWarning = ({
size = 'md',
schema,
entity,
}: {
size?: 'sm' | 'md'
schema: string
entity: string
}) => {
const [showModal, setShowModal] = useState(false)
const { isSchemaLocked, reason } = useIsProtectedSchema({ schema })
if (!isSchemaLocked) return null
return (
<Admonition
showIcon={false}
type="note"
title={
size === 'sm' ? `Viewing protected schema` : `Viewing ${entity} from a protected schema`
}
className={cn(
'[&>div>p]:prose [&>div>p]:max-w-full [&>div>p]:!leading-normal',
size === 'sm' ? '[&>div>p]:text-xs' : '[&>div>p]:text-sm'
)}
>
{reason === 'fdw' ? (
<p>
The <code className="text-xs">{schema}</code> schema is used by Supabase to connect to
analytics buckets and is read-only through the dashboard.
</p>
) : (
<>
<p className="mb-2">
The <code className="text-xs">{schema}</code> schema is managed by Supabase and is
read-only through the dashboard.
</p>
<Dialog open={showModal} onOpenChange={setShowModal}>
<DialogTrigger asChild>
<Button type="default" size="tiny" onClick={() => setShowModal(true)}>
Learn more
</Button>
</DialogTrigger>
<DialogContent>
<ProtectedSchemaDialog onClose={() => setShowModal(false)} />
</DialogContent>
</Dialog>
</>
)}
</Admonition>
)
}
@@ -1,6 +1,6 @@
import type { PostgresPublication } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
@@ -10,10 +10,10 @@ import InformationBox from 'components/ui/InformationBox'
import { Loading } from 'components/ui/Loading'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { AlertCircle, ChevronLeft, Search } from 'lucide-react'
import { Button, Input } from 'ui'
import PublicationsTableItem from './PublicationsTableItem'
import { ChevronLeft, Search, AlertCircle } from 'lucide-react'
interface PublicationsTablesProps {
selectedPublication: PostgresPublication
@@ -29,27 +29,26 @@ const PublicationsTables = ({ selectedPublication, onSelectBack }: PublicationsT
'publications'
)
const { data: protectedSchemas } = useProtectedSchemas()
const {
data: tables,
data: tablesData,
isLoading,
isSuccess,
isError,
error,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
select(tables) {
return tables.filter((table) =>
filterString.length === 0
? !PROTECTED_SCHEMAS.includes(table.schema)
: !PROTECTED_SCHEMAS.includes(table.schema) && table.name.includes(filterString)
)
},
}
)
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const tables = useMemo(() => {
return (tablesData || []).filter((table) =>
filterString.length === 0
? !protectedSchemas.find((s) => s.name === table.schema)
: !protectedSchemas.find((s) => s.name === table.schema) &&
table.name.includes(filterString)
)
}, [tablesData, protectedSchemas, filterString])
return (
<>
@@ -14,7 +14,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { isTableLike } from 'data/table-editor/table-editor-types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
DropdownMenu,
@@ -26,7 +26,7 @@ import {
TooltipContent,
TooltipTrigger,
} from 'ui'
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
interface ColumnListProps {
onAddColumn: () => void
@@ -63,7 +63,7 @@ const ColumnList = ({
? selectedTable?.columns ?? []
: selectedTable?.columns?.filter((column) => column.name.includes(filterString))) ?? []
const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
const canUpdateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
return (
@@ -81,7 +81,7 @@ const ColumnList = ({
icon={<Search size={12} />}
/>
</div>
{!isLocked && isTableEntity && (
{!isSchemaLocked && isTableEntity && (
<ButtonTooltip
icon={<Plus />}
disabled={!canUpdateColumns}
@@ -100,7 +100,9 @@ const ColumnList = ({
)}
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedTable?.schema ?? ''} entity="columns" />}
{isSchemaLocked && (
<ProtectedSchemaWarning schema={selectedTable?.schema ?? ''} entity="columns" />
)}
{isLoading && <GenericSkeletonLoader />}
@@ -156,7 +158,7 @@ const ColumnList = ({
)}
</Table.td>
<Table.td className="text-right">
{!isLocked && isTableEntity && (
{!isSchemaLocked && isTableEntity && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
@@ -183,7 +185,7 @@ const ColumnList = ({
<Tooltip>
<TooltipTrigger>
<DropdownMenuItem
disabled={!canUpdateColumns || isLocked}
disabled={!canUpdateColumns || isSchemaLocked}
onClick={() => onDeleteColumn(x)}
className="space-x-2"
>
@@ -35,7 +35,7 @@ import { useTablesQuery } from 'data/tables/tables-query'
import { useViewsQuery } from 'data/views/views-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
Checkbox_Shadcn_,
@@ -54,7 +54,7 @@ import {
TooltipTrigger,
cn,
} from 'ui'
import ProtectedSchemaWarning from '../ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import { formatAllEntities } from './Tables.utils'
interface TableListProps {
@@ -184,7 +184,7 @@ const TableList = ({
(x) => visibleTypes.includes(x.type)
)
const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const error = tablesError || viewsError || materializedViewsError || foreignTablesError
const isError = isErrorTables || isErrorViews || isErrorMaterializedViews || isErrorForeignTables
@@ -269,7 +269,7 @@ const TableList = ({
icon={<Search size={12} />}
/>
{!isLocked && (
{!isSchemaLocked && (
<ButtonTooltip
className="w-auto ml-auto"
icon={<Plus />}
@@ -290,7 +290,7 @@ const TableList = ({
</div>
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="tables" />}
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="tables" />}
{isLoading && <GenericSkeletonLoader />}
@@ -462,7 +462,7 @@ const TableList = ({
</Link>
</Button>
{!isLocked && (
{!isSchemaLocked && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
@@ -519,10 +519,10 @@ const TableList = ({
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem
disabled={!canUpdateTables || isLocked}
disabled={!canUpdateTables || isSchemaLocked}
className="!pointer-events-auto gap-x-2"
onClick={() => {
if (canUpdateTables && !isLocked) {
if (canUpdateTables && !isSchemaLocked) {
onDeleteTable({
...x,
schema: selectedSchema,
@@ -11,7 +11,7 @@ import { useDatabaseTriggerCreateMutation } from 'data/database-triggers/databas
import { useDatabaseTriggerUpdateMutation } from 'data/database-triggers/database-trigger-update-mutation'
import { useTablesQuery } from 'data/tables/tables-query'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import {
Button,
Checkbox_Shadcn_,
@@ -107,14 +107,16 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro
}
)
const { data = [], isSuccess } = useTablesQuery({
const { data = [], isSuccess: isSuccessTables } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: protectedSchemas, isSuccess: isSuccessProtectedSchemas } = useProtectedSchemas()
const isSuccess = isSuccessTables && isSuccessProtectedSchemas
const tables = data
.sort((a, b) => a.schema.localeCompare(b.schema))
.filter((a) => !PROTECTED_SCHEMAS.includes(a.schema))
.filter((a) => !protectedSchemas.find((s) => s.name === a.schema))
const isEditing = !!selectedTrigger
const form = useForm<z.infer<typeof FormSchema>>({
@@ -1,6 +1,6 @@
import { PostgresTrigger } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop, partition } from 'lodash'
import { noop } from 'lodash'
import { Plus, Search } from 'lucide-react'
import { useState } from 'react'
@@ -13,14 +13,13 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import SchemaSelector from 'components/ui/SchemaSelector'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema, useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Input } from 'ui'
import ProtectedSchemaWarning from '../../ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import TriggerList from './TriggerList'
interface TriggersListProps {
@@ -39,21 +38,15 @@ const TriggersList = ({
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const [filterString, setFilterString] = useState<string>('')
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas] = partition(schemas ?? [], (schema) =>
PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const { data: protectedSchemas } = useProtectedSchemas()
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const { data = [], isSuccess } = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const hasTables = data.filter((a) => !PROTECTED_SCHEMAS.includes(a.schema)).length > 0
const hasTables =
data.filter((a) => !protectedSchemas.find((s) => s.name === a.schema)).length > 0
const {
data: triggers,
@@ -115,7 +108,7 @@ const TriggersList = ({
onChange={(e) => setFilterString(e.target.value)}
/>
</div>
{!isLocked && (
{!isSchemaLocked && (
<div className="flex items-center gap-x-2">
<ButtonTooltip
disabled={!hasTables || !canCreateTriggers}
@@ -182,7 +175,7 @@ const TriggersList = ({
)}
</div>
{isLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="triggers" />}
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="triggers" />}
<div className="w-full overflow-hidden overflow-x-auto">
<Table
@@ -203,7 +196,7 @@ const TriggersList = ({
<TriggerList
schema={selectedSchema}
filterString={filterString}
isLocked={isLocked}
isLocked={isSchemaLocked}
editTrigger={editTrigger}
deleteTrigger={deleteTrigger}
/>
@@ -337,7 +337,7 @@ export const CreateIcebergWrapperSheet = ({
/>
<p className="text-foreground-lighter text-sm">
A new schema will be created. For security purposes, the wrapper tables
from the foreign schema cannot be created within an existing schema
from the foreign schema cannot be created within an existing schema.
</p>
</div>
</FormSectionContent>
@@ -440,7 +440,7 @@ export const CreateWrapperSheet = ({
/>
<p className="text-foreground-lighter text-sm">
A new schema will be created. For security purposes, the wrapper tables
from the foreign schema cannot be created within an existing schema
from the foreign schema cannot be created within an existing schema.
</p>
</div>
</FormSectionContent>
@@ -107,7 +107,7 @@ export const ImportForeignSchemaDialog = ({
const serverOptions = await getDecryptedParameters({
ref: project?.ref,
connectionString: project?.connectionString ?? undefined,
serverName,
wrapper,
})
const formValues: Record<string, string> = {
@@ -158,11 +158,7 @@ export const ImportForeignSchemaDialog = ({
hideFooter
visible={visible}
size="medium"
header={
<span>
Connect namespace <span className="text-brand">{namespace}</span>
</span>
}
header={<span>Connect namespace "{namespace}"</span>}
onCancel={() => onClose()}
>
<Form_Shadcn_ {...form}>
@@ -173,9 +169,9 @@ export const ImportForeignSchemaDialog = ({
name="targetSchema"
render={({ field }) => (
<FormItemLayout
label="Target Schema"
description="Enter a schema name under which the Iceberg data will be accessible. The schema will be created."
layout="vertical"
label="Specify a new schema for where the Iceberg data will be accessible"
description="A new schema will be created. For security purposes, the wrapper tables from the foreign schema cannot be created within an existing schema."
>
<Input_Shadcn_ {...field} placeholder="Enter schema name" />
</FormItemLayout>
@@ -1,4 +1,4 @@
import { getFDWs } from 'data/fdw/fdws-query'
import { type FDW } from 'data/fdw/fdws-query'
import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query'
import { INTEGRATIONS } from '../Integrations/Landing/Integrations.constants'
import { WrapperMeta } from '../Integrations/Wrappers/Wrappers.types'
@@ -7,18 +7,16 @@ import { convertKVStringArrayToJson } from '../Integrations/Wrappers/Wrappers.ut
export const getDecryptedParameters = async ({
ref,
connectionString,
serverName,
wrapper,
}: {
ref?: string
connectionString?: string
serverName: string
wrapper: FDW
}) => {
const integration = INTEGRATIONS.find((i) => i.id === 'iceberg_wrapper' && i.type === 'wrapper')
const wrapperMeta = (integration?.type === 'wrapper' && integration.meta) as WrapperMeta
const wrapperServerOptions = wrapperMeta.server.options
const FDWs = await getFDWs({ projectRef: ref, connectionString: connectionString })
const wrapper = FDWs.find((fdw) => fdw.server_name === serverName)
const serverOptions = convertKVStringArrayToJson(wrapper?.server_options ?? [])
const paramsToBeDecrypted = Object.fromEntries(
@@ -25,7 +25,7 @@ import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
Button,
@@ -60,7 +60,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
const isMaterializedView = isTableLikeMaterializedView(table)
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
const isLocked = PROTECTED_SCHEMAS.includes(table.schema)
const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema })
const { mutate: updateTable } = useTableUpdateMutation({
onError: (error) => {
@@ -200,10 +200,10 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
</TooltipContent>
</Tooltip>
)}
{isTable && !isLocked ? (
{isTable && !isSchemaLocked ? (
table.rls_enabled ? (
<>
{policies.length < 1 && !isLocked ? (
{policies.length < 1 && !isSchemaLocked ? (
<ButtonTooltip
asChild
type="default"
@@ -227,10 +227,10 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
) : (
<Button
asChild
type={policies.length < 1 && !isLocked ? 'warning' : 'default'}
type={policies.length < 1 && !isSchemaLocked ? 'warning' : 'default'}
className="group"
icon={
isLocked || policies.length > 0 ? (
isSchemaLocked || policies.length > 0 ? (
<div
className={cn(
'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
@@ -281,7 +281,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
With RLS enabled, anonymous users will not be able to read/write data in the
table.
</p>
{!isLocked && (
{!isSchemaLocked && (
<div className="mt-2">
<Button
type="default"
@@ -17,7 +17,7 @@ import {
useForeignKeyConstraintsQuery,
} from 'data/database/foreign-key-constraints-query'
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import type { Dictionary } from 'types'
import { Button, Checkbox, Input, SidePanel, Toggle } from 'ui'
import ActionBar from '../ActionBar'
@@ -83,8 +83,9 @@ const ColumnEditor = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] })
const enumTypes = (types ?? []).filter(
(type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
(type) => !protectedSchemas.find((s) => s.name === type.schema)
)
const { data: constraints } = useTableConstraintsQuery({
@@ -16,10 +16,12 @@ import {
useForeignKeyConstraintsQuery,
} from 'data/database/foreign-key-constraints-query'
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useUrlState } from 'hooks/ui/useUrlState'
import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Badge, Checkbox, Input, SidePanel } from 'ui'
import { Admonition } from 'ui-patterns'
@@ -41,8 +43,6 @@ import {
generateTableFieldFromPostgresTable,
validateFields,
} from './TableEditor.utils'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
export interface TableEditorProps {
table?: PostgresTable
@@ -100,8 +100,9 @@ const TableEditor = ({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] })
const enumTypes = (types ?? []).filter(
(type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
(type) => !protectedSchemas.find((s) => s.name === type.schema)
)
const { data: publications } = useDatabasePublicationsQuery({
@@ -14,7 +14,7 @@ import {
} from 'data/table-editor/table-editor-types'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useUrlState } from 'hooks/ui/useUrlState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAppStateSnapshot } from 'state/app-state'
import { TableEditorTableStateContextProvider } from 'state/table-editor-table'
import { createTabId, useTabsStateSnapshot } from 'state/tabs'
@@ -72,6 +72,8 @@ export const TableGridEditor = ({
}
}, [onClearDashboardHistory, router, selectedTable, tabs])
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
// NOTE: DO NOT PUT HOOKS AFTER THIS LINE
if (isLoadingSelectedTable || !projectRef) {
return (
@@ -138,8 +140,8 @@ export const TableGridEditor = ({
const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable)
const isTableSelected = isTableLike(selectedTable)
const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
const canEditViaTableEditor = isTableSelected && !isLocked
const canEditViaTableEditor = isTableSelected && !isSchemaLocked
const editable = !isReadOnly && canEditViaTableEditor
const gridKey = `${selectedTable.schema}_${selectedTable.name}`
@@ -1,14 +1,17 @@
import { type PostgresColumn } from '@supabase/postgres-meta'
import { AlertTriangle, Code, Loader2, Table2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo, useRef } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { COMMAND_MENU_SECTIONS } from 'components/interfaces/App/CommandMenu/CommandMenu.utils'
import { orderCommandSectionsByPriority } from 'components/interfaces/App/CommandMenu/ordering'
import { type SqlSnippet, useSqlSnippetsQuery } from 'data/content/sql-snippets-query'
import { useSqlSnippetsQuery, type SqlSnippet } from 'data/content/sql-snippets-query'
import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useProfile } from 'lib/profile'
import {
cn,
@@ -34,9 +37,6 @@ import {
useSetCommandMenuSize,
useSetPage,
} from 'ui-patterns/CommandMenu'
import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useEffect, useRef } from 'react'
export function useSqlEditorGotoCommands(options?: CommandOptions) {
let { ref } = useParams()
@@ -295,19 +295,20 @@ export function useQueryTableCommands(options?: CommandOptions) {
function TableSelector() {
const router = useRouter()
const project = useSelectedProject()
const { data: protectedSchemas } = useProtectedSchemas()
const {
data: tables,
data: tablesData,
isLoading,
isError,
isSuccess,
} = useTablesQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
includeColumns: true,
},
{ select: excludeSupabaseControlledSchemas }
)
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
includeColumns: true,
})
const tables = useMemo(() => {
return tablesData?.filter((table) => !protectedSchemas.find((s) => s.name === table.schema))
}, [tablesData, protectedSchemas])
return (
<CommandWrapper>
@@ -360,10 +361,6 @@ from ${formatTableIdentifier(table)}
`.trim()
}
function excludeSupabaseControlledSchemas(tables: TablesData) {
return tables.filter((table) => !PROTECTED_SCHEMAS.includes(table.schema))
}
// Not a perfectly spec-compliant regex , since Postgres also allows non-Latin
// letters and letters with diacritical marks, but quoting them defensively
// is easier than writing the regex. ¯\_(ツ)_/¯
@@ -1,30 +1,25 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { Filter, Plus } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
import { ExportDialog } from 'components/grid/components/header/ExportDialog'
import { ProtectedSchemaModal } from 'components/interfaces/Database/ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import EditorMenuListSkeleton from 'components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import InfiniteList from 'components/ui/InfiniteList'
import SchemaSelector from 'components/ui/SchemaSelector'
import { useSchemasQuery } from 'data/database/schemas-query'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query'
import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Checkbox_Shadcn_,
Label_Shadcn_,
@@ -44,14 +39,13 @@ import { useTableEditorTabsCleanUp } from '../Tabs/Tabs.utils'
import EntityListItem from './EntityListItem'
import { TableMenuEmptyState } from './TableMenuEmptyState'
const TableEditorMenu = () => {
export const TableEditorMenu = () => {
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const snap = useTableEditorStateSnapshot()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const isMobile = useBreakpoint()
const [showModal, setShowModal] = useState(false)
const [searchText, setSearchText] = useState<string>('')
const [tableToExport, setTableToExport] = useState<{ name: string; schema: string }>()
const [visibleTypes, setVisibleTypes] = useState<string[]>(Object.values(ENTITY_TYPE))
@@ -89,19 +83,9 @@ const TableEditorMenu = () => {
[data?.pages]
)
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const schema = schemas?.find((schema) => schema.name === selectedSchema)
const canCreateTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const [protectedSchemas] = partition(
(schemas ?? []).sort((a, b) => a.name.localeCompare(b.name)),
(schema) => PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
const { isSchemaLocked, reason } = useIsProtectedSchema({ schema: selectedSchema })
const { data: selectedTable } = useTableEditorQuery({
projectRef: project?.ref,
@@ -138,7 +122,7 @@ const TableEditorMenu = () => {
/>
<div className="grid gap-3 mx-4">
{!isLocked ? (
{!isSchemaLocked ? (
<ButtonTooltip
block
title="Create a new table"
@@ -161,19 +145,7 @@ const TableEditorMenu = () => {
New table
</ButtonTooltip>
) : (
<Alert_Shadcn_>
<AlertTitle_Shadcn_ className="text-sm">
Viewing protected schema
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="text-xs">
<p className="mb-2">
This schema is managed by Supabase and is read-only through the table editor
</p>
<Button type="default" size="tiny" onClick={() => setShowModal(true)}>
Learn more
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
<ProtectedSchemaWarning size="sm" schema={selectedSchema} entity="table" />
)}
</div>
</div>
@@ -282,7 +254,7 @@ const TableEditorMenu = () => {
itemProps={{
projectRef: project?.ref!,
id: Number(id),
isLocked,
isSchemaLocked,
onExportCLI: () => {
const entity = entityTypes?.find((x) => x.id === id)
if (!entity) return
@@ -308,7 +280,6 @@ const TableEditorMenu = () => {
if (!open) setTableToExport(undefined)
}}
/>
<ProtectedSchemaModal visible={showModal} onClose={() => setShowModal(false)} />
</>
)
}
@@ -2,10 +2,10 @@ import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { AnimatePresence, motion } from 'framer-motion'
import { X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useMemo } from 'react'
import { EntityTypeIcon } from 'components/ui/EntityTypeIcon'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useTabsStateSnapshot, type Tab } from 'state/tabs'
import { cn, TabsTrigger_Shadcn_ } from 'ui'
@@ -28,8 +28,7 @@ export const SortableTab = ({
openTabs: Tab[]
onClose: (id: string) => void
}) => {
const router = useRouter()
const currentSchema = (router.query.schema as string) || 'public'
const { selectedSchema: currentSchema } = useQuerySchemaState()
const tabs = useTabsStateSnapshot()
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: tab.id,
@@ -45,16 +44,8 @@ export const SortableTab = ({
const shouldShowSchema = useMemo(() => {
// For both table and schema tabs, show schema if:
// Any tab has a different schema than the current schema parameter
if (tab.type === 'r') {
const anyTabHasDifferentSchema = openTabs
.filter((t) => t.type === 'r')
.some((t) => t.metadata?.schema !== currentSchema)
return anyTabHasDifferentSchema
}
return false
}, [openTabs, currentSchema, tab.type])
return openTabs.some((t) => t.metadata?.schema !== currentSchema)
}, [openTabs, currentSchema])
// Create a motion version of TabsTrigger while preserving all functionality
// const MotionTabsTrigger = motion(TabsTrigger_Shadcn_)
+1 -1
View File
@@ -73,7 +73,7 @@ export const getDeleteFDWSql = ({
const deleteEncryptedSecretsSql = deleteEncryptedSecretsSqlArray.join('\n')
const sql = /* SQL */ `
drop foreign data wrapper if exists ${wrapper.name} cascade;
drop foreign data wrapper if exists "${wrapper.name}" cascade;
${deleteEncryptedSecretsSql}
`
@@ -12,7 +12,12 @@ const useIsomorphicUseQueryState = (defaultSchema: string) => {
return [defaultSchema, () => {}] as const
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useQueryState('schema', parseAsString.withDefault(defaultSchema))
return useQueryState(
'schema',
parseAsString.withDefault(defaultSchema).withOptions({
clearOnDefault: false,
})
)
}
}
+112
View File
@@ -0,0 +1,112 @@
import { uniq } from 'lodash'
import { useMemo } from 'react'
import { WRAPPER_HANDLERS } from 'components/interfaces/Integrations/Wrappers/Wrappers.constants'
import {
convertKVStringArrayToJson,
wrapperMetaComparator,
} from 'components/interfaces/Integrations/Wrappers/Wrappers.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
import { useFDWsQuery } from 'data/fdw/fdws-query'
/**
* A list of system schemas that users should not interact with
*/
export const INTERNAL_SCHEMAS = [
'auth',
'cron',
'extensions',
'information_schema',
'net',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
'pgtle',
'realtime',
'storage',
'supabase_functions',
'supabase_migrations',
'vault',
'graphql',
'graphql_public',
QUEUES_SCHEMA,
]
/**
* Get the list of schemas used by IcebergFDWs
*/
const useIcebergFdwSchemasQuery = () => {
const { project } = useProjectContext()
const result = useFDWsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const schemas = useMemo(() => {
const icebergFDWs = result.data?.filter((wrapper) =>
wrapperMetaComparator(
{ handlerName: WRAPPER_HANDLERS.ICEBERG, server: { options: [] } },
wrapper
)
)
const fdwSchemas = icebergFDWs
?.map((fdw) => convertKVStringArrayToJson(fdw.server_options))
.map((options) => options['supabase_target_schema'])
.flatMap((s) => s?.split(','))
.filter(Boolean)
return uniq(fdwSchemas)
}, [result.data])
return { ...result, data: schemas }
}
/**
* Returns a list of schemas that are protected by Supabase (internal schemas or schemas used by Iceberg FDWs).
*/
export const useProtectedSchemas = ({
excludeSchemas = [],
}: { excludeSchemas?: string[] } = {}) => {
// Stabilize the excludeSchemas array to prevent unnecessary re-computations
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableExcludeSchemas = useMemo(() => excludeSchemas, [JSON.stringify(excludeSchemas)])
const result = useIcebergFdwSchemasQuery()
const schemas = useMemo<{ name: string; type: 'fdw' | 'internal' }[]>(() => {
const internalSchemas = INTERNAL_SCHEMAS.map((s) => ({ name: s, type: 'internal' as const }))
const icebergFdwSchemas = result.data?.map((s) => ({ name: s, type: 'fdw' as const }))
const schemas = uniq([...internalSchemas, ...icebergFdwSchemas])
return schemas.filter((schema) => !stableExcludeSchemas.includes(schema.name))
}, [result.data, stableExcludeSchemas])
return { ...result, data: schemas }
}
/**
* Returns whether a given schema is protected by Supabase (internal schema or schema used by Iceberg FDWs).
*/
export const useIsProtectedSchema = ({
schema,
excludedSchemas = [],
}: {
schema: string
excludedSchemas?: string[]
}):
| { isSchemaLocked: false; reason: undefined }
| { isSchemaLocked: true; reason: 'fdw' | 'internal' } => {
const { data: schemas } = useProtectedSchemas({ excludeSchemas: excludedSchemas })
const foundSchema = schemas.find((s) => s.name === schema)
if (foundSchema) {
return {
isSchemaLocked: true,
reason: foundSchema.type,
}
}
return { isSchemaLocked: false, reason: undefined }
}
-28
View File
@@ -1,28 +0,0 @@
import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
/**
* A list of system schemas that users should not interact with
*/
export const PROTECTED_SCHEMAS = [
'auth',
'cron',
'extensions',
'information_schema',
'net',
'pgsodium',
'pgsodium_masks',
'pgbouncer',
'pgtle',
'realtime',
'storage',
'supabase_functions',
'supabase_migrations',
'vault',
'graphql',
'graphql_public',
QUEUES_SCHEMA,
]
export const PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS = PROTECTED_SCHEMAS.filter(
(x) => x !== 'extensions'
)
@@ -1,6 +1,5 @@
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { Search } from 'lucide-react'
import { useState } from 'react'
@@ -18,11 +17,10 @@ import NoPermission from 'components/ui/NoPermission'
import SchemaSelector from 'components/ui/SchemaSelector'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useUrlState } from 'hooks/ui/useUrlState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { Input } from 'ui'
@@ -76,16 +74,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState<PostgresPolicy>()
const { data: schemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const [protectedSchemas] = partition(
schemas,
(schema) => schema?.name !== 'realtime' && PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const selectedSchema = schemas?.find((s) => s.name === schema)
const isLocked = protectedSchemas.some((s) => s.id === selectedSchema?.id)
const { isSchemaLocked } = useIsProtectedSchema({ schema: schema, excludedSchemas: ['realtime'] })
const { data: policies } = useDatabasePoliciesQuery({
projectRef: project?.ref,
@@ -151,7 +140,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
schema={schema}
tables={filteredTables}
hasTables={tables.length > 0}
isLocked={isLocked}
isLocked={isSchemaLocked}
onSelectCreatePolicy={(table: string) => {
if (isInlineEditorEnabled) {
setEditorPanel({
@@ -16,7 +16,7 @@ import {
} from 'components/interfaces/Database/Privileges/Privileges.utils'
import PrivilegesHead from 'components/interfaces/Database/Privileges/PrivilegesHead'
import PrivilegesTable from 'components/interfaces/Database/Privileges/PrivilegesTable'
import ProtectedSchemaWarning from 'components/interfaces/Database/ProtectedSchemaWarning'
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -30,7 +30,7 @@ import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import type { NextPageWithLayout } from 'types'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
@@ -132,7 +132,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
const table = tableList?.find(
(table) => table.schema === selectedSchema && table.name === selectedTable
)
const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
const {
tableCheckedStates,
@@ -286,7 +286,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
)}
<PrivilegesHead
disabled={isLocked}
disabled={isSchemaLocked}
selectedSchema={selectedSchema}
selectedRole={selectedRole}
selectedTable={table}
@@ -300,7 +300,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
hasChanges={hasChanges}
isApplyingChanges={isApplyingChanges}
/>
{isLocked && (
{isSchemaLocked && (
<ProtectedSchemaWarning schema={selectedSchema} entity="column privileges" />
)}
{isLoading ? (
@@ -310,7 +310,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
) : table && tablePrivilege ? (
<div>
<PrivilegesTable
disabled={isLocked}
disabled={isSchemaLocked}
columnPrivileges={columnPrivileges}
tableCheckedStates={tableCheckedStates}
columnCheckedStates={columnCheckedStates}