From 8776db145ea2378b6c2611b7109bd16ffe568b7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 26 Apr 2024 12:11:06 +0200 Subject: [PATCH 01/44] Revert "ref: Add sentry logging to debug vercel issue" (#23292) Revert "ref: Add sentry logging to debug vercel issue (#23289)" This reverts commit 8f6ea0c7550e3923d9f107cb64a0277a99a8c369. --- .../interfaces/Integrations/ProjectLinker.tsx | 1 + .../SidePanelVercelProjectLinker.tsx | 13 ---------- .../data/integrations/integrations.types.ts | 1 + .../[slug]/deploy-button/new-project.tsx | 24 +++---------------- .../[slug]/marketplace/choose-project.tsx | 13 ---------- 5 files changed, 5 insertions(+), 47 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/ProjectLinker.tsx b/apps/studio/components/interfaces/Integrations/ProjectLinker.tsx index 5243301eded..03b17fab877 100644 --- a/apps/studio/components/interfaces/Integrations/ProjectLinker.tsx +++ b/apps/studio/components/interfaces/Integrations/ProjectLinker.tsx @@ -132,6 +132,7 @@ const ProjectLinker = ({ connection: { foreign_project_id: selectedForeignProject?.id, supabase_project_ref: selectedSupabaseProject?.ref, + integration_id: '0', metadata: { ...projectDetails, }, diff --git a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx index b9dd0ca7b4b..eb883e15110 100644 --- a/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx +++ b/apps/studio/components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx @@ -15,7 +15,6 @@ import { useSelectedOrganization } from 'hooks' import { BASE_PATH } from 'lib/constants' import { EMPTY_ARR } from 'lib/void' import { useSidePanelsStateSnapshot } from 'state/side-panels' -import * as Sentry from '@sentry/nextjs' const VERCEL_ICON = ( @@ -101,18 +100,6 @@ const SidePanelVercelProjectLinker = () => { const onCreateConnections = useCallback( (vars: any) => { - // TODO(kamil): Remove once "missing connection name" bug has been squashed. - if (!vars.connection?.metadata?.name) { - Sentry.captureMessage('Vercel connection is missing name', { - contexts: { - connection: vars.connection, - source: { - file: 'Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx', - }, - }, - }) - } - createConnections({ ...vars, connection: { diff --git a/apps/studio/data/integrations/integrations.types.ts b/apps/studio/data/integrations/integrations.types.ts index 773c8d76dbd..844b531a735 100644 --- a/apps/studio/data/integrations/integrations.types.ts +++ b/apps/studio/data/integrations/integrations.types.ts @@ -248,6 +248,7 @@ export type IntegrationConnectionsCreateVariables = { connection: { foreign_project_id: string supabase_project_ref: string + integration_id: string metadata: any } orgSlug: string | undefined diff --git a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx index e875e8013c4..bc19bf6d3ca 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/deploy-button/new-project.tsx @@ -23,7 +23,6 @@ import { passwordStrength } from 'lib/helpers' import { getInitialMigrationSQLFromGitHubRepo } from 'lib/integration-utils' import { useIntegrationInstallationSnapshot } from 'state/integration-installation' import type { NextPageWithLayout } from 'types' -import * as Sentry from '@sentry/nextjs' const VercelIntegration: NextPageWithLayout = () => { return ( @@ -204,11 +203,12 @@ const CreateProject = () => { const projectDetails = vercelProjects?.find((x: any) => x.id === foreignProjectId) try { - const connection = { + const { id: connectionId } = await createConnections({ organizationIntegrationId: organizationIntegration?.id, connection: { foreign_project_id: foreignProjectId, supabase_project_ref: newProjectRef, + integration_id: '0', metadata: { ...projectDetails, supabaseConfig: { @@ -219,26 +219,8 @@ const CreateProject = () => { }, }, orgSlug: selectedOrganization?.slug, - } + }) - // TODO(kamil): Remove once "missing connection name" bug has been squashed. - if (!projectDetails?.name) { - Sentry.captureMessage('Vercel connection is missing name', { - contexts: { - connection: connection, - data: { - projectDetails, - vercelProjects, - foreignProjectId, - }, - source: { - file: 'deploy-button/new-project.tsx', - }, - }, - }) - } - - const { id: connectionId } = await createConnections(connection) await syncEnvs({ connectionId }) } catch (error) { console.error('An error occurred during createConnections:', error) diff --git a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx index 8f9208c843a..d22d3a1f4ba 100644 --- a/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx +++ b/apps/studio/pages/integrations/vercel/[slug]/marketplace/choose-project.tsx @@ -19,7 +19,6 @@ import { BASE_PATH, PROJECT_STATUS } from 'lib/constants' import { EMPTY_ARR } from 'lib/void' import { useIntegrationInstallationSnapshot } from 'state/integration-installation' import type { NextPageWithLayout, Organization } from 'types' -import * as Sentry from '@sentry/nextjs' const VERCEL_ICON = ( Vercel Icon @@ -129,18 +128,6 @@ const VercelIntegration: NextPageWithLayout = () => { const onCreateConnections = useCallback( (vars: any) => { - // TODO(kamil): Remove once "missing connection name" bug has been squashed. - if (!vars.connection?.metadata?.name) { - Sentry.captureMessage('Vercel connection is missing name', { - contexts: { - connection: vars.connection, - source: { - file: 'marketplace/choose-project.tsx', - }, - }, - }) - } - createConnections({ ...vars, connection: { From 471a4fe76d79ff30c45ea8d67b662c79bdd9183d Mon Sep 17 00:00:00 2001 From: Oliver Rice Date: Fri, 26 Apr 2024 08:12:03 -0500 Subject: [PATCH 02/44] (Scheduled for 2024-04-26) blog post - Postgres Bloat (#23281) * blog post - postgres bloat * Update apps/www/_blog/2024-04-26-postgres-bloat.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/www/_blog/2024-04-26-postgres-bloat.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --- apps/www/_blog/2024-04-25-nix-postgres.mdx | 2 +- apps/www/_blog/2024-04-26-postgres-bloat.mdx | 199 ++++++++++++++++++ .../www/public/images/blog/postgres-bloat.png | Bin 0 -> 32350 bytes 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 apps/www/_blog/2024-04-26-postgres-bloat.mdx create mode 100644 apps/www/public/images/blog/postgres-bloat.png diff --git a/apps/www/_blog/2024-04-25-nix-postgres.mdx b/apps/www/_blog/2024-04-25-nix-postgres.mdx index bd733d9914f..4e25ed35f4a 100644 --- a/apps/www/_blog/2024-04-25-nix-postgres.mdx +++ b/apps/www/_blog/2024-04-25-nix-postgres.mdx @@ -5,7 +5,7 @@ author: samrose image: nix-postgres.png thumb: nix-postgres.png categories: - - product + - engineering tags: - supabase-engineering date: '2024-04-25' diff --git a/apps/www/_blog/2024-04-26-postgres-bloat.mdx b/apps/www/_blog/2024-04-26-postgres-bloat.mdx new file mode 100644 index 00000000000..773719ea995 --- /dev/null +++ b/apps/www/_blog/2024-04-26-postgres-bloat.mdx @@ -0,0 +1,199 @@ +--- +title: 'Postgres Bloat Minimization' +description: 'Understanding and minimizing Postgres table bloat' +author: pavel +image: postgres-bloat.png +thumb: postgres-bloat.png +categories: + - engineering +tags: + - supabase-engineering + - planetpg +date: '2024-04-26' +toc_depth: 3 +--- + +### How data of Postgres tables stored + +By default, all table data in Postgres are physically stored using the “heap” method. So every database is a set of 1Gb files (”segments”) and each file is logically split into 8Kb pages. Actual table rows are put into any page with enough free space. + +When the row data is updated, a new version of a whole row is constructed and written (to any free space). The old one remains because, at the time of the update, the transaction is not completed and can be rolled back in the future. When the transaction is completed we’ll have two or several versions of the same row in the table. Cleaning old ones is by an asynchronous process called vacuum (and autovacuum). + +### How does the vacuum work? + +Vacuum goes through all table files looking for row versions that were updated or deleted by already completed transactions and frees the space on the pages. + +Then it updates the table’s free-space-map to reflect that some page has a certain amount of free space for row inserts. + +It also updates the visibility map for a page. It marks that all remaining rows are visible. So index scans can skip visibility checks, which is not so for the modified page before vacuuming. This significantly increases the speed of queries using indexes. + +In many cases vacuum runs automatically, cleans everything, and requires little care. But in some scenarios, we need to go deeper and tune the autovacuum parameters or run the vacuum manually. + +### Cleaning relation files + +Vacuum marks space in a relation file as free to use for future row inserts or updates. And it’s not a problem unless we insert many rows, delete many rows at once, and then don’t make any inserts or updates. Space remains reserved in a file but we don’t use it. + +In this case, we could free actual filesystem space by running more aggressive mode: + +```sql +VACUUM FULL mytable; +``` + +It will rebuild the table from live rows and they will be placed compactly so that filesystem space will be freed. The downside is that it needs an exclusive lock and you won’t be able to modify the table while `VACUUM FULL` does it's work. It’s wise to execute that process when the database is least accessed e.g. at night. + +An alternative way that doesn’t need full locks is using pg_repack extension: + +```sh +pg_repack --table mytable test +``` + +pg_repack operates similarly to VACUUM FULL for database `test` and table `mytable`. + +### Table bloating and autovaccuum + +To see database bloat via the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) run: + +```sh +$ supabase inspect db bloat +``` + +or: + +```sql +-- number of dead rows +SELECT + n_dead_tup +FROM + pg_stat_user_tables +WHERE + relname = ‘mytable’; + +-- number of live rows +SELECT + count(*) +FROM + mytable; +``` + +If the numbers differ by more than 2x, chances are that the autovacuum didn’t start or hasn’t completed for a table. There could be several legitimate reasons for this. + +You can see information for the last successful autovacuum by running: + +```sh +$ supabase inspect db vacuum-stats +``` + +or: + +```sql +SELECT + last_vacuum, + last_autovacuum +FROM + pg_stat_user_tables +WHERE + relname = ‘mytable’; +``` + +Let’s turn on autovacuum logging so that all autovacuum events land in the log: + +```sql +ALTER TABLE mytable SET log_autovacuum_min_duration to 0; +``` + +There are two ways we can encounter this situation: + +- autovacuum hasn’t started +- autovacuum did start, (possibly multiple times) but never succeeded + +### Autovacuum hasn’t started for a table + +Autovacuum starts based on several configuration parameters like timeout and pattern of access to a particular table. Maybe it’s even legitimate that it hasn’t started. + +- `autovacuum_vacuum_threshold` - number of rows updated or deleted in a table to invoke autovacuum +- `autovacuum_vacuum_insert_threshold` - number of rows inserted into a table to invoke autovacuum +- `autovacuum_vacuum_scale_factor` - a fraction of table modified by updates or deletes to invoke autovacuum +- `autovacuum_vacuum_insert_scale_factor` - a fraction of table modified by inserts to invoke autovacuum + +With all these parameters set, the autovacuum will start if the number of rows updated or deleted exceeds: + +``` +autovacuum_vacuum_scale_factor * size_of_table + autovacuum_vacuum_threshold +``` + +The same logic applies for inserts. Default scale factors are 20% of a table, which could be too high for big tables. If we want autovacuum to occur on large tables more frequently and take less time each run, decrease the default values for these tables e.g.: + +```jsx +ALTER TABLE mytable SET autovacuum_vacuum_scale_factor to 0.05; +``` + +- `autovacuum_naptime` (default 1 min) - Each 1-minute autovacuum daemon will see the state of all tables in the database and decide whether to start autovacuum for a table. Most often this parameter does not need to be modified. + +To see global vacuum settings for your cluster let’s run: + +```jsx +SELECT * from pg_settings where category like 'Autovacuum'; +``` + +To see current settings for a table (that overrides global settings) run: + +```jsx +SELECT relname, reloptions FROM pg_class WHERE relname='mytable'; +``` + +### Autovacuum started but couldn’t succeed + +The most common reason autovacuum doesn’t succeed is long-running open transactions that access old row versions. In that case, Postgres recognizes that the row versions are still needed so any row versions created after that point can’t be marked as dead. One common cause for this problem is interactive sessions that were left open on accident. When tuples can’t be marked as dead, the database begins to bloat. + +To see all open transactions run: + +```jsx +SELECT xact_start, state FROM pg_stat_activity; +``` + +To close transactions found to be idling: + +```jsx +SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE xact_start = ''; +``` + +For automatically closing idle transactions in a session: + +```jsx +SET idle_in_transaction_session_timeout TO '10000s'; +``` + +The same parameter could be set per role or database as needed. + +Another, less likely, possibility is that autovacuum can’t succeed due to locks. If some of your processes take the `SHARE UPDATE EXCLUSIVE` lock e.g. `ALTER TABLE` clause, this lock will prevent vacuum from processing a table. Lock conflicts in your ordinary transactions could cause `SHARE UPDATE EXCLUSIVE` to be taken for a long time. A good recipe when this happens is to cancel all the open transactions and run VACUUM for a table manually (or wait until the next autovacuum comes for it). + +```jsx +SELECT pg_cancel_backend(pid) FROM pg_stat_activity WHERE state = 'active'; +VACUUM mytable; +``` + +### Other vacuum optimizations + +There could be too few autovacuum workers or each worker could operate slowly due to the low setting of `autovacuum_work_mem`. + +- `autovacuum_max_workers` (default 3) - Number of parallel workers doing autovacuum for tables. When you have enough cores, increasing the default value is worthwhile. Note that this will decrease the number of possible backends or regular parallel workers running at a time. +- `autovacuum_work_mem` (default equal to `maintenance_work_mem` or 64Mb) - work memory that is used per autovacuum worker. If you see in your logs that autovacuum for some tables starts but takes a long time, that time may be decreased by increasing this value. This parameter can only be modified in the config file. + +### Conclusion + +Vacuum and autovacuum are efficient ways to maintain the tables without bloat. They have several parameters that allow efficient tuning. Some insight into what database does can help prevent the cases where autovacuum becomes problematic and bloat increases i.e.: + +- Long open transactions +- Stuck locks +- Insufficient resources allocated to vacuuming +- Space not freed on a filesystem level after massive table modifications + +For more information about bloat and vacuuming, here are some direct references to resources: + +[Full official documentation for vacuum](https://www.postgresql.org/docs/current/routine-vacuuming.html#VACUUM-BASICS) + +[Per-table vacuum parameters](https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-STORAGE-PARAMETERS) + +[Global autovacuum parameters (most of them need modifying postgresql.conf, but could be overridden by per-table parameters)](https://www.postgresql.org/docs/current/runtime-config-autovacuum.html) + +[Supabase CLI reference](https://supabase.com/docs/reference/cli/global-flags) diff --git a/apps/www/public/images/blog/postgres-bloat.png b/apps/www/public/images/blog/postgres-bloat.png new file mode 100644 index 0000000000000000000000000000000000000000..7f660eeaef03fd4e61d4d71ab066c8acbd5fb11f GIT binary patch literal 32350 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

f{f>BeBwY`P`R~* zfk9^IW^8#PH7bzCAsmLX6@ol;{Lv$1JRuLCx;EQLe(T`~1wA(uze!l?$L{@6pjb9$ zL@iqGp!9{(J67x7UpF=OFfBI9U(*t}g326Lp)|6pZYevAs#{8k;ma=P)?NG}>0l9h z16iZ3&0}0^%ulV7YmQWVak<}0uyf4FmZJC(cb_M}JmW+=;H05-_IN#=k7rFR8GLr0 z!s8@$#~me6M$*KtyzKMb*cl+a%g+-PmKWF1H(SK>LqW^FIr^?ZN4k)kNu5wq{Wpb) zj+Q}oqC7AKHk1O^@jTSXwb3r@{KgzN7F4A*x2V*Th13^=Zs9z85!4{RnxwZ86 zuaLx3+zlN(Q`~3dK4#b6uS`t*;?j=U6h_Xg@C9WyUyfrfoyk$`UNirQBn!kMl$Q)W zELDEFNhcF_YUa0v&NRgw3o7%s6Uu4_WSZ6v%I@(NsF9Pp8I#0n^^qmKxx4ZP@YX*O zVuixEsWq#c28w$oXwpfy=P0(Q1EqOOf`Z_lT zGa2Dy#!q?JjOBt)=~+OKUb~RvH(78MI-xtk=|QX&)8Jj~jKuc!>G@Fg zPH?Vi<(2tI5TyXkq8yhn;~UN6f>X32F}e|kEP)B_6i<7X54VGjL}T|Up%|)~*A|A5 z!a~_!Pr1!v$?u?JI5aZk_0_UJ0#HqU$Dy3iWM0s zSfnHu-j`F}Wer^tyf$S@ZFZ6`*Pp(e^Z0GR`}-=~zgt6Dd-`#Iwi47grb*Dy&^TN% zAzYx-zV&sR=y}Fg%O6L~;wHINU1ef+bwas3aE@}uv91Pv>u~g?DyA8V?=n9`h0So} z5f_|BTZppB1n=d!mXEfS3+PLViFV69 zY*mNkTha?(ufT-*U+M>?baK?=6qFnl$K8S)3uv{>VG8@~yJKtEgMstC*z#6$mv*8z zV(oy$XG>sf;ZRdibxwkB$V8t>T?f#UQBu?VP_yyzw8Q9NBa4tT`q-Bg(DLtdILwp5 z2L4r>^q!&tHzox!&1TGHmo1u&g=`!lI^xNHI)#8kSMsTA>ZUdvU!nenq`Z^+Mhg$p z(zMCFvTQ)df2mF7Ths_g-uqa)C{gP~;RLcUiM~n$u$zw;GMUG zX1~%*V*GcUMD2U*cXzh6R1^;3yA{>Elj;0cp3s}bfetz_%>-ZgAgC|H`xr*cKRQpN z662Wr!Sug-MPDxKnK?C;+g)WL`1$SNjzj9t+XB`Ag8w;XzhAYw=B^h*-@O0KBaUtk zCZm(^(QKYIH#du9r0*rvSSL68O4F;-#ZpmG9WFH$(yz!HAg_W#8t5D(as7vRu?pmq z+(rq|i&X+~AXmbT3=lKdyHbGo_%4WFi7 zPZe{F@=?O?&S5!uJ=_;|QAQp*Hh?nWJ?~NDJNR}{@^UEj5C8pN@hSL*=Bp+5dOV9R zsqC?PzI9gm?`>cBNQRAAc010th6Nwa$DO0KJ_1oainZ9*WNGcmSWMgEen-V@+2kzw z)bBLh?r<5)NmZ580+hi0in#63{6*P}M}afGC&(NUvY}fTvct3z0*Ww%Xp6r)Z)M*q zPmL(n?kkf+aNoBlIpG#KMTPmA-~y_fI*Fm4gPSKRTKvKy=GU7%OKCQ#;O_`eZscd^ zoKA!pl@%2ZXPdIj7yza$LJKUd{WxuR1tp8s^OeW=H@+H>@qOJ4!$q9b&5D3_0TspS zX(?u@;tnzrRjGp_XDgtVw3aT4-so|%Sjv~EO89Cr29vi`?iV#LKcF?Wikx*cgMN_4 zo*D&63g+yr3ssYiBFURK+FDAw{r;Mwg%ll{C50En#l}@RwB_J~y}W5Y$|)pHJ!^xL z!K+POC z81yq8{yiznBy~mxIRc3>u6G#7^i4vCKiH4TeCV2>(VAJxs#rc`lDHLr!wQ%k!` zf4RhZ`Dw~DeEwBU#2=twCzc*F3*z55(^uOS4Elj0706w;WLekQuXlr4p+Tb+7Y#6w zNHATx462hQv8biK>^`h!xWBBDK{0Y3S8$%bly=RWsLtjDzY#gU<9ASlGZA5)ypNPs zHLTeLHI)!ctcd@qMi3aaXUR`R2zXlNWYr0_R#ys6-L*a7ujP=>Aw z07e^@C2S#ewte9H*kr+f z&`Q)gO+i^A?YY;-ArV~FEq2g_MH>}tG@^(V?9YL}d9gSBUb{A(A4IUZv4}(C_Ntf5 z-cKiLX@Xv64{N9(C%M*fate!nZ~-{Yq!X1RX#I#F1$8gbuKx|J!t?HOFqzZ)qEHVM zBW-PLn34xK&s@s~#>U3L4N^WA1qR%`Z^)3iXD2Xu+Tb-=&IYX*39+y~xWFpF)RTISPVX|3 z20G=E;b|3r{Lu6s$)QzDc`LSH+wm*U((2hP9<{U9CRor)1T$#*)PBnlLU|5BosVAL zYd`tlciF$#dwVeD1HL7b`xBG_S;^=G#t|?MgG&j?`)ecra&h}PEHOL^~DR~ z@tyOPm6h%7ZLsPw<2fZj7N1Mn=}ktUu{%-ba*Xb>Ns|#)2V{RFK>rGZDaAVg*+;35 zv)#GgYC9a+R&v40riicIol06{^VyeoRFlnq{RbxSz{wq1Hn!rHA2t>Wz2B1%Zxi(z zeco$u*`3J8rk2Z7G6prHAoTt67L=9M7w&Q-cfT&}Y;0^C9UUDU96%=ys3mea`~zQ7 z^JOL|^2Q&6MvJee-X$)^An^r{of#2F%Qy#W&syM>wzDD&zQ*7_w|(_Uy&lAP?;CeM z^i3MV)7eh=74H;enLf{4&gKh7%WU z%Z#Md)YsxzUh5-XUB5v~MX? z2q=`g_Fd9SN?nRSfy_0VF3V^V_zb&z(9YPntBecs7f>Q9vp3K$tKr^_7ePT413^_ zLDpduMYj5PbJNWn0sMnU=(7Gzz)7PGYWA6pE5zgw?B`3yu5edbS-4n8>(%2u5x0#ATbZ400tCw2mmXP zsXPV6U24Ib;7dE^%IeQu!;dy@9=mg(@kRdWTLbxO-*y7XeE4n90Q;KgZtF{(1b=$E zk$*3yi?lQX(qe#!fOLlv z(jeU;r67VxI)sSi&PIRU*CV=_fGlDIkRV<{nUEavrfDt zZ@zJ_4SoSD;=X&c zw599c;8h{&J`Pi+KS+V+*S<;^<|jYbVaPpL401~0^MWhND?O;p7CimwHbzd@&&%*2 zd$U-vSQ0|kf%iJ1r)4&m*mwTaLXt}HORe7<(LM+*ogMck47dYI_!WH z0|-kRIDY`|1LtI#Y0fjrK3YSOr5HTLK~ce*^Rf80K5s_JxWqeF7^A!1+<$_|AVzIw#b= zM(qwp{~U&;G0CQZm4>d@HXus1-mTAxZ=b(9oRK?I9~9ClGFilfh%%6+08;=Ndc&y2 z=ZiawX=f?~8l=8?0G^cF`LbJ7RMdANrDHVQ+XG$m#PGZKCU~L%8#S1@wH10grV%_0 zGqhY@y}o9M|67+vMEmfr$DfD^38{~22g5gV38rF0pAr}onQIT4Ml3g)poa4K#k9x& zUk>wU&H&XS?RjdCwxsmB$CY#F?(D2#dQ$`-8{@Ik-g^@bQkC!Cy<7YlFMT^*vmo>s z4152fZ=vQjzI(K6(^2%)``VBMq({hCUEa%T05-smKipOluKn2G*B871v80AR4sZLy z5T0}Y0xuNKeO<3p^x*0uL?ui5qaP}coE8qjlbs7xMy z0Xj85mCklv2CRVY1{Mv)kSl7|0rbqmhvIZ5W z)&Dt@v4YM({5oc%rtwUC&>Nf~@$}#Mct&PsKz00e(JYD{2sznZGd+3_T?4H~bSg_m zW+v-@6uysKs_*HyOrL}+Ic5;G;IQ%VDVI@x0Nr4FwplaN8hkLz@5fy91JySz8*Fcl zZXAFF0ayrk|7wTw@sk$udVVmJaYkz&;f;`jJAe1L|R4F1%+`h`i21! zNlwgr6R=j~&kzzs4%*J%2VKfDV*v+1)~-M+JLF(UHJ)9m>`8cz_G(Tv+d4vWRi@aX zmC%Ib5l0pRd@)4gOt>86D~Y)L~}WHkq4mU}PCA5Er! z^}+K3%1uLg`_jO5&<6CIJlS7irUOHAV0h>~bJ9-&l`q_&TH}MEM~h0fM~$HmzWb~H z26$E+EkJkjG+dW0&xAnsB@K2De>&bmCkME@4lbe#7tPqS$kYbDz?{wf z%*RD9SUsL@n(CT#4S-k(GNW?J;r+RP-F>iF2{}2a@CYpw;1?MgO%@NuZy0hecjgfz*bK~f69@PXGFsqD?0rkGyS+%xaUI%9< z`yi$JUAr+|Q)+D@7kF4w`zu12gB8#Qza@3?mOs@n*Zr(^s7kdpd$+;2!1YA2hJ6_{ z%KrGL4gqH4?ykp=Vr{-8^}JOsvewIh&Rk=W8KE9Li_xh&N|wgX!9Jja<8=UPBj9+VvI4j6ddT*HyF=)OgUcdmIqivhh`R6S_w!aena9edPXq1B45e`P?S7bZ6`0l3EI+_nBO=wI+#OmTpV zK&GNq`ejXo?4kuh;GKI}(BIv>3`U99l_q7%dyrFEDU0YmMUq#3ReKL~ss;M+1Bba|ORi3aV- zp(;OlHZl)4qE_9r+{*+`F)b$40M~1AEMvyP75@bt3Z1VL6Gj=v87K6iJu4q9?#RoR z@WrH&0xGnssw&7Bfl~lJ+^(lTTgkGe>dnM=zq8}r%F0SrHvb0>KRP=*!7t3*`aHG) z0E5GQfR$)KF9Bdkh6PCB0BuJWO6F8zf+K)&(-pElBMc)@&&0A&U+beb(8U+Zed{o%wv^q7dlpzoWv%%%VhK{eUG;9{*V;+;R6%zeebe< zfNBJ?Uq!#W=exh$rywt=vGS#b&x#Q!MG73ip6U%}D2|t!CR7Y7eRlxWpyWe;v|GqA zd-e8{zEl#g8bGmDyCn{e?7)icsRo!@{JFRb&fD9ku`ypWM0D-h^?5gM6mTMhwWriK zeog6wJ|lQ30$99P<02Hw0f+_4%iMpb*ExlTK6Ede9_85dL25AY&#s>&lev+R5f}wb z$sHsm3J0_2AQ3jV*D|-Y_VKB0KHJzRX|Tv1-tCOi{!FaU8szqGD&US6-6p6SU`w)_ zcdhxp96JApoahm%gKCrQ3<_svrC&8}vvRMv{RwGPJvlnsz40aHE8rG;^U%&FqTfBZ z$rjK2Q|(2Y)!-B)lj*)1#M)H*-sQ22GPz{U@j57u|MSCi+d{o(J@{683RFxge`zNe zOG_`(F}bTQ&(;!*tsPcO8P6d22V}&q-kIv@HH5y*pn+YhOc5Bhf090f2tE1qN2eCK zOCU*2PEQoTM z{9f5pS47)uy_G(fNP(zOtTm8j4G4cJ+UY^exkrh(8X0v+&e5s}nNe-oxFVQAf&}W_ zK{`Na)(sg;FvQ75E;lRA4o83N;@xJi5ggEY5G>5hWE@IB$Q$Bf7%ry0jM}6>A|5-V zU_IVTy@u(W`Lf$DN0JKfNG-e0G~z>>E6+s<0@5es3!x&!!Z|ub8}lfBg26s&T?(HD z#^>ug&uy)&g>J6vK6kk5dlWCSb3!<`U+17{7E!S&c$4to=v(PF*ILQ2u49g-3uTG( zNVy__>BflYQetOtC}F?{+rh(qMOqTgqJv*!adi@&_75IkDM=7}L9FMjeC<-38Vd*Y z$MG&H4!K`E%(1aHr912uul+K$1*etiZcy8QqHyrLrm7-eb-5=jCqm^9Lr80mGcV+s z=gOR%XZGxoPb+_A!9;wq?x;Ra76oURSyMD$(eut$#GrNKyLg=49fes*0udH=#s-Px zZTZC351FwIS0=1xK`v(n?urWEKzSl|Ioj~8e+bFy8SQgBoj+6}y!%6*Bkh1w{L>!wij#4oOyYEl z65Sy3FaWuxT-WxlTaDqZwRdv?LDtFfvz!6xZmM$XPYoFwcIwVj(A*Y3uLD8+54%X0 ziY!yY{1vv5Yri>jTMw_3SabK^nh{ZYAINV0Kr+~9^{0xJr>n4siPftRTwBX|9Cr`# zm`@T)@dGrD`zn}=TwD+KJRR3fja6h>Wtcz7@I@=7_iM6jQ~`WfG2DW&5jlsi~n z|MuSl#G>E8Koh)q$qh>y&dXIv~%+A&4BLn^;hCW z_f>WIcX!KihZw9<#Xal5Zv(4=m^`gJj*1qN3JP`0rj`UrT+}MIXLj}b`731kx|Vk8 z8kU;Ne|+%=8>XR6e#q7+N=EYz^GhR!NgjmOv*b0}cf+U!<^}8n|K{c@>sE)dn|mCb zT>G}qzuN&po)trot+hse{sW)O#2YrUuG^8IUra=jkw$1!7^Y4V4~TG2^=iHwD3?1e zIsO}Tu!=e#`(sPUOcAlh-}rg!{xMfkYJJI=n=L?Lyb`H2}eYC+N~LH_QvVXeOPq9h`?vr zHwU?s?Vm#Nn=m{MpCUp#;m&h5chn5u+38nrI^8Rvvv}?$!y@c|l{=j+;xkEvwn9PE z_V<#;r@eWdu_)Z90{mL2oe^;y2**!6mDi;V0E#?4;j+Qc!r^{HsTNYklKu8!pyK=1IHhBi?Zltjjbpw~i+ zCvd2nvL2u$#T_Hx^hNSlPQKI-E{mT#9oV`@Wr_wNGbCy!HWce7S-gMhq$HL3N*>hC zK!h-{1&eDo9X9OWA-i*p#2U89&Ac-t_H|%@HUlx;8Lh>Fz{Ek>>k`PF%U$76GbgXl zBZ^H#y#-r_eoPB3$LTE+I63p4OgZ0_rZ?^6`P$N9)Wz(6XEF7;i9Ip-?8g(?MU780 z&ZFHdaUU-p>XQ%C@`{QfN3*^KROda}!C8L{b_ehaPHR}BCd`;AlD*WRXZLF|%*LwJ z=4~_%Y)RbZN0G75%@cc58#out)SHgWLr)Lj=i__a1k=x%dh;EG?#8)e9=8uPpRFAn z%#XL6W7cSGLC-=C0l7IX@!p`0Z$hHE;)}(jFRbGAEWuuvhUy!i-rBaQ7CY}zNP55_i1u=VBY+ zs1Ui6M$|Txt;B%4<0bcH*^Nr}xoX-%Z&4>HX!3i<-|Krv_@HB|kgxlD5Kmv|@!%(4 zJa)-9%d&}Wf)p3hGQA1L{2%4+E{TMmAdcp+L+rSrzeU;Dwd^2Z|9B|=SPpga2@kD| zxY+&dlg3aqW<2B!xTFmK^$U#1>rdsVBVwl=5=*CyBr*J1$IsF(A4Z*0EcHXbQPT0% z-k3C7(}|X>NJ~P)Wn6TB%Wkw5>V)0DoTzq)S8?~=3Z9;DyoDluVc7E@n546(Z)it^!32 z{}J0vd#}Ofu$2gDM8d3qZE z)vLXJ(8P$L;|?PZ*=4qmQ;`JwLtM)Cdm=iF7CAEV*-itWuIdWv1SvOwaKKl;`tlDB zED(%0Fg*l+UfJ0h6Q899gfo9P^Lk<)0-Mae=@S=}FLikJ@K8@Ia zZ`|hibRViqY|RBe2lwLX6@)1NNMt5rV`Iw#byaqD{LjVr?U|Yd)0>ph9U(1l>J?j( zX@+!#U{?039_@m_$+a7=hh;PSarRw{yMU2T15wR=2A_UP2IkXz8ilTusp>lnPLOev zL{y}=J>t1CL6m8~r~V8AEX-xT_Id>pZ1oj_Lu38>*82A)9;k6dh?C0`RNQo+t_7H| zQ(wfA#Z`j9@)>I5E&bLcWs)Vv2m-5c^3Dp`FlvM6d~X!Cas-=M3X|E$lBr2@goy+Q zj6SyASm&W`FMh92P+>=8SqN$#TG88=TBjI%a#RR8^*ZLSi21^WoM|=$XDN!gK}aQk zMxZTtUr0}rBSL_&kl+i?e-0nYUZ&o3<+yGidd*29N(k0*>KlRWGf- z@P$#Y(7#ti|E&2e4V! zXbleMyWl|Enz>Tc0$vEMO?!f&kl1rLOrIQwqu|8A=T-sDk4D}fRMA-)aRv!U%X&(Gj~TBNT5nPy3P^@ z1_r{~oQ4SqSPw4V5y1QDUGAft)nJklL{YP1W56i5>?M2}To1%oB;r>N^M}_>#W%pj z_n3m;x6}-c*D{6V>Q~w`-90^ZQK_UGBaH1BOwwrzIrbh$#2pGwMqRSppHbZy^LHG40wMuh0ap}njiE8f0yI0a2;nRkOhSu0BhKU}%&n1&{-rB21 z105>&-+Z^B_;NF!(2^bKrNby?5_C@lqex3r(*`D>Co8hhOEwd6H%ye1x?Qheq`s-i z3@-zf>%9LRq@up9Mp81|@mkpmN=hvVgtr5u+9CbOIQ?w$brK1aKo5@vdUmydBT#qM zzji-VuFnjY+W_^E0F?K(wAWKhAaVuF0PGlI;X!?~0cp zi&I{ukyYd)1L{pmkN{>nIunTosVK z{PnW#^tB+FiVWC@{)i0BP(bV% zPj3!{J1&xBW1e2t)|5;pF^R`AOmSweNhIElvomXLI~KLq>iYsg)24B@U-&Y%0JRN{ zpck=-m;!^$)Mw_1ub{@^@J&N96t=52?(chgl62b1u<%@e*%KmdoToJ^0QX-bMBZ3+ zmoYo0nJ~nKC!k?2r0M1A>S~|HO*Vu%lvS^ z=WVCEOb}S|=|B85=q z1k>U^tdbbs_`r=&}?UCSywYs&$44 zsoZ*hrW}w01E@75+gt`jq7O?%lg{-$(K6s*9J4F_j0S=fc6&-k1p~HRiwAAARM*WtY@%@>8a)^M z!14)sE0A;&uJ_rk6-YVs^~GuYDnt7;b}a0G`Flb` za68~cA&`v#rrSxRBV}b}K-~d6+?ls;X78|20yko}buU|?)mCe!l5>8YMmwHz^73W>|{6p1vwz!`!#?!3f2LT9B znSCU(@0D+U&TiJydMMHxfv_0xR-OC-`(?wz&$Q<|6=P4n^(U3cIRUtC1{3wO;dC(@ zo)t@0Ha6MCx0wknq<1}O!ISL-x;6=f7-VTczVcQ#4+t6TN+C1OQn=L)O%NqP&PPvA z4~~qc5?kV_dUiy%(~s2q&LBSYQUyF3mvx0jND_YT9Lyy?=z|4f+ue z{;Q=$MM=q`*iHADQUL(BV9`s%^Sz<5Y^s;_z=Ofy2<`@t_Uc(5C)7LhEP$-kf@?Z= zWNii!C_QNmoY%mrV%~^VPP@0@@3i-$*~a~olasls@l0`DK-hzYz5j6a2n$FcnUu<~ zg0>m#(U`RGW%dtKwS*;`-&@lSp91l`zEr!ElwH(G09X&mL8z#xfIx&Vpcb&jj|{ZS z-huWlxPv4=OHl&90XA*B*LGvhWg*M5i@yC#mr)^Lxn+EOJf2OV0o&Wx*SEA}`>vd| zP#r$Kd4vTvp-7hUBz!Wu_lm%gfR#FkXo>d&2J=!e*RYL1YClbpZ0KNSnl49bSxu55TX-1Y~Ni93CD5l?vdQ z_HAQ)R^H?3)~6!LF4u$9Oyl-#NiCV`i5Wzf!|kTPBiE{Jzra8_@Vpl*ZGOdoOt)ZU z4dh^&_5jc}GrOH3`D>P*Jm}WhBmx;xuXJjrH1bk15%w6^(}$mn-@=JQ@U7yd27+tz z$ORM_ng-n;QzLQt4WYnWe~X5>!$7sXB-+ki&eLB5 z#%2IZKG?OZ_6OlHB&6A0y~68oI!n?BsbVz%Fe${&u3fSQL-V-ItcSBC6W~y-DUP5q zCZ%JWs)?UICXUa|&GBgUi9{=;1cfAqenNEojW>V+k=Re)DmQL@O^>`0d<#Avaa1(h z7qL%F_*XHpE`a9*3+xwRQI6W?RXu$Q&b~wZSPfTFP#*7ijjI$C$-e0$w>A%wVdS%0 zAZGO}U+!r1{KdtomCQ$!G{Q(uuWl;nP ze9X{EJ5vCs01D++BIm{8kb`ow-W&o*VxD?lBN|R1ICl_Nu%a}eK8KRsTDmWMcBB7JN zpV!ie@LM;OS7fapY90FsUZZ{Gi+u&L`919l|GDVl3TuxKrM%6kbNgGbjrd~&>&fbe z>`0-6+Kq%36D6t=ydbP|S`wdlFj%ol%Px4g`V9mRwKzIaU8b**vioIi!HcV=?kquV z6Z=HBK9}H5-DK223n9=6l|5;Vy(eOg5i=$NA6@4H9LeD>n?;?yWphp4*(OFj-x?{R zJJKzs4gUM?UV(@pP4VuMJ-uIcsJqj(I%xa{0mN0Z;Qa!~-}9O4eEthoqDEuy>{3|v z6sRp;$*@G-2v-a8Q&?ycr~bHuZu?!iGeg)DqSGhyN%SasJ+j09kpx|eXE(j`?wZ?TjG}zev#jPOa&qXlpHv-ZYV-0In zpB9(|gskOE^^wr5T}BI@ZyJ7;v-*Yc_VhhMy3b!k=`~#A9YRBT7k+wMBN3@k(vg9v2hB6mw&{oyqprakr&*>RIyQ!@m!a^sElYH} zH5D+kke1Rhqcl_Rt`HyB+ZsxWT<|&NFr<(7+UQKbO`TRF*9dL8ac(5D{4L@9CsEtR zTV*>HsnhXxbFm3|eG}VH5$ty>_Tc>P4x^9SHlBw);Va!*>R_tFQsv1PHfo6a zO84c-8CwH6AJ^j>c!?hYa#96RiRUdM~}dsz>cNij3A)Zw2Sl z^kn-Zt8S%N{&kcd5)fx%e3cislO1B$;Gg?;#;%vI;mpw~UX-}ZUoa0%RwR8^C8qn8 zV*{l6WPGwL1`H6$=I1L?;^yY_PXs0(la<3Lp{2owJ{nH5X-gK#l^Hu97>&fg8L6d@ zVnbMwKoQdkeo7DTu~1gfL4$RG7Sh!-nm`bVl?7v%i7^$ zHCp+j)OI?kn!;k~az(e(3+*zf+d#Dc${ecCwSmI5%mg;4cEe1IwxS%ITU5xj-3#k2 zCr`+4dz`+A{AaH|mx<&cBc!VVH{@NXz^paB60=k7^3(4w(?aldGNlkkDg!49{nijP zfTn<{MQRp|Y#(|4Oa}SQ{;#Sq&*r}>nVMm)MqUZRCe0tXH`Mc(vxO(j>k;{0Cm}5; zlEF2vs1EDghD6RED%}y~D)n0CLwjP=QFE;4NK~@7;h@V+Vw38mqPgYq^jYMtA@ z@`7UhkyM4;bQU>%V-{$|Bcf5b@Ip3>8l+|Z51Bd^M86sNArqc++R}vmI9xNIF4Z_R#ei*LG{k$);kn4*Goscrm64CWPHY}IiD3=0#@xe_t6Cs z#)(Qd9gR{kz3p3Be^ua_tGv+nd}`1JW1@}9V?<9r=_$?AHY2qhnF)BI78S@&`9uq{ z(bnLWydzv6>efjY)!OQ71N(ffZkCXq!N~dHk|U{^?nUlL$TJ{GW}@XzR>JL`>T#;J z<<~^<(K+yQ<^2WGqs-F4lj+)fQi8soqlmvQfpWDHSEAeoHXR<#yG1T^BODcKl9+P= z+(ib-zAt!f!a%PqWyTvRq}{OI?P5wkfv~`_J95U_hOw{sb%=>uDE#B&v z|DH47_e=0%5$91X^ITdt+}dvxT3ooNA2k77r^On|a{M|3UC0-d4Cj6CV*0{vii4dd zUP$ZpyM%4{rnANpbG7y+bJ4{GprovD3?ds)lTdH6$wccTBdl3#%5Toj3ppeP{bHMb zcZC5AG}k;We(@=4ytyc*yVQGa-Kqs#bz(H(EEQ`2uf-HQmckuBxH>^^u~(sexG(Xj zG&k1PKlXN>R-rI@xHjU{Q|evczyU;l^%%3;FY9oRx;!Qm5VJONxqRq0B^q?@4w=@F zr}H@x!YfAwJ$3PitxvQ^GF22cbeHK3MZIs`j>&Yg58l=rN^!338lJWWQY8_LDif7Pb?SL}qlSwXJRRoQ z@+DG&TO*H*pb4AxM^H(3u8o&YPQlwTQg+pMQs!>J^vJ$}gN1C8^v|!HQ-1=x^M2Dw zd)68090Gj0zlFHbEWHY)(4o!JOmt z*RO(MgPi9us!vNP+Oi+YzK*HK@^mzOa(ucx?i$Tx8GW!$tjY104uU7ivlWqm53BsR z`D`n0Vzz4>Hj=3~YTwXM` z5_=VSG#Ts|I?JB*s&vX3kq3V~uP1*2yRGZX=9IEQdPW;#8qiNgq!QgQnG-y6 z6B^r@)hV-PE{7ka7*|?w0K`R(8uElHE)PAsXJZu5Q<7utB-+v~hmhSUR55q6G_A9@)C=&Z+QD@rI5t8MyxU_PHknvehN z6LUNVU!ipOE-7QHu!sorCBi2}V~}gbP1mockn_{&@C4Cvs3iM-ZA}crsN4!bSe%zb z!D}TqxG&zw%b|1eeqQBofM!VWT8;GL-Mt?Hn}UGgCilgQc^{}>yr%apXcG|-Q1k!q zg8%K(|BlB0?BRc2iF*$N4YnE%}Ie^n3R sgTg({8%E+47H^|3-0a}n5j?Rxj4gEPGT-FI`+pxl)O=8;V)f?#0G(X0_W%F@ literal 0 HcmV?d00001 From e50aecc59486cfd1169fb7816da25dc3fc4bd6ec Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 26 Apr 2024 15:27:56 +0200 Subject: [PATCH 03/44] chore: Update the linter with the latest code (#23271) Update the linter with the latest code. --- apps/studio/data/lint/lint-query.ts | 124 ++++++++++++++++++---------- 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/apps/studio/data/lint/lint-query.ts b/apps/studio/data/lint/lint-query.ts index 5c290d45e79..5135ef2a9d7 100644 --- a/apps/studio/data/lint/lint-query.ts +++ b/apps/studio/data/lint/lint-query.ts @@ -90,22 +90,22 @@ select ) as detail, 'https://supabase.com/docs/guides/database/database-linter?lint=0002_auth_users_exposed' as remediation, jsonb_build_object( - 'schema', 'public', + 'schema', n.nspname, 'name', c.relname, 'type', 'view', 'exposed_to', array_remove(array_agg(DISTINCT case when pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') then 'anon' when pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') then 'authenticated' end), null) ) as metadata, - format('auth_users_exposed_%s_%s', 'public', c.relname) as cache_key + format('auth_users_exposed_%s_%s', n.nspname, c.relname) as cache_key from -- Identify the oid for auth.users - pg_catalog.pg_class auth_users_pg_class + pg_catalog.pg_class auth_users_pg_class join pg_catalog.pg_namespace auth_users_pg_namespace - on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid - and auth_users_pg_class.relname = 'users' - and auth_users_pg_namespace.nspname = 'auth' - -- Depends on auth.users + on auth_users_pg_class.relnamespace = auth_users_pg_namespace.oid + and auth_users_pg_class.relname = 'users' + and auth_users_pg_namespace.nspname = 'auth' + -- Depends on auth.users join pg_catalog.pg_depend d - on d.refobjid = auth_users_pg_class.oid + on d.refobjid = auth_users_pg_class.oid join pg_catalog.pg_rewrite r on r.oid = d.objid join pg_catalog.pg_class c @@ -116,7 +116,6 @@ from on d.refobjid = pg_class_auth_users.oid where d.deptype = 'n' - and n.nspname = 'public' and ( pg_catalog.has_table_privilege('anon', c.oid, 'SELECT') or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT') @@ -132,7 +131,7 @@ where -- Standard View, accessible to anon or authenticated that is security_definer ( c.relkind = 'v' -- v for view - -- Exclude security invoker views + -- Exclude security invoker views and not ( lower(coalesce(c.reloptions::text,'{}'))::text[] && array[ @@ -147,7 +146,7 @@ where -- Standard View, security invoker, but no RLS enabled on auth.users ( c.relkind in ('v') -- v for view - -- is security invoker + -- is security invoker and ( lower(coalesce(c.reloptions::text,'{}'))::text[] && array[ @@ -157,11 +156,13 @@ where 'security_invoker=on' ] ) - and not pg_class_auth_users.relrowsecurity + and not pg_class_auth_users.relrowsecurity ) ) group by - c.relname, c.oid) + n.nspname, + c.relname, + c.oid) union all ( with policies as ( @@ -219,18 +220,38 @@ where '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( + -- Example: auth.uid() ( - -- Example: auth.uid() - qual ~ '(auth)\\.(uid|jwt|role|email)\\(\\)' - -- Example: select auth.uid() - and lower(qual) !~ 'select\\s+(auth)\\.(uid|jwt|role|email)\\(\\)' + qual like '%auth.uid()%' + and lower(qual) not like '%select auth.uid()%' ) - or - ( - -- Example: auth.uid() - with_check ~ '(auth)\\.(uid|jwt|role|email)\\(\\)' - -- Example: select auth.uid() - and lower(with_check) !~ 'select\\s+(auth)\\.(uid|jwt|role|email)\\(\\)' + or ( + qual like '%auth.jwt()%' + and lower(qual) not like '%select auth.jwt()%' + ) + or ( + qual like '%auth.role()%' + and lower(qual) not like '%select auth.role()%' + ) + or ( + qual like '%auth.email()%' + and lower(qual) not like '%select auth.email()%' + ) + or ( + with_check like '%auth.uid()%' + and lower(with_check) not like '%select auth.uid()%' + ) + or ( + with_check like '%auth.jwt()%' + and lower(with_check) not like '%select auth.jwt()%' + ) + or ( + with_check like '%auth.role()%' + and lower(with_check) not like '%select auth.role()%' + ) + or ( + with_check like '%auth.email()%' + and lower(with_check) not like '%select auth.email()%' ) )) union all @@ -377,6 +398,7 @@ from ) act(cmd) where c.relkind = 'r' -- regular tables + and p.polpermissive -- policy is permissive and n.nspname not in ( '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) @@ -568,17 +590,23 @@ from and dep.deptype = 'e' where c.relkind = 'v' - and n.nspname = 'public' + and ( + pg_catalog.has_schema_privilege('anon', n.nspname, 'USAGE') + or pg_catalog.has_schema_privilege('authenticated', n.nspname, 'USAGE') + ) + and n.nspname not in ( + 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgsodium', 'pgsodium_masks', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'storage', 'supabase_functions', 'supabase_migrations', 'vault' + ) and dep.objid is null -- exclude views owned by extensions - and not ( - lower(coalesce(c.reloptions::text,'{}'))::text[] - && array[ - 'security_invoker=1', - 'security_invoker=true', - 'security_invoker=yes', - 'security_invoker=on' - ] - )) + and not ( + lower(coalesce(c.reloptions::text,'{}'))::text[] + && array[ + 'security_invoker=1', + 'security_invoker=true', + 'security_invoker=yes', + 'security_invoker=on' + ] + )) union all ( select @@ -648,12 +676,18 @@ from on c.relnamespace = n.oid where c.relkind = 'r' -- regular tables - and n.nspname = 'public' -- RLS is disabled - and not c.relrowsecurity) + and not c.relrowsecurity + and ( + pg_catalog.has_schema_privilege('anon', n.nspname, 'USAGE') + or pg_catalog.has_schema_privilege('authenticated', n.nspname, 'USAGE') + ) + and n.nspname not in ( + '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' + )) union all ( -select +select 'extension_in_public' as name, 'WARN' as level, 'EXTERNAL' as facing, @@ -679,6 +713,10 @@ where -- plpgsql is installed by default in public and outside user control -- confirmed safe pe.extname not in ('plpgsql') + -- Scoping this to public is not optimal. Ideally we would use the postgres + -- search path. That currently isn't available via SQL. In other lints + -- we have used has_schema_privilege('anon', 'extensions', 'USAGE') but that + -- is not appropriate here as it would evaluate true for the extensions schema and pe.extnamespace::regnamespace::text = 'public') union all ( @@ -726,13 +764,13 @@ where '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault' ) and ( - -- Example: auth.jwt() -> 'user_metadata' - -- False positives are possible, but it isn't practical to string match - -- If false positive rate is too high, this expression can iterate - qual like '%auth.jwt()%user_metadata%' - or qual like '%current_setting(%request.jwt.claims%)%user_metadata%' - or with_check like '%auth.jwt()%user_metadata%' - or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%' + -- Example: auth.jwt() -> 'user_metadata' + -- False positives are possible, but it isn't practical to string match + -- If false positive rate is too high, this expression can iterate + qual like '%auth.jwt()%user_metadata%' + or qual like '%current_setting(%request.jwt.claims%)%user_metadata%' + or with_check like '%auth.jwt()%user_metadata%' + or with_check like '%current_setting(%request.jwt.claims%)%user_metadata%' ))`.trim() // Array of all lint rules we handle right now. From 0aa014b9e56ed3f2ec6546382c699274562e2f2e Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:37:39 +0200 Subject: [PATCH 04/44] chore: update cli reference doc (#23290) --- apps/docs/spec/cli_v1_commands.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index d17bb6bb29e..f5ed03f07f1 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.163.2 + version: 1.163.6 title: Supabase CLI language: sh source: https://github.com/supabase/cli From b2bae1351cbeae9617abc26cb1edd76112efa7f8 Mon Sep 17 00:00:00 2001 From: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:40:30 -0700 Subject: [PATCH 05/44] docs: update vector reference to pgvector (#23305) --- apps/docs/content/guides/ai/vector-columns.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/content/guides/ai/vector-columns.mdx b/apps/docs/content/guides/ai/vector-columns.mdx index 5a1cc7ef803..84ca8474a83 100644 --- a/apps/docs/content/guides/ai/vector-columns.mdx +++ b/apps/docs/content/guides/ai/vector-columns.mdx @@ -24,7 +24,7 @@ Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvec 1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard. 2. Click on **Extensions** in the sidebar. -3. Search for "vector" and enable the extension. +3. Search for "pgvector" and enable the extension. From c30d5f0b1f9b7bf2678735741d2effc1221e1c94 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Fri, 26 Apr 2024 22:52:51 -0600 Subject: [PATCH 06/44] docs: rename vector to pgvector (#23308) --- apps/docs/content/guides/ai/vector-columns.mdx | 4 ++-- apps/docs/content/guides/database/extensions/pgvector.mdx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/docs/content/guides/ai/vector-columns.mdx b/apps/docs/content/guides/ai/vector-columns.mdx index 84ca8474a83..235eb47169b 100644 --- a/apps/docs/content/guides/ai/vector-columns.mdx +++ b/apps/docs/content/guides/ai/vector-columns.mdx @@ -30,12 +30,12 @@ Vectors in Supabase are enabled via [pgvector](https://github.com/pgvector/pgvec ```sql - -- Example: enable the "vector" extension. + -- Example: enable the pgvector extension. create extension vector with schema extensions; --- Example: disable the "vector" extension +-- Example: disable the pgvector extension drop extension if exists vector; ``` diff --git a/apps/docs/content/guides/database/extensions/pgvector.mdx b/apps/docs/content/guides/database/extensions/pgvector.mdx index 5495f587a07..b0b777eb11f 100644 --- a/apps/docs/content/guides/database/extensions/pgvector.mdx +++ b/apps/docs/content/guides/database/extensions/pgvector.mdx @@ -33,18 +33,18 @@ This is particularly useful if you're building on top of OpenAI's [GPT-3](https: 1. Go to the [Database](https://supabase.com/dashboard/project/_/database/tables) page in the Dashboard. 2. Click on **Extensions** in the sidebar. -3. Search for "vector" and enable the extension. +3. Search for "pgvector" and enable the extension. ```sql - -- Example: enable the "vector" extension. + -- Example: enable the pgvector extension. create extension vector with schema extensions; --- Example: disable the "vector" extension +-- Example: disable the pgvector extension drop extension if exists vector; ``` From d7f6fc69705329258bea50411b03268bd2aedb5d Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Sat, 27 Apr 2024 13:37:51 +0800 Subject: [PATCH 07/44] feat: branching on pricing page (#23213) --- packages/shared-data/pricing.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/shared-data/pricing.ts b/packages/shared-data/pricing.ts index 156deda1aa2..1047f2e1147 100644 --- a/packages/shared-data/pricing.ts +++ b/packages/shared-data/pricing.ts @@ -109,6 +109,16 @@ export const pricing: Pricing = { }, usage_based: false, }, + { + title: 'Branching', + plans: { + free: false, + pro: '$0.32 per branch, per day', + team: '$0.32 per branch, per day', + enterprise: 'Custom', + }, + usage_based: true, + }, { title: 'Bandwidth', tooltips: { From 65efaf3118f1174a2ebd9e8042da5af0f76dca06 Mon Sep 17 00:00:00 2001 From: Chris Gwilliams <517923+encima@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:14:20 +0300 Subject: [PATCH 08/44] Update docs for pg-safeupdate (#23177) update safeupdate docs to link to postgrest and fix examples for role/database enabling --- .../content/guides/database/extensions/pg-safeupdate.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx b/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx index faf7ee4dc67..c071df2b88a 100644 --- a/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx +++ b/apps/docs/content/guides/database/extensions/pg-safeupdate.mdx @@ -10,11 +10,12 @@ The `pg-safeupdate` extension is a useful tool for protecting data integrity and ## Enable the extension -`pg-safeupdate` can be enabled on a per connection basis: +`pg-safeupdate` can be enabled on a role basis (where `anon` is the role we want to modify in this example): {/* prettier-ignore */} ```sql -load 'safeupdate'; +GRANT SET ON PARAMETER session_preload_libraries TO anon; +ALTER ROLE anon SET session_preload_libraries = 'safeupdate'; ``` or for all connections: @@ -30,8 +31,6 @@ Let's take a look at an example to see how pg-safeupdate works. Suppose we have {/* prettier-ignore */} ```sql -load 'safeupdate'; - update employees set date_of_birth = '1987-01-28'; ``` @@ -52,3 +51,4 @@ update employees set date_of_birth = '1987-01-28' where id = 12345; ## Resources - Official [pg-safeupdate documentation](https://github.com/eradman/pg-safeupdate) +- Using [pg-safeupdate with PostgREST](https://postgrest.org/en/v12/integrations/pg-safeupdate.html) From cfffacca609c5bb67ea27d90d97a62b31850b3b1 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Sun, 28 Apr 2024 13:27:10 +0300 Subject: [PATCH 09/44] Fix: Feedback widget content (#23298) * Fix: Feedback widget content * Add: clear button to feedback * Add: clear button to feedback --- .../FeedbackDropdown/FeedbackWidget.tsx | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx index a1a5c553667..906cfd22fde 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/FeedbackDropdown/FeedbackWidget.tsx @@ -36,6 +36,9 @@ const FeedbackWidget = ({ screenshot, setScreenshot, }: FeedbackWidgetProps) => { + const FEEDBACK_STORAGE_KEY = 'feedback_content' + const SCREENSHOT_STORAGE_KEY = 'screenshot' + const router = useRouter() const { ref, slug } = useParams() const inputRef = useRef(null) @@ -45,6 +48,28 @@ const FeedbackWidget = ({ const [isSavingScreenshot, setIsSavingScreenshot] = useState(false) const { mutateAsync: submitFeedback } = useSendFeedbackMutation() + useEffect(() => { + const storedFeedback = localStorage.getItem(FEEDBACK_STORAGE_KEY) + if (storedFeedback) { + setFeedback(storedFeedback) + } + + const storedScreenshot = localStorage.getItem(SCREENSHOT_STORAGE_KEY) + if (storedScreenshot) { + setScreenshot(storedScreenshot) + } + }, []) + + useEffect(() => { + localStorage.setItem(FEEDBACK_STORAGE_KEY, feedback) + }, [feedback]) + + useEffect(() => { + if (screenshot) { + localStorage.setItem(SCREENSHOT_STORAGE_KEY, screenshot) + } + }, [screenshot]) + useEffect(() => { inputRef?.current?.focus() }, [inputRef]) @@ -53,6 +78,13 @@ const FeedbackWidget = ({ setFeedback(e.target.value) } + const clearFeedback = () => { + setFeedback('') + setScreenshot(undefined) + localStorage.removeItem(FEEDBACK_STORAGE_KEY) + localStorage.removeItem(SCREENSHOT_STORAGE_KEY) + } + const captureScreenshot = async () => { setIsSavingScreenshot(true) @@ -66,7 +98,10 @@ const FeedbackWidget = ({ // Give time for dropdown to close await timeout(100) toPng(document.body, { filter }) - .then((dataUrl: any) => setScreenshot(dataUrl)) + .then((dataUrl: any) => { + localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) + setScreenshot(dataUrl) + }) .catch((error: any) => toast.error('Failed to capture screenshot')) .finally(() => { setIsSavingScreenshot(false) @@ -79,7 +114,11 @@ const FeedbackWidget = ({ const reader = new FileReader() reader.onload = function (event) { - setScreenshot(event.target?.result as string) + const dataUrl = event.target?.result + if (typeof dataUrl === 'string') { + setScreenshot(dataUrl) + localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) + } } reader.readAsDataURL(file) event.target.value = '' @@ -105,6 +144,9 @@ const FeedbackWidget = ({ pathname: router.asPath, }) setFeedback('') + setScreenshot(undefined) + localStorage.removeItem(FEEDBACK_STORAGE_KEY) + localStorage.removeItem(SCREENSHOT_STORAGE_KEY) toast.success( 'Feedback sent. Thank you!\n\nPlease be aware that we do not provide responses to feedback. If you require assistance or a reply, consider submitting a support ticket.' ) @@ -133,6 +175,9 @@ const FeedbackWidget = ({ Cancel

+ {screenshot !== undefined ? (
Date: Sun, 28 Apr 2024 19:27:44 +0800 Subject: [PATCH 10/44] feat: expose auth reauthentication template (#22138) * feat: expose auth reauthentication template * fix: alter reauthentication copy * fix: remove unused config, update prettier * fix: run prettier --- .../Auth/AuthTemplatesValidation.tsx | 40 ++++++++++++++++++- apps/studio/pages/api/auth/[ref]/config.ts | 4 ++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx b/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx index d6e8f48ae09..cfc574698ec 100644 --- a/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx +++ b/apps/studio/components/interfaces/Auth/AuthTemplatesValidation.tsx @@ -178,5 +178,43 @@ const RECOVERY: FormSchema = { [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`, }, } +const REAUTHENTICATION: FormSchema = { + $schema: JSON_SCHEMA_VERSION, + id: 'REAUTHENTICATION', + type: 'object', + title: 'Reauthentication', + properties: { + MAILER_SUBJECTS_REAUTHENTICATION: { + title: 'Subject heading', + type: 'string', + }, + MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: { + title: 'Message body', + descriptionOptional: 'HTML body of your email', + type: 'code', + description: ` +- \`{{ .Token }}\` : The 6-digit numeric email OTP +- \`{{ .SiteURL }}\` : The URL of the site +- \`{{ .Email }}\` : The user's email address +- \`{{ .Data }}\` : The user's \`user_metadata\` +`, + }, + }, + validationSchema: object().shape({ + MAILER_SUBJECTS_REAUTHENTICATION: string().required('"Subject heading is required.'), + }), + misc: { + iconKey: 'email-icon2', + helper: `To complete setup, add this authorisation callback URL to your app's configuration in the Apple Developer Console. + [Learn more](https://supabase.com/docs/guides/auth/social-login/auth-apple#configure-your-services-id)`, + }, +} -export const TEMPLATES_SCHEMAS = [CONFIRMATION, INVITE, MAGIC_LINK, EMAIL_CHANGE, RECOVERY] +export const TEMPLATES_SCHEMAS = [ + CONFIRMATION, + INVITE, + MAGIC_LINK, + EMAIL_CHANGE, + RECOVERY, + REAUTHENTICATION, +] diff --git a/apps/studio/pages/api/auth/[ref]/config.ts b/apps/studio/pages/api/auth/[ref]/config.ts index 19ab6e288d8..1faef0ce619 100644 --- a/apps/studio/pages/api/auth/[ref]/config.ts +++ b/apps/studio/pages/api/auth/[ref]/config.ts @@ -120,6 +120,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { MAILER_SUBJECTS_RECOVERY: 'Reset Your Password', MAILER_SUBJECTS_EMAIL_CHANGE: 'Confirm Email Change', MAILER_SUBJECTS_MAGIC_LINK: 'Your Magic Link', + MAILER_SUBJECTS_REAUTHENTICATION: 'Confirm Reauthentication', MAILER_TEMPLATES_INVITE: null, MAILER_TEMPLATES_INVITE_CONTENT: '

You have been invited

\n\n

You have been invited to create a user on {{ .SiteURL }}. Follow this link to accept the invite:

\n

Accept the invite

', @@ -166,6 +167,9 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { SMS_VONAGE_FROM: null, SMS_TEXTLOCAL_API_KEY: null, SMS_TEXTLOCAL_SENDER: null, + MAILER_TEMPLATES_REAUTHENTICATION: null, + MAILER_TEMPLATES_REAUTHENTICATION_CONTENT: + '

Confirm reauthentication

Enter the code: {{ .Token }}

', }) } From a4208e9e9b82d89c44bb2ccf0ebb6b1c4161c266 Mon Sep 17 00:00:00 2001 From: Inian Date: Mon, 29 Apr 2024 10:48:50 +0800 Subject: [PATCH 11/44] Update humans.txt (#23384) --- apps/docs/public/humans.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/docs/public/humans.txt b/apps/docs/public/humans.txt index 1c721c74f13..2b127ce16c9 100644 --- a/apps/docs/public/humans.txt +++ b/apps/docs/public/humans.txt @@ -48,7 +48,6 @@ Margarita Sandomirskaia Marijana Pavlinić Mark Burggraf Monica El Khoury -Nikita Kotlyarov Oli R Pamela Chia Paul Caselton From 7341b30c278bc61b47a1d34a5e54669a94b0d02e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 29 Apr 2024 11:03:09 +0700 Subject: [PATCH 12/44] Fix vector extension (#23386) --- .../interfaces/Database/Extensions/ExtensionCard.tsx | 4 ++-- .../interfaces/Database/Extensions/Extensions.tsx | 4 ++-- .../data/database-extensions/database-extensions-query.ts | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx index 820fec90bf2..36728458e74 100644 --- a/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/ExtensionCard.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { useState } from 'react' import toast from 'react-hot-toast' import { extensions } from 'shared-data' -import { Badge, IconExternalLink, IconLoader, Modal, Toggle } from 'ui' +import { Badge, IconExternalLink, IconLoader, Toggle } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' @@ -72,7 +72,7 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => { title={extension.name} className="h-5 m-0 text-sm truncate cursor-pointer text-foreground" > - {extension.name} + {extension.altName || extension.name}

{extension?.installed_version ?? extension.default_version} diff --git a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx index a4fbd6fe5c1..0ed0da04907 100644 --- a/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx +++ b/apps/studio/components/interfaces/Database/Extensions/Extensions.tsx @@ -26,7 +26,7 @@ const Extensions = () => { }) const formattedExtensions = (data ?? []).map((ext) => { - if (ext.name === 'vector') return { ...ext, name: 'pgvector' } + if (ext.name === 'vector') return { ...ext, altName: 'pgvector' } else return ext }) @@ -34,7 +34,7 @@ const Extensions = () => { filterString.length === 0 ? formattedExtensions : formattedExtensions.filter((ext) => - ext.name.toLowerCase().includes(filterString.toLowerCase()) + (ext?.altName ?? ext.name).toLowerCase().includes(filterString.toLowerCase()) ) const extensionsWithoutHidden = extensions.filter((ext) => !HIDDEN_EXTENSIONS.includes(ext.name)) const [enabledExtensions, disabledExtensions] = partition( diff --git a/apps/studio/data/database-extensions/database-extensions-query.ts b/apps/studio/data/database-extensions/database-extensions-query.ts index 2d57aecf21d..bc0c2fb2af9 100644 --- a/apps/studio/data/database-extensions/database-extensions-query.ts +++ b/apps/studio/data/database-extensions/database-extensions-query.ts @@ -2,12 +2,18 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' import { get } from 'data/fetchers' import type { ResponseError } from 'types' import { databaseExtensionsKeys } from './keys' +import { components } from 'api-types' export type DatabaseExtensionsVariables = { projectRef?: string connectionString?: string } +export type Extension = components['schemas']['PostgresExtension'] +export interface DatabaseExtension extends Extension { + altName?: string +} + export async function getDatabaseExtensions( { projectRef, connectionString }: DatabaseExtensionsVariables, signal?: AbortSignal @@ -31,7 +37,7 @@ export async function getDatabaseExtensions( }) if (error) throw error - return data + return data as DatabaseExtension[] } export type DatabaseExtensionsData = Awaited> From efdcc8e6312b069baf68ec221cd2ff83d0162bb1 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 29 Apr 2024 15:47:55 +0700 Subject: [PATCH 13/44] Prevent selecting replica is status is not active healthy (#23395) --- .../studio/components/ui/DatabaseSelector.tsx | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index ab43c7fa5ba..b4bc3067c78 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -17,12 +17,18 @@ import { PopoverTrigger_Shadcn_, Popover_Shadcn_, ScrollArea, + TooltipContent_Shadcn_, + TooltipTrigger_Shadcn_, + Tooltip_Shadcn_, cn, } from 'ui' import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { formatDatabaseID, formatDatabaseRegion } from 'data/read-replicas/replicas.utils' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { Check } from 'lucide-react' +import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' +import { Markdown } from 'components/interfaces/Markdown' interface DatabaseSelectorProps { variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both' @@ -124,6 +130,33 @@ const DatabaseSelector = ({ const region = formatDatabaseRegion(database.region) const id = formatDatabaseID(database.identifier) + if (database.status !== 'ACTIVE_HEALTHY') { + const status = [ + REPLICA_STATUS.INIT_READ_REPLICA, + REPLICA_STATUS.COMING_UP, + ].includes(database.status) + ? 'coming up' + : 'not healthy' + + return ( + + +

+

+ Read replica ({region} - {id}) +

+
+ + + + + + ) + } + return ( - {database.identifier === selectedDatabaseId && } + {database.identifier === selectedDatabaseId && }
) From ac8cdcc58cf44c45bdc91b05beaf50fd86d99bfa Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 29 Apr 2024 15:54:33 +0700 Subject: [PATCH 14/44] Read replicas fix not removing node from UI when replica is dropped (#23267) * read replicas fix not removing node from UI when replica is dropped * Update refresh durationm --- .../DropAllReplicasConfirmationModal.tsx | 32 +++++++---------- .../DropReplicaConfirmationModal.tsx | 6 +++- .../InstanceConfiguration.tsx | 34 ++++++++++++------- .../InstanceNode.tsx | 32 ++++++++++++++--- .../read-replicas/replica-remove-mutation.ts | 20 ++++------- 5 files changed, 72 insertions(+), 52 deletions(-) diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx index f3b973fc212..a56372bfe50 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropAllReplicasConfirmationModal.tsx @@ -1,18 +1,11 @@ import { useParams } from 'common' import toast from 'react-hot-toast' -import { - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, - Alert_Shadcn_, - IconAlertTriangle, - Modal, -} from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -import { useReadReplicaRemoveMutation } from 'data/read-replicas/replica-remove-mutation' -import { Database, useReadReplicasQuery } from 'data/read-replicas/replicas-query' import { useQueryClient } from '@tanstack/react-query' import { replicaKeys } from 'data/read-replicas/keys' +import { useReadReplicaRemoveMutation } from 'data/read-replicas/replica-remove-mutation' +import { useReadReplicasQuery } from 'data/read-replicas/replicas-query' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' interface DropAllReplicasConfirmationModalProps { visible: boolean @@ -38,20 +31,19 @@ const DropAllReplicasConfirmationModal = ({ const replicas = databases.filter((db) => db.identifier !== projectRef) await Promise.all( replicas.map((db) => - removeReadReplica({ projectRef, identifier: db.identifier, skipInvalidateOnSuccess: true }) + removeReadReplica({ + projectRef, + identifier: db.identifier, + invalidateReplicaQueries: false, + }) ) ) toast.success(`Tearing down all read replicas`) - queryClient.setQueriesData(replicaKeys.list(projectRef), (old: Database[]) => { - return old.filter((db: Database) => db.identifier === projectRef) - }) - queryClient.setQueriesData(replicaKeys.loadBalancers(projectRef), (old: Database[]) => []) - - setTimeout(async () => { - await queryClient.invalidateQueries(replicaKeys.list(projectRef)) - await queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)) - }, 5000) + await Promise.all([ + queryClient.invalidateQueries(replicaKeys.list(projectRef)), + queryClient.invalidateQueries(replicaKeys.loadBalancers(projectRef)), + ]) onSuccess() onCancel() diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx index 096b5196991..18b6a21fbb2 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal.tsx @@ -38,7 +38,11 @@ const DropReplicaConfirmationModal = ({ if (!projectRef) return console.error('Project is required') if (selectedReplica === undefined) return toast.error('No replica selected') - await removeReadReplica({ projectRef, identifier: selectedReplica.identifier }) + await removeReadReplica({ + projectRef, + identifier: selectedReplica.identifier, + invalidateReplicaQueries: true, + }) } return ( diff --git a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx index 3d9d7be27be..e34538d6d05 100644 --- a/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx @@ -1,6 +1,6 @@ import { useParams } from 'common' import { partition } from 'lodash' -import { Globe2, Loader2, Network } from 'lucide-react' +import { ChevronDown, Globe2, Loader2, Network } from 'lucide-react' import { useTheme } from 'next-themes' import { useEffect, useMemo, useRef, useState } from 'react' import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow' @@ -12,7 +12,6 @@ import { DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, - IconChevronDown, } from 'ui' import AlertError from 'components/ui/AlertError' @@ -24,11 +23,12 @@ import { timeout } from 'lib/helpers' import { useSubscriptionPageStateSnapshot } from 'state/subscription-page' import ComputeInstanceSidePanel from '../../Addons/ComputeInstanceSidePanel' import DeployNewReplicaPanel from './DeployNewReplicaPanel' +import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal' import DropReplicaConfirmationModal from './DropReplicaConfirmationModal' +import { REPLICA_STATUS } from './InstanceConfiguration.constants' import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceConfiguration.utils' import { LoadBalancerNode, PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode' import MapView from './MapView' -import DropAllReplicasConfirmationModal from './DropAllReplicasConfirmationModal' // [Joshen] Just FYI, UI assumes single provider for primary + replicas // [Joshen] Idea to visualize grouping based on region: https://reactflow.dev/examples/layout/sub-flows @@ -38,7 +38,7 @@ const InstanceConfigurationUI = () => { const reactFlow = useReactFlow() const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() - const numComingUp = useRef() + const numTransition = useRef() const snap = useSubscriptionPageStateSnapshot() const [view, setView] = useState<'flow' | 'map'>('flow') @@ -76,13 +76,21 @@ const InstanceConfigurationUI = () => { { refetchInterval: refetchInterval as any, refetchOnWindowFocus: false, - onSuccess: async (data) => { - const comingUpReplicas = data.filter((db) => db.status === 'COMING_UP') - const hasTransientStatus = comingUpReplicas.length > 0 + onSuccess: async (res) => { + const fixedStatues = [ + REPLICA_STATUS.ACTIVE_HEALTHY, + REPLICA_STATUS.ACTIVE_UNHEALTHY, + REPLICA_STATUS.INIT_READ_REPLICA_FAILED, + ] + const replicasInTransition = res.filter((db) => !fixedStatues.includes(db.status)) + const hasTransientStatus = replicasInTransition.length > 0 // If any replica's status has changed, refetch databases - if (numComingUp.current !== comingUpReplicas.length) { - numComingUp.current = comingUpReplicas.length + if ( + numTransition.current !== replicasInTransition.length || + res.length !== (data ?? []).length + ) { + numTransition.current = replicasInTransition.length await refetchReplicas() setTimeout(() => refetchLoadBalancers(), 2000) } @@ -192,7 +200,7 @@ const InstanceConfigurationUI = () => {
diff --git a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx index 34cea58a523..0bcc0ca15a7 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogsPreviewer.tsx @@ -24,7 +24,7 @@ import { useSelectedOrganization } from 'hooks' import useLogsPreview from 'hooks/analytics/useLogsPreview' import { useUpgradePrompt } from 'hooks/misc/useUpgradePrompt' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' -import { LOGS_TABLES } from './Logs.constants' +import { LOGS_TABLES, LOG_ROUTES_WITH_REPLICA_SUPPORT } from './Logs.constants' import UpgradePrompt from './UpgradePrompt' /** @@ -110,9 +110,23 @@ export const LogsPreviewer = ({ }, [its, subscription]) useEffect(() => { - if (readReplicasEnabled && db !== undefined) { - const database = databases?.find((d) => d.identifier === db) - if (database !== undefined) state.setSelectedDatabaseId(db) + if (readReplicasEnabled) { + if (db !== undefined) { + const database = databases?.find((d) => d.identifier === db) + if (database !== undefined) state.setSelectedDatabaseId(db) + } else if ( + state.selectedDatabaseId !== undefined && + state.selectedDatabaseId !== projectRef + ) { + if (LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname)) { + router.push({ + pathname: router.pathname, + query: { ...router.query, db: state.selectedDatabaseId }, + }) + } else { + state.setSelectedDatabaseId(projectRef) + } + } } }, [readReplicasEnabled, db, isSuccess]) diff --git a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx index 845d27b9c10..b9ff3938c47 100644 --- a/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/PreviewFilterPanel.tsx @@ -1,25 +1,15 @@ +import { Eye, EyeOff, RefreshCw, Search, Terminal } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import { - Button, - IconEye, - IconEyeOff, - IconRefreshCw, - IconSearch, - IconTerminal, - Input, - TooltipContent_Shadcn_, - TooltipTrigger_Shadcn_, - Tooltip_Shadcn_, -} from 'ui' +import { Button, Input, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_, Tooltip_Shadcn_ } from 'ui' import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext' import CSVButton from 'components/ui/CSVButton' import DatabaseSelector from 'components/ui/DatabaseSelector' import { Filters, LogSearchCallback, LogTemplate, PREVIEWER_DATEPICKER_HELPERS } from '.' import DatePickers from './Logs.DatePickers' -import { FILTER_OPTIONS, LogsTableName } from './Logs.constants' +import { FILTER_OPTIONS, LOG_ROUTES_WITH_REPLICA_SUPPORT, LogsTableName } from './Logs.constants' import LogsFilterPopover from './LogsFilterPopover' interface PreviewFilterPanelProps { @@ -72,8 +62,7 @@ const PreviewFilterPanel = ({ // [Joshen] These are the routes tested that can show replica logs const showDatabaseSelector = - project?.is_read_replicas_enabled && - ['/project/[ref]/logs/edge-logs', '/project/[ref]/logs/pooler-logs'].includes(router.pathname) + project?.is_read_replicas_enabled && LOG_ROUTES_WITH_REPLICA_SUPPORT.includes(router.pathname) const hasEdits = search !== defaultSearchValue @@ -99,11 +88,11 @@ const PreviewFilterPanel = ({ {newCount > 1000 ? `${Math.floor(newCount / 100) / 10}K` : newCount}

-
+
)} - + } loading={isLoading} @@ -147,7 +136,7 @@ const PreviewFilterPanel = ({ }} icon={
- +
} value={search} @@ -205,7 +194,7 @@ const PreviewFilterPanel = ({ @@ -217,7 +206,7 @@ const PreviewFilterPanel = ({
- From 7ad6594648a1d33a034b32a31148c031200a2c46 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 29 Apr 2024 20:57:46 +0700 Subject: [PATCH 21/44] Add realtime explore button (#23404) * Add realtime explore button * Update icons --- .../Home/NewProjectPanel/NewProjectPanel.tsx | 61 ++++++------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx index 122e549468a..0ef1e55ef12 100644 --- a/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx +++ b/apps/studio/components/interfaces/Home/NewProjectPanel/NewProjectPanel.tsx @@ -1,10 +1,11 @@ +import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import SVG from 'react-inlinesvg' -import { Button, IconActivity, IconArchive, IconCode, IconExternalLink, IconKey } from 'ui' +import { Button } from 'ui' import Panel from 'components/ui/Panel' import { useIsFeatureEnabled } from 'hooks' +import { Auth, EdgeFunctions, Realtime, SqlEditor, Storage, TableEditor } from 'icons' import APIKeys from './APIKeys' import GetStartedHero from './GetStartedHero' @@ -45,37 +46,13 @@ const NewProjectPanel = () => {

- - -
+ -
@@ -56,7 +67,7 @@ const Introduction = ({ selectedLang }: IntroductionProps) => { If you still want to use GraphQL, you can. Supabase provides you with a full Postgres database, so as long as your middleware can connect to the database then you can still use the tools you love. You can find the database connection details{' '} - in the settings. + in the settings.

diff --git a/apps/studio/components/interfaces/Docs/PublicSchemaNotEnabledAlert.tsx b/apps/studio/components/interfaces/Docs/PublicSchemaNotEnabledAlert.tsx new file mode 100644 index 00000000000..99be76b020f --- /dev/null +++ b/apps/studio/components/interfaces/Docs/PublicSchemaNotEnabledAlert.tsx @@ -0,0 +1,37 @@ +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { Info } from 'lucide-react' +import ReactMarkdown from 'react-markdown' +import Link from 'next/link' +import { useParams } from 'common' + +const PublicSchemaNotEnabledAlert = () => { + const { ref: projectRef } = useParams() + + return ( + + + + The `public` schema for this project is not exposed + + + + You will not be able to query tables and views in the `public` schema via supabase-js or + HTTP clients. + + +
+ +
+
+
+ ) +} + +export default PublicSchemaNotEnabledAlert diff --git a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx index da72304b181..c3d13ae124f 100644 --- a/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx +++ b/apps/studio/components/interfaces/Settings/API/PostgrestConfig.tsx @@ -16,29 +16,50 @@ import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-co import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation' import { useSchemasQuery } from 'data/database/schemas-query' import { useCheckPermissions } from 'hooks' -import { Form, IconAlertCircle, Input, InputNumber } from 'ui' -import MultiSelect from 'ui-patterns/MultiSelect' +import { AlertCircle, Info } from 'lucide-react' +import { + AlertDescription_Shadcn_, + AlertTitle_Shadcn_, + Alert_Shadcn_, + Button, + Form, + Input, + InputNumber, +} from 'ui' +import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2' +import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' + +import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' +import Link from 'next/link' const PostgrestConfig = () => { const { ref: projectRef } = useParams() const { project } = useProjectContext() - const { data: schemas } = useSchemasQuery({ + + const { data: schemas, isLoading: isLoadingSchemas } = useSchemasQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef }) + const { mutate: updatePostgrestConfig, isLoading: isUpdating } = useProjectPostgrestConfigUpdateMutation({ onSuccess: () => { toast.success('Successfully saved settings') }, }) + const canUpdatePostgrestConfig = useCheckPermissions( PermissionAction.UPDATE, 'custom_config_postgrest' ) + const { data: extensions } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const formId = 'project-postgres-config' const initialValues = { db_schema: '', max_rows: '', db_extra_search_path: '', db_pool: '' } @@ -53,7 +74,6 @@ const PostgrestConfig = () => { }) } - const permanentSchema = ['public', 'storage'] const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions'] const schema = schemas @@ -66,15 +86,22 @@ const PostgrestConfig = () => { id: x.id, value: x.name, name: x.name, - disabled: indexOf(permanentSchema, x.name) >= 0 ? true : false, + disabled: false, } }) ?? [] + const isPublicSchemaEnabled = config?.db_schema + .split(',') + .map((name) => name.trim()) + .includes('public') + + const isGraphqlExtensionEnabled = + (extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null + return (
{}} onSubmit={updateConfig}> {({ handleReset, resetForm, values, initialValues }: any) => { const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues) - // [Alaister] although this "technically" is breaking the rules of React hooks // it won't error because the hooks are always rendered in the same order // eslint-disable-next-line react-hooks/rules-of-hooks @@ -86,6 +113,7 @@ const PostgrestConfig = () => { db_extra_search_path: config.db_extra_search_path ?? '', db_pool: config.db_pool || null, } + resetForm({ values, initialValues: values }) } }, [config]) @@ -114,51 +142,81 @@ const PostgrestConfig = () => { > {isError ? (
- +

Failed to retrieve API settings

) : ( <> Exposed schemas}> - - {schema.length >= 1 && ( - + {isLoadingSchemas ? ( +
+ + +
+ ) : ( + + {schema.length >= 1 && ( +
+ { + let updatedValues: any = values + updatedValues.db_schema = event.join(', ') + resetForm({ values: updatedValues, initialValues: updatedValues }) + updateConfig({ ...updatedValues }) + }} + /> +

The schemas to expose in your API. Tables, views and stored procedures in these schemas will get API endpoints. - public and{' '} - storage are protected by default. - - } - emptyMessage={ - <> - -

-

- No schema available to choose -

-

- New schemas you create will appear here -

-
- - } - // value must be passed as array of strings - value={(values?.db_schema ?? '').replace(/ /g, '').split(',')} - // onChange returns array of strings - onChange={(event) => { - let updatedValues: any = values - updatedValues.db_schema = event.join(', ') - resetForm({ values: updatedValues, initialValues: updatedValues }) - updateConfig({ ...updatedValues }) - }} - /> - )} - +

+ + {!isPublicSchemaEnabled && ( + + + + The public schema for this project is not exposed + + +

+ You will not be able to query tables and views in the{' '} + public schema via supabase-js or HTTP clients. +

+ {isGraphqlExtensionEnabled && ( +
+
+ Tables in the public schema are still exposed + over our GraphQL endpoints. +
+

+ +

+
+ )} +
+
+ )} +
+ )} +
+ )}
+ Extra search path}> { -
+
From bd9c919b244ca75d231a36be67d283b7571f9247 Mon Sep 17 00:00:00 2001 From: Monica Khoury <99693443+monicakh@users.noreply.github.com> Date: Tue, 30 Apr 2024 14:04:10 +0300 Subject: [PATCH 31/44] Fix: account deletion org message (#23442) Fix: account deletion message --- .../interfaces/Account/Preferences/DeleteAccountButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx index abe01bb2986..44bd433e9f1 100644 --- a/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx +++ b/apps/studio/components/interfaces/Account/Preferences/DeleteAccountButton.tsx @@ -107,7 +107,7 @@ export const DeleteAccountButton = () => { Before submitting an account deletion request, please ensure that your account is - not part of any organization. This can be done by leaving leaving or deleting the + not part of any organization. This can be done by leaving or deleting the organizations that you are a part of. */} + + + ), + }) + } else { + toast.error(`Failed to delete table row: ${data.message}`) + } } else { onError(data, variables, context) } diff --git a/apps/studio/data/tables/table-query.ts b/apps/studio/data/tables/table-query.ts index ae9b159e9f3..2e8bea2b5dd 100644 --- a/apps/studio/data/tables/table-query.ts +++ b/apps/studio/data/tables/table-query.ts @@ -1,10 +1,9 @@ import type { PostgresTable } from '@supabase/postgres-meta' import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' -// import { get } from 'lib/common/fetch' -import { API_URL } from 'lib/constants' -import { tableKeys } from './keys' import { useCallback } from 'react' + import { get, handleError } from 'data/fetchers' +import { tableKeys } from './keys' export type TableVariables = { id?: number diff --git a/packages/ui/src/layout/PortalToast/PortalToast.tsx b/packages/ui/src/layout/PortalToast/PortalToast.tsx index 38b646d3619..69d808ff15e 100644 --- a/packages/ui/src/layout/PortalToast/PortalToast.tsx +++ b/packages/ui/src/layout/PortalToast/PortalToast.tsx @@ -42,7 +42,7 @@ const PortalToast = () => ( ? 'max-w-[380px]' : isConsentToast ? 'max-w-none sm:max-w-[800px]' - : 'max-w-[260px]' + : 'max-w-[280px]' }`} > {message} From 855a117f5c972e53f881eba6f17f759f7005957f Mon Sep 17 00:00:00 2001 From: Stojan Dimitrovski Date: Thu, 2 May 2024 09:23:15 +0200 Subject: [PATCH 43/44] feat: add advanced teams / enterprise auth settings (#23418) * feat: add advanced teams / enterprise auth settings * tweaks + upgrade prompt * update codegen * Fix newline at end of file in api.d.ts --------- Co-authored-by: Alaister Young Co-authored-by: Alaister Young --- .../AdvancedAuthSettingsForm.tsx | 78 ++++++++++++++++++- packages/api-types/types/api.d.ts | 39 +++++++++- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx index 81079e9f98a..abea3607806 100644 --- a/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/AdvancedAuthSettingsForm/AdvancedAuthSettingsForm.tsx @@ -22,8 +22,11 @@ import { FormSectionLabel, } from 'components/ui/Forms' import { useAuthConfigQuery } from 'data/auth/auth-config-query' +import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation' -import { useCheckPermissions } from 'hooks' +import { useCheckPermissions, useSelectedOrganization } from 'hooks' +import { IS_PLATFORM } from 'lib/constants' +import UpgradeToPro from 'components/ui/UpgradeToPro' const schema = object({ JWT_EXP: number() @@ -35,7 +38,13 @@ const schema = object({ .required('Must have a Reuse Interval value'), MFA_MAX_ENROLLED_FACTORS: number() .min(0, 'Must be be a value more than 0') - .max(30, 'Must be a value less than 30'), + .max(30, 'Must be a value no greater than 30'), + DB_MAX_POOL_SIZE: number() + .min(1, 'Must be 1 or larger') + .max(200, 'Must be a value no greater than 200'), + API_MAX_REQUEST_DURATION: number() + .min(5, 'Must be 5 or larger') + .max(30, 'Must be a value no greater than 30'), }) const AdvancedAuthSettingsForm = () => { @@ -52,17 +61,33 @@ const AdvancedAuthSettingsForm = () => { const formId = 'auth-config-advanced-form' const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue') + const organization = useSelectedOrganization() + const { data: subscription, isSuccess: isSuccessSubscription } = useOrgSubscriptionQuery({ + orgSlug: organization?.slug, + }) + + const isTeamsEnterprisePlan = + isSuccessSubscription && subscription.plan.id !== 'free' && subscription.plan.id !== 'pro' + const promptTeamsEnterpriseUpgrade = IS_PLATFORM && !isTeamsEnterprisePlan + const INITIAL_VALUES = { SITE_URL: authConfig?.SITE_URL, JWT_EXP: authConfig?.JWT_EXP, REFRESH_TOKEN_ROTATION_ENABLED: authConfig?.REFRESH_TOKEN_ROTATION_ENABLED || false, SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: authConfig?.SECURITY_REFRESH_TOKEN_REUSE_INTERVAL, MFA_MAX_ENROLLED_FACTORS: authConfig?.MFA_MAX_ENROLLED_FACTORS || 10, + DB_MAX_POOL_SIZE: authConfig?.DB_MAX_POOL_SIZE || 10, + API_MAX_REQUEST_DURATION: authConfig?.API_MAX_REQUEST_DURATION || 10, } const onSubmit = (values: any, { resetForm }: any) => { const payload = { ...values } + if (!isTeamsEnterprisePlan) { + delete payload.DB_MAX_POOL_SIZE + delete payload.API_MAX_REQUEST_DURATION + } + updateAuthConfig( { projectRef: projectRef!, config: payload }, { @@ -167,10 +192,59 @@ const AdvancedAuthSettingsForm = () => { size="small" label="Maximum number of per-user MFA factors" descriptionText="How many MFA factors can be enrolled at once per user." + actions={factors} disabled={!canUpdateConfig} /> + + Max Direct Database Connections} + > + + {promptTeamsEnterpriseUpgrade && ( + + )} + + connections} + disabled={!canUpdateConfig || !isTeamsEnterprisePlan} + /> + + + + Max Request Duration}> + + {promptTeamsEnterpriseUpgrade && ( + + )} + + seconds} + disabled={!canUpdateConfig || !isTeamsEnterprisePlan} + /> + + ) diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index b338381e479..4181f308eb8 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -2127,14 +2127,19 @@ export interface components { SMS_TEST_OTP_VALID_UNTIL: string HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED: boolean HOOK_MFA_VERIFICATION_ATTEMPT_URI: string + HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS: string HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED: boolean HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI: string + HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS: string HOOK_CUSTOM_ACCESS_TOKEN_ENABLED: boolean HOOK_CUSTOM_ACCESS_TOKEN_URI: string + HOOK_CUSTOM_ACCESS_TOKEN_SECRETS: string HOOK_SEND_SMS_ENABLED: boolean HOOK_SEND_SMS_URI: string + HOOK_SEND_SMS_SECRETS: string HOOK_SEND_EMAIL_ENABLED: boolean HOOK_SEND_EMAIL_URI: string + HOOK_SEND_EMAIL_SECRETS: string EXTERNAL_APPLE_ENABLED: boolean EXTERNAL_APPLE_CLIENT_ID: string EXTERNAL_APPLE_SECRET: string @@ -2199,6 +2204,8 @@ export interface components { EXTERNAL_ZOOM_ENABLED: boolean EXTERNAL_ZOOM_CLIENT_ID: string EXTERNAL_ZOOM_SECRET: string + DB_MAX_POOL_SIZE: number | null + API_MAX_REQUEST_DURATION: number | null } UpdateGoTrueConfigBody: { SITE_URL?: string @@ -2282,14 +2289,19 @@ export interface components { SMS_TEMPLATE?: string HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED?: boolean HOOK_MFA_VERIFICATION_ATTEMPT_URI?: string + HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS?: string HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED?: boolean HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI?: string + HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS?: string HOOK_CUSTOM_ACCESS_TOKEN_ENABLED?: boolean HOOK_CUSTOM_ACCESS_TOKEN_URI?: string + HOOK_CUSTOM_ACCESS_TOKEN_SECRETS?: string HOOK_SEND_SMS_ENABLED?: boolean HOOK_SEND_SMS_URI?: string + HOOK_SEND_SMS_SECRETS?: string HOOK_SEND_EMAIL_ENABLED?: boolean HOOK_SEND_EMAIL_URI?: string + HOOK_SEND_EMAIL_SECRETS?: string EXTERNAL_APPLE_ENABLED?: boolean EXTERNAL_APPLE_CLIENT_ID?: string EXTERNAL_APPLE_SECRET?: string @@ -2354,6 +2366,8 @@ export interface components { EXTERNAL_ZOOM_ENABLED?: boolean EXTERNAL_ZOOM_CLIENT_ID?: string EXTERNAL_ZOOM_SECRET?: string + DB_MAX_POOL_SIZE?: number + API_MAX_REQUEST_DURATION?: number } UserBody: { id?: string @@ -2679,8 +2693,8 @@ export interface components { InviteResponse: { organization_name: string invite_id: string - sso_mismatch: boolean token_does_not_exist: boolean + sso_mismatch: boolean email_match: boolean authorized_user: boolean expired_token: boolean @@ -5138,6 +5152,8 @@ export interface components { connection_string?: string } AuthConfigResponse: { + api_max_request_duration: number | null + db_max_pool_size: number | null disable_signup: boolean | null external_anonymous_users_enabled: boolean | null external_apple_additional_client_ids: string | null @@ -5208,10 +5224,19 @@ export interface components { external_zoom_secret: string | null hook_custom_access_token_enabled: boolean | null hook_custom_access_token_uri: string | null + hook_custom_access_token_secrets: string | null hook_mfa_verification_attempt_enabled: boolean | null hook_mfa_verification_attempt_uri: string | null + hook_mfa_verification_attempt_secrets: string | null hook_password_verification_attempt_enabled: boolean | null hook_password_verification_attempt_uri: string | null + hook_password_verification_attempt_secrets: string | null + hook_send_sms_enabled: boolean | null + hook_send_sms_uri: string | null + hook_send_sms_secrets: string | null + hook_send_email_enabled: boolean | null + hook_send_email_uri: string | null + hook_send_email_secrets: string | null jwt_exp: number | null mailer_allow_unverified_email_sign_ins: boolean | null mailer_autoconfirm: boolean | null @@ -5222,14 +5247,14 @@ export interface components { mailer_subjects_email_change: string | null mailer_subjects_invite: string | null mailer_subjects_magic_link: string | null - mailer_subjects_recovery: string | null mailer_subjects_reauthentication: string | null + mailer_subjects_recovery: string | null mailer_templates_confirmation_content: string | null mailer_templates_email_change_content: string | null mailer_templates_invite_content: string | null mailer_templates_magic_link_content: string | null - mailer_templates_recovery_content: string | null mailer_templates_reauthentication_content: string | null + mailer_templates_recovery_content: string | null mfa_max_enrolled_factors: number | null password_hibp_enabled: boolean | null password_min_length: number | null @@ -5365,14 +5390,19 @@ export interface components { sms_template?: string hook_mfa_verification_attempt_enabled?: boolean hook_mfa_verification_attempt_uri?: string + hook_mfa_verification_attempt_secrets?: string hook_password_verification_attempt_enabled?: boolean hook_password_verification_attempt_uri?: string + hook_password_verification_attempt_secrets?: string hook_custom_access_token_enabled?: boolean hook_custom_access_token_uri?: string + hook_custom_access_token_secrets?: string hook_send_sms_enabled?: boolean hook_send_sms_uri?: string + hook_send_sms_secrets?: string hook_send_email_enabled?: boolean hook_send_email_uri?: string + hook_send_email_secrets?: string external_apple_enabled?: boolean external_apple_client_id?: string external_apple_secret?: string @@ -5437,6 +5467,8 @@ export interface components { external_zoom_enabled?: boolean external_zoom_client_id?: string external_zoom_secret?: string + db_max_pool_size?: number + api_max_request_duration?: number } CreateThirdPartyAuthBody: { oidc_issuer_url?: string @@ -9623,6 +9655,7 @@ export interface operations { startDate: string endDate: string interval?: '1m' | '5m' | '10m' | '30m' | '1h' | '1d' + databaseIdentifier?: string } path: { /** @description Project ref */ From 049d339a0a6d7f5506010c72c827d1cc675588d7 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 2 May 2024 16:12:50 +0800 Subject: [PATCH 44/44] chore: mark functions failed status as unhealthy (#23626) --- .../components/interfaces/BranchManagement/BranchStatusBadge.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/studio/components/interfaces/BranchManagement/BranchStatusBadge.tsx b/apps/studio/components/interfaces/BranchManagement/BranchStatusBadge.tsx index fb4a9f5112a..f094cc0e65f 100644 --- a/apps/studio/components/interfaces/BranchManagement/BranchStatusBadge.tsx +++ b/apps/studio/components/interfaces/BranchManagement/BranchStatusBadge.tsx @@ -14,6 +14,7 @@ const UNHEALTHY_STATUES: Status[] = [ 'INIT_FAILED', 'UNKNOWN', 'MIGRATIONS_FAILED', + 'FUNCTIONS_FAILED', ] const WAITING_STATUSES: Status[] = [ 'COMING_UP',