Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/docs/content/guides/database/extensions/pg_net.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ It eliminates the need for servers to continuously poll for database changes and

```sql
-- Example: enable the "pg_net" extension.
create extension pg_net;
-- Note: The extension creates its own schema/namespace named "net" to avoid naming conflicts.
create extension pg_net with schema "extensions";
-- Note: The extension creates its own schema/namespace named "net" to avoid naming conflicts. Registering it in the extensions schema avoids exposing it in public and satisfies the Security Advisor check.

-- Example: disable the "pg_net" extension
drop extension if exists pg_net;
Expand Down
12 changes: 9 additions & 3 deletions apps/docs/content/guides/platform/temporary-access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Enabling temporary access only applies to connections to Postgres and Supavisor

<Admonition type="note">

[Enforce SSL](/docs/guides/platform/ssl-enforcement) on incoming connections must be enabled before temporary access can be used.

</Admonition>

<Admonition type="note">

Projects need to be at least on Postgres 17.6.1.081 (or higher) to enable temporary access. You can find the Postgres version of your project on the [General Settings](/dashboard/project/_/settings/general) page. If your project is on an older version, you will need to [upgrade](/docs/guides/platform/upgrading) to use this feature.

</Admonition>
Expand All @@ -27,19 +33,19 @@ export SUPABASE_MANAGEMENT_API_TOKEN="your-access-token"
export PROJECT_REF="your-project-ref"

# Get current temporary access status
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \
curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/jit-access" \
-H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN"

# Enable temporary access
curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \
curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/jit-access" \
-H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"state":"enabled"
}'

# Disable temporary access
curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/database/jit-access" \
curl -X PUT "https://api.supabase.com/v1/projects/$PROJECT_REF/jit-access" \
-H "Authorization: Bearer $SUPABASE_MANAGEMENT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
Expand Down
58 changes: 36 additions & 22 deletions apps/docs/content/guides/self-hosting/self-hosted-functions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ On managed Supabase platform, Edge Functions are deployed across multiple region
The default `hello` function is located at `volumes/functions/hello/index.ts`. You can invoke it immediately after starting your stack:

```sh
curl http://<your-domain>/functions/v1/hello
curl http://<your-domain>/functions/v1/hello \
--header 'apiKey: <sb_publishable/sb_secret key>'
```

This returns `"Hello from Edge Functions!"`.
This returns:

```json
{ "message": "Hello from Edge Functions!" }
```

## Create a new function

Expand All @@ -34,16 +39,20 @@ touch volumes/functions/my-function/index.ts
Add the following code to `index.ts`:

```typescript
Deno.serve(async (req: Request) => {
const { name } = await req.json()
const message = `Hello, ${name}!`
import { withSupabase } from '@supabase/server'

return new Response(JSON.stringify({ message }), {
headers: { 'Content-Type': 'application/json' },
})
})
export default {
fetch: withSupabase({ auth: 'none' }, async (req) => {
const { name } = await req.json()
const message = `Hello, ${name}!`

return Response.json({ message })
}),
}
```

The `auth` option controls who can call the function: `'none'` accepts every request, `'user'` requires a valid user JWT, and `'publishable'` / `'secret'` require an API key. See the [Edge Functions auth guide](/docs/guides/functions/auth) for details.

### Step 2: Restart the functions service to pick up the new function

```sh
Expand Down Expand Up @@ -134,25 +143,30 @@ The functions service is pre-configured with the following environment variables
| `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_...}` | New secret API key |
| `SUPABASE_JWKS` | `{"keys":[{...}]}` | JWKS used to verify JWTs issued by Auth |

Here's an example function that queries a table using `@supabase/supabase-js`:
Here's an example function that queries a table using the admin client provided by `@supabase/server`:

```typescript
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

Deno.serve(async () => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
import { withSupabase } from '@supabase/server'

const { data, error } = await supabase.from('todos').select('*')
export default {
fetch: withSupabase({ auth: 'secret' }, async (_req, ctx) => {
// ctx.supabaseAdmin bypasses RLS. This function requires a secret
// API key, so only server-to-server callers can reach it.
const { data, error } = await ctx.supabaseAdmin.from('todos').select('*')

return new Response(JSON.stringify({ data, error }), {
headers: { 'Content-Type': 'application/json' },
})
})
return Response.json({ data, error })
}),
}
```

`withSupabase` reads `SUPABASE_URL`, the API keys, and `SUPABASE_JWKS` from the environment variables above. You don't need to wire up `createClient` yourself.

<Admonition type="note">

`auth: 'user'` verifies caller JWTs against `SUPABASE_JWKS`. If you're on a legacy setup without it configured, see [New API Keys and Asymmetric Authentication](/docs/guides/self-hosting/self-hosted-auth-keys).

</Admonition>

### Internal vs external URLs

This is a key distinction that affects how you build URLs in your functions:
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/components/grid/SupabaseGrid.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export function useSyncTableEditorStateFromLocalStorageWithUrl({
}, [urlParams, table, projectRef])
}

export const handleCellKeyDown = <TRow extends SupaRow = SupaRow>(
export const handleCellKeyDown = <TRow extends Record<string, unknown> = SupaRow>(
args: CellKeyDownArgs<TRow, unknown>,
event: CellKeyboardEvent,
context?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
import { DestinationForm } from './DestinationForm'
import { DestinationType } from './DestinationPanel.types'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { ReadReplicaForm } from './ReadReplicaForm'
import { ReadReplicaForm } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaForm'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { DocsButton } from '@/components/ui/DocsButton'
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import { DestinationType } from './DestinationPanel/DestinationPanel.types'
import { DestinationRow } from './DestinationRow'
import { DisablePipelinesDialog } from './DisablePipelinesDialog'
import { EnablePipelinesModal } from './EnablePipelinesCallout'
import { ReadReplicaRow } from './ReadReplicas/ReadReplicaRow'
import { REPLICA_STATUS } from './Replication.constants'
import {
useIsETLBigQueryPrivateAlpha,
Expand All @@ -39,6 +38,7 @@ import {
useIsETLIcebergPrivateAlpha,
useIsETLSnowflakePrivateAlpha,
} from './useIsETLPrivateAlpha'
import { ReadReplicaRow } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicaRow'
import { AlertError } from '@/components/ui/AlertError'
import { DocsButton } from '@/components/ui/DocsButton'
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { components } from 'api-types'

import { PROJECT_STATUS } from '@/lib/constants'

export const STATUS_REFRESH_FREQUENCY_MS: number = 10000 // 10 seconds

export enum PipelineStatusName {
Expand All @@ -13,10 +9,5 @@ export enum PipelineStatusName {
UNKNOWN = 'unknown',
}

export const REPLICA_STATUS: {
[key: string]: components['schemas']['DatabaseStatusResponse']['status']
} = {
...PROJECT_STATUS,
INIT_READ_REPLICA: 'INIT_READ_REPLICA',
INIT_READ_REPLICA_FAILED: 'INIT_READ_REPLICA_FAILED',
}
/** @deprecated Import from Settings/Infrastructure/ReadReplicas/ReadReplicas.constants */
export { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.constants'
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'

import { DestinationIcon } from '../DestinationIcon'
import { getStatusName } from '../Pipeline.utils'
import { getStatusLabel } from '../ReadReplicas/ReadReplicas.utils'
import { STATUS_REFRESH_FREQUENCY_MS } from '../Replication.constants'
import { getReplicationDestinationType } from './Nodes.utils'
import { getStatusLabel } from '@/components/interfaces/Settings/Infrastructure/ReadReplicas/ReadReplicas.utils'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { formatDatabaseID } from '@/data/read-replicas/replicas.utils'
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { FeatureFlagContext } from 'common'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { ExplorerQuerySourceMenu } from './ExplorerQuerySourceMenu'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'

mockAnimationsApi()

beforeEach(() => {
addAPIMock({
method: 'get',
path: '/platform/projects/:ref',
response: {
id: 1,
ref: 'default',
organization_id: 1,
name: 'Test Project',
status: 'ACTIVE_HEALTHY',
cloud_provider: 'AWS',
region: 'us-east-1',
db_host: 'db.default.supabase.co',
restUrl: 'https://default.supabase.co/rest/v1/',
inserted_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
subscription_id: 'sub_123',
is_branch_enabled: false,
is_physical_backups_enabled: false,
high_availability: false,
integration_source: null,
connectionString: 'postgresql://postgres@localhost:5432/postgres',
is_hibernating: false,
},
})
})

describe('ExplorerQuerySourceMenu', () => {
const renderWithFlags = (
source: Parameters<typeof ExplorerQuerySourceMenu>[0]['source'],
flags: Record<string, boolean>
) =>
customRender(
<FeatureFlagContext.Provider value={{ configcat: flags, posthog: {}, hasLoaded: true }}>
<ExplorerQuerySourceMenu source={source} onSourceChange={vi.fn()} />
</FeatureFlagContext.Provider>
)

it('emits a complete default binding when the query changes source', async () => {
const onSourceChange = vi.fn()

customRender(
<ExplorerQuerySourceMenu
source={{
_tag: 'logs',
time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' },
}}
onSourceChange={onSourceChange}
/>
)

await userEvent.click(screen.getByRole('button', { name: 'Query source: Logs' }))
await userEvent.click(screen.getByText('Database'))

expect(onSourceChange).toHaveBeenCalledWith({ _tag: 'database' })
})

it('emits the selected log time range as source parameters', async () => {
const onSourceChange = vi.fn()

customRender(
<ExplorerQuerySourceMenu
source={{
_tag: 'logs',
time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' },
}}
onSourceChange={onSourceChange}
/>
)

await userEvent.click(screen.getByRole('button', { name: 'Query source: Logs' }))
await userEvent.hover(screen.getByText('Time range'))
await userEvent.click(await screen.findByText('Last 3 hours'))

expect(onSourceChange).toHaveBeenCalledWith({
_tag: 'logs',
time_range: { _tag: 'relative_time_range', amount: 3, unit: 'hour' },
})
})

it('does not offer logs when source flags are disabled for a database query', async () => {
renderWithFlags({ _tag: 'database' }, { sqlEditorLogsSource: false, otelLegacyLogs: false })

await userEvent.click(screen.getByRole('button', { name: 'Query source: Database' }))

expect(screen.queryByText('Logs')).not.toBeInTheDocument()
})

it('keeps logs available when an existing query already uses it', async () => {
renderWithFlags(
{
_tag: 'logs',
time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' },
},
{ sqlEditorLogsSource: false, otelLegacyLogs: false }
)

await userEvent.click(screen.getByRole('button', { name: 'Query source: Logs' }))

expect(screen.getAllByText('Logs')).toHaveLength(2)
expect(screen.getByText('Database')).toBeInTheDocument()
})
})
Loading
Loading