mirror of
https://github.com/supabase/supabase.git
synced 2026-10-19 08:09:11 +03:00
Merge branch 'master' of https://github.com/supabase/supabase into feat/event-collections
This commit is contained in:
commit
014e8a8150
329 files changed
+16087
-3517
No files matched your search
+2
-2
@@ -14,7 +14,7 @@ To ensure a positive and inclusive environment, please read our [code of conduc
|
||||
|
||||
If you find a bug, please create an Issue and we’ll triage it.
|
||||
|
||||
- Please search [existing Issues](https://github.com/supabase/supabase) before creating a new one.
|
||||
- Please search [existing Issues](https://github.com/supabase/supabase/issues) before creating a new one.
|
||||
- Please include a clear description of the problem along with steps to reproduce it. Exact steps with screenshots and urls really help here.
|
||||
|
||||
## Pull Requests
|
||||
@@ -23,7 +23,7 @@ We actively welcome your Pull Requests! A couple of things to keep in mind befor
|
||||
|
||||
- If you’re fixing an Issue, make sure someone else hasn’t already created a PR fixing the same issue. Likewise, make sure to link your PR to the related Issue(s).
|
||||
- We will always try to accept the first viable PR that resolves the Issue.
|
||||
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/pulls?q=is%3Aopen+is%3Apr+label%3A%22good+first+issue%22).
|
||||
- If you're new, we encourage you to take a look at issues tagged with [good first issue](https://github.com/supabase/supabase/labels/good%20first%20issue).
|
||||
- If you’re submitting a new feature, make sure you have opened a [Discussion](https://github.com/orgs/supabase/discussions/new/choose) to discuss the new feature before opening a PR. We’d love to accept your hard work, but unfortunately if a feature hasn’t gone through a proper design process, your PR will be closed.
|
||||
- Please use the PR message template and provide detailed context for quicker review. PRs without clear problem statements will be closed.
|
||||
|
||||
|
||||
@@ -111,6 +111,15 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td><a href="https://github.com/supabase/storage-dart" target="_blank" rel="noopener noreferrer">storage-dart</a></td>
|
||||
<td><a href="https://github.com/supabase/functions-dart" target="_blank" rel="noopener noreferrer">functions-dart</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/PostgREST" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Auth" target="_blank" rel="noopener noreferrer">auth-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Realtime" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Storage" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Functions" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<!-- /notranslate -->
|
||||
<th colspan="7">💚 Community 💚</th>
|
||||
<!-- notranslate -->
|
||||
@@ -177,15 +186,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Swift</td>
|
||||
<td><a href="https://github.com/supabase-community/supabase-swift" target="_blank" rel="noopener noreferrer">supabase-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/postgrest-swift" target="_blank" rel="noopener noreferrer">postgrest-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/gotrue-swift" target="_blank" rel="noopener noreferrer">gotrue-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/realtime-swift" target="_blank" rel="noopener noreferrer">realtime-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/storage-swift" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
|
||||
<td><a href="https://github.com/supabase-community/functions-swift" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Godot Engine (GDScript)</td>
|
||||
<td><a href="https://github.com/supabase-community/godot-engine.supabase" target="_blank" rel="noopener noreferrer">supabase-gdscript</a></td>
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"common": "*",
|
||||
"config": "*",
|
||||
"dayjs": "^1.11.10",
|
||||
"eslint-config-next": "^14.1.4",
|
||||
"eslint-config-next": "^14.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.338.0",
|
||||
"next": "^14.1.4",
|
||||
"next": "^14.2.0",
|
||||
"openai": "^4.20.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Button, cn } from 'ui'
|
||||
import { useSendFeedbackMutation } from '~/lib/fetch/feedback'
|
||||
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
||||
import { FeedbackModal, type FeedbackFields } from './FeedbackModal'
|
||||
import { getSanitizedTabParams } from './Feedback.utils'
|
||||
|
||||
const FeedbackButton = forwardRef<
|
||||
HTMLButtonElement,
|
||||
@@ -89,7 +90,7 @@ function Feedback() {
|
||||
vote: response,
|
||||
page: pathname,
|
||||
metadata: {
|
||||
query: Object.fromEntries(new URLSearchParams(window.location.search).entries()),
|
||||
query: getSanitizedTabParams(),
|
||||
},
|
||||
})
|
||||
if (error) console.error(error)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { pick } from 'lodash'
|
||||
|
||||
/**
|
||||
* Gets the tab selection state from the URL search params.
|
||||
*
|
||||
* Sanitizes by including only those search params that are explicitly marked
|
||||
* as query groups.
|
||||
*/
|
||||
const getSanitizedTabParams = () => {
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const queryGroups = searchParams.getAll('queryGroups')
|
||||
|
||||
return pick(Object.fromEntries(searchParams.entries()), queryGroups)
|
||||
}
|
||||
|
||||
export { getSanitizedTabParams }
|
||||
@@ -112,6 +112,12 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
href: '/reference/dart/introduction',
|
||||
level: 'reference_dart',
|
||||
},
|
||||
{
|
||||
label: 'Swift',
|
||||
icon: 'reference-swift',
|
||||
href: '/reference/swift/introduction',
|
||||
level: 'reference_swift',
|
||||
},
|
||||
{
|
||||
label: 'Python',
|
||||
icon: 'reference-python',
|
||||
@@ -126,13 +132,6 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
|
||||
level: 'reference_csharp',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Swift',
|
||||
icon: 'reference-swift',
|
||||
href: '/reference/swift/introduction',
|
||||
level: 'reference_swift',
|
||||
community: true,
|
||||
},
|
||||
{
|
||||
label: 'Kotlin',
|
||||
icon: 'reference-kotlin',
|
||||
@@ -1140,6 +1139,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Handling Routing in Functions',
|
||||
url: '/guides/functions/routing',
|
||||
},
|
||||
{
|
||||
name: 'Running AI Models',
|
||||
url: '/guides/functions/ai-models',
|
||||
},
|
||||
{
|
||||
name: 'Regional invocations',
|
||||
url: '/guides/functions/regional-invocation',
|
||||
@@ -1188,6 +1191,10 @@ export const functions: NavMenuConstant = {
|
||||
name: 'Generating OG images ',
|
||||
url: '/guides/functions/examples/og-image',
|
||||
},
|
||||
{
|
||||
name: 'Semantic AI Search',
|
||||
url: '/guides/functions/examples/semantic-search',
|
||||
},
|
||||
{
|
||||
name: 'CAPTCHA support with Cloudflare Turnstile',
|
||||
url: '/guides/functions/examples/cloudflare-turnstile',
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
---
|
||||
id: 'ai-generating-embeddings'
|
||||
title: 'Generate Embeddings'
|
||||
subtitle: 'Generate text embeddings using Edge Functions + Transformer.js.'
|
||||
subtitle: 'Generate text embeddings using Edge Functions.'
|
||||
breadcrumb: 'AI Quickstarts'
|
||||
---
|
||||
|
||||
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using Transformers.js. Inference is performed directly in Edge Functions using open source Hugging Face models, so no external API is required.
|
||||
|
||||
## What is Transformers.js?
|
||||
|
||||
[Transformers.js](https://huggingface.co/docs/transformers.js/index) is a library that allows you to perform inference using the JavaScript runtime. You can run Hugging Face embedding models through Transformers.js directly in Supabase [Edge Functions](/docs/guides/functions).
|
||||
This guide will walk you through how to generate high quality text embeddings in [Edge Functions](/docs/guides/functions) using its built-in AI inference API, so no external API is required.
|
||||
|
||||
## Build the Edge Function
|
||||
|
||||
@@ -60,89 +56,45 @@ Let's build an Edge Function that will accept an input string and generate an em
|
||||
|
||||
<StepHikeCompact.Step step={3}>
|
||||
|
||||
<StepHikeCompact.Details title="Import & configure Transformer.js" fullWidth>
|
||||
<StepHikeCompact.Details title="Setup Inference Session" fullWidth>
|
||||
|
||||
Let's modify the Edge Function to import the Transformers.js client and configure it for the Deno runtime.
|
||||
Let's create a new inference session to be used in the lifetime of this function. Multiple requests can use the same inference session.
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
|
||||
import { env, pipeline } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.5.0'
|
||||
|
||||
// Configuration for Deno runtime
|
||||
env.useBrowserCache = false;
|
||||
env.allowLocalModels = false;
|
||||
```
|
||||
Currently, only the `gte-small` (https://huggingface.co/Supabase/gte-small) text embedding model is supported in Supabase's Edge Runtime.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
const session = new Supabase.ai.Session('gte-small');
|
||||
```
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={4}>
|
||||
|
||||
<StepHikeCompact.Details title="Construct embedding pipeline">
|
||||
|
||||
Next let's construct the `pipe()` function we'll use to generate the embeddings. We use the Transformer.js `pipeline()` function to create this.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
const pipe = await pipeline(
|
||||
'feature-extraction',
|
||||
'Supabase/gte-small',
|
||||
);
|
||||
```
|
||||
|
||||
</StepHikeCompact.Code>
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Note the two arguments we pass to `pipeline()`:
|
||||
|
||||
1. The first argument specifies the type of [inference task](https://huggingface.co/docs/transformers.js/index#tasks) to perform. `feature-extraction` is used for embedding generation.
|
||||
2. The second argument specifies which Hugging Face model to use for the embedding generation. Supabase officially supports models available on the [Supabase org](https://huggingface.co/Supabase).
|
||||
|
||||
<Admonition>
|
||||
|
||||
We intentially construct the pipeline before the `serve()` function. This allows us to reuse the pre-loaded model in future warm-start requests, significantly speeding up future queries. Note that you'll only notice this speedup after deploying your function to Supabase - requests in local development will always be a cold-start.
|
||||
|
||||
</Admonition>
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={5}>
|
||||
|
||||
<StepHikeCompact.Details title="Implement request handler">
|
||||
|
||||
Modify our `serve()` request handler to accept an `input` string from the POST request JSON body.
|
||||
Modify our request handler to accept an `input` string from the POST request JSON body.
|
||||
|
||||
Finally let's generate the embedding by:
|
||||
|
||||
1. Calling `pipe()` to perform the embedding generation
|
||||
2. Extracting the embedding from the output as a number array
|
||||
3. Returning it as a JSON response
|
||||
Then generate the embedding by calling `session.run(input)`.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
<StepHikeCompact.Code>
|
||||
|
||||
```ts ./supabase/functions/embed/index.ts
|
||||
serve(async (req) => {
|
||||
Deno.serve(async (req) => {
|
||||
// Extract input string from JSON body
|
||||
const { input } = await req.json();
|
||||
|
||||
// Generate the embedding from the user input
|
||||
const output = await pipe(input, {
|
||||
pooling: 'mean',
|
||||
const embedding = await session.run(input, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
});
|
||||
|
||||
// Extract the embedding output
|
||||
const embedding = Array.from(output.data);
|
||||
|
||||
// Return the embedding
|
||||
return new Response(
|
||||
JSON.stringify({ embedding }),
|
||||
@@ -155,16 +107,16 @@ Let's build an Edge Function that will accept an input string and generate an em
|
||||
|
||||
<StepHikeCompact.Details fullWidth>
|
||||
|
||||
Note the two options we pass to `pipe()`:
|
||||
Note the two options we pass to `session.run()`:
|
||||
|
||||
- `pooling`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings and is the method supported by Transformers.js.
|
||||
- `normalize`: The second option tells Transformers.js to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
|
||||
- `mean_pool`: The first option sets `pooling` to `mean`. Pooling referes to how token-level embedding representations are compressed into a single sentence embedding that reflects the meaning of the entire sentence. Average pooling is the most common type of pooling for sentence embeddings.
|
||||
- `normalize`: The second option tells to normalize the embedding vector so that it can be used with distance measures like dot product. A normalized vector means its length (magnitude) is 1 - also referred to as a unit vector. A vector is normalized by dividing each element by the vector's length (magnitude), which maintains its direction but changes its length to 1.
|
||||
|
||||
</StepHikeCompact.Details>
|
||||
|
||||
</StepHikeCompact.Step>
|
||||
|
||||
<StepHikeCompact.Step step={6}>
|
||||
<StepHikeCompact.Step step={5}>
|
||||
|
||||
<StepHikeCompact.Details title="Test it!">
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Embeddings are generated using a language model, and embeddings are compared to
|
||||
|
||||
## Embedding models
|
||||
|
||||
There are many embedding models available today. Some can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
|
||||
There are many embedding models available today. Supabase Edge Functions has [built in support](/docs/guides/functions/examples/semantic-search) for the `gte-small` model. Others can be accessed through third-party APIs like [OpenAI](https://platform.openai.com/docs/guides/embeddings), where you send your text in the request and receive an embedding vector in the response. Others can run locally on your own compute, such as through Transformers.js for JavaScript implementations. For more information on local implementation, see [Generate embeddings](/docs/guides/ai/quickstarts/generate-text-embeddings).
|
||||
|
||||
It's crucial to remember that when using embedding models with semantic search, you must use the same model for all embedding comparisons. Comparing embeddings created by different models will yield meaningless results.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Javascript/Typescript | [supabase-js](https://github.com/supabase/supabase-js) | [Docs](https://supabase.com/docs/reference/javascript/introduction) |
|
||||
| Dart/Flutter | [supabase-flutter](https://github.com/supabase/supabase-flutter/tree/main/packages/supabase_flutter) | [Docs](https://supabase.com/docs/reference/dart/introduction) |
|
||||
| Swift | [supabase-swift](https://github.com/supabase/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
|
||||
|
||||
## Community libraries
|
||||
|
||||
@@ -23,5 +24,4 @@ Supabase provides client libraries for the REST and Realtime APIs. Some librarie
|
||||
| Kotlin | [supabase-kt](https://github.com/supabase-community/supabase-kt) | [Docs](https://supabase.com/docs/reference/kotlin/introduction) |
|
||||
| Python | [supabase-py](https://github.com/supabase-community/supabase-py) | [Docs](https://supabase.com/docs/reference/python/initializing) |
|
||||
| Ruby | [supabase-rb](https://github.com/supabase-community/supabase-rb) | |
|
||||
| Swift | [supabase-swift](https://github.com/supabase-community/supabase-swift) | [Docs](https://supabase.com/docs/reference/swift/introduction) |
|
||||
| Godot Engine (GDScript) | [supabase-gdscript](https://github.com/supabase-community/godot-engine.supabase) | |
|
||||
@@ -4,13 +4,7 @@ title: 'Anonymous Sign-Ins'
|
||||
subtitle: 'Create and use anonymous users to authenticate with Supabase'
|
||||
---
|
||||
|
||||
Enable Anonymous Sign-In to build apps which provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, they can attach an authentication method to their account.
|
||||
|
||||
It's most useful for:
|
||||
|
||||
- E-commerce applications, such as shopping carts before check-out
|
||||
- Full-feature demos without collecting personal information
|
||||
- Temporary or throw-away accounts
|
||||
[Enable Anonymous Sign-Ins](/dashboard/project/_/settings/auth) to build apps which provide users an authenticated experience without requiring users to enter an email address, password, use an OAuth provider or provide any other PII (Personally Identifiable Information). Later, when ready, the user can link an authentication method to their account.
|
||||
|
||||
<Admonition type="note" label="Anonymous user vs the anon key">
|
||||
|
||||
@@ -22,7 +16,11 @@ This is different from the `anon` API key which does not create a user and can b
|
||||
|
||||
</Admonition>
|
||||
|
||||
To get started, [enable anonymous sign-ins](/dashboard/project/_/settings/auth) for your Supabase Project.
|
||||
Anonymous sign-ins can be used to build:
|
||||
|
||||
- E-commerce applications, such as shopping carts before check-out
|
||||
- Full-feature demos without collecting personal information
|
||||
- Temporary or throw-away accounts
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
@@ -87,7 +85,7 @@ supabase.auth.signInAnonymously()
|
||||
|
||||
## Convert an anonymous user to a permanent user
|
||||
|
||||
Converting an anonymous user to a permanent user requires linking an identity to the user.
|
||||
Converting an anonymous user to a permanent user requires [linking an identity](/docs/guides/auth/auth-identity-linking#manual-linking-beta) to the user. This requires you to [enable manual linking](/dashboard/project/_/settings/auth) in your Supabase project.
|
||||
|
||||
### Link an Email / Phone identity
|
||||
|
||||
@@ -100,10 +98,16 @@ Converting an anonymous user to a permanent user requires linking an identity to
|
||||
>
|
||||
<TabPanel id="js" label="JavaScript">
|
||||
|
||||
You can use the [`updateUser()`](/docs/reference/javascript/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
You can use the [`updateUser()`](/docs/reference/javascript/auth-updateuser) method to link an email or phone identity to the anonymous user. To add a password for the anonymous user, the user's email or phone number needs to be verified first.
|
||||
|
||||
```js
|
||||
const { data, error } = await supabase.auth.updateUser({ email: 'example@email.com' })
|
||||
|
||||
// verify the user's email by clicking on the email change link
|
||||
// or entering the 6-digit OTP sent to the email address
|
||||
|
||||
// once the user has been verified, update the password
|
||||
const { data, error } = await supabase.auth.updateUser({ password: 'password' })
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -129,10 +133,10 @@ try await supabase.auth.updateUser(
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
You can use the [`modifyUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
You can use the [`updateUser()`](/docs/reference/kotlin/auth-updateuser) method to link an email or phone identity to the anonymous user.
|
||||
|
||||
```kotlin
|
||||
supabase.auth.modifyUser {
|
||||
supabase.auth.updateUser {
|
||||
email = "example@email.com"
|
||||
}
|
||||
```
|
||||
@@ -233,7 +237,7 @@ where is_anonymous is true and created_at < now() - interval '30 days';
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
|
||||
@@ -207,7 +207,7 @@ suspend fun logout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
- [Supabase Kotlin Client](https://github.com/supabase-community/supabase-kt)
|
||||
@@ -76,6 +76,22 @@ const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
})
|
||||
```
|
||||
|
||||
## Email templates when using `redirectTo`
|
||||
|
||||
When using a `redirectTo` option, you may need to replace the `{{ .SiteURL }}` with `{{ .RedirectTo }}` in your email templates. See the [Email Templates guide](/docs/guides/auth/auth-email-templates) for more information.
|
||||
|
||||
For example, change the following:
|
||||
|
||||
```html
|
||||
<!-- Old -->
|
||||
<a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup">Confirm your mail</a>
|
||||
|
||||
<!-- New -->
|
||||
<a href="{{ .RedirectTo }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup"
|
||||
>Confirm your mail</a
|
||||
>
|
||||
```
|
||||
|
||||
## Mobile deep linking URIs
|
||||
|
||||
For mobile applications you can use deep linking URIs. For example, for your `SITE_URL` you can specify something like `com.supabase://login-callback/` and for additional redirect URLs something like `com.supabase.staging://login-callback/` if needed.
|
||||
|
||||
@@ -157,7 +157,7 @@ If successful the user will now be logged in and you should receive a valid sess
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Phone SMS & WhatsApp OTP](/docs/guides/auth/phone-login)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
@@ -158,6 +158,6 @@ suspend fun logout() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Flutter Client](https://github.com/supabase/supabase-flutter)
|
||||
@@ -149,10 +149,10 @@ try await supabase.auth.updateUser(user: UserAttributes(password: newPassword))
|
||||
</TabPanel>
|
||||
<TabPanel id="kotlin" label="Kotlin">
|
||||
|
||||
To update the password, call the [`.modifyUser`](/docs/reference/kotlin/auth-updateuser) method with the new password.
|
||||
To update the password, call the [`.updateUser`](/docs/reference/kotlin/auth-updateuser) method with the new password.
|
||||
|
||||
```kotlin
|
||||
supabase.gotrue.modifyUser {
|
||||
supabase.auth.updateUser {
|
||||
password = "new_password"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -237,7 +237,7 @@ In the server, you need to handle the redirect to the OAuth provider's authentic
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: '/auth/callback',
|
||||
redirectTo: 'http://example.com/auth/callback',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -112,6 +112,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Bitbucket Account](https://bitbucket.org)
|
||||
@@ -113,7 +113,7 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Discord Account](https://discord.com)
|
||||
- [Discord Developer Portal](https://discord.com/developers)
|
||||
@@ -131,6 +131,6 @@ You can read more about App Review [here](https://developers.facebook.com/docs/a
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Facebook Developers Dashboard](https://developers.facebook.com/)
|
||||
@@ -112,6 +112,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Figma Developers page](https://www.figma.com/developers)
|
||||
@@ -123,6 +123,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
@@ -109,6 +109,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [GitLab Account](https://gitlab.com)
|
||||
@@ -135,6 +135,6 @@ Do reach out to support if you have any concerns around this change.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [LinkedIn Developer Dashboard](https://api.LinkedIn.com/apps)
|
||||
@@ -112,7 +112,7 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Notion Account](https://notion.so)
|
||||
- [Notion Developer Portal](https://www.notion.so/my-integrations)
|
||||
@@ -130,6 +130,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Slack Developer Dashboard](https://api.slack.com/apps)
|
||||
@@ -124,6 +124,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/)
|
||||
@@ -127,7 +127,7 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Twitch Account](https://twitch.tv)
|
||||
- [Twitch Developer Console](https://dev.twitch.tv/console)
|
||||
@@ -118,6 +118,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Twitter Developer Dashboard](https://developer.twitter.com/en/portal/dashboard)
|
||||
@@ -125,6 +125,6 @@ suspend fun signOut() {
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Zoom App Marketplace](https://marketplace.zoom.us/)
|
||||
@@ -210,5 +210,5 @@ returns:
|
||||
## Resources
|
||||
|
||||
- [Supabase JS Client](https://github.com/supabase/supabase-js)
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [PostgreSQL Arrays](https://www.postgresql.org/docs/15/arrays.html)
|
||||
@@ -4,7 +4,20 @@ title: 'index_advisor: query optimization'
|
||||
description: 'Automatically optimize SQL queries'
|
||||
---
|
||||
|
||||
[Index advisor](https://database.dev/olirice/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
|
||||
[Index advisor](https://github.com/supabase/index_advisor) is a Postgres extension for recommending indexes to improve query performance.
|
||||
|
||||
Features:
|
||||
|
||||
- Supports generic parameters e.g. `$1`, `$2`
|
||||
- Supports materialized views
|
||||
- Identifies tables/columns obfuscated by views
|
||||
- Skips duplicate indexes
|
||||
|
||||
index_advisor is accessible directly through Supabase Studio by navigating to the [Query Performance Report](/dashboard/project/*/database/query-performance) and selecting a query and then the "indexes" tab.
|
||||
|
||||

|
||||
|
||||
Alternatively, you can use index_advisor directly via SQL.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -20,25 +33,12 @@ from
|
||||
(1 row)
|
||||
```
|
||||
|
||||
Features:
|
||||
|
||||
- Supports generic parameters e.g. `$1`, `$2`
|
||||
- Supports materialized views
|
||||
- Identifies tables/columns obfuscated by views
|
||||
- Skips duplicate indexes
|
||||
|
||||
## Installation
|
||||
|
||||
index_advisor is a trusted language extension, which means it is directly installable by users from the [database.dev](https://database.dev/) SQL package repository.
|
||||
|
||||
To get started, enable the dbdev client by executing the [setup SQL script](https://database.dev/installer).
|
||||
|
||||
Then, install index_advisor by running
|
||||
To get started, enable index_advisor by running
|
||||
|
||||
```sql
|
||||
select dbdev.install('olirice-index_advisor');
|
||||
create extension if not exists hypopg;
|
||||
create extension "olirice-index_advisor";
|
||||
create extension index_advisor;
|
||||
```
|
||||
|
||||
## API
|
||||
@@ -152,5 +152,4 @@ from
|
||||
|
||||
## Resources
|
||||
|
||||
- dbdev [`index_advisor`](https://database.dev/olirice/index_advisor) docs
|
||||
- dbdev [Github Repository](https://github.com/supabase/dbdev)
|
||||
- [`index_advisor`](https://github.com/supabase/index_advisor) repo
|
||||
@@ -104,3 +104,27 @@ reindex table concurrently persons;
|
||||
```
|
||||
|
||||
Take note that `reindex` can be used inside a transaction, but `reindex [index/table] concurrently` cannot.
|
||||
|
||||
## Index Advisor
|
||||
|
||||
Indexes can improve query performance of your tables as they grow. The Supabase Dashboard offers an Index Advisor, which suggests potential indexes to add to your tables.
|
||||
|
||||
For more information on the Index Advisor and its suggestions, see the [`index_advisor` extension](/docs/guides/database/extensions/index_advisor).
|
||||
|
||||
To use the Dashboard Index Advisor:
|
||||
|
||||
1. Go to the [Query Performance](/dashboard/project/_/database/query-performance) page.
|
||||
1. Click on a query to bring up the Details side panel.
|
||||
1. Select the Indexes tab.
|
||||
1. Enable Index Advisor if prompted.
|
||||
|
||||
### Understanding Index Advisor results
|
||||
|
||||
The Indexes tab shows the existing indexes used in the selected query. Note that indexes suggested in the "New Index Recommendations" section may not be used when you create them. Postgres' query planner may intentionally ignore an available index if it determines that the query will be faster without. For example, on a small table, a sequential scan might be faster than an index scan. In that case, the planner will switch to using the index as the table size grows, helping to future proof the query.
|
||||
|
||||
If additional indexes might improve your query, the Index Advisor shows the suggested indexes with the estimated improvement in startup and total costs:
|
||||
|
||||
- Startup cost is the cost to fetch the first row
|
||||
- Total cost is the cost to fetch all the rows
|
||||
|
||||
Costs are in arbitrary units, where a single sequential page read costs 1.0 units.
|
||||
@@ -362,7 +362,7 @@ Always use the Role of inside your policies, specified by the `TO` operator. For
|
||||
|
||||
```sql
|
||||
create policy "rls_test_select" on rls_test
|
||||
using ( (select auth.uid()) = user_id );
|
||||
using ( auth.uid() = user_id );
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
@@ -77,7 +77,7 @@ const { count, error } = await supabase
|
||||
|
||||
## Resources
|
||||
|
||||
- [Supabase Account - Free Plan OK](https://supabase.com)
|
||||
- [Supabase - Get started for free](https://supabase.com)
|
||||
- [Postgrest Operators](https://postgrest.org/en/stable/api.html#operators)
|
||||
- [Supabase API: JavaScript select](/docs/reference/javascript/select)
|
||||
- [Supabase API: JavaScript modifiers](/docs/reference/javascript/using-modifiers)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
id: 'function-ai-models'
|
||||
title: 'Running AI Models'
|
||||
description: 'How to run AI models in Edge Functions.'
|
||||
subtitle: 'How to run AI models in Edge Functions.'
|
||||
---
|
||||
|
||||
[Supabase Edge Runtime](https://github.com/supabase/edge-runtime) has a built-in API for running AI models. You can use this API to generate embeddings, build conversational workflows, and do other AI related tasks in your Edge Functions.
|
||||
|
||||
## Setup
|
||||
|
||||
There are no external dependencies or packages to install to enable the API.
|
||||
|
||||
You can create a new inference session by doing:
|
||||
|
||||
```ts
|
||||
const model = new Supabase.ai.Session('model-name')
|
||||
```
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
To get type hints and checks for the API we recommend adding this triple-slash directive to the start of the Edge Function code: `/// <reference types="https://esm.sh/@supabase/functions-js/src/edge-runtime.d.ts" />`
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Running a model inference
|
||||
|
||||
Once the session is instantiated, you can call it with inputs to perform inferences. Depending on the model you run, you may need to provide different options (discussed below).
|
||||
|
||||
```ts
|
||||
const output = await model.run(input, options)
|
||||
```
|
||||
|
||||
## How to generate text embeddings
|
||||
|
||||
Now let's see how to write an Edge Function using the `Supabase.ai` API to generate text embeddings. Currently, `Supabase.ai` API only supports the [gte-small](https://huggingface.co/Supabase/gte-small) model.
|
||||
|
||||
<Admonition type="tip">
|
||||
|
||||
`gte-small` model exclusively caters to English texts, and any lengthy texts will be truncated to a maximum of 512 tokens. While you can provide inputs longer than 512 tokens, truncation may affect the accuracy.
|
||||
|
||||
</Admonition>
|
||||
|
||||
```ts
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const input = params.get('input')
|
||||
const output = await model.run(input, { mean_pool: true, normalize: true })
|
||||
return new Response(JSON.stringify(output), {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Using Large Language Models
|
||||
|
||||
Inference via larger models is supported via [Ollama](https://ollama.com/). In the first iteration, the `llama2` and `mistral` models are supported and you would need to manage the Ollama server yourself.
|
||||
|
||||
<video width="99%" muted playsInline controls={true}>
|
||||
<source
|
||||
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/docs/guides/edge-functions-inference-2.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
|
||||
### Running locally
|
||||
|
||||
1. Install Ollama and pull the Mistral model
|
||||
|
||||
```
|
||||
ollama pull mistral
|
||||
```
|
||||
|
||||
1. Run the Ollama server locally
|
||||
|
||||
```
|
||||
ollama serve
|
||||
```
|
||||
|
||||
1. Set a function secret called AI_INFERENCE_API_HOST to point to the Ollama server
|
||||
|
||||
```
|
||||
echo "AI_INFERENCE_API_HOST=http://host.docker.internal:11434/api/generate" >> supabase/functions/.env
|
||||
```
|
||||
|
||||
1. Create a new function with the following code
|
||||
|
||||
```
|
||||
supabase functions new ollama-test
|
||||
```
|
||||
|
||||
```ts
|
||||
/// <reference types="https://esm.sh/@supabase/functions-js/src/edge-runtime.d.ts" />
|
||||
const session = new Supabase.ai.Session('mistral')
|
||||
|
||||
Deno.serve(async (req: Request) => {
|
||||
const params = new URL(req.url).searchParams
|
||||
const prompt = params.get('prompt') ?? ''
|
||||
|
||||
// Get the output as a stream
|
||||
const output = await session.run(prompt, { stream: true })
|
||||
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
})
|
||||
|
||||
// Create a stream
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
try {
|
||||
for await (const chunk of output) {
|
||||
controller.enqueue(encoder.encode(chunk.response ?? ''))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Stream error:', err)
|
||||
} finally {
|
||||
controller.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Return the stream to the user
|
||||
return new Response(stream, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
1. Execute the function
|
||||
|
||||
```
|
||||
curl --get "http://localhost:54321/functions/v1/ollama-test" \
|
||||
--data-urlencode "prompt=write a short rap song about Supabase, the Postgres Developer platform, as sung by Nicki Minaj" \
|
||||
-H "Authorization: $ANON_KEY"
|
||||
```
|
||||
|
||||
### Deploying to production
|
||||
|
||||
Once the function is working locally, it's time to deploy to production.
|
||||
|
||||
1. Deploy a Ollama server and set a function secret called `AI_INFERENCE_API_HOST` to point to the deployed Ollama server
|
||||
|
||||
```
|
||||
supabase secrets set AI_INFERENCE_API_HOST=https://path-to-your-ollama-server/
|
||||
```
|
||||
|
||||
2. Deploy the Supabase function
|
||||
|
||||
```
|
||||
supabase functions deploy ollama-test
|
||||
```
|
||||
|
||||
3. Execute the function
|
||||
|
||||
```
|
||||
curl --get "https://project-ref.supabase.co/functions/v1/ollama-test" \
|
||||
--data-urlencode "prompt=write a short rap song about Supabase, the Postgres Developer platform, as sung by Nicki Minaj" \
|
||||
-H "Authorization: $ANON_KEY"
|
||||
```
|
||||
|
||||
As demonstrated in the video above, running Ollama locally is typically slower than running it in on a server with dedicated GPUs. We are collaborating with the Ollama team to improve local performance.
|
||||
|
||||
In the future, a hosted Ollama API, will be provided as part of the Supabase platform. Supabase will scale and manage the API and GPUs for you. To sign up for early access, fill up [this form](https://forms.supabase.com/supabase.ai-llm-early-access).
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
id: 'function-ai-models'
|
||||
title: 'Semantic Search'
|
||||
description: 'Semantic Search with pgvector and Supabase Edge Functions'
|
||||
subtitle: 'Semantic Search with pgvector and Supabase Edge Functions'
|
||||
tocVideo: 'w4Rr_1whU-U'
|
||||
---
|
||||
|
||||
[Semantic search](/docs/guides/ai/semantic-search) interprets the meaning behind user queries rather than exact [keywords](/docs/guides/ai/keyword-search). It uses machine learning to capture the intent and context behind the query, handling language nuances like synonyms, phrasing variations, and word relationships.
|
||||
|
||||
Since Supabase Edge Runtime [v1.36.0](https://github.com/supabase/edge-runtime/releases/tag/v1.36.0) you can run the [`gte-small` model](https://huggingface.co/Supabase/gte-small) natively within Supabase Edge Functions without any external dependencies! This allows you to easily generate text embeddings without calling any external APIs!
|
||||
|
||||
In this tutorial you're implementing three parts:
|
||||
|
||||
1. A [`generate-embedding`](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) database webhook edge function which generates embeddings when a content row is added (or updated) in the [`public.embeddings`](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql) table.
|
||||
2. A [`query_embeddings` Postgres function](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an egde function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
|
||||
3. A [`search` edge function](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions/supabase/functions/search/index.ts) which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result.
|
||||
|
||||
You can find the complete example code on [GitHub](https://github.com/supabase/supabase/tree/master/examples/ai/edge-functions)
|
||||
|
||||
### Create the database table and webhook
|
||||
|
||||
Given the [following table definition](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240408072601_embeddings.sql):
|
||||
|
||||
```sql
|
||||
create extension if not exists vector with schema extensions;
|
||||
|
||||
create table embeddings (
|
||||
id bigint primary key generated always as identity,
|
||||
content text not null,
|
||||
embedding vector (384)
|
||||
);
|
||||
alter table embeddings enable row level security;
|
||||
|
||||
create index on embeddings using hnsw (embedding vector_ip_ops);
|
||||
```
|
||||
|
||||
You can deploy the [following edge function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/generate-embedding/index.ts) as a [database webhook](/docs/guides/database/webhooks) to generate the embeddings for any text content inserted into the table:
|
||||
|
||||
```tsx
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const payload: WebhookPayload = await req.json()
|
||||
const { content, id } = payload.record
|
||||
|
||||
// Generate embedding.
|
||||
const embedding = await model.run(content, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
// Store in database.
|
||||
const { error } = await supabase
|
||||
.from('embeddings')
|
||||
.update({ embedding: JSON.stringify(embedding) })
|
||||
.eq('id', id)
|
||||
if (error) console.warn(error.message)
|
||||
|
||||
return new Response('ok')
|
||||
})
|
||||
```
|
||||
|
||||
## Create a Postgres Function and RPC
|
||||
|
||||
With the embeddings now stored in your Postgres database table, you can query them from Supabase Edge Functions by utilizing [Remote Procedure Calls (RPC)](/docs/guides/database/functions?language=js).
|
||||
|
||||
Given the [following Postgres Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/migrations/20240410031515_vector-search.sql):
|
||||
|
||||
```sql
|
||||
-- Matches document sections using vector similarity search on embeddings
|
||||
--
|
||||
-- Returns a setof embeddings so that we can use PostgREST resource embeddings (joins with other tables)
|
||||
-- Additional filtering like limits can be chained to this function call
|
||||
create or replace function query_embeddings(embedding vector(384), match_threshold float)
|
||||
returns setof embeddings
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
return query
|
||||
select *
|
||||
from embeddings
|
||||
|
||||
-- The inner product is negative, so we negate match_threshold
|
||||
where embeddings.embedding <#> embedding < -match_threshold
|
||||
|
||||
-- Our embeddings are normalized to length 1, so cosine similarity
|
||||
-- and inner product will produce the same query results.
|
||||
-- Using inner product which can be computed faster.
|
||||
--
|
||||
-- For the different distance functions, see https://github.com/pgvector/pgvector
|
||||
order by embeddings.embedding <#> embedding;
|
||||
end;
|
||||
$$;
|
||||
```
|
||||
|
||||
## Query vectors in Supabase Edge Functions
|
||||
|
||||
You can use `supabase-js` to first generate the embedding for the search term and then invoke the Postgres function to find the relevant results from your stored embeddings, right from your [Supabase Edge Function](https://github.com/supabase/supabase/blob/master/examples/ai/edge-functions/supabase/functions/search/index.ts):
|
||||
|
||||
```tsx
|
||||
const model = new Supabase.ai.Session('gte-small')
|
||||
|
||||
Deno.serve(async (req) => {
|
||||
const { search } = await req.json()
|
||||
if (!search) return new Response('Please provide a search param!')
|
||||
// Generate embedding for search term.
|
||||
const embedding = await model.run(search, {
|
||||
mean_pool: true,
|
||||
normalize: true,
|
||||
})
|
||||
|
||||
// Query embeddings.
|
||||
const { data: result, error } = await supabase
|
||||
.rpc('query_embeddings', {
|
||||
embedding,
|
||||
match_threshold: 0.8,
|
||||
})
|
||||
.select('content')
|
||||
.limit(3)
|
||||
if (error) {
|
||||
return Response.json(error)
|
||||
}
|
||||
|
||||
return Response.json({ search, result })
|
||||
})
|
||||
```
|
||||
|
||||
That's it, you now have AI powered semantic search set up without any external dependencies! Just you, pgvector, and Supabase Edge Functions!
|
||||
@@ -66,6 +66,20 @@ These are the currently supported region values you can provide for `x-region` h
|
||||
- `us-west-1`
|
||||
- `us-west-2`
|
||||
|
||||
## Using the client library
|
||||
|
||||
You can also specify the region when invoking a Function using the Supabase client library:
|
||||
|
||||
```js
|
||||
const { createClient, FunctionRegion } = require('@supabase/supabase-js')
|
||||
const { data: ret, error } = await supabase.functions.invoke('my-function-name', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: 'GET',
|
||||
body: {},
|
||||
region: FunctionRegion.UsEast1,
|
||||
})
|
||||
```
|
||||
|
||||
## Handling regional outages
|
||||
|
||||
If you explicitly specify the region via `x-region` header, requests **will NOT** be automatically re-routed to another region and you should consider temporarily changing regions during the outage.
|
||||
@@ -58,7 +58,7 @@ You can deploy the function to Supabase via:
|
||||
supabase functions deploy hello-world
|
||||
```
|
||||
|
||||
Once the function is deployed, you can try to call the two ednpoints using cURL (or Postman).
|
||||
Once the function is deployed, you can try to call the two endpoints using cURL (or Postman).
|
||||
|
||||
```bash
|
||||
# https://supabase.com/docs/guides/functions/deploy#invoking-remote-functions
|
||||
|
||||
@@ -145,7 +145,7 @@ Less, but better.
|
||||
|
||||
To avoid lock-in, we make it easy to migrate in and out. Our cloud offering is compatible with our self-hosted product.
|
||||
We use existing standards to increase portability (like `pg_dump` and CSV files). If a new standard emerges which competes with a "Supabase" approach, we will deprecate the approach in favor of the standard.
|
||||
This forces us compete on experience. We aim to be the best Postgres hosting service.
|
||||
This forces us to compete on user experience. We aim to be the best Postgres hosting service.
|
||||
|
||||
### Play the long game
|
||||
|
||||
|
||||
@@ -12,51 +12,89 @@ This is a non-exhaustive list of features that Supabase provides for every proje
|
||||
|
||||
Every project is a full Postgres database. [Docs](/docs/guides/database).
|
||||
|
||||
### Database extensions
|
||||
### Vector database
|
||||
|
||||
Every database comes with a full set of Postgres extensions. [Docs](/docs/guides/database/extensions).
|
||||
Store vector embeddings right next to the rest of your data. [Docs](/docs/guides/ai).
|
||||
|
||||
### Database functions
|
||||
### Auto-generated Rest API via PostgREST
|
||||
|
||||
Create custom database functions which you can call from the browser. [Docs](/docs/guides/database/functions).
|
||||
RESTful APIs are auto-generated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
|
||||
|
||||
### Database triggers
|
||||
### Auto-generated GraphQL API via pg_graphql
|
||||
|
||||
Attach triggers to your tables to handle database changes. [Docs](/docs/guides/auth/managing-user-data#using-triggers).
|
||||
Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guides/graphql/api).
|
||||
|
||||
### Database webhooks
|
||||
|
||||
Send database changes to any external service using Webhooks. [Link](https://supabase.com/blog/supabase-functions-updates#database-webhooks-alpha).
|
||||
|
||||
### Database backups
|
||||
|
||||
Projects are backed up daily with the option to upgrade to Point in Time recovery.
|
||||
|
||||
### Search
|
||||
|
||||
Build search functionality using Postgres Full Text Search. [Docs](/docs/guides/database/full-text-search).
|
||||
Send database changes to any external service using Webhooks. [Docs](/docs/guides/database/webhooks).
|
||||
|
||||
### Secrets and encryption
|
||||
|
||||
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Link](https://supabase.com/blog/supabase-vault).
|
||||
Encrypt sensitive data and store secrets using our Postgres extension, Supabase Vault. [Docs](/docs/guides/database/vault).
|
||||
|
||||
### Database migrations
|
||||
## Platform
|
||||
|
||||
Develop locally and push your changes to your production database using migrations. [Docs](/docs/guides/getting-started/local-development#database-migrations)
|
||||
### Database backups
|
||||
|
||||
Projects are backed up daily with the option to upgrade to Point in Time recovery. [Docs](/docs/guides/platform/backups).
|
||||
|
||||
### Custom domains
|
||||
|
||||
White-label the Supabase APIs to create a branded experience for your users. [Docs](/docs/guides/platform/custom-domains).
|
||||
|
||||
### Network restrictions
|
||||
|
||||
Restrict IP ranges that can connect to your database. [Docs](/docs/guides/platform/network-restrictions).
|
||||
|
||||
### SSL enforcement
|
||||
|
||||
Enforce Postgres clients to connect via SSL. [Docs](/docs/guides/platform/ssl-enforcement).
|
||||
|
||||
### Branching
|
||||
|
||||
Use Supabase Branches to test and preview changes. [Docs](/docs/guides/platform/branching).
|
||||
|
||||
### Terraform provider
|
||||
|
||||
Manage Supabase infrastructure via Terraform, an Infrastructure as Code tool. [Docs](/docs/guides/platform/terraform).
|
||||
|
||||
### Read replicas
|
||||
|
||||
Deploy read-only databases across multiple regions, for lower latency and better resource management. [Docs](/docs/guides/platform/read-replicas).
|
||||
|
||||
### Fly Postgres
|
||||
|
||||
Launch the Supabase stack on Fly.io edge network. [Docs](/docs/guides/platform/fly-postgres).
|
||||
|
||||
## Studio
|
||||
|
||||
### Studio Single Sign On
|
||||
|
||||
Login to the Supabase dashboard via SSO. [Docs](/docs/guides/platform/sso).
|
||||
|
||||
<br />
|
||||
|
||||
## Realtime
|
||||
|
||||
### Postgres changes
|
||||
|
||||
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-changes).
|
||||
|
||||
### Broadcast
|
||||
|
||||
Send messages between connected users through websockets. [Docs](/docs/guides/realtime/broadcast).
|
||||
|
||||
### Presence
|
||||
|
||||
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime/presence).
|
||||
|
||||
## Auth
|
||||
|
||||
### Email & password logins
|
||||
### Email login
|
||||
|
||||
Build email logins for your application or website. [Docs](/docs/guides/auth/auth-email).
|
||||
|
||||
### Magic links
|
||||
|
||||
Build passwordless logins for your application or website.[Docs](/docs/guides/auth/auth-magic-link).
|
||||
|
||||
### Social logins
|
||||
### Social login
|
||||
|
||||
Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs/guides/auth/social-login).
|
||||
|
||||
@@ -64,46 +102,21 @@ Provide social logins - everything from Apple, to GitHub, to Slack. [Docs](/docs
|
||||
|
||||
Provide phone logins using a third-party SMS provider. [Docs](/docs/guides/auth/phone-login).
|
||||
|
||||
### Row Level Security
|
||||
### Passwordless login
|
||||
|
||||
Build passwordless logins via magic links for your application or website.[Docs](/docs/guides/auth/auth-magic-link).
|
||||
|
||||
### Authorization via Row Level Security
|
||||
|
||||
Control the data each user can access with Postgres Policies. [Docs](/docs/guides/auth/row-level-security).
|
||||
|
||||
### Server-side Auth helpers
|
||||
### Captcha protection
|
||||
|
||||
Helpers for implementing user authentication in popular frameworks like [Next.js](/docs/guides/auth/auth-helpers/nextjs) and [SvelteKit](/docs/guides/auth/auth-helpers/sveltekit)
|
||||
Add Captcha to your sign-in, sign-up, and password reset forms. [Docs](/docs/guides/auth/auth-captcha).
|
||||
|
||||
### Auth UI kit
|
||||
### Server-side Auth
|
||||
|
||||
Build login and registration pages with custom themes. [Docs](/docs/guides/auth/auth-helpers/auth-ui).
|
||||
|
||||
<br />
|
||||
|
||||
## APIs & client libraries
|
||||
|
||||
### Auto-generated REST API
|
||||
|
||||
RESTful APIs are auto-generated from your database, without a single line of code. [Docs](/docs/guides/api#rest-api-overview).
|
||||
|
||||
### Auto-generated GraphQL API
|
||||
|
||||
Fast GraphQL APIs using our custom Postgres GraphQL extension. [Docs](/docs/guides/graphql/api).
|
||||
|
||||
### Realtime database changes
|
||||
|
||||
Receive your database changes through websockets. [Docs](/docs/guides/realtime/postgres-changes).
|
||||
|
||||
### User broadcasting
|
||||
|
||||
Send messages between connected users through websockets. [Docs](/docs/guides/realtime/broadcast).
|
||||
|
||||
### User presence
|
||||
|
||||
Synchronize shared state across your users, including online status and typing indicators. [Docs](/docs/guides/realtime/presence).
|
||||
|
||||
### Client libraries
|
||||
|
||||
Official client libraries for [JavaScript](/docs/reference/javascript/) and [Dart](/docs/reference/dart).
|
||||
Unofficial libraries [supported by the community](https://github.com/supabase-community#client-libraries).
|
||||
Helpers for implementing user authentication in popular server-side languages and frameworks like Next.js, SvelteKit and Remix. [Docs](/docs/guides/auth/server-side/overview).
|
||||
|
||||
<br />
|
||||
|
||||
@@ -113,19 +126,21 @@ Unofficial libraries [supported by the community](https://github.com/supabase-co
|
||||
|
||||
Supabase Storage makes it simple to store and serve files. [Docs](/docs/guides/storage).
|
||||
|
||||
### Resumable uploads
|
||||
|
||||
Upload large files easily using resumable uploads. [Docs](/docs/guides/storage/uploads/resumable-uploads).
|
||||
|
||||
### Storage CDN
|
||||
### Content Delivery Network
|
||||
|
||||
Cache large files using the Supabase CDN. [Docs](/docs/guides/storage/cdn/fundamentals).
|
||||
|
||||
### Smart Content Delivery Network
|
||||
|
||||
Automatically revalidate assets at the edge via the Smart CDN. [Docs](/docs/guides/storage/cdn/smart-cdn).
|
||||
|
||||
### Image transformations
|
||||
|
||||
Transform images on the fly. [Docs](/docs/guides/storage/serving/image-transformations).
|
||||
|
||||
<br />
|
||||
### Resumable uploads
|
||||
|
||||
Upload large files using resumable uploads. [Docs](/docs/guides/storage/uploads/resumable-uploads).
|
||||
|
||||
## Edge Functions
|
||||
|
||||
@@ -133,7 +148,13 @@ Transform images on the fly. [Docs](/docs/guides/storage/serving/image-transform
|
||||
|
||||
Globally distributed TypeScript functions to execute custom business logic. [Docs](/docs/guides/functions).
|
||||
|
||||
<br />
|
||||
### Regional invocations
|
||||
|
||||
Execute an Edge Function in a region close to your database. [Docs](/docs/guides/functions/regional-invocation).
|
||||
|
||||
### NPM compatibility
|
||||
|
||||
Edge functions natively support NPM modules and Node built-in APIs. [Link](https://supabase.com/blog/edge-functions-node-npm).
|
||||
|
||||
## Project management
|
||||
|
||||
@@ -145,42 +166,75 @@ Use our CLI to develop your project locally and deploy to the Supabase Platform.
|
||||
|
||||
Manage your projects programmatically. [Docs](/docs/reference/api).
|
||||
|
||||
<br />
|
||||
## Client libraries
|
||||
|
||||
Official client libraries for [JavaScript](/docs/reference/javascript/start), [Flutter](docs/reference/dart/initializing) and [Swift](/docs/reference/swift/introduction).
|
||||
Unofficial libraries are supported by the community.
|
||||
|
||||
## Feature status
|
||||
|
||||
Both Postgres and the Supabase Platform are production-ready. Some tools we offer on top of Postgres are still under development.
|
||||
Supabase Features are in 4 different states - Private Alpha, Public Alpha, Beta and Generally Available.
|
||||
|
||||
| Product | Feature | Stage | Available on self-hosted |
|
||||
| -------------------------- | ---------------------- | ------ | ------------------------------------------- |
|
||||
| Database | Postgres | `GA` | ✅ |
|
||||
| Database | Triggers | `GA` | ✅ |
|
||||
| Database | Functions | `GA` | ✅ |
|
||||
| Database | Extensions | `GA` | ✅ |
|
||||
| Database | Full Text Search | `GA` | ✅ |
|
||||
| Database | Webhooks | `GA` | ✅ |
|
||||
| Database | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
|
||||
| Database | Vault | `beta` | ✅ |
|
||||
| Studio | | `GA` | ✅ |
|
||||
| Realtime | Postgres Changes | `GA` | ✅ |
|
||||
| Realtime | Broadcast | `beta` | ✅ |
|
||||
| Realtime | Presence | `beta` | ✅ |
|
||||
| Storage | | `GA` | ✅ |
|
||||
| Storage | S3 Backend | `GA` | ✅ |
|
||||
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
|
||||
| Storage | Smart CDN | `beta` | 🚧 [Cloudflare](https://www.cloudflare.com) |
|
||||
| Storage | Image Transformations | `GA` | ✅ |
|
||||
| Storage | Resumable Uploads | `beta` | ✅ |
|
||||
| Edge Functions | | `beta` | ✅ |
|
||||
| Auth | OAuth Providers | `GA` | ✅ |
|
||||
| Auth | Passwordless | `GA` | ✅ |
|
||||
| Auth | Next.js Auth Helpers | `beta` | ✅ |
|
||||
| Auth | SvelteKit Auth Helpers | `beta` | ✅ |
|
||||
| Auth | Remix Auth Helpers | `beta` | ✅ |
|
||||
| CLI | | `beta` | ✅ Works with self-hosted |
|
||||
| Management API | | `beta` | N/A |
|
||||
| Client Library: JavaScript | | `GA` | N/A |
|
||||
| Client Library: Dart | | `beta` | N/A |
|
||||
### Private Alpha
|
||||
|
||||
Features are initially launched as a private alpha to gather feedback from the community. To join our early access program, send an email to product-ops@supabase.io.
|
||||
|
||||
### Public Alpha
|
||||
|
||||
he alpha stage indicates that the API might change in the future, not that the service isn’t stable. Even though the [uptime Service Level Agreement](https://supabase.com/sla) does not cover products in Alpha, we do our best to have the service as stable as possible.
|
||||
|
||||
### Beta
|
||||
|
||||
Features in Beta are tested by an external penetration tester for security issues. The API is guaranteed to be stable and there is a strict communication process for breaking changes.
|
||||
|
||||
### Generally Available
|
||||
|
||||
In addition to the Beta requirements, features in GA are covered by the [uptime SLA](https://supabase.com/sla).
|
||||
|
||||
| Product | Feature | Stage | Available on self-hosted |
|
||||
| -------------- | -------------------------- | --------------- | ------------------------------------------- |
|
||||
| Database | Postgres | `GA` | ✅ |
|
||||
| Database | Vector Database | `GA` | ✅ |
|
||||
| Database | Auto-generated Rest API | `GA` | ✅ |
|
||||
| Database | Auto-generated GraphQL API | `GA` | ✅ |
|
||||
| Database | Webhooks | `beta` | ✅ |
|
||||
| Database | Vault | `public alpha` | ✅ |
|
||||
| Platform | | `GA` | ✅ |
|
||||
| Platform | Point-in-Time Recovery | `GA` | 🚧 [wal-g](https://github.com/wal-g/wal-g) |
|
||||
| Platform | Custom Domains | `GA` | N/A |
|
||||
| Platform | Network Restrictions | `beta` | N/A |
|
||||
| Platform | SSL enforcement | `beta` | N/A |
|
||||
| Platform | Branching | `public alpha` | N/A |
|
||||
| Platform | Terraform Provider | `public alpha` | N/A |
|
||||
| Platform | Read Replicas | `private alpha` | N/A |
|
||||
| Platform | Fly Postgres | `private alpha` | N/A |
|
||||
| Studio | | `GA` | ✅ |
|
||||
| Studio | SSO | `GA` | ✅ |
|
||||
| Realtime | Postgres Changes | `GA` | ✅ |
|
||||
| Realtime | Broadcast | `GA` | ✅ |
|
||||
| Realtime | Presence | `GA` | ✅ |
|
||||
| Storage | | `GA` | ✅ |
|
||||
| Storage | CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
|
||||
| Storage | Smart CDN | `GA` | 🚧 [Cloudflare](https://www.cloudflare.com) |
|
||||
| Storage | Image Transformations | `GA` | ✅ |
|
||||
| Storage | Resumable Uploads | `GA` | ✅ |
|
||||
| Edge Functions | | `beta` | ✅ |
|
||||
| Edge Functions | Regional Invocations | `beta` | ✅ |
|
||||
| Edge Functions | NPM compatibility | `beta` | ✅ |
|
||||
| Auth | | `GA` | ✅ |
|
||||
| Auth | Email login | `GA` | ✅ |
|
||||
| Auth | Social login | `GA` | ✅ |
|
||||
| Auth | Phone login | `GA` | ✅ |
|
||||
| Auth | Passwordless login | `GA` | ✅ |
|
||||
| Auth | SSO with SAML | `GA` | ✅ |
|
||||
| Auth | Authorization via RLS | `GA` | ✅ |
|
||||
| Auth | Captcha protection | `GA` | ✅ |
|
||||
| Auth | Server-side Auth | `beta` | ✅ |
|
||||
| CLI | | `GA` | ✅ Works with self-hosted |
|
||||
| Management API | | `GA` | N/A |
|
||||
| Client Library | JavaScript | `GA` | N/A |
|
||||
| Client Library | Flutter | `beta` | N/A |
|
||||
| Client Library | Swift | `beta` | N/A |
|
||||
|
||||
- ✅ = Fully Available
|
||||
- 🚧 = Available, but requires external tools or configuration
|
||||
@@ -143,14 +143,14 @@ Create a new `src/routes/+layout.server.ts` file to handle the session on the se
|
||||
// src/routes/+layout.server.ts
|
||||
import type { LayoutServerLoad } from './$types'
|
||||
|
||||
export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
|
||||
export const load = (async ({ locals: { safeGetSession } }) => {
|
||||
const { session, user } = await safeGetSession()
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
}
|
||||
}
|
||||
}) satisfies LayoutServerLoad
|
||||
```
|
||||
|
||||
> Start your dev server (`npm run dev`) in order to generate the `./$types` files we are referencing in our project.
|
||||
@@ -163,7 +163,7 @@ import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/publi
|
||||
import type { LayoutLoad } from './$types'
|
||||
import { createBrowserClient, isBrowser, parse } from '@supabase/ssr'
|
||||
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
export const load = (async ({ fetch, data, depends }) => {
|
||||
depends('supabase:auth')
|
||||
|
||||
const supabase = createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
|
||||
@@ -192,7 +192,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
} = await supabase.auth.getSession()
|
||||
|
||||
return { supabase, session }
|
||||
}
|
||||
}) satisfies LayoutLoad
|
||||
```
|
||||
|
||||
Update your `src/routes/+layout.svelte`:
|
||||
|
||||
@@ -23,9 +23,9 @@ Let's start building the SwiftUI app from scratch.
|
||||
|
||||
Open Xcode and create a new SwiftUI project.
|
||||
|
||||
Add the [supabase-swift](https://github.com/supabase-community/supabase-swift) dependency.
|
||||
Add the [supabase-swift](https://github.com/supabase/supabase-swift) dependency.
|
||||
|
||||
Add the `https://github.com/supabase-community/supabase-swift` package to your app. For instructions, see the [Apple tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
|
||||
Add the `https://github.com/supabase/supabase-swift` package to your app. For instructions, see the [Apple tutorial on adding package dependencies](https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app).
|
||||
|
||||
Create a helper file to initialize the Supabase client.
|
||||
You need the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
---
|
||||
title: 'Read Replicas'
|
||||
description: 'Deploy additional read-only databases in multiple regions for your project.'
|
||||
subtitle: 'Deploy additional read-only databases in multiple regions for your project'
|
||||
description: 'Deploy read-only databases across multiple regions, for lower latency.'
|
||||
subtitle: 'Deploy read-only databases across multiple regions, for lower latency and better resource management.'
|
||||
---
|
||||
|
||||
<Admonition type="note" label="Read Replicas are available to Pro, Team, and Enterprise organizations who have signed up for early access.">
|
||||
Read Replicas are additional databases that are kept in sync with your Primary database. You can read your data from a Read Replica, which helps with:
|
||||
|
||||
[Request early access](https://forms.supabase.com/read-replica) and you'll be notified once Read Replicas are available to your organization.
|
||||
|
||||
</Admonition>
|
||||
- **Load balancing:** Read Replicas reduce load on the Primary database. For example, you can use a Read Replica for complex analytical queries and reserve the Primary for user-facing create, update, and delete operations.
|
||||
- **Improved latency:** For projects with a global user base, additional databases can be deployed closer to users to reduce latency.
|
||||
- **Redundancy:** Read Replicas provide data redundancy.
|
||||
|
||||
<figure className="max-w-[700px] mx-auto">
|
||||
|
||||
<Image
|
||||
className="hidden dark:block"
|
||||
alt="Map view of all project databases"
|
||||
@@ -25,57 +26,74 @@ subtitle: 'Deploy additional read-only databases in multiple regions for your pr
|
||||
|
||||
</figure>
|
||||
|
||||
Read Replicas are Postgres databases that are continuously synced to a Supabase project's Primary database. You can both read from and write to a Primary database, but you can _only_ read from a Read Replica, such as using `select` statements.
|
||||
## About Read Replicas
|
||||
|
||||
The database you start with when launching a Supabase project is your Primary database. Read Replicas are kept in sync with the Primary through a process called "replication." Replication is asynchronous to ensure that transactions on the Primary aren't blocked. There is a delay between an update on the Primary and the time that a Read Replica receives the change. This delay is called "replication lag."
|
||||
|
||||
You can only read data from a Read Replica. This is in contrast to a Primary database, where you can both read and write:
|
||||
|
||||
| | select | insert | update | delete |
|
||||
| ------------ | ------ | ------ | ------ | ------ |
|
||||
| Primary | ✅ | ✅ | ✅ | ✅ |
|
||||
| Read Replica | ✅ | - | - | - |
|
||||
|
||||
A database is spun up automatically with every Supabase project. This database is the Primary database. You can choose to add Read Replicas to your project and those Replicas will receive and synchronize data from the Primary database.
|
||||
|
||||
Replication is asynchronous and happens in the background so transactions on the Primary aren't blocked. The time delay between writing data to the Primary and the Read Replica receiving the change is replication lag.
|
||||
|
||||
Read Replicas have many applications:
|
||||
|
||||
- They reduce read load on the Primary database.
|
||||
- For example, you can use a Read Replica for complex analytical queries and reserve the Primary for user-facing create, update, and delete operations.
|
||||
- For projects that need to cover a global audience, additional databases can be deployed closer to users in any of our twelve supported regions to reduce latency.
|
||||
- Read Replicas provide data redundancy.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A project needs to satisfy these requirements to deploy a Read Replica:
|
||||
<Admonition type="note" label="Read Replicas are available on the Team and Enterprise plans">
|
||||
|
||||
Read replicas are available to projects created on the Teams and Enterprise plans after 2024 March 20. They will be progressively rolled out to older projects, and eventually available on the Pro plan.
|
||||
|
||||
If your project doesn't meet these criteria, you can join the [early access queue](https://forms.supabase.com/read-replica).
|
||||
|
||||
</Admonition>
|
||||
|
||||
Projects must meet these requirements to use Read Replicas:
|
||||
|
||||
1. Running on AWS.
|
||||
- Support for projects on Fly.io is coming.
|
||||
1. Running on at least a [Small compute add-on](/docs/guides/platform/compute-add-ons).
|
||||
- Read Replicas are currently placed on the same compute instance as the database to make sure they can keep up with the Primary's activities.
|
||||
1. Running on Postgres 15.
|
||||
- For projects running on older versions of PostgreSQL, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
|
||||
- Read Replicas are started on the same compute instance as the Primary to keep up with changes.
|
||||
1. Running on Postgres 15+.
|
||||
- For projects running on older versions of Postgres, you will need to [upgrade to the latest platform version](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade).
|
||||
1. Using [physical backups](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
- Physical backups are automatically enabled if using [PITR](/docs/guides/platform/backups#point-in-time-recovery)
|
||||
- If you're not using PITR, you'll be able to switch to physical backups as part of the Read Replica setup process. Note that physical backups can't be downloaded from the dashboard in the way logical backups can.
|
||||
|
||||
## Getting started
|
||||
|
||||
Read Replicas can be managed from the [infrastructure settings page](https://supabase.com/dashboard/project/_/settings/infrastructure). For now, each project can only deploy up to two Read Replicas and all Read Replicas inherit the compute size of their Primary database.
|
||||
To add a Read Replica, go to the [Infrastructure Settings page](/dashboard/project/_/settings/infrastructure) in your dashboard. Each project can deploy up to two Read Replicas and all Read Replicas inherit the compute size of their Primary database.
|
||||
|
||||
## Features
|
||||
|
||||
Read Replicas offer the following features:
|
||||
|
||||
### Dedicated endpoints
|
||||
|
||||
Each Read Replica has its own dedicated database and API endpoints. The database endpoint can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection info**. The API endpoint can be found in [dashboard API settings](https://supabase.com/dashboard/project/_/settings/api) under **Project URL**.
|
||||
Each Read Replica has its own dedicated database and API endpoints.
|
||||
|
||||
A Read Replica's API endpoint only supports `GET` requests to the [REST API](https://supabase.com/docs/guides/api). Requests towards other Supabase products, such as Auth, Storage, and Realtime, are not able to utilize a Read Replica nor its API endpoint for now. We are working on adding Read Replica support for them.
|
||||
- Find the database endpoint on the [Database Settings page](/dashboard/project/_/settings/database) under **Connection Info**
|
||||
- Find the API endpoint on the [API Settings page](/dashboard/project/_/settings/api) under **Project URL**
|
||||
|
||||
### Dedicated connection pool
|
||||
Read Replicas only support `GET` requests and the [REST API](/docs/guides/api). Requests to other Supabase products, such as Auth, Storage, and Realtime, aren't able to use a Read Replica or its API endpoint. Support for more products will be added in the future.
|
||||
|
||||
A connection pool through Supavisor is also available for each Read Replica. Its unique connection string can be found in your [dashboard database settings](https://supabase.com/dashboard/project/_/settings/database) under **Connection string**.
|
||||
## Dedicated connection pool
|
||||
|
||||
A connection pool through Supavisor is also available for each Read Replica. Find the connection string on the [Database Settings page](/dashboard/project/_/settings/database) under **Connection String**.
|
||||
|
||||
## API load balancer
|
||||
|
||||
A load balancer is deployed to balance requests between your Primary database and Read Replicas. Find its endpoint on the [API Settings page](/dashboard/project/_/settings/api).
|
||||
|
||||
The load balancer uses a round-robin strategy to route `GET` requests to all available API endpoints, including the Primary database. Non-`GET` requests can also be sent through this endpoint, and will be routed to the Primary database. Read-only Postgres functions can be called using a [`GET` request](https://github.com/supabase/supabase-js/blob/v2.42.3/src/SupabaseClient.ts#L176-L177) as well.
|
||||
|
||||
If you remove all Read Replicas from your project, the load balancer and its endpoint are removed as well. Make sure to redirect requests back to your Primary database before removal.
|
||||
|
||||
### Querying through the SQL editor
|
||||
|
||||
In the SQL editor, you can choose if you want to run the query on a particular Read Replica.
|
||||
|
||||
<figure className="max-w-[700px] mx-auto">
|
||||
|
||||
<Image
|
||||
className="hidden dark:block"
|
||||
alt="SQL editor view."
|
||||
@@ -90,23 +108,28 @@ A connection pool through Supavisor is also available for each Read Replica. Its
|
||||
|
||||
</figure>
|
||||
|
||||
In the SQL editor, you can choose if you want to run the query on the Primary or one of the Replicas.
|
||||
### Metrics
|
||||
|
||||
### API load balancer
|
||||
Observability and metrics for Read Replicas are not currently available in the Supabase Dashboard.
|
||||
|
||||
When Read Replicas are deployed, an API load balancer becomes available for the project. An endpoint for it is provisioned and can be found [here](https://supabase.com/dashboard/project/_/settings/api).
|
||||
|
||||
The load balancer uses a round-robin strategy to route `GET` requests to any of the API endpoints available, including that of the Primary database. Non-`GET` requests can also be sent through this endpoint. They are immediately redirected to the Primary database.
|
||||
|
||||
Similar to the Read Replica API endpoints, only requests to the REST API are available for now. Support for other Supabase products will become available once Read Replica support has been completed for them.
|
||||
|
||||
When a project no longer has any Read Replicas, the load balancer and its endpoint are brought down as well.
|
||||
If you're ingesting your [project's metrics](/docs/guides/platform/metrics#accessing-the-metrics-endpoint) into your own environment, you can update [your scraping setup](https://github.com/supabase/supabase-grafana?tab=readme-ov-file#read-replica-support) to additionally ingest metrics from your Read Replicas.
|
||||
|
||||
### Centralized configuration management
|
||||
|
||||
All settings made through the dashboard will be propagated across all databases of a project. This ensures that no Read Replica get out of sync with its Primary database or with other Read Replicas.
|
||||
All settings configured through the dashboard will be propagated across all databases of a project. This ensures that no Read Replica get out of sync with the Primary database or with other Read Replicas.
|
||||
|
||||
## How are Read Replicas made?
|
||||
## Operations blocked by Read Replicas
|
||||
|
||||
### Project upgrades and data restorations
|
||||
|
||||
The following procedures require all Read Replicas for a project to be brought down before they can be performed:
|
||||
|
||||
1. [Project upgrades](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade)
|
||||
1. [Data restorations](/docs/guides/platform/backups#pitr-restoration-process)
|
||||
|
||||
These operations need to be completed before Read Replicas can be re-deployed.
|
||||
|
||||
## About replication
|
||||
|
||||
We use a hybrid approach to replicate data from a Primary to its Read Replicas, combining the native methods of streaming replication and file-based log shipping.
|
||||
|
||||
@@ -135,14 +158,3 @@ In this replication method, the Primary continuously buffers WAL changes to a lo
|
||||
We bring these two methods together to achieve quick, stable, and reliable replication. Each method addresses the limitations of the other. Streaming replication minimizes replication lag, while file-based log shipping provides a fallback. For file-based log shipping, we use our existing Point In Time Recovery (PITR) infrastructure. We regularly archive files from the Primary using [WAL-G](https://github.com/wal-g/wal-g), an open source archival and restoration tool, and ship the WAL files to S3.
|
||||
|
||||
We combine it with streaming replication to reduce replication lag. Once WAL-G files have been synced from S3, Read Replicas connect to the Primary and stream the WAL directly.
|
||||
|
||||
## Operations blocked by Read Replicas
|
||||
|
||||
### Project upgrades and data restorations
|
||||
|
||||
The following procedures require all Read Replicas for a project to be brought down before they can be performed:
|
||||
|
||||
1. [Project upgrades](/docs/guides/platform/migrating-and-upgrading-projects#pgupgrade)
|
||||
1. [Data restorations](/docs/guides/platform/backups#pitr-restoration-process)
|
||||
|
||||
These operations need to be completed before Read Replicas can be re-deployed.
|
||||
@@ -14,7 +14,7 @@ The Management API is in `beta`. It is usable in it's current state, but it's li
|
||||
|
||||
## Authentication
|
||||
|
||||
All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization Bearer <supabase_personal_token`.
|
||||
All API requests require a Supabase Personal token to be included in the Authorization header: `Authorization: Bearer <supabase_personal_token>`.
|
||||
To generate or manage your API token, visit your [account](https://supabase.com/dashboard/account/tokens) page.
|
||||
Your API tokens carry the same privileges as your user account, so be sure to keep it secret.
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: installing
|
||||
title: 'Installing'
|
||||
slug: installing
|
||||
custom_edit_url: https://github.com/supabase/supabase/edit/master/web/spec/supabase.yml
|
||||
---
|
||||
|
||||
### Install using Swift Package Manager
|
||||
|
||||
<RefSubLayout.EducationRow>
|
||||
<RefSubLayout.Details>
|
||||
|
||||
You can install Supabase package using Swift Package Manager.
|
||||
|
||||
The package exposes multiple libraries, you can choose between adding all of them using Supabase, or some of:
|
||||
|
||||
- `Auth`
|
||||
- `Realtime`
|
||||
- `Postgrest`
|
||||
- `Functions`
|
||||
- `Storage`
|
||||
|
||||
</RefSubLayout.Details>
|
||||
|
||||
<RefSubLayout.Examples>
|
||||
|
||||
<Tabs
|
||||
size="small"
|
||||
type="underlined"
|
||||
defaultActiveId="swift"
|
||||
queryGroup="framework"
|
||||
>
|
||||
<TabPanel id="swift" label="Package.swift">
|
||||
|
||||
```swift
|
||||
let package = Package(
|
||||
...
|
||||
dependencies: [
|
||||
...
|
||||
.package(
|
||||
url: "https://github.com/supabase/supabase-swift.git",
|
||||
from: "2.0.0"
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "YourTargetName",
|
||||
dependencies: [
|
||||
.product(
|
||||
name: "Supabase", // Auth, Realtime, Postgrest, Functions, or Storage
|
||||
package: "supabase-swift"
|
||||
),
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
</RefSubLayout.Examples>
|
||||
</RefSubLayout.EducationRow>
|
||||
@@ -8,25 +8,13 @@ hideTitle: true
|
||||
<IconMenuSwift width={35} height={35} />
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-3xl text-foreground m-0">Swift Client Library</h1>
|
||||
<h2 className="text-base font-mono text-foreground-light">
|
||||
@supabase-community/supabase-swift
|
||||
</h2>
|
||||
<h2 className="text-base font-mono text-foreground-light">@supabase/supabase-swift</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl">
|
||||
|
||||
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase-community/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
We also provide a [supabase](https://pub.dev/packages/supabase) package for non-Swift projects.
|
||||
|
||||
</div>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
|
||||
The Swift client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.
|
||||
|
||||
Huge thanks to official maintainers, [Guilherme](https://github.com/grdsdev) and [Maail](https://github.com/maail).
|
||||
This reference documents every object and method available in Supabase's Swift library, [supabase-swift](https://github.com/supabase/supabase-swift). You can use supabase-swift to interact with your Postgres database, listen to database changes, invoke Deno Edge Functions, build login and user management functionality, and manage large files.
|
||||
|
||||
</div>
|
||||
@@ -1,12 +1,17 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
|
||||
const useHash = () => {
|
||||
const [hash, setHash] = useState(() =>
|
||||
typeof window !== 'undefined' ? window.location.hash.split('#')[1] : undefined
|
||||
)
|
||||
const [hash, setHash] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const url = new URL(window.location.href)
|
||||
return url.hash.substring(1) // Removes the leading '#'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const hashChangeHandler = useCallback(() => {
|
||||
setHash(window.location.hash.split('#')[1])
|
||||
const url = new URL(window.location.href)
|
||||
setHash(url.hash.substring(1))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -18,7 +23,9 @@ const useHash = () => {
|
||||
|
||||
const updateHash = useCallback(
|
||||
(newHash) => {
|
||||
if (newHash !== hash) window.location.hash = newHash
|
||||
if (newHash !== hash) {
|
||||
window.location.hash = newHash
|
||||
}
|
||||
},
|
||||
[hash]
|
||||
)
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"@code-hike/mdx": "^0.9.0",
|
||||
"@mdx-js/loader": "^2.1.5",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/bundle-analyzer": "^14.1.4",
|
||||
"@next/mdx": "^14.1.4",
|
||||
"@next/bundle-analyzer": "^14.2.0",
|
||||
"@next/mdx": "^14.2.0",
|
||||
"@octokit/auth-app": "^6.0.3",
|
||||
"@octokit/core": "^5.1.0",
|
||||
"@octokit/graphql": "^7.0.2",
|
||||
@@ -54,7 +54,7 @@
|
||||
"mdx-mermaid": "2.0.0-rc3",
|
||||
"mermaid": "^10.0.2",
|
||||
"micromark-extension-mdxjs": "^1.0.0",
|
||||
"next": "^14.1.4",
|
||||
"next": "^14.2.0",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"next-plugin-yaml": "^1.0.1",
|
||||
"next-seo": "^6.5.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -1,7 +1,7 @@
|
||||
clispec: '001'
|
||||
info:
|
||||
id: cli
|
||||
version: 1.151.1
|
||||
version: 1.157.2
|
||||
title: Supabase CLI
|
||||
language: sh
|
||||
source: https://github.com/supabase/cli
|
||||
@@ -10,6 +10,8 @@ info:
|
||||
description: |
|
||||
Supabase CLI provides you with tools to develop your application locally, and deploy your application to the Supabase platform.
|
||||
tags:
|
||||
- id: quick-start
|
||||
title: Quick Start
|
||||
- id: local-dev
|
||||
title: Local Development
|
||||
- id: management-api
|
||||
@@ -1349,19 +1351,21 @@ commands:
|
||||
|
||||
If your local and remote migration history goes out of sync, you can repair the remote history by marking specific migrations as `--status applied` or `--status reverted`. Marking as `reverted` will delete an existing record from the migration history table while marking as `applied` will insert a new record.
|
||||
|
||||
For example, your migration history table may look like this after running `db pull` for the first time.
|
||||
For example, your migration history may look like the table below, with missing entries in either local or remote.
|
||||
|
||||
```bash
|
||||
$ supabase migration list
|
||||
LOCAL │ REMOTE │ TIME (UTC)
|
||||
─────────────────┼────────────────┼──────────────────────
|
||||
20230103054303 │ 20230103054303 │ 2023-01-03 05:43:03
|
||||
│ 20230103054303 │ 2023-01-03 05:43:03
|
||||
20230103054315 │ │ 2023-01-03 05:43:15
|
||||
```
|
||||
|
||||
To reset your migration history to a clean state, first delete your local migration file.
|
||||
|
||||
```bash
|
||||
$ rm supabase/migrations/20230103054303_remote_commit.sql
|
||||
$ rm supabase/migrations/20230103054315_remote_commit.sql
|
||||
|
||||
$ supabase migration list
|
||||
LOCAL │ REMOTE │ TIME (UTC)
|
||||
─────────────────┼────────────────┼──────────────────────
|
||||
@@ -1372,13 +1376,30 @@ commands:
|
||||
|
||||
```bash
|
||||
$ supabase migration repair 20230103054303 --status reverted
|
||||
Repaired migration history: 20230103054303 => reverted
|
||||
Connecting to remote database...
|
||||
Repaired migration history: [20220810154537] => reverted
|
||||
Finished supabase migration repair.
|
||||
|
||||
$ supabase migration list
|
||||
LOCAL │ REMOTE │ TIME (UTC)
|
||||
─────────────────┼────────────────┼──────────────────────
|
||||
```
|
||||
|
||||
Now you can run `db pull` again to dump the remote schema as a local migration file.
|
||||
|
||||
```bash
|
||||
$ supabase db pull
|
||||
Connecting to remote database...
|
||||
Schema written to supabase/migrations/20240414044403_remote_schema.sql
|
||||
Update remote migration history table? [Y/n]
|
||||
Repaired migration history: [20240414044403] => applied
|
||||
Finished supabase db pull.
|
||||
|
||||
$ supabase migration list
|
||||
LOCAL │ REMOTE │ TIME (UTC)
|
||||
─────────────────┼────────────────┼──────────────────────
|
||||
20240414044403 │ 20240414044403 │ 2024-04-14 04:44:03
|
||||
```
|
||||
examples:
|
||||
- id: basic-usage
|
||||
name: Mark a migration as reverted
|
||||
@@ -1541,9 +1562,8 @@ commands:
|
||||
flags:
|
||||
- id: name
|
||||
name: --name <string>
|
||||
description: |
|
||||
Name that will be used to store token in your settings, defaults to built-in token name generator
|
||||
default_value: ''
|
||||
description: Name that will be used to store token in your settings
|
||||
default_value: built-in token name generator
|
||||
- id: no-browser
|
||||
name: --no-browser
|
||||
description: Do not open browser automatically
|
||||
@@ -2320,9 +2340,13 @@ commands:
|
||||
usage: supabase init [flags]
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: force
|
||||
name: --force
|
||||
description: Overwrite existing supabase/config.toml.
|
||||
default_value: 'false'
|
||||
- id: use-orioledb
|
||||
name: --use-orioledb
|
||||
description: Use OrioleDB storage engine for Postgres
|
||||
description: Use OrioleDB storage engine for Postgres.
|
||||
default_value: 'false'
|
||||
- id: with-intellij-settings
|
||||
name: --with-intellij-settings
|
||||
@@ -2828,6 +2852,39 @@ commands:
|
||||
Optionally, a new row can be inserted into the migration history table to reflect the current state of the remote database.
|
||||
|
||||
If no entries exist in the migration history table, `pg_dump` will be used to capture all contents of the remote schemas you have created. Otherwise, this command will only diff schema changes against the remote database, similar to running `db diff --linked`.
|
||||
examples:
|
||||
- id: basic-usage
|
||||
name: Basic usage
|
||||
code: supabase db pull
|
||||
response: |
|
||||
Connecting to remote database...
|
||||
Schema written to supabase/migrations/20240414044403_remote_schema.sql
|
||||
Update remote migration history table? [Y/n]
|
||||
Repaired migration history: [20240414044403] => applied
|
||||
Finished supabase db pull.
|
||||
The auth and storage schemas are excluded. Run supabase db pull --schema auth,storage again to diff them.
|
||||
- id: local-studio
|
||||
name: Local studio
|
||||
code: supabase db pull --local
|
||||
response: |
|
||||
Connecting to local database...
|
||||
Setting up initial schema....
|
||||
Creating custom roles supabase/roles.sql...
|
||||
Applying migration 20240414044403_remote_schema.sql...
|
||||
No schema changes found
|
||||
The auth and storage schemas are excluded. Run supabase db pull --schema auth,storage again to diff them.
|
||||
exit status 1
|
||||
- id: custom-schemas
|
||||
name: Custom schemas
|
||||
code: supabase db pull --schema auth,storage
|
||||
response: |
|
||||
Connecting to remote database...
|
||||
Setting up initial schema....
|
||||
Creating custom roles supabase/roles.sql...
|
||||
Applying migration 20240414044403_remote_schema.sql...
|
||||
No schema changes found
|
||||
Try rerunning the command with --debug to troubleshoot the error.
|
||||
exit status 1
|
||||
tags: []
|
||||
links: []
|
||||
usage: supabase db pull [migration name] [flags]
|
||||
@@ -3437,3 +3494,16 @@ commands:
|
||||
description: enable experimental features
|
||||
required: true
|
||||
default_value: 'false'
|
||||
- id: supabase-bootstrap
|
||||
title: supabase bootstrap
|
||||
summary: Bootstrap a Supabase project from a starter template
|
||||
tags:
|
||||
- quick-start
|
||||
links: []
|
||||
usage: supabase bootstrap [template] [flags]
|
||||
subcommands: []
|
||||
flags:
|
||||
- id: password
|
||||
name: -p, --password <string>
|
||||
description: Password to your remote Postgres database.
|
||||
default_value: ''
|
||||
@@ -33,6 +33,7 @@
|
||||
"reference_python_v2",
|
||||
"reference_csharp_v0",
|
||||
"reference_swift_v1",
|
||||
"reference_swift_v2",
|
||||
"reference_kotlin_v1",
|
||||
"reference_kotlin_v2"
|
||||
]
|
||||
@@ -48,6 +49,7 @@
|
||||
"reference_python_v2",
|
||||
"reference_csharp_v0",
|
||||
"reference_swift_v1",
|
||||
"reference_swift_v2",
|
||||
"reference_kotlin_v1",
|
||||
"reference_kotlin_v2"
|
||||
]
|
||||
@@ -986,6 +988,7 @@
|
||||
"reference_python_v2",
|
||||
"reference_csharp_v0",
|
||||
"reference_swift_v1",
|
||||
"reference_swift_v2",
|
||||
"reference_kotlin_v1",
|
||||
"reference_kotlin_v2"
|
||||
],
|
||||
|
||||
@@ -568,7 +568,7 @@ functions:
|
||||
notes: |
|
||||
- Requires either an email or phone number.
|
||||
- This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number.
|
||||
- If the user doesn't exist, `signInWithOtp()` will signup the user instead. To restrict this behaviour, you can set `shouldCreateUser` in `SignInWithPasswordlessCredentials.options` to `false`.
|
||||
- If the user doesn't exist, `signInWithOtp()` will signup the user instead. To restrict this behavior, you can set `shouldCreateUser` in `SignInWithPasswordlessCredentials.options` to `false`.
|
||||
- If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP.
|
||||
- If you're using phone, you can configure whether you want the user to receive a OTP.
|
||||
- The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls).
|
||||
@@ -4925,7 +4925,7 @@ functions:
|
||||
notes: |
|
||||
- Requires an Authorization header.
|
||||
- Invoke params generally match the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) spec.
|
||||
- When you pass in a body to your function, we automatically attach the Content-Type header for `Blob`, `ArrayBuffer`, `File`, `FormData` and `String`. If it doesn't match any of these types we assume the payload is `json`, serialise it and attach the `Content-Type` header as `application/json`. You can override this behaviour by passing in a `Content-Type` header of your own.
|
||||
- When you pass in a body to your function, we automatically attach the Content-Type header for `Blob`, `ArrayBuffer`, `File`, `FormData` and `String`. If it doesn't match any of these types we assume the payload is `json`, serialize it and attach the `Content-Type` header as `application/json`. You can override this behavior by passing in a `Content-Type` header of your own.
|
||||
- Responses are automatically parsed as `json`, `blob` and `form-data` depending on the `Content-Type` header sent by your function. Responses are parsed as `text` by default.
|
||||
examples:
|
||||
- id: basic-invocation
|
||||
@@ -4991,6 +4991,36 @@ functions:
|
||||
method: 'DELETE'
|
||||
})
|
||||
```
|
||||
- id: regional-invocation
|
||||
name: Invoking a Function in the UsEast1 region
|
||||
description: |
|
||||
Here are the available regions:
|
||||
- `FunctionRegion.Any`
|
||||
- `FunctionRegion.ApNortheast1`
|
||||
- `FunctionRegion.ApNortheast2`
|
||||
- `FunctionRegion.ApSouth1`
|
||||
- `FunctionRegion.ApSoutheast1`
|
||||
- `FunctionRegion.ApSoutheast2`
|
||||
- `FunctionRegion.CaCentral1`
|
||||
- `FunctionRegion.EuCentral1`
|
||||
- `FunctionRegion.EuWest1`
|
||||
- `FunctionRegion.EuWest2`
|
||||
- `FunctionRegion.EuWest3`
|
||||
- `FunctionRegion.SaEast1`
|
||||
- `FunctionRegion.UsEast1`
|
||||
- `FunctionRegion.UsWest1`
|
||||
- `FunctionRegion.UsWest2`
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```js
|
||||
import { createClient, FunctionRegion } from '@supabase/supabase-js'
|
||||
|
||||
const { data, error } = await supabase.functions.invoke('hello', {
|
||||
body: { foo: 'bar' },
|
||||
region: FunctionRegion.UsEast1
|
||||
})
|
||||
```
|
||||
|
||||
- id: calling-with-get-verb
|
||||
name: Calling with GET HTTP verb
|
||||
description: |
|
||||
|
||||
@@ -57,7 +57,7 @@ functions:
|
||||
scheme = "deeplink scheme"
|
||||
|
||||
// On Android only, you can set OAuth and SSO logins to open in a custom tab, rather than an external browser:
|
||||
defaultExternalAuthAction = ExternalAuthAction.CUSTOM_TABS //defaults to EXTERNAL_BROWSER
|
||||
defaultExternalAuthAction = ExternalAuthAction.CustomTabs() //defaults to ExternalAuthAction.ExternalBrowser
|
||||
}
|
||||
```
|
||||
4. **Call platform specific function on startup** \
|
||||
@@ -684,10 +684,10 @@ functions:
|
||||
isOptional: true # Not directly `optional`, as there are two overloads one with and one without `parameters`.
|
||||
type: T
|
||||
description: Parameters to pass to the function. T can be any serializable type.
|
||||
- name: head
|
||||
- name: method
|
||||
isOptional: true
|
||||
type: Boolean
|
||||
description: If true, delete will delete the selected data.
|
||||
type: RpcMethod
|
||||
description: The HTTP method to use. Defaults to `RpcMethod.POST`
|
||||
- name: request
|
||||
isOptional: true
|
||||
type: PostgrestRequestBuilder.() -> Unit
|
||||
@@ -3317,7 +3317,7 @@ functions:
|
||||
title: 'reauthenticate()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.reauthenticate'
|
||||
notes: |
|
||||
- This method is used together with `modifyUser()` when a user's password needs to be updated.
|
||||
- This method is used together with `updateUser()` when a user's password needs to be updated.
|
||||
- This method will send a nonce to the user's email. If the user doesn't have a confirmed email address, the method will send the nonce to the user's confirmed phone number instead.
|
||||
examples:
|
||||
- id: send-reauthentication-nonce
|
||||
@@ -3422,11 +3422,11 @@ functions:
|
||||
val user = supabase.auth.retrieveUser("JWT")
|
||||
```
|
||||
- id: update-user
|
||||
title: 'modifyUser()'
|
||||
title: 'updateUser()'
|
||||
$ref: '@supabase/gotrue-js.GoTrueClient.updateUser'
|
||||
notes: |
|
||||
Modifies the user data.
|
||||
- In order to use the `modifyUser()` method, the user needs to be signed in first.
|
||||
- In order to use the `updateUser()` method, the user needs to be signed in first.
|
||||
- By default, email updates sends a confirmation link to both the user's current and new email.
|
||||
To only send a confirmation link to the user's new email, disable **Secure email change** in your project's [email auth provider settings](https://supabase.com/dashboard/project/_/auth/providers).
|
||||
params:
|
||||
@@ -3469,7 +3469,7 @@ functions:
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```kotlin
|
||||
val user = supabase.auth.modifyUser {
|
||||
val user = supabase.auth.updateUser {
|
||||
email = "newEmail@email.com"
|
||||
}
|
||||
```
|
||||
@@ -3478,7 +3478,7 @@ functions:
|
||||
isSpotlight: false
|
||||
code: |
|
||||
```kotlin
|
||||
val user = supabase.auth.modifyUser {
|
||||
val user = supabase.auth.updateUser {
|
||||
password = "secretPassword"
|
||||
}
|
||||
```
|
||||
@@ -3487,7 +3487,7 @@ functions:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```kotlin
|
||||
val user = supabase.auth.modifyUser {
|
||||
val user = supabase.auth.updateUser {
|
||||
data {
|
||||
put("name", "John")
|
||||
}
|
||||
@@ -3681,11 +3681,11 @@ functions:
|
||||
Sends a password reset request to the given email address.
|
||||
- The password reset flow consist of 2 broad steps: (i) Allow the user to login via the password reset link; (ii) Update the user's password.
|
||||
- The `resetPasswordForEmail()` only sends a password reset link to the user's email.
|
||||
To update the user's password, see [`modifyUser()`](/docs/reference/kotlin/auth-updateuser).
|
||||
To update the user's password, see [`updateUser()`](/docs/reference/kotlin/auth-updateuser).
|
||||
- The user gets redirected back to your app, assuming you setup [OTP handling](/docs/reference/kotlin/initializing)
|
||||
- After the user has been redirected successfully, prompt them for a new password and call `modifyUser()`:
|
||||
- After the user has been redirected successfully, prompt them for a new password and call `updateUser()`:
|
||||
```kotlin
|
||||
supabase.auth.modifyUser {
|
||||
supabase.auth.updateUser {
|
||||
password = "1234567"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -92,7 +92,7 @@ functions:
|
||||
- To fetch the currently logged-in user, refer to [`getUser()`](/docs/reference/swift/get-user).
|
||||
examples:
|
||||
- id: sign-up
|
||||
name: Sign up
|
||||
name: Sign up with email and password
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
@@ -101,6 +101,30 @@ functions:
|
||||
password: "example-password"
|
||||
)
|
||||
```
|
||||
- id: sign-up-phone
|
||||
name: Sign up with a phone number and password (SMS)
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
try await supabase.auth.signUp(
|
||||
phone: "123456789",
|
||||
password: "example-password",
|
||||
channel: "sms"
|
||||
)
|
||||
```
|
||||
- id: sign-up-phone-whatsapp
|
||||
name: Sign up with a phone number and password (whatsapp)
|
||||
isSpotlight: true
|
||||
description: |
|
||||
The user will be sent a WhatsApp message which contains a OTP. By default, a given user can only request a OTP once every 60 seconds. Note that a user will need to have a valid WhatsApp account that is linked to Twilio in order to use this feature.
|
||||
code: |
|
||||
```swift
|
||||
try await supabase.auth.signUp(
|
||||
phone: "123456789",
|
||||
password: "example-password",
|
||||
channel: "whatsapp"
|
||||
)
|
||||
```
|
||||
- id: sign-up-with-additional-user-metadata
|
||||
name: Sign up with additional user metadata
|
||||
isSpotlight: false
|
||||
@@ -120,6 +144,7 @@ functions:
|
||||
- id: sign-up-with-redirect
|
||||
name: Sign up with a redirect URL
|
||||
description: |
|
||||
- You can provide a default redirect URL when initializing the client.
|
||||
- See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
|
||||
code: |
|
||||
```swift
|
||||
@@ -129,6 +154,69 @@ functions:
|
||||
redirectTo: URL(string: "https://example.com/welcome")!
|
||||
)
|
||||
```
|
||||
- id: on-auth-state-change
|
||||
title: "onAuthStateChange()"
|
||||
notes: |
|
||||
- Subscribes to important events occurring on the user's session.
|
||||
- Emitted events:
|
||||
- `INITIAL_SESSION`
|
||||
- Emitted right after the Supabase client is constructed and the initial session from storage is loaded.
|
||||
- `SIGNED_IN`
|
||||
- Emitted each time a user session is confirmed or re-established, including on user sign in.
|
||||
- Avoid making assumptions as to when this event is fired, this may occur even when the user is already signed in. Instead, check the user object attached to the event to see if a new user has signed in and update your application's UI.
|
||||
- `SIGNED_OUT`
|
||||
- Emitted when the user signs out. This can be after:
|
||||
- A call to `supabase.auth.signOut()`.
|
||||
- After the user's session has expired for any reason:
|
||||
- User has signed out on another device.
|
||||
- The session has reached its timebox limit or inactivity timeout.
|
||||
- User has signed in on another device with single session per user enabled.
|
||||
- Check the [User Sessions](/docs/guides/auth/sessions) docs for more information.
|
||||
- Use this to clean up any local storage your application has associated with the user.
|
||||
- `TOKEN_REFRESHED`
|
||||
- Emitted each time a new access and refresh token are fetched for the signed in user.
|
||||
- It's best practice and highly recommended to extract the access token (JWT) and store it in memory for further use in your application.
|
||||
- Avoid frequent calls to `supabase.auth.session` for the same purpose.
|
||||
- There is a background process that keeps track of when the session should be refreshed so you will always receive valid tokens by listening to this event.
|
||||
- The frequency of this event is related to the JWT expiry limit configured on your project.
|
||||
- `USER_UPDATED`
|
||||
- Emitted each time the `supabase.auth.update(user:)` method finishes successfully. Listen to it to update your application's UI based on new profile information.
|
||||
- `PASSWORD_RECOVERY`
|
||||
- Emitted instead of the `SIGNED_IN` event when the user lands on a page that includes a password recovery link in the URL.
|
||||
- Use it to show a UI to the user where they can [reset their password](/docs/guides/auth/passwords#resetting-a-users-password-forgot-password).
|
||||
|
||||
examples:
|
||||
- id: listen-to-auth-changes
|
||||
name: Listen to auth changes
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
|
||||
// Using AsyncStream
|
||||
for await (event, session) in await supabase.auth.authStateChanges {
|
||||
print(event, session)
|
||||
}
|
||||
|
||||
// Using Closure
|
||||
let subscription = await supabase.auth.onAuthStateChange { event, session in
|
||||
print(event, session)
|
||||
}
|
||||
|
||||
// call remove() to remove subscription
|
||||
subscription.remove()
|
||||
```
|
||||
description: |
|
||||
- When using closure based, remember to call `remove()` on the returned subscription.
|
||||
- When using AsyncStream, it automatically removes the subscription when AsyncStream is canceled, or finishes.
|
||||
- id: list-to-a-specific-event
|
||||
name: Listen to a specific event
|
||||
code: |
|
||||
```swift
|
||||
for await (_, session) in await supabase.auth.authStateChanges
|
||||
.filter({ $0.event == .signedIn }) {
|
||||
// handle signIn event.
|
||||
}
|
||||
```
|
||||
- id: sign-in-anonymously
|
||||
title: "signInAnonymously()"
|
||||
notes: |
|
||||
@@ -187,14 +275,14 @@ functions:
|
||||
- id: sign-in-with-otp
|
||||
title: "signInWithOTP()"
|
||||
notes: |
|
||||
- Requires either an email or phone number.
|
||||
- This method is used for passwordless sign-ins where a OTP is sent to the user's email or phone number.
|
||||
- If the user doesn't exist, `signInWithOTP()` will signup the user instead. To restrict this behavior, you can set `shouldCreateUser` to `false`.
|
||||
- If the user doesn't exist, `signInWithOTP()` will signup the user instead. To restrict this behavior, you can set `shouldCreateUser` to `false``.
|
||||
- If you're using an email, you can configure whether you want the user to receive a magiclink or a OTP.
|
||||
- If you're using phone, you can configure whether you want the user to receive a OTP.
|
||||
- The magic link's destination URL is determined by the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls).
|
||||
- See [redirect URLs and wildcards](/docs/guides/auth#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
|
||||
- Magic links and OTPs share the same implementation. To send users a one-time code instead of a magic link, [modify the magic link email template](/dashboard/project/_/auth/templates) to include `{{ .Token }}` instead of `{{ .ConfirmationURL }}`.
|
||||
- When using magic links, specify a `redirectTo` that matches a configured url scheme in your iOS app, so Supabase can correctly redirect back to your app.
|
||||
- See our [Twilio Phone Auth Guide](/docs/guides/auth/phone-login/twilio) for details about configuring WhatsApp sign in.
|
||||
examples:
|
||||
- id: sign-in-with-email
|
||||
@@ -216,13 +304,55 @@ functions:
|
||||
```swift
|
||||
try await supabase.auth.signInWithOTP(phone: "+13334445555")
|
||||
```
|
||||
- id: sign-in-with-whatsapp-otp
|
||||
name: Sign in with WhatsApp OTP
|
||||
isSpotlight: false
|
||||
description: The user will be sent a WhatsApp message which contains a OTP. By default, a given user can only request a OTP once every 60 seconds. Note that a user will need to have a valid WhatsApp account that is linked to Twilio in order to use this feature.
|
||||
code: |
|
||||
```swift
|
||||
try await supabase.auth.signInWithOTP(
|
||||
phone: "+13334445555",
|
||||
channel: "whatsapp"
|
||||
)
|
||||
```
|
||||
- id: sign-in-with-oauth
|
||||
title: "getOAuthSignInURL()"
|
||||
title: "signInWithOAuth()"
|
||||
notes: |
|
||||
- This method is used for signing in using a third-party provider.
|
||||
- Supabase supports many different [third-party providers](https://supabase.com/docs/guides/auth#providers).
|
||||
examples:
|
||||
- id: sign-in-using-a-third-party-provider
|
||||
- id: sign-in-using-ASWebAuthenticationSession
|
||||
name: Sign in with OAuth using ASWebAuthenticationSession
|
||||
isSpotlight: true
|
||||
description: |
|
||||
- Use `configure` parameter to customize the `ASWebAuthenticationSession`
|
||||
code: |
|
||||
```swift
|
||||
let session = try await supabase.auth.signInWithOAuth(
|
||||
provider: .github
|
||||
) { (session: ASWebAuthenticationSession) in
|
||||
// customize session
|
||||
}
|
||||
```
|
||||
|
||||
- id: sign-in-using-generic-flow
|
||||
name: Sign in with OAuth and customize flow
|
||||
isSpotlight: true
|
||||
description: |
|
||||
- Use `launchFlow` parameter to customize the flow.
|
||||
code: |
|
||||
```swift
|
||||
let session = try await supabase.auth.signInWithOAuth(
|
||||
provider: .github
|
||||
) { url in
|
||||
// use url to start OAuth flow
|
||||
// and return a result url that contains the OAuth token.
|
||||
// ...
|
||||
return resultURL
|
||||
}
|
||||
```
|
||||
|
||||
- id: sign-in-using-manual-implementation
|
||||
name: Sign in using a third-party provider
|
||||
isSpotlight: true
|
||||
description: |
|
||||
@@ -231,7 +361,7 @@ functions:
|
||||
- When using `ASWebAuthenticationSession` or any other implementation, use the returning URL as input to `session(from:)` method.
|
||||
code: |
|
||||
```swift
|
||||
let url = try await supabase.auth.getOAuthSignInURL(provider: .github)
|
||||
let url = try await supabase.auth.getOAuthSignInURL(provider: .github, redirectTo: URL(string: "my-app-scheme://"))
|
||||
|
||||
let session = ASWebAuthenticationSession(url: url, callbackURLScheme: "my-app-scheme") { url, error in
|
||||
guard let url else { return }
|
||||
@@ -246,21 +376,6 @@ functions:
|
||||
session.start()
|
||||
```
|
||||
|
||||
- id: sign-in-using-a-third-party-provider-with-redirect
|
||||
name: Sign in using a third-party provider with redirect
|
||||
isSpotlight: false
|
||||
description: |
|
||||
- When the third-party provider successfully authenticates the user, the provider redirects the user to the URL specified in the `redirectTo` parameter. This parameter defaults to the [`SITE_URL`](/docs/guides/auth/concepts/redirect-urls). It does not redirect the user immediately after invoking this method.
|
||||
- See [redirect URLs and wildcards](/docs/guides/auth/overview#redirect-urls-and-wildcards) to add additional redirect URLs to your project.
|
||||
- getOAuthSignInURL() provides the URL which needs to be opened in a SFSafariViewController instance.
|
||||
- The redirectTo URL needs to be setup correctly in your project under Authentication -> URL Configuration -> Redirect URLs.
|
||||
code: |
|
||||
```swift
|
||||
let url = try await supabase.auth.getOAuthSignInURL(
|
||||
provider: .google,
|
||||
redirectTo: URL(string: "https://example.com/welcome")!
|
||||
)
|
||||
```
|
||||
- id: sign-in-with-scopes
|
||||
name: Sign in with scopes
|
||||
isSpotlight: false
|
||||
@@ -269,7 +384,7 @@ functions:
|
||||
You may also need to specify the scopes in the provider's OAuth app settings, depending on the provider. The list of scopes will be documented by the third-party provider you are using and specifying scopes will enable you to use the OAuth provider token to call additional APIs supported by the third-party provider to get more information.
|
||||
code: |
|
||||
```swift
|
||||
let url = try await supabase.auth.getOAuthSignInURL(
|
||||
let url = try await supabase.auth.signInWithOAuth(
|
||||
provider: .github,
|
||||
scopes: "repo gist notifications"
|
||||
)
|
||||
@@ -290,6 +405,45 @@ functions:
|
||||
)
|
||||
)
|
||||
```
|
||||
- id: sign-in-with-sso
|
||||
title: 'signInWithSSO()'
|
||||
notes: |
|
||||
- Before you can call this method you need to [establish a connection](/docs/guides/auth/sso/auth-sso-saml#managing-saml-20-connections) to an identity provider. Use the [CLI commands](/docs/reference/cli/supabase-sso) to do this.
|
||||
- If you've associated an email domain to the identity provider, you can use the `domain` property to start a sign-in flow.
|
||||
- In case you need to use a different way to start the authentication flow with an identity provider, you can use the `providerId` property. For example:
|
||||
- Mapping specific user email addresses with an identity provider.
|
||||
- Using different hints to identity the identity provider to be used by the user, like a company-specific page, IP address or other tracking information.
|
||||
examples:
|
||||
- id: sign-in-with-domain
|
||||
name: Sign in with email domain
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
// You can extract the user's email domain and use it to trigger the
|
||||
// authentication flow with the correct identity provider.
|
||||
|
||||
let url = try await await supabase.auth.signInWithSSO{
|
||||
domain: "company.com"
|
||||
}
|
||||
|
||||
// Open the URL using your preferred method to complete sign-in process.
|
||||
UIApplication.shared.open(url)
|
||||
```
|
||||
- id: sign-in-with-provider-uuid
|
||||
name: Sign in with provider UUID
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
// Useful when you need to map a user's sign in request according
|
||||
// to different rules that can't use email domains.
|
||||
|
||||
let url = try await supabase.auth.signInWithSSO{
|
||||
providerId: "21648a9d-8d5a-4555-a9d1-d6375dc14e92"
|
||||
}
|
||||
|
||||
// Open the URL using your preferred method to complete sign-in process.
|
||||
UIApplication.shared.open(url)
|
||||
```
|
||||
- id: sign-out
|
||||
title: "signOut()"
|
||||
notes: |
|
||||
@@ -410,6 +564,30 @@ functions:
|
||||
```swift
|
||||
let identities = try await supabase.auth.userIdentities()
|
||||
```
|
||||
- id: unlink-identity
|
||||
title: 'unlinkIdentity()'
|
||||
notes: |
|
||||
- The **Enable Manual Linking** option must be enabled from your [project's authentication settings](/dashboard/project/_/settings/auth).
|
||||
- The user needs to be signed in to call `unlinkIdentity()`.
|
||||
- The user must have at least 2 identities in order to unlink an identity.
|
||||
- The identity to be unlinked must belong to the user.
|
||||
examples:
|
||||
- id: unlink-identity
|
||||
name: Unlink an identity
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
// retrieve all identites linked to a user
|
||||
let identities = try await supabase.auth.userIdentities()
|
||||
|
||||
// find the google identity
|
||||
let googleIdentity = identities.first {
|
||||
$0.provider == .google
|
||||
}
|
||||
|
||||
// unlink the google identity
|
||||
try await supabase.auth.unlinkIdentity(googleIdentity)
|
||||
```
|
||||
- id: send-password-reauthentication
|
||||
title: "reauthenticate()"
|
||||
notes: |
|
||||
@@ -512,29 +690,7 @@ functions:
|
||||
```swift
|
||||
let session = try await supabase.auth.refreshSession(refreshToken: "custom-refresh-token")
|
||||
```
|
||||
- id: on-auth-state-change
|
||||
title: "authStateChanges"
|
||||
notes: |
|
||||
- Types of auth events: `INITIAL_SESSION`, `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `USER_UPDATED`, `PASSWORD_RECOVERY`, `MFA_CHALLENGE_VERIFIED`
|
||||
- The `INITIAL_SESSION` can be used to allow you to invoke the callback function when `authStateChanges` is first called.
|
||||
examples:
|
||||
- id: listen-to-auth-changes
|
||||
name: Listen to auth changes
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
for await (event, session) in await supabase.auth.authStateChanges {
|
||||
print(event, session)
|
||||
}
|
||||
```
|
||||
- id: list-to-a-specific-event
|
||||
name: Listen to a specific event
|
||||
code: |
|
||||
```swift
|
||||
for await (_, session) in await supabase.auth.authStateChanges.filter({ $0.event == .signedIn }) {
|
||||
// handle signIn event.
|
||||
}
|
||||
```
|
||||
|
||||
- id: exchange-code-for-session
|
||||
title: "exchangeCodeForSession()"
|
||||
notes: |
|
||||
@@ -679,6 +835,41 @@ functions:
|
||||
```swift
|
||||
let factors = try await supabase.auth.mfa.listFactors()
|
||||
```
|
||||
- id: admin-api
|
||||
title: 'Overview'
|
||||
notes: |
|
||||
- Any method under the `supabase.auth.admin` namespace requires a `service_role` key.
|
||||
- These methods are considered admin methods and should be called on a trusted server. Never expose your `service_role` key in the browser.
|
||||
examples:
|
||||
- id: create-auth-admin-client
|
||||
name: Create server-side auth client
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
import Supabase
|
||||
|
||||
let supabase = SupabaseClient(
|
||||
supabaseURL: supabaseURL,
|
||||
supabaseKey: serviceRoleKey
|
||||
)
|
||||
|
||||
// Access auth admin api
|
||||
let adminAuthClient = supabase.auth.admin
|
||||
```
|
||||
- id: delete-user
|
||||
title: 'deleteUser()'
|
||||
notes: |
|
||||
- The `deleteUser()` method requires the user's ID, which maps to the `auth.users.id` column.
|
||||
examples:
|
||||
- id: removes-a-user
|
||||
name: Removes a user
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
try await supabase.auth.admin.deleteUser(
|
||||
id: "715ed5db-f090-4b8c-a067-640ecee36aa0"
|
||||
)
|
||||
```
|
||||
- id: select
|
||||
title: "Fetch data: select()"
|
||||
notes: |
|
||||
@@ -2899,133 +3090,140 @@ functions:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let channel = supabase
|
||||
.realtime
|
||||
.channel("room1")
|
||||
let channel = await supabase.channel("room1")
|
||||
|
||||
channel
|
||||
.on("broadcast", filter: ChannelFilter(event: "cursor-pos")) { message in
|
||||
print("Cursor position received!", message.payload)
|
||||
}
|
||||
.subscribe { status, error in
|
||||
if status == .subscribed {
|
||||
Task {
|
||||
await channel.send(
|
||||
type: .broadcast,
|
||||
event: "cursor-pos",
|
||||
payload: ["x": Double.random(in: 0...1), "y": Double.random(in: 0...1)]
|
||||
)
|
||||
}
|
||||
}
|
||||
let broadcastStream = await channel.broadcastStream(event: "cursor-pos")
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
Task {
|
||||
for await message in broadcastMessage {
|
||||
print("Cursor position received", message.payload)
|
||||
}
|
||||
}
|
||||
|
||||
Task {
|
||||
await channel.broadcast(
|
||||
event: "cursor-pos",
|
||||
message: [
|
||||
"x": .double(.random(in: 0...1)),
|
||||
"y": .double(.random(in: 0...1))
|
||||
]
|
||||
)
|
||||
}
|
||||
```
|
||||
- id: listen-to-presence-sync
|
||||
name: Listen to presence sync
|
||||
isSpotlight: true
|
||||
|
||||
- id: listen-to-presence-updates
|
||||
name: Listen to presence updates
|
||||
code: |
|
||||
```swift
|
||||
let channel = supabase.realtime.channel("room1")
|
||||
channel
|
||||
.on("presence", filter: ChannelFilter(event: "sync")) { _ in
|
||||
print("Synced presence state: ", channel.presenceState())
|
||||
struct PresenceState: Codable {
|
||||
let username: String
|
||||
}
|
||||
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
let presenceChange = await channel.presenceChange()
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
Task {
|
||||
for await presence in presenceChange {
|
||||
let joins = try presence.decodeJoins(as: PresenceState.self)
|
||||
let leaves = try presence.decodeLeaves(as: PresenceState.self)
|
||||
}
|
||||
.subscribe { status, error in
|
||||
if status == .subscribed {
|
||||
Task {
|
||||
await channel.track(["online_at": Date().ISO8601Format()])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- id: listen-to-presence-join
|
||||
name: Listen to presence join
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let channel = supabase.realtime.channel("room1")
|
||||
channel
|
||||
.on("presence", filter: ChannelFilter(event: "join")) { message in
|
||||
print("Newly joined presences: ", message.payload)
|
||||
}
|
||||
.subscribe { status, error in
|
||||
if status == .subscribed {
|
||||
Task {
|
||||
await channel.track(["online_at": Date().ISO8601Format()])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- id: listen-to-presence-leave
|
||||
name: Listen to presence leave
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let channel = supabase.realtime.channel("room1")
|
||||
channel
|
||||
.on("presence", filter: ChannelFilter(event: "leave")) { message in
|
||||
print("Newly left presences: ", message.payload)
|
||||
}
|
||||
.subscribe { status, error in
|
||||
if status == .subscribed {
|
||||
Task {
|
||||
await channel.track(["online_at": Date().ISO8601Format()])
|
||||
await channel.untrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
|
||||
// Send your own state
|
||||
Task {
|
||||
try await channel.track(PresenceState(username: "John"))
|
||||
}
|
||||
|
||||
- id: listen-to-all-database-changes
|
||||
name: Listen to all database changes
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "*", schema: "*")) { message in
|
||||
print("Change received!", message.payload)
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
let changeStream = await channel.postgresChange(AnyAction.self, schema: "public")
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
for await change in changeStream {
|
||||
switch change {
|
||||
case .delete(let action): print("Deleted: \(action.oldRecord)")
|
||||
case .insert(let action): print("Inserted: \(action.record)")
|
||||
case .select(let action): print("Selected: \(action.record)")
|
||||
case .update(let action): print("Updated": \(action.oldRecord) with \(action.record)")
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
```
|
||||
- id: listen-to-a-specific-table
|
||||
name: Listen to a specific table
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "*", schema: "public", table: "countries")) { message in
|
||||
print("Change received!", message.payload)
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
let changeStream = await channel.postgresChange(
|
||||
AnyAction.self,
|
||||
schema: "public",
|
||||
table: "users"
|
||||
)
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
for await change in changeStream {
|
||||
switch change {
|
||||
case .delete(let action): print("Deleted: \(action.oldRecord)")
|
||||
case .insert(let action): print("Inserted: \(action.record)")
|
||||
case .select(let action): print("Selected: \(action.record)")
|
||||
case .update(let action): print("Updated": \(action.oldRecord) with \(action.record)")
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
```
|
||||
- id: listen-to-inserts
|
||||
name: Listen to inserts
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "INSERT", schema: "public", table: "countries")) { message in
|
||||
print("Change received!", message.payload)
|
||||
}
|
||||
.subscribe()
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
let insertions = await channel.postgresChange(
|
||||
InsertAction.self,
|
||||
schema: "public",
|
||||
table: "users"
|
||||
)
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
for await insert in insertions {
|
||||
print("Inserted: \(insert.record)")
|
||||
}
|
||||
```
|
||||
- id: listen-to-updates
|
||||
name: Listen to updates
|
||||
description: |
|
||||
By default, Supabase will send only the updated record. If you want to receive the previous values as well you can
|
||||
enable full replication for the table you are listening to:
|
||||
enable full replication for the table you are listening too:
|
||||
|
||||
```sql
|
||||
alter table "your_table" replica identity full;
|
||||
```
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "UPDATE", schema: "public", table: "countries")) { message in
|
||||
print("Change received!", message.payload)
|
||||
}
|
||||
.subscribe()
|
||||
```
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
let updates = await channel.postgresChange(
|
||||
UpdateAction.self,
|
||||
schema: "public",
|
||||
table: "users"
|
||||
)
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
for await update in updates {
|
||||
print("Updated: \(update.oldRecord) with \(update.record)")
|
||||
}
|
||||
```
|
||||
- id: listen-to-deletes
|
||||
name: Listen to deletes
|
||||
description: |
|
||||
@@ -3037,131 +3235,96 @@ functions:
|
||||
```
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "DELETE", schema: "public", table: "countries")) { message in
|
||||
print("Change received!", message.payload)
|
||||
}
|
||||
.subscribe()
|
||||
```
|
||||
- id: listen-to-multiple-events
|
||||
name: Listen to multiple events
|
||||
description: You can chain listeners if you want to listen to multiple events for each table.
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "INSERT", schema: "public", table: "countries"), handler: handleRecordInserted)
|
||||
.on("postgres_changes", filter: ChannelFilter(event: "DELETE", schema: "public", table: "countries"), handler: handleRecordDeleted)
|
||||
.subscribe()
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
func handleRecordInserted(_ message: Message) {
|
||||
// handle message
|
||||
}
|
||||
let deletions = await channel.postgresChange(
|
||||
DeleteAction.self,
|
||||
schema: "public",
|
||||
table: "users"
|
||||
)
|
||||
|
||||
func handleRecordDeleted(_ message: Message) {
|
||||
// handle message
|
||||
await channel.subscribe()
|
||||
|
||||
for await deletion in deletions {
|
||||
print("Deleted: \(deletion.oldRecord)")
|
||||
}
|
||||
```
|
||||
- id: listening-to-row-level-changes
|
||||
name: Listen to row level changes
|
||||
description: You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match.
|
||||
notes: |
|
||||
- ``eq`` filter works with all database types as under the hood, it's casting both the filter value and the database value to the correct type and then comparing them.
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.on(
|
||||
"postgres_changes",
|
||||
filter: ChannelFilter(
|
||||
event: "INSERT",
|
||||
schema: "public",
|
||||
table: "countries",
|
||||
filter: "id=eq.200"
|
||||
),
|
||||
handler: handleRecordInserted
|
||||
)
|
||||
.subscribe()
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
func handleRecordInserted(_ message: Message) {
|
||||
// handle message
|
||||
}
|
||||
```
|
||||
- id: broadcast-message
|
||||
title: broadcastMessage()
|
||||
description: |
|
||||
Broadcast a message to all connected clients to a channel.
|
||||
notes: |
|
||||
- When using REST you don't need to subscribe to the channel
|
||||
examples:
|
||||
- id: send-a-message
|
||||
name: Send a message via websocket
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime
|
||||
.channel("room1")
|
||||
.subscribe { status, error in
|
||||
if status == .subscribed {
|
||||
Task {
|
||||
await channel.send(
|
||||
type: "broadcast",
|
||||
event: "cursor-pos",
|
||||
payload: ["x": Double.random(in: 0...1), "y": Double.random(in: 0...1)]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- id: send-a-message-via-rest
|
||||
name: Send a message via REST
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
await supabase.realtime
|
||||
.channel("room1")
|
||||
.send(
|
||||
type: "broadcast",
|
||||
event: "cursor-pos",
|
||||
payload: ["x": Double.random(in: 0...1), "y": Double.random(in: 0...1)]
|
||||
)
|
||||
```
|
||||
- id: get-channels
|
||||
title: channels
|
||||
examples:
|
||||
- id: get-all-channels
|
||||
name: Get all channels
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let channels = supabase.realtime.channels
|
||||
let deletions = await channel.postgresChange(
|
||||
DeleteAction.self,
|
||||
schema: "public",
|
||||
table: "users",
|
||||
filter: "id=eq.1"
|
||||
)
|
||||
|
||||
await channel.subscribe()
|
||||
|
||||
for await deletion in deletions {
|
||||
print("Deleted: \(deletion.oldRecord)")
|
||||
}
|
||||
```
|
||||
|
||||
- id: remove-channel
|
||||
title: removeChannel()
|
||||
description: |
|
||||
Unsubscribes and removes Realtime channel from Realtime client.
|
||||
title: "removeChannel()"
|
||||
notes: |
|
||||
- Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
|
||||
- Removing a channel is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes.
|
||||
- Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
|
||||
- If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false.
|
||||
examples:
|
||||
- id: removes-a-channel
|
||||
name: Removes a channel
|
||||
name: Remove a channel
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime.remove(myChannel)
|
||||
```
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
//...
|
||||
await supabase.removeChannel(channel)
|
||||
```
|
||||
- id: unsubscribe-channel
|
||||
name: Unsubscribe from a channel
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let channel = await supabase.channel("channelId")
|
||||
|
||||
//...
|
||||
await channel.unsubscribe()
|
||||
```
|
||||
- id: remove-all-channels
|
||||
title: removeAllChannels()
|
||||
$ref: "@supabase/supabase-js.index.SupabaseClient.removeAllChannels"
|
||||
notes: |
|
||||
Unsubscribes and removes all Realtime channels from Realtime client.
|
||||
- Removing channels is a great way to maintain the performance of your project's Realtime service as well as your database if you're listening to Postgres changes. Supabase will automatically handle cleanup 30 seconds after a client is disconnected, but unused channels may cause degradation as more clients are simultaneously subscribed.
|
||||
- If you removed all channels, the client automatically disconnects from the Realtime websocket. This can be disabled in the Realtime config by setting `disconnectOnNoSubscriptions` to false.
|
||||
examples:
|
||||
- id: remove-all-channels
|
||||
name: Remove all channels
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
supabase.realtime.removeAllChannels()
|
||||
await supabase.removeAllChannels()
|
||||
```
|
||||
- id: get-channels
|
||||
title: getChannels()
|
||||
$ref: "@supabase/supabase-js.index.SupabaseClient.getChannels"
|
||||
notes: |
|
||||
Returns all Realtime channels.
|
||||
examples:
|
||||
- id: get-all-channels
|
||||
name: Get all channels
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```swift
|
||||
let channels = await supabase.channels
|
||||
```
|
||||
|
||||
- id: list-buckets
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_ } from 'ui'
|
||||
import { CriticalIcon } from 'ui-patterns/Icons/StatusIcons'
|
||||
import { DeleteAccountButton } from './DeleteAccountButton'
|
||||
|
||||
export const AccountDeletion = () => {
|
||||
return (
|
||||
<>
|
||||
<Panel className="[&>div:first-child]:uppercase" title="Danger Zone">
|
||||
<Panel.Content>
|
||||
<Alert_Shadcn_ variant="destructive">
|
||||
<CriticalIcon />
|
||||
<AlertTitle_Shadcn_>Request for account deletion</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Deleting your account is permanent and cannot be undone. Your data will be deleted
|
||||
within 30 days, except we may retain some metadata and logs for longer where required
|
||||
or permitted by law.
|
||||
</AlertDescription_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="mt-3">
|
||||
<DeleteAccountButton />
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { SupportCategories } from '@supabase/shared-types/out/constants'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { useSendSupportTicketMutation } from 'data/feedback/support-ticket-send'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogSection,
|
||||
DialogSectionSeparator,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
FormItem_Shadcn_,
|
||||
FormLabel_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Input_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
export const DeleteAccountButton = () => {
|
||||
const { profile } = useProfile()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const { data: organizations, isSuccess } = useOrganizationsQuery()
|
||||
|
||||
const accountEmail = profile?.primary_email
|
||||
const FormSchema = z.object({ account: z.string() })
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
mode: 'onBlur',
|
||||
reValidateMode: 'onBlur',
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: { account: '' },
|
||||
})
|
||||
const { account } = form.watch()
|
||||
|
||||
const { mutate: submitSupportTicket, isLoading } = useSendSupportTicketMutation({
|
||||
onSuccess: () => {
|
||||
setIsOpen(false)
|
||||
toast.success(
|
||||
'Successfully submitted account deletion request - we will reach out to you via email once the request is completed!',
|
||||
{ duration: 8000 }
|
||||
)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(`Failed to submit account deletion request: ${error}`)
|
||||
},
|
||||
})
|
||||
|
||||
const onConfirmDelete = async () => {
|
||||
if (!accountEmail) return console.error('Account information is required')
|
||||
|
||||
const payload = {
|
||||
subject: 'Account Deletion Request',
|
||||
message: 'I want to delete my account.',
|
||||
category: SupportCategories.PROBLEM,
|
||||
severity: 'Low',
|
||||
allowSupportAccess: false,
|
||||
verified: true,
|
||||
projectRef: 'no-project',
|
||||
}
|
||||
|
||||
submitSupportTicket(payload)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && form !== undefined) form.reset({ account: '' })
|
||||
}, [form, isOpen])
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="danger" loading={!accountEmail}>
|
||||
Request to delete account
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="!w-[500px]">
|
||||
<DialogHeader className="pb-0">
|
||||
{(organizations ?? []).length > 0 ? (
|
||||
<>
|
||||
<DialogTitle>Leave all organizations before requesting account deletion</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will allow us to process your account deletion request faster
|
||||
</DialogDescription>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Deleting your account is permanent and{' '}
|
||||
<span className="text-foreground">cannot</span> be undone
|
||||
</DialogDescription>
|
||||
</>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
{organizations.length > 0 ? (
|
||||
<DialogSection className="!pt-0">
|
||||
<span className="text-sm text-foreground flex flex-col gap-y-2">
|
||||
Before submitting an account deletion request, please ensure that your account is
|
||||
not part of any organization. This can be done by leaving leaving or deleting the
|
||||
organizations that you are a part of.
|
||||
</span>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
size="medium"
|
||||
className="mt-6"
|
||||
onClick={() => setIsOpen(false)}
|
||||
>
|
||||
Understood
|
||||
</Button>
|
||||
</DialogSection>
|
||||
) : (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form
|
||||
id="account-deletion-request"
|
||||
className="flex flex-col gap-y-4"
|
||||
onSubmit={form.handleSubmit(() => onConfirmDelete())}
|
||||
>
|
||||
<FormField_Shadcn_
|
||||
name="account"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="px-7">
|
||||
<FormLabel_Shadcn_>
|
||||
Please type{' '}
|
||||
<span className="font-bold">{profile?.primary_email ?? ''}</span> to
|
||||
confirm
|
||||
</FormLabel_Shadcn_>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
autoFocus
|
||||
{...field}
|
||||
autoComplete="off"
|
||||
disabled={isLoading}
|
||||
placeholder="Enter the account above"
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
<DialogSectionSeparator />
|
||||
<div className="px-7 pb-4">
|
||||
<Button
|
||||
block
|
||||
size="small"
|
||||
type="danger"
|
||||
htmlType="submit"
|
||||
loading={isLoading}
|
||||
disabled={account !== accountEmail || isLoading}
|
||||
>
|
||||
Submit request for account deletion
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
+8
-8
@@ -3,11 +3,11 @@ import { Lock } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import { Button } from 'ui'
|
||||
import { generateAlterPolicyQuery, generateCreatePolicyQuery } from './AIPolicyEditorPanel.utils'
|
||||
|
||||
interface LockedCreateQuerySection {
|
||||
schema: string
|
||||
selectedPolicy?: PostgresPolicy
|
||||
formFields: { name: string; table: string; behavior: string; command: string; roles: string }
|
||||
editorOneRef: any
|
||||
@@ -15,6 +15,7 @@ interface LockedCreateQuerySection {
|
||||
}
|
||||
|
||||
export const LockedCreateQuerySection = ({
|
||||
schema,
|
||||
selectedPolicy,
|
||||
formFields,
|
||||
editorOneRef,
|
||||
@@ -22,7 +23,6 @@ export const LockedCreateQuerySection = ({
|
||||
}: LockedCreateQuerySection) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const state = useTableEditorStateSnapshot()
|
||||
|
||||
const isEditing = selectedPolicy !== undefined
|
||||
const { name, table, behavior, command, roles } = formFields
|
||||
@@ -44,7 +44,7 @@ export const LockedCreateQuerySection = ({
|
||||
const query = isEditing
|
||||
? generateCreatePolicyQuery({
|
||||
name,
|
||||
schema: state.selectedSchemaName,
|
||||
schema,
|
||||
table,
|
||||
behavior,
|
||||
command,
|
||||
@@ -58,7 +58,7 @@ export const LockedCreateQuerySection = ({
|
||||
: generateAlterPolicyQuery({
|
||||
name: '',
|
||||
newName: name,
|
||||
schema: state.selectedSchemaName,
|
||||
schema,
|
||||
table,
|
||||
command,
|
||||
roles: roles.length === 0 ? 'public' : roles,
|
||||
@@ -84,7 +84,7 @@ export const LockedCreateQuerySection = ({
|
||||
<div className="flex items-start" style={{ fontSize: '14px' }}>
|
||||
<p className="px-6 font-mono text-sm text-foreground-light select-none">2</p>
|
||||
<p className="font-mono tracking-tighter">
|
||||
<span className="text-[#569cd6]">on</span> "{state.selectedSchemaName}"."
|
||||
<span className="text-[#569cd6]">on</span> "{schema}"."
|
||||
{table}"
|
||||
</p>
|
||||
</div>
|
||||
@@ -124,16 +124,16 @@ export const LockedCreateQuerySection = ({
|
||||
export const LockedRenameQuerySection = ({
|
||||
oldName,
|
||||
newName,
|
||||
schema,
|
||||
table,
|
||||
lineNumber,
|
||||
}: {
|
||||
oldName: string
|
||||
newName: string
|
||||
schema: string
|
||||
table: string
|
||||
lineNumber: number
|
||||
}) => {
|
||||
const state = useTableEditorStateSnapshot()
|
||||
|
||||
return (
|
||||
<div className="bg-surface-300 py-1">
|
||||
<div className="flex items-center" style={{ fontSize: '14px' }}>
|
||||
@@ -153,7 +153,7 @@ export const LockedRenameQuerySection = ({
|
||||
</p>
|
||||
</div>
|
||||
<p className="font-mono tracking-tighter">
|
||||
<span className="text-[#569cd6]">on</span> "{state.selectedSchemaName}"."{table}"
|
||||
<span className="text-[#569cd6]">on</span> "{schema}"."{table}"
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ fontSize: '14px' }}>
|
||||
|
||||
+5
-6
@@ -1,10 +1,8 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import {
|
||||
FormControl_Shadcn_,
|
||||
FormField_Shadcn_,
|
||||
@@ -24,6 +22,7 @@ import {
|
||||
import { MultiSelectV2 } from 'ui-patterns/MultiSelect/MultiSelectV2'
|
||||
|
||||
interface PolicyDetailsV2Props {
|
||||
schema: string
|
||||
searchString?: string
|
||||
isEditing: boolean
|
||||
form: any
|
||||
@@ -31,18 +30,18 @@ interface PolicyDetailsV2Props {
|
||||
}
|
||||
|
||||
export const PolicyDetailsV2 = ({
|
||||
schema,
|
||||
searchString,
|
||||
isEditing,
|
||||
form,
|
||||
onUpdateCommand,
|
||||
}: PolicyDetailsV2Props) => {
|
||||
const { project } = useProjectContext()
|
||||
const snap = useTableEditorStateSnapshot()
|
||||
|
||||
const { data: tables, isSuccess: isSuccessTables } = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
schema: snap.selectedSchemaName,
|
||||
schema: schema,
|
||||
sortByProperty: 'name',
|
||||
includeColumns: true,
|
||||
})
|
||||
@@ -66,7 +65,7 @@ export const PolicyDetailsV2 = ({
|
||||
if (!isEditing) {
|
||||
const table = tables?.find(
|
||||
(table) =>
|
||||
table.schema === snap.selectedSchemaName &&
|
||||
table.schema === schema &&
|
||||
(table.id.toString() === searchString || table.name === searchString)
|
||||
)
|
||||
if (table) {
|
||||
@@ -117,7 +116,7 @@ export const PolicyDetailsV2 = ({
|
||||
onValueChange={(value) => form.setValue('table', value)}
|
||||
>
|
||||
<SelectTrigger_Shadcn_ className="text-sm h-10">
|
||||
{snap.selectedSchemaName}.{field.value}
|
||||
{schema}.{field.value}
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
<SelectGroup_Shadcn_>
|
||||
|
||||
+3
-6
@@ -9,9 +9,9 @@ import CardButton from 'components/ui/CardButton'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import NoSearchResults from 'components/ui/NoSearchResults'
|
||||
import { getGeneralPolicyTemplates } from '../PolicyEditorModal/PolicyEditorModal.constants'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
|
||||
interface PolicyTemplatesProps {
|
||||
schema: string
|
||||
table: string
|
||||
selectedPolicy?: PostgresPolicy
|
||||
selectedTemplate?: string
|
||||
@@ -19,17 +19,14 @@ interface PolicyTemplatesProps {
|
||||
}
|
||||
|
||||
export const PolicyTemplates = ({
|
||||
schema,
|
||||
table,
|
||||
selectedPolicy,
|
||||
selectedTemplate,
|
||||
onSelectTemplate,
|
||||
}: PolicyTemplatesProps) => {
|
||||
const [search, setSearch] = useState('')
|
||||
const state = useTableEditorStateSnapshot()
|
||||
const templates = getGeneralPolicyTemplates(
|
||||
state.selectedSchemaName,
|
||||
table.length > 0 ? table : 'table_name'
|
||||
)
|
||||
const templates = getGeneralPolicyTemplates(schema, table.length > 0 ? table : 'table_name')
|
||||
|
||||
const baseTemplates =
|
||||
selectedPolicy !== undefined
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Checkbox_Shadcn_,
|
||||
Form_Shadcn_,
|
||||
Label_Shadcn_,
|
||||
Modal,
|
||||
ScrollArea,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -45,7 +44,6 @@ import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { BASE_PATH, OPT_IN_TAGS } from 'lib/constants'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import Telemetry from 'lib/telemetry'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { AIPolicyChat } from './AIPolicyChat'
|
||||
import {
|
||||
@@ -70,6 +68,7 @@ const DiffEditor = dynamic(
|
||||
|
||||
interface AIPolicyEditorPanelProps {
|
||||
visible: boolean
|
||||
schema: string
|
||||
searchString?: string
|
||||
selectedPolicy?: PostgresPolicy
|
||||
onSelectCancel: () => void
|
||||
@@ -80,6 +79,7 @@ interface AIPolicyEditorPanelProps {
|
||||
*/
|
||||
export const AIPolicyEditorPanel = memo(function ({
|
||||
visible,
|
||||
schema,
|
||||
searchString,
|
||||
selectedPolicy,
|
||||
onSelectCancel,
|
||||
@@ -91,7 +91,6 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const state = useTableEditorStateSnapshot()
|
||||
const isAiAssistantEnabled = useIsRLSAIAssistantEnabled()
|
||||
|
||||
// [Joshen] Hyrid form fields, just spit balling to get a decent POC out
|
||||
@@ -330,7 +329,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
if (selectedPolicy === undefined) {
|
||||
const sql = generateCreatePolicyQuery({
|
||||
name: name,
|
||||
schema: state.selectedSchemaName,
|
||||
schema,
|
||||
table,
|
||||
behavior,
|
||||
command,
|
||||
@@ -534,6 +533,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
) : (
|
||||
<>
|
||||
<PolicyDetailsV2
|
||||
schema={schema}
|
||||
searchString={searchString}
|
||||
isEditing={selectedPolicy !== undefined}
|
||||
form={form}
|
||||
@@ -544,6 +544,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
/>
|
||||
<div className="h-full">
|
||||
<LockedCreateQuerySection
|
||||
schema={schema}
|
||||
selectedPolicy={selectedPolicy}
|
||||
editorOneRef={editorOneRef}
|
||||
editorTwoRef={editorTwoRef}
|
||||
@@ -670,6 +671,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
<LockedRenameQuerySection
|
||||
oldName={selectedPolicy.name}
|
||||
newName={name}
|
||||
schema={schema}
|
||||
table={table}
|
||||
lineNumber={
|
||||
8 + expOneLineCount + (showCheckBlock ? expTwoLineCount : 0)
|
||||
@@ -789,6 +791,7 @@ export const AIPolicyEditorPanel = memo(function ({
|
||||
>
|
||||
<ScrollArea className="h-full w-full">
|
||||
<PolicyTemplates
|
||||
schema={schema}
|
||||
table={table}
|
||||
selectedPolicy={selectedPolicy}
|
||||
selectedTemplate={selectedDiff}
|
||||
|
||||
@@ -14,10 +14,10 @@ import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import InformationBox from 'components/ui/InformationBox'
|
||||
import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation'
|
||||
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
|
||||
import { useTableEditorStateSnapshot } from 'state/table-editor'
|
||||
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
|
||||
|
||||
interface PoliciesProps {
|
||||
schema: string
|
||||
tables: PostgresTable[]
|
||||
hasTables: boolean
|
||||
isLocked: boolean
|
||||
@@ -25,6 +25,7 @@ interface PoliciesProps {
|
||||
}
|
||||
|
||||
const Policies = ({
|
||||
schema,
|
||||
tables,
|
||||
hasTables,
|
||||
isLocked,
|
||||
@@ -33,7 +34,6 @@ const Policies = ({
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
const snap = useTableEditorStateSnapshot()
|
||||
|
||||
const [selectedTableToToggleRLS, setSelectedTableToToggleRLS] = useState<any>({})
|
||||
const [selectedPolicyToDelete, setSelectedPolicyToDelete] = useState<any>({})
|
||||
@@ -137,7 +137,7 @@ const Policies = ({
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-4 pb-4">
|
||||
{isLocked && <ProtectedSchemaWarning schema={snap.selectedSchemaName} entity="policies" />}
|
||||
{isLocked && <ProtectedSchemaWarning schema={schema} entity="policies" />}
|
||||
{tables.length > 0 ? (
|
||||
tables.map((table: any) => (
|
||||
<section key={table.id}>
|
||||
|
||||
@@ -43,7 +43,7 @@ const PolicyRoles = ({ selectedRoles, onUpdateSelectedRoles }: PolicyRolesProps)
|
||||
</div>
|
||||
<div className="relative w-2/3">
|
||||
{isLoading && <ShimmeringLoader className="py-4" />}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve database roles" />}
|
||||
{isError && <AlertError error={error as any} subject="Failed to retrieve database roles" />}
|
||||
{isSuccess && (
|
||||
<MultiSelect
|
||||
options={formattedRoles}
|
||||
|
||||
@@ -48,10 +48,10 @@ const UserListItem = ({
|
||||
<span className="text-foreground">{createdAt?.format('DD MMM, YYYY HH:mm')}</span>
|
||||
</Table.td>
|
||||
<Table.td className="table-cell">
|
||||
{!isUserConfirmed ? (
|
||||
<Badge variant="warning">Waiting for verification..</Badge>
|
||||
) : user.last_sign_in_at ? (
|
||||
{user?.last_sign_in_at ? (
|
||||
lastSignedIn?.format('DD MMM, YYYY HH:mm')
|
||||
) : !isUserConfirmed ? (
|
||||
<Badge variant="warning">Waiting for verification..</Badge>
|
||||
) : (
|
||||
'Never'
|
||||
)}
|
||||
|
||||
@@ -17,3 +17,16 @@ export const subscriptionHasHipaaAddon = (subscription?: OrgSubscription): boole
|
||||
(addon) => addon.supabase_prod_id === 'addon_security_hipaa'
|
||||
)
|
||||
}
|
||||
|
||||
export const billingPartnerLabel = (billingPartner?: string) => {
|
||||
if (!billingPartner) return billingPartner
|
||||
|
||||
switch (billingPartner) {
|
||||
case 'fly':
|
||||
return 'Fly.io'
|
||||
case 'aws':
|
||||
return 'AWS'
|
||||
default:
|
||||
return billingPartner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import { Button, Input, Modal } from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const CreateWarehouseAccessToken = (props: Props) => {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="outline" onClick={() => setOpen(true)}>
|
||||
Create access token
|
||||
</Button>
|
||||
<Modal
|
||||
size="medium"
|
||||
onCancel={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
header="Create access token"
|
||||
visible={open}
|
||||
alignFooter="right"
|
||||
onConfirm={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Modal.Content>
|
||||
<form>
|
||||
<FormItemLayout
|
||||
label="Description"
|
||||
description="A short description for identifying what this access token is to be used for."
|
||||
isReactForm={false}
|
||||
>
|
||||
<Input placeholder="Description" />
|
||||
</FormItemLayout>
|
||||
</form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateWarehouseAccessToken
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import CreateWarehouseAccessToken from './CreateWarehouseAccessToken'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const WarehouseAccessTokens = (props: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<CreateWarehouseAccessToken />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WarehouseAccessTokens
|
||||
@@ -26,22 +26,22 @@ interface CreateRolePanelProps {
|
||||
|
||||
const FormSchema = z.object({
|
||||
name: z.string().trim().min(1, 'You must provide a name').default(''),
|
||||
is_superuser: z.boolean().default(false),
|
||||
can_login: z.boolean().default(false),
|
||||
can_create_role: z.boolean().default(false),
|
||||
can_create_db: z.boolean().default(false),
|
||||
is_replication_role: z.boolean().default(false),
|
||||
can_bypass_rls: z.boolean().default(false),
|
||||
isSuperuser: z.boolean().default(false),
|
||||
canLogin: z.boolean().default(false),
|
||||
canCreateRole: z.boolean().default(false),
|
||||
canCreateDb: z.boolean().default(false),
|
||||
isReplicationRole: z.boolean().default(false),
|
||||
canBypassRls: z.boolean().default(false),
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
name: '',
|
||||
is_superuser: false,
|
||||
can_login: false,
|
||||
can_create_role: false,
|
||||
can_create_db: false,
|
||||
is_replication_role: false,
|
||||
can_bypass_rls: false,
|
||||
isSuperuser: false,
|
||||
canLogin: false,
|
||||
canCreateRole: false,
|
||||
canCreateDb: false,
|
||||
isReplicationRole: false,
|
||||
canBypassRls: false,
|
||||
}
|
||||
|
||||
const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
@@ -54,8 +54,8 @@ const CreateRolePanel = ({ visible, onClose }: CreateRolePanelProps) => {
|
||||
})
|
||||
|
||||
const { mutate: createDatabaseRole, isLoading: isCreating } = useDatabaseRoleCreateMutation({
|
||||
onSuccess: (res) => {
|
||||
toast.success(`Successfully created new role: ${res.name}`)
|
||||
onSuccess: (_, vars) => {
|
||||
toast.success(`Successfully created new role: ${vars.payload.name}`)
|
||||
handleClose()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => {
|
||||
deleteDatabaseRole({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id: role.id.toString(),
|
||||
id: role.id,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import type { PostgresRole } from '@supabase/postgres-meta'
|
||||
import { useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import {
|
||||
@@ -18,13 +17,14 @@ import {
|
||||
} from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { PgRole } from 'data/database-roles/database-roles-query'
|
||||
import { useDatabaseRoleUpdateMutation } from 'data/database-roles/database-role-update-mutation'
|
||||
import { ROLE_PERMISSIONS } from './Roles.constants'
|
||||
|
||||
interface RoleRowProps {
|
||||
role: PostgresRole
|
||||
role: PgRole
|
||||
disabled?: boolean
|
||||
onSelectDelete: (role: PostgresRole) => void
|
||||
onSelectDelete: (role: PgRole) => void
|
||||
}
|
||||
|
||||
const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
@@ -33,25 +33,22 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
|
||||
const { mutate: updateDatabaseRole, isLoading: isUpdating } = useDatabaseRoleUpdateMutation()
|
||||
|
||||
const {
|
||||
is_superuser,
|
||||
can_login,
|
||||
can_create_role,
|
||||
can_create_db,
|
||||
is_replication_role,
|
||||
can_bypass_rls,
|
||||
} = role
|
||||
const { isSuperuser, canLogin, canCreateRole, canCreateDb, isReplicationRole, canBypassRls } =
|
||||
role
|
||||
|
||||
const onSaveChanges = async (values: any, { resetForm }: any) => {
|
||||
const onSaveChanges = async (values: Partial<PgRole>, { resetForm }: any) => {
|
||||
if (!project) return console.error('Project is required')
|
||||
|
||||
const { is_superuser, is_replication_role, ...payload } = values
|
||||
const changed = Object.fromEntries(
|
||||
Object.entries(values).filter(([k, v]) => v !== (role as any)[k])
|
||||
)
|
||||
|
||||
updateDatabaseRole(
|
||||
{
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
id: role.id,
|
||||
payload,
|
||||
payload: changed,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -66,12 +63,12 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
<Form
|
||||
name="role-update-form"
|
||||
initialValues={{
|
||||
is_superuser,
|
||||
can_login,
|
||||
can_create_role,
|
||||
can_create_db,
|
||||
is_replication_role,
|
||||
can_bypass_rls,
|
||||
isSuperuser,
|
||||
canLogin,
|
||||
canCreateRole,
|
||||
canCreateDb,
|
||||
isReplicationRole,
|
||||
canBypassRls,
|
||||
}}
|
||||
onSubmit={onSaveChanges}
|
||||
className={[
|
||||
@@ -118,7 +115,7 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{role.active_connections > 0 && (
|
||||
{role.activeConnections > 0 && (
|
||||
<div className="relative h-2 w-2">
|
||||
<span className="flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-brand opacity-75"></span>
|
||||
@@ -129,10 +126,10 @@ const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
<p
|
||||
id="collapsible-trigger"
|
||||
className={`text-sm ${
|
||||
role.active_connections > 0 ? 'text-foreground' : 'text-foreground-light'
|
||||
role.activeConnections > 0 ? 'text-foreground' : 'text-foreground-light'
|
||||
}`}
|
||||
>
|
||||
{role.active_connections} connections
|
||||
{role.activeConnections} connections
|
||||
</p>
|
||||
{!disabled && (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -33,35 +33,35 @@ export const SYSTEM_ROLES = [
|
||||
] as const
|
||||
|
||||
export const ROLE_PERMISSIONS = {
|
||||
can_login: {
|
||||
canLogin: {
|
||||
disabled: false,
|
||||
description: 'User can login',
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
can_create_role: {
|
||||
canCreateRole: {
|
||||
disabled: false,
|
||||
description: 'User can create roles',
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
can_create_db: {
|
||||
canCreateDb: {
|
||||
disabled: false,
|
||||
description: 'User can create databases',
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
can_bypass_rls: {
|
||||
disabled: true,
|
||||
canBypassRls: {
|
||||
disabled: false,
|
||||
description: 'User bypasses every row level security policy',
|
||||
grant_by_dashboard: false,
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
is_superuser: {
|
||||
isSuperuser: {
|
||||
disabled: true,
|
||||
description: 'User is a Superuser',
|
||||
grant_by_dashboard: false,
|
||||
},
|
||||
is_replication_role: {
|
||||
disabled: true,
|
||||
isReplicationRole: {
|
||||
disabled: false,
|
||||
description:
|
||||
'User can initiate streaming replication and put the system in and out of backup mode',
|
||||
grant_by_dashboard: false,
|
||||
grant_by_dashboard: true,
|
||||
},
|
||||
} as const
|
||||
@@ -42,19 +42,19 @@ const RolesList = () => {
|
||||
const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase())
|
||||
|
||||
const filteredRoles = (
|
||||
filterType === 'active' ? roles.filter((role) => role.active_connections > 0) : roles
|
||||
filterType === 'active' ? roles.filter((role) => role.activeConnections > 0) : roles
|
||||
).filter((role) => role.name.includes(filterString))
|
||||
const [supabaseRoles, otherRoles] = partition(filteredRoles, (role) =>
|
||||
SUPABASE_ROLES.includes(role.name as SUPABASE_ROLE)
|
||||
)
|
||||
|
||||
const totalActiveConnections = roles
|
||||
.map((role) => role.active_connections)
|
||||
.map((role) => role.activeConnections)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
// order the roles with active connections by number of connections, most connections first
|
||||
const rolesWithActiveConnections = sortBy(
|
||||
roles.filter((role) => role.active_connections > 0),
|
||||
(r) => -r.active_connections
|
||||
roles.filter((role) => role.activeConnections > 0),
|
||||
(r) => -r.activeConnections
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -145,7 +145,7 @@ const RolesList = () => {
|
||||
<p className="text-xs text-foreground-light pr-2">Connections by roles:</p>
|
||||
{rolesWithActiveConnections.map((role) => (
|
||||
<div key={role.id} className="text-xs text-foreground">
|
||||
{role.name}: {role.active_connections}
|
||||
{role.name}: {role.activeConnections}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
FormSectionLabel,
|
||||
FormsContainer,
|
||||
} from 'components/ui/Forms'
|
||||
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
|
||||
import { invalidateSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useFDWCreateMutation } from 'data/fdw/fdw-create-mutation'
|
||||
import { useCheckPermissions } from 'hooks'
|
||||
@@ -62,6 +63,16 @@ const CreateWrapper = () => {
|
||||
}
|
||||
: {}
|
||||
|
||||
const { data } = useDatabaseExtensionsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const wrappersExtension = data?.find((ext) => ext.name === 'wrappers')
|
||||
|
||||
const hasRequiredVersion =
|
||||
(wrappersExtension?.installed_version ?? '') >= (wrapperMeta?.minimumExtensionVersion ?? '')
|
||||
|
||||
if (wrapperMeta === undefined) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
|
||||
@@ -79,6 +90,37 @@ const CreateWrapper = () => {
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasRequiredVersion) {
|
||||
const databaseNeedsUpgrading =
|
||||
wrappersExtension?.installed_version !== wrappersExtension?.default_version
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full space-y-4">
|
||||
<div className="space-y-2 flex flex-col items-center w-[400px]">
|
||||
<p>Your extension version is outdated for this wrapper.</p>
|
||||
<p className="text-sm text-center text-foreground-light">
|
||||
The wrapper type {type} requires a minimum extension version of{' '}
|
||||
{wrapperMeta.minimumExtensionVersion}. You have version{' '}
|
||||
{wrappersExtension?.installed_version} installed. Please{' '}
|
||||
{databaseNeedsUpgrading && 'upgrade your database then '}reinstall the extension to
|
||||
create this wrapper.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={
|
||||
databaseNeedsUpgrading
|
||||
? `/project/${ref}/settings/infrastructure`
|
||||
: `/project/${ref}/database/extensions?filter=wrappers`
|
||||
}
|
||||
>
|
||||
{databaseNeedsUpgrading ? 'Upgrade Database' : 'Reinstall Extension'}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onUpdateTable = (values: any) => {
|
||||
setNewTables((prev) => {
|
||||
// if the new values have tableIndex, we are editing an existing table
|
||||
|
||||
@@ -9,6 +9,10 @@ export const WRAPPER_HANDLERS = {
|
||||
BIG_QUERY: 'big_query_fdw_handler',
|
||||
AIRTABLE: 'airtable_fdw_handler',
|
||||
LOGFLARE: 'logflare_fdw_handler',
|
||||
AUTH0: 'auth0_fdw_handler',
|
||||
COGNITO: 'cognito_fdw_handler',
|
||||
MSSQL: 'mssql_fdw_handler',
|
||||
REDIS: 'redis_fdw_handler',
|
||||
}
|
||||
|
||||
export const WRAPPERS: WrapperMeta[] = [
|
||||
@@ -831,4 +835,335 @@ export const WRAPPERS: WrapperMeta[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'auth0_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.AUTH0,
|
||||
validatorName: 'auth0_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/auth0-icon.svg`,
|
||||
extensionName: 'Auth0Fdw',
|
||||
label: 'Auth0',
|
||||
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/auth0',
|
||||
minimumExtensionVersion: '0.3.0',
|
||||
server: {
|
||||
options: [
|
||||
{
|
||||
name: 'api_key_id',
|
||||
label: 'Auth0 API key or PAT',
|
||||
required: true,
|
||||
encrypted: true,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
label: 'Auth0 API URL',
|
||||
defaultValue: 'https://dev-<tenant-id>.us.auth0.com/api/v2/users',
|
||||
required: false,
|
||||
encrypted: false,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
tables: [
|
||||
{
|
||||
label: 'Users',
|
||||
description: 'Auth0 Users',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'user_id',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'email_verified',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'phone_number',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'phone_verified',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'updated_at',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'identities',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'app_metadata',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'user_metadata',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'picture',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'nickname',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'multifactor',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'last_ip',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'last_login',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'logins_count',
|
||||
type: 'integer',
|
||||
},
|
||||
{
|
||||
name: 'blocked',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'given_name',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'family_name',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'users',
|
||||
editable: false,
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cognito_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.COGNITO,
|
||||
validatorName: 'cognito_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/cognito-icon.svg`,
|
||||
extensionName: 'CognitoFdw',
|
||||
label: 'Cognito',
|
||||
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/cognito',
|
||||
minimumExtensionVersion: '0.3.0',
|
||||
server: {
|
||||
options: [
|
||||
{
|
||||
name: 'aws_access_key_id',
|
||||
label: 'AWS Access Key ID',
|
||||
required: true,
|
||||
encrypted: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'api_key_id',
|
||||
label: 'AWS Secret Key',
|
||||
required: true,
|
||||
encrypted: true,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
name: 'region',
|
||||
label: 'Region',
|
||||
required: true,
|
||||
encrypted: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
name: 'user_pool_id',
|
||||
label: 'User Pool ID',
|
||||
required: true,
|
||||
encrypted: false,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
tables: [
|
||||
{
|
||||
label: 'Users',
|
||||
description: 'Cognito Users',
|
||||
availableColumns: [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'created_at',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'email_verified',
|
||||
type: 'boolean',
|
||||
},
|
||||
{
|
||||
name: 'identities',
|
||||
type: 'jsonb',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
options: [
|
||||
{
|
||||
name: 'object',
|
||||
defaultValue: 'users',
|
||||
editable: false,
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'mssql_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.MSSQL,
|
||||
validatorName: 'mssql_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/mssql-icon.svg`,
|
||||
extensionName: 'mssqlFdw',
|
||||
label: 'Microsoft SQL Server',
|
||||
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/mssql',
|
||||
minimumExtensionVersion: '0.3.0',
|
||||
server: {
|
||||
options: [
|
||||
{
|
||||
name: 'conn_string_id',
|
||||
label: 'Connection String',
|
||||
required: true,
|
||||
encrypted: true,
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
tables: [
|
||||
{
|
||||
label: 'Microsoft SQL Server Table',
|
||||
description: 'Map to an Microsoft SQL Server Table',
|
||||
options: [
|
||||
{
|
||||
name: 'table',
|
||||
label: 'MSSQL Table',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'redis_wrapper',
|
||||
handlerName: WRAPPER_HANDLERS.REDIS,
|
||||
validatorName: 'redis_fdw_validator',
|
||||
icon: `${BASE_PATH}/img/icons/redis-icon.svg`,
|
||||
extensionName: 'redisFdw',
|
||||
label: 'Redis',
|
||||
docsUrl: 'https://supabase.com/docs/guides/database/extensions/wrappers/redis',
|
||||
minimumExtensionVersion: '0.3.0',
|
||||
server: {
|
||||
options: [
|
||||
{
|
||||
name: 'conn_url_id',
|
||||
label: 'Connection URL',
|
||||
required: true,
|
||||
encrypted: true,
|
||||
hidden: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
tables: [
|
||||
{
|
||||
label: 'Redis Table',
|
||||
description: 'Map to an Redis Table',
|
||||
options: [
|
||||
{
|
||||
name: 'src_type',
|
||||
label: 'Source Type',
|
||||
editable: true,
|
||||
required: true,
|
||||
type: 'select',
|
||||
defaultValue: 'list',
|
||||
options: [
|
||||
{
|
||||
label: 'list',
|
||||
value: 'list',
|
||||
},
|
||||
{
|
||||
label: 'set',
|
||||
value: 'set',
|
||||
},
|
||||
{
|
||||
label: 'hash',
|
||||
value: 'hash',
|
||||
},
|
||||
{
|
||||
label: 'zset',
|
||||
value: 'zset',
|
||||
},
|
||||
{
|
||||
label: 'stream',
|
||||
value: 'stream',
|
||||
},
|
||||
{
|
||||
label: 'multi_list',
|
||||
value: 'multi_list',
|
||||
},
|
||||
{
|
||||
label: 'multi_set',
|
||||
value: 'multi_set',
|
||||
},
|
||||
{
|
||||
label: 'multi_hash',
|
||||
value: 'multi_hash',
|
||||
},
|
||||
{
|
||||
label: 'multi_zset',
|
||||
value: 'multi_zset',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'src_key',
|
||||
label: 'Source Key',
|
||||
editable: true,
|
||||
required: false,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -8,6 +8,7 @@ export type WrapperMeta = {
|
||||
server: Server
|
||||
tables: Table[]
|
||||
docsUrl: string
|
||||
minimumExtensionVersion?: string
|
||||
}
|
||||
|
||||
export type ServerOption = {
|
||||
|
||||
@@ -67,7 +67,7 @@ const WrappersDisabledState = () => {
|
||||
}
|
||||
} finally {
|
||||
setIsEnabling(false)
|
||||
if (!hasError) toast.success('Wrappers is now enabled!')
|
||||
if (!hasError) toast.success('Wrappers are now enabled!')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,20 @@ export const FRAMEWORKS: ConnectionType[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'flutter',
|
||||
label: 'Flutter',
|
||||
icon: 'flutter',
|
||||
guideLink: 'https://supabase.com/docs/guides/getting-started/tutorials/with-flutter',
|
||||
children: [
|
||||
{
|
||||
key: 'supabaseflutter',
|
||||
label: 'supabase-flutter',
|
||||
children: [],
|
||||
icon: 'supabase',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const ORMS: ConnectionType[] = [
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import type { ContentFileProps } from 'components/interfaces/Home/Connect/Connect.types'
|
||||
|
||||
import {
|
||||
ConnectTabs,
|
||||
ConnectTabTriggers,
|
||||
ConnectTabTrigger,
|
||||
ConnectTabContent,
|
||||
} from 'components/interfaces/Home/Connect/ConnectTabs'
|
||||
import SimpleCodeBlock from 'components/to-be-cleaned/SimpleCodeBlock'
|
||||
|
||||
const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
return (
|
||||
<ConnectTabs>
|
||||
<ConnectTabTriggers>
|
||||
<ConnectTabTrigger value="lib/main.dart" />
|
||||
<ConnectTabTrigger value="lib/main.dart (app)" />
|
||||
</ConnectTabTriggers>
|
||||
|
||||
<ConnectTabContent value="lib/main.dart">
|
||||
<SimpleCodeBlock className="dart" parentClassName="min-h-72">
|
||||
{`
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
await Supabase.initialize(
|
||||
url: ${projectKeys.apiUrl ?? 'your-project-url'},
|
||||
anonKey: ${projectKeys.anonKey ?? 'your-anon-key'},
|
||||
);
|
||||
runApp(MyApp());
|
||||
}
|
||||
`}
|
||||
</SimpleCodeBlock>
|
||||
</ConnectTabContent>
|
||||
|
||||
<ConnectTabContent value="lib/main.dart (app)">
|
||||
<SimpleCodeBlock className="dart" parentClassName="min-h-72">
|
||||
{`
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
title: 'Todos',
|
||||
home: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
final _future = Supabase.instance.client
|
||||
.from('todos')
|
||||
.select();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: FutureBuilder(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final todos = snapshot.data!;
|
||||
return ListView.builder(
|
||||
itemCount: todos.length,
|
||||
itemBuilder: ((context, index) {
|
||||
final todo = todos[index];
|
||||
return ListTile(
|
||||
title: Text(todo['name']),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
`}
|
||||
</SimpleCodeBlock>
|
||||
</ConnectTabContent>
|
||||
</ConnectTabs>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentFile
|
||||
@@ -30,10 +30,10 @@ export const CLIENT_LIBRARIES = [
|
||||
},
|
||||
{
|
||||
language: 'Swift',
|
||||
officialSupport: false,
|
||||
officialSupport: true,
|
||||
releaseState: undefined,
|
||||
docsUrl: 'https://supabase.com/docs/reference/swift/initializing',
|
||||
gitUrl: 'https://github.com/supabase-community/supabase-swift',
|
||||
gitUrl: 'https://github.com/supabase/supabase-swift',
|
||||
},
|
||||
{
|
||||
language: 'Kotlin',
|
||||
|
||||
@@ -8,14 +8,25 @@ import CreditBalance from './CreditBalance'
|
||||
import PaymentMethods from './PaymentMethods/PaymentMethods'
|
||||
import Subscription from './Subscription/Subscription'
|
||||
import TaxID from './TaxID/TaxID'
|
||||
import { useParams } from 'common'
|
||||
import { useOrgSubscriptionQuery } from '../../../../data/subscriptions/org-subscription-query'
|
||||
|
||||
const BillingSettings = () => {
|
||||
const {
|
||||
billingAccountData: billingAccountDataEnabled,
|
||||
billingPaymentMethods: billingPaymentMethodsEnabled,
|
||||
billingCredits: billingCreditsEnabled,
|
||||
billingAccountData: isBillingAccountDataEnabledOnProfileLevel,
|
||||
billingPaymentMethods: isBillingPaymentMethodsEnabledOnProfileLevel,
|
||||
billingCredits: isBillingCreditsEnabledOnProfileLevel,
|
||||
} = useIsFeatureEnabled(['billing:account_data', 'billing:payment_methods', 'billing:credits'])
|
||||
|
||||
const { slug: orgSlug } = useParams()
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug })
|
||||
const isNotOrgWithPartnerBilling = !subscription?.billing_via_partner ?? true
|
||||
|
||||
const billingAccountDataEnabled =
|
||||
isBillingAccountDataEnabledOnProfileLevel && isNotOrgWithPartnerBilling
|
||||
const billingPaymentMethodsEnabled =
|
||||
isBillingPaymentMethodsEnabledOnProfileLevel && isNotOrgWithPartnerBilling
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer id="subscription">
|
||||
@@ -34,7 +45,7 @@ const BillingSettings = () => {
|
||||
<BillingBreakdown />
|
||||
</ScaffoldContainer>
|
||||
|
||||
{billingCreditsEnabled && (
|
||||
{isBillingCreditsEnabledOnProfileLevel && (
|
||||
<>
|
||||
<ScaffoldDivider />
|
||||
|
||||
|
||||
+4
-2
@@ -39,6 +39,7 @@ import ExitSurveyModal from './ExitSurveyModal'
|
||||
import UpgradeSurveyModal from './UpgradeModal'
|
||||
import MembersExceedLimitModal from './MembersExceedLimitModal'
|
||||
import PaymentMethodSelection from './PaymentMethodSelection'
|
||||
import { billingPartnerLabel } from 'components/interfaces/Billing/Subscription/Subscription.utils'
|
||||
|
||||
const PlanUpdateSidePanel = () => {
|
||||
const router = useRouter()
|
||||
@@ -545,8 +546,9 @@ const PlanUpdateSidePanel = () => {
|
||||
) : (
|
||||
<div className="py-4 space-y-2">
|
||||
<p className="text-sm">
|
||||
This organization is billed through our partner Fly.io and you will be charged by
|
||||
them directly.
|
||||
This organization is billed through our partner{' '}
|
||||
{billingPartnerLabel(subscription?.billing_partner)} and you will be charged by them
|
||||
directly.
|
||||
</p>
|
||||
{subscriptionPreview?.billed_via_partner &&
|
||||
subscriptionPreview?.plan_change_type === 'downgrade' && (
|
||||
|
||||
+45
-22
@@ -3,23 +3,23 @@ import { ExternalLink } from 'lucide-react'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useDatabaseExtensionEnableMutation } from 'data/database-extensions/database-extension-enable-mutation'
|
||||
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
|
||||
import { useIndexAdvisorEnableMutation } from 'data/database/index-advisor-enable-mutation'
|
||||
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
|
||||
import { Markdown } from '../Markdown'
|
||||
import Link from 'next/link'
|
||||
import { useParams } from 'common'
|
||||
|
||||
export const IndexAdvisorDisabledState = () => {
|
||||
const { ref } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
const { data: extensions } = useDatabaseExtensionsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
const hypopgExtension = (extensions ?? []).find((ext) => ext.name === 'hypopg')
|
||||
const indexAdvisorExtension = (extensions ?? []).find((ext) => ext.name === 'index_advisor')
|
||||
|
||||
const { mutateAsync: enableExtension, isLoading: isEnablingExtension } =
|
||||
useDatabaseExtensionEnableMutation()
|
||||
const { mutateAsync: enableIndexAdvisor, isLoading: isEnablingIndexAdvisor } =
|
||||
useIndexAdvisorEnableMutation()
|
||||
const isEnabling = isEnablingExtension || isEnablingIndexAdvisor
|
||||
|
||||
const onEnableIndexAdvisor = async () => {
|
||||
if (project === undefined) return console.error('Project is required')
|
||||
@@ -32,33 +32,56 @@ export const IndexAdvisorDisabledState = () => {
|
||||
version: hypopgExtension.default_version,
|
||||
})
|
||||
}
|
||||
|
||||
// [Joshen] Once index_advisor ext is ready, it'll just be enabling an extension
|
||||
await enableIndexAdvisor({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
})
|
||||
if (indexAdvisorExtension?.installed_version === null) {
|
||||
await enableExtension({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
name: indexAdvisorExtension.name,
|
||||
schema: indexAdvisorExtension?.schema ?? 'extensions',
|
||||
version: indexAdvisorExtension.default_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert_Shadcn_>
|
||||
<Alert_Shadcn_ className="mb-6">
|
||||
<AlertTitle_Shadcn_>
|
||||
Get index suggestions to improve your query performance
|
||||
<Markdown
|
||||
className="text-foreground"
|
||||
content={
|
||||
indexAdvisorExtension === undefined
|
||||
? 'Newer version of Postgres required'
|
||||
: 'Postgres extensions `index_advisor` and `hypopg` required'
|
||||
}
|
||||
/>
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
<Markdown content="The `index_advisor` extension can help in recommending database indexes to reduce the costs of your query." />
|
||||
<Markdown
|
||||
content={
|
||||
indexAdvisorExtension === undefined
|
||||
? 'Upgrade to the latest version of Postgres to get recommendations on indexes for your queries'
|
||||
: 'These extensions can help in recommending database indexes to reduce the costs of your query.'
|
||||
}
|
||||
/>
|
||||
</AlertDescription_Shadcn_>
|
||||
|
||||
<AlertDescription_Shadcn_ className="mt-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isEnabling}
|
||||
loading={isEnabling}
|
||||
onClick={() => onEnableIndexAdvisor()}
|
||||
>
|
||||
Enable index advisor
|
||||
</Button>
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
{indexAdvisorExtension === undefined ? (
|
||||
<Button asChild type="default">
|
||||
<Link href={`/project/${ref}/settings/infrastructure`}>Upgrade Postgres version</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="default"
|
||||
disabled={isEnablingExtension}
|
||||
loading={isEnablingExtension}
|
||||
onClick={() => onEnableIndexAdvisor()}
|
||||
>
|
||||
Enable extensions
|
||||
</Button>
|
||||
)}
|
||||
<Button asChild type="outline" icon={<ExternalLink />}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-ex
|
||||
import { useGetIndexAdvisorResult } from 'data/database/retrieve-index-advisor-result-query'
|
||||
import { useGetIndexesFromSelectQuery } from 'data/database/retrieve-index-from-select-query'
|
||||
import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { useExecuteSqlQuery } from 'data/sql/execute-sql-query'
|
||||
import {
|
||||
AccordionContent_Shadcn_,
|
||||
AccordionItem_Shadcn_,
|
||||
@@ -51,21 +50,16 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
|
||||
query: selectedRow?.['query'],
|
||||
})
|
||||
|
||||
const { data: indexAdvisorExt, isLoading: isLoadingIndexAdvisorExt } = useExecuteSqlQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
sql: `select * from pg_proc where proname = 'index_advisor';`,
|
||||
queryKey: ['index-advisor-function-check'],
|
||||
})
|
||||
const { data: extensions, isLoading: isLoadingExtensions } = useDatabaseExtensionsQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const isLoadingChecks = isLoadingIndexAdvisorExt || isLoadingExtensions
|
||||
const hypopgExtension = (extensions ?? []).find((ext) => ext.name === 'hypopg')
|
||||
const indexAdvisorExtension = (extensions ?? []).find((ext) => ext.name === 'index_advisor')
|
||||
const isIndexAdvisorAvailable =
|
||||
(indexAdvisorExt?.result ?? []).length > 0 &&
|
||||
indexAdvisorExtension !== undefined &&
|
||||
indexAdvisorExtension.installed_version !== null &&
|
||||
hypopgExtension !== undefined &&
|
||||
hypopgExtension.installed_version !== null
|
||||
|
||||
@@ -165,7 +159,7 @@ export const QueryIndexes = ({ selectedRow }: QueryIndexesProps) => {
|
||||
<QueryPanelSection className="flex flex-col gap-y-6">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm">New index recommendations</p>
|
||||
{isLoadingChecks ? (
|
||||
{isLoadingExtensions ? (
|
||||
<GenericSkeletonLoader />
|
||||
) : !isIndexAdvisorAvailable ? (
|
||||
<IndexAdvisorDisabledState />
|
||||
|
||||
+22
-20
@@ -28,9 +28,8 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens
|
||||
: undefined
|
||||
|
||||
const anonRoleKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.serviceApiKey
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
|
||||
const { data: postgrestConfig } = useProjectPostgrestConfigQuery(
|
||||
{
|
||||
projectRef: config.projectRef,
|
||||
@@ -59,27 +58,30 @@ export const RealtimeTokensPopover = ({ config, onChangeConfig }: RealtimeTokens
|
||||
}, [snap.role])
|
||||
|
||||
useEffect(() => {
|
||||
let token: string | undefined
|
||||
let bearer: string | null = null
|
||||
const triggerUpdateTokenBearer = async () => {
|
||||
let token: string | undefined
|
||||
let bearer: string | null = null
|
||||
|
||||
if (
|
||||
config.projectRef !== undefined &&
|
||||
jwtSecret !== undefined &&
|
||||
snap.role !== undefined &&
|
||||
snap.role.type === 'postgrest'
|
||||
) {
|
||||
token = anonRoleKey
|
||||
getRoleImpersonationJWT(config.projectRef, jwtSecret, snap.role)
|
||||
.then((token) => (bearer = token))
|
||||
.catch((err) => toast.error(`Failed to get JWT for role: ${err.message}`))
|
||||
} else {
|
||||
token = serviceRoleKey
|
||||
if (
|
||||
config.projectRef !== undefined &&
|
||||
jwtSecret !== undefined &&
|
||||
snap.role !== undefined &&
|
||||
snap.role.type === 'postgrest'
|
||||
) {
|
||||
token = anonRoleKey
|
||||
await getRoleImpersonationJWT(config.projectRef, jwtSecret, snap.role)
|
||||
.then((b) => (bearer = b))
|
||||
.catch((err) => toast.error(`Failed to get JWT for role: ${err.message}`))
|
||||
} else {
|
||||
token = serviceRoleKey
|
||||
}
|
||||
if (token) {
|
||||
onChangeConfig({ ...config, token, bearer })
|
||||
}
|
||||
}
|
||||
|
||||
if (token) {
|
||||
onChangeConfig({ ...config, token, bearer })
|
||||
}
|
||||
}, [config.projectRef, jwtSecret, serviceRoleKey, snap.role])
|
||||
triggerUpdateTokenBearer()
|
||||
}, [snap.role, anonRoleKey, serviceRoleKey])
|
||||
|
||||
return <RoleImpersonationPopover align="start" variant="connected-on-both" />
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export const RealtimeInspector = () => {
|
||||
enableBroadcast: true,
|
||||
})
|
||||
|
||||
const { logData, sendMessage } = useRealtimeMessages(realtimeConfig)
|
||||
const { logData, sendMessage } = useRealtimeMessages(realtimeConfig, setRealtimeConfig)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col grow h-full">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { RealtimeChannel, SupabaseClient } from '@supabase/supabase-js'
|
||||
import { sortBy, take } from 'lodash'
|
||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import { Dispatch, SetStateAction, useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
@@ -50,20 +50,25 @@ export interface RealtimeConfig {
|
||||
enableDbChanges: boolean
|
||||
}
|
||||
|
||||
export const useRealtimeMessages = ({
|
||||
enabled,
|
||||
channelName,
|
||||
projectRef,
|
||||
logLevel,
|
||||
token,
|
||||
schema,
|
||||
table,
|
||||
filter,
|
||||
bearer,
|
||||
enablePresence,
|
||||
enableDbChanges,
|
||||
enableBroadcast,
|
||||
}: RealtimeConfig) => {
|
||||
export const useRealtimeMessages = (
|
||||
config: RealtimeConfig,
|
||||
setRealtimeConfig: Dispatch<SetStateAction<RealtimeConfig>>
|
||||
) => {
|
||||
const {
|
||||
enabled,
|
||||
channelName,
|
||||
projectRef,
|
||||
logLevel,
|
||||
token,
|
||||
schema,
|
||||
table,
|
||||
filter,
|
||||
bearer,
|
||||
enablePresence,
|
||||
enableDbChanges,
|
||||
enableBroadcast,
|
||||
} = config
|
||||
|
||||
const { data } = useProjectApiQuery({ projectRef: projectRef })
|
||||
|
||||
// the default host is prod until the correct one comes through an API call.
|
||||
@@ -97,7 +102,8 @@ export const useRealtimeMessages = ({
|
||||
},
|
||||
}
|
||||
const newClient = new SupabaseClient(host, token, opts)
|
||||
if (bearer != '') {
|
||||
|
||||
if (bearer) {
|
||||
newClient.realtime.setAuth(bearer)
|
||||
}
|
||||
|
||||
@@ -170,6 +176,14 @@ export const useRealtimeMessages = ({
|
||||
}
|
||||
} else if (status === 'CLOSED') {
|
||||
// console.log(`Realtime Channel status: ${status}`)
|
||||
} else if (status === 'CHANNEL_ERROR') {
|
||||
toast.error(
|
||||
'Failed to connect to the channel: This may be due to restrictive RLS policies. Check your role and try again.'
|
||||
)
|
||||
|
||||
newChannel.unsubscribe()
|
||||
setChannel(undefined)
|
||||
setRealtimeConfig({ ...config, enabled: false })
|
||||
} else {
|
||||
// console.error(`Realtime Channel error status: ${status}`)
|
||||
// console.error(`Realtime Channel error: ${error}`)
|
||||
|
||||
@@ -32,6 +32,8 @@ export function getHumanReadableTitle(title: LINT_TYPES) {
|
||||
return 'Security Definer View'
|
||||
case 'duplicate_index':
|
||||
return 'Duplicate Index'
|
||||
case 'extension_in_public':
|
||||
return 'Extension in Public'
|
||||
default:
|
||||
assertUnreachable(title)
|
||||
throw new Error('This case should never be reached')
|
||||
@@ -172,7 +174,18 @@ export const LintCTA = ({
|
||||
case 'security_definer_view':
|
||||
// we don't have a good place to send the user to check out a view
|
||||
return <></>
|
||||
|
||||
case 'extension_in_public':
|
||||
return (
|
||||
<Button asChild type="default">
|
||||
<Link
|
||||
href={`/project/${projectRef}/database/extensions?filter=${metadata?.name}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
View extension
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
default:
|
||||
assertUnreachable(title)
|
||||
return <></>
|
||||
|
||||
@@ -162,7 +162,7 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
>
|
||||
<Link
|
||||
passHref
|
||||
href={`/project/${projectRef}/auth/policies?search=${table.id}`}
|
||||
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
|
||||
>
|
||||
Add RLS policy
|
||||
</Link>
|
||||
@@ -202,7 +202,10 @@ const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeade
|
||||
)
|
||||
}
|
||||
>
|
||||
<Link passHref href={`/project/${projectRef}/auth/policies?search=${table.id}`}>
|
||||
<Link
|
||||
passHref
|
||||
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
|
||||
>
|
||||
Auth {policies.length > 1 ? 'policies' : 'policy'}
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -8,13 +8,17 @@ import { AccountLayout } from './'
|
||||
import { ScaffoldContainer, ScaffoldDivider, ScaffoldHeader, ScaffoldTitle } from './Scaffold'
|
||||
import SettingsLayout from './SettingsLayout/SettingsLayout'
|
||||
import Link from 'next/link'
|
||||
import { useOrgSubscriptionQuery } from '../../data/subscriptions/org-subscription-query'
|
||||
|
||||
const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const router = useRouter()
|
||||
const { slug } = useParams()
|
||||
|
||||
const invoicesEnabled = useIsFeatureEnabled('billing:invoices')
|
||||
const invoicesEnabledOnProfileLevel = useIsFeatureEnabled('billing:invoices')
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: slug })
|
||||
const isNotOrgWithPartnerBilling = !subscription?.billing_via_partner ?? true
|
||||
const invoicesEnabled = invoicesEnabledOnProfileLevel && isNotOrgWithPartnerBilling
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
import { differenceInDays } from 'date-fns'
|
||||
import React from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import { FormHeader } from 'components/ui/Forms'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useS3AccessKeyCreateMutation } from 'data/storage/s3-access-key-create-mutation'
|
||||
import { useS3AccessKeyDeleteMutation } from 'data/storage/s3-access-key-delete-mutation'
|
||||
import { useStorageCredentialsQuery } from 'data/storage/s3-access-key-query'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconMoreVertical,
|
||||
IconTrash,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
|
||||
export const S3Connection = () => {
|
||||
const [openCreateCred, setOpenCreateCred] = React.useState(false)
|
||||
const [showSuccess, setShowSuccess] = React.useState(false)
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false)
|
||||
const [deleteCredId, setDeleteCredId] = React.useState<string | null>(null)
|
||||
|
||||
const { ref: projectRef } = useParams()
|
||||
const { project, isLoading: projectIsLoading } = useProjectContext()
|
||||
|
||||
const { data: storageCreds, ...storageCredsQuery } = useStorageCredentialsQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const hasStorageCreds = storageCreds?.data && storageCreds.data.length > 0
|
||||
|
||||
const createS3AccessKey = useS3AccessKeyCreateMutation({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const deleteS3AccessKey = useS3AccessKeyDeleteMutation({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const { data: projectAPI } = useProjectApiQuery({ projectRef: projectRef })
|
||||
|
||||
function getConnectionURL() {
|
||||
const projUrl = projectAPI
|
||||
? `${projectAPI.autoApiService.protocol}://${projectAPI.autoApiService.endpoint}`
|
||||
: `https://${projectRef}.supabase.co`
|
||||
|
||||
const url = new URL(projUrl)
|
||||
url.pathname = '/storage/v1/s3'
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
const s3connectionUrl = getConnectionURL()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<FormHeader
|
||||
title="S3 Connection"
|
||||
description="Connect to your bucket via the S3 protocol."
|
||||
docsUrl="https://supabase.com/docs/guides/storage/s3/authentication"
|
||||
/>
|
||||
<Panel className="grid gap-4 p-4 !mb-0">
|
||||
<FormItemLayout layout="horizontal" label="Endpoint" isReactForm={false}>
|
||||
<Input readOnly copy disabled value={s3connectionUrl} />
|
||||
</FormItemLayout>
|
||||
{projectIsLoading ? (
|
||||
<></>
|
||||
) : (
|
||||
<FormItemLayout layout="horizontal" label="Region" isReactForm={false}>
|
||||
<Input className="input-mono" copy disabled value={project?.region} />
|
||||
</FormItemLayout>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FormHeader
|
||||
title="S3 Credentials"
|
||||
description="Manage your S3 credentials for this project."
|
||||
actions={
|
||||
<Dialog
|
||||
open={openCreateCred}
|
||||
onOpenChange={(open) => {
|
||||
setOpenCreateCred(open)
|
||||
if (!open) setShowSuccess(false)
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="outline">New credential</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent
|
||||
className="p-4"
|
||||
onInteractOutside={(e) => {
|
||||
if (showSuccess) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{showSuccess ? (
|
||||
<>
|
||||
<DialogTitle>Save your new S3 access keys</DialogTitle>
|
||||
<DialogDescription>
|
||||
You won't be able to see them again. If you lose these credentials, you'll
|
||||
need to create a new ones.
|
||||
</DialogDescription>
|
||||
|
||||
<FormItemLayout label="Access key id" isReactForm={false}>
|
||||
<Input
|
||||
className="input-mono"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={createS3AccessKey.data?.data?.access_key}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
<FormItemLayout label={'Secret access key'} isReactForm={false}>
|
||||
<Input
|
||||
className="input-mono"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={createS3AccessKey.data?.data?.secret_key}
|
||||
/>
|
||||
</FormItemLayout>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
setOpenCreateCred(false)
|
||||
setShowSuccess(false)
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogTitle>Create new S3 access keys</DialogTitle>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const formData = new FormData(e.target as HTMLFormElement)
|
||||
const description = formData.get('description') as string
|
||||
|
||||
await createS3AccessKey.mutateAsync({ description })
|
||||
setShowSuccess(true)
|
||||
}}
|
||||
>
|
||||
<FormItemLayout label="Description" isReactForm={false}>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder="My test key"
|
||||
type="text"
|
||||
name="description"
|
||||
required
|
||||
/>
|
||||
</FormItemLayout>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
className="mt-4"
|
||||
htmlType="submit"
|
||||
loading={createS3AccessKey.isLoading}
|
||||
>
|
||||
Create credential
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn([
|
||||
'bg-surface-100',
|
||||
'overflow-hidden border-muted',
|
||||
'rounded-md border shadow',
|
||||
])}
|
||||
>
|
||||
{storageCredsQuery.isLoading ? (
|
||||
<div className="p-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="">Description</Table.th>,
|
||||
<Table.th key="">Access key id</Table.th>,
|
||||
<Table.th key="">Created at</Table.th>,
|
||||
<Table.th key="actions" />,
|
||||
]}
|
||||
body={
|
||||
hasStorageCreds ? (
|
||||
storageCreds.data?.map((cred: any) => (
|
||||
<StorageCredItem
|
||||
key={cred.id}
|
||||
created_at={cred.created_at}
|
||||
access_key={cred.access_key}
|
||||
description={cred.description}
|
||||
id={cred.id}
|
||||
onDeleteClick={() => {
|
||||
setDeleteCredId(cred.id)
|
||||
setOpenDeleteDialog(true)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={4}>
|
||||
<p className="text-sm text-foreground">No credentials created</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
There are no credentials associated with your project yet
|
||||
</p>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={openDeleteDialog} onOpenChange={setOpenDeleteDialog}>
|
||||
<DialogContent className="p-4">
|
||||
<DialogTitle>Revoke S3 access keys</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action is irreversible and requests made with these credentials will stop working.
|
||||
</DialogDescription>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
setOpenDeleteDialog(false)
|
||||
setDeleteCredId(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
loading={deleteS3AccessKey.isLoading}
|
||||
onClick={async () => {
|
||||
if (!deleteCredId) return
|
||||
await deleteS3AccessKey.mutateAsync({ id: deleteCredId })
|
||||
setOpenDeleteDialog(false)
|
||||
toast.success('S3 access keys revoked')
|
||||
}}
|
||||
>
|
||||
Yes, revoke credentials
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function StorageCredItem({
|
||||
description,
|
||||
id,
|
||||
created_at,
|
||||
access_key,
|
||||
onDeleteClick,
|
||||
}: {
|
||||
description: string
|
||||
id: string
|
||||
created_at: string
|
||||
access_key: string
|
||||
onDeleteClick: (id: string) => void
|
||||
}) {
|
||||
function daysSince(date: string) {
|
||||
const now = new Date()
|
||||
const created = new Date(date)
|
||||
const diffInDays = differenceInDays(now, created)
|
||||
|
||||
if (diffInDays === 0) {
|
||||
return 'Today'
|
||||
} else if (diffInDays === 1) {
|
||||
return `${diffInDays} day ago`
|
||||
} else {
|
||||
return `${diffInDays} days ago`
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="h-8 text-ellipsis group">
|
||||
<td>{description}</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-ellipsis font-mono cursor-default">{access_key}</span>
|
||||
<span className="w-24 text-right opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<CopyButton text={access_key} type="outline" />
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{daysSince(created_at)}</td>
|
||||
<td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="flex items-center justify-end w-full">
|
||||
<IconMoreVertical />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="max-w-40">
|
||||
<DropdownMenuItem
|
||||
className="flex gap-1.5 "
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
onDeleteClick(id)
|
||||
}}
|
||||
>
|
||||
<IconTrash />
|
||||
Revoke credentials
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
@@ -4,16 +4,19 @@ import ReactMarkdown from 'react-markdown'
|
||||
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { Button } from 'ui'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
const FormHeader = ({
|
||||
title,
|
||||
description,
|
||||
docsUrl,
|
||||
actions,
|
||||
className,
|
||||
}: {
|
||||
title: string
|
||||
description?: string
|
||||
docsUrl?: string
|
||||
actions?: ReactNode
|
||||
className?: string
|
||||
}) => {
|
||||
return (
|
||||
@@ -26,13 +29,16 @@ const FormHeader = ({
|
||||
</h3>
|
||||
{description && <Markdown content={description} className="max-w-full" />}
|
||||
</div>
|
||||
{docsUrl !== undefined && (
|
||||
<Button asChild type="default" icon={<ExternalLink size={14} />}>
|
||||
<Link href={docsUrl} target="_blank" rel="noreferrer">
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-x-2">
|
||||
{docsUrl !== undefined && (
|
||||
<Button asChild type="default" icon={<ExternalLink size={14} />}>
|
||||
<Link href={docsUrl} target="_blank" rel="noreferrer">
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,8 +25,13 @@ function Panel(props: PropsWithChildren<PanelProps>) {
|
||||
)}
|
||||
>
|
||||
{props.title && (
|
||||
<div className={cn('bg-surface-100 border-b border-muted', props.titleClasses)}>
|
||||
<div className="flex items-center px-6 py-4">{props.title}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-surface-100 border-b border-muted flex items-center px-6 py-4',
|
||||
props.titleClasses
|
||||
)}
|
||||
>
|
||||
{props.title}
|
||||
</div>
|
||||
)}
|
||||
{props.children}
|
||||
|
||||
Vendored
+3925
-10
File diff suppressed because it is too large.
Load diff
@@ -1,12 +1,12 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import pgMeta from '@supabase/pg-meta'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { databaseRolesKeys } from './keys'
|
||||
import type { components } from 'data/api'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { invalidateRolesQuery } from './database-roles-query'
|
||||
|
||||
type CreateRoleBody = components['schemas']['CreateRoleBody']
|
||||
type CreateRoleBody = Parameters<typeof pgMeta.roles.create>[0]
|
||||
|
||||
export type DatabaseRoleCreateVariables = {
|
||||
projectRef: string
|
||||
@@ -19,20 +19,14 @@ export async function createDatabaseRole({
|
||||
connectionString,
|
||||
payload,
|
||||
}: DatabaseRoleCreateVariables) {
|
||||
let headers = new Headers()
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
const { data, error } = await post('/platform/pg-meta/{ref}/roles', {
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString! },
|
||||
path: { ref: projectRef },
|
||||
},
|
||||
body: payload,
|
||||
headers,
|
||||
const sql = pgMeta.roles.create(payload).sql
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: ['roles', 'create'],
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
return result
|
||||
}
|
||||
|
||||
type DatabaseRoleCreateData = Awaited<ReturnType<typeof createDatabaseRole>>
|
||||
@@ -52,7 +46,7 @@ export const useDatabaseRoleCreateMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef))
|
||||
await invalidateRolesQuery(queryClient, projectRef)
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import pgMeta from '@supabase/pg-meta'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { del } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { databaseRolesKeys } from './keys'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { invalidateRolesQuery } from './database-roles-query'
|
||||
|
||||
type DropRoleBody = Parameters<typeof pgMeta.roles.remove>[1]
|
||||
|
||||
export type DatabaseRoleDeleteVariables = {
|
||||
projectRef: string
|
||||
connectionString?: string
|
||||
id: string
|
||||
id: number
|
||||
payload?: DropRoleBody
|
||||
}
|
||||
|
||||
export async function deleteDatabaseRole({
|
||||
projectRef,
|
||||
connectionString,
|
||||
id,
|
||||
payload,
|
||||
}: DatabaseRoleDeleteVariables) {
|
||||
let headers = new Headers()
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
const { data, error } = await del('/platform/pg-meta/{ref}/roles', {
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString! },
|
||||
path: { ref: projectRef },
|
||||
query: { id },
|
||||
},
|
||||
headers,
|
||||
const sql = pgMeta.roles.remove({ id }, payload).sql
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: ['roles', 'delete'],
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
return result
|
||||
}
|
||||
|
||||
type DatabaseRoleDeleteData = Awaited<ReturnType<typeof deleteDatabaseRole>>
|
||||
@@ -49,7 +48,7 @@ export const useDatabaseRoleDeleteMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef))
|
||||
await invalidateRolesQuery(queryClient, projectRef)
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import pgMeta from '@supabase/pg-meta'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { patch } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { databaseRolesKeys } from './keys'
|
||||
import type { components } from 'data/api'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { invalidateRolesQuery } from './database-roles-query'
|
||||
|
||||
type UpdateRoleBody = components['schemas']['UpdateRoleBody']
|
||||
type UpdateRoleBody = Parameters<typeof pgMeta.roles.update>[1]
|
||||
|
||||
export type DatabaseRoleUpdateVariables = {
|
||||
projectRef: string
|
||||
@@ -21,21 +21,14 @@ export async function updateDatabaseRole({
|
||||
id,
|
||||
payload,
|
||||
}: DatabaseRoleUpdateVariables) {
|
||||
let headers = new Headers()
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
const { data, error } = await patch('/platform/pg-meta/{ref}/roles', {
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString! },
|
||||
path: { ref: projectRef },
|
||||
query: { id },
|
||||
},
|
||||
body: payload,
|
||||
headers,
|
||||
const sql = pgMeta.roles.update({ id }, payload).sql
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: ['roles', 'update'],
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
return result
|
||||
}
|
||||
|
||||
type DatabaseRoleUpdateData = Awaited<ReturnType<typeof updateDatabaseRole>>
|
||||
@@ -55,7 +48,7 @@ export const useDatabaseRoleUpdateMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(databaseRolesKeys.list(projectRef))
|
||||
await invalidateRolesQuery(queryClient, projectRef)
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -1,49 +1,41 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
|
||||
import type { PostgresRole } from '@supabase/postgres-meta'
|
||||
import pgMeta from '@supabase/pg-meta'
|
||||
import { QueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { get } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { databaseRolesKeys } from './keys'
|
||||
import { ExecuteSqlData, useExecuteSqlQuery } from 'data/sql/execute-sql-query'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
|
||||
export type DatabaseRolesVariables = {
|
||||
projectRef?: string
|
||||
connectionString?: string
|
||||
}
|
||||
|
||||
export async function getDatabaseRoles(
|
||||
{ projectRef, connectionString }: DatabaseRolesVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
export type PgRole = z.infer<typeof pgMeta.roles.zod>
|
||||
|
||||
let headers = new Headers()
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
const pgMetaRolesList = pgMeta.roles.list()
|
||||
|
||||
const { data, error } = await get('/platform/pg-meta/{ref}/roles', {
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString! },
|
||||
path: { ref: projectRef },
|
||||
},
|
||||
headers,
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data as PostgresRole[]
|
||||
}
|
||||
|
||||
export type DatabaseRolesData = Awaited<ReturnType<typeof getDatabaseRoles>>
|
||||
export type DatabaseRolesError = ResponseError
|
||||
export type DatabaseRolesData = z.infer<typeof pgMetaRolesList.zod>
|
||||
export type DatabaseRolesError = unknown
|
||||
|
||||
export const useDatabaseRolesQuery = <TData = DatabaseRolesData>(
|
||||
{ projectRef, connectionString }: DatabaseRolesVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<DatabaseRolesData, DatabaseRolesError, TData> = {}
|
||||
options: UseQueryOptions<ExecuteSqlData, DatabaseRolesError, TData> = {}
|
||||
) =>
|
||||
useQuery<DatabaseRolesData, DatabaseRolesError, TData>(
|
||||
databaseRolesKeys.list(projectRef),
|
||||
({ signal }) => getDatabaseRoles({ projectRef, connectionString }, signal),
|
||||
useExecuteSqlQuery(
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: pgMetaRolesList.sql,
|
||||
queryKey: ['roles', 'list'],
|
||||
},
|
||||
{
|
||||
select(data) {
|
||||
return data.result
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export function invalidateRolesQuery(client: QueryClient, projectRef: string | undefined) {
|
||||
return client.invalidateQueries(sqlKeys.query(projectRef, ['roles', 'list']))
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const databaseRolesKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'database-roles'] as const,
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import type { ResponseError } from 'types'
|
||||
import { databaseKeys } from './keys'
|
||||
import { invalidateSchemasQuery } from './schemas-query'
|
||||
|
||||
export type HooksEnableVariables = {
|
||||
ref: string
|
||||
@@ -34,7 +34,7 @@ export const useHooksEnableMutation = ({
|
||||
async onSuccess(data, variables, context) {
|
||||
const { ref } = variables
|
||||
await onSuccess?.(data, variables, context)
|
||||
await queryClient.invalidateQueries(databaseKeys.schemaList(ref))
|
||||
await invalidateSchemasQuery(queryClient, ref)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import type { ResponseError } from 'types'
|
||||
import { sqlKeys } from 'data/sql/keys'
|
||||
|
||||
// [Joshen] This is just temporary while we wait for Index Advisor to be baked into the image itself
|
||||
|
||||
export type IndexAdvisorEnableVariables = {
|
||||
projectRef: string
|
||||
connectionString?: string
|
||||
}
|
||||
|
||||
export async function enableIndexAdvisor({
|
||||
projectRef,
|
||||
connectionString,
|
||||
}: IndexAdvisorEnableVariables) {
|
||||
const sql = `
|
||||
create or replace function index_advisor(
|
||||
query text
|
||||
)
|
||||
returns table (
|
||||
startup_cost_before jsonb,
|
||||
startup_cost_after jsonb,
|
||||
total_cost_before jsonb,
|
||||
total_cost_after jsonb,
|
||||
index_statements text[],
|
||||
errors text[]
|
||||
)
|
||||
volatile
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
n_args int;
|
||||
prepared_statement_name text = 'index_advisor_working_statement';
|
||||
hypopg_schema_name text = (select extnamespace::regnamespace::text from pg_extension where extname = 'hypopg');
|
||||
explain_plan_statement text;
|
||||
error_message text;
|
||||
rec record;
|
||||
plan_initial jsonb;
|
||||
plan_final jsonb;
|
||||
statements text[] = '{}';
|
||||
begin
|
||||
|
||||
-- Remove comment lines (its common that they contain semicolons)
|
||||
query := trim(
|
||||
regexp_replace(
|
||||
regexp_replace(
|
||||
regexp_replace(query,'\\/\\*.+\\*\\/', '', 'g'),
|
||||
'--[^\\r\\n]*', ' ', 'g'),
|
||||
'\\s+', ' ', 'g')
|
||||
);
|
||||
|
||||
-- Remove trailing semicolon
|
||||
query := regexp_replace(query, ';\\s*$', '');
|
||||
|
||||
begin
|
||||
-- Disallow multiple statements
|
||||
if query ilike '%;%' then
|
||||
raise exception 'Query must not contain a semicolon';
|
||||
end if;
|
||||
|
||||
-- Hack to support PostgREST because the prepared statement for args incorrectly defaults to text
|
||||
query := replace(query, 'WITH pgrst_payload AS (SELECT $1 AS json_data)', 'WITH pgrst_payload AS (SELECT $1::json AS json_data)');
|
||||
|
||||
-- Create a prepared statement for the given query
|
||||
deallocate all;
|
||||
execute format('prepare %I as %s', prepared_statement_name, query);
|
||||
|
||||
-- Detect how many arguments are present in the prepared statement
|
||||
n_args = (
|
||||
select
|
||||
coalesce(array_length(parameter_types, 1), 0)
|
||||
from
|
||||
pg_prepared_statements
|
||||
where
|
||||
name = prepared_statement_name
|
||||
limit
|
||||
1
|
||||
);
|
||||
|
||||
-- Create a SQL statement that can be executed to collect the explain plan
|
||||
explain_plan_statement = format(
|
||||
'set local plan_cache_mode = force_generic_plan; explain (format json) execute %I%s',
|
||||
--'explain (format json) execute %I%s',
|
||||
prepared_statement_name,
|
||||
case
|
||||
when n_args = 0 then ''
|
||||
else format(
|
||||
'(%s)', array_to_string(array_fill('null'::text, array[n_args]), ',')
|
||||
)
|
||||
end
|
||||
);
|
||||
|
||||
-- Store the query plan before any new indexes
|
||||
execute explain_plan_statement into plan_initial;
|
||||
|
||||
-- Create possible indexes
|
||||
for rec in (
|
||||
with extension_regclass as (
|
||||
select
|
||||
distinct objid as oid
|
||||
from
|
||||
pg_catalog.pg_depend
|
||||
where
|
||||
deptype = 'e'
|
||||
)
|
||||
select
|
||||
pc.relnamespace::regnamespace::text as schema_name,
|
||||
pc.relname as table_name,
|
||||
pa.attname as column_name,
|
||||
format(
|
||||
'select %I.hypopg_create_index($i$create index on %I.%I(%I)$i$)',
|
||||
hypopg_schema_name,
|
||||
pc.relnamespace::regnamespace::text,
|
||||
pc.relname,
|
||||
pa.attname
|
||||
) hypopg_statement
|
||||
from
|
||||
pg_catalog.pg_class pc
|
||||
join pg_catalog.pg_attribute pa
|
||||
on pc.oid = pa.attrelid
|
||||
left join extension_regclass er
|
||||
on pc.oid = er.oid
|
||||
left join pg_catalog.pg_index pi
|
||||
on pc.oid = pi.indrelid
|
||||
and (select array_agg(x) from unnest(pi.indkey) v(x)) = array[pa.attnum]
|
||||
and pi.indexprs is null -- ignore expression indexes
|
||||
and pi.indpred is null -- ignore partial indexes
|
||||
where
|
||||
pc.relnamespace::regnamespace::text not in ( -- ignore schema list
|
||||
'pg_catalog', 'pg_toast', 'information_schema'
|
||||
)
|
||||
and er.oid is null -- ignore entities owned by extensions
|
||||
and pc.relkind in ('r', 'm') -- regular tables, and materialized views
|
||||
and pc.relpersistence = 'p' -- permanent tables (not unlogged or temporary)
|
||||
and pa.attnum > 0
|
||||
and not pa.attisdropped
|
||||
and pi.indrelid is null
|
||||
and pa.atttypid in (20,16,1082,1184,1114,701,23,21,700,1083,2950,1700,25,18,1042,1043)
|
||||
)
|
||||
loop
|
||||
-- Create the hypothetical index
|
||||
execute rec.hypopg_statement;
|
||||
end loop;
|
||||
|
||||
/*
|
||||
for rec in select * from hypopg()
|
||||
loop
|
||||
raise notice '%', rec;
|
||||
end loop;
|
||||
*/
|
||||
|
||||
-- Create a prepared statement for the given query
|
||||
-- The original prepared statement MUST be dropped because its plan is cached
|
||||
execute format('deallocate %I', prepared_statement_name);
|
||||
execute format('prepare %I as %s', prepared_statement_name, query);
|
||||
|
||||
-- Store the query plan after new indexes
|
||||
execute explain_plan_statement into plan_final;
|
||||
|
||||
--raise notice '%', plan_final;
|
||||
|
||||
-- Idenfity referenced indexes in new plan
|
||||
execute format(
|
||||
'select
|
||||
coalesce(array_agg(hypopg_get_indexdef(indexrelid) order by indrelid, indkey::text), $i\${}$i$::text[])
|
||||
from
|
||||
%I.hypopg()
|
||||
where
|
||||
%s ilike ($i$%%$i$ || indexname || $i$%%$i$)
|
||||
',
|
||||
hypopg_schema_name,
|
||||
quote_literal(plan_final)::text
|
||||
) into statements;
|
||||
|
||||
-- Reset all hypothetical indexes
|
||||
perform hypopg_reset();
|
||||
|
||||
-- Reset prepared statements
|
||||
deallocate all;
|
||||
|
||||
return query values (
|
||||
(plan_initial -> 0 -> 'Plan' -> 'Startup Cost'),
|
||||
(plan_final -> 0 -> 'Plan' -> 'Startup Cost'),
|
||||
(plan_initial -> 0 -> 'Plan' -> 'Total Cost'),
|
||||
(plan_final -> 0 -> 'Plan' -> 'Total Cost'),
|
||||
statements::text[],
|
||||
array[]::text[]
|
||||
);
|
||||
return;
|
||||
|
||||
exception when others then
|
||||
get stacked diagnostics error_message = MESSAGE_TEXT;
|
||||
|
||||
return query values (
|
||||
null::jsonb,
|
||||
null::jsonb,
|
||||
null::jsonb,
|
||||
null::jsonb,
|
||||
array[]::text[],
|
||||
array[error_message]::text[]
|
||||
);
|
||||
return;
|
||||
end;
|
||||
|
||||
end;
|
||||
$$;
|
||||
`
|
||||
const { result } = await executeSql({
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql,
|
||||
queryKey: ['enable-index-advisor'],
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type IndexAdvisorEnableData = Awaited<ReturnType<typeof enableIndexAdvisor>>
|
||||
|
||||
export const useIndexAdvisorEnableMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<IndexAdvisorEnableData, ResponseError, IndexAdvisorEnableVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<IndexAdvisorEnableData, ResponseError, IndexAdvisorEnableVariables>(
|
||||
(vars) => enableIndexAdvisor(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
queryClient.invalidateQueries(sqlKeys.query(projectRef, ['index-advisor-function-check']))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to enable index advisor: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export const databaseKeys = {
|
||||
schemaList: (projectRef: string | undefined) => ['projects', projectRef, 'schemas'] as const,
|
||||
backups: (projectRef: string | undefined) => [projectRef, 'database', 'backups'] as const,
|
||||
poolingConfiguration: (projectRef: string | undefined) =>
|
||||
[projectRef, 'database', 'pooling-configuration'] as const,
|
||||
|
||||
Loaded 100 of 329 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user