import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { indexOf } from 'lodash' import { Lock } from 'lucide-react' import Link from 'next/link' import { useCallback, useEffect, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, Card, CardContent, CardFooter, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, Input_Shadcn_, PrePostTab, Skeleton, } from 'ui' import { GenericSkeletonLoader, PageSection, PageSectionContent } from 'ui-patterns' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { MultiSelector, MultiSelectorContent, MultiSelectorItem, MultiSelectorList, MultiSelectorTrigger, } from 'ui-patterns/multi-select' import { z } from 'zod' import { HardenAPIModal } from './HardenAPIModal' import { FormActions } from '@/components/ui/Forms/FormActions' import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation' import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' import { useSchemasQuery } from '@/data/database/schemas-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' const formSchema = z.object({ dbSchema: z.array(z.string()), dbExtraSearchPath: z.array(z.string()), maxRows: z.number().max(1000000, "Can't be more than 1,000,000"), dbPool: z .number() .min(0, 'Must be more than 0') .max(1000, "Can't be more than 1000") .optional() .nullable(), }) export const PostgrestConfig = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const [showModal, setShowModal] = useState(false) const { data: config, isError, isPending: isLoadingConfig, } = useProjectPostgrestConfigQuery({ projectRef }) const { data: extensions } = useDatabaseExtensionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data: allSchemas = [], isPending: isLoadingSchemas, isSuccess: isSuccessSchemas, } = useSchemasQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const isLoading = isLoadingConfig || isLoadingSchemas const { mutate: updatePostgrestConfig, isPending: isUpdating } = useProjectPostgrestConfigUpdateMutation({ onSuccess: () => { toast.success('Successfully saved settings') }, }) const formId = 'project-postgres-config' const hiddenSchema = [ 'auth', 'pgbouncer', 'hooks', 'extensions', 'vault', 'storage', 'realtime', 'pgsodium', 'pgsodium_masks', ] const { can: canUpdatePostgrestConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'custom_config_postgrest') const isGraphqlExtensionEnabled = (extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null const defaultValues = useMemo(() => { const dbSchema = config?.db_schema ? config?.db_schema.split(',').map((x) => x.trim()) : [] return { dbSchema, maxRows: config?.max_rows, dbExtraSearchPath: (config?.db_extra_search_path ?? '') .split(',') .map((x) => x.trim()) .filter((x) => x.length > 0 && allSchemas.find((y) => y.name === x)), dbPool: config?.db_pool, } }, [config, allSchemas]) const form = useForm>({ resolver: zodResolver(formSchema), mode: 'onChange', defaultValues, }) const schemas = allSchemas .filter((x) => { const find = indexOf(hiddenSchema, x.name) if (find < 0) return x }) .map((x) => { return { id: x.id, value: x.name, name: x.name, disabled: false, } }) ?? [] const resetForm = useCallback(() => { form.reset({ ...defaultValues }) }, [form, defaultValues]) const onSubmit = async (values: z.infer) => { if (!projectRef) return console.error('Project ref is required') // is this needed ? updatePostgrestConfig({ projectRef, dbSchema: values.dbSchema.join(', '), maxRows: values.maxRows, dbExtraSearchPath: values.dbExtraSearchPath.join(','), dbPool: values.dbPool ? values.dbPool : null, }) } useEffect(() => { if (config && isSuccessSchemas) { resetForm() } }, [config, isSuccessSchemas, resetForm]) return (
{isLoading ? ( ) : isError ? ( ) : ( <> ( {isLoadingSchemas ? (
) : ( {schemas.length <= 0 ? ( no ) : ( schemas.map((x) => ( {x.name} )) )} )}
{!field.value.includes('public') && field.value.length > 0 && (

You will not be able to query tables and views in the{' '} public schema via supabase-js or HTTP clients.

{isGraphqlExtensionEnabled && ( <>

Tables in the{' '} public schema are still exposed over our GraphQL endpoints.

)} } /> )}
)} />
( {isLoadingSchemas ? (
) : ( {allSchemas.length <= 0 ? ( no ) : ( allSchemas.map((x) => ( {x.name} )) )} )}
)} />
( field.onChange(Number(e.target.value))} /> )} /> ( field.onChange( e.target.value === '' ? null : Number(e.target.value) ) } value={field.value === null ? '' : field.value} /> )} /> )}
setShowModal(false)} />
) }