Merge pull request #9316 from supabase/master

Prod deploy Studio
This commit is contained in:
Joshen Lim authored and GitHub committed 2022-10-04 13:04:25 +08:00
commit 60b095a08e
31 files changed
+595 -350

No files matched your search

@@ -107,6 +107,10 @@ http://localhost:54321/auth/v1/ # Auth (GoTrue)
</TabItem>
</Tabs>
:::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.
@@ -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.
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/ZQ8TG-A-CIw?rel=0"
frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; modestbranding; showinfo=0"
allowfullscreen
width="100%"
height="400px"
></iframe>
</div>
## 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/).
+1
View File
@@ -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',
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

+243
View File
@@ -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.
<div className="bg-gray-300 rounded-lg p-6 italic">
🐈 Who's Snaplet?
<br />
Snaplet gives developers production-accurate data and preview databases that they can code against, so they can focus on shipping.
<br />
Checkout out how you can <a href="https://docs.snaplet.dev/tutorials/supabase-clone-environments">use Snaplet to clone Supabase environments</a>.
</div>
## 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](<https://en.wikipedia.org/wiki/9P_(protocol)>) 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:<PORT>`
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)
+1 -1
View File
@@ -42,7 +42,7 @@ const data: CommunityItem[] = [
},
{
title: 'Twitter',
stat: '40,000+',
stat: '44,000+',
statLabel: 'Followers',
img: 'twitter.png',
detail: () => (
+8
View File
@@ -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"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+8 -1
View File
@@ -5,9 +5,16 @@
<link>https://supabase.com</link>
<description>Latest news from Supabase</description>
<language>en</language>
<lastBuildDate>Wed, 07 Sep 2022 16:04:00 +0000</lastBuildDate>
<lastBuildDate>Sun, 02 Oct 2022 16:04:00 +0000</lastBuildDate>
<atom:link href="https://supabase.com/rss.xml" rel="self" type="application/rss+xml"/>
<item>
<guid>https://supabase.com/blog/postgres-wasm</guid>
<title>Postgres WASM by Snaplet and Supabase</title>
<link>https://supabase.com/blog/postgres-wasm</link>
<description>We're open sourcing postgres-wasm, a PostgresQL server that runs inside a browser, with our friends at Snaplet.</description>
<pubDate>Sun, 02 Oct 2022 16:04:00 +0000</pubDate>
</item>
<item>
<guid>https://supabase.com/blog/choosing-a-postgres-primary-key</guid>
<title>Choosing a Postgres Primary Key</title>
<link>https://supabase.com/blog/choosing-a-postgres-primary-key</link>
+1
View File
@@ -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',
+18 -4
View File
@@ -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'
@@ -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',
},
{
@@ -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<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
const [isSaving, setIsSaving] = useState(false)
const [errors, setErrors] = useState<string[]>([])
const [taxIdValues, setTaxIdValues] = useState(taxIds)
const formattedTaxIds = taxIds.map((taxId: any) => {
const [taxIdValues, setTaxIdValues] = useState<StripeTaxId[]>(taxIds)
const formattedTaxIds: StripeTaxId[] = taxIds.map((taxId: StripeTaxId) => {
return {
id: taxId.id,
type: taxId.type,
@@ -78,7 +79,7 @@ const TaxID: FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
<div>
<h4>Tax ID</h4>
<p className="text-sm opacity-50">
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. <br />
Make sure the tax ID looks exactly like the placeholder text.
</p>
</div>
@@ -150,7 +152,7 @@ const TaxID: FC<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
loading={loading}
footer={
!loading && (
<div className="flex justify-between w-full">
<div className="flex w-full justify-between">
{!canUpdateTaxIds ? (
<p className="text-sm text-scale-1000">
You need additional permissions to update this organization's tax IDs
@@ -183,15 +185,15 @@ const TaxID: FC<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
>
{loading && taxIdValues.length === 0 ? (
<div className="flex flex-col justify-between space-y-2 py-4 px-4">
<div className="shimmering-loader rounded py-3 mx-1 w-2/3" />
<div className="shimmering-loader rounded py-3 mx-1 w-1/2" />
<div className="shimmering-loader rounded py-3 mx-1 w-1/3" />
<div className="shimmering-loader mx-1 w-2/3 rounded py-3" />
<div className="shimmering-loader mx-1 w-1/2 rounded py-3" />
<div className="shimmering-loader mx-1 w-1/3 rounded py-3" />
</div>
) : (
<Panel.Content className="w-3/5 space-y-4">
<Panel.Content className="w-8/12 space-y-4">
{taxIdValues.length >= 1 ? (
<div className="w-full space-y-2">
{taxIdValues.map((taxId: any, idx: number) => {
{taxIdValues.map((taxId: StripeTaxId, idx: number) => {
const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name)
return (
<div key={`tax-id-${idx}`} className="flex items-center space-x-2">
@@ -200,11 +202,13 @@ const TaxID: FC<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
onChange={(e: any) => onUpdateTaxId(taxId.id, 'name', e.target.value)}
disabled={!canUpdateTaxIds}
>
{TAX_IDS.map((option) => (
<Select.Option key={option.name} value={option.name}>
{option.name}
</Select.Option>
))}
{TAX_IDS.sort((a, b) => a.country.localeCompare(b.country)).map(
(option) => (
<Select.Option key={option.name} value={option.name}>
{option.country} - {option.name}
</Select.Option>
)
)}
</Select>
<Input
value={taxId.value}
@@ -230,7 +234,7 @@ const TaxID: FC<Props> = ({ loading, taxIds, onTaxIdsUpdated }) => {
)}
{canUpdateTaxIds && (
<div
className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition cursor-pointer"
className="flex cursor-pointer items-center space-x-2 opacity-50 transition hover:opacity-100"
onClick={() => onAddNewTaxId()}
>
<IconPlus size={14} />
@@ -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
}
@@ -60,8 +60,9 @@ const ColumnDefaultValue: FC<Props> = ({
}
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 })
@@ -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<Props> = ({
updateEditorDirty = () => {},
}) => {
const isNewRecord = isUndefined(column)
const hasPrimaryKey = (selectedTable?.primary_keys ?? []).length > 0
const originalForeignKey = column ? getColumnForeignKey(column, selectedTable) : undefined
const [errors, setErrors] = useState<Dictionary<any>>({})
@@ -210,21 +210,24 @@ const ColumnEditor: FC<Props> = ({
enumTypes={enumTypes}
error={errors.format}
disabled={!isUndefined(columnFields?.foreignKey)}
description={
<Link href="https://supabase.com/docs/guides/database/tables#data-types">
<a>
<Button
as="span"
type="text"
size="small"
className="text-scale-1000 hover:text-scale-1200"
icon={<IconExternalLink size={14} strokeWidth={2} />}
>
Learn more about data types
</Button>
</a>
</Link>
}
onOptionSelect={(format: string) => onUpdateField({ format, defaultValue: null })}
/>
<div className="text-sm grid md:grid-cols-12 md:gap-x-4 ">
<p className="text-scale-1100 col-span-7 col-start-5 -mt-4">
<a
href="https://supabase.com/docs/guides/database/tables#data-types"
target="_blank"
className="underline inline-block gap-1 p-2 rounded-sm"
>
Learn more about data types
</a>
</p>
</div>
{isUndefined(columnFields.foreignKey) && (
<div className="grid grid-cols-12 gap-4">
{columnFields.format.includes('int') && (
@@ -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
}
}
@@ -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<Props> = ({
error,
disabled = false,
showLabel = true,
description,
onOptionSelect = () => {},
}) => {
// @ts-ignore
@@ -68,73 +70,75 @@ const ColumnType: FC<Props> = ({
}
return (
<Listbox
label={showLabel ? 'Type' : ''}
layout={showLabel ? 'horizontal' : 'vertical'}
value={value}
size={size}
error={error}
disabled={disabled}
className={`${className} ${disabled ? 'column-type-disabled' : ''} rounded-md`}
onChange={(value: string) => onOptionSelect(value)}
optionsWidth={480}
>
<Listbox.Option key="empty" value="" label="---">
---
</Listbox.Option>
<Listbox
label={showLabel ? 'Type' : ''}
layout={showLabel ? 'horizontal' : 'vertical'}
value={value}
size={size}
error={error}
disabled={disabled}
// @ts-ignore
descriptionText={description}
className={`${className} ${disabled ? 'column-type-disabled' : ''} rounded-md`}
onChange={(value: string) => onOptionSelect(value)}
optionsWidth={480}
>
<Listbox.Option key="empty" value="" label="---">
---
</Listbox.Option>
{/*
{/*
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 ? (
<Listbox.Option disabled key="header-1" value="header-1" label="header-1">
User-defined Enumerated Types
</Listbox.Option>
) : (
<></>
)}
{enumTypes.length > 0 ? (
// @ts-ignore
enumTypes.map((enumType: PostgresType) => (
<Listbox.Option
key={enumType.name}
value={enumType.name}
label={enumType.name}
addOnBefore={() => {
return <div className="bg-scale-1200 mx-1 h-2 w-2 rounded-full" />
}}
>
<div className="flex items-center space-x-4">
<p>{enumType.name}</p>
</div>
</Listbox.Option>
))
) : (
<></>
)}
<Listbox.Option disabled value="header-2" label="header-2">
PostgreSQL Data Types
{enumTypes.length > 0 ? (
<Listbox.Option disabled key="header-1" value="header-1" label="header-1">
User-defined Enumerated Types
</Listbox.Option>
) : (
<></>
)}
{POSTGRES_DATA_TYPE_OPTIONS.map((option: PostgresDataTypeOption) => (
{enumTypes.length > 0 ? (
// @ts-ignore
enumTypes.map((enumType: PostgresType) => (
<Listbox.Option
key={option.name}
value={option.name}
label={option.name}
addOnBefore={() => inferIcon(option.type)}
key={enumType.name}
value={enumType.name}
label={enumType.name}
addOnBefore={() => {
return <div className="bg-scale-1200 mx-1 h-2 w-2 rounded-full" />
}}
>
<div className="flex items-center space-x-4">
<span className="text-scale-1200">{option.name}</span>
<span className="text-scale-900">{option.description}</span>
<p>{enumType.name}</p>
</div>
</Listbox.Option>
))}
</Listbox>
))
) : (
<></>
)}
<Listbox.Option disabled value="header-2" label="header-2">
PostgreSQL Data Types
</Listbox.Option>
{POSTGRES_DATA_TYPE_OPTIONS.map((option: PostgresDataTypeOption) => (
<Listbox.Option
key={option.name}
value={option.name}
label={option.name}
addOnBefore={() => inferIcon(option.type)}
>
<div className="flex items-center space-x-4">
<span className="text-scale-1200">{option.name}</span>
<span className="text-scale-900">{option.description}</span>
</div>
</Listbox.Option>
))}
</Listbox>
)
}
@@ -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<Props> = ({
value = '',
format,
suggestions = [],
suggestionsTooltip,
suggestionsHeader,
onChange = () => {},
onSelectSuggestion = () => {},
}) => {
@@ -75,7 +77,7 @@ const InputWithSuggestions: FC<Props> = ({
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<Props> = ({
side="bottom"
overlay={
<>
<Dropdown.Label>Suggestions</Dropdown.Label>
<Dropdown.Label>{suggestionsHeader || 'Suggestions'}</Dropdown.Label>
<Dropdown.Seperator />
{filteredSuggestions.map((suggestion: Suggestion) => (
<Dropdown.Item
@@ -101,7 +103,29 @@ const InputWithSuggestions: FC<Props> = ({
</>
}
>
<Button as="span" type="default" icon={<IconList strokeWidth={1.5} />}></Button>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<Button
as="span"
type="default"
className="!px-1 mr-1"
icon={<IconList strokeWidth={1.5} />}
/>
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'bg-scale-100 rounded py-1 px-2 leading-none shadow',
'border-scale-200 border',
].join(' ')}
>
<span className="text-scale-1200 text-xs">
{suggestionsTooltip || 'Suggestions'}
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
</Dropdown>
)
}
@@ -100,9 +100,11 @@ const InputField: FC<Props> = ({
rows={5}
value={field.value ?? ''}
placeholder={
typeof field.defaultValue === 'string' && field.defaultValue.length === 0
field.defaultValue === null
? ''
: typeof field.defaultValue === 'string' && field.defaultValue.length === 0
? 'Default: Empty string'
: field.defaultValue
: `Default: ${field.defaultValue}`
}
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
/>
@@ -119,7 +121,7 @@ const InputField: FC<Props> = ({
descriptionText={field.comment}
labelOptional={field.format}
disabled={!isEditable}
placeholder={field.defaultValue}
placeholder={field?.defaultValue ?? ''}
error={errors[field.name]}
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
actions={
@@ -156,7 +158,13 @@ const InputField: FC<Props> = ({
labelOptional={field.format}
error={errors[field.name]}
value={field.value ?? ''}
placeholder={field.isIdentity ? 'Automatically generated as identity' : field.defaultValue}
placeholder={
field.isIdentity
? 'Automatically generated as identity'
: field.defaultValue !== null
? `Default: ${field.defaultValue}`
: ''
}
disabled={!isEditable}
onChange={(event: any) => onUpdateField({ [field.name]: event.target.value })}
/>
@@ -18,7 +18,7 @@ export interface RowField {
format: string
enums: string[]
value: string | null
defaultValue: string
defaultValue: string | null
foreignKey?: PostgresRelationship
isNullable: boolean
isIdentity: boolean
@@ -5,13 +5,7 @@ import { PostgresTable } from '@supabase/postgres-meta'
import { uuidv4, minifyJSON, tryParseJson } from 'lib/helpers'
import { RowField } from './RowEditor.types'
import {
JSON_TYPES,
NUMERICAL_TYPES,
DATETIME_TYPES,
TIME_TYPES,
TIMESTAMP_TYPES,
} from '../SidePanelEditor.constants'
import { DATETIME_TYPES, TIME_TYPES, TIMESTAMP_TYPES } from '../SidePanelEditor.constants'
export const generateRowFields = (
row: Dictionary<any> | undefined,
@@ -24,14 +18,13 @@ export const generateRowFields = (
return table.columns.map((column) => {
const defaultValue = column?.default_value as string | null
const value =
isNewRecord && defaultValue?.includes('now()')
? nowDateTimeValue(column.format)
: isUndefined(row)
? ''
: DATETIME_TYPES.includes(column.format)
? convertPostgresDatetimeToInputDatetime(column.format, row[column.name])
: parseValue(row[column.name], column.format, column.data_type)
const value = isUndefined(row)
? ''
: isNewRecord && defaultValue?.includes('now()')
? nowDateTimeValue(column.format)
: DATETIME_TYPES.includes(column.format)
? convertPostgresDatetimeToInputDatetime(column.format, row[column.name])
: parseValue(row[column.name], column.format, column.data_type)
const foreignKey = find(relationships, (relationship) => {
return (
@@ -49,7 +42,7 @@ export const generateRowFields = (
comment: parseDescription(column.comment),
format: column.format,
enums: column.enums as any,
defaultValue: parseValue(column.default_value as string, column.format, column.data_type),
defaultValue: column?.default_value as string | null,
isNullable: column.is_nullable,
isIdentity: column.is_identity,
isPrimaryKey: primaryKeyColumns.includes(column.name),
@@ -109,26 +102,6 @@ const parseValue = (originalValue: string, format: string, dataType: string) =>
return JSON.stringify(originalValue)
} else if (typeof originalValue === 'boolean') {
return (originalValue as any).toString()
} else if (includes(JSON_TYPES, format)) {
const value = _unescapeLiteral(dataType, originalValue, format)
return minifyJSON(value)
}
// escape literal format from postgres-meta
const value = _unescapeLiteral(dataType, originalValue, format)
if (dataType && dataType.toLowerCase() == 'array') {
if (originalValue && originalValue.includes("}'::") && originalValue.endsWith('[]')) {
// for array default value, we need to use this method to parse literal format
// TODO: should merge with above method... if we can
return _unescapeLiteralArray(originalValue)
} else if (typeof value === 'string') {
const parsedValue = JSON.parse(value)
return JSON.parse(parsedValue)
} else {
return JSON.stringify(value)
}
} else {
return value
}
} catch (error) {
return originalValue
@@ -201,68 +174,6 @@ const convertInputDatetimeToPostgresDatetime = (format: string, value: string |
}
}
/**
* postgres-meta can return default value with format like
* 'hello world'::character varying
* '232.34'::double precision
* 'USER'::"Role" user-defined type
* this method will help convert them to valid default value
*/
const _unescapeLiteral = (dataType: string, value: string, format: string): any => {
// unEscape format literal
let temp = `::${dataType}`
let tempWithQuotes = `::"${dataType}"`
// for user-defined type, need to use format instead
if (dataType?.toLowerCase() == 'user-defined') {
temp = `::${format}`
tempWithQuotes = `::"${format}"`
}
if (value && value.includes(temp)) {
value = value.replace(temp, '')
// remove quotes
value = value.slice(1, value.length - 1)
} else if (value && value.includes(tempWithQuotes)) {
value = value.replace(tempWithQuotes, '')
// remove quotes
value = value.slice(1, value.length - 1)
}
return value
}
/**
* postgres-meta can return default value for array like
* ex: '{apple,banana}'::text[] => ["apple","banana"]
* ex: '{1,2,3,4,5,6}'::integer[] => [1,2,3,4,5,6]
* ex: '{{meeting,lunch},{training,presentation}}'::character varying[]
* this method will help convert them to valid default value
*/
const _unescapeLiteralArray = (value: any) => {
const splits = value.split("'::")
if (splits.length < 1) return value
let temp = splits[0].replace("'{", '{')
if (value.endsWith('integer[]') || value.endsWith('real[]')) {
temp = temp.replaceAll('{', '[')
temp = temp.replaceAll('}', ']')
return temp
} else {
const matches = temp.match(/\{([^{}]+)\}/g)
if (matches) {
const array = [...matches]
array.forEach((x) => {
let _x = x.replaceAll('{', '{"')
_x = _x.replaceAll('}', '"}')
_x = _x.replaceAll(',', '","')
temp = temp.replace(x, _x)
})
temp = temp.replaceAll('{', '[')
temp = temp.replaceAll('}', ']')
}
return temp
}
}
export const generateRowObjectFromFields = (
fields: RowField[],
includeNullProperties = false
@@ -128,7 +128,7 @@ const Column: FC<Props> = ({
}`}
suggestions={suggestions}
suggestionsHeader="Suggested expressions"
suggestionsWidth={410}
suggestionsTooltip="Suggested expressions"
onChange={(event: any) => onUpdateColumn({ defaultValue: event.target.value })}
onSelectSuggestion={(suggestion: Suggestion) =>
onUpdateColumn({ defaultValue: suggestion.value })
@@ -0,0 +1,49 @@
import { sanitizeTaxID } from 'components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils'
/**
* We're sanitizing EU tax ids. Stripe expects a prefixed tax id (ATU12345678),
* but users might not realize this and enter only the numbers (12345678) without the
* country code prefix.
*
* Our sanitize function should handle 3 cases:
* 1. take an un-prefixed tax id (12345678) and add the country prefix to it (ATU12345678)
* 2. take a correctly prefixed tax id (ATU12345678) and just pass it through
* 3. take a non-EU tax id and just pass it through
*/
describe('TaxID utils: sanitizeTaxID', () => {
test('should prefix an EU tax ID correctly', () => {
const austriaTaxID = {
id: 'txi_1LoPlYJDPojXS6LNVd1FOTk2',
type: 'eu_vat',
value: '12345678',
name: 'AT VAT',
}
const sanitizedID = sanitizeTaxID(austriaTaxID)
expect(sanitizedID.value).toBe('ATU12345678')
})
test('should check that EU prefix is correct', () => {
const austriaTaxID = {
id: 'txi_1LoPlYJDPojXS6LNVd1FOTk2',
type: 'eu_vat',
value: 'ATU12345678',
name: 'AT VAT',
}
const sanitizedID = sanitizeTaxID(austriaTaxID)
expect(sanitizedID.value).toBe('ATU12345678')
})
test('should not prefix an non-EU tax ID', () => {
const unitedStatesID = {
id: 'txi_1LoPlYJDPojXS6LNVd1FOTk2',
type: 'us_ein',
value: '12-3456789',
name: 'US EIN',
}
const sanitizedID = sanitizeTaxID(unitedStatesID)
expect(sanitizedID.value).toBe('12-3456789')
})
})
@@ -1,72 +0,0 @@
import { unescapeLiteral } from 'components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
describe('ColumnEditor.utils: unescapeLiteral', () => {
test('should return varchar properly', () => {
const mockInput = `nextval('"Test_id_seq"'::regclass)`
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual(`nextval('"Test_id_seq"')`)
})
test('should return varchar properly', () => {
const mockInput = "'potato'::character varying"
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('potato')
})
test('should return text properly', () => {
const mockInput = "'tomato'::text"
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('tomato')
})
test('should return null properly', () => {
const mockInput = null
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual(null)
})
test('should return empty string properly', () => {
const mockInput = `''::text`
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('')
})
test('should return numerical arrays properly', () => {
const mockInput = "'{1,2,3,4}'::integer[]"
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('[1,2,3,4]')
})
test('should return string arrays properly', () => {
const mockInput = "'{apple,orange}'::text[]"
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('["apple","orange"]')
})
test('should return single word functions as they are', () => {
const mockInput = 'uuid_generate_v4()'
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual(mockInput)
})
test('should return functions beyond one word properly', () => {
const mockInput = "(now() AT TIME ZONE 'utc'::text)"
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual("(now() at time zone 'utc')")
})
test('should return expressions that combine values properly', () => {
const mockInput1 = "('hi_'::text || uuid_generate_v4())"
const value1 = unescapeLiteral(mockInput1)
expect(value1).toStrictEqual("('hi_' || uuid_generate_v4())")
const mockInput2 = "(('hi'::text || 'bye'::text) || 'hello'::text)"
const value2 = unescapeLiteral(mockInput2)
expect(value2).toStrictEqual("(('hi' || 'bye') || 'hello')")
const mockInput3 = "(uuid_generate_v4() || 'hello'::text)"
const value3 = unescapeLiteral(mockInput3)
expect(value3).toStrictEqual("(uuid_generate_v4() || 'hello')")
})
test('should return json object properly', () => {
const mockInput = `'{\"version\": 10, \"dimensions\": {\"width\": 50, \"height\": 20}}'::jsonb`
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('{"version": 10, "dimensions": {"width": 50, "height": 20}}')
})
test('should return json array properly', () => {
const mockInput = `'[{\"version\": 10}, {\"version\": 11}]'::jsonb`
const value = unescapeLiteral(mockInput)
expect(value).toStrictEqual('[{"version": 10}, {"version": 11}]')
})
})