mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 02:15:05 +03:00
24 files changed
+180
-178
No files matched your search
@@ -38,6 +38,12 @@ Primary keys are **guaranteed not to change**. Columns, indices, constraints or
|
||||
|
||||
You may delete users directly or via the management console at Authentication > Users. Note that deleting a user from the `auth.users` table does not automatically sign out a user. As Supabase makes use of JSON Web Tokens (JWT), a user's JWT will remain "valid" until it has expired. Should you wish to immediately revoke access for a user, do considering making use of a Row Level Security policy as described below.
|
||||
|
||||
<Admonition type="caution">
|
||||
You cannot delete a user if they are the owner of any objects in Supabase Storage.
|
||||
|
||||
You will encounter an error when you try to delete an Auth user that owns any Storage objects. If this happens, try deleting all the objects for that user, or reassign ownership to another user.
|
||||
</Admonition>
|
||||
|
||||
## Public access
|
||||
|
||||
Since Row Level Security is enabled, this table is accessible via the API but no data will be returned unless we set up some Policies.
|
||||
|
||||
@@ -17,6 +17,7 @@ This guide explains how to quickly connect the Postgres database provided by Sup
|
||||
## Step 1: Get the connection string from Supabase project settings
|
||||
|
||||
Go to the settings page from the sidebar and navigate to the **Database** tab. You’ll find the database’s connection string with a placeholder for the password you provided when you created the project.
|
||||
|
||||

|
||||
|
||||
## Step 2: Testing the connection
|
||||
@@ -59,27 +60,17 @@ This starter also comes with the following packages installed:
|
||||
|
||||
### Configuring the project
|
||||
|
||||
By default, Prisma migrations will try to drop the `postgres` database, which can lead to conflicts with Supabase databases. For this scenario, use [Prisma Shadow Databases](https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#cloud-hosted-shadow-databases-must-be-created-manually).
|
||||
|
||||
Create a shadow database in your PostgreSQL server within the same Supabase project using the `psql` CLI and the `DATABASE_URL` from the previous steps (or use the local database).
|
||||
Create a `.env` file at the root of your project:
|
||||
|
||||
```bash
|
||||
psql postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432
|
||||
touch .env
|
||||
```
|
||||
|
||||
After you connect to your project's PostgreSQL instance, create another database (e.g., `postgres_shadow`):
|
||||
|
||||
```bash
|
||||
postgres=> CREATE DATABASE postgres_shadow;
|
||||
postgres=> exit
|
||||
```
|
||||
|
||||
In the `.env` file, update `DATABASE_URL` and `SHADOW_DATABASE_URL` to the connection string from **step 1**. The `.env` file should look like:
|
||||
In the `.env` file, add a `DATABASE_URL` variable and add the connection string from **step 1**. The `.env` file should look like:
|
||||
|
||||
```env
|
||||
# .env
|
||||
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"
|
||||
SHADOW_DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres_shadow"
|
||||
```
|
||||
|
||||
This is what your `schema.prisma` file should look like:
|
||||
@@ -88,7 +79,6 @@ This is what your `schema.prisma` file should look like:
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
@@ -141,7 +131,7 @@ That’s it! You have now successfully connected a Prisma project to a PostgreSQ
|
||||
|
||||
If you’re working in a serverless environment (for example Node.js functions hosted on AWS Lambda, Vercel or Netlify Functions), you need to set up [connection pooling](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#serverless-environments-faas) using a tool like [PgBouncer](https://www.pgbouncer.org/). That’s because every function invocation may result in a [new connection to the database](https://www.prisma.io/docs/guides/performance-and-optimization/connection-management#the-serverless-challenge).
|
||||
|
||||
Supabase [supports connection management using PgBouncer](/docs/guides/database/connecting-to-postgres#connection-pool).
|
||||
Supabase [supports connection management using PgBouncer](/docs/guides/database/connecting-to-postgres#connection-pool) which prevents a traffic spike from overwhelming your database.
|
||||
|
||||
Go to the **Database** page from the sidebar in the Supabase dashboard and navigate to **Connection pool** settings:
|
||||
|
||||
@@ -153,18 +143,17 @@ Update your `.env` file with the following changes:
|
||||
1. Rename the `DATABASE_URL` environment variable to `DIRECT_URL`
|
||||
1. Create a `DATABASE_URL` environment variable and paste in the new connection string from the dashboard as its value
|
||||
|
||||
It is recommended to minimize the number of concurrent connections by setting the `connection_limit` to `1`. You can set connection limit by appending `?connection_limit=1` to your connection string
|
||||
Append the `?pgbouncer=true` flag to the `DATABASE_URL` variable.
|
||||
|
||||
Your `.env` file should resemble the following:
|
||||
|
||||
```env
|
||||
# .env
|
||||
# PostgreSQL connection string used for migrations
|
||||
DIRECT_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres"
|
||||
DIRECT_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres"
|
||||
# PostgreSQL connection string with pgBouncer config — used by Prisma Client
|
||||
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?connection_limit=1"
|
||||
DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"
|
||||
|
||||
SHADOW_DATABASE_URL="postgres://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres_shadow"
|
||||
```
|
||||
|
||||
Update your Prisma schema by setting the `directUrl` in the datasource block:
|
||||
@@ -173,8 +162,7 @@ Update your Prisma schema by setting the `directUrl` in the datasource block:
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
}
|
||||
```
|
||||
|
||||
@@ -184,11 +172,11 @@ If you want to learn more about Prisma, check out the [docs](https://www.prisma.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 1. Missing grants
|
||||
### Missing grants
|
||||
|
||||
If you run `prisma migrate dev --name init` multiple times, it sometimes asks if you want to recreate the whole schema. If you chose yes, it will delete the public schema and recreate it. The default grants are missing after this.
|
||||
If your database schema is out of sync from your migration history, `prisma migrate dev` will detect a migration history conflict or a [schema drift](https://www.prisma.io/docs/guides/database/developing-with-prisma-migrate/troubleshooting-development#schema-drift). When `prisma migrate dev` detects the drift, it might ask to to reset your database schema. If you choose yes, it will delete the `public` schema along with the default grants defined in your database.
|
||||
|
||||
If you run into this problem, create a draft migration using `prisma migrate dev --create-only`, and add the following helper SQL:
|
||||
If you run into this problem, create a draft migration using `prisma migrate dev --create-only`, and add the following helper SQL:
|
||||
|
||||
```sql
|
||||
grant usage on schema public to postgres, anon, authenticated, service_role;
|
||||
@@ -204,16 +192,15 @@ alter default privileges in schema public grant all on sequences to postgres, an
|
||||
|
||||
Run `prisma migrate dev` to apply the draft migration to the database.
|
||||
|
||||
### 2. Using Prisma with multiple PostgreSQL schemas
|
||||
### Using Supabase Auth with Prisma
|
||||
|
||||
If you're using multiple database schemas, enable the `multiSchema` Preview feature flag in the `generator` block of your Prisma schema:
|
||||
If you would like to use Supabase Auth and Prisma in your application, you will have to enable the `multiSchema` Preview feature flag in the `generator` block of your Prisma schema:
|
||||
|
||||
```go
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
@@ -229,7 +216,6 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
schemas = ["public", "auth"]
|
||||
}
|
||||
|
||||
@@ -252,7 +238,16 @@ model User {
|
||||
|
||||
To learn more about using Prisma with multiple database schemas, refer to [this page in the Prisma docs](https://www.prisma.io/docs/guides/database/multi-schema#learn-more-about-the-multischema-preview-feature).
|
||||
|
||||
### 3. Enabling PosgreSQL extensions
|
||||
### Using PostgreSQL Row Level Security with Prisma
|
||||
|
||||
|
||||
If you would like to use Row Level Security (RLS) with Prisma, check out the [Prisma Client Extension - Row Level Security example](https://github.com/prisma/prisma-client-extensions/tree/main/row-level-security) that provides the primitives you could use to build and extend Prisma Client in PostgreSQL.
|
||||
|
||||
Also check out [useSupabaseRowLevelSecurity](https://github.com/dthyresson/prisma-extension-supabase-rls) Prisma Client extension that supports [Supabase RLS](/docs/guides/auth/row-level-security#authrole) and policies written to use [Supabase auth](/docs/guides/auth/overview).
|
||||
|
||||
The example and extension use [Prisma Client extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions) Preview feature.
|
||||
|
||||
### Enabling PosgreSQL extensions
|
||||
|
||||
If you would like to use a PostgreSQL extension with Prisma, enable the `postgresqlExtensions` Preview feature flag in the `generator` block of your Prisma schema:
|
||||
|
||||
@@ -261,7 +256,6 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
@@ -277,7 +271,6 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directURL = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
extensions = [hstore(schema: "myHstoreSchema"), pg_trgm, postgis(version: "2.1")]
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ Dave Wilson
|
||||
Div Arora
|
||||
Egor Romanov
|
||||
Fabrizio Fenoglio
|
||||
Francesco Sansalvadore
|
||||
Haydn Maley
|
||||
Hieu Pham
|
||||
Inian P
|
||||
|
||||
@@ -87,8 +87,8 @@ An Information request about Supabase or feature request.
|
||||
|
||||
| Severity Level | Standard | Priority | Priority Plus |
|
||||
| -------------- | ------------------------------------- | ------------------------------------- | -------------------------------------- |
|
||||
| 1. Urgent | 1 business hour<br />24/7 × 365 | 1 business hour<br />24/7 × 365 | 1 business hour<br />24/7 × 365 |
|
||||
| 2. High | 4 business hours<br />Monday - Friday | 2 business hours<br />Monday - Friday | 2 business hours<br />24/7 × 365 |
|
||||
| 1. Urgent | 1 hour<br />24/7 × 365 | 1 hour<br />24/7 × 365 | 1 hour<br />24/7 × 365 |
|
||||
| 2. High | 4 business hours<br />Monday - Friday | 2 business hours<br />Monday - Friday | 2 hours<br />24/7 × 365 |
|
||||
| 3. Normal | 1 business day<br />Monday - Friday | 1 business day<br />Monday - Friday | 12 business hours<br />Monday - Friday |
|
||||
| 4. Low | 2 business days<br />Monday - Friday | 2 business days<br />Monday - Friday | 1 business day<br />Monday - Friday |
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, IconArchive, IconCode, IconDatabase, IconKey } from 'ui'
|
||||
import { IconArchive, IconCode, IconDatabase, IconKey, IconZap } from 'ui'
|
||||
|
||||
export const CANCELLATION_REASONS = [
|
||||
'Pricing',
|
||||
@@ -154,4 +154,36 @@ export const USAGE_BASED_PRODUCTS = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Realtime',
|
||||
icon: <IconZap className="dark:text-scale-100" size={16} strokeWidth={2} />,
|
||||
features: [
|
||||
{
|
||||
key: 'realtime_message_count',
|
||||
attribute: 'total_realtime_message_count',
|
||||
title: 'Realtime Messages',
|
||||
units: 'absolute',
|
||||
costPerUnit: 0.0000025,
|
||||
tooltip: (
|
||||
<span>
|
||||
Billing is based on the total amount of messages throughout your billing period.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'realtime_peak_connection',
|
||||
attribute: 'total_realtime_peak_connection',
|
||||
title: 'Realtime Concurrent Peak Connections',
|
||||
units: 'absolute',
|
||||
costPerUnit: 0.01,
|
||||
tooltip: (
|
||||
<span>
|
||||
Billing is based on the maximum amount of concurrent peak connections throughout your
|
||||
billing period.
|
||||
</span>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -47,7 +47,7 @@ const EdgeFunctionDetails: FC<Props> = () => {
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
: '[YOUR ANON KEY]'
|
||||
|
||||
const endpoint = apiService?.app_config.endpoint ?? ''
|
||||
const endpointSections = endpoint.split('.')
|
||||
|
||||
@@ -23,7 +23,7 @@ const TerminalInstructions: FC<Props> = ({ closable = false, removeBorder = fals
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
: '[YOUR ANON KEY]'
|
||||
const endpoint = settings?.autoApiService.app_config.endpoint ?? ''
|
||||
|
||||
const endpointSections = endpoint.split('.')
|
||||
|
||||
@@ -44,10 +44,10 @@ import styles from './graphiql.module.css'
|
||||
export type GraphiQLProps = {
|
||||
fetcher: Fetcher
|
||||
theme?: 'dark' | 'light'
|
||||
apiKey?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
const GraphiQL = ({ fetcher, theme = 'dark', apiKey }: GraphiQLProps) => {
|
||||
const GraphiQL = ({ fetcher, theme = 'dark', accessToken }: GraphiQLProps) => {
|
||||
// Ensure props are correct
|
||||
if (typeof fetcher !== 'function') {
|
||||
throw new TypeError(
|
||||
@@ -56,7 +56,14 @@ const GraphiQL = ({ fetcher, theme = 'dark', apiKey }: GraphiQLProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphiQLProvider fetcher={fetcher} defaultHeaders={JSON.stringify({ apiKey }, null, 2)}>
|
||||
<GraphiQLProvider
|
||||
fetcher={fetcher}
|
||||
defaultHeaders={
|
||||
accessToken !== undefined
|
||||
? JSON.stringify({ Authorization: `Bearer ${accessToken}` })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<GraphiQLInterface theme={theme} />
|
||||
</GraphiQLProvider>
|
||||
)
|
||||
|
||||
@@ -122,17 +122,12 @@ const TableGridEditor: FC<Props> = ({
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
const swaggerUrl = settings?.autoApiService?.restUrl
|
||||
|
||||
const {
|
||||
data: jsonSchema,
|
||||
error: jsonSchemaError,
|
||||
refetch,
|
||||
} = useProjectJsonSchemaQuery({
|
||||
projectRef,
|
||||
swaggerUrl,
|
||||
apiKey: anonKey,
|
||||
})
|
||||
} = useProjectJsonSchemaQuery({ projectRef })
|
||||
|
||||
if (jsonSchemaError) console.log('jsonSchemaError', jsonSchemaError)
|
||||
|
||||
|
||||
@@ -37,9 +37,7 @@ const ConnectingState: FC<Props> = ({ project }) => {
|
||||
}, [project])
|
||||
|
||||
const testProjectConnection = async () => {
|
||||
const result = await pingPostgrest(project.restUrl!, project.ref, {
|
||||
kpsVersion: project.kpsVersion,
|
||||
})
|
||||
const result = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion })
|
||||
if (result) {
|
||||
clearInterval(checkProjectConnectionIntervalRef.current)
|
||||
app.onProjectPostgrestStatusUpdated(project.id, 'ONLINE')
|
||||
|
||||
@@ -118,7 +118,7 @@ const SignInLayout = ({
|
||||
|
||||
<div className="flex flex-1">
|
||||
<main className="flex flex-col items-center flex-1 flex-shrink-0 px-5 pt-16 pb-8 border-r shadow-lg bg-scale-200 border-scale-500">
|
||||
<div className="flex-1 flex flex-col justify-center w-[384px]">
|
||||
<div className="flex-1 flex flex-col justify-center w-[330px] sm:w-[384px]">
|
||||
<div className="mb-10">
|
||||
<h1 className="mt-8 mb-2 text-2xl lg:text-3xl">{heading}</h1>
|
||||
<h2 className="text-sm text-scale-1100">{subheading}</h2>
|
||||
|
||||
@@ -5,9 +5,13 @@ import CodeSnippet from '../CodeSnippet'
|
||||
export default function Authentication({ autoApiService, selectedLang, showApiKey }) {
|
||||
// [Joshen] ShowApiKey should really be a boolean, its confusing
|
||||
const defaultApiKey =
|
||||
showApiKey !== 'SUPABASE_KEY' ? autoApiService.defaultApiKey : 'SUPABASE_CLIENT_API_KEY'
|
||||
showApiKey !== 'SUPABASE_KEY'
|
||||
? autoApiService?.defaultApiKey ?? 'SUPABASE_CLIENT_API_KEY'
|
||||
: 'SUPABASE_CLIENT_API_KEY'
|
||||
const serviceApiKey =
|
||||
showApiKey !== 'SUPABASE_KEY' ? autoApiService.serviceApiKey : 'SUPABASE_SERVICE_KEY'
|
||||
showApiKey !== 'SUPABASE_KEY'
|
||||
? autoApiService?.serviceApiKey ?? 'SUPABASE_SERVICE_KEY'
|
||||
: 'SUPABASE_SERVICE_KEY'
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const authKeys = {
|
||||
accessToken: () => ['access-token'] as const,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { authKeys } from './keys'
|
||||
import { auth } from 'lib/gotrue'
|
||||
|
||||
export async function getSessionAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
const {
|
||||
data: { session },
|
||||
} = await auth.getSession()
|
||||
return session?.access_token
|
||||
}
|
||||
|
||||
export type SessionAccessTokenData = Awaited<ReturnType<typeof getSessionAccessToken>>
|
||||
export type SessionAccessTokenError = unknown
|
||||
|
||||
export const useSessionAccessTokenQuery = <TData = SessionAccessTokenData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<SessionAccessTokenData, SessionAccessTokenError, TData> = {}) =>
|
||||
useQuery<SessionAccessTokenData, SessionAccessTokenError, TData>(
|
||||
authKeys.accessToken(),
|
||||
() => getSessionAccessToken(),
|
||||
options
|
||||
)
|
||||
|
||||
export const useSessionAccessTokenPrefetch = () => {
|
||||
const client = useQueryClient()
|
||||
return useCallback(
|
||||
() => client.prefetchQuery(authKeys.accessToken(), () => getSessionAccessToken()),
|
||||
[]
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
export const docsKeys = {
|
||||
jsonSchema: (
|
||||
projectRef: string | undefined,
|
||||
swaggerUrl: string | undefined,
|
||||
apiKey: string | undefined
|
||||
) => ['projects', projectRef, 'docs', { swaggerUrl, apiKey }] as const,
|
||||
jsonSchema: (projectRef: string | undefined) => ['projects', projectRef, 'docs'] as const,
|
||||
}
|
||||
@@ -1,41 +1,26 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { docsKeys } from './keys'
|
||||
|
||||
export type ProjectJsonSchemaVariables = {
|
||||
projectRef?: string
|
||||
swaggerUrl?: string
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
export type ProjectJsonSchemaResponse = any
|
||||
|
||||
export async function getProjectJsonSchema(
|
||||
{ projectRef, swaggerUrl, apiKey }: ProjectJsonSchemaVariables,
|
||||
{ projectRef }: ProjectJsonSchemaVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!swaggerUrl) {
|
||||
throw new Error('id is required')
|
||||
}
|
||||
|
||||
let headers: {
|
||||
[key: string]: string | undefined
|
||||
} = { apikey: apiKey }
|
||||
if ((apiKey?.length ?? 0) > 40) headers['Authorization'] = `Bearer ${apiKey}`
|
||||
|
||||
const response = await get(swaggerUrl, {
|
||||
signal,
|
||||
headers,
|
||||
credentials: 'omit',
|
||||
})
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
const url = `${API_URL}/projects/${projectRef}/api/rest`
|
||||
const response = await get(url, { signal })
|
||||
if (response.error) throw response.error
|
||||
return response as ProjectJsonSchemaResponse
|
||||
}
|
||||
|
||||
@@ -43,33 +28,29 @@ export type ProjectJsonSchemaData = Awaited<ReturnType<typeof getProjectJsonSche
|
||||
export type ProjectJsonSchemaError = unknown
|
||||
|
||||
export const useProjectJsonSchemaQuery = <TData = ProjectJsonSchemaData>(
|
||||
{ projectRef, swaggerUrl, apiKey }: ProjectJsonSchemaVariables,
|
||||
{ projectRef }: ProjectJsonSchemaVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ProjectJsonSchemaData, ProjectJsonSchemaError, TData> = {}
|
||||
) =>
|
||||
useQuery<ProjectJsonSchemaData, ProjectJsonSchemaError, TData>(
|
||||
docsKeys.jsonSchema(projectRef, swaggerUrl, apiKey),
|
||||
({ signal }) => getProjectJsonSchema({ projectRef, swaggerUrl, apiKey }, signal),
|
||||
docsKeys.jsonSchema(projectRef),
|
||||
({ signal }) => getProjectJsonSchema({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof swaggerUrl !== 'undefined',
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
|
||||
export const useProjectJsonSchemaPrefetch = ({
|
||||
projectRef,
|
||||
swaggerUrl,
|
||||
apiKey,
|
||||
}: ProjectJsonSchemaVariables) => {
|
||||
export const useProjectJsonSchemaPrefetch = ({ projectRef }: ProjectJsonSchemaVariables) => {
|
||||
const client = useQueryClient()
|
||||
|
||||
return useCallback(() => {
|
||||
if (projectRef && swaggerUrl) {
|
||||
client.prefetchQuery(docsKeys.jsonSchema(projectRef, swaggerUrl, apiKey), ({ signal }) =>
|
||||
getProjectJsonSchema({ projectRef, swaggerUrl, apiKey }, signal)
|
||||
if (projectRef) {
|
||||
client.prefetchQuery(docsKeys.jsonSchema(projectRef), ({ signal }) =>
|
||||
getProjectJsonSchema({ projectRef }, signal)
|
||||
)
|
||||
}
|
||||
}, [projectRef, swaggerUrl])
|
||||
}, [projectRef])
|
||||
}
|
||||
+10
-28
@@ -2,6 +2,8 @@ import semver from 'semver'
|
||||
import { headWithTimeout, getWithTimeout } from './common/fetch'
|
||||
import { API_URL } from './constants'
|
||||
|
||||
const DEFAULT_TIMEOUT_MILLISECONDS = 2000
|
||||
|
||||
/**
|
||||
* Ping Postgrest for health check. Default timeout in 2s.
|
||||
*
|
||||
@@ -15,7 +17,6 @@ import { API_URL } from './constants'
|
||||
* @return true if ping is successful else false
|
||||
*/
|
||||
async function pingPostgrest(
|
||||
restUrl: string,
|
||||
projectRef: string,
|
||||
options?: {
|
||||
kpsVersion?: string
|
||||
@@ -23,8 +24,6 @@ async function pingPostgrest(
|
||||
}
|
||||
) {
|
||||
if (projectRef === undefined) return false
|
||||
const apiKey = await getApiKey(projectRef, options?.timeout)
|
||||
if (apiKey === undefined) return false
|
||||
|
||||
const { kpsVersion, timeout } = options ?? {}
|
||||
const healthCheckApiEnable = semver.gte(
|
||||
@@ -33,35 +32,21 @@ async function pingPostgrest(
|
||||
semver.coerce('kps-v3.8.6')
|
||||
)
|
||||
if (healthCheckApiEnable) {
|
||||
const healthCheckUrl = `${restUrl.replace('/rest/', '/rest-admin/')}live`
|
||||
return pingHealthCheckApi(healthCheckUrl, apiKey, timeout)
|
||||
return pingHealthCheckApi(projectRef, timeout)
|
||||
} else {
|
||||
return pingOpenApi(projectRef, timeout)
|
||||
}
|
||||
|
||||
return pingOpenApi(restUrl, apiKey, timeout)
|
||||
}
|
||||
|
||||
export default pingPostgrest
|
||||
|
||||
const getApiKey = async (
|
||||
projectRef: string,
|
||||
timeout = DEFAULT_TIMEOUT_MILLISECONDS
|
||||
): Promise<string | undefined> => {
|
||||
const response = await getWithTimeout(`${API_URL}/props/project/${projectRef}/api`, { timeout })
|
||||
if (response.error || response.autoApiService.service_api_keys.length === 0) return undefined
|
||||
return response.autoApiService.defaultApiKey
|
||||
}
|
||||
|
||||
const DEFAULT_TIMEOUT_MILLISECONDS = 2000
|
||||
|
||||
/**
|
||||
* Send a HEAD request to postgrest OpenAPI.
|
||||
*
|
||||
* @return true if there's no error else false
|
||||
*/
|
||||
async function pingOpenApi(url: string, apikey: string, timeout?: number) {
|
||||
const headers = { apikey, Authorization: `Bearer ${apikey}` }
|
||||
const { error } = await headWithTimeout(url, [], {
|
||||
headers,
|
||||
credentials: 'omit',
|
||||
async function pingOpenApi(ref: string, timeout?: number) {
|
||||
const { error } = await headWithTimeout(`${API_URL}/projects/${ref}/api/rest`, [], {
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MILLISECONDS,
|
||||
})
|
||||
return error === undefined
|
||||
@@ -72,11 +57,8 @@ async function pingOpenApi(url: string, apikey: string, timeout?: number) {
|
||||
*
|
||||
* @return true if there's no error else false
|
||||
*/
|
||||
async function pingHealthCheckApi(url: string, apikey: string, timeout?: number) {
|
||||
const headers = { apikey }
|
||||
const { error } = await getWithTimeout(url, {
|
||||
headers,
|
||||
credentials: 'omit',
|
||||
async function pingHealthCheckApi(ref: string, timeout?: number) {
|
||||
const { error } = await getWithTimeout(`${API_URL}/projects/${ref}/live`, {
|
||||
timeout: timeout ?? DEFAULT_TIMEOUT_MILLISECONDS,
|
||||
})
|
||||
return error === undefined
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit'
|
||||
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard'
|
||||
import GraphiQL from 'components/interfaces/GraphQL/GraphiQL'
|
||||
import { DocsLayout } from 'components/layouts'
|
||||
import Connecting from 'components/ui/Loading/Loading'
|
||||
import { useParams, useStore } from 'hooks'
|
||||
import { useProjectApiQuery } from 'data/config/project-api-query'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { IconAlertCircle } from 'ui'
|
||||
import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
|
||||
|
||||
const GraphiQLPage: NextPageWithLayout = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
@@ -17,6 +17,7 @@ const GraphiQLPage: NextPageWithLayout = () => {
|
||||
|
||||
const isExtensionsLoading = meta.extensions.isLoading
|
||||
const pgGraphqlExtension = meta.extensions.byId('pg_graphql')
|
||||
const { data: accessToken } = useSessionAccessTokenQuery()
|
||||
|
||||
useEffect(() => {
|
||||
if (ui.selectedProject?.ref) {
|
||||
@@ -26,42 +27,16 @@ const GraphiQLPage: NextPageWithLayout = () => {
|
||||
}
|
||||
}, [ui.selectedProject?.ref])
|
||||
|
||||
const {
|
||||
data: settings,
|
||||
isError: isProjectSettingsError,
|
||||
isLoading: isProjectSettingsLoading,
|
||||
} = useProjectApiQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
const apiService = settings?.autoApiService
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
const endpoint = settings?.autoApiService.app_config.endpoint ?? ''
|
||||
|
||||
const graphqlUrl = `https://${endpoint}/graphql/v1`
|
||||
const graphqlUrl = `${API_URL}/projects/${projectRef}/api/graphql`
|
||||
|
||||
const fetcher = useMemo(() => {
|
||||
return createGraphiQLFetcher({
|
||||
url: graphqlUrl,
|
||||
fetch,
|
||||
})
|
||||
return createGraphiQLFetcher({ url: graphqlUrl, fetch })
|
||||
}, [graphqlUrl])
|
||||
|
||||
if (isProjectSettingsLoading || (isExtensionsLoading && !pgGraphqlExtension)) {
|
||||
if (!accessToken || (isExtensionsLoading && !pgGraphqlExtension)) {
|
||||
return <Connecting />
|
||||
}
|
||||
|
||||
if (isProjectSettingsError) {
|
||||
return (
|
||||
<div className="py-8 flex items-center justify-center space-x-2">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} />
|
||||
<p className="text-sm text-scale-1100">Failed to retrieve API keys</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (pgGraphqlExtension?.installed_version === null) {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col justify-center items-center px-4">
|
||||
@@ -80,7 +55,7 @@ const GraphiQLPage: NextPageWithLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
return <GraphiQL fetcher={fetcher} theme={ui.theme} apiKey={anonKey} />
|
||||
return <GraphiQL fetcher={fetcher} theme={ui.theme} accessToken={accessToken} />
|
||||
}
|
||||
|
||||
GraphiQLPage.getLayout = (page) => <DocsLayout title="GraphiQL">{page}</DocsLayout>
|
||||
|
||||
@@ -77,17 +77,12 @@ const DocView: FC<any> = observer(({}) => {
|
||||
const anonKey = apiService?.service_api_keys.find((x) => x.name === 'anon key')
|
||||
? apiService.defaultApiKey
|
||||
: undefined
|
||||
const swaggerUrl = data?.autoApiService?.restUrl
|
||||
|
||||
const {
|
||||
data: jsonSchema,
|
||||
error: jsonSchemaError,
|
||||
refetch,
|
||||
} = useProjectJsonSchemaQuery({
|
||||
projectRef,
|
||||
swaggerUrl,
|
||||
apiKey: anonKey,
|
||||
})
|
||||
} = useProjectJsonSchemaQuery({ projectRef })
|
||||
|
||||
useEffect(() => {
|
||||
PageState.setJsonSchema(jsonSchema)
|
||||
|
||||
@@ -62,7 +62,7 @@ const QueryPerformanceReport: NextPageWithLayout = () => {
|
||||
|
||||
It relies on the \`pg_stat_statements\` table. Read more about [examining query performance]((https://supabase.com/docs/guides/platform/performance#examining-query-performance)).
|
||||
|
||||
Consider resetting the analysis after optimising any queries.
|
||||
Consider resetting the analysis after optimizing any queries.
|
||||
|
||||
`
|
||||
|
||||
|
||||
@@ -68,9 +68,7 @@ export default class ProjectStore extends PostgresMetaInterface<Project> {
|
||||
|
||||
async pingPostgrest(project: Project): Promise<'ONLINE' | 'OFFLINE' | undefined> {
|
||||
if (project.status === PROJECT_STATUS.ACTIVE_HEALTHY && project.restUrl) {
|
||||
const success = await pingPostgrest(project.restUrl, project.ref, {
|
||||
kpsVersion: project.kpsVersion,
|
||||
})
|
||||
const success = await pingPostgrest(project.ref, { kpsVersion: project.kpsVersion })
|
||||
return success ? 'ONLINE' : 'OFFLINE'
|
||||
}
|
||||
return undefined
|
||||
|
||||
@@ -18,9 +18,11 @@ export const CONFIRMATION: FormSchema = {
|
||||
descriptionOptional: 'HTML body of your email',
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the e-mail address for the new account
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The users email address
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -49,9 +51,11 @@ export const INVITE: FormSchema = {
|
||||
descriptionOptional: 'HTML body of your email',
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .ConfirmationURL }}\` : URL to accept the invitation to create an account
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The users email address
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -80,9 +84,11 @@ export const MAGIC_LINK: FormSchema = {
|
||||
descriptionOptional: 'HTML body of your email',
|
||||
type: 'code',
|
||||
description: `
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the message
|
||||
- \`{{ .ConfirmationURL }}\` : URL for a one-time login to the user's account
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The users email address
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -114,6 +120,9 @@ export const EMAIL_CHANGE: FormSchema = {
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the email change
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The original users email address
|
||||
- \`{{ .NewEmail }\` : The users new email address
|
||||
`,
|
||||
},
|
||||
},
|
||||
@@ -145,6 +154,8 @@ export const RECOVERY: FormSchema = {
|
||||
- \`{{ .ConfirmationURL }}\` : URL to confirm the password reset
|
||||
- \`{{ .Token }}\` : The 6-digit numeric email OTP
|
||||
- \`{{ .TokenHash }}\` : The hashed token used in the URL
|
||||
- \`{{ .SiteURL }}\` : The URL of the site
|
||||
- \`{{ .Email }}\` : The users email address
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -183,7 +183,7 @@ export default class MetaStore implements IMetaStore {
|
||||
|
||||
this.openApi = new OpenApiStore(
|
||||
this.rootStore,
|
||||
`${API_URL}/props/project/${this.projectRef}/api`
|
||||
`${API_URL}/projects/${this.projectRef}/api/rest`
|
||||
)
|
||||
this.tables = new TableStore(this.rootStore, `${this.baseUrl}/tables`, this.headers)
|
||||
this.columns = new ColumnStore(this.rootStore, `${this.baseUrl}/columns`, this.headers)
|
||||
@@ -895,7 +895,7 @@ export default class MetaStore implements IMetaStore {
|
||||
this.headers['x-connection-encrypted'] = connectionString
|
||||
}
|
||||
|
||||
this.openApi.setUrl(`${API_URL}/props/project/${this.projectRef}/api`)
|
||||
this.openApi.setUrl(`${API_URL}/projects/${this.projectRef}/api/rest`)
|
||||
this.openApi.setHeaders(this.headers)
|
||||
|
||||
this.tables.setUrl(`${this.baseUrl}/tables`)
|
||||
|
||||
@@ -52,17 +52,7 @@ export default class OpenApiStore implements IOpenApiStore {
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const headers = { 'Content-Type': 'application/json', ...this.headers }
|
||||
const projectConfig = await get(this.url, { headers })
|
||||
if (projectConfig.error) throw projectConfig.error
|
||||
|
||||
const apiKey = projectConfig.autoApiService?.defaultApiKey
|
||||
const restApiUrl = projectConfig.autoApiService?.restUrl
|
||||
|
||||
const response = await get<OpenAPIV2.Document>(`${restApiUrl}?apikey=${apiKey}`, {
|
||||
headers: { ...headers, apiKey: apiKey, Authorization: `Bearer ${apiKey}` },
|
||||
credentials: 'omit',
|
||||
})
|
||||
const response = await get<OpenAPIV2.Document>(this.url)
|
||||
if (response.error) throw response.error
|
||||
|
||||
const tables = response.definitions
|
||||
|
||||
Reference in new issue
Block a user