import { zodResolver } from '@hookform/resolvers/zod' import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta/src/pg-format' import { isEmpty, isNull, keyBy, mapValues, partition } from 'lodash' import { Plus, Trash } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form' import { toast } from 'sonner' import { Button, cn, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Input, RadioGroupStacked, RadioGroupStackedItem, ScrollArea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Sheet, SheetContent, SheetFooter, SheetSection, Switch, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import z from 'zod' import { convertArgumentTypes, convertConfigParams } from '../Functions.utils' import { CreateFunctionConfigParamsSection } from './CreateFunctionConfigParamsSection' import { CreateFunctionHeader } from './CreateFunctionHeader' import { FunctionEditor } from './FunctionEditor' import { POSTGRES_DATA_TYPES } from '@/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants' import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' import SchemaSelector from '@/components/ui/SchemaSelector' import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' import { useDatabaseFunctionCreateMutation } from '@/data/database-functions/database-functions-create-mutation' import type { SavedDatabaseFunction } from '@/data/database-functions/database-functions-query' import { useDatabaseFunctionUpdateMutation } from '@/data/database-functions/database-functions-update-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' const FORM_ID = 'create-function-sidepanel' interface CreateFunctionProps { func?: SavedDatabaseFunction isDuplicating?: boolean visible: boolean onClose: () => void } const FormSchema = z.object({ name: z.string().trim().min(1), schema: z.string().trim().min(1), args: z.array(z.object({ name: z.string().trim().min(1), type: z.string().trim() })), behavior: z.enum(['IMMUTABLE', 'STABLE', 'VOLATILE']), definition: z.string().trim().min(1), language: z.string().trim(), return_type: z.string().trim(), security_definer: z.boolean(), config_params: z .array(z.object({ name: z.string().trim().min(1), value: z.string().trim().min(1) })) .optional(), }) export const CreateFunction = ({ func, visible, isDuplicating = false, onClose, }: CreateFunctionProps) => { const { data: project } = useSelectedProjectQuery() const [advancedSettingsShown, setAdvancedSettingsShown] = useState(false) const [focusedEditor, setFocusedEditor] = useState(false) const isEditing = !isDuplicating && !!func?.id const form = useForm>({ resolver: zodResolver(FormSchema as any), }) const language = form.watch('language') const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ checkIsDirty: () => form.formState.isDirty, onClose, }) const { mutate: createDatabaseFunction, isPending: isCreating } = useDatabaseFunctionCreateMutation() const { mutate: updateDatabaseFunction, isPending: isUpdating } = useDatabaseFunctionUpdateMutation() const onSubmit: SubmitHandler> = async (data) => { if (!project) return console.error('Project is required') // Submit click is the explicit user gesture that promotes form-entered SQL fragments // (`args` items, `return_type`, and each `config_params` value) to executable. const payload = { ...data, args: data.args.map((x) => acceptUntrustedSql(untrustedSql(`${x.name} ${x.type}`))), return_type: acceptUntrustedSql(untrustedSql(data.return_type)), config_params: mapValues(keyBy(data.config_params, 'name'), (item) => acceptUntrustedSql(untrustedSql(item.value)) ), } if (isEditing) { updateDatabaseFunction( { func, projectRef: project.ref, connectionString: project.connectionString, payload, }, { onSuccess: () => { toast.success(`Successfully updated function ${data.name}`) onClose() }, } ) } else { createDatabaseFunction( { projectRef: project.ref, connectionString: project.connectionString, payload, }, { onSuccess: () => { toast.success(`Successfully created function ${data.name}`) onClose() }, } ) } } useEffect(() => { if (visible) { setFocusedEditor(false) form.reset({ name: func?.name ?? '', schema: func?.schema ?? 'public', args: convertArgumentTypes(func?.argument_types || '').value, behavior: func?.behavior ?? 'VOLATILE', definition: func?.definition ?? '', language: func?.language ?? 'plpgsql', return_type: func?.return_type ?? 'void', security_definer: func?.security_definer ?? false, config_params: convertConfigParams(func?.config_params).value, }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, func?.id]) const { data: protectedSchemas } = useProtectedSchemas() return (
( )} /> ( s.name)} size="small" onSelectSchema={(name) => field.onChange(name)} /> )} /> {!isEditing && ( ( {/* Form selects don't need form controls, otherwise the CSS gets weird */} )} /> )} (
Definition

The language below should be written in {language}.

{!isEditing &&

Change the language in the Advanced Settings below.

}
)} />
{isEditing ? ( <> ) : ( <>
Show advanced settings These are settings that might be familiar for Postgres developers
setAdvancedSettingsShown(checked)} />
{advancedSettingsShown && ( <> ( {/* Form selects don't need form controls, otherwise the CSS gets weird */} )} />
Type of Security
( field.onChange(value == 'SECURITY_DEFINER') } value={field.value ? 'SECURITY_DEFINER' : 'SECURITY_INVOKER'} > Function is to be executed with the privileges of the user that calls it. } /> Function is to be executed with the privileges of the user that created it. } /> )} />
)} )}
) } interface FormFieldConfigParamsProps { readonly?: boolean } const FormFieldArgs = ({ readonly }: FormFieldConfigParamsProps) => { const { fields, append, remove } = useFieldArray>({ name: 'args', }) return ( <>
Arguments

Arguments can be referenced in the function body using either names or numbers.

{readonly && isEmpty(fields) && ( No argument for this function )} {fields.map((field, index) => { return (
( )} /> ( {readonly ? ( ) : ( <> )} )} /> {!readonly && (
) })} {!readonly && ( )}
) } const ALL_ALLOWED_LANGUAGES = ['plpgsql', 'sql', 'plcoffee', 'plv8', 'plls'] const FormFieldLanguage = () => { const { data: project } = useSelectedProjectQuery() const { data: enabledExtensions } = useDatabaseExtensionsQuery( { projectRef: project?.ref, connectionString: project?.connectionString, }, { select(data) { return partition(data, (ext) => !isNull(ext.installed_version))[0] }, } ) const allowedLanguages = useMemo(() => { return ALL_ALLOWED_LANGUAGES.filter((lang) => { if (lang.startsWith('pl')) { return enabledExtensions?.find((ex) => ex.name === lang) !== undefined } return true }) }, [enabledExtensions]) return ( ( {/* Form selects don't need form controls, otherwise the CSS gets weird */} )} /> ) }