import { zodResolver } from '@hookform/resolvers/zod' import { PostgresFunction } from '@supabase/postgres-meta' 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 'react-hot-toast' import z from 'zod' import { POSTGRES_DATA_TYPES } from 'components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor.constants' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' 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 { useDatabaseFunctionUpdateMutation } from 'data/database-functions/database-functions-update-mutation' import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas' import type { FormSchema } from 'types' import { Button, FormControl_Shadcn_, FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, FormMessage_Shadcn_, Form_Shadcn_, Input_Shadcn_, Radio, ScrollArea, SelectContent_Shadcn_, SelectItem_Shadcn_, SelectTrigger_Shadcn_, SelectValue_Shadcn_, Select_Shadcn_, Separator, Sheet, SheetContent, SheetFooter, SheetSection, Toggle, cn, useWatch_Shadcn_, } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { convertArgumentTypes, convertConfigParams } from '../Functions.utils' import { CreateFunctionHeader } from './CreateFunctionHeader' import { FunctionEditor } from './FunctionEditor' const FORM_ID = 'create-function-sidepanel' interface CreateFunctionProps { func?: PostgresFunction visible: boolean setVisible: (value: boolean) => 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(), }) const CreateFunction = ({ func, visible, setVisible }: CreateFunctionProps) => { const { project } = useProjectContext() const [isClosingPanel, setIsClosingPanel] = useState(false) const [advancedSettingsShown, setAdvancedSettingsShown] = useState(false) // For now, there's no AI assistant for functions const [assistantVisible, setAssistantVisible] = useState(false) const [focusedEditor, setFocusedEditor] = useState(false) const isEditing = !!func?.id const form = useForm>({ resolver: zodResolver(FormSchema), }) const { mutate: createDatabaseFunction, isLoading: isCreating } = useDatabaseFunctionCreateMutation() const { mutate: updateDatabaseFunction, isLoading: isUpdating } = useDatabaseFunctionUpdateMutation() function isClosingSidePanel() { form.formState.isDirty ? setIsClosingPanel(true) : setVisible(!visible) } const onSubmit: SubmitHandler> = async (data) => { if (!project) return console.error('Project is required') const payload = { ...data, args: data.args.map((x) => `${x.name} ${x.type}`), config_params: mapValues(keyBy(data.config_params, 'name'), 'value') as Record, } if (isEditing) { updateDatabaseFunction( { id: func.id, projectRef: project.ref, connectionString: project.connectionString, payload, }, { onSuccess: () => { toast.success(`Successfully updated function ${data.name}`) setVisible(!visible) }, } ) } else { createDatabaseFunction( { projectRef: project.ref, connectionString: project.connectionString, payload, }, { onSuccess: () => { toast.success(`Successfully created function ${data.name}`) setVisible(!visible) }, } ) } } useEffect(() => { if (visible) { 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, }) } }, [visible, func]) return ( isClosingSidePanel()}>
( )} /> ( field.onChange(name)} /> )} /> {!isEditing && ( ( {/* Form selects don't need form controls, otherwise the CSS gets weird */} {['void', 'record', 'trigger', 'integer', ...POSTGRES_DATA_TYPES].map( (option) => ( {option} ) )} )} /> )} (
Definition

The language below should be written in{' '} .

{!isEditing &&

Change the language in the Advanced Settings below.

}
)} />
{isEditing ? ( <> ) : ( <>
setAdvancedSettingsShown(!advancedSettingsShown)} label="Show advanced settings" checked={advancedSettingsShown} labelOptional="These are settings that might be familiar for Postgres developers" />
{advancedSettingsShown && ( <> ( {/* Form selects don't need form controls, otherwise the CSS gets weird */} immutable stable volatile )} />
Type of Security
( {/* TODO: This RadioGroup imports Formik state, replace it with a clean component */} field.onChange(event.target.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. } /> )} />
)} )}
{assistantVisible ? (
{/* This is where the AI assistant would be added */}
) : null} setIsClosingPanel(false)} onConfirm={() => { setIsClosingPanel(false) setVisible(!visible) }} >

There are unsaved changes. Are you sure you want to close the panel? Your changes will be lost.

) } export default CreateFunction 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 ? ( ) : ( <> {['integer', ...POSTGRES_DATA_TYPES].map((option) => ( {option} ))} )} )} /> {!readonly && (
) })} {!readonly && ( )}
) } interface FormFieldConfigParamsProps { readonly?: boolean } const FormFieldConfigParams = ({ readonly }: FormFieldConfigParamsProps) => { const { fields, append, remove } = useFieldArray>({ name: 'config_params', }) return ( <>
Configuration Parameters
{readonly && isEmpty(fields) && ( No argument for this function )} {fields.map((field, index) => { return (
( )} /> ( )} /> {!readonly && (
) })} {!readonly && ( )}
) } const ALL_ALLOWED_LANGUAGES = ['plpgsql', 'sql', 'plcoffee', 'plv8', 'plls'] const FormFieldLanguage = () => { const { project } = useProjectContext() 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 */} {allowedLanguages.map((option) => ( {option} ))} )} /> ) } const FormLanguage = () => { const language = useWatch_Shadcn_({ name: 'language' }) return language }