File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -120,4 +120,45 @@ describe('withPostgres', () => {
120120 expect ( h . issued ) . toContain ( 'rollback' )
121121 expect ( h . release ) . toHaveBeenCalled ( )
122122 } )
123+
124+ it ( 'appends a grants hint to permission-denied (42501) errors' , async ( ) => {
125+ // begin, set_config, set role succeed; the user query hits missing grants.
126+ h . clientQuery
127+ . mockImplementationOnce ( async ( t : string ) => {
128+ h . issued . push ( t )
129+ return { rows : [ ] }
130+ } )
131+ . mockImplementationOnce ( async ( t : string ) => {
132+ h . issued . push ( t )
133+ return { rows : [ ] }
134+ } )
135+ . mockImplementationOnce ( async ( t : string ) => {
136+ h . issued . push ( t )
137+ return { rows : [ ] }
138+ } )
139+ . mockImplementationOnce ( async ( ) => {
140+ const err = new Error ( 'permission denied for table notes' ) as Error & {
141+ code : string
142+ }
143+ err . code = '42501'
144+ throw err
145+ } )
146+
147+ const handler = withPostgres ( async ( _req , ctx ) => {
148+ await ctx . postgres . query ( 'select * from notes' )
149+ return Response . json ( { ok : true } )
150+ } )
151+
152+ await expect (
153+ handler ( new Request ( 'http://localhost' ) , {
154+ _runtime : runtime ,
155+ jwtClaims : { role : 'authenticated' } ,
156+ } ) ,
157+ ) . rejects . toThrow (
158+ / p e r m i s s i o n d e n i e d f o r t a b l e n o t e s \( R L S - s c o p e d q u e r i e s r u n a s t h e c a l l e r ' s r o l e ' a u t h e n t i c a t e d ' / ,
159+ )
160+
161+ expect ( h . issued ) . toContain ( 'rollback' )
162+ expect ( h . release ) . toHaveBeenCalled ( )
163+ } )
123164} )
Original file line number Diff line number Diff line change @@ -71,6 +71,11 @@ export interface WithPostgresConfig {
7171 * Standalone (no `withSupabase`), pair it with `withClaims` so `ctx.jwtClaims`
7272 * is present before it runs.
7373 *
74+ * > **Table grants.** Queries run as `authenticated` or `anon`, so those
75+ * > roles need explicit table privileges (e.g. `grant select, insert on
76+ * > <table> to authenticated`) in addition to RLS policies. A missing grant
77+ * > fails with `permission denied` (SQLSTATE 42501) before RLS is consulted.
78+ *
7479 * > **Runtime note.** `pg` needs raw TCP, so this runs on Node/Deno (including
7580 * > the Supabase Edge runtime), **not** on Workers-style isolates.
7681 *
@@ -114,6 +119,10 @@ export const withPostgres = defineMiddleware<
114119 return res . rows as T [ ]
115120 } catch ( e ) {
116121 await client . query ( 'rollback' )
122+ // 42501 insufficient_privilege: the role lacks table grants.
123+ if ( e instanceof Error && ( e as { code ?: string } ) . code === '42501' ) {
124+ e . message += ` (RLS-scoped queries run as the caller's role '${ role } ' — grant that role the table privileges it needs, e.g. "grant select on <table> to ${ role } ")`
125+ }
117126 throw e
118127 } finally {
119128 client . release ( )
You can’t perform that action at this time.
0 commit comments