diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..b5924c32836 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules/ +.vercel +.next +.env.local +.env.production +.env.dev +.env.* \ No newline at end of file diff --git a/README.md b/README.md index 446e3316709..e8a1708cb3e 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i - [Portuguese (Brazilian) / Português Brasileiro](/i18n/README.pt-br.md) - [Romanian / Română](/i18n/README.ro.md) - [Russian / Pусский](/i18n/README.ru.md) +- [Serbian / Srpski](/i18n/README.sr.md) - [Sinhala / සිංහල](/i18n/README.si.md) - [Spanish / Español](/i18n/README.es.md) - [Simplified Chinese / 简体中文](/i18n/README.zh-cn.md) diff --git a/apps/reference/docs/guides/api/generating-types.mdx b/apps/reference/docs/guides/api/generating-types.mdx index 21ce6c15340..7c1ccdb41f8 100644 --- a/apps/reference/docs/guides/api/generating-types.mdx +++ b/apps/reference/docs/guides/api/generating-types.mdx @@ -26,7 +26,7 @@ Important notes: - Since the generator uses JSON API, there is no way to determine if a column is an Array. It will generate array types as `string`, even though Supabase handles this automatically and returns arrays. You can fix this manually in the files by changing the type, e.g. `names: string` -> `names: string[]` -- The types won't automatically stay in sync with your database, so make sure to regenerate your types after your make changes to your database. +- The types won't automatically stay in sync with your database, so make sure to regenerate your types after you make changes to your database. After you have generated your types, you can use them in your TypeScript projects: diff --git a/apps/reference/docs/guides/auth/auth-azure.mdx b/apps/reference/docs/guides/auth/auth-azure.mdx index 4123c627219..0f7984fdaa5 100644 --- a/apps/reference/docs/guides/auth/auth-azure.mdx +++ b/apps/reference/docs/guides/auth/auth-azure.mdx @@ -16,7 +16,7 @@ Azure OAuth consists of four broad steps: - Create an application under Azure Active Directory. - Obtain a `Application (client) ID` with “Sign In with Azure” capabilities. This will be used as the `client id`. - Create a `Secret ID` with “Sign In with Azure” capabilities. The value of the secret will be used as the `client secret`. -- Whitelist the callback url of your application. +- Add the callback url of your application to the allowlist. ## Steps diff --git a/apps/reference/docs/guides/auth/auth-captcha.mdx b/apps/reference/docs/guides/auth/auth-captcha.mdx new file mode 100644 index 00000000000..65837976a17 --- /dev/null +++ b/apps/reference/docs/guides/auth/auth-captcha.mdx @@ -0,0 +1,113 @@ +--- +id: auth-captcha +title: 'Enable Captcha Protection' +description: Add Captcha Protection to your Supabase project +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# hCaptcha guide + +Supabase provides you with the option of adding captcha to your sign-in, sign-up, and password reset forms. This keeps your website safe from bots and malicious scripts. Supabase authentication has support for [hCaptcha](https://www.hcaptcha.com/). + +### Sign up for hCaptcha + +Go to the [hCaptcha](https://www.hcaptcha.com/) website and sign up for an account. On the welcome page, copy the **Sitekey** and **Secret key**. + +If you have already signed up and didn't copy this information from the welcome page, you can get the **Secret key** from the settings page. + +![site_secret_settings.png](/img/guides/auth-captcha/site_secret_settings.png) + +The **Sitekey** can be found in the **Settings** of the active site you created. + +![sites_dashboard.png](/img/guides/auth-captcha/sites_dashboard.png) + +In the Settings page, look for the **Sitekey** section and copy the key. + +![sitekey_settings.png](/img/guides/auth-captcha/sitekey_settings.png) + +### Enable hCaptcha protection for your Supabase project + +Navigate to the **[Authentication](https://app.supabase.com/project/_/auth/settings)** page in the Supabase Dashboard and find the **Enable hCaptcha protection** toggle under the **Security and Protection** section. + +![supabase_auth_general_settings.png](/img/guides/auth-captcha/supabase_auth_general_settings.png) + +Enter your hCaptcha **Secret key** and click **Save**. + +### Add the hCaptcha frontend component + +The frontend requires some changes to provide the captcha on-screen for the user. This example uses React and the hCaptcha React component, but hCaptcha can be used with any JavaScript framework. + +Install `@hcaptcha/react-hcaptcha` in your project as a dependency. + +```bash +npm install @hcaptcha/react-hcaptcha +``` + +Now import the `HCaptcha` component from the `@hcaptcha/react-hcaptcha` library. + +```javascript +import HCaptcha from '@hcaptcha/react-hcaptcha' +``` + +Let’s create a empty state to store our `captchaToken` + +```jsx +const [captchaToken, setCaptchaToken] = useState() +``` + +Now lets add the HCaptcha component to the JSX section of our code + +```html + +``` + +We will pass it the sitekey we copied from the hCaptcha website as a property along with a onVerify property which takes a callback function. This callback function will have a token as one of its properties. Let's set the token in the state using `setCaptchaToken` + +```jsx + { setCaptchaToken(token) } +/> +``` + +Now lets use the captcha token we receive in our Supabase signUp function. + +```jsx +await supabase.auth.signUp({ + email, + password, + options: { captchaToken }, +}) +``` + +We will also need to reset the captcha challenge after we have made a call to the function above. + +Create a ref to use on our HCaptcha component. + +```jsx +const captcha = useRef() +``` + +Let's add a ref attribute on the `HCaptcha` component and assign the `captcha` constant to it. + +```jsx + { + setCaptchaToken(token) + }} +/> +``` + +Reset the `captcha` after the signUp function is called using the following code: + +```jsx +captcha.current.resetCaptcha() +``` + +In order to test that this works locally we will need to use something like [ngrok](https://ngrok.com/) or add an entry to your hosts file. You can read more about this in the [hCaptcha docs](https://docs.hcaptcha.com/#local-development). + +Run the application and you should now be provided with a captcha challenge. diff --git a/apps/reference/docs/guides/auth/auth-helpers/sveltekit.mdx b/apps/reference/docs/guides/auth/auth-helpers/sveltekit.mdx index 849c9c5a771..4d7d1f3d317 100644 --- a/apps/reference/docs/guides/auth/auth-helpers/sveltekit.mdx +++ b/apps/reference/docs/guides/auth/auth-helpers/sveltekit.mdx @@ -5,31 +5,39 @@ description: Convenience helpers for implementing user authentication in SvelteK sidebar_label: SvelteKit --- +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + This submodule provides convenience helpers for implementing user authentication in [SvelteKit](https://kit.svelte.dev/) applications. ## Installation -Using [npm](https://npmjs.org): +This library supports Node.js `^16.15.0`. + + + + ```sh npm install @supabase/auth-helpers-sveltekit - -# Main component for Svelte based frameworks (optional but recommended) -npm install @supabase/auth-helpers-svelte ``` -Using [yarn](https://yarnpkg.com/): + + + ```sh yarn add @supabase/auth-helpers-sveltekit - -# Main component for Svelte based frameworks (optional but recommended) -yarn add @supabase/auth-helpers-svelte ``` -This library supports the following tooling versions: - -- Node.js: `^16.15.0` + + ## Getting Started @@ -39,34 +47,488 @@ Set up the fillowing env vars. For local development you can set them in a `.env ```bash # Find these in your Supabase project settings > API -VITE_SUPABASE_URL=https://your-project.supabase.co -VITE_SUPABASE_ANON_KEY=your-anon-key +PUBLIC_SUPABASE_URL=https://your-project.supabase.co +PUBLIC_SUPABASE_ANON_KEY=your-anon-key ``` -### SupabaseClient and SupaAuthHelper component setup +### Set up the Supabase client -We will start off by creating a `db.ts` file inside of our `src/lib` directory. Now lets instantiate our `supabaseClient` by using our `createSupabaseClient` function from the `@supabase/auth-helpers-sveltekit` library. +Start off by creating a `db.ts` file inside of the `src/lib` directory and instantiate the `supabaseClient`. + +```ts title="src/lib/db.ts" +import { createClient } from '@supabase/supabase-js'; +import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'; +import { dev } from '$app/environment'; +import { env } from '$env/dynamic/public'; +// or use the static env +// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; + +export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, { + persistSession: false, + autoRefreshToken: false +}); + +setupSupabaseHelpers({ + supabaseClient, + cookieOptions: { + secure: !dev + } +}); +``` + +### Initialize the client + +Edit your `+layout.svelte` file and set up the client-side. + +```html title="src/routes/+layout.svelte" + + + +``` + +### Set up hooks + +The `hooks.ts` file is where the heavy lifting of this library happens: + +```ts title="src/hooks.server.ts" +// make sure the supabase instance is initialized on the server +import '$lib/db'; +import { dev } from '$app/environment'; +import { auth } from '@supabase/auth-helpers-sveltekit/server'; + +export const handle = auth(); + +// use the sequence helper if you have additional Handle methods +import { sequence } from '@sveltejs/kit/hooks'; + +export const handle = sequence(auth(), yourHandler); +``` + +There are three handle methods available: + +- `callback()`: + This creates a handler for `/api/auth/callback`. The `client` forwards the session details here every time `onAuthStateChange` fires on the client-side. This is needed to set up the cookies for your application so that SSR works seamlessly. +- `session()`: + This parses the session from the cookie and populate it in locals +- `auth()`: + a shorthand for `sequence(callback(), session())` that uses both handlers + +### Send session to client + +In order to make the session available to the UI (e.g., pages and layouts), pass the session in the root layout load function: + +```ts title="src/routes/+layout.server.ts" +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + return { + session: locals.session + }; +}; +``` + +### Typings + +In order to get the most out of TypeScript and IntelliSense, import the types into the `app.d.ts` type definition file that comes with your SvelteKit project. + +```ts title="src/app.d.ts" +/// + +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + interface Locals { + session: import('@supabase/auth-helpers-sveltekit').SupabaseSession; + } + interface PageData { + session: import('@supabase/auth-helpers-sveltekit').SupabaseSession; + } + // interface Error {} + // interface Platform {} +} +``` + +### Signing out + +This library has provided a method to delete the session cookie. + +Create a server only page with an action to delete the cookie + +```ts title="src/routes/logout/+page.server.ts" +import { deleteSession } from '@supabase/auth-helpers-sveltekit/server'; +import { redirect } from '@sveltejs/kit'; +import type { Actions } from './$types'; + +export const actions: Actions = { + async default({ cookies }) { + deleteSession(cookies); + throw redirect(303, '/'); + } +}; +``` +Then make a **POST** request with a form to the server only page above. + +```html + + +
+ +
+``` + +### Basic setup + +You can now determine if a user is authenticated on the client-side by checking that the `user` object in `$page.data.session` is defined. + +```html title="src/routes/+page.svelte" + + +{#if !$page.data.session.user} +

I am not logged in

+{:else} +

Welcome {$page.data.session.user.email}

+

I am logged in!

+{/if} +``` + +## Client-side data fetching with RLS + +For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to import the `{ supabaseClient }` from `$lib/db` and only run your query once the user is defined client-side in `$page.data.session`: + +```html + + +{#if $page.data.session.user} +

client-side data fetching with RLS

+
{JSON.stringify(loadedData, null, 2)}
+{/if} +``` + +## Server-side data fetching with RLS + +```html title="src/routes/profile/+page.svelte" + + +
Protected content for {user.email}
+
{JSON.stringify(tableData, null, 2)}
+
{JSON.stringify(user, null, 2)}
+``` + +For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to use the `withAuth` helper to check if the user is authenticated. The helper extends the event with `session` and `getSupabaseClient()`: + +```ts title="src/routes/profile/+page.ts" +import type { PageLoad } from './$types'; +import { withAuth } from '@supabase/auth-helpers-sveltekit'; +import { redirect } from '@sveltejs/kit'; + +interface TestTable { + id: string; + created_at: string; +} + +export const load: PageLoad = withAuth(async ({ getSupabaseClient, session }) => { + if (!session.user) { + throw redirect(303, '/'); + } + const { data: tableData } = await getSupabaseClient() + .from('test') + .select('*'); + + return { + user: session.user, + tableData + }; +); +``` + +**Caution:** + +Always use the instance returned by `getSupabaseClient()` directly! ```ts -// src/lib/db.ts -import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit' +// Bad +const supabaseClient = getSupabaseClient(); + +await supabaseClient.from('table1').select(); +await supabaseClient.from('table2').select(); + +// Good +await getSupabaseClient().from('table1').select(); +await getSupabaseClient().from('table2').select(); +``` + +## Protecting API routes + +Wrap an API Route to check that the user has a valid session. If they're not logged in the session is `null`. + +```ts title="src/routes/api/protected-route/+server.ts" +import type { RequestHandler } from './$types'; +import { withAuth } from '@supabase/auth-helpers-sveltekit'; +import { json, redirect } from '@sveltejs/kit'; + +interface TestTable { + id: string; + created_at: string; +} + +export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => { + if (!session.user) { + throw redirect(303, '/'); + } + const { data } = await getSupabaseClient() + .from('test') + .select('*'); + + return json({ data }); +); +``` + +If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response. + +## Protecting Actions + +Wrap an Action to check that the user has a valid session. If they're not logged in, the session is `null`. + +```ts title="src/routes/posts/+page.server.ts" +import type { Actions } from './$types'; +import { withAuth } from '@supabase/auth-helpers-sveltekit'; +import { error, invalid } from '@sveltejs/kit'; + +export const actions: Actions = { + createPost: withAuth(async ({ session, getSupabaseClient, request }) => { + if (!session.user) { + // the user is not signed in + throw error(403, { message: 'Unauthorized' }); + } + // we are save, let the user create the post + const formData = await request.formData(); + const content = formData.get('content'); + + const { error: createPostError, data: newPost } = await getSupabaseClient() + .from('posts') + .insert({ content }); + + if (createPostError) { + return invalid(500, { + supabaseErrorMessage: createPostError.message + }); + } + return { + newPost + }; + }) +}; +``` + +If you try to submit a form with the action `?/createPost` without a valid session cookie, you will get a 403 error response. + +## Saving and deleting the session + +Use `saveSession` to save the session cookies: + +```ts +import type { Actions } from './$types'; +import { supabaseClient } from '$lib/db'; +import { invalid, redirect } from '@sveltejs/kit'; +import { saveSession } from '@supabase/auth-helpers-sveltekit/server'; + +export const actions: Actions = { + async signin({ request, cookies, url }) { + const formData = await request.formData(); + + const email = formData.get('email') as string; + const password = formData.get('password') as string; + + const { data, error } = await supabaseClient.auth.api.signInWithEmail( + email, + password, + { + redirectTo: `${url.origin}/logging-in` + } + ); + + if (error || !data) { + if (error?.status === 400) { + return invalid(400, { + error: 'Invalid credentials', + values: { + email + } + }); + } + return invalid(500, { + error: 'Server error. Try again later.', + values: { + email + } + }); + } + + saveSession(cookies, data); + throw redirect(303, '/dashboard'); + } +}; +``` + +Use `deleteSession` to delete the session cookies: + +```ts +import type { Actions } from './$types'; +import { deleteSession } from '@supabase/auth-helpers-sveltekit/server'; +import { redirect } from '@sveltejs/kit'; + +export const actions: Actions = { + async logout({ cookies }) { + deleteSession(cookies); + throw redirect(303, '/'); + } +}; +``` + +## Custom session namespace + +If you want to use something other than `locals.session` and `$page.data.session`, update the types and create three helper functions: + +```ts title="src/app.d.ts" +declare namespace App { + interface Locals { + mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession; + } + interface PageData { + mySupabaseSession: import('@supabase/auth-helpers-sveltekit').SupabaseSession; + } +} +``` + +```ts title="src/hooks.server.ts" +setupSupabaseServer({ + supabaseClient, + cookieOptions: { + secure: !dev + }, + // --- change location within locals --- + getSessionFromLocals: (locals) => locals.mySupabaseSession, + setSessionToLocals: (locals, session) => (locals.mySupabaseSession = session) +}); +``` + +```ts title="src/lib/db.ts" +setupSupabaseClient({ + supabaseClient, + // --- change location within pageData --- + getSessionFromPageData: (data) => data.mySupabaseSession +}); +``` + +## Migrate from 0.6.11 and below to 0.7.0 {#migration} + +There are numerous breaking changes in the latest 0.7.0 version of this library. + +### Environment variable prefix + +The environment variable prefix is now `PUBLIC_` instead of `VITE_` (e.g., `VITE_SUPABASE_URL` is now `PUBLIC_SUPABASE_URL`). + +### Set up the Supabase client {#migration-set-up-supabase-client} + + + + + +```js title="src/lib/db.ts" +import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit'; const { supabaseClient } = createSupabaseClient( import.meta.env.VITE_SUPABASE_URL as string, import.meta.env.VITE_SUPABASE_ANON_KEY as string -) +); -export { supabaseClient } +export { supabaseClient }; ``` -Edit your `__layout.svelte` file and add import the `SupaAuthHelper` component, the `supabaseClient` we just instantiated and the `session` store. + -```html -// src/routes/__layout.svelte + + +```js title="src/lib/db.ts" +import { createClient } from '@supabase/supabase-js'; +import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'; +import { dev } from '$app/environment'; +import { env } from '$env/dynamic/public'; +// or use the static env + +// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; + +export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, { + persistSession: false, + autoRefreshToken: false +}); + +setupSupabaseHelpers({ + supabaseClient, + cookieOptions: { + secure: !dev + } +}); +``` + + + + +### Initialize the client {#migration-initialize-client} + + + + + +```html title="src/routes/__layout.svelte" @@ -74,232 +536,336 @@ Edit your `__layout.svelte` file and add import the `SupaAuthHelper` component, ``` -### Hooks setup + -Our `hooks.ts` file is where the heavy lifting of this library happens, we need to import our function to handle the sign in, signing out and cookie creation phase. we can import all the hooks using `handleAuth` function and destructure its returned data. + -```ts -// src/hooks.ts -import { handleAuth } from '@supabase/auth-helpers-sveltekit' -import type { GetSession, Handle } from '@sveltejs/kit' -import { sequence } from '@sveltejs/kit/hooks' +The `@supabase/auth-helpers-svelte` library is no longer required as the `@supabase/auth-helpers-sveltekit` library handles all the client-side code. -export const handle: Handle = sequence(...handleAuth()) +```html title="src/routes/+layout.svelte" + + + +``` + + + + +### Set up hooks {#migration-set-up-hooks} + + + + + +```ts title="src/hooks.ts" +import { handleAuth } from '@supabase/auth-helpers-sveltekit'; +import type { GetSession, Handle } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; + +export const handle: Handle = sequence(...handleAuth()); export const getSession: GetSession = async (event) => { - const { user, accessToken, error } = event.locals - return { - user, - accessToken, - error, + const { user, accessToken, error } = event.locals; + return { + user, + accessToken, + error } } ``` -These will create the handlers under the hood that perform different parts of the authentication flow: + -- `/api/auth/callback`: The `UserHelper` forwards the session details here every time `onAuthStateChange` fires on the client side. This is needed to set up the cookies for your application so that SSR works seamlessly. -- `/api/auth/user`: You can fetch user profile information in JSON format. -- `/api/auth/logout`: You can logout the user. + -### Typings +```ts title="src/hooks.server.ts" +// make sure the supabase instance is initialized on the server +import '$lib/db'; +import { dev } from '$app/environment'; +import { auth } from '@supabase/auth-helpers-sveltekit/server'; -In order to get the most out of TypeScript and its intellisense, you should import our types into the `app.d.ts` type definition file that comes with your SvelteKit project. +export const handle = auth(); +``` -```ts -// src/app.d.ts +**Optional** *if using additional handle methods* + +```ts title="src/hooks.server.ts" +// make sure the supabase instance is initialized on the server +import '$lib/db'; +import { dev } from '$app/environment'; +import { auth } from '@supabase/auth-helpers-sveltekit/server'; +import { sequence } from '@sveltejs/kit/hooks'; + +export const handle = sequence(auth(), yourHandler); +``` + + + + +### Typings {#migration-typings} + + + + + +```ts title="src/app.d.ts" /// // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare namespace App { interface UserSession { - user: import('@supabase/supabase-js').User - accessToken?: string + user: import('@supabase/supabase-js').User; + accessToken?: string; } + interface Locals extends UserSession { - error: import('@supabase/supabase-js').ApiError + error: import('@supabase/supabase-js').ApiError; } - interface Session extends UserSession {} // interface Platform {} // interface Stuff {} + interface Session extends UserSession {} + + // interface Platform {} + // interface Stuff {} } ``` -### Signing out + -This library has provided a dedicated endpoint for you to use to sign a user out. This endpoint will sign the user out of the Gotrue server, clear the cookies that were set when the user logged in and redirect the user to a configurable path. + -The logout handler endpoint is `/api/auth/logout`, this will take a `GET` request which means it can be used as the href for a normal `a` tag in your html. +```ts title="src/app.d.ts" +/// -```html -Sign out +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + interface Locals { + session: import('@supabase/auth-helpers-sveltekit').SupabaseSession; + } + + interface PageData { + session: import('@supabase/auth-helpers-sveltekit').SupabaseSession; + } + + // interface Error {} + // interface Platform {} +} ``` -### Logout handler configuration + + -In your `src/hooks.ts` file the logout handler is already setup and you can configure the redirect path from here. +### Check the user on the client -> By default the redirect path after logging out will be `/`. + -```ts -export const handle = sequence( - ...handleAuth({ - logout: { returnTo: '/auth/signin' }, - }) -) -``` + -### Basic Setup - -You can now determine if a user is authenticated on the client-side by checking that the `user` object returned by the `$session` store is defined. - -```html -// example +```html title="src/routes/index.svelte" {#if !$session.user} -

I am not logged in

+

I am not logged in

{:else} -

Welcome {$session.user.email}

-

I am logged in!

+

Welcome {$session.user.email}

+

I am logged in!

{/if} ``` -## Client-side data fetching with RLS +
-For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work properly when fetching data client-side, you need to make sure to import the `{ supabaseClient }` from `@supabase/auth-helpers-sveltekit` and only run your query once the user is defined client-side in the `$session`: + -```html +```html title="src/routes/+page.svelte" -{#if !$session.user} - {#if $error} -

{$error.message}

- {/if} -

{$isLoading ? `Loading...` : `Loaded!`}

- +{#if !$page.data.session.user} +

I am not logged in

{:else} - Sign out -

user:

-
{JSON.stringify($session.user, null, 2)}
-

client-side data fetching with RLS

-
{JSON.stringify(loadedData, null, 2)}
+

Welcome {$page.data.session.user.email}

+

I am logged in!

{/if} ``` -### Server-side data fetching with RLS +
+
-For [row level security](https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security) to work in a server environment, you need to inject the request context into the supabase client: +### withPageAuth + + + + + +```html title="src/routes/protected-route.svelte" + -```html -
Protected content for {user.email}
+

server-side fetched data with RLS:

{JSON.stringify(data, null, 2)}
+

user:

{JSON.stringify(user, null, 2)}
``` -```ts -// src/routes/profile.ts -import { - supabaseServerClient, - withApiAuth, -} from '@supabase/auth-helpers-sveltekit' -import type { RequestHandler } from './__types/profile' +
-interface TestTable { - id: string - created_at: string -} + -interface GetOutput { - user: User - data: TestTable[] -} +```html title="src/routes/protected-route/+page.svelte" + - return { - body: { - user: locals.user, - data, - }, - } - } - ) + +
Protected content for {user.email}
+

server-side fetched data with RLS:

+
{JSON.stringify(tableData, null, 2)}
+

user:

+
{JSON.stringify(user, null, 2)}
``` -## Protecting API routes +```ts title="src/routes/protected-route/+page.ts" +import { withAuth } from '@supabase/auth-helpers-sveltekit'; +import { redirect } from '@sveltejs/kit'; +import type { PageLoad } from './$types'; -Wrap an API Route to check that the user has a valid session. If they're not logged in the handler will return a -303 and redirect header. +export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => { + if (!session.user) { + throw redirect(303, '/'); + } -```ts -// src/routes/api/protected-route.ts -import { - supabaseServerClient, - withApiAuth, -} from '@supabase/auth-helpers-sveltekit' -import type { RequestHandler } from './__types/protected-route' + const { data: tableData } = await getSupabaseClient().from('test').select('*'); + return { tableData, user: session.user }; +}); +``` + +
+
+ +### withApiAuth + + + + + +```ts title="src/routes/api/protected-route.ts" +import { supabaseServerClient, withApiAuth } from "@supabase/auth-helpers-sveltekit"; +import type { RequestHandler } from "./__types/protected-route"; interface TestTable { - id: string - created_at: string + id: string; + created_at: string; } interface GetOutput { - data: TestTable[] + data: TestTable[]; } export const GET: RequestHandler = async ({ locals, request }) => withApiAuth({ user: locals.user }, async () => { // Run queries with RLS on the server - const { data } = await supabaseServerClient(request) - .from('test') - .select('*') + const { data } = await supabaseServerClient(request).from("test").select("*"); return { status: 200, - body: { data }, - } - }) + body: { data } + }; + }); ``` -If you visit `/api/protected-route` without a valid session cookie, you will get a 303 response. + + + + +```ts title="src/routes/api/protected-route/+server.ts" +import type { RequestHandler } from './$types'; +import { withAuth } from '@supabase/auth-helpers-sveltekit'; +import { json, redirect } from '@sveltejs/kit'; + +interface TestTable { + id: string; + created_at: string; +} + +export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => { + if (!session.user) { + throw redirect(303, '/'); + } + + const { data } = await getSupabaseClient() + .from('test') + .select('*'); + + return json({ data }); +); +``` + + + ## Additional Links diff --git a/apps/reference/docs/guides/auth/auth-keycloak.mdx b/apps/reference/docs/guides/auth/auth-keycloak.mdx index 39d1eb96196..0282eb6cb75 100644 --- a/apps/reference/docs/guides/auth/auth-keycloak.mdx +++ b/apps/reference/docs/guides/auth/auth-keycloak.mdx @@ -22,7 +22,7 @@ Keycloak OAuth consists of five broad steps: - Ensure that the new client has the "Client Protocol" set to "openid-connect" and the "Access Type" is set to "confidential". - The `Client ID` of the client created will be used as the `client id`. - Obtain the `Secret` from the credentials tab which will be used as the `client secret`. -- Whitelist the callback url of your application. +- Add the callback url of your application to your allowlist. ## Steps diff --git a/apps/reference/docs/guides/integrations/polyscale.mdx b/apps/reference/docs/guides/integrations/polyscale.mdx new file mode 100644 index 00000000000..0d2098f1004 --- /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 caching engine which allows low-latency reads from your database 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/docs/guides/platform/logs.mdx b/apps/reference/docs/guides/platform/logs.mdx index 7219fd5f673..1607b4f3ae8 100644 --- a/apps/reference/docs/guides/platform/logs.mdx +++ b/apps/reference/docs/guides/platform/logs.mdx @@ -4,78 +4,212 @@ title: Logging description: Getting started with Supabase Platform Log Browser --- -The Supabase Platform provides a log explorer that allows log tracing and debugging. Currently, PostgreSQL and Cloudflare edge logs are available. +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' +import ThemedImage from '@theme/ThemedImage'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +The Supabase Platform includes a Logs Explorer that allows log tracing and debugging. Log retention is based on your [project's pricing plan](https://supabase.com/pricing). :::note - -The features discussed in this article are only available through the Supabase Platform and are not available on self-hosted. - +These features are not currently available for self-hosting and local development.
+This is on the roadmap and you can follow the progress in the [Logflare repository](https://github.com/Logflare/logflare). ::: ## Product Logs -As well as a Log Explorer, Supabase provides a logging interface specific to each product. +Supabase provides a logging interface specific to each product. You can use simple regular expressions for keywords and patterns to search log event messages. You can also export and download the log events matching your query as a spreadsheet. -### API Logs + + + + + +[API logs](https://app.supabase.com/project/_/database/api-logs) show all network requests and response for the REST and GraphQL [APIs](../../guides/api). ![API Logs](/img/guides/platform/logs/logs-api.png) -The API Logs can be found under `Database > API Logs`. These show all network requests for the REST and GraphQL [API](../../guides/api). + + -### Postgres Logs +[Postgres logs](https://app.supabase.com/project/_/database/postgres-logs) show all queries and activity for your [database](../../guides/database). ![Postgres Logs](/img/guides/platform/logs/logs-database.png) -The Postgres Logs can be found under `Database > Postgres Logs`. These show all queries and activity for your [Database](../../guides/database). + + -## Log Explorer +[Auth logs](https://app.supabase.com/project/_/auth/logs) show all server logs for your [Auth usage](../../guides/auth). -![Sidebar navigation steps](/img/guides/platform/logs/sidebar-navigation.png) +![Auth Logs](/img/guides/platform/logs/logs-auth.png) -The log browser can be accessed in the sidebar under **Logs Explorer**. The **Logs Explorer** is for querying and aggregating project logs across products using SQL `SELECT` queries. + + -### Example +[Storage logs](https://app.supabase.com/project/_/storage/logs) shows all server logs for your [Storage API](../../guides/storage). -For example, you may enter the following into the SQL editor to query for each user's IP address: +![Storage Logs](/img/guides/platform/logs/logs-storage.png) + + + + +[Realtime logs](https://app.supabase.com/project/_/database/realtime-logs) show all server logs for your [Realtime API usage](../../guides/realtime). + +![Realtime Logs](/img/guides/platform/logs/logs-realtime.png) + + + + +For each [Edge Function](https://app.supabase.com/project/_/functions), logs are available under the following tabs: + +**Invocations** + +The Invocations tab displays the edge logs of function calls. + +![Function Edge Logs](/img/guides/platform/logs/logs-functions-edge.png) + +**Logs** + +The Logs tab displays logs emitted during function execution. + +![Function Logs](/img/guides/platform/logs/logs-functions.png) + + + + +## Logs Explorer + +The [Logs Explorer](https://app.supabase.com/project/_/logs-explorer) exposes logs from each part of the Supabase stack as a separate table that can be queried and joined using SQL. + +![Logs Explorer](/img/guides/platform/logs/logs-explorer.png) + +You can access the following logs from the **Sources** drop-down: + +- `auth_logs`: GoTrue server logs, containing authentication/authorization activity. +- `edge_logs`: Edge network logs, containing request and response metadata retrieved from Cloudflare. +- `function_edge_logs`: Edge network logs for only edge functions, containing network requests and response metadata for each execution. +- `function_logs`: Function internal logs, containing any `console` logging from within the edge function. +- `postgres_logs`: Postgres database logs, containing statements executed by connected applications. +- `realtime_logs`: Realtime server logs, containing client connection information. +- `storage_logs`: Storage server logs, containing object upload and retrieval information. + +## Querying with the Logs Explorer + +The Logs Explorer uses BigQuery and supports all [available SQL functions and operators](https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators). + +### Timestamp Display and Behavior + +Each log entry is stored with a `timestamp` as a `TIMESTAMP` data type. Use the appropriate [timestamp function](https://cloud.google.com/bigquery/docs/reference/standard-sql/timestamp_functions#timestamp) to utilize the `timestamp` field in a query. + +Raw top-level timestamp values are rendered as unix microsecond. To render the timestamps in a human-readable format, use the `DATETIME()` function to convert the unix timestamp display into an ISO-8601 timestamp. ```sql -SELECT timestamp, h.x_real_ip -FROM edge_logs - LEFT JOIN UNNEST(metadata) as m ON TRUE - LEFT JOIN UNNEST(m.request) AS r ON TRUE - LEFT JOIN UNNEST(r.headers) AS h ON TRUE -WHERE h.x_real_ip IS NOT NULL +-- timestamp column without datetime() +select timestamp from .... +-- 1664270180000 + +-- timestamp column with datetime() +select datetime(timestamp) from .... +-- 2022-09-27T09:17:10.439Z ``` -![SELECT query example](/img/guides/platform/logs/select-query.png) - -The list of supported product sources can be found under the **Sources** dropdown. - ### Unnesting Arrays -To query the metadata in the above example, you can unnest the field and "join" the unnested data. Clicking on the log row shows that log metadata is stored as an array of objects. +Each log event stores metadata an array of objects with multiple levels, and can be seen by selecting single log events in the Logs Explorer. To query arrays, use `unnest()` on each array field and add it to the query as a join. This allows you to reference the nested objects with an alias and select their individual fields. -In order to query any value that is an array, we would need to `UNNEST()` that field and add it to the query as a join, thereby allowing us to reference the nested fields within the array. +For example, to query the edge logs without any joins: -:::caution +```sql +select timestamp, metadata from edge_logs t +``` +The resulting `metadata` key is rendered as an array of objects in the Logs Explorer. In the following diagram, each box represents a nested array of objects: -Large projects may run into a `Resources Exceeded` memory limit error when selecting large objects with many nested keys. To avoid this error, select individual keys separately or reduce the queried date range. -::: + -### Functions + -You may have also noticed from the above examples that we are able to use certain SQL functions within our queries. The querying engine currently supported is BigQuery, hence you may use [any valid BigQuery function](https://cloud.google.com/bigquery/docs/reference/standard-sql/functions-and-operators) within your query. -#### Timestamp Behavior +Perform a `cross join unnest()` to work with the keys nested in the `metadata` key. -Each log entry is stored with a `timestamp`. In order to utilize the `timestamp` field in a query, you can use the appropriate [timestamp functions](https://cloud.google.com/bigquery/docs/reference/standard-sql/timestamp_functions#timestamp). +To query for a nested value, add a join for each array level: -:::note +```sql +select timestamp, request.method, header.cf_ipcountry +from edge_logs t +cross join unnest(t.metadata) as metadata +cross join unnest(metadata.request) as request +cross join unnest(request.headers) as header +``` -In the Log Explorer, timestamps are rendered as unix microsecond timestamps. SQL queries, however, should always use the `TIMESTAMP` data type. If you are using a unix timestamp value in a query, cast the value to a `TIMESTAMP` data type. +This surfaces the following columns available for selection: -::: + -## Templates +This allows you to select the `method` and `cf_ipcountry` columns. In JS dot notation, the full paths for each selected column are: -Templates are available to help craft you log queries. Templates are available under the **Templates** tab, or under the **Templates** Dropdown in the **Query** tab. +- `metadata[].request[].method` +- `metadata[].request[].headers[].cf_ipcountry` + +### LIMIT and Result Row Limitations + +The Logs Explorer has a maximum of 1000 rows per run. Use `LIMIT` to optimize your queries by reducing the number of rows returned further. + +### Best Practices + +1. Include a filter over **timestamp** + + Querying your entire log history might seem appealing. For **Enterprise** customers that have a large retention range, you run the risk of timeouts due additional time required to scan the larger dataset. + +2. Avoid selecting large nested objects. Select individual values instead. + + When querying large objects, the columnar storage engine selects each column associated with each nested key, resulting in a large number of columns being selected. This inadvertently impacts the query speed and may result in timeouts or memory errors, especially for projects with a lot of logs. + + Instead, select only the values required. + + ```sql + -- ❌ Avoid doing this + select + datetime(timestamp), + m as metadata -- <- metadata contains many nested keys + from edge_logs t + cross join unnest(t.metadata) as m; + + -- ✅ Do this + select + datetime(timestamp), + r.method -- <- select only the required values + from edge_logs t + cross join unnest(t.metadata) as m + cross join unnest(m.request) as r + ``` + +### Examples and Templates + +The Logs Explorer includes **Templates** (available in the Templates tab or the dropdown in the Query tab) to help you get started. + +For example, you can enter the following query in the SQL Editor to retrieve each user's IP address: + +```sql +select datetime(timestamp), h.x_real_ip +from edge_logs + cross join unnest(metadata) as m + cross join unnest(m.request) AS r + cross join unnest(r.headers) AS h +where h.x_real_ip is not null and r.method = "GET" +``` diff --git a/apps/reference/nav/_referenceSidebars.js b/apps/reference/nav/_referenceSidebars.js index 0b7d2f58b4e..bab8deb5fea 100644 --- a/apps/reference/nav/_referenceSidebars.js +++ b/apps/reference/nav/_referenceSidebars.js @@ -79,6 +79,7 @@ const sidebars = { items: [ 'guides/auth/row-level-security', 'guides/auth/managing-user-data', + 'guides/auth/auth-captcha', ], }, { @@ -219,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/nav/supabase_js_sidebars.js b/apps/reference/nav/supabase_js_sidebars.js index 228a4dea15e..990a6b04951 100644 --- a/apps/reference/nav/supabase_js_sidebars.js +++ b/apps/reference/nav/supabase_js_sidebars.js @@ -117,9 +117,9 @@ const sidebars = { 'generated/single', 'generated/maybesingle', 'generated/db-csv', - 'generated/db-geojson', - 'generated/db-explain', - 'generated/db-rollback', + // 'generated/db-geojson', + // 'generated/db-explain', + // 'generated/db-rollback', ], collapsed: true, }, diff --git a/apps/reference/static/img/guides/auth-captcha/site_secret_settings.png b/apps/reference/static/img/guides/auth-captcha/site_secret_settings.png new file mode 100644 index 00000000000..bcba8182c91 Binary files /dev/null and b/apps/reference/static/img/guides/auth-captcha/site_secret_settings.png differ diff --git a/apps/reference/static/img/guides/auth-captcha/sitekey_settings.png b/apps/reference/static/img/guides/auth-captcha/sitekey_settings.png new file mode 100644 index 00000000000..194762726e0 Binary files /dev/null and b/apps/reference/static/img/guides/auth-captcha/sitekey_settings.png differ diff --git a/apps/reference/static/img/guides/auth-captcha/sites_dashboard.png b/apps/reference/static/img/guides/auth-captcha/sites_dashboard.png new file mode 100644 index 00000000000..20a07cb1e2e Binary files /dev/null and b/apps/reference/static/img/guides/auth-captcha/sites_dashboard.png differ diff --git a/apps/reference/static/img/guides/auth-captcha/supabase_auth_general_settings.png b/apps/reference/static/img/guides/auth-captcha/supabase_auth_general_settings.png new file mode 100644 index 00000000000..9a2a2a65e34 Binary files /dev/null and b/apps/reference/static/img/guides/auth-captcha/supabase_auth_general_settings.png differ 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/reference/static/img/guides/platform/logs/logs-api.png b/apps/reference/static/img/guides/platform/logs/logs-api.png index a5051f4ca2a..798abee5f73 100644 Binary files a/apps/reference/static/img/guides/platform/logs/logs-api.png and b/apps/reference/static/img/guides/platform/logs/logs-api.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-auth.png b/apps/reference/static/img/guides/platform/logs/logs-auth.png new file mode 100644 index 00000000000..a020bb86430 Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/logs-auth.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-database.png b/apps/reference/static/img/guides/platform/logs/logs-database.png index 2b67b16a2a8..78f9036eeaa 100644 Binary files a/apps/reference/static/img/guides/platform/logs/logs-database.png and b/apps/reference/static/img/guides/platform/logs/logs-database.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-explorer.png b/apps/reference/static/img/guides/platform/logs/logs-explorer.png new file mode 100644 index 00000000000..3d21fb0569c Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/logs-explorer.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-functions-edge.png b/apps/reference/static/img/guides/platform/logs/logs-functions-edge.png new file mode 100644 index 00000000000..2d0d699cac1 Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/logs-functions-edge.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-functions.png b/apps/reference/static/img/guides/platform/logs/logs-functions.png new file mode 100644 index 00000000000..efd455955c9 Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/logs-functions.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-realtime.png b/apps/reference/static/img/guides/platform/logs/logs-realtime.png new file mode 100644 index 00000000000..4b93e87e990 Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/logs-realtime.png differ diff --git a/apps/reference/static/img/guides/platform/logs/logs-storage.png b/apps/reference/static/img/guides/platform/logs/logs-storage.png new file mode 100644 index 00000000000..d0f98ea43cc Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/logs-storage.png differ diff --git a/apps/reference/static/img/guides/platform/logs/select-query.png b/apps/reference/static/img/guides/platform/logs/select-query.png deleted file mode 100644 index 6d9c71a9664..00000000000 Binary files a/apps/reference/static/img/guides/platform/logs/select-query.png and /dev/null differ diff --git a/apps/reference/static/img/guides/platform/logs/sidebar-navigation.png b/apps/reference/static/img/guides/platform/logs/sidebar-navigation.png deleted file mode 100644 index 41cf3ebdfe4..00000000000 Binary files a/apps/reference/static/img/guides/platform/logs/sidebar-navigation.png and /dev/null differ diff --git a/apps/reference/static/img/guides/platform/logs/unnesting-2-dark.png b/apps/reference/static/img/guides/platform/logs/unnesting-2-dark.png new file mode 100644 index 00000000000..eeb81e2839e Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/unnesting-2-dark.png differ diff --git a/apps/reference/static/img/guides/platform/logs/unnesting-2.png b/apps/reference/static/img/guides/platform/logs/unnesting-2.png new file mode 100644 index 00000000000..86ed5cb385f Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/unnesting-2.png differ diff --git a/apps/reference/static/img/guides/platform/logs/unnesting-none-dark.png b/apps/reference/static/img/guides/platform/logs/unnesting-none-dark.png new file mode 100644 index 00000000000..3d2b0ccc1d7 Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/unnesting-none-dark.png differ diff --git a/apps/reference/static/img/guides/platform/logs/unnesting-none.png b/apps/reference/static/img/guides/platform/logs/unnesting-none.png new file mode 100644 index 00000000000..71cb27efece Binary files /dev/null and b/apps/reference/static/img/guides/platform/logs/unnesting-none.png differ diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000000..b5924c32836 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,7 @@ +node_modules/ +.vercel +.next +.env.local +.env.production +.env.dev +.env.* \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example index b6aa9a0fd95..8e476f74c31 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -70,5 +70,8 @@ ENABLE_PHONE_AUTOCONFIRM=true # Studio - Configuration for the Dashboard ############ +STUDIO_ORGANIZATION_NAME=Default Organization +STUDIO_PROJECT_NAME=Default Project + STUDIO_PORT=3000 PUBLIC_REST_URL=http://localhost:8000/rest/v1/ # replace if you intend to use Studio outside of localhost diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d879f3ba834..af69fb28459 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -17,6 +17,9 @@ services: STUDIO_PG_META_URL: http://meta:8080 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DEFAULT_ORGANIZATION: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT: ${STUDIO_DEFAULT_PROJECT} + SUPABASE_URL: http://kong:8000 SUPABASE_REST_URL: ${PUBLIC_REST_URL} SUPABASE_ANON_KEY: ${ANON_KEY} diff --git a/i18n/README.sr.md b/i18n/README.sr.md new file mode 100644 index 00000000000..0a2c541cee7 --- /dev/null +++ b/i18n/README.sr.md @@ -0,0 +1,206 @@ +

+ + +

+ +--- + +# Supabase + +[Supabase](https://supabase.com) je open source Firebase alternativa. Mi razvijamo funkcionalnosti Firebase-a koristeći profesionalne open source alate. + +- [x] Hostovana Postgres Baza. [Dokumentacija](https://supabase.com/docs/guides/database) +- [x] Autentifikacija i Autorizacija. [Dokumentacija](https://supabase.com/docs/guides/auth) +- [x] Auto-generisani API-ji. + - [x] REST. [Dokumentacija](https://supabase.com/docs/guides/api#rest-api) + - [x] Subskripcije u realnom vremenu. [Dokumentacija](https://supabase.com/docs/guides/api#realtime-api) + - [x] GraphQL (Beta). [Dokumentacija](https://supabase.com/docs/guides/api#graphql-api) +- [x] Funkcije. + - [x] Funkcije u bazi. [Dokumentacija](https://supabase.com/docs/guides/database/functions) + - [x] Edge Funkcije [Dokumentacija](https://supabase.com/docs/guides/functions) +- [x] Skladištenje fajlova. [Dokumentacija](https://supabase.com/docs/guides/storage) +- [x] Kontrolna tabla + +![Supabase kontrolna tabla](https://raw.githubusercontent.com/supabase/supabase/master/apps/www/public/images/github/supabase-dashboard.png) + +## Dokumentacija + +Za punu dokumentaciju, posetite [supabase.com/docs](https://supabase.com/docs) + +Da vidite kako možete doprineti razvoju, posetite [Getting Started](./DEVELOPERS.md) + +## Zajednica & Podrška + +- [Forum zajednice](https://github.com/supabase/supabase/discussions). Najbolje mesto za: pomoć u razvijanju, diskusije o najboljim praksama u vezi baze. +- [GitHub Issues](https://github.com/supabase/supabase/issues). Najbolje mesto za: bagove i greške sa kojima se susretnete tokom korišćenja Supabase. +- [Email Podrška](https://supabase.com/docs/support#business-support). Najbolje mesto za: probleme sa vašom bazom ili infrastrukturom. +- [Discord](https://discord.supabase.com). Najbolje mesto za: deljenje vaših aplikacija i druženje sa zajednicom. + +## Status + +- [x] Alfa: Testiramo Supabase sa zatvorenim setom klijenata +- [x] Javna Alfa: Bilo ko može da se prijavi na [app.supabase.com](https://app.supabase.com). Ali ne budite grubi, biće nekih poteškoća +- [x] Javna Beta: Dovoljno stabilno za većinu ne-enterprise projekata +- [ ] Objavljeno: spremno za produkciju + +Trenutno smo u javnoj beti. Pratite "releasove" ovog repozitorijuma da bi ste ostali u toku sa bitnim objavama. + +Pratite ovaj repo + +--- + +## Kako radi + +Supabase je kombinacija open source alata. Razvijamo funkcionalnosti Firebase-a koristeći profesionalne open source projekte. Ako već postoje alati i zajednice, sa MIT, Apache 2 ili ekvivalentnim otvorenim licencama, koristićemo i podržati taj alat. Ako alat ne postoji, mi ga razvijamo i open sourcujemo sami. Supabase nije 1-na-1 mapiranje Firebase-a. Naš cilj je da pružimo developerima iskustvo u razvoju slično kao Firebase koristeći open source alate. + +**Arhitektura** + +Supabase je [hostovana platforma](https://app.supabase.com). Možete se prijaviti i početi sa korišćenjem Supabase-a bez instaliranja bilo čega. +Takođe možete da [hostujete sami](https://supabase.com/docs/guides/hosting/overview) i da [razvijate u lokalu](https://supabase.com/docs/guides/local-development). + +![Arhitektura](https://user-images.githubusercontent.com/70828596/187547862-ffa9d058-0c3a-4851-a3e7-92ccfca4b596.png) + +- [PostgreSQL](https://www.postgresql.org/) je sistem objektno-relacione baze sa preko 30 godina aktivnog razvoja čime je zaslužio jaku reputaciju za pouzdanost, robustnost funkcionalnosti i performanse. +- [Realtime](https://github.com/supabase/realtime) je Elixir server koji vam omogućava da slušate PostgreSQL inserte, updateove, i brisanja koristeći websokete. Realtime polluje Postgres-ovu ugrađenu funkcionalnost replikacije za promene nad bazom, konvertuje promene u JSON, zatim broadcastuje taj JSON kroz websoket autorizovanim klijentima. +- [PostgREST](http://postgrest.org/) je web server koji pretvara vašu PostgreSQL bazu direktno u RESTful API +- [Storage](https://github.com/supabase/storage-api) daje RESTful interfejs za upravljanje fajlovima sačuvanim na S3, koristeći Postgres za upravljanje permisijama. +- [postgres-meta](https://github.com/supabase/postgres-meta) je RESTful API za upravljanje vašom Postgres isntancom, omogućavajući vam da dohvatite tabele, dodate role, izvršavate upite, itd. +- [GoTrue](https://github.com/netlify/gotrue) je SWT baziran API za upravljanje korisnicima i izdavanje SWT tokena. +- [Kong](https://github.com/Kong/kong) je cloud-nativan API gateway. + +#### Klijentske biblioteke + +Naš pristup klijentskim bibliotekama je modularan. Svaka pod-biblioteka je samostalna implementacija za jedan eksterni sistem. Ovo je jedan od načina na koji podržavamo postojeće alate. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
JezikKlijentFunkcionalni klijenti (spakovani u Supabase klijenta)
SupabasePostgRESTGoTrueRealtimeStorageFunctions
⚡️ Oficijelni ⚡️
JavaScript (TypeScript)supabase-jspostgrest-jsgotrue-jsrealtime-jsstorage-jsfunctions-js
💚 Od zajednice 💚
C#supabase-csharppostgrest-csharpgotrue-csharprealtime-csharpstorage-csharpfunctions-csharp
Dart (Flutter)supabase-dartpostgrest-dartgotrue-dartrealtime-dartstorage-dartfunctions-dart
Go-postgrest-go--storage-go-
Java--gotrue-java---
Kotlin-postgrest-ktgotrue-kt---
Pythonsupabase-pypostgrest-pygotrue-pyrealtime-pystorage-pyfunctions-py
Rubysupabase-rbpostgrest-rb----
Rust-postgrest-rs----
Swiftsupabase-swiftpostgrest-swiftgotrue-swiftrealtime-swiftstorage-swift-
+ + + + +## Prevodi + +- [Lista prevoda](/i18n/languages.md) + +--- + +## Sponzori + +[![Novi sponzor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) diff --git a/i18n/languages.md b/i18n/languages.md index b747bb786f7..c3d55c57cf9 100644 --- a/i18n/languages.md +++ b/i18n/languages.md @@ -30,6 +30,7 @@ - [Portuguese (Brazilian) / Português Brasileiro](/i18n/README.pt-br.md) - [Romanian / Română](/i18n/README.ro.md) - [Russian / Pусский](/i18n/README.ru.md) +- [Serbian / Srpski](/i18n/README.sr.md) - [Sinhala / සිංහල](/i18n/README.si.md) - [Spanish / Español](/i18n/README.es.md) - [Simplified Chinese / 简体中文](/i18n/README.zh-cn.md) diff --git a/spec/supabase_js_v2_legacy.yml b/spec/supabase_js_v2_legacy.yml index bb2011bd883..aeeed1d11c1 100644 --- a/spec/supabase_js_v2_legacy.yml +++ b/spec/supabase_js_v2_legacy.yml @@ -640,267 +640,1125 @@ pages: title: 'Fetch data: select()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.select' notes: | - - By default, Supabase projects will return a maximum of 1,000 rows. This setting can be changed in Project API Settings. It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data. - - `select()` can be combined with [Modifiers](/docs/reference/javascript/using-modifiers) - - `select()` can be combined with [Filters](/docs/reference/javascript/using-filters) - - If using the Supabase hosted platform `apikey` is technically a reserved keyword, since the API gateway will pluck it out for authentication. [It should be avoided as a column name](https://github.com/supabase/supabase/issues/5465). + - By default, Supabase projects return a maximum of 1,000 rows. This setting can be changed in your project's [API settings](https://app.supabase.com/project/_/settings/api). It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data. + - `select()` can be combined with [Filters](/docs/reference/javascript/next/using-filters) + - `select()` can be combined with [Modifiers](/docs/reference/javascript/next/using-modifiers) + - `apikey` is a reserved keyword if you're using the [Supabase Platform](/docs/guides/hosting/platform) and should be avoided as a column name](https://github.com/supabase/supabase/issues/5465). examples: - name: Getting your data + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Afghanistan" + }, + { + "id": 2, + "name": "Albania" + }, + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') + .from('countries') .select() ``` - name: Selecting specific columns - description: You can select specific fields from your tables. - js: | - ```js - const { data, error } = await supabase - .from('cities') - .select('name') - ``` + description: | + You can select specific fields from your tables. + + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + ``` + + + + + ```json + { + "data": [ + { + "name": "Afghanistan" + }, + { + "name": "Albania" + }, + { + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true - name: Query foreign tables - description: If your database has foreign key relationships, you can query related tables too. - js: | - ```js - const { data, error } = await supabase - .from('countries') - .select(` - name, + description: | + If your database has foreign key relationships, you can query related tables too. + + + + + ```sql + create table + countries (id int8 primary key, name text); + create table cities ( - name - ) - `) - ``` - note: | - What about join tables - If you're in a situation where your tables are **NOT** directly related, but instead are joined by a _join table_, - you can still use the `select()` method to query the related data. The PostgREST engine detects the relationship automatically. - For more details, [follow the link](https://postgrest.org/en/latest/api.html#embedding-through-join-tables). + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select(` + name, + cities ( + name + ) + `) + ``` + + + + + ```json + { + "data": [ + { + "name": "Germany", + "cities": [ + { + "name": "Munich" + } + ] + }, + { + "name": "Indonesia", + "cities": [ + { + "name": "Bali" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + - name: Query foreign tables through a join table + description: | + If you're in a situation where your tables are **NOT** directly + related, but instead are joined by a _join table_, you can still use + the `select()` method to query the related data. The join table needs + to have the foreign keys as part of its composite primary key. + + + + + ```sql + create table + users ( + id int8 primary key, + name text + ); + create table + teams ( + id int8 primary key, + name text + ); + -- join table + create table + users_teams ( + user_id int8 not null references users, + team_id int8 not null references teams, + -- both foreign keys must be part of a composite primary key + primary key (user_id, team_id) + ); + + insert into + users (id, name) + values + (1, 'Kiran'), + (2, 'Evan'); + insert into + teams (id, name) + values + (1, 'Green'), + (2, 'Blue'); + insert into + users_teams (user_id, team_id) + values + (1, 1), + (1, 2), + (2, 2); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('users') + .select(` + name, + teams ( + name + ) + `) + ``` + + + + + ```json + { + "data": [ + { + "name": "Kiran", + "teams": [ + { + "name": "Green" + }, + { + "name": "Blue" + } + ] + }, + { + "name": "Evan", + "teams": [ + { + "name": "Blue" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true - name: Query the same foreign table multiple times description: | - Sometimes you will need to query the same foreign table twice. - In this case, you can use the name of the joined column to identify - which join you intend to use. For convenience, you can also give an - alias for each column. For example, if we had a shop of products, - and we wanted to get the supplier and the purchaser at the same time - (both in the users) table: - js: | - ```js - const { data, error } = await supabase - .from('products') - .select(` - id, - supplier:supplier_id ( name ), - purchaser:purchaser_id ( name ) - `) - ``` - - name: Filtering with inner joins + If you need to query the same foreign table twice, use the name of the + joined column to identify which join to use. You can also give each + column an alias. + + + + + ```sql + create table + users (id int8 primary key, name text); + + create table + messages ( + sender_id int8 not null references users, + receiver_id int8 not null references users, + content text + ); + + insert into + users (id, name) + values + (1, 'Kiran'), + (2, 'Evan'); + + insert into + messages (sender_id, receiver_id, content) + values + (1, 2, '👋'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('messages') + .select(` + content, + from:sender_id(name), + to:receiver_id(name) + `) + ``` + + + + + ```json + { + "data": [ + { + "content": "👋", + "from": { + "name": "Kiran" + }, + "to": { + "name": "Evan" + } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + - name: Filtering through foreign tables description: | - If you want to filter a table based on a child table's values you can use the `!inner()` function. For example, if you wanted - to select all rows in a `message` table which belong to a user with the `username` "Jane": - js: | - ```js - const { data, error } = await supabase - .from('messages') - .select('*, users!inner(*)') - .eq('users.username', 'Jane') - ``` + If the filter on a foreign table's column is not satisfied, the foreign + table returns `[]` or `null` but the parent table is not filtered out. + + + + + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('cities') + .select('name, countries(*)') + .eq('countries.name', 'Estonia') + ``` + + + + + ```json + { + "data": [ + { + "name": "Bali", + "countries": null + }, + { + "name": "Munich", + "countries": null + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + + If you want to filter out the parent table rows, use the `!inner` hint: + + + + + ```ts + const { data, error } = await supabase + .from('cities') + .select('name, countries!inner(*)') + .eq('countries.name', 'Estonia') + ``` + + + + + ```json + { + "data": [], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true - name: Querying with count option description: | - You can get the number of rows by using the count option. - Allowed values for count option are `null`, [exact](https://postgrest.org/en/stable/api.html#exact-count), [planned](https://postgrest.org/en/stable/api.html#planned-count) and [estimated](https://postgrest.org/en/stable/api.html#estimated-count). - js: | - ```js - const { data, error, count } = await supabase - .from('cities') - .select('name', { count: 'exact' }) // if you don't want to return any rows, you can use { count: 'exact', head: true } - ``` + You can get the number of rows by using the + [count](/docs/reference/javascript/next/select#parameters) option. For + example, to get the table count without returning all rows: + + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { count, error } = await supabase + .from('countries') + .select('*', { count: 'exact', head: true }) + ``` + + + + + ```json + { + "count": 3, + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true - name: Querying JSON data description: | - If you have data inside of a JSONB column, you can apply select - and query filters to the data values. Postgres offers a - [number of operators](https://www.postgresql.org/docs/current/functions-json.html) - for querying JSON data. Also see - [PostgREST docs](http://postgrest.org/en/v7.0.0/api.html#json-columns) for more details. - js: | - ```js + You can select and filter data inside of + [JSON](/docs/guides/database/json) columns. Postgres offers some + [operators](/docs/guides/database/json#query-the-jsonb-data) for + querying JSON data. + + + + + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Avdotya', '{"city":"Saint Petersburg"}'); + ``` + + + + + ```ts const { data, error } = await supabase .from('users') .select(` id, name, - address->street + address->city `) - .eq('address->postcode', 90210) - ``` - - name: Return data as CSV - description: | - By default the data is returned in JSON format, however you can also request for it to be returned as Comma Separated Values. - js: | - ```js - const { data, error } = await supabase - .from('users') - .select() - .csv() - ``` - - name: Aborting requests in-flight - description: | - You can use an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to abort requests. Note that `status` and `statusText` doesn't mean anything for aborted requests, since the request wasn't actually fulfilled. - js: | - ```js - const ac = new AbortController() - supabase - .from('very_big_table') - .select() - .abortSignal(ac.signal) - .then(console.log) - ac.abort() - // { - // error: { - // message: 'FetchError: The user aborted a request.', - // details: '', - // hint: '', - // code: '' - // }, - // data: null, - // body: null, - // count: null, - // status: 400, - // statusText: 'Bad Request' - // } ``` + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Avdotya", + "city": "Saint Petersburg" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + insert(): title: 'Create data: insert()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.insert' examples: - name: Create a record + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + ``` + + + + + ```ts + const { error } = await supabase + .from('countries') + .insert({ id: 1, name: 'Denmark' }) + ``` + + + + + ```json + { + "status": 201, + "statusText": "Created" + } + ``` + + + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Denmark" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js - const { data, error } = await supabase - .from('cities') - .insert([ - { name: 'The Shire', country_id: 554 } - ]) + ```ts + const { error } = await supabase + .from('countries') + .insert({ id: 1, name: 'Denmark' }) ``` - name: Create a record and return it - js: | - ```js - const { data, error } = await supabase - .from('cities') - .insert([ - { name: 'The Shire', country_id: 554 } - ]) - .select() - ``` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .insert({ id: 1, name: 'Denmark' }) + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Denmark" + } + ], + "status": 201, + "statusText": "Created" + } + ``` + + + + hideCodeBlock: true - name: Bulk create description: | - When running a bulk create, the operation is handled in a single transaction. If any of the inserts fail, all other operations are - rolled back. - js: | - ```js - const { data, error } = await supabase - .from('cities') - .insert([ - { name: 'The Shire', country_id: 554 }, - { name: 'Rohan', country_id: 555 }, - ]) - ``` + A bulk create operation is handled in a single transaction. + If any of the inserts fail, none of the rows are inserted. + + + + + ```sql + create table + countries (id int8 primary key, name text); + ``` + + + + + ```ts + const { error } = await supabase + .from('countries') + .insert([ + { id: 1, name: 'Nepal' }, + { id: 1, name: 'Vietnam' }, + ]) + ``` + + + + + ```json + { + "error": { + "code": "23505", + "details": "Key (id)=(1) already exists.", + "hint": null, + "message": "duplicate key value violates unique constraint \"countries_pkey\"" + }, + "status": 409, + "statusText": "Conflict" + } + ``` + + + + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + ``` + + + + + ```json + { + "data": [], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true update(): title: 'Modify data: update()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.update' notes: | - - `update()` should always be combined with [Filters](/docs/reference/javascript/using-filters) to target the item(s) you wish to update. + - `update()` should always be combined with [Filters](/docs/reference/javascript/next/using-filters) to target the item(s) you wish to update. examples: - name: Updating your data + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Taiwan'); + ``` + + + + + ```ts + const { error } = await supabase + .from('countries') + .update({ name: 'Australia' }) + .eq('id', 1) + ``` + + + + + ```json + { + "status": 204, + "statusText": "No Content" + } + ``` + + + + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Australia" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Middle Earth' }) - .match({ name: 'Auckland' }) + ```ts + const { error } = await supabase + .from('countries') + .update({ name: 'Australia' }) + .eq('id', 1) ``` + - name: Update a record and return it + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Taiwan'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .update({ name: 'Australia' }) + .eq('id', 1) + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Australia" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true - name: Updating JSON data description: | - Postgres offers a - [number of operators](https://www.postgresql.org/docs/current/functions-json.html) - for working with JSON data. Right now it is only possible to update an entire JSON document, - but we are [working on ideas](https://github.com/PostgREST/postgrest/issues/465) for updating individual keys. For example: - js: | - ```js - const { data, error } = await supabase - .from('users') - .update(` - address: { - street: 'Melrose Place', - postcode: 90210 - } - `) - .eq('address->postcode', 90210) - ``` + Postgres offers some + [operators](/docs/guides/database/json#query-the-jsonb-data) for + working with JSON data. + + + + + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Michael', '{ "postcode": 90210 }'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('users') + .update({ + address: { + street: 'Melrose Place', + postcode: 90210 + } + }) + .eq('address->postcode', 90210) + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Michael", + "address": { + "street": "Melrose Place", + "postcode": 90210 + } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + + Currently, it is only possible to update the entire JSON document. + hideCodeBlock: true upsert(): title: 'Upsert data: upsert()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.upsert' notes: | - - Primary keys should be included in the data payload in order for an update to work correctly. - - Primary keys must be natural, not surrogate. There are however, [workarounds](https://github.com/PostgREST/postgrest/issues/1118) for surrogate primary keys. + - Primary keys must be included in `values` to use upsert. examples: - name: Upsert your data + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .upsert({ id: 1, name: 'Albania' }) + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Albania" + } + ], + "status": 201, + "statusText": "Created" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('messages') - .upsert({ id: 3, message: 'foo', username: 'supabot' }) + .from('countries') + .upsert({ id: 1, name: 'Albania' }) + .select() ``` - name: Bulk Upsert your data - isSpotlight: false - js: | - ```js - const { data, error } = await supabase - .from('messages') - .upsert([ - { id: 3, message: 'foo', username: 'supabot' }, - { id: 4, message: 'bar', username: 'supabot' } - ]) - ``` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .upsert([ + { id: 1, name: 'Albania' }, + { id: 2, name: 'Algeria' }, + ]) + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Albania" + }, + { + "id": 2, + "name": "Algeria" + } + ], + "status": 201, + "statusText": "Created" + } + ``` + + + + hideCodeBlock: true - name: Upserting into tables with constraints description: | - Running the following will cause supabase to upsert data into the `users` table. - If the username 'supabot' already exists, the `onConflict` argument tells supabase to overwrite that row - based on the column passed into `onConflict`. - isSpotlight: true - js: | - ```js - const { data, error } = await supabase - .from('users') - .upsert({ username: 'supabot' }, { onConflict: 'username' }) - ``` - - name: Return the exact number of rows - isSpotlight: true - js: | - ```js - const { data, error, count } = await supabase - .from('users') - .upsert({ - id: 3, message: 'foo', - username: 'supabot' - }, { - count: 'exact' - }) - ``` + In the following query, `upsert()` implicitly uses the `id` + (primary key) column to determine conflicts. If there is no existing + row with the same `id`, `upsert()` inserts a new row, which + will fail in this case as there is already a row with `handle` `"saoirse"`. + + + + + ```sql + create table + users ( + id int8 generated by default as identity primary key, + handle text not null unique, + display_name text + ); + + insert into + users (id, handle, display_name) + values + (1, 'saoirse', null); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('users') + .upsert({ id: 42, handle: 'saoirse', display_name: 'Saoirse' }) + .select() + ``` + + + + + ```json + { + "error": { + "code": "23505", + "details": "Key (handle)=(saoirse) already exists.", + "hint": null, + "message": "duplicate key value violates unique constraint \"users_handle_key\"" + }, + "status": 409, + "statusText": "Conflict" + } + ``` + + + + + Using the `onConflict` option, you can instruct `upsert()` to use + another column with a unique constraint to determine conflicts: + + + + + ```ts + await supabase + .from('users') + .upsert( + { id: 42, handle: 'saoirse', display_name: 'Saoirse' }, + { onConflict: 'handle' }, + ) + const { data, error } = await supabase + .from('users') + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 42, + "handle": "saoirse", + "display_name": "Saoirse" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true delete(): title: 'Delete data: delete()' $ref: '@supabase/postgrest-js.PostgrestQueryBuilder.delete' notes: | - - `delete()` should always be combined with [filters](/docs/reference/javascript/using-filters) to target the item(s) you wish to delete. + - `delete()` should always be combined with [filters](/docs/reference/javascript/next/using-filters) to target the item(s) you wish to delete. - If you use `delete()` with filters and you have [RLS](/docs/learn/auth-deep-dive/auth-row-level-security) enabled, only rows visible through `SELECT` policies are deleted. Note that by default @@ -908,13 +1766,73 @@ pages: makes the rows visible. examples: - name: Delete records + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Spain'); + ``` + + + + + ```ts + const { error } = await supabase + .from('countries') + .delete() + .eq('id', 1) + ``` + + + + + ```json + { + "status": 204, + "statusText": "No Content" + } + ``` + + + + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + ``` + + + + + ```json + { + "data": [], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js - const { data, error } = await supabase - .from('cities') + ```ts + const { error } = await supabase + .from('countries') .delete() - .match({ id: 666 }) + .eq('id', 1) ``` rpc(): @@ -932,60 +1850,180 @@ pages: ``` $ref: '@supabase/postgrest-js.PostgrestClient.rpc' examples: - - name: Call a Postgres function + - name: Call a Postgres function without arguments + description: | + + + + ```sql + create function hello_world() returns text as $$ + select 'Hello world'; + $$ language sql; + ``` + + + + + ```ts + const { data, error } = await supabase.rpc('hello_world') + ``` + + + + + ```json + { + "data": "Hello world", + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true - description: This is an example of invoking a Postgres function with no parameters. js: | - ```js - const { data, error } = await supabase - .rpc('hello_world') - ``` - - name: With Parameters - js: | - ```js - const { data, error } = await supabase - .rpc('echo_city', { name: 'The Shire' }) + ```ts + const { data, error } = await supabase.rpc('hello_world') ``` + - name: Call a Postgres function with arguments + description: | + + + + ```sql + create function echo(say text) returns text as $$ + select say; + $$ language sql; + ``` + + + + + ```ts + const { data, error } = await supabase.rpc('echo', { say: '👋' }) + ``` + + + + + ```json + { + "data": "👋", + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true - name: Bulk processing - description: You can process large payloads at once using [array parameters](https://postgrest.org/en/stable/api.html#calling-functions-with-array-parameters). - js: | - ```js - const { data, error } = await postgrest - .rpc('echo_cities', { names: ['The Shire', 'Mordor'] }) - ``` - - name: With filters + description: | + You can process large payloads by passing in an array as an argument: + + + + + ```sql + create function add_one_each(arr int[]) returns int[] as $$ + select array_agg(n + 1) from unnest(arr) as n; + $$ language sql; + ``` + + + + + ```ts + const { data, error } = await supabase.rpc('add_one_each', { arr: [1, 2, 3] }) + ``` + + + + + ```json + { + "data": [ + 2, + 3, + 4 + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + - name: Call a Postgres function with filters description: | Postgres functions that return tables can also be combined with - [Modifiers](/docs/reference/javascript/using-modifiers) and - [Filters](/docs/reference/javascript/using-filters). - js: | - ```js - const { data, error } = await supabase - .rpc('echo_all_cities') - .select('name, population') - .eq('name', 'The Shire') - ``` - - name: With count option - description: | - You can specify a count option to get the row count along with your data. - Allowed values for count option are `null`, `exact`, `planned` and `estimated`. - js: | - ```js - const { data, error, count } = await supabase - .rpc('hello_world', {}, { count: 'exact' }) - ``` + [Filters](/docs/reference/javascript/next/using-filters) and + [Modifiers](/docs/reference/javascript/next/using-modifiers). + + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'France'), + (2, 'United Kingdom'); + + create function list_stored_countries() returns setof countries as $$ + select * from countries; + $$ language sql; + ``` + + + + + ```ts + const { data, error } = await supabase + .rpc('list_stored_countries') + .eq('id', 1) + .single() + ``` + + + + + ```json + { + "data": { + "id": 1, + "name": "France" + }, + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true Using Filters: description: | - Filters can be used on `select()`, `update()`, and `delete()` queries. + Filters allow you to only return rows that match certain conditions. + + Filters can be used on `select()`, `update()`, `upsert()`, and `delete()` queries. If a Postgres function returns a table response, you can also apply filters. ### Applying Filters - You must apply your filters to the end of your query. For example: + Filters must be applied after any of `select()`, `update()`, `upsert()`, + `delete()`, and `rpc()` and before + [modifiers](/docs/reference/javascript/next/using-modifiers). - ```js + ```ts const { data, error } = await supabase .from('cities') .select('name, country_id') @@ -999,9 +2037,10 @@ pages: ### Chaining - Filters can be chained together to produce advanced queries. For example: + Filters can be chained together to produce advanced queries. For example, + to query cities with population between 1,000 and 10,000: - ```js + ```ts const { data, error } = await supabase .from('cities') .select('name, country_id') @@ -1013,7 +2052,7 @@ pages: Filters can be built up one step at a time and then executed. For example: - ```js + ```ts const filterByName = null const filterPopLow = 1000 const filterPopHigh = 10000 @@ -1029,336 +2068,689 @@ pages: const { data, error } = await query ``` + ### Filter by values within a JSON column + + + + + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Michael', '{ "postcode": 90210 }'), + (2, 'Jane', null); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('users') + .select() + .eq('address->postcode', 90210) + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Michael", + "address": { + "postcode": 90210 + } + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + + ### Filter Foreign Tables + + You can filter on foreign tables in your `select()` query using dot + notation: + + + + + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select(` + name, + cities!inner ( + name + ) + `) + .eq('cities.name', 'Bali') + ``` + + + + + ```json + { + "data": [ + { + "name": "Indonesia", + "cities": [ + { + "name": "Bali" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + eq(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.eq' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .eq('name', 'Albania') + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .eq('name', 'The shire') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .eq('name', 'San Francisco') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .eq('name', 'Mordor') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .eq('name', 'San Francisco') + .from('countries') + .select() + .eq('name', 'Albania') ``` neq(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.neq' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .neq('name', 'Albania') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Afghanistan" + }, + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .neq('name', 'The shire') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .neq('name', 'San Francisco') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .neq('name', 'Mordor') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .neq('name', 'Lagos') + .from('countries') + .select() + .neq('name', 'Albania') ``` gt(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.gt' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .gt('id', 2) + ``` + + + + + ```json + { + "data": [ + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .gt('country_id', 250) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .gt('country_id', 250) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .gt('country_id', 250) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .gt('country_id', 250) + .from('countries') + .select() + .gt('id', 2) ``` gte(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.gte' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .gte('id', 2) + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "name": "Albania" + }, + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .gte('country_id', 250) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .gte('country_id', 250) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .gte('country_id', 250) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .gte('country_id', 250) + .from('countries') + .select() + .gte('id', 2) ``` lt(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.lt' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .lt('id', 2) + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Afghanistan" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .lt('country_id', 250) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .lt('country_id', 250) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .lt('country_id', 250) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .lt('country_id', 250) + .from('countries') + .select() + .lt('id', 2) ``` lte(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.lte' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .lte('id', 2) + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Afghanistan" + }, + { + "id": 2, + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .lte('country_id', 250) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .lte('country_id', 250) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .lte('country_id', 250) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .lte('country_id', 250) + .from('countries') + .select() + .lte('id', 2) ``` like(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.like' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .like('name', '%Alba%') + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .like('name', '%la%') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .like('name', '%la%') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .like('name', '%la%') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .like('name', '%la%') + .from('countries') + .select() + .like('name', '%Alba%') ``` ilike(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.ilike' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .like('name', '%alba%') + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .ilike('name', '%la%') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .ilike('name', '%la%') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .ilike('name', '%la%') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .ilike('name', '%la%') + .from('countries') + .select() + .like('name', '%alba%') ``` is(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.is' examples: - - name: With `select()` + - name: Checking nullness + description: | + Using the `eq()` filter doesn't work when filtering for `null`: + + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'null'), + (2, null); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .eq('name', null) + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "null" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + + Instead, you need to use `is()`: + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .is('name', null) + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "name": null + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .is('name', null) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .is('name', null) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .is('name', null) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') + .from('countries') + .select() .is('name', null) ``` @@ -1366,369 +2758,849 @@ pages: $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.in' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .in('name', ['Albania', 'Algeria']) + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "name": "Albania" + }, + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .in('name', ['Rio de Janeiro', 'San Francisco']) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .in('name', ['Rio de Janeiro', 'San Francisco']) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .in('name', ['Rio de Janeiro', 'San Francisco']) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .in('name', ['Rio de Janeiro', 'San Francisco']) + .from('countries') + .select() + .in('name', ['Albania', 'Algeria']) ``` contains(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.contains' - notes: | - - `.contains()` can work on array columns or range columns. - It is very useful for finding rows where a tag array contains all the values in the filter array. - - ```js - .contains('arraycol',["a","b"]) // You can use a javascript array for an array column - .contains('arraycol','{"a","b"}') // You can use a string with Postgres array {} for array column. - .contains('rangecol','(1,2]') // Use Postgres range syntax for range column. - .contains('rangecol',`(${arr}]`) // You can insert an array into a string. - ``` examples: - - name: With `select()` + - name: On array columns + description: | + + + + ```sql + create table + issues ( + id int8 primary key, + title text, + tags text[] + ); + + insert into + issues (id, title, tags) + values + (1, 'Cache invalidation is not working', array['is:open', 'severity:high', 'priority:low']), + (2, 'Use better names', array['is:open', 'severity:low', 'priority:medium']); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('issues') + .select('title') + .contains('tags', ['is:open', 'severity:high']) + ``` + + + + + ```json + { + "data": [ + { + "title": "Cache invalidation is not working" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, main_exports') - .contains('main_exports', ['oil']) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .contains('main_exports', ['oil']) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .contains('main_exports', ['oil']) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .contains('main_exports', ['oil']) + .from('users') + .select() + .contains('name', ['is:online', 'faction:red']) ``` + - name: On range columns + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .contains('during', '[2000-01-01 13:00, 2000-01-01 13:30)') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + - name: On `jsonb` columns + description: | + + + + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Michael', '{ "postcode": 90210, "street": "Melrose Place" }'), + (2, 'Jane', '{}'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('users') + .select('name') + .contains('address', { postcode: 90210 }) + ``` + + + + + ```json + { + "data": [ + { + "name": "Michael" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true containedBy(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.containedBy' - notes: | - - `.containedBy()` can work on array columns or range columns. - - ```js - .containedBy('arraycol',["a","b"]) // You can use a javascript array for an array column - .containedBy('arraycol','{"a","b"}') // You can use a string with Postgres array {} for array column. - .containedBy('rangecol','(1,2]') // Use Postgres range syntax for range column. - .containedBy('rangecol',`(${arr}]`) // You can insert an array into a string. - ``` examples: - - name: With `select()` + - name: On array columns + description: | + + + + ```sql + create table + classes ( + id int8 primary key, + name text, + days text[] + ); + + insert into + classes (id, name, days) + values + (1, 'Chemistry', array['monday', 'friday']), + (2, 'History', array['monday', 'wednesday', 'thursday']); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('classes') + .select('name') + .containedBy('days', ['monday', 'tuesday', 'wednesday', 'friday']) + ``` + + + + + ```json + { + "data": [ + { + "name": "Chemistry" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, main_exports') - .containedBy('main_exports', ['cars', 'food', 'machine']) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .containedBy('main_exports', ['orks', 'surveillance', 'evil']) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .containedBy('main_exports', ['cars', 'food', 'machine']) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .containedBy('main_exports', ['cars', 'food', 'machine']) + .from('classes') + .select('name') + .containedBy('days', ['monday', 'tuesday', 'wednesday', 'friday']) ``` + - name: On range columns + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .containedBy('during', '[2000-01-01 00:00, 2000-01-01 23:59)') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + - name: On `jsonb` columns + description: | + + + + ```sql + create table + users ( + id int8 primary key, + name text, + address jsonb + ); + + insert into + users (id, name, address) + values + (1, 'Michael', '{ "postcode": 90210, "street": "Melrose Place" }'), + (2, 'Jane', '{}'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('users') + .select('name') + .containedBy('address', {}) + ``` + + + + + ```json + { + "data": [ + { + "name": "Jane" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true rangeGt(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeGt' examples: - name: With `select()` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .rangeGt('during', '[2000-01-02 08:00, 2000-01-02 09:00)') + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "room_name": "Topaz", + "during": "[\"2000-01-02 09:00:00\",\"2000-01-02 10:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, population_range_millions') - .rangeGt('population_range_millions', '[150, 250]') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .rangeGt('population_range_millions', '[150, 250]') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .rangeGt('population_range_millions', '[150, 250]') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .rangeGt('population_range_millions', '[150, 250]') + .from('reservations') + .select() + .rangeGt('during', '[2000-01-02 08:00, 2000-01-02 09:00)') ``` rangeGte(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeGte' examples: - name: With `select()` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .rangeGte('during', '[2000-01-02 08:30, 2000-01-02 09:30)') + ``` + + + + + ```json + { + "data": [ + { + "id": 2, + "room_name": "Topaz", + "during": "[\"2000-01-02 09:00:00\",\"2000-01-02 10:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, population_range_millions') - .rangeGte('population_range_millions', '[150, 250]') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .rangeGte('population_range_millions', '[150, 250]') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .rangeGte('population_range_millions', '[150, 250]') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .rangeGte('population_range_millions', '[150, 250]') + .from('reservations') + .select() + .rangeGte('during', '[2000-01-02 08:30, 2000-01-02 09:30)') ``` rangeLt(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeLt' examples: - name: With `select()` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .rangeLt('during', '[2000-01-01 15:00, 2000-01-01 16:00)') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, population_range_millions') - .rangeLt('population_range_millions', '[150, 250]') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .rangeLt('population_range_millions', '[150, 250]') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .rangeLt('population_range_millions', '[150, 250]') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .rangeLt('population_range_millions', '[150, 250]') + .from('reservations') + .select() + .rangeLt('during', '[2000-01-01 15:00, 2000-01-01 16:00)') ``` rangeLte(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeLte' examples: - name: With `select()` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .rangeLte('during', '[2000-01-01 14:00, 2000-01-01 16:00)') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, population_range_millions') - .rangeLte('population_range_millions', '[150, 250]') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .rangeLte('population_range_millions', '[150, 250]') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .rangeLte('population_range_millions', '[150, 250]') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .rangeLte('population_range_millions', '[150, 250]') + .from('reservations') + .select() + .rangeLte('during', '[2000-01-01 14:00, 2000-01-01 16:00)') ``` rangeAdjacent(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.rangeAdjacent' examples: - name: With `select()` + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .rangeAdjacent('during', '[2000-01-01 12:00, 2000-01-01 13:00)') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, population_range_millions') - .rangeAdjacent('population_range_millions', '[70, 185]') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .rangeAdjacent('population_range_millions', '[70, 185]') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .rangeAdjacent('population_range_millions', '[70, 185]') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .rangeAdjacent('population_range_millions', '[70, 185]') + .from('reservations') + .select() + .rangeAdjacent('during', '[2000-01-01 12:00, 2000-01-01 13:00)') ``` overlaps(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.overlaps' - notes: | - - `.overlaps()` can work on array columns or range columns. - - ```js - .overlaps('arraycol',["a","b"]) // You can use a javascript array for an array column - .overlaps('arraycol','{"a","b"}') // You can use a string with Postgres array {} for array column. - .overlaps('rangecol','(1,2]') // Use Postgres range syntax for range column. - .overlaps('rangecol',`(${arr}]`) // You can insert an array into a string. - ``` examples: - - name: With `select()` + - name: On array columns + description: | + + + + ```sql + create table + issues ( + id int8 primary key, + title text, + tags text[] + ); + + insert into + issues (id, title, tags) + values + (1, 'Cache invalidation is not working', array['is:open', 'severity:high', 'priority:low']), + (2, 'Use better names', array['is:open', 'severity:low', 'priority:medium']); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('issues') + .select('title') + .overlaps('tags', ['is:closed', 'severity:high']) + ``` + + + + + ```json + { + "data": [ + { + "title": "Cache invalidation is not working" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('countries') - .select('name, id, main_exports') - .overlaps('main_exports', ['computers', 'minerals']) - ``` - - name: With `update()` - js: | - ```js - let countries = await supabase - .from('countries') - .update({ name: 'Mordor' }) - .overlaps('main_exports', ['computers', 'minerals']) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('countries') - .delete() - .overlaps('main_exports', ['computers', 'minerals']) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_countries') - .overlaps('main_exports', ['computers', 'minerals']) + .from('issues') + .select('title') + .overlaps('tags', ['is:closed', 'severity:high']) ``` + - name: On range columns + description: | + Postgres supports a number of [range + types](https://www.postgresql.org/docs/current/rangetypes.html). You + can filter on range columns using the string representation of range + values. + + + + ```sql + create table + reservations ( + id int8 primary key, + room_name text, + during tsrange + ); + + insert into + reservations (id, room_name, during) + values + (1, 'Emerald', '[2000-01-01 13:00, 2000-01-01 15:00)'), + (2, 'Topaz', '[2000-01-02 09:00, 2000-01-02 10:00)'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('reservations') + .select() + .overlaps('during', '[2000-01-01 12:45, 2000-01-01 13:15)') + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "room_name": "Emerald", + "during": "[\"2000-01-01 13:00:00\",\"2000-01-01 15:00:00\")" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + + # TODO: schema & result textSearch(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.textSearch' + notes: + - For more information, see [Postgres full text search](/docs/guides/database/full-text-search). examples: - name: Text search js: | - ```js + ```ts const { data, error } = await supabase .from('quotes') .select('catchphrase') @@ -1739,7 +3611,7 @@ pages: - name: Basic normalization description: Uses PostgreSQL's `plainto_tsquery` function. js: | - ```js + ```ts const { data, error } = await supabase .from('quotes') .select('catchphrase') @@ -1751,7 +3623,7 @@ pages: - name: Full normalization description: Uses PostgreSQL's `phraseto_tsquery` function. js: | - ```js + ```ts const { data, error } = await supabase .from('quotes') .select('catchphrase') @@ -1772,7 +3644,7 @@ pages: - `-`: a dash will be converted to the ! operator. js: | - ```js + ```ts const { data, error } = await supabase .from('quotes') .select('catchphrase') @@ -1786,259 +3658,937 @@ pages: $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.match' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + .match({ id: 2, name: 'Albania' }) + ``` + + + + + ```json + { + "data": [ + { + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .match({name: 'Beijing', country_id: 156}) - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .match({name: 'Beijing', country_id: 156}) - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .match({name: 'Beijing', country_id: 156}) - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .match({name: 'Beijing', country_id: 156}) + .from('countries') + .select('name') + .match({ id: 2, name: 'Albania' }) ``` not(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.not' - notes: | - - `.not()` expects you to use the raw [PostgREST syntax](https://postgrest.org/en/stable/api.html#horizontal-filtering-rows) for the filter names and values. - - ```js - .not('name','eq','Paris') - .not('arraycol','cs','{"a","b"}') // Use Postgres array {} for array column and 'cs' for contains. - .not('rangecol','cs','(1,2]') // Use Postgres range syntax for range column. - .not('id','in','(6,7)') // Use Postgres list () for in filter. - .not('id','in',`(${arr})`) // You can insert a javascript array. - ``` examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'null'), + (2, null); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .not('name', 'is', null) + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "null" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .not('name', 'eq', 'Paris') - ``` - - name: With `update()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .not('name', 'eq', 'Paris') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .not('name', 'eq', 'Paris') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .not('name', 'eq', 'Paris') + .from('countries') + .select() + .not('name', 'is', null) ``` or(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.or' - notes: | - - `.or()` expects you to use the raw [PostgREST syntax](https://postgrest.org/en/stable/api.html#horizontal-filtering-rows) for the filter names and values. - - ```js - .or('id.in.(6,7), arraycol.cs.{"a","b"}') // Use Postgres list () for in filter. Array {} for array column and 'cs' for contains. - .or(`id.in.(${arrList}),arraycol.cs.{${arr}}`) // You can insert a javascipt array for list or array on array column. - .or(`id.in.(${arrList}),rangecol.cs.[${arrRange})`) // You can insert a javascipt array for list or range on a range column. - ``` examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + .or('id.eq.2,name.eq.Algeria') + ``` + + + + + ```json + { + "data": [ + { + "name": "Albania" + }, + { + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js - const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .or('id.eq.20,id.eq.30') - ``` - - name: Use `or` with `and` - isSpotlight: true - js: | - ```js - const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .or('id.gt.20,and(name.eq.New Zealand,name.eq.France)') - ``` - - name: Use `or` on foreign tables - isSpotlight: true - js: | - ```js + ```ts const { data, error } = await supabase .from('countries') - .select('id, cities(*)') - .or('name.eq.Wellington,name.eq.Paris', { foreignTable: "cities" }) + .select('name') + .or('id.eq.2,name.eq.Algeria') ``` + - name: Use `or` with `and` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + .or('id.gt.3,and(id.eq.1,name.eq.Afghanistan)') + ``` + + + + + + ```json + { + "data": [ + { + "name": "Afghanistan" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + - name: Use `or` on foreign tables + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select(` + name, + cities!inner ( + name + ) + `) + .or('country_id.eq.1,name.eq.Beijing', { foreignTable: 'cities' }) + ``` + + + + + ```json + { + "data": [ + { + "name": "Germany", + "cities": [ + { + "name": "Munich" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true filter(): $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.filter' - notes: | - - `.filter()` expects you to use the raw [PostgREST syntax](https://postgrest.org/en/stable/api.html#horizontal-filtering-rows) for the filter names and values, so it should only be used as an escape hatch in case other filters don't work. - ```js - .filter('arraycol','cs','{"a","b"}') // Use Postgres array {} for array column and 'cs' for contains. - .filter('rangecol','cs','(1,2]') // Use Postgres range syntax for range column. - .filter('id','in','(6,7)') // Use Postgres list () for in filter. - .filter('id','in',`(${arr})`) // You can insert a javascript array. - ``` examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .filter('name', 'in', '("Algeria","Japan")') + ``` + + + + + ```json + { + "data": [ + { + "id": 3, + "name": "Algeria" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .filter('name', 'in', '("Paris","Tokyo")') + .from('countries') + .select() + .filter('name', 'in', '("Algeria","Japan")') ``` - - name: With `update()` + - name: On a foreign table + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'Germany'), + (2, 'Indonesia'); + insert into + cities (id, country_id, name) + values + (1, 2, 'Bali'), + (2, 1, 'Munich'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select(` + name, + cities!inner ( + name + ) + `) + .filter('cities.name', 'eq', 'Bali') + ``` + + + + + ```json + { + "data": [ + { + "name": "Indonesia", + "cities": [ + { + "name": "Bali" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .update({ name: 'Mordor' }) - .filter('name', 'in', '("Paris","Tokyo")') - ``` - - name: With `delete()` - js: | - ```js - const { data, error } = await supabase - .from('cities') - .delete() - .filter('name', 'in', '("Paris","Tokyo")') - ``` - - name: With `rpc()` - js: | - ```js - // Only valid if the Postgres function returns a table type. - const { data, error } = await supabase - .rpc('echo_all_cities') - .filter('name', 'in', '("Paris","Tokyo")') - ``` - - name: Filter embedded resources - js: | - ```js - const { data, error } = await supabase - .from('cities') - .select('name, countries ( name )') - .filter('countries.name', 'in', '("France","Japan")') + .from('countries') + .select(` + name, + cities!inner ( + name + ) + `) + .filter('cities.name', 'eq', 'Bali') ``` Using Modifiers: description: | - Modifiers can be used on `select()` queries. + Filters work on the row level—they allows you to return rows that + only match certain conditions without changing the shape of the rows. + Modifiers are everything that don't fit that definition—allowing you to + change the format of the response (e.g., returning a CSV string). - If a Postgres function returns a table response, you can also apply modifiers to the `rpc()` function. + Modifiers must be specified after filters. Some modifiers only apply for + queries that return rows (e.g., `select()` or `rpc()` on a function that + returns a table response). db.modifiers.select(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.select' title: select() + examples: + - name: With `upsert()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .upsert({ id: 1, name: 'Albania' }) + ``` + + + + + ```json + { + "status": 201, + "statusText": "Created" + } + ``` + + + + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .upsert({ id: 1, name: 'Algeria' }) + .select() + ``` + + + + + ```json + { + "data": [ + { + "id": 1, + "name": "Algeria" + } + ], + "status": 201, + "statusText": "Created" + } + ``` + + + + hideCodeBlock: true + isSpotlight: true + js: | + ```ts + const { data, error } = await supabase + .from('countries') + .upsert({ id: 1, name: 'Algeria' }) + .select() + ``` order(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.order' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .order('name', { ascending: false }) + ``` + + + + + ```json + { + "data": [ + { + "id": 3, + "name": "Algeria" + }, + { + "id": 2, + "name": "Albania" + }, + { + "id": 1, + "name": "Afghanistan" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase .from('cities') .select('name', 'country_id') .order('id', { ascending: false }) ``` - - name: With embedded resources - js: | - ```js - const { data, error } = await supabase - .from('countries') - .select('name, cities(name)') - .eq('name', 'United States') - .order('name', {foreignTable: 'cities'}) - ``` + - name: On a foreign table + description: | + Ordering on foreign tables doesn't affect the ordering of + the parent table. + + + + + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'United States'), + (2, 'Vanuatu'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Atlanta'), + (2, 1, 'New York City'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select(` + name, + cities ( + name + ) + `) + .order('name', { foreignTable: 'cities', ascending: false }) + ``` + + + + + ```json + { + "data": [ + { + "name": "United States", + "cities": [ + { + "name": "New York City" + }, + { + "name": "Atlanta" + } + ] + }, + { + "name": "Vanuatu", + "cities": [] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true limit(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.limit' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + .limit(1) + ``` + + + + + ```json + { + "data": [ + { + "name": "Afghanistan" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js - const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .limit(1) - ``` - - name: With embedded resources - js: | - ```js + ```ts const { data, error } = await supabase .from('countries') - .select('name, cities(name)') - .eq('name', 'United States') - .limit(1, { foreignTable: 'cities' }) + .select('name') + .limit(1) ``` + - name: On a foreign table + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + create table + cities ( + id int8 primary key, + country_id int8 not null references countries, + name text + ); + + insert into + countries (id, name) + values + (1, 'United States'); + insert into + cities (id, country_id, name) + values + (1, 1, 'Atlanta'), + (2, 1, 'New York City'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select(` + name, + cities ( + name + ) + `) + .limit(1, { foreignTable: 'cities' }) + ``` + + + + + ```json + { + "data": [ + { + "name": "United States", + "cities": [ + { + "name": "Atlanta" + } + ] + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true range(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.range' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + .range(0, 1) + ``` + + + + + ```json + { + "data": [ + { + "name": "Afghanistan" + }, + { + "name": "Albania" + } + ], + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') - .range(0,3) + .from('countries') + .select('name') + .range(0, 1) ``` db.abortSignal(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.abortSignal' title: abortSignal() + examples: + - name: Aborting requests in-flight + description: | + You can use an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to abort requests. Note that `status` and `statusText` don't mean anything for aborted requests as the request wasn't fulfilled. + + + + + ```ts + const ac = new AbortController() + ac.abort() + const { data, error } = await supabase + .from('very_big_table') + .select() + .abortSignal(ac.signal) + ``` + + + + + ```json + { + "error": { + "message": "FetchError: The user aborted a request.", + "details": "", + "hint": "", + "code": "" + }, + "status": 400, + "statusText": "Bad Request" + } + ``` + + + + hideCodeBlock: true + isSpotlight: true + js: | + ```ts + const ac = new AbortController() + ac.abort() + const { data, error } = await supabase + .from('very_big_table') + .select() + .abortSignal(ac.signal) + ``` single(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.single' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select('name') + .limit(1) + .single() + ``` + + + + + ```json + { + "data": { + "name": "Afghanistan" + }, + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') + .from('countries') + .select('name') .limit(1) .single() ``` @@ -2047,12 +4597,52 @@ pages: $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.maybeSingle' examples: - name: With `select()` + description: | + + + + ```sql + create table + countries (id int8 primary key, name text); + + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .eq('name', 'Singapore') + .maybeSingle() + ``` + + + + + ```json + { + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true isSpotlight: true js: | - ```js + ```ts const { data, error } = await supabase - .from('cities') - .select('name, country_id') + .from('countries') + .select() .eq('name', 'Singapore') .maybeSingle() ``` @@ -2060,18 +4650,140 @@ pages: db.csv(): $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.csv' title: csv() + examples: + - name: Return data as CSV + description: | + By default, the data is returned in JSON format, but can also be returned as Comma Separated Values. - db.geojson(): - $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.geojson' - title: geojson() + + - db.explain(): - $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.explain' - title: explain() + ```sql + create table + countries (id int8 primary key, name text); - db.rollback(): - $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.rollback' - title: rollback() + insert into + countries (id, name) + values + (1, 'Afghanistan'), + (2, 'Albania'), + (3, 'Algeria'); + ``` + + + + + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .csv() + ``` + + + + + ```json + { + "data": "id,name\n1,Afghanistan\n2,Albania\n3,Algeria", + "status": 200, + "statusText": "OK" + } + ``` + + + + hideCodeBlock: true + isSpotlight: true + js: | + ```ts + const { data, error } = await supabase + .from('countries') + .select() + .csv() + ``` + + # NOTE: Not available on currently deployed PostgREST + # db.geojson(): + # $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.geojson' + # title: geojson() + + # NOTE: Not available on currently deployed PostgREST + # db.explain(): + # $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.explain' + # title: explain() + + # NOTE: Not available on currently deployed PostgREST + # db.rollback(): + # $ref: '@supabase/postgrest-js.PostgrestTransformBuilder.rollback' + # title: rollback() + # examples: + # - name: With `delete()` + # description: | + #
+ # Schema + + # ```sql + # create table + # countries (id int8 primary key, name text); + + # insert into + # countries (id, name) + # values + # (1, 'Spain'); + # ``` + #
+ + # ```ts + # const { error } = await supabase + # .from('countries') + # .delete() + # .eq('id', 1) + # .rollback() + # ``` + + #
+ # Result + + # ```json + # { + # "status": 204, + # "statusText": "No Content" + # } + # ``` + #
+ + # ```ts + # const { data, error } = await supabase + # .from('countries') + # .select() + # ``` + + #
+ # Result + + # ```json + # { + # "data": [ + # { + # "id": 1, + # "name": "Spain" + # } + # ], + # "status": 200, + # "statusText": "OK" + # } + # ``` + #
+ # hideCodeBlock: true + # isSpotlight: true + # js: | + # ```ts + # const { error } = await supabase + # .from('countries') + # .delete() + # .eq('id', 1) + # ``` invoke(): title: 'invoke()' diff --git a/studio/.env b/studio/.env index db284f3411e..478baf5dc20 100644 --- a/studio/.env +++ b/studio/.env @@ -1,6 +1,9 @@ STUDIO_PG_META_URL=http://localhost:8000/pg POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password +DEFAULT_ORGANIZATION_NAME=Default Organization +DEFAULT_PROJECT_NAME=Default Project + SUPABASE_URL=http://localhost:8000 SUPABASE_REST_URL=http://localhost:8000/rest/v1/ SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE diff --git a/studio/Dockerfile b/studio/Dockerfile index dd58253ec2a..9ff4a0ca6fc 100644 --- a/studio/Dockerfile +++ b/studio/Dockerfile @@ -6,25 +6,41 @@ # docker build --target production --no-cache -t supabase/studio:latest . # docker builder prune - -FROM node:14-slim as base -RUN apt-get update && apt-get install -y \ +FROM node:16-slim as base +RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ build-essential \ && rm -rf /var/lib/apt/lists/* + +FROM base as builder WORKDIR /usr/src/app -# Do `npm ci` separately so we can cache `node_modules` -# https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ -COPY package*.json ./ -RUN npm clean-install && npm cache clean --force -COPY . . +COPY package*.json . +COPY turbo.json . +COPY packages packages +COPY studio studio +# the following command is still unsupported for npm // https://github.com/vercel/turborepo/issues/1830 +# RUN npm i -g turbo +# RUN turbo prune --scope=studio --docker +# RUN npm clean-install && npm cache clean --force ENTRYPOINT ["docker-entrypoint.sh"] -FROM base as dev +FROM builder as dev +COPY --from=builder /usr/src/app ./ +RUN npm clean-install && npm cache clean --force EXPOSE 8082 -CMD ["npm", "run", "dev"] +CMD ["npx", "turbo", "run", "dev", "--filter=studio"] -FROM base as production -RUN npm run build && rm -rf .next/cache/webpack && npm prune --production +FROM builder as productionprep +WORKDIR /app +COPY --from=builder /usr/src/app ./ +RUN npm clean-install \ + && npm cache clean --force \ + && npx turbo run build --scope=studio --include-dependencies --no-deps \ + && rm -rf ./studio/.next/cache/webpack \ + && npm prune --production + +FROM node:16-slim as production +COPY --from=productionprep /app /app +WORKDIR /app/studio EXPOSE 3000 CMD ["npm", "run", "start"] diff --git a/studio/README.md b/studio/README.md index 6613fc80390..7dd002c04c3 100644 --- a/studio/README.md +++ b/studio/README.md @@ -67,3 +67,10 @@ Then run the following commands to install dependencies and start the dashboard. npm install npm run dev ``` + +If you would like to configure different defaults for "Default Organization" and "Default Project", you will need to update the `.env` in the studio folder with the corresponding values. + +``` +DEFAULT_ORGANIZATION_NAME= +DEFAULT_PROJECT_NAME= +``` diff --git a/studio/components/grid/SupabaseGrid.utils.ts b/studio/components/grid/SupabaseGrid.utils.ts index 14ece39471e..60731c00291 100644 --- a/studio/components/grid/SupabaseGrid.utils.ts +++ b/studio/components/grid/SupabaseGrid.utils.ts @@ -79,10 +79,20 @@ export function initTable( ? onLoadStorage(props.storageRef, table.name, table.schema) : undefined - // Load sort and filters via URL param only if given + // Check for saved state on initial load and also, load sort and filters via URL param only if given // Otherwise load from local storage to resume user session - if (sort === undefined && filter === undefined && (savedState?.sorts || savedState?.filters)) { - return { savedState: { sorts: savedState.sorts, filters: savedState.filters } } + if ( + !state.isInitialComplete && + sort === undefined && + filter === undefined && + (savedState?.sorts || savedState?.filters) + ) { + return { + savedState: { + sorts: savedState.sorts, + filters: savedState.filters, + }, + } } const gridColumns = getGridColumns(table, { diff --git a/studio/components/grid/components/common/BlockKeys.tsx b/studio/components/grid/components/common/BlockKeys.tsx index 4a8007326c8..c64ef804e2d 100644 --- a/studio/components/grid/components/common/BlockKeys.tsx +++ b/studio/components/grid/components/common/BlockKeys.tsx @@ -1,9 +1,10 @@ -import * as React from 'react' +import { FC, useCallback, KeyboardEvent, ReactNode } from 'react' -type BlockKeysProps = { +interface BlockKeysProps { + value: string | null + children: ReactNode onEscape?: (value: string | null) => void onEnter?: (value: string | null) => void - value: string | null } /** @@ -11,9 +12,9 @@ type BlockKeysProps = { * We use this with cell editor to allow editor component to handle keys. * Example: press enter to add newline on textEditor */ -export const BlockKeys: React.FC = ({ onEscape, onEnter, value, children }) => { - const handleKeyDown = React.useCallback( - (ev: React.KeyboardEvent) => { +export const BlockKeys: FC = ({ value, children, onEscape, onEnter }) => { + const handleKeyDown = useCallback( + (ev: KeyboardEvent) => { switch (ev.key) { case 'Escape': ev.stopPropagation() @@ -21,7 +22,10 @@ export const BlockKeys: React.FC = ({ onEscape, onEnter, value, break case 'Enter': ev.stopPropagation() - if (onEnter) onEnter(value) + if (!ev.shiftKey && onEnter) { + ev.preventDefault() + onEnter(value) + } break } }, @@ -29,7 +33,7 @@ export const BlockKeys: React.FC = ({ onEscape, onEnter, value, ) function onBlur() { - if (onEscape) onEscape(value) + if (onEnter) onEnter(value) } return ( diff --git a/studio/components/grid/components/editor/JsonEditor.tsx b/studio/components/grid/components/editor/JsonEditor.tsx index 66c3fa1e65d..7eaa0379a8f 100644 --- a/studio/components/grid/components/editor/JsonEditor.tsx +++ b/studio/components/grid/components/editor/JsonEditor.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useCallback } from 'react' import { EditorProps } from '@supabase/react-data-grid' import { useTrackedState } from '../../store' @@ -11,18 +11,22 @@ export function JsonEditor({ onRowChange, }: EditorProps) { const state = useTrackedState() - const [isPopoverOpen, setIsPopoverOpen] = React.useState(true) + const [isPopoverOpen, setIsPopoverOpen] = useState(true) const gridColumn = state.gridColumns.find((x) => x.name == column.key) const initialValue = row[column.key as keyof TRow] as unknown const jsonString = initialValue ? JSON.stringify(initialValue) : '' const prettyJsonValue = prettifyJSON(jsonString) - const [value, setValue] = React.useState(prettyJsonValue) + const [value, setValue] = useState(prettyJsonValue) - const onEscape = React.useCallback((newValue: string | null) => { - commitChange(newValue) + const cancelChanges = useCallback(() => { + onRowChange(row, true) setIsPopoverOpen(false) }, []) + const saveChanges = useCallback((newValue: string | null) => { + commitChange(newValue) + }, []) + function onChange(_value: string | undefined) { if (!_value || _value == '') setValue(null) else setValue(_value) @@ -31,12 +35,14 @@ export function JsonEditor({ function commitChange(newValue: string | null) { if (!newValue) { onRowChange({ ...row, [column.key]: null }, true) + setIsPopoverOpen(false) } else if (verifyJSON(newValue)) { const jsonValue = JSON.parse(newValue) onRowChange({ ...row, [column.key]: jsonValue }, true) + setIsPopoverOpen(false) } else { const { onError } = state - if (onError) onError(Error('invalid input')) + if (onError) onError(Error('Please enter a valid JSON')) } } @@ -48,13 +54,17 @@ export function JsonEditor({ sideOffset={-35} className="rounded-none" overlay={ - + +
+

Save changes

+ ⏎ +
} > diff --git a/studio/components/grid/components/editor/TextEditor.tsx b/studio/components/grid/components/editor/TextEditor.tsx index 00dd5cd671c..7b122efc5d8 100644 --- a/studio/components/grid/components/editor/TextEditor.tsx +++ b/studio/components/grid/components/editor/TextEditor.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useCallback } from 'react' import { EditorProps } from '@supabase/react-data-grid' import { useTrackedState } from '../../store' import { BlockKeys, MonacoEditor, NullValue, EmptyValue } from '../common' @@ -10,12 +10,17 @@ export function TextEditor({ onRowChange, }: EditorProps) { const state = useTrackedState() - const [isPopoverOpen, setIsPopoverOpen] = React.useState(true) + const [isPopoverOpen, setIsPopoverOpen] = useState(true) const gridColumn = state.gridColumns.find((x) => x.name == column.key) const initialValue = row[column.key as keyof TRow] as unknown as string - const [value, setValue] = React.useState(initialValue) + const [value, setValue] = useState(initialValue) - const onEscape = React.useCallback((newValue: string | null) => { + const cancelChanges = useCallback(() => { + onRowChange(row, true) + setIsPopoverOpen(false) + }, []) + + const saveChanges = useCallback((newValue: string | null) => { onRowChange({ ...row, [column.key]: newValue }, true) setIsPopoverOpen(false) }, []) @@ -33,12 +38,16 @@ export function TextEditor({ sideOffset={-35} className="rounded-none" overlay={ - + +
+

Save changes

+ ⏎ +
} > diff --git a/studio/components/interfaces/Auth/Users/UserDropdown.tsx b/studio/components/interfaces/Auth/Users/UserDropdown.tsx index 13b7601ee43..e5584fea741 100644 --- a/studio/components/interfaces/Auth/Users/UserDropdown.tsx +++ b/studio/components/interfaces/Auth/Users/UserDropdown.tsx @@ -156,11 +156,11 @@ const UserDropdown: FC = ({ user, canRemoveUser }) => {
- + You need additional permissions to delete users
diff --git a/studio/components/interfaces/Auth/Users/Users.tsx b/studio/components/interfaces/Auth/Users/Users.tsx index 406d3c76955..a54cf2cd73e 100644 --- a/studio/components/interfaces/Auth/Users/Users.tsx +++ b/studio/components/interfaces/Auth/Users/Users.tsx @@ -53,8 +53,8 @@ const Users = () => { } return ( -
-
+
+
{
-
+
-
-
-
+
+
+
diff --git a/studio/components/interfaces/Auth/Users/UsersList.tsx b/studio/components/interfaces/Auth/Users/UsersList.tsx index def1468399a..998516b6d58 100644 --- a/studio/components/interfaces/Auth/Users/UsersList.tsx +++ b/studio/components/interfaces/Auth/Users/UsersList.tsx @@ -20,10 +20,10 @@ const UsersList = ({}) => { <> Email Phone - Provider - Created - Last Sign In - User UID + Provider + Created + Last Sign In + User UID } @@ -34,7 +34,7 @@ const UsersList = ({}) => { {/* @ts-ignore */}
diff --git a/studio/components/interfaces/Auth/Users/UsersListItem.tsx b/studio/components/interfaces/Auth/Users/UsersListItem.tsx index 0210f8926a9..faedd69c46e 100644 --- a/studio/components/interfaces/Auth/Users/UsersListItem.tsx +++ b/studio/components/interfaces/Auth/Users/UsersListItem.tsx @@ -19,24 +19,24 @@ const UserListItem: FC = ({ user, canRemoveUser }) => { const lastSignedIn = getDateFromIsoString(user.last_sign_in_at) return ( - +
- {!user.email ? '-' : user.email} + {!user.email ? '-' : user.email}
{!user.phone ? '-' : user.phone} - - + + {user?.raw_app_meta_data?.provider || user?.app_metadata?.provider} - + {createdAt?.format('DD MMM, YYYY HH:mm')} - + {!isUserConfirmed ? ( Waiting for verification.. ) : user.last_sign_in_at ? ( @@ -45,10 +45,13 @@ const UserListItem: FC = ({ user, canRemoveUser }) => { 'Never' )} - - - {user.id} - + +
+ + {user.id} + +
...
+
diff --git a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.constants.ts b/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts similarity index 99% rename from studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.constants.ts rename to studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts index c0f1f870270..bf1877b267c 100644 --- a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.constants.ts +++ b/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts @@ -462,14 +462,6 @@ export const ALL_TIMEZONES = [ text: '(UTC) Coordinated Universal Time', utc: ['America/Danmarkshavn', 'Etc/GMT'], }, - { - value: 'GMT Standard Time', - abbr: 'GMT', - offset: 0, - isdst: false, - text: '(UTC) Edinburgh, London', - utc: ['Europe/Isle_of_Man', 'Europe/Guernsey', 'Europe/Jersey', 'Europe/London'], - }, { value: 'British Summer Time', abbr: 'BST', diff --git a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.types.ts b/studio/components/interfaces/Database/Backups/PITR/PITR.types.ts similarity index 66% rename from studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.types.ts rename to studio/components/interfaces/Database/Backups/PITR/PITR.types.ts index cd86f6ea2fc..7787003f9b4 100644 --- a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.types.ts +++ b/studio/components/interfaces/Database/Backups/PITR/PITR.types.ts @@ -6,3 +6,9 @@ export interface Timezone { text: string utc: string[] } + +export interface Time { + h: number + m: number + s: number +} diff --git a/studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts b/studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts new file mode 100644 index 00000000000..edd9bb07aba --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts @@ -0,0 +1,63 @@ +import dayjs from 'dayjs' +import { ALL_TIMEZONES } from './PITR.constants' +import { Time, Timezone } from './PITR.types' + +export const getDatesBetweenRange = (start: Date, end: Date) => { + const arr = [] + for (let i, dt = new Date(start); dt <= new Date(end); dt.setDate(dt.getDate() + 1)) { + arr.push(new Date(dt)) + } + return arr +} + +export const getClientTimezone = () => { + const defaultTz = dayjs.tz.guess() + const utcTz = ALL_TIMEZONES.find((option) => option.value === 'UTC') + const timezone = ALL_TIMEZONES.find((option) => { + if (option.utc.includes(defaultTz)) return option + else return undefined + }) + return timezone ?? (utcTz || ALL_TIMEZONES[0]) +} + +export const checkMatchingDates = (selectedDate: Date | undefined, targetDate: Date) => { + if (!selectedDate) return false + + const formattedSelectedDate = new Date(selectedDate.getTime()) + formattedSelectedDate.setHours(0, 0, 0, 0) + + const formattedTargetDate = new Date(targetDate.getTime()) + formattedTargetDate.setHours(0, 0, 0, 0) + + return Number(formattedSelectedDate) === Number(formattedTargetDate) +} + +export const formatNumberToTwoDigits = (number: Number) => { + return number.toLocaleString('en-US', { + minimumIntegerDigits: 2, + useGrouping: false, + }) +} + +// Formats Time object to hh:mm:ss +export const formatTimeToTimeString = (time: Time) => { + return `${formatNumberToTwoDigits(time.h)}:${formatNumberToTwoDigits( + time.m + )}:${formatNumberToTwoDigits(time.s)}` +} + +export const getTimezoneOffsetText = (timezone: Timezone) => { + return timezone.text.split(')')[0].slice(1) +} + +export const getTimezoneOffset = (timezone: Timezone) => { + return timezone.text.split(')')[0].slice(4) +} + +export const convertDatetimetoUnixS = (date: Date, time: Time, timezone: Timezone) => { + const dateString = dayjs(date).format('YYYY-MM-DD') + const timeString = formatTimeToTimeString(time) + const timezoneOffset = getTimezoneOffset(timezone) + const datetimestringWithTimezone = `${dateString}T${timeString}${timezoneOffset || '+00:00'}` + return dayjs(datetimestringWithTimezone).unix() +} diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx new file mode 100644 index 00000000000..66a91621104 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx @@ -0,0 +1,44 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { Button, IconCalendar } from '@supabase/ui' +import { FormPanel } from 'components/ui/Forms' + +const PITRNotice = ({}) => { + const router = useRouter() + const { ref } = router.query + + return ( + + + Recovery retention period can be increased - reach out to us! + + + + + + +
+ } + > +
+
+ +
+
+

Recovery retention period

+

+ Database changes are logged every 2 minutes, + with a total recovery period of up to 7 days. +

+
+
+ + ) +} + +export default PITRNotice diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx new file mode 100644 index 00000000000..25898919845 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx @@ -0,0 +1,389 @@ +import dayjs from 'dayjs' +import { format } from 'date-fns' +import { useEffect, useState } from 'react' +import DatePicker from 'react-datepicker' +import { + Button, + Modal, + IconChevronLeft, + IconChevronRight, + IconHelpCircle, + Alert, +} from '@supabase/ui' +import * as Tooltip from '@radix-ui/react-tooltip' + +import { useStore } from 'hooks' +import { post } from 'lib/common/fetch' +import { API_URL, PROJECT_STATUS } from 'lib/constants' +import { FormHeader, FormPanel } from 'components/ui/Forms' +import InformationBox from 'components/ui/InformationBox' +import TimeInput from './TimeInput' +import TimezoneSelection from './TimezoneSelection' +import { Time, Timezone } from './PITR.types' +import PITRStatus from './PITRStatus' +import { + checkMatchingDates, + convertDatetimetoUnixS, + formatNumberToTwoDigits, + formatTimeToTimeString, + getClientTimezone, + getDatesBetweenRange, +} from './PITR.utils' +import { useRouter } from 'next/router' + +const DEFAULT_TIME = { h: 0, m: 0, s: 0 } + +const PITRSelection = ({}) => { + const router = useRouter() + const { ref } = router.query + + const { app, ui, backups } = useStore() + const projectId = ui.selectedProject?.id ?? -1 + + const [selectedDate, setSelectedDate] = useState() + const [selectedTime, setSelectedTime] = useState
+ } + > +
+
+ 'cursor-pointer'} + minDate={earliestAvailableBackupFormatted} + maxDate={latestAvailableBackupFormatted} + highlightDates={availableDates} + renderCustomHeader={({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => ( +
+
+ + {format(date, 'MMMM yyyy')} + +
+
+ )} + /> +
+
+

Point in time back up available

+
+
+ +
+ {!selectedDate ? ( +
+
+ } + title="Select a date which you'd like to restore your database to" + /> +
+
+ ) : ( +
+
+

Restore database to

+

+ {dayjs(selectedDate).format('DD MMM YYYY')} + + , {formatNumberToTwoDigits(selectedTime.h)}: + {formatNumberToTwoDigits(selectedTime.m)}: + {formatNumberToTwoDigits(selectedTime.s)} + +

+
+
+

Time of recovery

+
+ +
+ +
+ {isSelectedOnEarliest && ( +

+ Earliest available backup on this date is at{' '} + {earliestAvailableBackup.format('HH:mm:ss')} +

+ )} + {isSelectedOnLatest && ( +

+ Latest available backup on this date is at{' '} + {latestAvailableBackup.format('HH:mm:ss')} +

+ )} +
+
+
+ )} +
+
+ + )} + setShowConfirmation(false)} + customFooter={ +
+ + +
+ } + > +
+ +

Point in time recovery review

+
+ + +
+

Your database will be restored to

+

{recoveryTimeString}

+

{selectedTimezone?.text}

+
+
+ + + +
+

+ Any changes made to your database after this point in time will be lost. This + includes any changes to your project's storage and authentication. +

+
+
+
+ + +

+ Restores may take from a few minutes up to several hours depending on the size of your + database. During this period, your project will not be available. +

+
+
+
+ + ) +} + +export default PITRSelection diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx new file mode 100644 index 00000000000..899192e513c --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx @@ -0,0 +1,72 @@ +import dayjs from 'dayjs' +import { FC } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, IconAlertCircle } from '@supabase/ui' + +import { useStore } from 'hooks' +import { Timezone } from './PITR.types' +import { FormPanel } from 'components/ui/Forms' +import TimezoneSelection from './TimezoneSelection' + +interface Props { + selectedTimezone: Timezone + onUpdateTimezone: (timezone: Timezone) => void + onSetConfiguration: () => void +} + +const PITRStatus: FC = ({ selectedTimezone, onUpdateTimezone, onSetConfiguration }) => { + const { backups } = useStore() + const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } = + backups?.configuration?.physicalBackupData ?? {} + + const earliestAvailableBackup = dayjs + .unix(earliestPhysicalBackupDateUnix) + .tz(selectedTimezone?.utc[0]) + .format('DD MMM YYYY, HH:mm:ss') + + const latestAvailableBackup = dayjs + .unix(latestPhysicalBackupDateUnix) + .tz(selectedTimezone?.utc[0]) + .format('DD MMM YYYY, HH:mm:ss') + + return ( + <> + +
+ + + You'll be able to pick the right date and time when you begin + +
+ +
+ } + > +
+
+ +
+
+
+

Database restore available from

+

{earliestAvailableBackup}

+
+
+

Latest restore available at

+

{latestAvailableBackup}

+
+
+
+ + + ) +} + +export default observer(PITRStatus) diff --git a/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx b/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx new file mode 100644 index 00000000000..e3300bba842 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx @@ -0,0 +1,187 @@ +import dayjs from 'dayjs' +import { isNaN } from 'lodash' +import { ChangeEvent, FC, useEffect, useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { IconClock } from '@supabase/ui' + +import { Time } from './PITR.types' +import { formatNumberToTwoDigits, formatTimeToTimeString } from './PITR.utils' + +// [Joshen] This is trying to do the same thing as TimeSplitInput.tsx +// so can we please look to try to combine these 2 components together if possible +// The problem with TimeSplitInput is that it's tightly coupled to the date + its +// tightly coupled to the context of a range. Ideally it should be more modular +// which is what this component is trying to achieve + +// [Joshen] Potential extension, give option to toggle 24 hours or AM/PM + +interface Props { + defaultTime?: Time + minimumTime?: Time + maximumTime?: Time + onChange?: (time: Time) => void +} + +const TimeInput: FC = ({ defaultTime, minimumTime, maximumTime, onChange = () => {} }) => { + const [isFocused, setIsFocused] = useState(false) + const [error, setError] = useState() + const [time, setTime] = useState