From b7057fa9d88acd53cbf6584472f210bde2047e2d Mon Sep 17 00:00:00 2001 From: Julien Goux Date: Tue, 20 May 2025 09:41:43 +0200 Subject: [PATCH] fix: move triggers requests to query route (#35667) --- .../Database/Hooks/DeleteHookModal.tsx | 6 +- .../Database/Hooks/EditHookPanel.tsx | 9 +- .../Database/Triggers/DeleteTrigger.tsx | 6 +- .../Database/Triggers/TriggerSheet.tsx | 10 +- .../database-trigger-create-mutation.ts | 25 +++-- .../database-trigger-delete-mutation.ts | 33 ++++--- .../database-trigger-update-mutation.ts | 35 +++---- ...ase-trigger-update-transaction-mutation.ts | 13 ++- packages/pg-meta/src/pg-meta-triggers.ts | 91 +++++++------------ 9 files changed, 102 insertions(+), 126 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx b/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx index 90729724dea..08f466b1c77 100644 --- a/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx @@ -12,7 +12,7 @@ interface DeleteHookModalProps { } const DeleteHookModal = ({ selectedHook, visible, onClose }: DeleteHookModalProps) => { - const { id, name, schema } = selectedHook ?? {} + const { name, schema } = selectedHook ?? {} const { project } = useProjectContext() const { mutate: deleteDatabaseTrigger, isLoading: isDeleting } = useDatabaseTriggerDeleteMutation( @@ -28,12 +28,12 @@ const DeleteHookModal = ({ selectedHook, visible, onClose }: DeleteHookModalProp if (!project) { return console.error('Project ref is required') } - if (!id) { + if (!selectedHook) { return toast.error('Unable find selected hook') } deleteDatabaseTrigger({ - id, + trigger: selectedHook, projectRef: project.ref, connectionString: project.connectionString, }) diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index ead406fa0f7..73f739cd8f8 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -14,11 +14,12 @@ import { } from 'data/edge-functions/edge-functions-query' import { getTableEditor } from 'data/table-editor/table-editor-query' import { useTablesQuery } from 'data/tables/tables-query' -import { isValidHttpUrl, tryParseJson, uuidv4 } from 'lib/helpers' +import { isValidHttpUrl, uuidv4 } from 'lib/helpers' import { Button, Checkbox, Form, Input, Listbox, Radio, SidePanel } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import HTTPRequestFields from './HTTPRequestFields' import { AVAILABLE_WEBHOOK_TYPES, HOOK_EVENTS } from './Hooks.constants' +import { PGTriggerCreate } from '@supabase/pg-meta/src/pg-meta-triggers' export interface EditHookPanelProps { visible: boolean @@ -231,15 +232,13 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = return value }) - const payload: any = { + const payload: PGTriggerCreate = { events, activation: 'AFTER', orientation: 'ROW', - enabled_mode: 'ORIGIN', name: values.name, table: selectedTable.name, schema: selectedTable.schema, - table_id: values.table_id, function_name: 'http_request', function_schema: 'supabase_functions', function_args: [ @@ -262,7 +261,7 @@ const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) = projectRef: project?.ref, connectionString: project?.connectionString, originalTrigger: selectedHook, - updatedTrigger: payload, + updatedTrigger: { ...payload, enabled_mode: 'ORIGIN' }, }) } } diff --git a/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx b/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx index d57da933a45..bca6a011c20 100644 --- a/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/DeleteTrigger.tsx @@ -13,19 +13,19 @@ interface DeleteTriggerProps { export const DeleteTrigger = ({ trigger, visible, setVisible }: DeleteTriggerProps) => { const { project } = useProjectContext() - const { id, name, schema } = trigger ?? {} + const { name, schema } = trigger ?? {} const { mutate: deleteDatabaseTrigger, isLoading } = useDatabaseTriggerDeleteMutation() async function handleDelete() { if (!project) return console.error('Project is required') - if (!id) return console.error('Trigger ID is required') + if (!trigger) return console.error('Trigger ID is required') deleteDatabaseTrigger( { projectRef: project.ref, connectionString: project.connectionString, - id, + trigger, }, { onSuccess: () => { diff --git a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx index 4fd676ca4c3..a4e5987ba15 100644 --- a/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx +++ b/apps/studio/components/interfaces/Database/Triggers/TriggerSheet.tsx @@ -50,9 +50,9 @@ const FormSchema = z.object({ .regex(/^\S+$/, 'Name should not contain spaces or whitespaces'), schema: z.string(), table: z.string(), - activation: z.string(), - enabled_mode: z.string(), - orientation: z.string(), + activation: z.enum(['BEFORE', 'AFTER', 'INSTEAD OF']), + enabled_mode: z.enum(['ORIGIN', 'REPLICA', 'ALWAYS', 'DISABLED']), + orientation: z.enum(['ROW', 'STATEMENT']), function_name: z.string().min(1, 'Please select a database function for your trigger to call'), function_schema: z.string(), events: z.array(z.string()).min(1, 'Please select at least one event'), @@ -61,7 +61,7 @@ const FormSchema = z.object({ tableId: z.string().optional(), }) -const defaultValues = { +const defaultValues: z.infer = { name: '', schema: '', table: '', @@ -133,7 +133,7 @@ export const TriggerSheet = ({ selectedTrigger, open, setOpen }: TriggerSheetPro updateDatabaseTrigger({ projectRef: project?.ref, connectionString: project?.connectionString, - id: selectedTrigger.id, + originalTrigger: selectedTrigger, payload: { name: payload.name, enabled_mode: payload.enabled_mode }, }) } else { diff --git a/apps/studio/data/database-triggers/database-trigger-create-mutation.ts b/apps/studio/data/database-triggers/database-trigger-create-mutation.ts index afb88d8292b..d9e4e74523c 100644 --- a/apps/studio/data/database-triggers/database-trigger-create-mutation.ts +++ b/apps/studio/data/database-triggers/database-trigger-create-mutation.ts @@ -1,14 +1,15 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' - -import { handleError, post } from 'data/fetchers' +import pgMeta from '@supabase/pg-meta' import type { ResponseError } from 'types' import { databaseTriggerKeys } from './keys' +import { executeSql } from 'data/sql/execute-sql-query' +import { PGTriggerCreate } from '@supabase/pg-meta/src/pg-meta-triggers' export type DatabaseTriggerCreateVariables = { projectRef: string connectionString?: string | null - payload: any + payload: PGTriggerCreate } export async function createDatabaseTrigger({ @@ -16,20 +17,16 @@ export async function createDatabaseTrigger({ connectionString, payload, }: DatabaseTriggerCreateVariables) { - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) + const { sql } = pgMeta.triggers.create(payload) - const { data, error } = await post('/platform/pg-meta/{ref}/triggers', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - }, - body: payload, - headers, + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['trigger', 'create'], }) - if (error) handleError(error) - return data + return result } type DatabaseTriggerCreateData = Awaited> diff --git a/apps/studio/data/database-triggers/database-trigger-delete-mutation.ts b/apps/studio/data/database-triggers/database-trigger-delete-mutation.ts index 10c75a6ff24..02d88c15606 100644 --- a/apps/studio/data/database-triggers/database-trigger-delete-mutation.ts +++ b/apps/studio/data/database-triggers/database-trigger-delete-mutation.ts @@ -1,38 +1,37 @@ import type { PostgresTrigger } from '@supabase/postgres-meta' import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' - -import { del, handleError } from 'data/fetchers' +import pgMeta from '@supabase/pg-meta' import type { ResponseError } from 'types' import { databaseTriggerKeys } from './keys' +import { executeSql } from 'data/sql/execute-sql-query' export type DatabaseTriggerDeleteVariables = { - id: number + trigger: { + id: number + name: string + schema: string + table: string + } projectRef: string connectionString?: string | null } -type DeleteDatabaseTriggerResponse = PostgresTrigger & { error?: any } - export async function deleteDatabaseTrigger({ - id, + trigger, projectRef, connectionString, }: DatabaseTriggerDeleteVariables) { - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) + const { sql } = pgMeta.triggers.remove(trigger) - const { data, error } = await del('/platform/pg-meta/{ref}/triggers', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - query: { id }, - }, - headers, + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['trigger', 'delete', trigger.id], }) - if (error) handleError(error) - return data + return result } type DatabaseTriggerDeleteData = Awaited> diff --git a/apps/studio/data/database-triggers/database-trigger-update-mutation.ts b/apps/studio/data/database-triggers/database-trigger-update-mutation.ts index dbfd9a32f6d..1c0c8d62f26 100644 --- a/apps/studio/data/database-triggers/database-trigger-update-mutation.ts +++ b/apps/studio/data/database-triggers/database-trigger-update-mutation.ts @@ -1,38 +1,39 @@ import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' - -import { handleError, patch } from 'data/fetchers' +import pgMeta from '@supabase/pg-meta' import type { ResponseError } from 'types' import { databaseTriggerKeys } from './keys' +import { executeSql } from 'data/sql/execute-sql-query' +import { PGTriggerUpdate } from '@supabase/pg-meta/src/pg-meta-triggers' export type DatabaseTriggerUpdateVariables = { - id: number + originalTrigger: { + id: number + name: string + schema: string + table: string + } projectRef: string connectionString?: string | null - payload: any + payload: PGTriggerUpdate } export async function updateDatabaseTrigger({ - id, + originalTrigger, projectRef, connectionString, payload, }: DatabaseTriggerUpdateVariables) { - let headers = new Headers() - if (connectionString) headers.set('x-connection-encrypted', connectionString) + const { sql } = pgMeta.triggers.update(originalTrigger, payload) - const { data, error } = await patch('/platform/pg-meta/{ref}/triggers', { - params: { - header: { 'x-connection-encrypted': connectionString! }, - path: { ref: projectRef }, - query: { id }, - }, - body: payload, - headers, + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['trigger', 'update', originalTrigger.id], }) - if (error) handleError(error) - return data + return result } type DatabaseTriggerUpdateData = Awaited> diff --git a/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts b/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts index 9acf3c75cd5..a010202a23d 100644 --- a/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts +++ b/apps/studio/data/database-triggers/database-trigger-update-transaction-mutation.ts @@ -5,6 +5,8 @@ import { executeSql } from 'data/sql/execute-sql-query' import { quoteLiteral } from 'lib/pg-format' import type { ResponseError } from 'types' import { databaseTriggerKeys } from './keys' +import { PostgresTrigger } from '@supabase/postgres-meta' +import { PGTrigger, PGTriggerCreate } from '@supabase/pg-meta/src/pg-meta-triggers' // [Joshen] Writing this query within FE as the PATCH endpoint from pg-meta only supports updating // trigger name and enabled mode. So we'll delete and create the trigger, within a single transaction @@ -13,8 +15,8 @@ import { databaseTriggerKeys } from './keys' export type DatabaseTriggerUpdateVariables = { projectRef: string connectionString?: string | null - originalTrigger: any - updatedTrigger: any + originalTrigger: PostgresTrigger + updatedTrigger: PGTriggerCreate & Pick } export function getDatabaseTriggerUpdateSQL({ @@ -40,7 +42,12 @@ export async function updateDatabaseTrigger({ updatedTrigger, }: DatabaseTriggerUpdateVariables) { const sql = getDatabaseTriggerUpdateSQL({ originalTrigger, updatedTrigger }) - await executeSql({ projectRef, connectionString, sql }) + await executeSql({ + projectRef, + connectionString, + sql, + queryKey: ['trigger', 'update', originalTrigger.id], + }) return updatedTrigger } diff --git a/packages/pg-meta/src/pg-meta-triggers.ts b/packages/pg-meta/src/pg-meta-triggers.ts index 66fd004c76f..a6042a32f50 100644 --- a/packages/pg-meta/src/pg-meta-triggers.ts +++ b/packages/pg-meta/src/pg-meta-triggers.ts @@ -121,14 +121,14 @@ export function create({ } { const qualifiedTableName = `${ident(schema)}.${ident(table)}` const qualifiedFunctionName = `${ident(function_schema)}.${ident(function_name)}` - const triggerEvents = events.join(' OR ') - const triggerOrientation = orientation ? `FOR EACH ${orientation}` : '' - const triggerCondition = condition ? `WHEN (${condition})` : '' + const triggerEvents = events.join(' or ') + const triggerOrientation = orientation ? `for each ${orientation}` : '' + const triggerCondition = condition ? `when (${condition})` : '' const functionArgsStr = function_args.map(literal).join(',') - const sql = `CREATE TRIGGER ${ident( + const sql = `create trigger ${ident( name - )} ${activation} ${triggerEvents} ON ${qualifiedTableName} ${triggerOrientation} ${triggerCondition} EXECUTE FUNCTION ${qualifiedFunctionName}(${functionArgsStr});` + )} ${activation} ${triggerEvents} on ${qualifiedTableName} ${triggerOrientation} ${triggerCondition} execute function ${qualifiedFunctionName}(${functionArgsStr});` return { sql, @@ -144,82 +144,55 @@ export const pgTriggerUpdateZod = z.object({ export type PGTriggerUpdate = z.infer export function update( - identifier: TriggerIdentifier, + id: { name: string; schema: string; table: string }, params: PGTriggerUpdate ): { sql: string + zod: z.ZodType } { - const whereIdentifierCondition = getIdentifierWhereClause(identifier) + const qualifiedTableName = `${ident(id.schema)}.${ident(id.table)}` - const sql = ` -do $$ -declare - old record; -begin - with triggers as (${TRIGGERS_SQL}) - select * into old from triggers where ${whereIdentifierCondition}; - - if old is null then - raise exception 'Cannot find trigger: %', ${literal(whereIdentifierCondition)}; - end if; + let enabledModeSql = '' - ${ - params.enabled_mode - ? ` - execute(format('alter table %I.%I ${ - params.enabled_mode === 'DISABLED' - ? 'DISABLE' - : 'ENABLE' + - (params.enabled_mode === 'ALWAYS' || params.enabled_mode === 'REPLICA' - ? ' ' + params.enabled_mode - : '') - } TRIGGER %I', - old.schema, old.table, old.name));` - : '' + switch (params.enabled_mode) { + case 'ORIGIN': + enabledModeSql = `alter table ${qualifiedTableName} enable trigger ${ident(id.name)};` + break + case 'DISABLED': + enabledModeSql = `alter table ${qualifiedTableName} disable trigger ${ident(id.name)};` + break + case 'REPLICA': + case 'ALWAYS': + enabledModeSql = `alter table ${qualifiedTableName} enable ${params.enabled_mode} trigger ${ident(id.name)};` + break + default: + break } - ${ - params.name - ? ` - -- Using the same name in the rename clause gives an error, so only do it if the new name is different. - if ${literal(params.name)} != old.name then - execute(format('alter trigger %I on %I.%I rename to %I;', old.name, old.schema, old.table, ${literal(params.name)})); - end if;` + const updateNameSql = + params.name && params.name !== id.name + ? `alter trigger ${ident(id.name)} on ${qualifiedTableName} rename to ${ident(params.name)};` : '' - } -end -$$;` + + // updateNameSql must be last + const sql = `begin; ${enabledModeSql}; ${updateNameSql}; commit;` return { sql, + zod: z.void(), } } export function remove( - identifier: TriggerIdentifier, + id: { name: string; schema: string; table: string }, { cascade = false } = {} ): { sql: string zod: z.ZodType } { - const whereIdentifierCondition = getIdentifierWhereClause(identifier) + const qualifiedTableName = `${ident(id.schema)}.${ident(id.table)}` - const sql = ` -do $$ -declare - old record; -begin - with triggers as (${TRIGGERS_SQL}) - select * into old from triggers where ${whereIdentifierCondition}; - - if old is null then - raise exception 'Cannot find trigger'; - end if; - - execute(format('DROP TRIGGER %I ON %I.%I ${cascade ? 'CASCADE' : ''}', - old.name, old.schema, old.table)); -end -$$;` + const sql = `drop trigger ${ident(id.name)} on ${qualifiedTableName} ${cascade ? 'cascade' : ''};` return { sql,