fix: move triggers requests to query route (#35667)

This commit is contained in:
Julien Goux authored and GitHub committed 2025-05-20 15:41:43 +08:00
1 parent a57e30f863
commit b7057fa9d8
9 files changed
+102 -126

No files matched your search

@@ -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,
})
@@ -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' },
})
}
}
@@ -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: () => {
@@ -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<typeof FormSchema> = {
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 {
@@ -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<ReturnType<typeof createDatabaseTrigger>>
@@ -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<ReturnType<typeof deleteDatabaseTrigger>>
@@ -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<ReturnType<typeof updateDatabaseTrigger>>
@@ -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<PGTrigger, 'enabled_mode'>
}
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
}
+32 -59
View File
@@ -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<typeof pgTriggerUpdateZod>
export function update(
identifier: TriggerIdentifier,
id: { name: string; schema: string; table: string },
params: PGTriggerUpdate
): {
sql: string
zod: z.ZodType<void>
} {
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<void>
} {
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,