Files
supabase/packages/pg-meta/test/triggers.test.ts
Charis e925385415 studio,pg-meta: SafeSql for functions/policies/triggers (3/7) (#45990)
## Summary

Third PR in the SafeSql migration stack. Flips the input/output types on
`pgMeta.functions/policies/triggers`'s `.create/.update/.remove` to use
`SafeSqlFragment`, and updates every Studio consumer atomically.

### pg-meta
- `pgMeta.functions/policies/triggers` `.create/.update/.remove` now
return `{ sql: SafeSqlFragment }` and accept branded input parameters
(`PGFunctionCreate`, `PGSavedFunction`, `PolicyCreate/UpdateParams`,
`PGTriggerCreate` with branded condition).
- `QueryModifier.toSql()` returns `SafeSqlFragment`.

### Studio consumers updated to the new branded API
- `data/database-functions/*` (query, create/update/delete mutations)
- `data/database-policies/*` (create, update mutations)
- `data/database-triggers/database-trigger-update-transaction-mutation`
- `components/Database/Triggers/TriggerSheet`
- `components/Database/Functions/CreateFunction`
- `components/Auth/Policies/PolicyEditorPanel`

These consumers land atomically with the pg-meta API change because the
input-type strictness flip (string → `SafeSqlFragment` for SQL fields)
forces every call site to update together.

## Stack

- 1/7: #45897 (merged)
- 2/7: #45903 (merged)
- 3/7: this PR
- 4/7–7/7: upcoming

## Test plan

- [x] `pnpm typecheck` passes
- [x] `pnpm --filter @supabase/pg-meta test` passes
- [x] Dev-server smoke test: function editor, policy editor, trigger
sheet

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Strengthened SQL safety across policy, function, and trigger workflows
by converting raw SQL strings to typed SQL fragments and safer
composition
* Updated editor behavior to handle policy conditions/checks as typed
SQL fragments with improved initialization and template handling
* Aligned query and modifier interfaces to return typed SQL fragments
for safer composition

* **Tests**
* Updated tests to use typed SQL fragments and synchronous builders
where applicable

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45990)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-15 12:08:20 -04:00

366 lines
9.6 KiB
TypeScript

import { afterAll, beforeAll, expect, test } from 'vitest'
import pgMeta, { safeSql } from '../src/index'
import { cleanupRoot, createTestDatabase } from './db/utils'
beforeAll(async () => {
// Any global setup if needed
})
afterAll(async () => {
await cleanupRoot()
})
const withTestDatabase = (
name: string,
fn: (db: Awaited<ReturnType<typeof createTestDatabase>>) => Promise<void>
) => {
test(name, async () => {
const db = await createTestDatabase()
try {
await fn(db)
} finally {
await db.cleanup()
}
})
}
withTestDatabase('retrieve, create, update, delete', async ({ executeQuery }) => {
// Create trigger
const { sql: createSql } = pgMeta.triggers.create({
name: 'test_trigger',
schema: 'public',
table: 'users_audit',
function_schema: 'public',
function_name: 'audit_action',
function_args: ['test1', 'test2'],
activation: 'AFTER',
events: ['UPDATE'],
orientation: 'ROW',
condition: safeSql`(old.* IS DISTINCT FROM new.*)`,
})
await executeQuery(createSql)
const { sql: listTriggers, zod: listZod } = pgMeta.triggers.list()
const listedTriggers = await listZod.parse(await executeQuery(listTriggers))
const createdTriggers = listedTriggers.find(
(t) => t.name === 'test_trigger' && t.table === 'users_audit' && t.schema === 'public'
)
expect(createdTriggers!).toMatchInlineSnapshot(
{ id: expect.any(Number), table_id: expect.any(Number) },
`
{
"activation": "AFTER",
"condition": "(old.* IS DISTINCT FROM new.*)",
"enabled_mode": "ORIGIN",
"events": [
"UPDATE",
],
"function_args": [
"test1",
"test2",
],
"function_name": "audit_action",
"function_schema": "public",
"id": Any<Number>,
"name": "test_trigger",
"orientation": "ROW",
"schema": "public",
"table": "users_audit",
"table_id": Any<Number>,
}
`
)
// Retrieve created trigger by name
const { sql: retrieveSqlByName, zod: retrieveZod } = pgMeta.triggers.retrieve({
name: 'test_trigger',
table: 'users_audit',
schema: 'public',
})
const trigger = retrieveZod.parse((await executeQuery(retrieveSqlByName))[0])
expect(trigger!).toMatchInlineSnapshot(
{ id: expect.any(Number), table_id: expect.any(Number) },
`
{
"activation": "AFTER",
"condition": "(old.* IS DISTINCT FROM new.*)",
"enabled_mode": "ORIGIN",
"events": [
"UPDATE",
],
"function_args": [
"test1",
"test2",
],
"function_name": "audit_action",
"function_schema": "public",
"id": Any<Number>,
"name": "test_trigger",
"orientation": "ROW",
"schema": "public",
"table": "users_audit",
"table_id": Any<Number>,
}
`
)
// Retrieve created trigger by id
const { sql: retrieveSqlById } = pgMeta.triggers.retrieve({
id: trigger!.id,
})
const triggerById = retrieveZod.parse((await executeQuery(retrieveSqlById))[0])
expect(triggerById!).toMatchInlineSnapshot(
{ id: expect.any(Number), table_id: expect.any(Number) },
`
{
"activation": "AFTER",
"condition": "(old.* IS DISTINCT FROM new.*)",
"enabled_mode": "ORIGIN",
"events": [
"UPDATE",
],
"function_args": [
"test1",
"test2",
],
"function_name": "audit_action",
"function_schema": "public",
"id": Any<Number>,
"name": "test_trigger",
"orientation": "ROW",
"schema": "public",
"table": "users_audit",
"table_id": Any<Number>,
}
`
)
// Update trigger
const { sql: updateSql } = pgMeta.triggers.update(trigger!, {
name: 'test_trigger_renamed',
enabled_mode: 'DISABLED',
})
await executeQuery(updateSql)
// Verify update
const { sql: retrieveUpdatedSql } = pgMeta.triggers.retrieve({
id: trigger!.id,
})
const updatedTrigger = retrieveZod.parse((await executeQuery(retrieveUpdatedSql))[0])
expect(updatedTrigger).toMatchInlineSnapshot(
{ id: expect.any(Number), table_id: expect.any(Number) },
`
{
"activation": "AFTER",
"condition": "(old.* IS DISTINCT FROM new.*)",
"enabled_mode": "DISABLED",
"events": [
"UPDATE",
],
"function_args": [
"test1",
"test2",
],
"function_name": "audit_action",
"function_schema": "public",
"id": Any<Number>,
"name": "test_trigger_renamed",
"orientation": "ROW",
"schema": "public",
"table": "users_audit",
"table_id": Any<Number>,
}
`
)
// Update trigger again
const { sql: updateSql2 } = pgMeta.triggers.update(updatedTrigger!, {
enabled_mode: 'REPLICA',
})
await executeQuery(updateSql2)
// Verify second update
const { sql: retrieveUpdated2Sql } = pgMeta.triggers.retrieve({
name: 'test_trigger_renamed',
table: 'users_audit',
schema: 'public',
})
const updatedTrigger2 = retrieveZod.parse((await executeQuery(retrieveUpdated2Sql))[0])
expect(updatedTrigger2).toMatchObject({
enabled_mode: 'REPLICA',
})
// Remove trigger
const { sql: removeSql } = pgMeta.triggers.remove(updatedTrigger2!)
await executeQuery(removeSql)
// Verify removal
const { sql: verifyRemoveSql } = pgMeta.triggers.retrieve({
name: 'test_trigger_renamed',
table: 'users_audit',
schema: 'public',
})
const result = retrieveZod.parse((await executeQuery(verifyRemoveSql))[0])
expect(result).toBeUndefined()
})
withTestDatabase('multi event', async ({ executeQuery }) => {
// Create trigger
const { sql: createSql } = pgMeta.triggers.create({
name: 'test_multi_event_trigger',
schema: 'public',
table: 'users_audit',
function_schema: 'public',
function_name: 'audit_action',
function_args: ['test1', 'test2'],
activation: 'AFTER',
events: ['insert', 'update', 'delete'],
orientation: 'ROW',
})
await executeQuery(createSql)
// Verify created trigger
const { sql: retrieveSql, zod: retrieveZod } = pgMeta.triggers.retrieve({
name: 'test_multi_event_trigger',
table: 'users_audit',
schema: 'public',
})
const trigger = retrieveZod.parse((await executeQuery(retrieveSql))[0])
expect(trigger).toMatchInlineSnapshot(
{ id: expect.any(Number), table_id: expect.any(Number) },
`
{
"activation": "AFTER",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
"DELETE",
"UPDATE",
],
"function_args": [
"test1",
"test2",
],
"function_name": "audit_action",
"function_schema": "public",
"id": Any<Number>,
"name": "test_multi_event_trigger",
"orientation": "ROW",
"schema": "public",
"table": "users_audit",
"table_id": Any<Number>,
}
`
)
// Remove trigger
const { sql: removeSql } = pgMeta.triggers.remove(trigger!)
await executeQuery(removeSql)
// Verify removal
const { sql: verifyRemoveSql } = pgMeta.triggers.retrieve({
name: 'test_multi_event_trigger',
table: 'users_audit',
schema: 'public',
})
const result = retrieveZod.parse((await executeQuery(verifyRemoveSql))[0])
expect(result).toBeUndefined()
})
withTestDatabase('triggers with the same name on different schemas', async ({ executeQuery }) => {
// Create test schemas and triggers
await executeQuery(`
create function tr_f() returns trigger language plpgsql as 'begin end';
create schema s1; create table s1.t(); create trigger tr before insert on s1.t execute function tr_f();
create schema s2; create table s2.t(); create trigger tr before insert on s2.t execute function tr_f();
`)
// List and verify triggers
const { sql: listSql, zod: listZod } = pgMeta.triggers.list()
const triggers = listZod.parse(await executeQuery(listSql))
expect(triggers.map(({ id, table_id, ...trigger }) => trigger)).toMatchInlineSnapshot(`
[
{
"activation": "BEFORE",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
],
"function_args": [],
"function_name": "tr_f",
"function_schema": "public",
"name": "tr",
"orientation": "STATEMENT",
"schema": "s1",
"table": "t",
},
{
"activation": "BEFORE",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
],
"function_args": [],
"function_name": "tr_f",
"function_schema": "public",
"name": "tr",
"orientation": "STATEMENT",
"schema": "s2",
"table": "t",
},
]
`)
})
withTestDatabase('triggers on capitalized schema and table names', async ({ executeQuery }) => {
// Create test schema and trigger
await executeQuery(`
CREATE SCHEMA "MySchema";
CREATE TABLE "MySchema"."MyTable" (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
CREATE OR REPLACE FUNCTION "MySchema"."my_trigger_function"()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at := CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER "my_trigger"
BEFORE INSERT ON "MySchema"."MyTable"
FOR EACH ROW
EXECUTE FUNCTION "MySchema"."my_trigger_function"();
`)
// List and verify triggers
const { sql: listSql, zod: listZod } = pgMeta.triggers.list()
const triggers = listZod.parse(await executeQuery(listSql))
expect(triggers.map(({ id, table_id, ...trigger }) => trigger)).toMatchInlineSnapshot(`
[
{
"activation": "BEFORE",
"condition": null,
"enabled_mode": "ORIGIN",
"events": [
"INSERT",
],
"function_args": [],
"function_name": "my_trigger_function",
"function_schema": "MySchema",
"name": "my_trigger",
"orientation": "ROW",
"schema": "MySchema",
"table": "MyTable",
},
]
`)
})