diff --git a/e2e/studio/features/database-webhooks.spec.ts b/e2e/studio/features/database-webhooks.spec.ts index 537f2e9a504..a5e9f429ee1 100644 --- a/e2e/studio/features/database-webhooks.spec.ts +++ b/e2e/studio/features/database-webhooks.spec.ts @@ -1,13 +1,14 @@ import { expect, Page } from '@playwright/test' + +import { createTable, dropTable } from '../utils/db/index.js' import { test } from '../utils/test.js' import { toUrl } from '../utils/to-url.js' -import { createTable, dropTable } from '../utils/db/index.js' const ensureWebhooksEnabled = async (page: Page, ref: string) => { await page.goto(toUrl(`/project/${ref}/integrations/webhooks/overview`)) - await expect( - page.getByText('Database Webhooks allow you to send real-time data') - ).toBeVisible({ timeout: 30000 }) + await expect(page.getByText('Database Webhooks allow you to send real-time data')).toBeVisible({ + timeout: 30000, + }) const enableButton = page.getByRole('button', { name: 'Enable webhooks' }) if ((await enableButton.count()) > 0) { @@ -37,9 +38,23 @@ const createWebhookViaUI = async (page: Page, hookName: string, tableName: strin .fill('http://localhost:3000/test-webhook') await page.getByRole('button', { name: 'Create webhook' }).click() - await expect( - page.getByText(`Successfully created new webhook "${hookName}"`) - ).toBeVisible({ timeout: 10000 }) + await expect(page.getByText(`Successfully created new webhook "${hookName}"`)).toBeVisible({ + timeout: 10000, + }) +} + +const openWebhookEditor = async (page: Page, hookName: string) => { + const webhookText = page.getByText(hookName, { exact: true }) + const webhookRow = page.locator('tr').filter({ has: webhookText }) + await webhookRow.getByRole('button').click() + await page.getByRole('menuitem', { name: 'Edit hook' }).click() + await expect(page.getByText(`Update webhook ${hookName}`)).toBeVisible({ timeout: 10000 }) +} + +const addCustomHeader = async (page: Page, name: string, value: string) => { + await page.getByRole('button', { name: 'Add a new header' }).click() + await page.getByPlaceholder('Header name').last().fill(name) + await page.getByPlaceholder('Header value').last().fill(value) } const deleteWebhookViaUI = async (page: Page, name: string) => { @@ -97,9 +112,9 @@ test.describe('Database Webhooks', () => { await page.getByRole('button', { name: 'Create webhook' }).click() - await expect( - page.getByText(`Successfully created new webhook "${hook}"`) - ).toBeVisible({ timeout: 10000 }) + await expect(page.getByText(`Successfully created new webhook "${hook}"`)).toBeVisible({ + timeout: 10000, + }) await expect(page.getByText(hook, { exact: true })).toBeVisible({ timeout: 10000 }) } finally { @@ -130,9 +145,9 @@ test.describe('Database Webhooks', () => { await page.getByRole('button', { name: 'Update webhook' }).click() - await expect( - page.getByText(`Successfully updated webhook "${hook}"`) - ).toBeVisible({ timeout: 10000 }) + await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({ + timeout: 10000, + }) await expect(page.getByText('Webhook not found')).not.toBeVisible() } finally { @@ -140,6 +155,51 @@ test.describe('Database Webhooks', () => { } }) + test('preserves webhook URL path and custom headers after editing', async ({ page, ref }) => { + const { table, hook } = uniqueNames('edit_persist') + const originalUrl = 'http://localhost:3000/test-webhook' + const updatedUrl = 'http://localhost:3000/test-webhook-updated/path' + const headerName = 'X-API-Key' + const headerValue = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12' + + await setupTable(table) + + try { + await ensureWebhooksEnabled(page, ref) + await navigateToWebhooksList(page, ref) + await createWebhookViaUI(page, hook, table) + + await openWebhookEditor(page, hook) + await addCustomHeader(page, headerName, headerValue) + await page.getByRole('button', { name: 'Update webhook' }).click() + + await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({ + timeout: 10000, + }) + + await openWebhookEditor(page, hook) + await expect(page.getByPlaceholder('http://api.com/path/resource')).toHaveValue(originalUrl) + await expect(page.getByPlaceholder('Header name').last()).toHaveValue(headerName) + await expect(page.getByPlaceholder('Header value').last()).toHaveValue(headerValue) + + const urlInput = page.getByPlaceholder('http://api.com/path/resource') + await urlInput.clear() + await urlInput.fill(updatedUrl) + await page.getByRole('button', { name: 'Update webhook' }).click() + + await expect(page.getByText(`Successfully updated webhook "${hook}"`)).toBeVisible({ + timeout: 10000, + }) + + await openWebhookEditor(page, hook) + await expect(page.getByPlaceholder('http://api.com/path/resource')).toHaveValue(updatedUrl) + await expect(page.getByPlaceholder('Header name').last()).toHaveValue(headerName) + await expect(page.getByPlaceholder('Header value').last()).toHaveValue(headerValue) + } finally { + await dropTable(table) + } + }) + test('can delete a webhook with correct toast message', async ({ page, ref }) => { const { table, hook } = uniqueNames('delete') await setupTable(table) diff --git a/packages/pg-meta/src/sql/studio/database/triggers.ts b/packages/pg-meta/src/sql/studio/database/triggers.ts index 7fba812d058..c3c191137a3 100644 --- a/packages/pg-meta/src/sql/studio/database/triggers.ts +++ b/packages/pg-meta/src/sql/studio/database/triggers.ts @@ -1,4 +1,4 @@ -import { ident } from '../../../pg-format' +import { literal } from '../../../pg-format' import { PGTrigger, PGTriggerCreate } from '../../../pg-meta-triggers' // [Joshen] Writing this query within FE as the PATCH endpoint from pg-meta only supports updating @@ -32,7 +32,7 @@ BEGIN; DROP TRIGGER "${originalTrigger.name}" ON "${originalTrigger.schema}"."${originalTrigger.table}"; CREATE TRIGGER "${name}" ${activation} ${events.join(' OR ')} ON "${schema}"."${table}" FOR EACH ROW EXECUTE FUNCTION - "${function_schema}"."${function_name}"(${function_args?.map(ident).join(',') ?? ''}); + "${function_schema}"."${function_name}"(${function_args?.map(literal).join(',') ?? ''}); COMMIT; `.trim() }