Files
supabase/apps/studio/pages/api/parse-query.ts
Joshen Lim 5f867e5f6c Feature Preview: RLS Tester (#45121)
## Context

Resolves FE-3077
Related discussion: https://github.com/orgs/supabase/discussions/45233

Verifying the correctness of your RLS policies set up has always been a
gap, as highlighted by a number of GitHub discussions like
[here](https://github.com/orgs/supabase/discussions/12269) and
[here](https://github.com/orgs/supabase/discussions/14401). As such,
we're piloting a dedicated UI for RLS testing (using role impersonation
as the base), in which you'll be able to
- Run a SQL query as a user (not logged in / logged in - this is the
role impersonation part)
- See which RLS policies are being evaluated as part of the query
- And hopefully be able to debug which policies are not set up correctly

Changes are currently set as a feature preview - and we'll iterate as we
get feedback from everyone 🙂 🙏

<img width="613" height="957" alt="image"
src="https://github.com/user-attachments/assets/83c37f8a-28fc-43b3-b0ff-e28571d8710c"
/>


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

* **New Features**
* RLS Tester: run queries as anon or authenticated users, view inferred
SQL, per-table policy summaries, and data previews of accessible rows.
* UI preview: new RLS Tester preview card and modal with opt-in toggle;
RLS Tester sheet with role/user selector and query editor.
  * SQLEditor: “Explain” tab is always visible.

* **Chores**
* Added supporting API endpoints, background checks for table RLS
status, and a local-storage flag to persist the preview opt-in.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 15:02:49 +08:00

66 lines
2.1 KiB
TypeScript

import { parse } from 'libpg-query'
import { NextApiRequest, NextApiResponse } from 'next'
const getOperation = async (sql: string) => {
const ast = await parse(sql)
const stmt = ast.stmts?.[0]?.stmt
if (!stmt) return null
if ('SelectStmt' in stmt) return 'SELECT'
if ('InsertStmt' in stmt) return 'INSERT'
if ('UpdateStmt' in stmt) return 'UPDATE'
if ('DeleteStmt' in stmt) return 'DELETE'
}
const getTablesInQuery = async (sql: string) => {
const ast = await parse(sql)
const tables: string[] = []
function traverse(node: unknown): void {
if (!node || typeof node !== 'object') return
const obj = node as Record<string, unknown>
if ('RangeVar' in obj) {
const rv = obj.RangeVar as { relname?: string; schemaname?: string }
if (rv.relname) tables.push(rv.schemaname ? `${rv.schemaname}.${rv.relname}` : rv.relname)
}
if ('relation' in obj && obj.relation && typeof obj.relation === 'object') {
const rv = obj.relation as { relname?: string; schemaname?: string }
if (rv.relname) tables.push(rv.schemaname ? `${rv.schemaname}.${rv.relname}` : rv.relname)
}
for (const value of Object.values(obj)) {
Array.isArray(value) ? value.forEach(traverse) : traverse(value)
}
}
traverse(ast)
return [...new Set(tables)].sort((a, b) => a.localeCompare(b))
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST'])
return res.status(405).json({ error: `Method ${req.method} Not Allowed` })
}
try {
const { sql } = req.body
if (typeof sql !== 'string' || sql.trim().length === 0) {
return res.status(400).json({ error: 'Missing or invalid "sql" in request body' })
}
const tables = await getTablesInQuery(sql)
const operation = await getOperation(sql)
return res.status(200).json({ tables, operation })
} catch (error) {
const message =
(error as { sqlDetails?: { message?: string } })?.sqlDetails?.message ??
(error instanceof Error ? error.message : 'Failed to parse SQL')
return res.status(400).json({ error: message })
}
}