diff --git a/apps/reference/docs/guides/cli/local-development.mdx b/apps/reference/docs/guides/cli/local-development.mdx index aade041eaa5..7883f25647c 100644 --- a/apps/reference/docs/guides/cli/local-development.mdx +++ b/apps/reference/docs/guides/cli/local-development.mdx @@ -107,6 +107,10 @@ http://localhost:54321/auth/v1/ # Auth (GoTrue) +:::note +To access the database from an edge function in your local Supabase setup, replace `localhost` with `host.docker.internal`. +::: + ## Database migrations Database changes are managed through "migrations." Database migrations are a common way of tracking changes to your database over time. diff --git a/apps/reference/docs/guides/integrations/polyscale.mdx b/apps/reference/docs/guides/integrations/polyscale.mdx new file mode 100644 index 00000000000..a3534305bf4 --- /dev/null +++ b/apps/reference/docs/guides/integrations/polyscale.mdx @@ -0,0 +1,63 @@ +--- +id: polyscale +title: 'PolyScale' +description: 'The easiest way to get low-latency reads from your Supabase database for multi-region applications is by using PolyScale, a code-free global caching service.' +--- + +[PolyScale](https://polyscale.ai) is an intelligent, serverless database caching engine which allows low-latency reads from Supabase globally, no coding required. Supabase can be connected to PolyScale in minutes, providing you fast access to your Supabase data around the globe. + +This guide explains how to connect Supabase to a PolyScale cache. + +The video below illustrates how to get connected. Or you can read the steps below. + +
+ +
+ +## Step 0: Create a PolyScale account + +If you do not already have a PolyScale account, you can create an account [here](https://app.polyscale.ai/signup). PolyScale offers a free tier and no credit card is required. + +## Step 1: Create your PolyScale Cache + +### 1.1 Retrieve your Supabase Host + +In your Supabase project, click on `Settings > Database` and scroll down to the `Connection info` section to copy your database `Host`. + +![supabase-host](/img/guides/integrations/polyscale/supabase-host.png) + +### 1.2 Configure your PolyScale Cache + +- In your PolyScale account, click on the **New Cache** button +- Give the cache a **Name** +- Select **PostgreSQL** for the **Type** +- Enter the **Host** from Step 1.1 above +- Enter `5432` for the **Port** +- Click **Create** + +![create-cache-supabase](/img/guides/integrations/polyscale/create-cache-supabase-400.png) + +Your cache is now created. PolyScale automatically checks to see that your database is accessible from all our global PoPs. + +## Step 2: Connect to your PolyScale Cache + +Using your PolyScale cache is simple -- instead of connecting to your Supabase database directly, you'll replace your orignal connection string with the PolyScale connection string in your application. + +For example, if your original connection string was: `postgres://postgres:zqSPGHFAbPLvVCKw@db.rogpiubvixysbakciwqz.supabase.co:5432` + +Your PolyScale connection string would be: `postgres://postgres:zqSPGHFAbPLvVCKw@psedge.global:5432?application_name=a645cb93-fa53-46b2-9d6c-227e357e5bfb` + +You can read more about connecting to PolyScale [here](https://docs.polyscale.ai/connecting-to-polyscale#postgresql) + +That's it. + +## All done! + +You can read more about PolyScale [here](https://www.polyscale.ai/) or check out our [documentation](https://docs.polyscale.ai/). \ No newline at end of file diff --git a/apps/reference/nav/_referenceSidebars.js b/apps/reference/nav/_referenceSidebars.js index f66802b35b4..bab8deb5fea 100644 --- a/apps/reference/nav/_referenceSidebars.js +++ b/apps/reference/nav/_referenceSidebars.js @@ -220,6 +220,7 @@ const sidebars = { 'guides/integrations/draftbit', 'guides/integrations/pgmustard', 'guides/integrations/plasmic', + 'guides/integrations/polyscale', 'guides/integrations/prisma', 'guides/integrations/sequin', 'guides/integrations/snaplet', diff --git a/apps/reference/static/img/guides/integrations/polyscale/create-cache-supabase-400.png b/apps/reference/static/img/guides/integrations/polyscale/create-cache-supabase-400.png new file mode 100644 index 00000000000..4037eb53877 Binary files /dev/null and b/apps/reference/static/img/guides/integrations/polyscale/create-cache-supabase-400.png differ diff --git a/apps/reference/static/img/guides/integrations/polyscale/supabase-host.png b/apps/reference/static/img/guides/integrations/polyscale/supabase-host.png new file mode 100644 index 00000000000..2202234d716 Binary files /dev/null and b/apps/reference/static/img/guides/integrations/polyscale/supabase-host.png differ diff --git a/apps/www/_blog/2022-10-03-postgres-wasm.mdx b/apps/www/_blog/2022-10-03-postgres-wasm.mdx new file mode 100644 index 00000000000..d3108c92b04 --- /dev/null +++ b/apps/www/_blog/2022-10-03-postgres-wasm.mdx @@ -0,0 +1,243 @@ +--- +title: Postgres WASM by Snaplet and Supabase +description: We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. +author: burggraf +image: postgres-wasm/postgres-wasm.jpg +thumb: postgres-wasm/postgres-wasm.jpg +tags: + - postgres +date: '2022-10-03' +toc_depth: 3 +--- + +Today we're open sourcing [`postgres-wasm`](https://github.com/snaplet/postgres-wasm) with our friends at [Snaplet](https://www.snaplet.dev/). + +postgres-wasm is a PostgreSQL server that runs inside a browser. It provides a full suite of features, including persisting state to browser, +restoring from pg_dump, and logical replication from a remote database. + +We're not the first to run Postgres in the browser - that title belongs to the team at [Crunchy Data](https://www.crunchydata.com/) who shared their +version of this [on HN](https://news.ycombinator.com/item?id=32498435) a month ago. It's awesome, and we wanted +an [open source version](https://staltz.com/time-till-open-source-alternative.html), so we teamed up with Snaplet build it. +Let's explore how it's built, and some extra features we've added. + +
+ +🐈 Who's Snaplet? +
+Snaplet gives developers production-accurate data and preview databases that they can code against, so they can focus on shipping. +
+Checkout out how you can use Snaplet to clone Supabase environments. + +
+ +## Demo + +Before we get into the technical details, try it out yourself (warning: it will download ~30MB): + +[wasm.supabase.com](https://wasm.supabase.com) + +![Postgres inside a browser](/images/blog/postgres-wasm/postgres-wasm-browser.png) + +To run it locally: + +```bash +# From Snaplet Repo +git clone git@github.com:snaplet/postgres-wasm.git +cd postgres-browser/packages/pg-browser +npx serve + +# From Supabase Fork +git clone git@github.com:supabase-community/postgres-wasm.git +cd postgres-wasm +git checkout web +cd packages/supabrowser +npx serve +``` + +And open a browser at [localhost:3000](http://localhost:3000). + +## Features + +Our demo version has a few neat features! + +- Postgres 14.5, psql, pg_dump, etc. +- Save & restore state to/from a file. +- Save & restore Postgres state to/from the browser storage (IndexedDB). +- Quick start from a state file or fully reboot the emulator. +- Memory configuration options from 128MB to 1024MB. +- Adjust the font size for the terminal. +- Upload files to the emulator (including database dumps and CSVs). +- Download files from the emulator. +- Outgoing network connectivity from the emulator to the internet. +- Incoming network tunnel to postgres port 5432 inside the emulator. + +## Why? + +That's a good question. `postgres-wasm` is currently about 30mb. So at this stage, running Postgres in the browser isn't great for general use-cases. +It has a lot of potential though. Some ideas we'll be playing with over the next few months: + +- **Documentation:** for tutorials and demos. +- **Offline data:** running it in the browser for an offline cache, similar to [sql.js](https://sql.js.org/#/) or [absurd-sql](https://github.com/jlongster/absurd-sql). +- **Offline data analysis:** using it in a dashboard for offline data analysis and charts. +- **Testing:** testing PostgresSQL functions, triggers, data modeling, logical replication, etc. +- **Dev environments:** use it as a development environment — pull data from production or push new data, functions, triggers, views up to production. +- **Snapshots:** create a test version of your database with sample data, then take a snapshot to send to other developers. +- **Support:** send snapshots of your database to support personnel to demonstrate an issue you're having. + +## Repository Overview + +We've divided in our repository into three folders: a virtual machine, a web application, and a network proxy. + +### Virtual Machine (VM) + +We create an embeddable Virtual Machine (VM) using [Buildroot](https://en.wikipedia.org/wiki/Buildroot). +Our VM is a stripped-down Linux build with Postgres installed. + +See [source code](https://github.com/snaplet/postgres-browser/tree/main/packages/buildroot). + +### Web application + +Next, we need to run the the VM inside our browser. How? [WASM](https://en.wikipedia.org/wiki/WebAssembly). +We use [v86](https://github.com/copy/v86) to run our VM inside the browser. Our demo application is very simple - plain HTML and some basic styling. + +See [source code](https://github.com/snaplet/postgres-browser/tree/main/packages/pg-browser). + +### Network proxy + +Running Postgres in a browser is great, but connecting to it with PgAdmin is even better. Unfortunately browsers block TCP network access to the VM. +To circumvent this, we proxy the traffic through websockets. We run a fork of [Websockproxy](https://github.com/benjamincburns/websockproxy) +which allows the emulator to talk to the internet by converting data sent over a websocket port into TCP packets. +Our fork of Websockproxy adds the ability to tunnel into the Postgres server. + +See [source code](https://github.com/snaplet/postgres-browser/tree/main/packages/websockproxy). + +## Technical deep-dive + +There were a lot of hurdles that we discovered throughout the development of postgres-wasm. + +### WASM + +The first thing to point out is that our implementation isn't _pure_ WASM. We attempted to compile Postgres for WASM directly from source, +but it was more complicated that we anticipated. + +Crunchy's HN post provided some hints about the approach they took, which was to virtualize a machine in the browser. We pursued this strategy too, settling on v86 +which emulates an x86-compatible CPU and hardware in the browser. + +### PostgreSQL 14 segfault errors + +We quickly had PostgreSQL 13.3 running using an outdated version of Buildroot. But version PG14+ wouldn't start, giving a segfault during initialization. We tried: + +- manually copying build files for PG14 into the older version(s) of Buildroot +- building with (many) newer copies of Buildroot +- adjusting kernel parameters and environment settings such as the amount of memory allocated to the emulator, etc. + +Eventually, Fabian, the creator of v86 suggested we turn off JIT compilation for v86 and that solved the issue. +He narrowed it down to a bug in v86 and is pushing an update that will fix it. +Switching Postgres from posix to sysv memory management also solved the issue for the current release of V86. + +### Optimizing startup time and image size + +With PG14 running in the emulator, we shifted our focus to performance. The image size for the emulator was too big for a browser-based tool. +Even with our best efforts, a compressed snapshot was over 30mb - a fairly large payload to download before you can see any interaction. + +We solved this by booting only a minimal Linux image and then dynamically loading the rest of the VM over HTTPS after initialization. + +We achieve this by mounting a compressed [9P filesystem]() in the VM. +9P provides a [Python script](https://github.com/supabase-community/postgres-wasm/tree/main/packages/buildroot/tools) which takes a filesystem folder, +renames every file an 8-character name and produces a `filesystem.json` file representing a nested structure with files, original file names, sizes, etc. +We then [copy this compressed output](https://github.com/supabase-community/postgres-wasm/blob/main/packages/buildroot/config/board/pg-browser/post-image.sh) to the VM. +We modified the kernel command line and v86 boot parameters to boot directly from the 9P filesystem, and even put our kernel file into the p9 filesystem. +All non-essential files are loaded asyncrhonously over HTTPS in the browser as needed. + +The initial state was smaller, but still 15-20mb in size. We discussed this with Fabian who pointed us towards the `page_poison=on` kernel parameter. +This parameter allowed us to clear caches before creating the snapshots, by forcing Linux to write arbitrary bytes on freed memory instead of random bytes, +so unused memory is compressed much more efficiently. + +The end result of all these changes? The compressed initial state file is about 12mb - including a _running network state and Postgres 14.4 running with psql loaded._ + +We experimented without Postgres started too - this produced an even smaller initial state but the time to initialize +Postgres was usually longer than the time to download a few extra megabytes. We opted for a faster startup time. + +Additionally, without a running state you'd hit a performance penalty every time you refreshed the page, +since Postgres would need to be initialized each time. In our current version, after the initial state is +downloaded and cached by the browser the VM feels almost instant on refresh. + +### Networking + +Networking was particularly difficult to solve. For security reasons, browsers only support access to a nominal set of ports. +In short, browsers speak “80/443” and Postgres speaks “5432” so there was no way for our Postgres to communicate with the outside world. + +WASM isn't much help here - a WebAssembly module running in the browser is generally not able to do anything outside itself, +except for calling functions exposed from JavaScript. + +Browsers provide another connection option for us however: Websockets. The nice thing about websocket connections is that they are _persistent_, +so all we needed was to figure out how to proxy the virtual machine traffic through a websocket connection. + +The VM includes an emulated network card (`ne2k-pci`) and a `proxy_url` startup parameter which points to an external server +running a websocket proxy (i.e. on the internet). The websocket proxy establishes connections via a websocket port, then accepts raw ethernet packets. +It turns those raw packets into TCP/IP packets and routes them between the Internet and the VM. + +![Postgres WASM to the proxy](/images/blog/postgres-wasm/pg-wasm-diagram1.jpg) + +With our tunnel established, we could now send network traffic to the outside world. For example, try “starting” the network on the VM, +"exiting" out of psql (`cmd`+`D`), and then run `ping 1.1.1.1`. + +![Postgres WASM to the internet](/images/blog/postgres-wasm/pg-wasm-diagram2.jpg) + +Most v86 VM's use an open proxy at `wss://relay.widgetry.org/` based on [websockproxy](https://github.com/benjamincburns/websockproxy). +This proxy allows the VM to communicate with the outside world. It can handle all the basic use-cases like loading +data from other PostgreSQL databases using `pg_dump` and `psql`, or operating as read-only replica of another database. + +However, this proxy doesn't allow incoming traffic to be routed into a VM running locally on a laptop. +For instance, you can't connect a local `PgAdmin4` to the PostgreSQL instance running inside your browser. +So we [forked](https://github.com/snaplet/postgres-wasm/tree/main/packages/websockproxy) the [proxy](https://github.com/benjamincburns/websockproxy) and +added nginx to provide a reverse proxy. + +![Postgres WASM with psql](/images/blog/postgres-wasm/pg-wasm-diagram3.jpg) + +The reverse proxy listens on a range of ports, mapping each port to the VM at a specific private address you want to reach. +The simple solution was to take the last 2 segments of the private IP and map it to a port number. So if your private IP is 10.5.6.123, +you connect to the proxy on port 6123 and that points you to PostgreSQL (running on port 5432) on private IP 10.5.6.123. Shortened addresses are padded, +so 10.5.6.2 maps to port 6002 and 10.5.6.44 to port 6044. + +![Postgres WASM with remote logins](/images/blog/postgres-wasm/pg-wasm-diagram4.jpg) + +### Sending commands to the emulator from the browser UI + +Once the emulator is running, in the words of Fabian, “it runs as a black box process”, and the only way to communicate between the +UI and the operating system running in the emulator is through `serial0_send` to send keystrokes over the emulated serial port, +or through the `create_file` function which uploads a file to the running emulator. + +To restart the network after restoring a state file, we need to run a short script to unbind and re-bind the emulated network card +(so it gets a new mac address and thus a unique private IP address at the proxy), and then a standard network restart command. +We could run this script by sending keystrokes to the emulator, but that's clumsy. What if the user is at a `psql` prompt? Then we'd send `\! script_command`. +But if they're at an OS prompt, that won't work. + +We solved the issue by creating a folder called `\inbox` and added a listener to the folder. When files arrive there, if the file ends in `.sh` (a shell script), +we `chmod +x` that file, execute it, then delete it. That allows us to run commands “in the background” without upsetting the UI. + +## Just for fun + +If you want to do something very cool, you can connect to another user's Postgres instance that they are running in their browser. +Here are the steps: + +1. Find a friend (sometimes the first step is the hardest) +2. Tell them to go to [wasm.supabase.com](https://wasm.supabase.com/) +3. Tell them to “Start Network” and get their Network URL +4. Have them set a password on their database `ALTER ROLE postgres WITH PASSWORD 'my_password';` +5. On your own machine, open a terminal and run + `psql postgres://postgres:my_password@proxy.wasm.supabase.com:` +6. And you're in! You're now connecting to a remote Postgres instance, inside a VM, inside a browser, from a terminal on your own computer. + +**Database Replication** + +Even more mind-blowing - it's possible to replicate data from an online PostgreSQL database (for example, a Supabase project) to PostgreSQL running inside your browser, +and vice-versa using Postgres's logical replication. The steps are no different than any other Postgres database - just use the proxy URL as the target. + +## What's next? + +For now, this is very experimental - but it has a lot of potential. If you want to get involved, please reach out to us or +the team at [Snaplet](https://www.snaplet.dev/). The work they're doing over at Snaplet is incredible, and we've had a blast collaborating with them. + +- Snaplet repo: [https://github.com/snaplet/postgres-wasm](https://github.com/snaplet/postgres-wasm) +- Supabase fork: [https://github.com/supabase-community/postgres-wasm](https://github.com/supabase-community/postgres-wasm) diff --git a/apps/www/data/Community.tsx b/apps/www/data/Community.tsx index a8a8506dc3a..56fce8a2552 100644 --- a/apps/www/data/Community.tsx +++ b/apps/www/data/Community.tsx @@ -42,7 +42,7 @@ const data: CommunityItem[] = [ }, { title: 'Twitter', - stat: '40,000+', + stat: '44,000+', statLabel: 'Followers', img: 'twitter.png', detail: () => ( diff --git a/apps/www/lib/authors.json b/apps/www/lib/authors.json index 17fe79ba71e..264614b5159 100644 --- a/apps/www/lib/authors.json +++ b/apps/www/lib/authors.json @@ -213,5 +213,13 @@ "position": "Guest Author", "author_url": "https://twitter.com/schlimmson", "author_image_url": "https://github.com/saimon24.png" + }, + { + "author_id": "burggraf", + "author": "Mark Burggraf", + "username": "burggraf", + "position": "Engineering", + "author_url": "https://github.com/burggraf", + "author_image_url": "https://github.com/burggraf.png" } ] diff --git a/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram1.jpg b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram1.jpg new file mode 100644 index 00000000000..14f86a8297b Binary files /dev/null and b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram1.jpg differ diff --git a/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram2.jpg b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram2.jpg new file mode 100644 index 00000000000..9d1c12dbc85 Binary files /dev/null and b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram2.jpg differ diff --git a/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram3.jpg b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram3.jpg new file mode 100644 index 00000000000..79ec87aad92 Binary files /dev/null and b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram3.jpg differ diff --git a/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram4.jpg b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram4.jpg new file mode 100644 index 00000000000..f936b13d80c Binary files /dev/null and b/apps/www/public/images/blog/postgres-wasm/pg-wasm-diagram4.jpg differ diff --git a/apps/www/public/images/blog/postgres-wasm/postgres-wasm-browser.png b/apps/www/public/images/blog/postgres-wasm/postgres-wasm-browser.png new file mode 100644 index 00000000000..94ff2d44901 Binary files /dev/null and b/apps/www/public/images/blog/postgres-wasm/postgres-wasm-browser.png differ diff --git a/apps/www/public/images/blog/postgres-wasm/postgres-wasm.jpg b/apps/www/public/images/blog/postgres-wasm/postgres-wasm.jpg new file mode 100644 index 00000000000..13b3a1497b3 Binary files /dev/null and b/apps/www/public/images/blog/postgres-wasm/postgres-wasm.jpg differ diff --git a/apps/www/public/rss.xml b/apps/www/public/rss.xml index a30e8bcc91b..f0910002bdd 100644 --- a/apps/www/public/rss.xml +++ b/apps/www/public/rss.xml @@ -5,9 +5,16 @@ https://supabase.com Latest news from Supabase en - Wed, 07 Sep 2022 16:04:00 +0000 + Sun, 02 Oct 2022 16:04:00 +0000 + https://supabase.com/blog/postgres-wasm + Postgres WASM by Snaplet and Supabase + https://supabase.com/blog/postgres-wasm + We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet. + Sun, 02 Oct 2022 16:04:00 +0000 + + https://supabase.com/blog/choosing-a-postgres-primary-key Choosing a Postgres Primary Key https://supabase.com/blog/choosing-a-postgres-primary-key diff --git a/packages/config/tailwind.config.js b/packages/config/tailwind.config.js index 0ca5ff20efb..9aa9601e9e1 100644 --- a/packages/config/tailwind.config.js +++ b/packages/config/tailwind.config.js @@ -108,6 +108,7 @@ module.exports = ui({ backgroundColor: theme('colors.scale[400]'), border: '1px solid ' + theme('colors.scale[500]'), borderRadius: theme('borderRadius.lg'), + wordBreak: 'break-all', }, a: { transition: 'box-shadow 0.1s ease-in-out', diff --git a/spec/supabase_js_v1_legacy.yml b/spec/supabase_js_v1_legacy.yml index 18655240e94..785be77887c 100644 --- a/spec/supabase_js_v1_legacy.yml +++ b/spec/supabase_js_v1_legacy.yml @@ -496,9 +496,9 @@ pages: const { data, error } = await supabase.auth.api.resetPasswordForEmail( 'user@email.com' ) - + /** - * Step 2: Once the user is redirected back to your application, + * Step 2: Once the user is redirected back to your application, * ask the user to reset their password. */ useEffect(() => { @@ -508,10 +508,10 @@ pages: const { data, error } = await supabase.auth.update({ password: newPassword, }) - + if (data) alert("Password updated successfully!") if (error) alert("There was an error updating your password.") - } + } }) }, []) ``` @@ -989,6 +989,20 @@ pages: .rpc('hello_world', {}, { count: 'exact' }) ``` + privileges: + description: | + By default, functions can be executed by any role. You can restrict this by + sql: | + ```sql + ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; + ``` + Then, you can choose which roles can execute functions + sql: | + ```sql + GRANT EXECUTE ON FUNCTION hello_world TO authenticated; + GRANT EXECUTE ON FUNCTION hello_world TO service_role; + ``` + subscribe(): title: 'on().subscribe()' $ref: '@supabase/supabase-js.lib/SupabaseQueryBuilder.SupabaseQueryBuilder.on' diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts index ac31dee7260..e20cebbba26 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts @@ -3,13 +3,21 @@ // Last updated as of 29th March 2022. // The code may not necessarily match with the name (ref SE_VAT) // https://stripe.com/docs/api/customer_tax_ids/create -interface TaxId { +export interface TaxId { name: string code: string country: string placeholder: string } +export interface StripeTaxId { + id: string + type: string + value: string + name: string + country?: string +} + export const TAX_IDS: TaxId[] = [ { name: 'AE TRN', @@ -174,7 +182,7 @@ export const TAX_IDS: TaxId[] = [ placeholder: '123456789', }, { - name: 'GR VAT', + name: 'EL VAT', code: 'eu_vat', country: 'Greece', placeholder: 'EL123456789', @@ -187,7 +195,7 @@ export const TAX_IDS: TaxId[] = [ }, { name: 'HR VAT', - code: 'hr_vat', + code: 'eu_vat', country: 'Croatia', placeholder: 'HR12345678912', }, @@ -207,7 +215,7 @@ export const TAX_IDS: TaxId[] = [ name: 'IE VAT', code: 'eu_vat', country: 'Ireland', - placeholder: 'IE12345678AB', + placeholder: 'IE1234567AB', }, { name: 'IL VAT', @@ -415,8 +423,8 @@ export const TAX_IDS: TaxId[] = [ }, { name: 'XI VAT', - code: 'xi_vat', - country: 'United Kingdom', + code: 'eu_vat', + country: 'United Kingdom (Northern Ireland)', placeholder: 'XI123456789', }, { diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx index e60fed693cb..35407e689b9 100644 --- a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx @@ -8,8 +8,9 @@ import { uuidv4 } from 'lib/helpers' import { post, delete_ } from 'lib/common/fetch' import { API_URL } from 'lib/constants' import Panel from 'components/ui/Panel' -import { TAX_IDS } from './TaxID.constants' +import { StripeTaxId, TAX_IDS } from './TaxID.constants' import NoPermission from 'components/ui/NoPermission' +import { sanitizeTaxID } from './TaxID.utils' interface Props { loading: boolean @@ -26,8 +27,8 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => { const [isSaving, setIsSaving] = useState(false) const [errors, setErrors] = useState([]) - const [taxIdValues, setTaxIdValues] = useState(taxIds) - const formattedTaxIds = taxIds.map((taxId: any) => { + const [taxIdValues, setTaxIdValues] = useState(taxIds) + const formattedTaxIds: StripeTaxId[] = taxIds.map((taxId: StripeTaxId) => { return { id: taxId.id, type: taxId.type, @@ -78,7 +79,7 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => { } const onRemoveTaxId = (id: string) => { - const updatedTaxIds = taxIdValues.filter((taxId: any) => { + const updatedTaxIds = taxIdValues.filter((taxId: StripeTaxId) => { return taxId.id !== id }) setTaxIdValues(updatedTaxIds) @@ -93,21 +94,21 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => { setIsSaving(true) try { const deletedIds = await Promise.all( - taxIds.map(async (taxId: any) => { + taxIds.map(async (taxId: StripeTaxId) => { return await delete_(`${API_URL}/organizations/${slug}/tax-ids`, { id: taxId.id }) }) ) const newIds = await Promise.all( - taxIdValues.map(async (taxId: any) => { + taxIdValues.map(async (taxId: StripeTaxId) => { + const sanitizedID = sanitizeTaxID(taxId) const result = await post(`${API_URL}/organizations/${slug}/tax-ids`, { - type: taxId.type, - value: taxId.value, + type: sanitizedID.type, + value: sanitizedID.value, }) - return { id: taxId.id, result } + return { id: sanitizedID.id, result } }) ) - const taxIdsWithErrors = newIds.filter((taxId: any) => { if (taxId.result.error) return taxId }) @@ -137,7 +138,8 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => {

Tax ID

- If you would like to include specific tax ID(s) to your invoices + If you would like to include specific tax ID(s) to your invoices.
+ Make sure the tax ID looks exactly like the placeholder text.

@@ -150,7 +152,7 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => { loading={loading} footer={ !loading && ( -
+
{!canUpdateTaxIds ? (

You need additional permissions to update this organization's tax IDs @@ -183,15 +185,15 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => { > {loading && taxIdValues.length === 0 ? (

-
-
-
+
+
+
) : ( - + {taxIdValues.length >= 1 ? (
- {taxIdValues.map((taxId: any, idx: number) => { + {taxIdValues.map((taxId: StripeTaxId, idx: number) => { const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name) return (
@@ -200,11 +202,13 @@ const TaxID: FC = ({ loading, taxIds, onTaxIdsUpdated }) => { onChange={(e: any) => onUpdateTaxId(taxId.id, 'name', e.target.value)} disabled={!canUpdateTaxIds} > - {TAX_IDS.map((option) => ( - - {option.name} - - ))} + {TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map( + (option) => ( + + {option.country} - {option.name} + + ) + )} = ({ loading, taxIds, onTaxIdsUpdated }) => { )} {canUpdateTaxIds && (
onAddNewTaxId()} > diff --git a/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts new file mode 100644 index 00000000000..f30e7ca3b5c --- /dev/null +++ b/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts @@ -0,0 +1,23 @@ +import { StripeTaxId, TAX_IDS } from './TaxID.constants' + +/** + * Sanitize EU VAT ids so they get prepended with the country code + * + * Ex: Belgium's VAT ID should be BE0123456789 not 0123456789 + * Only EU VAT ids are affected + */ +export const sanitizeTaxID = (taxId: StripeTaxId) => { + let newID = taxId + + const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name) + + // The prefix of the country name (ex: "BE" for Belgium, "ESA" for Spain)") + const countryPrefix = selectedTaxId?.placeholder.match(/[a-z]+|[^a-z]+/gi)?.[0] + + // if the value doesn't start with the country code, prepend them + if (taxId.type === 'eu_vat' && countryPrefix && !taxId.value.startsWith(countryPrefix)) { + newID.value = `${countryPrefix}${taxId.value}` + } + + return newID +} diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx index 8c3818e61ce..61bb58f92e2 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnDefaultValue.tsx @@ -60,8 +60,9 @@ const ColumnDefaultValue: FC = ({ } value={columnFields?.defaultValue ?? ''} format={columnFields?.format} - suggestionsHeader="Suggested expressions" suggestions={suggestions} + suggestionsHeader="Suggested expressions" + suggestionsTooltip="Suggested expressions" onChange={(event: any) => onUpdateField({ defaultValue: event.target.value })} onSelectSuggestion={(suggestion: Suggestion) => onUpdateField({ defaultValue: suggestion.value }) diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx index 201ab855edf..7e815011d86 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx @@ -1,7 +1,7 @@ import { FC, useEffect, useState } from 'react' import { isUndefined, isEmpty } from 'lodash' import { Dictionary } from 'components/grid' -import { Checkbox, SidePanel, Input } from '@supabase/ui' +import { Checkbox, SidePanel, Input, Button, IconExternalLink } from '@supabase/ui' import { PostgresColumn, PostgresRelationship, @@ -25,6 +25,7 @@ import { } from './ColumnEditor.utils' import { TEXT_TYPES } from '../SidePanelEditor.constants' import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from '../SidePanelEditor.types' +import Link from 'next/link' interface Props { column?: PostgresColumn @@ -54,7 +55,6 @@ const ColumnEditor: FC = ({ updateEditorDirty = () => {}, }) => { const isNewRecord = isUndefined(column) - const hasPrimaryKey = (selectedTable?.primary_keys ?? []).length > 0 const originalForeignKey = column ? getColumnForeignKey(column, selectedTable) : undefined const [errors, setErrors] = useState>({}) @@ -210,21 +210,24 @@ const ColumnEditor: FC = ({ enumTypes={enumTypes} error={errors.format} disabled={!isUndefined(columnFields?.foreignKey)} + description={ + + + + + + } onOptionSelect={(format: string) => onUpdateField({ format, defaultValue: null })} /> - - {isUndefined(columnFields.foreignKey) && (
{columnFields.format.includes('int') && ( diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts index fd68f6d2fa5..299dbfad1d8 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils.ts @@ -11,6 +11,8 @@ import { uuidv4 } from 'lib/helpers' import { ColumnField, CreateColumnPayload, UpdateColumnPayload } from '../SidePanelEditor.types' const isSQLExpression = (input: string) => { + if (['CURRENT_DATE'].includes(input)) return true + if (input[0] === '(' && input[input.length - 1] === ')') { return true } @@ -60,7 +62,7 @@ export const generateColumnFieldFromPostgresColumn = ( name: column.name, comment: column?.comment ?? '', format: isArray ? column.format.slice(1) : column.format, - defaultValue: unescapeLiteral(column?.default_value as string), + defaultValue: column?.default_value as string | null, isArray: isArray, isNullable: column.is_nullable, isIdentity: column.is_identity, @@ -125,7 +127,7 @@ export const generateUpdateColumnPayload = ( if (!isEqual(originalColumn.format, type)) { payload.type = type } - if (!isEqual(unescapeLiteral(originalColumn.default_value as string), defaultValue)) { + if (!isEqual(originalColumn.default_value as string, defaultValue)) { payload.defaultValue = defaultValue payload.defaultValueFormat = isNull(defaultValue) || isSQLExpression(defaultValue) @@ -203,64 +205,3 @@ const formatArrayToPostgresArray = (arrayString: string) => { if (!arrayString) return null return arrayString.replaceAll('[', '{').replaceAll(']', '}') } - -export const unescapeLiteral = (value: string) => { - if (!value) return value - - // Handle combinations - this is really not the best solution imo - // It's a log(n) attempt to strip off the ::type strings - if (value.includes('||')) { - const formattedValue = [] - let found = false - for (let i = 0; i < value.length; i++) { - if (value[i] === ':') found = true - if (((value[i] === ' ' || value[i] === ')') && found) || i === value.length - 1) found = false - if (!found) formattedValue.push(value[i]) - } - return formattedValue.join('') - } - - const splits = value.split("'::") - if (splits.length <= 1) return value - - // Handle timezones - if (value.toLowerCase().includes('time zone')) { - return `${splits[0].toLowerCase()}')` - } - - // Handle nextval - if (value.toLowerCase().includes('nextval')) { - return `${splits[0]}')` - } - - // Handle json - if (value.toLowerCase().includes('json')) { - return splits[0].slice(1) - } - - let temp = splits[0].slice(1).replace("'{", '{') - if ( - value.endsWith('integer[]') || - value.endsWith('real[]') || - value.endsWith('bigint[]') || - value.endsWith('smallint[]') - ) { - temp = temp.replace(/{/g, '[') - temp = temp.replace(/}/g, ']') - return temp - } else { - const matches = temp.match(/\{([^{}]+)\}/g) - if (matches) { - const array = [...matches] - array.forEach((x) => { - let _x = x.replace(/{/g, '{"') - _x = _x.replace(/}/g, '"}') - _x = _x.replace(/,/g, '","') - temp = temp.replace(x, _x) - }) - temp = temp.replace(/{/g, '[') - temp = temp.replace(/}/g, ']') - } - return temp - } -} diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx index 36f22b3553a..f7e6a73dfc2 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnType.tsx @@ -1,4 +1,4 @@ -import React, { FC } from 'react' +import React, { FC, ReactNode } from 'react' import { IconCalendar, IconType, IconHash, Listbox, IconToggleRight, Input } from '@supabase/ui' import { PostgresType } from '@supabase/postgres-meta' import { POSTGRES_DATA_TYPES, POSTGRES_DATA_TYPE_OPTIONS } from '../SidePanelEditor.constants' @@ -12,6 +12,7 @@ interface Props { error?: any disabled?: boolean showLabel?: boolean + description?: ReactNode onOptionSelect: (value: string) => void } @@ -23,6 +24,7 @@ const ColumnType: FC = ({ error, disabled = false, showLabel = true, + description, onOptionSelect = () => {}, }) => { // @ts-ignore @@ -68,73 +70,75 @@ const ColumnType: FC = ({ } return ( - onOptionSelect(value)} - optionsWidth={480} - > - - --- - + onOptionSelect(value)} + optionsWidth={480} + > + + --- + - {/* + {/* Weird issue with Listbox here 1. Can't do render conditionally (&&) within Listbox hence why using Fragment 2. Can't wrap these 2 components within a Fragment conditional (enumTypes.length) as selecting the enumType option will not render it in the Listbox component */} - {enumTypes.length > 0 ? ( - - User-defined Enumerated Types - - ) : ( - <> - )} - - {enumTypes.length > 0 ? ( - // @ts-ignore - enumTypes.map((enumType: PostgresType) => ( - { - return
- }} - > -
-

{enumType.name}

-
- - )) - ) : ( - <> - )} - - - PostgreSQL Data Types + {enumTypes.length > 0 ? ( + + User-defined Enumerated Types + ) : ( + <> + )} - {POSTGRES_DATA_TYPE_OPTIONS.map((option: PostgresDataTypeOption) => ( + {enumTypes.length > 0 ? ( + // @ts-ignore + enumTypes.map((enumType: PostgresType) => ( inferIcon(option.type)} + key={enumType.name} + value={enumType.name} + label={enumType.name} + addOnBefore={() => { + return
+ }} >
- {option.name} - {option.description} +

{enumType.name}

- ))} - + )) + ) : ( + <> + )} + + + PostgreSQL Data Types + + + {POSTGRES_DATA_TYPE_OPTIONS.map((option: PostgresDataTypeOption) => ( + inferIcon(option.type)} + > +
+ {option.name} + {option.description} +
+
+ ))} + ) } diff --git a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx index b73579ecfb0..f294211cfeb 100644 --- a/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx +++ b/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx @@ -6,11 +6,11 @@ // the component over to the UI library import { FC, useEffect, useRef, useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' import { Button, Dropdown, IconList, Input } from '@supabase/ui' import { Suggestion } from './ColumnEditor.types' const MAX_SUGGESTIONS = 3 -const DEFAULT_SUGGESTIONS_WIDTH = 400 interface Props { label?: string @@ -22,9 +22,9 @@ interface Props { className?: string value: string format?: string - suggestionsHeader?: string - suggestionsWidth?: number suggestions: Suggestion[] + suggestionsTooltip?: string + suggestionsHeader?: string onChange: (event: any) => void onSelectSuggestion: (suggestion: Suggestion) => void } @@ -40,6 +40,8 @@ const InputWithSuggestions: FC = ({ value = '', format, suggestions = [], + suggestionsTooltip, + suggestionsHeader, onChange = () => {}, onSelectSuggestion = () => {}, }) => { @@ -75,7 +77,7 @@ const InputWithSuggestions: FC = ({ size={size} layout={layout} disabled={disabled} - className={`${className} ${format?.includes('json') ? 'input-mono' : ''}`} + className={className} type="text" value={value} onChange={onInputChange} @@ -87,7 +89,7 @@ const InputWithSuggestions: FC = ({ side="bottom" overlay={ <> - Suggestions + {suggestionsHeader || 'Suggestions'} {filteredSuggestions.map((suggestion: Suggestion) => ( = ({ } > - + + +