improve overall seeding approach and fix some of the console noise

This commit is contained in:
Filipe Cabaço committed 2026-05-08 11:40:45 +01:00
1 parent 6f0a00e8c0
commit 2b68bcdd64
5 files changed
+190 -112

No files matched your search

@@ -1,6 +1,6 @@
import { useMonaco } from '@monaco-editor/react'
import { DatabaseZap, Download, Plus, RefreshCw, ShieldCheck, Sparkles } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Button, cn, Modal, TextArea_Shadcn_ } from 'ui'
import { Admonition } from 'ui-patterns'
@@ -28,12 +28,12 @@ import { useTableColumnsQuery } from '@/data/database/table-columns-query'
import { useProjectSchemaDDLQuery } from '@/data/rls-sandbox/project-schema-ddl-query'
import { useProjectSeedDataQuery } from '@/data/rls-sandbox/project-seed-data-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
import { getErrorMessage } from '@/lib/get-error-message'
import type { TileConfig, TileResult } from '@/lib/rls-sandbox/sandbox-core'
import { useSandbox } from '@/lib/rls-sandbox/SandboxProvider'
const SEED_ROW_LIMIT = 100
const DDL_SCHEMAS = ['public']
export interface LocalTile {
id: string
@@ -92,28 +92,12 @@ export function RLSPlayground() {
onSuccess: ({ sql }) => setAiGeneratedSQL(sql),
})
const { data: schemaDDL, refetch: refetchSchema } = useProjectSchemaDDLQuery(
{ projectRef: project?.ref, connectionString: project?.connectionString, schemas: DDL_SCHEMAS },
{ enabled: false }
)
const { refetch: refetchSeed } = useProjectSeedDataQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tables: schemaDDL?.rlsStatuses ?? [],
rowLimit: SEED_ROW_LIMIT,
},
{ enabled: false }
)
const { data: usersData } = useUsersInfiniteQuery(
{ projectRef: project?.ref, connectionString: project?.connectionString },
{ enabled: !!project?.ref }
)
const users = usersData?.pages.flatMap((p) => p.result) ?? []
// ── Postgres IntelliSense ──────────────────────────────────────────────────
const monaco = useMonaco()
const pgInfoRef = useRef<any>(null)
@@ -134,6 +118,23 @@ export function RLSPlayground() {
connectionString: project?.connectionString,
})
const ddlSchemas = useMemo(() => schemas?.map((s) => s.name) ?? [], [schemas])
const { data: schemaDDL, refetch: refetchSchema } = useProjectSchemaDDLQuery(
{ projectRef: project?.ref, connectionString: project?.connectionString, schemas: ddlSchemas },
{ enabled: false }
)
const { refetch: refetchSeed } = useProjectSeedDataQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
tables: schemaDDL?.rlsStatuses ?? [],
rowLimit: SEED_ROW_LIMIT,
},
{ enabled: false }
)
const isPgInfoReady =
isKeywordsSuccess && isFunctionsSuccess && isSchemasSuccess && isTableColumnsSuccess
if (isPgInfoReady) {
@@ -161,8 +162,11 @@ export function RLSPlayground() {
}, [isPgInfoReady, monaco])
// ──────────────────────────────────────────────────────────────────────────
const syncInProgress = useRef(false)
const syncSchema = useCallback(async () => {
if (!core) return
if (!core || syncInProgress.current) return
syncInProgress.current = true
setSchemaSyncStatus('syncing')
try {
const { data } = await refetchSchema()
@@ -173,9 +177,13 @@ export function RLSPlayground() {
} catch (err) {
setSchemaSyncStatus('error')
toast.error(`Schema sync failed: ${getErrorMessage(err) ?? ''}`)
} finally {
syncInProgress.current = false
}
}, [core, refetchSchema])
const onAutoSync = useStaticEffectEvent(syncSchema)
const seedFromProject = useCallback(async () => {
if (!core) return
setSeedStatus('syncing')
@@ -298,10 +306,10 @@ export function RLSPlayground() {
const isSandboxReady = sandboxStatus === 'ready'
useEffect(() => {
if (isSandboxReady && schemaSyncStatus === 'idle') {
syncSchema()
if (isSandboxReady && ddlSchemas.length > 0 && schemaSyncStatus === 'idle') {
onAutoSync()
}
}, [isSandboxReady, schemaSyncStatus, syncSchema])
}, [isSandboxReady, ddlSchemas.length, schemaSyncStatus, onAutoSync])
return (
<>
@@ -1,9 +1,10 @@
import { getEntityDefinitionsSql } from '@supabase/pg-meta'
import pgMeta, { getEntityDefinitionsSql } from '@supabase/pg-meta'
import type { PostgresPolicy } from '@supabase/postgres-meta'
import { useQuery } from '@tanstack/react-query'
import { z } from 'zod'
import { getDatabasePolicies } from '@/data/database-policies/database-policies-query'
import { executeSql } from '@/data/sql/execute-sql-query'
import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas'
import { quoteLiteral } from '@/lib/pg-format'
import type { UseCustomQueryOptions } from '@/types'
@@ -14,11 +15,10 @@ export interface RlsTableStatus {
rls_forced: boolean
}
export interface CustomRole {
name: string
}
export type CustomRole = z.infer<typeof pgMeta.roles.zod>
export interface ProjectSchemaDDL {
schemas: string[]
typeDefinitions: string[]
entityDefinitions: string[]
functionDefinitions: string[]
@@ -27,7 +27,13 @@ export interface ProjectSchemaDDL {
customRoles: CustomRole[]
}
const SYSTEM_ROLES = [
// Extension-owned / platform-specific schemas whose DDL depends on C extensions,
// custom operators, and platform functions that PGlite cannot replicate.
// We skip entity/function/type DDL for these but still fetch their policies —
// those may reference user tables we do load.
const SUPABASE_INTERNAL_SCHEMAS = new Set([...INTERNAL_SCHEMAS, '_realtime'])
const SYSTEM_ROLES = new Set([
'postgres',
'anon',
'authenticated',
@@ -47,17 +53,12 @@ const SYSTEM_ROLES = [
'pg_signal_backend',
'dashboard_user',
'pgbouncer',
'authenticator',
]
])
const CUSTOM_ROLES_SQL = `
SELECT rolname AS name
FROM pg_roles
WHERE rolname NOT IN (${SYSTEM_ROLES.map(quoteLiteral).join(',')})
AND rolname NOT LIKE 'pg_%'
AND rolname NOT LIKE 'supabase_%'
ORDER BY rolname
`
const pgMetaRolesList = pgMeta.roles.list()
const pgMetaFunctionsZod = pgMeta.functions.list().zod
const pgMetaPoliciesZod = pgMeta.policies.list().zod
const pgMetaTablesZod = pgMeta.tables.list().zod
function toSqlList(schemas: string[]) {
return schemas.map(quoteLiteral).join(', ')
@@ -95,37 +96,6 @@ function getTypeDefinitionsSql(schemas: string[]) {
`
}
function getFunctionDefinitionsSql(schemas: string[]) {
return `
SELECT pg_get_functiondef(p.oid) AS definition
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
JOIN pg_language l ON l.oid = p.prolang
LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
WHERE n.nspname IN (${toSqlList(schemas)})
AND p.prokind = 'f'
AND l.lanname IN ('sql', 'plpgsql')
AND p.prorettype <> 'pg_catalog.trigger'::regtype
AND d.objid IS NULL
ORDER BY n.nspname, p.proname
`
}
function getRlsStatusSql(schemas: string[]) {
return `
SELECT
n.nspname AS schema,
c.relname AS table,
c.relrowsecurity AS rls_enabled,
c.relforcerowsecurity AS rls_forced
FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE c.relkind = 'r'
AND n.nspname IN (${toSqlList(schemas)})
ORDER BY n.nspname, c.relname
`
}
type Variables = {
projectRef?: string
connectionString?: string | null
@@ -136,7 +106,12 @@ async function getProjectSchemaDDL(
{ projectRef, connectionString, schemas }: Variables,
signal?: AbortSignal
): Promise<ProjectSchemaDDL> {
const entitySql = getEntityDefinitionsSql({ schemas })
const userSchemas = schemas.filter((s) => !SUPABASE_INTERNAL_SCHEMAS.has(s))
const entitySql = getEntityDefinitionsSql({ schemas: userSchemas })
const functionsSql = pgMeta.functions.list({ includedSchemas: userSchemas }).sql
const policiesSql = pgMeta.policies.list({ includedSchemas: schemas }).sql
const tablesSql = pgMeta.tables.list({ includedSchemas: userSchemas }).sql
const [entityResult, policiesResult, rlsResult, rolesResult, functionsResult, typesResult] =
await Promise.all([
@@ -144,25 +119,23 @@ async function getProjectSchemaDDL(
{ projectRef, connectionString, sql: entitySql, queryKey: ['rls-sandbox-ddl'] },
signal
),
getDatabasePolicies({ projectRef, connectionString }, signal),
executeSql(
{
projectRef,
connectionString,
sql: getRlsStatusSql(schemas),
queryKey: ['rls-sandbox-rls'],
},
{ projectRef, connectionString, sql: policiesSql, queryKey: ['rls-sandbox-policies'] },
signal
),
executeSql(
{ projectRef, connectionString, sql: CUSTOM_ROLES_SQL, queryKey: ['rls-sandbox-roles'] },
{ projectRef, connectionString, sql: tablesSql, queryKey: ['rls-sandbox-rls'] },
signal
),
executeSql(
{ projectRef, connectionString, sql: pgMetaRolesList.sql, queryKey: ['rls-sandbox-roles'] },
signal
),
executeSql(
{
projectRef,
connectionString,
sql: getFunctionDefinitionsSql(schemas),
sql: functionsSql,
queryKey: ['rls-sandbox-functions'],
},
signal
@@ -171,24 +144,36 @@ async function getProjectSchemaDDL(
{
projectRef,
connectionString,
sql: getTypeDefinitionsSql(schemas),
sql: getTypeDefinitionsSql(userSchemas),
queryKey: ['rls-sandbox-types'],
},
signal
),
])
const roles = (rolesResult.result as z.infer<typeof pgMetaRolesList.zod>).filter(
(r) => !SYSTEM_ROLES.has(r.name) && !r.name.startsWith('pg_') && !r.name.startsWith('supabase_')
)
const functions = (functionsResult.result as z.infer<typeof pgMetaFunctionsZod>).filter(
(f) => (f.language === 'sql' || f.language === 'plpgsql') && f.return_type !== 'trigger'
)
return {
schemas: userSchemas,
typeDefinitions: (typesResult.result as { definition: string }[]).map((r) => r.definition),
entityDefinitions: (entityResult.result[0]?.data?.definitions ?? []).map(
(d: { sql: string }) => d.sql
),
functionDefinitions: (functionsResult.result as { definition: string }[]).map(
(r) => r.definition
),
policies: (policiesResult ?? []) as PostgresPolicy[],
rlsStatuses: rlsResult.result as RlsTableStatus[],
customRoles: rolesResult.result as CustomRole[],
functionDefinitions: functions.map((f) => f.complete_statement),
policies: policiesResult.result as z.infer<typeof pgMetaPoliciesZod> as PostgresPolicy[],
rlsStatuses: (rlsResult.result as z.infer<typeof pgMetaTablesZod>).map((t) => ({
schema: t.schema,
table: t.name,
rls_enabled: t.rls_enabled,
rls_forced: t.rls_forced,
})),
customRoles: roles,
}
}
+30 -21
View File
@@ -65,6 +65,7 @@ async function applyDDLWithRetries(sandbox: Executor, ddlStatements: string[]):
export async function applySchema(
sandbox: Executor,
{
schemas,
typeDefinitions,
entityDefinitions,
functionDefinitions,
@@ -73,6 +74,17 @@ export async function applySchema(
customRoles,
}: ProjectSchemaDDL
): Promise<void> {
for (const schema of schemas) {
if (schema === 'public') continue
const safe = schema.replace(/"/g, '""')
await tryExec(sandbox, `CREATE SCHEMA IF NOT EXISTS "${safe}"`, `schema ${schema}`)
await tryExec(
sandbox,
`GRANT USAGE ON SCHEMA "${safe}" TO anon, authenticated, service_role`,
`grant schema ${schema}`
)
}
if (customRoles.length > 0) {
const checks = customRoles
.map(({ name }) => {
@@ -87,28 +99,25 @@ export async function applySchema(
await applyDDLWithRetries(sandbox, typeDefinitions)
await applyDDLWithRetries(sandbox, entityDefinitions)
await tryExec(
sandbox,
`GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role`,
'grant schema usage'
)
for (const schema of [...new Set(rlsStatuses.map((t) => t.schema))]) {
const safe = schema.replace(/"/g, '""')
await tryExec(
sandbox,
`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "${safe}" TO anon, authenticated, service_role`,
`grant tables in schema ${schema}`
)
}
if (rlsStatuses.length > 0) {
const grants = rlsStatuses
.map(
(t) =>
`GRANT SELECT, INSERT, UPDATE, DELETE ON "${t.schema}"."${t.table}" TO anon, authenticated, service_role`
)
.join('; ')
await tryExec(sandbox, grants, 'grant roles on all tables')
const rlsEnables = rlsStatuses
.filter((t) => t.rls_enabled)
.map((t) => `ALTER TABLE "${t.schema}"."${t.table}" ENABLE ROW LEVEL SECURITY`)
.join('; ')
if (rlsEnables) {
await tryExec(sandbox, rlsEnables, 'enable RLS on all tables')
}
for (const { schema, table, rls_enabled, rls_forced } of rlsStatuses) {
const actions: string[] = []
if (rls_enabled) actions.push('ENABLE ROW LEVEL SECURITY')
if (rls_forced) actions.push('FORCE ROW LEVEL SECURITY')
if (actions.length === 0) continue
await tryExec(
sandbox,
`ALTER TABLE "${schema}"."${table}" ${actions.join(', ')}`,
`RLS on ${schema}.${table}`
)
}
// Disable check_function_bodies so functions referencing not-yet-created objects don't abort.
@@ -8,6 +8,7 @@ worker({
return new PGlite({
dataDir: 'memory://',
extensions: { pgcrypto, uuid_ossp },
debug: 0,
})
},
})
+80 -5
View File
@@ -23,7 +23,6 @@ export interface SandboxCore {
destroy(): Promise<void>
}
// All superuser-required setup runs as the PGlite bootstrap user.
// ALTER ROLE postgres SUPERUSER succeeds because the bootstrap connection owns the cluster.
// Each statement is individual so a single failure cannot abort the rest.
const SANDBOX_SETUP_STATEMENTS = [
@@ -38,6 +37,18 @@ const SANDBOX_SETUP_STATEMENTS = [
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'service_role') THEN
CREATE ROLE service_role NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'authenticator') THEN
CREATE ROLE authenticator NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'dashboard_user') THEN
CREATE ROLE dashboard_user NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'pgbouncer') THEN
CREATE ROLE pgbouncer NOLOGIN;
END IF;
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'supabase_admin') THEN
CREATE ROLE supabase_admin NOLOGIN;
END IF;
END $$`,
`ALTER ROLE service_role BYPASSRLS`,
`GRANT anon TO postgres WITH ADMIN OPTION`,
@@ -56,10 +67,76 @@ const SANDBOX_SETUP_STATEMENTS = [
`GRANT EXECUTE ON FUNCTION auth.uid() TO anon, authenticated, service_role`,
`GRANT EXECUTE ON FUNCTION auth.role() TO anon, authenticated, service_role`,
`GRANT EXECUTE ON FUNCTION auth.email() TO anon, authenticated, service_role`,
// Minimal auth table stubs — enough for FK references and policy expressions.
// Projects commonly have FKs to auth.users from public schema tables (e.g. profiles),
// so without this stub those tables fail to create and their policies can't be tested.
`CREATE TABLE IF NOT EXISTS auth.users (
instance_id uuid,
id uuid NOT NULL PRIMARY KEY,
aud varchar(255),
role varchar(255),
email varchar(255),
encrypted_password varchar(255),
email_confirmed_at timestamptz,
invited_at timestamptz,
confirmation_token varchar(255),
confirmation_sent_at timestamptz,
recovery_token varchar(255),
recovery_sent_at timestamptz,
email_change_token_new varchar(255),
email_change varchar(255),
email_change_sent_at timestamptz,
last_sign_in_at timestamptz,
raw_app_meta_data jsonb,
raw_user_meta_data jsonb,
is_super_admin boolean,
created_at timestamptz,
updated_at timestamptz,
phone text DEFAULT NULL,
phone_confirmed_at timestamptz,
phone_change text DEFAULT '',
phone_change_token varchar(255) DEFAULT '',
phone_change_sent_at timestamptz,
confirmed_at timestamptz,
email_change_token_current varchar(255) DEFAULT '',
email_change_confirm_status smallint DEFAULT 0,
banned_until timestamptz,
reauthentication_token varchar(255) DEFAULT '',
reauthentication_sent_at timestamptz,
is_sso_user boolean NOT NULL DEFAULT false,
deleted_at timestamptz,
is_anonymous boolean NOT NULL DEFAULT false
)`,
`CREATE TABLE IF NOT EXISTS auth.sessions (
id uuid NOT NULL PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at timestamptz,
updated_at timestamptz,
factor_id uuid,
aal text,
not_after timestamptz,
refreshed_at timestamp,
user_agent text,
ip inet,
tag text
)`,
`CREATE TABLE IF NOT EXISTS auth.mfa_factors (
id uuid NOT NULL PRIMARY KEY,
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
friendly_name text,
factor_type text NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL,
secret text,
phone text,
last_challenged_at timestamptz,
web_authn_credential jsonb,
web_authn_aaguid uuid
)`,
`GRANT SELECT, INSERT, UPDATE, DELETE ON auth.users, auth.sessions, auth.mfa_factors TO anon, authenticated, service_role`,
]
// ── Singleton ────────────────────────────────────────────────────────────────
let instance: SandboxCore | null = null
let initPromise: Promise<SandboxCore> | null = null
@@ -80,8 +157,6 @@ export function destroySandboxCore(): Promise<void> {
return current?.destroy() ?? Promise.resolve()
}
// ── Boot ─────────────────────────────────────────────────────────────────────
async function boot(): Promise<SandboxCore> {
const webWorker = new Worker(new URL('./pglite.worker.ts', import.meta.url), { type: 'module' })
const pg = await PGliteWorker.create(webWorker)