mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
Merge branch 'master' into chore/text-component
This commit is contained in:
commit
381fbc051b
280 files changed
+2191
-4283
No files matched your search
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
env:
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Comment on external PRs with linter results
|
||||
name: docs_lint_comment_external
|
||||
|
||||
# This is a continuation of ./docs-lint-v2.yml, to write comments on external
|
||||
# PRs.
|
||||
@@ -11,7 +11,7 @@ name: Comment on external PRs with linter results
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['[Docs] Lint v2']
|
||||
workflows: [docs_lint]
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
jobs:
|
||||
comment_on_pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
|
||||
steps:
|
||||
- id: download_artifact
|
||||
name: 'Download artifact'
|
||||
|
||||
@@ -45,18 +45,15 @@ jobs:
|
||||
git config --global user.email 'github-docs-bot@supabase.com'
|
||||
BRANCH_NAME="bot/docs-lint-fixes"
|
||||
EXISTING_BRANCH=$(git ls-remote --heads origin $BRANCH_NAME)
|
||||
if [ -n "$EXISTING_BRANCH" ]; then
|
||||
git checkout $BRANCH_NAME
|
||||
else
|
||||
git checkout -b $BRANCH_NAME
|
||||
if [[ -n "$EXISTING_BRANCH" ]]; then
|
||||
git push origin --delete $BRANCH_NAME
|
||||
fi
|
||||
git checkout -b $BRANCH_NAME
|
||||
supa-mdx-lint apps/docs/content --fix || FIX_FAILED=1
|
||||
git add .
|
||||
git commit -m '[bot] fix lint errors' || true
|
||||
git push origin $BRANCH_NAME
|
||||
if [ -z "$EXISTING_BRANCH" ]; then
|
||||
gh pr create --title '[bot] fix lint errors' --body 'This PR fixes lint errors in the documentation.' --head $BRANCH_NAME
|
||||
fi
|
||||
gh pr create --title '[bot] fix lint errors' --body 'This PR fixes lint errors in the documentation.' --head $BRANCH_NAME
|
||||
if [ "${FIX_FAILED:-0}" -eq 1 ]; then
|
||||
echo "Fix did not correct all errors."
|
||||
exit 1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: '[Docs] Lint v2'
|
||||
name: docs_lint
|
||||
|
||||
# Runs the docs linter on PRs that edit docs content.
|
||||
# There are two branches of this workflow for internal and external PRs, due
|
||||
@@ -102,3 +102,6 @@ jobs:
|
||||
with:
|
||||
name: lint_results
|
||||
path: __github_actions__pr/
|
||||
- name: fail if linter fails (external)
|
||||
if: steps.filter.outputs.docs == 'true' && github.event.pull_request.head.repo.full_name != github.repository && steps.external_lint.outputs.LINT_EXIT_CODE != 0
|
||||
run: exit 1
|
||||
@@ -17,9 +17,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
|
||||
SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
|
||||
SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
|
||||
DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
|
||||
DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
|
||||
DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
|
||||
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
|
||||
|
||||
@@ -29,12 +29,14 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
DOCS_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
|
||||
DOCS_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
|
||||
DOCS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
|
||||
NEXT_PUBLIC_MISC_URL: ${{ secrets.NEXT_PUBLIC_MISC_URL}}
|
||||
NEXT_PUBLIC_MISC_ANON_KEY: ${{ secrets.NEXT_PUBLIC_MISC_ANON_KEY }}
|
||||
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SEARCH_SUPABASE_URL }}
|
||||
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
SEARCH_GITHUB_APP_ID: ${{ secrets.SEARCH_GITHUB_APP_ID }}
|
||||
SEARCH_GITHUB_APP_INSTALLATION_ID: ${{ secrets.SEARCH_GITHUB_APP_INSTALLATION_ID }}
|
||||
SEARCH_GITHUB_APP_PRIVATE_KEY: ${{ secrets.SEARCH_GITHUB_APP_PRIVATE_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
SUPABASE_SECRET_KEY: ${{ secrets.SEARCH_SUPABASE_SERVICE_ROLE_KEY }}
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
@@ -58,10 +60,6 @@ jobs:
|
||||
- name: Download dependencies
|
||||
run: pnpm i
|
||||
|
||||
# Need the miscellaneous use API, which is available publicly (by design) in www
|
||||
- name: Copy environment variables
|
||||
run: cp apps/www/.env.local.example apps/docs/.env
|
||||
|
||||
- name: Update embeddings
|
||||
working-directory: ./apps/docs
|
||||
if: ${{ !inputs.refresh }}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const Index: Record<string, any> = {
|
||||
name: "ConfirmationModal",
|
||||
type: "components:fragment",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/Dialogs/ConfirmationModal")),
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/src/Dialogs/ConfirmationModal")),
|
||||
source: "",
|
||||
files: ["registry/default//Dialogs/ConfirmationModal.tsx"],
|
||||
category: "undefined",
|
||||
@@ -20,7 +20,7 @@ export const Index: Record<string, any> = {
|
||||
name: "TextConfirmModal",
|
||||
type: "components:fragment",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/Dialogs/TextConfirmModal")),
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/src/Dialogs/TextConfirmModal")),
|
||||
source: "",
|
||||
files: ["registry/default//Dialogs/TextConfirmModal.tsx"],
|
||||
category: "undefined",
|
||||
@@ -31,7 +31,7 @@ export const Index: Record<string, any> = {
|
||||
name: "ConfirmDialog",
|
||||
type: "components:fragment",
|
||||
registryDependencies: undefined,
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/Dialogs/ConfirmDialog")),
|
||||
component: React.lazy(() => import("@/../../packages/ui-patterns/src/Dialogs/ConfirmDialog")),
|
||||
source: "",
|
||||
files: ["registry/default//Dialogs/ConfirmDialog.tsx"],
|
||||
category: "undefined",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"@types/lodash.template": "4.5.0",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"autoprefixer": "^10.0.1",
|
||||
"config": "workspace:^",
|
||||
"mdast-util-toc": "^6.1.1",
|
||||
"postcss": "^8.5.3",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -211,7 +211,7 @@ export const Index: Record<string, any> = {
|
||||
packagePath = `../../packages/ui/src/components/shadcn/ui`
|
||||
}
|
||||
if (type === 'fragment') {
|
||||
packagePath = `../../packages/ui-patterns${item.optionalPath}`
|
||||
packagePath = `../../packages/ui-patterns/src${item.optionalPath}`
|
||||
}
|
||||
if (type === 'example') {
|
||||
packagePath = `registry/${style.name}/${type}`
|
||||
|
||||
@@ -8,8 +8,7 @@ module.exports = config({
|
||||
// purge styles from grid library
|
||||
//
|
||||
'./../../packages/ui/src/**/*.{tsx,ts,js}',
|
||||
'./../../packages/ui-patterns/**/*.{tsx,ts,js}',
|
||||
'!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule
|
||||
'./../../packages/ui-patterns/src/**/*.{tsx,ts,js}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -2,5 +2,17 @@
|
||||
# Only publicly available environment variables should be listed here.
|
||||
# Do not add secrets here. Use .env.local instead.
|
||||
|
||||
NEXT_PUBLIC_SITE_URL="http://localhost:3001"
|
||||
NEXT_PUBLIC_BASE_PATH="/docs"
|
||||
|
||||
# Whether to enable features available only on Supabase's own hosted services
|
||||
# Setting this to true requires certain secret keys
|
||||
NEXT_PUBLIC_IS_PLATFORM="false"
|
||||
|
||||
# Supabase project containing integration information
|
||||
NEXT_PUBLIC_MISC_URL="https://obuldanrptloktxcffvn.supabase.co"
|
||||
NEXT_PUBLIC_MISC_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9idWxkYW5ycHRsb2t0eGNmZnZuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTg2MTQ2ODUsImV4cCI6MjAzNDE5MDY4NX0.NFt49g6DFkc1X5khCzN5p01iAVo2TMxlx88cY1V0E2M"
|
||||
|
||||
# Supabase project containing docs content information
|
||||
NEXT_PUBLIC_SUPABASE_URL="https://xguihxuzqibwxjnimxev.supabase.co"
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhndWloeHV6cWlid3hqbmlteGV2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTgzNzc1MTgsImV4cCI6MjAzMzk1MzUxOH0.aIqjQ9V7djMxYit-DT1fYNV_VWMHSqldh_18XfX2_BE"
|
||||
@@ -1,14 +0,0 @@
|
||||
# PUBLIC
|
||||
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
|
||||
# PRIVATE
|
||||
OPENAI_API_KEY=
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
SEARCH_GITHUB_APP_ID=
|
||||
SEARCH_GITHUB_APP_INSTALLATION_ID=
|
||||
SEARCH_GITHUB_APP_PRIVATE_KEY=
|
||||
|
||||
# Set IS_PLATFORM to true if you have the api keys and urls
|
||||
NEXT_PUBLIC_IS_PLATFORM=false
|
||||
NEXT_PUBLIC_LOCAL_SUPABASE=false
|
||||
File renamed without changes.
@@ -131,6 +131,9 @@ type RootQueryType {
|
||||
|
||||
"""Search the Supabase docs for content matching a query string"""
|
||||
searchDocs(query: String!, limit: Int): SearchResultCollection
|
||||
|
||||
"""Get the details of an error code returned from a Supabase service"""
|
||||
error(code: String!, service: Service!): Error
|
||||
}
|
||||
|
||||
"""A collection of search results containing content from Supabase docs"""
|
||||
@@ -149,5 +152,30 @@ type SearchResultCollection {
|
||||
type SearchResultEdge {
|
||||
"""The SearchResult at the end of the edge"""
|
||||
node: SearchResult!
|
||||
}
|
||||
|
||||
"""An error returned by a Supabase service"""
|
||||
type Error {
|
||||
"""
|
||||
The unique code identifying the error. The code is stable, and can be used for string matching during error handling.
|
||||
"""
|
||||
code: String!
|
||||
|
||||
"""The Supabase service that returns this error."""
|
||||
service: Service!
|
||||
|
||||
"""The HTTP status code returned with this error."""
|
||||
httpStatusCode: Int
|
||||
|
||||
"""
|
||||
A human-readable message describing the error. The message is not stable, and should not be used for string matching during error handling. Use the code instead.
|
||||
"""
|
||||
message: String
|
||||
}
|
||||
|
||||
enum Service {
|
||||
AUTH
|
||||
REALTIME
|
||||
STORAGE
|
||||
}"
|
||||
`;
|
||||
@@ -3,15 +3,11 @@ import { POST } from './route'
|
||||
|
||||
describe('/api/graphql basic error statuses', () => {
|
||||
beforeAll(() => {
|
||||
vi.mock('server-only', () => {
|
||||
return {}
|
||||
})
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.doUnmock('server-only')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { POST } from '../route'
|
||||
|
||||
describe('/api/graphql error', () => {
|
||||
it('returns the error matching given error code and service', async () => {
|
||||
const errorQuery = `
|
||||
query {
|
||||
error(code: "test_code", service: AUTH) {
|
||||
code
|
||||
service
|
||||
httpStatusCode
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
const request = new Request('http://localhost/api/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query: errorQuery }),
|
||||
})
|
||||
|
||||
const result = await POST(request)
|
||||
const {
|
||||
data: { error },
|
||||
errors,
|
||||
} = await result.json()
|
||||
|
||||
expect(errors).toBeUndefined()
|
||||
expect(error.code).toBe('test_code')
|
||||
expect(error.service).toBe('AUTH')
|
||||
expect(error.httpStatusCode).toBe(500)
|
||||
expect(error.message).toBe('This is a test error message')
|
||||
})
|
||||
|
||||
it('returns error if no matching error exists', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const errorQuery = `
|
||||
query {
|
||||
error(code: "nonexistent_code", service: AUTH) {
|
||||
code
|
||||
service
|
||||
httpStatusCode
|
||||
message
|
||||
}
|
||||
}
|
||||
`
|
||||
const request = new Request('http://localhost/api/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query: errorQuery }),
|
||||
})
|
||||
|
||||
const result = await POST(request)
|
||||
const {
|
||||
data: { error },
|
||||
errors,
|
||||
} = await result.json()
|
||||
|
||||
expect(error).toBe(null)
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].message).toMatch(/not found/i)
|
||||
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
})
|
||||
@@ -45,6 +45,24 @@ export class InvalidRequestError<Details extends ObjectOrNever = never> extends
|
||||
}
|
||||
}
|
||||
|
||||
export class NoDataError<Details extends ObjectOrNever = never> extends ApiError<Details> {
|
||||
constructor(message: string, source?: unknown, details?: Details) {
|
||||
super(`Data not found: ${message}`, source, details)
|
||||
}
|
||||
|
||||
isPrivate() {
|
||||
return false
|
||||
}
|
||||
|
||||
isUserError() {
|
||||
return true
|
||||
}
|
||||
|
||||
statusCode() {
|
||||
return 404
|
||||
}
|
||||
}
|
||||
|
||||
export function convertUnknownToApiError(error: unknown): ApiError {
|
||||
return new ApiError('Unknown error', error)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -135,4 +135,4 @@ const generateStaticParams = () => pageMap.map(({ slug }) => ({ slug }))
|
||||
const generateMetadata = genGuideMeta(getContent)
|
||||
|
||||
export default PythonClientDocs
|
||||
export { generateStaticParams, generateMetadata }
|
||||
export { generateMetadata, generateStaticParams }
|
||||
@@ -1,19 +1,18 @@
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { capitalize } from 'lodash'
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { Heading } from 'ui'
|
||||
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { Tabs, TabPanel } from '~/features/ui/Tabs'
|
||||
import { TabPanel, Tabs } from '~/features/ui/Tabs'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import matter from 'gray-matter'
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join, relative } from 'node:path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import emoji from 'remark-emoji'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import {
|
||||
genGuideMeta,
|
||||
@@ -17,6 +17,7 @@ import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { octokit } from '~/lib/octokit'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { relative } from 'node:path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -133,4 +133,4 @@ const generateStaticParams = () => pageMap.map(({ slug }) => ({ slug }))
|
||||
const generateMetadata = genGuideMeta(getContent)
|
||||
|
||||
export default ActionDocs
|
||||
export { generateStaticParams, generateMetadata }
|
||||
export { generateMetadata, generateStaticParams }
|
||||
@@ -1,10 +1,9 @@
|
||||
import matter from 'gray-matter'
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { notFound } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, removeRedundantH1 } from '~/features/docs/GuidesMdx.utils'
|
||||
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
|
||||
import { isValidGuideFrontmatter } from '~/lib/docs'
|
||||
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
terraformDocsOrg,
|
||||
terraformDocsRepo,
|
||||
} from '../terraformConstants'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
@@ -146,4 +146,4 @@ const generateStaticParams = async () => pageMap.map(({ slug }) => ({ slug: slug
|
||||
const generateMetadata = genGuideMeta(getContent)
|
||||
|
||||
export default TerraformDocs
|
||||
export { generateStaticParams, generateMetadata }
|
||||
export { generateMetadata, generateStaticParams }
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { isAbsolute, relative } from 'path'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { REVALIDATION_TAGS } from '~/features/helpers.fetch'
|
||||
@@ -9,6 +9,7 @@ import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLin
|
||||
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
|
||||
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
|
||||
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pick } from 'lodash'
|
||||
import { pick } from 'lodash-es'
|
||||
|
||||
/**
|
||||
* Gets the Notion team to send feedback to based on the pathname.
|
||||
|
||||
@@ -119,7 +119,7 @@ export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
|
||||
level: 'platform',
|
||||
},
|
||||
{
|
||||
label: 'Security',
|
||||
label: 'Security & Compliance',
|
||||
icon: 'security',
|
||||
href: '/guides/security',
|
||||
level: 'security',
|
||||
@@ -2092,6 +2092,19 @@ export const security: NavMenuConstant = {
|
||||
{ name: 'HIPAA', url: '/guides/security/hipaa-compliance' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Guides',
|
||||
url: undefined,
|
||||
items: [
|
||||
{ name: 'Production Checklist', url: '/guides/deployment/going-into-prod' },
|
||||
{
|
||||
name: 'Shared Responsibility Model',
|
||||
url: '/guides/deployment/shared-responsibility-model',
|
||||
},
|
||||
{ name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
|
||||
{ name: 'Hardening the Data API', url: '/guides/database/hardening-data-api' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { noop } from 'lodash-es'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { noop } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button_Shadcn_ as Button,
|
||||
Popover_Shadcn_ as Popover,
|
||||
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
||||
PopoverContent_Shadcn_ as PopoverContent,
|
||||
Command_Shadcn_ as Command,
|
||||
CommandInput_Shadcn_ as CommandInput,
|
||||
CommandEmpty_Shadcn_ as CommandEmpty,
|
||||
CommandItem_Shadcn_ as CommandItem,
|
||||
CommandGroup_Shadcn_ as CommandGroup,
|
||||
CommandList_Shadcn_ as CommandList,
|
||||
cn,
|
||||
Command_Shadcn_ as Command,
|
||||
CommandEmpty_Shadcn_ as CommandEmpty,
|
||||
CommandGroup_Shadcn_ as CommandGroup,
|
||||
CommandInput_Shadcn_ as CommandInput,
|
||||
CommandItem_Shadcn_ as CommandItem,
|
||||
CommandList_Shadcn_ as CommandList,
|
||||
Popover_Shadcn_ as Popover,
|
||||
PopoverContent_Shadcn_ as PopoverContent,
|
||||
PopoverTrigger_Shadcn_ as PopoverTrigger,
|
||||
ScrollArea,
|
||||
} from 'ui'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { at } from 'lodash'
|
||||
import { at } from 'lodash-es'
|
||||
import { ReactNode } from 'react'
|
||||
import { config, logConstants } from 'shared-data'
|
||||
|
||||
|
||||
@@ -141,6 +141,84 @@ To use `MergeDeep`, set `compilerOptions.strictNullChecks` to `true` in your `ts
|
||||
|
||||
</Admonition>
|
||||
|
||||
## Enhanced type inference for JSON fields
|
||||
|
||||
Starting from [supabase-js v2.48.0](https://github.com/supabase/supabase-js/releases/tag/v2.48.0), you can define custom types for JSON fields and get enhanced type inference when using JSON selectors with the `->` and `->>` operators. This makes your code more type-safe and intuitive when working with JSON/JSONB columns.
|
||||
|
||||
### Defining custom JSON types
|
||||
|
||||
You can extend your generated database types to include custom JSON schemas using `MergeDeep`:
|
||||
|
||||
```ts ./database.types.ts
|
||||
import { MergeDeep } from 'type-fest'
|
||||
import { Database as DatabaseGenerated } from './database-generated.types'
|
||||
|
||||
// Define your custom JSON type
|
||||
type CustomJsonType = {
|
||||
foo: string
|
||||
bar: { baz: number }
|
||||
en: 'ONE' | 'TWO' | 'THREE'
|
||||
}
|
||||
|
||||
export type Database = MergeDeep<
|
||||
DatabaseGenerated,
|
||||
{
|
||||
public: {
|
||||
Tables: {
|
||||
your_table: {
|
||||
Row: {
|
||||
data: CustomJsonType | null
|
||||
}
|
||||
// Optional: Use if you want type-checking for inserts and updates
|
||||
// Insert: {
|
||||
// data?: CustomJsonType | null;
|
||||
// };
|
||||
// Update: {
|
||||
// data?: CustomJsonType | null;
|
||||
// };
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
your_view: {
|
||||
Row: {
|
||||
data: CustomJsonType | null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
```
|
||||
|
||||
### Type-safe JSON querying
|
||||
|
||||
Once you've defined your custom JSON types, TypeScript will automatically infer the correct types when using JSON selectors:
|
||||
|
||||
```ts
|
||||
const res = await client.from('your_table').select('data->bar->baz, data->en, data->bar')
|
||||
|
||||
if (res.data) {
|
||||
console.log(res.data)
|
||||
// TypeScript infers the shape of your JSON data:
|
||||
// [
|
||||
// {
|
||||
// baz: number;
|
||||
// en: 'ONE' | 'TWO' | 'THREE';
|
||||
// bar: { baz: number };
|
||||
// }
|
||||
// ]
|
||||
}
|
||||
```
|
||||
|
||||
This feature works with:
|
||||
|
||||
- Single-level JSON access: `data->foo`
|
||||
- Nested JSON access: `data->bar->baz`
|
||||
- Text extraction: `data->>foo` (returns string)
|
||||
- Mixed selections combining multiple JSON paths
|
||||
|
||||
The type inference automatically handles the difference between `->` (returns JSON) and `->>` (returns text) operators, ensuring your TypeScript types match the actual runtime behavior.
|
||||
|
||||
You can also override the type of an individual successful response if needed:
|
||||
|
||||
```ts
|
||||
|
||||
@@ -10,8 +10,9 @@ Claims returned must conform to our specification. Supabase Auth will check for
|
||||
|
||||
These are the fields currently available on an access token:
|
||||
|
||||
Required Claims: `iss`, `aud`, `exp`, `iat`, `sub`, `role`, `aal`, `session_id`
|
||||
Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`, `email`, `phone`
|
||||
Required Claims: `iss`, `aud`, `exp`, `iat`, `sub`, `role`, `aal`, `session_id`, `email`, `phone`, `is_anonymous`
|
||||
|
||||
Optional Claims: `jti`, `nbf`, `app_metadata`, `user_metadata`, `amr`,
|
||||
|
||||
**Inputs**
|
||||
|
||||
@@ -241,7 +242,10 @@ as $$
|
||||
'sub',
|
||||
'role',
|
||||
'aal',
|
||||
'session_id'
|
||||
'session_id',
|
||||
'email',
|
||||
'phone',
|
||||
'is_anonymous'
|
||||
] loop
|
||||
if original_claims ? claim then
|
||||
-- original_claims contains one of the listed claims, set it on new_claims
|
||||
|
||||
@@ -182,7 +182,7 @@ val metadata = user?.userMetadata
|
||||
|
||||
## Deleting users
|
||||
|
||||
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 consider making use of a Row Level Security policy as described below.
|
||||
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.
|
||||
|
||||
<Admonition type="caution">
|
||||
|
||||
|
||||
@@ -2,7 +2,24 @@
|
||||
title: 'Hardening the Data API'
|
||||
---
|
||||
|
||||
Your database's automatically generated Data API exposes the `public` schema by default. If your `public` schema is used by other tools as a default space, you might want to lock down this schema. This helps prevent accidental exposure of data that's automatically added to `public`.
|
||||
Your database's auto-generated Data API exposes the `public` schema by default. You can change this to any schema in your database, or even disable the Data API completely.
|
||||
|
||||
Any tables that are accessible through the Data API _must_ have [Row Level Security](/docs/guides/database/postgres/row-level-security) enabled. Row Level Security (RLS) is enabled by default when you create tables from the Supabase Dashboard. If you create a table using the SQL editor or your own SQL client or migration runner, you*must* enable RLS yourself.
|
||||
|
||||
## Shared responsibility
|
||||
|
||||
Your application's security is your responsibility as a developer. This includes RLS, falling under the [Shared Responsibility](/docs/guides/deployment/shared-responsibility-model) model. To help you:
|
||||
|
||||
- Supabase sends daily emails warning of any tables that are exposed to the Data API which do not have RLS enabled.
|
||||
- Supabase provides a Security Advisor and other tools in the Supabase Dashboard to fix any issues.
|
||||
|
||||
## Private schemas
|
||||
|
||||
We highly recommend creating a `private` schema for storing tables that you do not want to expose via the Data API. These tables can be accessed via Supabase Edge Functions or any other serverside tool. In this model, you should implement your security model in your serverside code. Although it's not required, we _still_ recommend enabling RLS for private tables and then connecting to your database using a Postgres role with `bypassrls` privileges.
|
||||
|
||||
## Managing the public schema
|
||||
|
||||
If your `public` schema is used by other tools as a default space, you might want to lock down this schema. This helps prevent accidental exposure of data that's automatically added to `public`.
|
||||
|
||||
There are two levels of security hardening for the Data API:
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ sidebar_label: 'Setting up replication and CDC'
|
||||
To set up replication, the following is recommended:
|
||||
|
||||
- Instance size of XL or greater
|
||||
- [IPv4 add-on](/guides/platform/ipv4-address) enabled
|
||||
- [IPv4 add-on](/docs/guides/platform/ipv4-address) enabled
|
||||
|
||||
To create a replication slot, you will need to use the `postgres` user and follow the instructions in our [guide](/guides/database/postgres/setup-replication-external).
|
||||
To create a replication slot, you will need to use the `postgres` user and follow the instructions in our [guide](/docs/guides/database/postgres/setup-replication-external).
|
||||
|
||||
<Admonition type="note">
|
||||
If you are running Postgres 17 or higher, you can create a new user and grant them replication
|
||||
|
||||
@@ -19,6 +19,11 @@ Deno.serve(async (req: Request) => {
|
||||
const supabaseClient = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
||||
{
|
||||
global: {
|
||||
headers: { Authorization: req.headers.get('Authorization') },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Get the session or user object
|
||||
@@ -43,6 +48,11 @@ Deno.serve(async (req: Request) => {
|
||||
const supabaseClient = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
||||
{
|
||||
global: {
|
||||
headers: { Authorization: req.headers.get('Authorization') },
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Get the session or user object
|
||||
@@ -71,6 +81,11 @@ Deno.serve(async (req: Request) => {
|
||||
const supabaseClient = createClient(
|
||||
Deno.env.get('SUPABASE_URL') ?? '',
|
||||
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
|
||||
{
|
||||
global: {
|
||||
headers: { Authorization: req.headers.get('Authorization') },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Get the session or user object
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ Set the secrets from the `.env` file:
|
||||
supabase secrets set --env-file supabase/functions/.env
|
||||
```
|
||||
|
||||
Now your Supabase Edge Function will be triggered anytime an Auth Email needs to be send to the user!
|
||||
Now your Supabase Edge Function will be triggered anytime an Auth Email needs to be sent to the user!
|
||||
|
||||
## More resources
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Data sent to the client when retrieving data stored in your database.
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
There are various ways to interact with your database, such as through the PostgREST API using one of the client SDKs or via the Supavisor connection pooler. On the Supabase Dashboard, Egress from the PostgREST API is labeled as **Database Egress**, while Egress through Supavisor is labeled as **Supavisor Egress**.
|
||||
There are various ways to interact with your database, such as through the PostgREST API using one of the client SDKs or via the Supavisor connection pooler. On the Supabase Dashboard, Egress from the PostgREST API is labeled as **Database Egress**, while Egress through Supavisor is labeled as **Shared Pooler Egress**.
|
||||
|
||||
</Admonition>
|
||||
|
||||
@@ -43,6 +43,12 @@ Data pushed to clients via Supabase Realtime for subscribed events.
|
||||
|
||||
**Example:** When a user views a product page in an online shop, their client subscribes to real-time inventory updates. As stock levels change, Supabase Realtime pushes updates to all subscribed clients, contributing to Realtime Egress.
|
||||
|
||||
### Shared pooler Egress
|
||||
|
||||
Data sent to the client when using the shared connection pooler (Supavisor) to access your database. WHen using the shared connection pooler, we do not count database egress, as this would otherwise count double (Database -> Shared Pooler + Shared Pooler -> Client).
|
||||
|
||||
**Example:** You are using our [shared connection pooler](https://supabase.com/docs/guides/database/connecting-to-postgres#shared-pooler) and you query a list of invoices in your backend. The data returned from that query is contributing to Shared Pooler Egress.
|
||||
|
||||
### Log Drain Egress
|
||||
|
||||
Data pushed to the connected log drain.
|
||||
|
||||
@@ -76,11 +76,15 @@ Go to your Supabase project's [API Settings](https://supabase.com/dashboard/proj
|
||||
<TabPanel id="python" label="Python">
|
||||
|
||||
```python
|
||||
from supabase import create_client
|
||||
import asyncio
|
||||
from supabase import acreate_client
|
||||
|
||||
URL = "https://<project>.supabase.co"
|
||||
KEY = "<your-anon-key>"
|
||||
supabase = create_client(URL, KEY)
|
||||
|
||||
async def create_supabase():
|
||||
supabase = await acreate_client(URL, KEY)
|
||||
return supabase
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -179,6 +183,11 @@ You can provide a callback for the `broadcast` channel to receive message. This
|
||||
|
||||
</TabPanel>
|
||||
<TabPanel id="python" label="Python">
|
||||
<Admonition type="note">
|
||||
|
||||
In the following Realtime examples, certain methods are awaited. These should be enclosed within an `async` function.
|
||||
|
||||
</Admonition>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```python
|
||||
@@ -190,7 +199,7 @@ You can provide a callback for the `broadcast` channel to receive message. This
|
||||
print(f"Broadcast received: {payload}")
|
||||
|
||||
# Subscribe to the Channel
|
||||
my_channel
|
||||
await my_channel
|
||||
.on_broadcast('shout', message_received) # Listen for "shout". Can be "*" to listen to all events
|
||||
.subscribe()
|
||||
```
|
||||
@@ -323,6 +332,12 @@ You can use the Supabase client libraries to send Broadcast messages.
|
||||
|
||||
<TabPanel id="python" label="Python">
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
{/* prettier-ignore */}
|
||||
```python
|
||||
my_channel = supabase.channel('test-channel')
|
||||
@@ -332,12 +347,12 @@ You can use the Supabase client libraries to send Broadcast messages.
|
||||
if status != RealtimeSubscribeStates.SUBSCRIBED:
|
||||
return
|
||||
|
||||
my_channel.send_broadcast(
|
||||
asyncio.create_task(my_channel.send_broadcast(
|
||||
'shout',
|
||||
{ "message": 'hello, world' },
|
||||
)
|
||||
))
|
||||
|
||||
my_channel.subscribe(on_subscribe)
|
||||
await my_channel.subscribe(on_subscribe)
|
||||
```
|
||||
|
||||
</TabPanel>
|
||||
@@ -543,6 +558,12 @@ You can pass configuration options while initializing the Supabase Client.
|
||||
</TabPanel>
|
||||
<TabPanel id="python" label="Python">
|
||||
|
||||
<Admonition type="note">
|
||||
|
||||
When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the [initialize the client](#initialize-the-client) example includes an import of `asyncio`.
|
||||
|
||||
</Admonition>
|
||||
|
||||
By default, broadcast messages are only sent to other clients. You can broadcast messages back to the sender by setting Broadcast's `self` parameter to `True`.
|
||||
|
||||
```python
|
||||
@@ -559,10 +580,10 @@ You can pass configuration options while initializing the Supabase Client.
|
||||
return
|
||||
|
||||
# Send a message once the client is subscribed
|
||||
channel_b.send_broadcast(
|
||||
asyncio.create_task(channel_b.send_broadcast(
|
||||
'test-my-messages',
|
||||
{ "message": 'talking to myself' },
|
||||
)
|
||||
))
|
||||
|
||||
my_channel.subscribe(on_subscribe)
|
||||
```
|
||||
|
||||
@@ -354,7 +354,7 @@ services:
|
||||
|
||||
```bash name=.env
|
||||
## Never check your secrets into version control
|
||||
`${JWT_SECRET}`
|
||||
JWT_SECRET=${JWT_SECRET}
|
||||
```
|
||||
|
||||
</$CodeTabs>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { type ReactNode } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@@ -11,6 +10,7 @@ import { TocAnchorsProvider } from '~/features/docs/GuidesMdx.client'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import type { WithRequired } from '~/features/helpers.types'
|
||||
import { type GuideFrontmatter } from '~/lib/docs'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
const EDIT_LINK_SYMBOL = Symbol('edit link')
|
||||
interface EditLink {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||
import { type ComponentProps } from 'react'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import { preprocessMdxWithDefaults } from '~/features/directives/utils'
|
||||
import { components } from '~/features/docs/MdxBase.shared'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
const mdxOptions: SerializeOptions = {
|
||||
mdxOptions: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isPlainObject, keyBy } from 'lodash'
|
||||
import { isPlainObject, keyBy } from 'lodash-es'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as Collapsible from '@radix-ui/react-collapsible'
|
||||
|
||||
import { debounce } from 'lodash'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { ChevronUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEqual } from 'lodash'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import { ChevronRight, XCircle } from 'lucide-react'
|
||||
import type { HTMLAttributes, PropsWithChildren } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* Discussions.
|
||||
*/
|
||||
|
||||
import _configureDotEnv from '../../scripts/utils/dotenv'
|
||||
|
||||
import { createAppAuth } from '@octokit/auth-app'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
@@ -20,13 +22,12 @@ import { gfm } from 'micromark-extension-gfm'
|
||||
import { mdxjs } from 'micromark-extension-mdxjs'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { parse, stringify } from 'smol-toml'
|
||||
|
||||
import {
|
||||
getAllTroubleshootingEntriesInternal as getAllTroubleshootingEntries,
|
||||
getArticleSlug,
|
||||
} from './Troubleshooting.utils.common.mjs'
|
||||
|
||||
import 'dotenv/config'
|
||||
const _ = _configureDotEnv
|
||||
|
||||
const REPOSITORY_ID = 'MDEwOlJlcG9zaXRvcnkyMTQ1ODcxOTM='
|
||||
const TROUBLESHOOTING_CATEGORY_ID = 'DIC_kwDODMpXOc4CUvEr'
|
||||
@@ -52,9 +53,9 @@ function octokit() {
|
||||
octokitInstance = new Octokit({
|
||||
authStrategy: createAppAuth,
|
||||
auth: {
|
||||
appId: process.env.SEARCH_GITHUB_APP_ID,
|
||||
installationId: process.env.SEARCH_GITHUB_APP_INSTALLATION_ID,
|
||||
privateKey: process.env.SEARCH_GITHUB_APP_PRIVATE_KEY,
|
||||
appId: process.env.DOCS_GITHUB_APP_ID,
|
||||
installationId: process.env.DOCS_GITHUB_APP_INSTALLATION_ID,
|
||||
privateKey: process.env.DOCS_GITHUB_APP_PRIVATE_KEY,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as fs from 'fs'
|
||||
import { mapValues, values } from 'lodash'
|
||||
import { mapValues, values } from 'lodash-es'
|
||||
|
||||
export const slugify = (text: string) => {
|
||||
if (!text) return ''
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
* tsx ReferenceGenerator.ts -o docs/client spec/supabase.yml
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { flattenSections } from '../lib/helpers'
|
||||
import Example from './legacy/components/Example'
|
||||
import Page from './legacy/components/Page'
|
||||
import Tab from './legacy/components/Tab'
|
||||
import { OpenRef, TsDoc } from './legacy/definitions'
|
||||
import { slugify, tsDocCommentToMdComment, writeToDisk } from './legacy/lib/helpers'
|
||||
import { TsDoc, OpenRef } from './legacy/definitions'
|
||||
import { uniqBy } from 'lodash'
|
||||
import * as fs from 'fs'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { flattenSections } from '../lib/helpers'
|
||||
|
||||
const commonDocSpecJson = JSON.parse(
|
||||
fs.readFileSync('spec/common-client-libs-sections.json', 'utf8')
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import matter from 'gray-matter'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import type { SerializeOptions } from 'next-mdx-remote/dist/types'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir, readFile } from 'node:fs/promises'
|
||||
import { basename, extname, join, sep } from 'node:path'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import codeHikeTheme from 'config/code-hike.theme.json' with { type: 'json' }
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
|
||||
// MUST be process.cwd() here, not import.meta.url, or files that are added
|
||||
// with outputFileTracingIncludes (not auto-traced) will not be found at
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { TsDoc } from '../../generator/legacy/definitions'
|
||||
|
||||
import { values, mapValues } from 'lodash'
|
||||
import { mapValues, values } from 'lodash-es'
|
||||
import type { OpenAPIV3 } from 'openapi-types'
|
||||
import { flattenSections } from '../helpers'
|
||||
import type { ICommonItem } from '~/components/reference/Reference.types'
|
||||
import { flattenSections } from '../helpers'
|
||||
|
||||
export function extractTsDocNode(nodeToFind: string, definition: any) {
|
||||
const nodePath = nodeToFind.split('.')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = [
|
||||
export default [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'avatars.githubusercontent.com',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createClient, type SupabaseClient } from '@supabase/supabase-js'
|
||||
import { type Database as DatabaseGenerated } from 'common'
|
||||
|
||||
type Database = {
|
||||
content: DatabaseGenerated['content']
|
||||
graphql_public: DatabaseGenerated['graphql_public']
|
||||
public: {
|
||||
Tables: DatabaseGenerated['public']['Tables']
|
||||
|
||||
@@ -34,13 +34,6 @@ const nextConfig = {
|
||||
// @ts-ignore
|
||||
remotePatterns,
|
||||
},
|
||||
// TODO: @next/mdx ^13.0.2 only supports experimental mdxRs flag. next ^13.0.2 will stop warning about this being unsupported.
|
||||
// mdxRs: true,
|
||||
modularizeImports: {
|
||||
lodash: {
|
||||
transform: 'lodash/{{member}}',
|
||||
},
|
||||
},
|
||||
webpack: (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.include$/,
|
||||
|
||||
+23
-21
@@ -2,37 +2,38 @@
|
||||
"name": "docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"",
|
||||
"dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs",
|
||||
"dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs",
|
||||
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"build": "next build",
|
||||
"build:analyze": "ANALYZE=true next build",
|
||||
"build:sitemap": "tsx ./internals/generate-sitemap.ts",
|
||||
"build:llms": "tsx ./scripts/llms.ts",
|
||||
"postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
|
||||
"start": "next start",
|
||||
"build:sitemap": "tsx ./internals/generate-sitemap.ts",
|
||||
"clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__",
|
||||
"codegen:examples": "cp -r ../../examples ./examples",
|
||||
"codegen:graphql": "tsx --conditions=react-server ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts",
|
||||
"codegen:references": "tsx features/docs/Reference.generated.script.ts",
|
||||
"codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"",
|
||||
"dev": "concurrently --kill-others \"next dev --port 3001\" \"pnpm run dev:watch:troubleshooting\"",
|
||||
"dev:secrets:pull": "AWS_PROFILE=supabase-dev node ../../scripts/getSecrets.js -n local/docs",
|
||||
"dev:watch:troubleshooting": "node ./scripts/troubleshooting/watch.mjs",
|
||||
"embeddings": "tsx scripts/search/generate-embeddings.ts",
|
||||
"embeddings:refresh": "pnpm run embeddings --refresh",
|
||||
"last-changed": "tsx scripts/last-changed.ts",
|
||||
"last-changed:reset": "pnpm run last-changed -- --reset",
|
||||
"lint": "next lint",
|
||||
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
|
||||
"pretypecheck": "pnpm run codegen:graphql",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"postbuild": "pnpm run build:sitemap && pnpm run build:llms && ./../../scripts/upload-static-assets.sh",
|
||||
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"pretest": "pnpm run codegen:examples",
|
||||
"pretypecheck": "pnpm run codegen:graphql",
|
||||
"start": "next start",
|
||||
"test": "pnpm supabase start && pnpm run test:local && pnpm supabase stop",
|
||||
"test:local": "vitest --exclude \"**/*.smoke.test.ts\"",
|
||||
"test:smoke": "pnpm run codegen:references && vitest -t \"prod smoke test\"",
|
||||
"embeddings": "tsx scripts/search/generate-embeddings.ts",
|
||||
"embeddings:refresh": "pnpm run embeddings --refresh",
|
||||
"troubleshooting:sync": "node features/docs/Troubleshooting.script.mjs",
|
||||
"last-changed": "tsx scripts/last-changed.ts",
|
||||
"last-changed:reset": "pnpm run last-changed -- --reset",
|
||||
"codegen:examples": "cp -r ../../examples ./examples",
|
||||
"codemod:frontmatter": "node ./scripts/codemod/mdx-meta.mjs && prettier --write \"content/**/*.mdx\"",
|
||||
"codegen:graphql": "tsx --conditions=react-server ./scripts/graphqlSchema.ts && graphql-codegen --config codegen.ts",
|
||||
"codegen:references": "tsx features/docs/Reference.generated.script.ts",
|
||||
"clean": "rimraf .next .turbo node_modules features/docs/generated examples __generated__"
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@har-sdk/openapi-sampler": "^2.2.0",
|
||||
@@ -70,7 +71,7 @@
|
||||
"jsrsasign": "^11.0.0",
|
||||
"katex": "^0.16.21",
|
||||
"libpg-query": "15.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-react": "*",
|
||||
"mdast": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^1.2.0",
|
||||
@@ -125,6 +126,7 @@
|
||||
"@types/graphql-validation-complexity": "^0.4.4",
|
||||
"@types/hast": "^2.3.4",
|
||||
"@types/jsrsasign": "^10.5.12",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mdast": "^3.0.0",
|
||||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,74 @@
|
||||
import { ApiErrorGeneric, convertPostgrestToApiError, NoDataError } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { supabase } from '~/lib/supabase'
|
||||
|
||||
export const SERVICES = {
|
||||
AUTH: {
|
||||
value: 'AUTH',
|
||||
},
|
||||
REALTIME: {
|
||||
value: 'REALTIME',
|
||||
},
|
||||
STORAGE: {
|
||||
value: 'STORAGE',
|
||||
},
|
||||
} as const
|
||||
|
||||
type Service = keyof typeof SERVICES
|
||||
|
||||
export class ErrorModel {
|
||||
public code: string
|
||||
public service: Service
|
||||
public httpStatusCode?: number
|
||||
public message?: string
|
||||
|
||||
constructor({
|
||||
code,
|
||||
service,
|
||||
httpStatusCode: httpStatusCode,
|
||||
message,
|
||||
}: {
|
||||
code: string
|
||||
service: Service
|
||||
httpStatusCode?: number
|
||||
message?: string
|
||||
}) {
|
||||
this.code = code
|
||||
this.service = service
|
||||
this.httpStatusCode = httpStatusCode
|
||||
this.message = message
|
||||
}
|
||||
|
||||
static async loadSingleError({
|
||||
code,
|
||||
service,
|
||||
}: {
|
||||
code: string
|
||||
service: Service
|
||||
}): Promise<Result<ErrorModel, ApiErrorGeneric>> {
|
||||
return new Result(
|
||||
await supabase()
|
||||
.schema('content')
|
||||
.from('error')
|
||||
.select('code, ...service(service:name), httpStatusCode:http_status_code, message')
|
||||
.eq('code', code)
|
||||
.eq('service.name', service)
|
||||
.is('deleted_at', null)
|
||||
.single<{
|
||||
code: string
|
||||
service: Service
|
||||
httpStatusCode?: number
|
||||
message?: string
|
||||
}>()
|
||||
)
|
||||
.map((data) => {
|
||||
return new ErrorModel(data)
|
||||
})
|
||||
.mapError((error) => {
|
||||
if (error.code === 'PGRST116') {
|
||||
return new NoDataError('Error for given code and service does not exist', error)
|
||||
}
|
||||
return convertPostgrestToApiError(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { GraphQLError, GraphQLNonNull, GraphQLResolveInfo, GraphQLString } from 'graphql'
|
||||
import { type RootQueryTypeErrorArgs } from '~/__generated__/graphql'
|
||||
import { convertUnknownToApiError } from '~/app/api/utils'
|
||||
import { Result } from '~/features/helpers.fn'
|
||||
import { ErrorModel } from './errorModel'
|
||||
import {
|
||||
GRAPHQL_FIELD_ERROR_GLOBAL,
|
||||
GraphQLEnumTypeService,
|
||||
GraphQLObjectTypeError,
|
||||
} from './errorSchema'
|
||||
|
||||
async function resolveSingleError(
|
||||
_parent: unknown,
|
||||
args: RootQueryTypeErrorArgs,
|
||||
_context: unknown,
|
||||
_info: GraphQLResolveInfo
|
||||
): Promise<ErrorModel | GraphQLError> {
|
||||
return (
|
||||
await Result.tryCatchFlat(ErrorModel.loadSingleError, convertUnknownToApiError, args)
|
||||
).match(
|
||||
(data) => data,
|
||||
(error) => {
|
||||
console.error(`Error resolving ${GRAPHQL_FIELD_ERROR_GLOBAL}:`, error)
|
||||
return new GraphQLError(error.isPrivate() ? 'Internal Server Error' : error.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const errorRoot = {
|
||||
[GRAPHQL_FIELD_ERROR_GLOBAL]: {
|
||||
description: 'Get the details of an error code returned from a Supabase service',
|
||||
args: {
|
||||
code: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
},
|
||||
service: {
|
||||
type: new GraphQLNonNull(GraphQLEnumTypeService),
|
||||
},
|
||||
},
|
||||
type: GraphQLObjectTypeError,
|
||||
resolve: resolveSingleError,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInt,
|
||||
GraphQLNonNull,
|
||||
GraphQLObjectType,
|
||||
GraphQLString,
|
||||
} from 'graphql'
|
||||
import { SERVICES } from './errorModel'
|
||||
|
||||
export const GRAPHQL_FIELD_ERROR_GLOBAL = 'error' as const
|
||||
|
||||
export const GraphQLEnumTypeService = new GraphQLEnumType({
|
||||
name: 'Service',
|
||||
values: SERVICES,
|
||||
})
|
||||
|
||||
export const GraphQLObjectTypeError = new GraphQLObjectType({
|
||||
name: 'Error',
|
||||
description: 'An error returned by a Supabase service',
|
||||
fields: {
|
||||
code: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
description:
|
||||
'The unique code identifying the error. The code is stable, and can be used for string matching during error handling.',
|
||||
},
|
||||
service: {
|
||||
type: new GraphQLNonNull(GraphQLEnumTypeService),
|
||||
description: 'The Supabase service that returns this error.',
|
||||
},
|
||||
httpStatusCode: {
|
||||
type: GraphQLInt,
|
||||
description: 'The HTTP status code returned with this error.',
|
||||
},
|
||||
message: {
|
||||
type: GraphQLString,
|
||||
description:
|
||||
'A human-readable message describing the error. The message is not stable, and should not be used for string matching during error handling. Use the code instead.',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
printSchema,
|
||||
} from 'graphql'
|
||||
import { RootQueryTypeResolvers } from '~/__generated__/graphql'
|
||||
import { errorRoot } from './error/errorResolver'
|
||||
import { searchRoot } from './globalSearch/globalSearchResolver'
|
||||
import { GraphQLObjectTypeGuide } from './guide/guideSchema'
|
||||
import { GraphQLObjectTypeReferenceCLICommand } from './reference/referenceCLISchema'
|
||||
@@ -35,6 +36,7 @@ export const rootGraphQLSchema = new GraphQLSchema({
|
||||
fields: {
|
||||
...introspectRoot,
|
||||
...searchRoot,
|
||||
...errorRoot,
|
||||
},
|
||||
}),
|
||||
types: [
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { printSchema } from 'graphql'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { rootGraphQLSchema } from '../resources/rootSchema'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { rootGraphQLSchema } from '../resources/rootSchema.js'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function generateGraphQLSchema() {
|
||||
try {
|
||||
@@ -22,6 +25,6 @@ async function generateGraphQLSchema() {
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
generateGraphQLSchema()
|
||||
}
|
||||
@@ -11,18 +11,21 @@
|
||||
* the last Git commit date.
|
||||
*/
|
||||
|
||||
import 'dotenv/config'
|
||||
import _configureDotEnv from './utils/dotenv'
|
||||
|
||||
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
|
||||
import matter from 'gray-matter'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import path, { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { SimpleGit, simpleGit } from 'simple-git'
|
||||
import { parse } from 'smol-toml'
|
||||
import { type Section } from './helpers.mdx'
|
||||
import { Section } from './helpers.mdx.js'
|
||||
|
||||
const _ = _configureDotEnv
|
||||
|
||||
interface Options {
|
||||
reset: boolean
|
||||
@@ -46,9 +49,11 @@ type SectionWithChecksum = Omit<Section, 'heading'> &
|
||||
|
||||
const REQUIRED_ENV_VARS = {
|
||||
SUPABASE_URL: 'NEXT_PUBLIC_SUPABASE_URL',
|
||||
SERVICE_ROLE_KEY: 'SUPABASE_SERVICE_ROLE_KEY',
|
||||
SERVICE_ROLE_KEY: 'SUPABASE_SECRET_KEY',
|
||||
} as const
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function main() {
|
||||
console.log('Updating content timestamps....')
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _configureDotEnv from './utils/dotenv'
|
||||
|
||||
import 'dotenv/config'
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import {
|
||||
fetchCliLibReferenceSource,
|
||||
fetchCSharpLibReferenceSource,
|
||||
@@ -11,7 +13,9 @@ import {
|
||||
fetchPythonLibReferenceSource,
|
||||
fetchSwiftLibReferenceSource,
|
||||
type SearchSource,
|
||||
} from './search/sources'
|
||||
} from './search/sources/index.js'
|
||||
|
||||
const _ = _configureDotEnv
|
||||
|
||||
interface Source {
|
||||
title: string
|
||||
@@ -97,6 +101,6 @@ async function generateLlmsTxt() {
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
generateLlmsTxt()
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import _configureDotenv from '../utils/dotenv'
|
||||
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import 'dotenv/config'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { OpenAI } from 'openai'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import type { Json, Section } from '../helpers.mdx'
|
||||
import { fetchAllSources } from './sources'
|
||||
import type { Json, Section } from '../helpers.mdx.js'
|
||||
import { fetchAllSources } from './sources/index.js'
|
||||
|
||||
const _ = _configureDotenv
|
||||
|
||||
const args = parseArgs({
|
||||
options: {
|
||||
@@ -18,19 +21,20 @@ async function generateEmbeddings() {
|
||||
const shouldRefresh = Boolean(args.values.refresh)
|
||||
|
||||
const requiredEnvVars = [
|
||||
'DOCS_GITHUB_APP_ID',
|
||||
'DOCS_GITHUB_APP_INSTALLATION_ID',
|
||||
'DOCS_GITHUB_APP_PRIVATE_KEY',
|
||||
'NEXT_PUBLIC_MISC_ANON_KEY',
|
||||
'NEXT_PUBLIC_MISC_URL',
|
||||
'NEXT_PUBLIC_SUPABASE_URL',
|
||||
'SUPABASE_SERVICE_ROLE_KEY',
|
||||
'OPENAI_API_KEY',
|
||||
'NEXT_PUBLIC_MISC_USE_URL',
|
||||
'NEXT_PUBLIC_MISC_USE_ANON_KEY',
|
||||
'SEARCH_GITHUB_APP_ID',
|
||||
'SEARCH_GITHUB_APP_INSTALLATION_ID',
|
||||
'SEARCH_GITHUB_APP_PRIVATE_KEY',
|
||||
'SUPABASE_SECRET_KEY',
|
||||
]
|
||||
|
||||
if (requiredEnvVars.some((name) => !process.env[name])) {
|
||||
const missingEnvVars = requiredEnvVars.filter((name) => !process.env[name])
|
||||
if (missingEnvVars.length > 0) {
|
||||
throw new Error(
|
||||
`Environment variables ${requiredEnvVars.join(
|
||||
`Environment variables ${missingEnvVars.join(
|
||||
', '
|
||||
)} are required: skipping embeddings generation`
|
||||
)
|
||||
@@ -38,8 +42,7 @@ async function generateEmbeddings() {
|
||||
|
||||
const supabaseClient = createClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
process.env.SUPABASE_SERVICE_ROLE_KEY,
|
||||
process.env.SUPABASE_SECRET_KEY,
|
||||
{
|
||||
auth: {
|
||||
persistSession: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Json, Section } from '../../helpers.mdx'
|
||||
import type { Json, Section } from '../../helpers.mdx.js'
|
||||
|
||||
export abstract class BaseLoader {
|
||||
type: string
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createAppAuth } from '@octokit/auth-app'
|
||||
import { Octokit } from '@octokit/core'
|
||||
import { paginateGraphql } from '@octokit/plugin-paginate-graphql'
|
||||
import crypto, { createHash } from 'node:crypto'
|
||||
import { BaseLoader, BaseSource } from './base'
|
||||
import { BaseLoader, BaseSource } from './base.js'
|
||||
|
||||
export const ExtendedOctokit = Octokit.plugin(paginateGraphql)
|
||||
export type ExtendedOctokit = InstanceType<typeof ExtendedOctokit>
|
||||
@@ -25,11 +25,9 @@ export type DiscussionsResponse = {
|
||||
}
|
||||
}
|
||||
|
||||
const appId = process.env.SEARCH_GITHUB_APP_ID ?? process.env.DOCS_GITHUB_APP_ID
|
||||
const installationId =
|
||||
process.env.SEARCH_GITHUB_APP_INSTALLATION_ID ?? process.env.DOCS_GITHUB_APP_INSTALLATION_ID
|
||||
const privateKey =
|
||||
process.env.SEARCH_GITHUB_APP_PRIVATE_KEY ?? process.env.DOCS_GITHUB_APP_PRIVATE_KEY
|
||||
const appId = process.env.DOCS_GITHUB_APP_ID
|
||||
const installationId = process.env.DOCS_GITHUB_APP_INSTALLATION_ID
|
||||
const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY
|
||||
|
||||
/**
|
||||
* Fetches GitHub discussions for a repository + category
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
GitHubDiscussionLoader,
|
||||
type GitHubDiscussionSource,
|
||||
fetchDiscussions,
|
||||
} from './github-discussion'
|
||||
import { MarkdownLoader, type MarkdownSource } from './markdown'
|
||||
import { IntegrationLoader, type IntegrationSource, fetchPartners } from './partner-integrations'
|
||||
} from './github-discussion.js'
|
||||
import { MarkdownLoader, type MarkdownSource } from './markdown.js'
|
||||
import { IntegrationLoader, type IntegrationSource, fetchPartners } from './partner-integrations.js'
|
||||
import {
|
||||
CliReferenceLoader,
|
||||
type CliReferenceSource,
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
type ClientLibReferenceSource,
|
||||
OpenApiReferenceLoader,
|
||||
type OpenApiReferenceSource,
|
||||
} from './reference-doc'
|
||||
import { walk } from './util'
|
||||
} from './reference-doc.js'
|
||||
import { walk } from './util.js'
|
||||
|
||||
const ignoredFiles = ['pages/404.mdx']
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFile } from 'fs/promises'
|
||||
import { processMdx } from '../../helpers.mdx'
|
||||
import { BaseLoader, BaseSource } from './base'
|
||||
import { processMdx } from '../../helpers.mdx.js'
|
||||
import { BaseLoader, BaseSource } from './base.js'
|
||||
|
||||
export class MarkdownLoader extends BaseLoader {
|
||||
type = 'markdown' as const
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { type SupabaseClient, createClient } from '@supabase/supabase-js'
|
||||
import { upperFirst } from 'lodash'
|
||||
|
||||
import { BaseLoader, BaseSource } from './base'
|
||||
import { processMdx } from '../../helpers.mdx'
|
||||
import { upperFirst } from 'lodash-es'
|
||||
import { processMdx } from '../../helpers.mdx.js'
|
||||
import { BaseLoader, BaseSource } from './base.js'
|
||||
|
||||
type PartnerData = {
|
||||
slug: string // The partner slug corresponding to the last part of the URL
|
||||
overview: string // The Markdown content for indexing
|
||||
}
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_MISC_USE_URL ?? process.env.NEXT_PUBLIC_MISC_URL
|
||||
const supabaseAnonKey =
|
||||
process.env.NEXT_PUBLIC_MISC_USE_ANON_KEY ?? process.env.NEXT_PUBLIC_MISC_ANON_KEY
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_MISC_URL
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_MISC_ANON_KEY
|
||||
|
||||
let supabaseClient: SupabaseClient
|
||||
function getSupabaseClient() {
|
||||
|
||||
@@ -7,12 +7,12 @@ import type {
|
||||
ICommonSection,
|
||||
IFunctionDefinition,
|
||||
ISpec,
|
||||
} from '../../../components/reference/Reference.types'
|
||||
import type { CliCommand, CliSpec } from '../../../generator/types/CliSpec'
|
||||
import { flattenSections } from '../../../lib/helpers'
|
||||
import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers'
|
||||
import type { Json } from '../../helpers.mdx'
|
||||
import { BaseLoader, BaseSource } from './base'
|
||||
} from '../../../components/reference/Reference.types.js'
|
||||
import type { CliCommand, CliSpec } from '../../../generator/types/CliSpec.js'
|
||||
import { flattenSections } from '../../../lib/helpers.js'
|
||||
import { enrichedOperation, gen_v3 } from '../../../lib/refGenerator/helpers.js'
|
||||
import type { Json } from '../../helpers.mdx.js'
|
||||
import { BaseLoader, BaseSource } from './base.js'
|
||||
|
||||
export abstract class ReferenceLoader<SpecSection> extends BaseLoader {
|
||||
type = 'reference' as const
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// generator files need their own tsconfig.json because they don't like "module": "esnext" setting that nextjs requires in the main tsconfig
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"incremental": true,
|
||||
"noImplicitAny": false,
|
||||
"esModuleInterop": true,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config({ path: '.env.local' })
|
||||
|
||||
export default undefined
|
||||
@@ -1,7 +1,7 @@
|
||||
clispec: '001'
|
||||
info:
|
||||
id: cli
|
||||
version: 2.23.4
|
||||
version: 2.24.3
|
||||
title: Supabase CLI
|
||||
language: sh
|
||||
source: https://github.com/supabase/cli
|
||||
@@ -1070,35 +1070,62 @@ commands:
|
||||
description: Select a desired instance size for your project.
|
||||
default_value: ''
|
||||
accepted_values:
|
||||
- id: micro
|
||||
name: micro
|
||||
type: string
|
||||
- id: small
|
||||
name: small
|
||||
- id: large
|
||||
name: large
|
||||
type: string
|
||||
- id: medium
|
||||
name: medium
|
||||
type: string
|
||||
- id: large
|
||||
name: large
|
||||
- id: micro
|
||||
name: micro
|
||||
type: string
|
||||
- id: xlarge
|
||||
name: xlarge
|
||||
- id: 12xlarge
|
||||
name: 12xlarge
|
||||
type: string
|
||||
- id: 16xlarge
|
||||
name: 16xlarge
|
||||
type: string
|
||||
- id: 24xlarge
|
||||
name: 24xlarge
|
||||
type: string
|
||||
- id: 24xlarge_high_memory
|
||||
name: 24xlarge_high_memory
|
||||
type: string
|
||||
- id: 24xlarge_optimized_cpu
|
||||
name: 24xlarge_optimized_cpu
|
||||
type: string
|
||||
- id: 24xlarge_optimized_memory
|
||||
name: 24xlarge_optimized_memory
|
||||
type: string
|
||||
- id: 2xlarge
|
||||
name: 2xlarge
|
||||
type: string
|
||||
- id: 48xlarge
|
||||
name: 48xlarge
|
||||
type: string
|
||||
- id: 48xlarge_high_memory
|
||||
name: 48xlarge_high_memory
|
||||
type: string
|
||||
- id: 48xlarge_optimized_cpu
|
||||
name: 48xlarge_optimized_cpu
|
||||
type: string
|
||||
- id: 48xlarge_optimized_memory
|
||||
name: 48xlarge_optimized_memory
|
||||
type: string
|
||||
- id: 4xlarge
|
||||
name: 4xlarge
|
||||
type: string
|
||||
- id: 8xlarge
|
||||
name: 8xlarge
|
||||
type: string
|
||||
- id: 12xlarge
|
||||
name: 12xlarge
|
||||
- id: pico
|
||||
name: pico
|
||||
type: string
|
||||
- id: 16xlarge
|
||||
name: 16xlarge
|
||||
- id: small
|
||||
name: small
|
||||
type: string
|
||||
- id: xlarge
|
||||
name: xlarge
|
||||
type: string
|
||||
- id: supabase-projects-api-keys
|
||||
title: supabase projects api-keys
|
||||
@@ -3770,35 +3797,62 @@ commands:
|
||||
description: Select a desired instance size for the branch database.
|
||||
default_value: ''
|
||||
accepted_values:
|
||||
- id: micro
|
||||
name: micro
|
||||
type: string
|
||||
- id: small
|
||||
name: small
|
||||
- id: large
|
||||
name: large
|
||||
type: string
|
||||
- id: medium
|
||||
name: medium
|
||||
type: string
|
||||
- id: large
|
||||
name: large
|
||||
- id: micro
|
||||
name: micro
|
||||
type: string
|
||||
- id: xlarge
|
||||
name: xlarge
|
||||
- id: 12xlarge
|
||||
name: 12xlarge
|
||||
type: string
|
||||
- id: 16xlarge
|
||||
name: 16xlarge
|
||||
type: string
|
||||
- id: 24xlarge
|
||||
name: 24xlarge
|
||||
type: string
|
||||
- id: 24xlarge_high_memory
|
||||
name: 24xlarge_high_memory
|
||||
type: string
|
||||
- id: 24xlarge_optimized_cpu
|
||||
name: 24xlarge_optimized_cpu
|
||||
type: string
|
||||
- id: 24xlarge_optimized_memory
|
||||
name: 24xlarge_optimized_memory
|
||||
type: string
|
||||
- id: 2xlarge
|
||||
name: 2xlarge
|
||||
type: string
|
||||
- id: 48xlarge
|
||||
name: 48xlarge
|
||||
type: string
|
||||
- id: 48xlarge_high_memory
|
||||
name: 48xlarge_high_memory
|
||||
type: string
|
||||
- id: 48xlarge_optimized_cpu
|
||||
name: 48xlarge_optimized_cpu
|
||||
type: string
|
||||
- id: 48xlarge_optimized_memory
|
||||
name: 48xlarge_optimized_memory
|
||||
type: string
|
||||
- id: 4xlarge
|
||||
name: 4xlarge
|
||||
type: string
|
||||
- id: 8xlarge
|
||||
name: 8xlarge
|
||||
type: string
|
||||
- id: 12xlarge
|
||||
name: 12xlarge
|
||||
- id: pico
|
||||
name: pico
|
||||
type: string
|
||||
- id: 16xlarge
|
||||
name: 16xlarge
|
||||
- id: small
|
||||
name: small
|
||||
type: string
|
||||
- id: xlarge
|
||||
name: xlarge
|
||||
type: string
|
||||
- id: experimental
|
||||
name: --experimental
|
||||
|
||||
@@ -791,6 +791,12 @@
|
||||
"type": "category",
|
||||
"title": "Realtime",
|
||||
"items": [
|
||||
{
|
||||
"id": "realtime-api",
|
||||
"title": "Overview",
|
||||
"slug": "realtime-api",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"id": "stream",
|
||||
"title": "Listen to database changes",
|
||||
|
||||
@@ -97,7 +97,7 @@ functions:
|
||||
url: str = os.environ.get("SUPABASE_URL")
|
||||
key: str = os.environ.get("SUPABASE_KEY")
|
||||
supabase: Client = create_client(
|
||||
url,
|
||||
url,
|
||||
key,
|
||||
options=ClientOptions(
|
||||
postgrest_client_timeout=10,
|
||||
@@ -173,7 +173,7 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.sign_up(
|
||||
{
|
||||
"email": "email@example.com",
|
||||
"email": "email@example.com",
|
||||
"password": "password",
|
||||
}
|
||||
)
|
||||
@@ -281,7 +281,7 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.sign_up(
|
||||
{
|
||||
"phone": "123456789",
|
||||
"phone": "123456789",
|
||||
"password": "password",
|
||||
}
|
||||
)
|
||||
@@ -467,7 +467,7 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.sign_in_with_password(
|
||||
{
|
||||
"email": "email@example.com",
|
||||
"email": "email@example.com",
|
||||
"password": "example-password",
|
||||
}
|
||||
)
|
||||
@@ -551,7 +551,7 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.sign_in_with_password(
|
||||
{
|
||||
"phone": "+13334445555",
|
||||
"phone": "+13334445555",
|
||||
"password": "some-password",
|
||||
}
|
||||
)
|
||||
@@ -776,7 +776,7 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.sign_in_with_id_token(
|
||||
{
|
||||
"provider": "google",
|
||||
"provider": "google",
|
||||
"token": "your-id-token",
|
||||
}
|
||||
)
|
||||
@@ -1000,8 +1000,8 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.verify_otp(
|
||||
{
|
||||
"email": "email@example.com",
|
||||
"token": "123456",
|
||||
"email": "email@example.com",
|
||||
"token": "123456",
|
||||
"type": "email",
|
||||
}
|
||||
)
|
||||
@@ -1081,8 +1081,8 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.verify_otp(
|
||||
{
|
||||
"phone": "+13334445555",
|
||||
"token": "123456",
|
||||
"phone": "+13334445555",
|
||||
"token": "123456",
|
||||
"type": "sms",
|
||||
}
|
||||
)
|
||||
@@ -1094,8 +1094,8 @@ functions:
|
||||
```python
|
||||
response = supabase.auth.verify_otp(
|
||||
{
|
||||
"email": "email@example.com",
|
||||
"token_hash": "<token-hash>",
|
||||
"email": "email@example.com",
|
||||
"token_hash": "<token-hash>",
|
||||
"type": "email",
|
||||
}
|
||||
)
|
||||
@@ -5531,8 +5531,8 @@ functions:
|
||||
supabase.table("texts")
|
||||
.select("content")
|
||||
.text_search(
|
||||
"content",
|
||||
"'eggs' & 'ham'",
|
||||
"content",
|
||||
"'eggs' & 'ham'",
|
||||
options={"config": "english"},
|
||||
)
|
||||
.execute()
|
||||
@@ -6650,7 +6650,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = supabase.functions.invoke(
|
||||
"hello-world",
|
||||
"hello-world",
|
||||
invoke_options={
|
||||
"body": {"name": "Functions"},
|
||||
},
|
||||
@@ -6703,6 +6703,32 @@ functions:
|
||||
)
|
||||
```
|
||||
|
||||
- id: realtime-api
|
||||
title: 'Overview'
|
||||
description: |
|
||||
Realtime in Python only works with the asynchronous client.
|
||||
You can initialize a new Supabase client using the `acreate_client()` method.
|
||||
|
||||
- Some Realtime methods are asynchronous and must be awaited. Ensure these methods are called within an `async` function.
|
||||
- In the following Realtime examples, certain methods are awaited. These should be enclosed within an `async` function.
|
||||
- When an asynchronous method needs to be used within a synchronous context, such as the callback for `.subscribe()`, utilize `asyncio.create_task()` to schedule the coroutine. This is why the `acreate_client` example includes an import of `asyncio`.
|
||||
examples:
|
||||
- id: acreate-client
|
||||
name: acreate_client()
|
||||
code: |
|
||||
```python
|
||||
import os
|
||||
import asyncio
|
||||
from supabase import acreate_client, AsyncClient
|
||||
|
||||
url: str = os.environ.get("SUPABASE_URL")
|
||||
key: str = os.environ.get("SUPABASE_KEY")
|
||||
|
||||
async def create_supabase():
|
||||
supabase: AsyncClient = await acreate_client(url, key)
|
||||
return supabase
|
||||
```
|
||||
|
||||
- id: subscribe
|
||||
title: on().subscribe()
|
||||
notes: |
|
||||
@@ -6720,15 +6746,15 @@ functions:
|
||||
|
||||
def on_subscribe(status, err):
|
||||
if status == RealtimeSubscribeStates.SUBSCRIBED:
|
||||
channel.send_broadcast(
|
||||
"cursor-pos",
|
||||
asyncio.create_task(channel.send_broadcast(
|
||||
"cursor-pos",
|
||||
{"x": random.random(), "y": random.random()}
|
||||
)
|
||||
))
|
||||
|
||||
def handle_broadcast(payload):
|
||||
print("Cursor position received!", payload)
|
||||
|
||||
channel.on_broadcast(event="cursor-pos", callback=handle_broadcast).subscribe(on_subscribe)
|
||||
await channel.on_broadcast(event="cursor-pos", callback=handle_broadcast).subscribe(on_subscribe)
|
||||
```
|
||||
- id: listen-to-presence-sync
|
||||
name: Listen to presence sync
|
||||
@@ -6739,12 +6765,12 @@ functions:
|
||||
|
||||
def on_subscribe(status, err):
|
||||
if status == RealtimeSubscribeStates.SUBSCRIBED:
|
||||
channel.track({"online_at": datetime.datetime.now().isoformat()})
|
||||
asyncio.create_task(channel.track({"online_at": datetime.datetime.now().isoformat()}))
|
||||
|
||||
def handle_presence_sync():
|
||||
print("Synced presence state: ", channel.presence.state)
|
||||
|
||||
channel.on_presence_sync(callback=handle_presence_sync).subscribe(on_subscribe)
|
||||
await channel.on_presence_sync(callback=handle_presence_sync).subscribe(on_subscribe)
|
||||
```
|
||||
- id: listen-to-presence-join
|
||||
name: Listen to presence join
|
||||
@@ -6758,9 +6784,9 @@ functions:
|
||||
|
||||
def on_subscribe(status, err):
|
||||
if status == RealtimeSubscribeStates.SUBSCRIBED:
|
||||
channel.track({"online_at": datetime.datetime.now().isoformat()})
|
||||
asyncio.create_task(channel.track({"online_at": datetime.datetime.now().isoformat()}))
|
||||
|
||||
channel.on_presence_join(callback=handle_presence_join).subscribe(on_subscribe)
|
||||
await channel.on_presence_join(callback=handle_presence_join).subscribe(on_subscribe)
|
||||
```
|
||||
- id: listen-to-presence-leave
|
||||
name: Listen to presence leave
|
||||
@@ -6774,10 +6800,10 @@ functions:
|
||||
|
||||
def on_subscribe(status, err):
|
||||
if status == RealtimeSubscribeStates.SUBSCRIBED:
|
||||
channel.track({"online_at": datetime.datetime.now().isoformat()})
|
||||
channel.untrack()
|
||||
asyncio.create_task(channel.track({"online_at": datetime.datetime.now().isoformat()}))
|
||||
asyncio.create_task(channel.untrack())
|
||||
|
||||
channel.on_presence_leave(callback=handle_presence_leave).subscribe(on_subscribe)
|
||||
await channel.on_presence_leave(callback=handle_presence_leave).subscribe(on_subscribe)
|
||||
```
|
||||
- id: listen-to-all-database-changes
|
||||
name: Listen to all database changes
|
||||
@@ -6785,7 +6811,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("*", schema="*", callback=handle_record_updated)
|
||||
.subscribe()
|
||||
)
|
||||
@@ -6795,7 +6821,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("*", schema="public", table="countries", callback=handle_record_updated)
|
||||
.subscribe()
|
||||
)
|
||||
@@ -6805,7 +6831,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("INSERT", schema="public", table="countries", callback=handle_record_inserted)
|
||||
.subscribe()
|
||||
)
|
||||
@@ -6822,7 +6848,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("UPDATE", schema="public", table="countries", callback=handle_record_updated)
|
||||
.subscribe()
|
||||
)
|
||||
@@ -6839,7 +6865,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("DELETE", schema="public", table="countries", callback=handle_record_deleted)
|
||||
.subscribe()
|
||||
)
|
||||
@@ -6850,7 +6876,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("INSERT", schema="public", table="countries", callback=handle_record_inserted)
|
||||
.on_postgres_changes("DELETE", schema="public", table="countries", callback=handle_record_deleted)
|
||||
.subscribe()
|
||||
@@ -6864,7 +6890,7 @@ functions:
|
||||
code: |
|
||||
```python
|
||||
response = (
|
||||
supabase.channel("room1")
|
||||
await supabase.channel("room1")
|
||||
.on_postgres_changes("UPDATE", schema="public", table="countries", filter="id=eq.200", callback=handle_record_updated)
|
||||
.subscribe()
|
||||
)
|
||||
@@ -6883,9 +6909,9 @@ functions:
|
||||
|
||||
def on_subscribe(status, err):
|
||||
if status == RealtimeSubscribeStates.SUBSCRIBED:
|
||||
channel.send_broadcast('cursor-pos', {"x": random.random(), "y": random.random()})
|
||||
asyncio.create_task(channel.send_broadcast('cursor-pos', {"x": random.random(), "y": random.random()}))
|
||||
|
||||
channel.subscribe(on_subscribe)
|
||||
await channel.subscribe(on_subscribe)
|
||||
```
|
||||
response: |
|
||||
```
|
||||
@@ -6912,7 +6938,7 @@ functions:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```python
|
||||
supabase.remove_channel(myChannel)
|
||||
await supabase.remove_channel(myChannel)
|
||||
```
|
||||
|
||||
- id: remove-all-channels
|
||||
@@ -6925,7 +6951,7 @@ functions:
|
||||
isSpotlight: true
|
||||
code: |
|
||||
```python
|
||||
supabase.remove_all_channels()
|
||||
await supabase.remove_all_channels()
|
||||
```
|
||||
|
||||
- id: list-buckets
|
||||
@@ -7305,7 +7331,7 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.move(
|
||||
"public/avatar1.png",
|
||||
"public/avatar1.png",
|
||||
"private/avatar2.png"
|
||||
)
|
||||
)
|
||||
@@ -7342,7 +7368,7 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.copy(
|
||||
"public/avatar1.png",
|
||||
"public/avatar1.png",
|
||||
"private/avatar2.png"
|
||||
)
|
||||
)
|
||||
@@ -7391,7 +7417,7 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.create_signed_urls(
|
||||
["folder/avatar1.png", "folder/avatar2.png"],
|
||||
["folder/avatar1.png", "folder/avatar2.png"],
|
||||
60
|
||||
)
|
||||
)
|
||||
@@ -7475,7 +7501,7 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.create_signed_url(
|
||||
"folder/avatar1.png",
|
||||
"folder/avatar1.png",
|
||||
60
|
||||
)
|
||||
)
|
||||
@@ -7495,8 +7521,8 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.create_signed_url(
|
||||
"folder/avatar1.png",
|
||||
60,
|
||||
"folder/avatar1.png",
|
||||
60,
|
||||
{"transform": {"width": 100, "height": 100}}
|
||||
)
|
||||
)
|
||||
@@ -7510,8 +7536,8 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.create_signed_url(
|
||||
"folder/avatar1.png",
|
||||
60,
|
||||
"folder/avatar1.png",
|
||||
60,
|
||||
{"download": True}
|
||||
)
|
||||
)
|
||||
@@ -7691,7 +7717,7 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.get_public_url(
|
||||
"folder/avatar1.jpg",
|
||||
"folder/avatar1.jpg",
|
||||
{"transform": {"width": 100, "height": 100}}
|
||||
)
|
||||
)
|
||||
@@ -7705,7 +7731,7 @@ functions:
|
||||
supabase.storage
|
||||
.from_("avatars")
|
||||
.get_public_url(
|
||||
"folder/avatar1.jpg",
|
||||
"folder/avatar1.jpg",
|
||||
{"download": True}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -10,8 +10,7 @@ module.exports = config({
|
||||
'./layouts/**/*.tsx',
|
||||
'./pages/**/*.{tsx,mdx}',
|
||||
'./../../packages/ui/src/**/*.{tsx,ts,js}',
|
||||
'./../../packages/ui-patterns/**/*.{tsx,ts,js}',
|
||||
'!./../../packages/ui-patterns/node_modules/**/*', // ignore node_modules rule
|
||||
'./../../packages/ui-patterns/src/**/*.{tsx,ts,js}',
|
||||
],
|
||||
plugins: [
|
||||
function ({ addUtilities, addVariant }) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
"NEXT_PUBLIC_SUPABASE_URL",
|
||||
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
||||
"NEXT_PUBLIC_MISC_URL",
|
||||
"NEXT_PUBLIC_MISC_USE_URL",
|
||||
"NEXT_PUBLIC_MISC_ANON_KEY",
|
||||
"NEXT_PUBLIC_MISC_USE_ANON_KEY",
|
||||
"NODE_ENV",
|
||||
"NEXT_PUBLIC_API_URL",
|
||||
"NEXT_PUBLIC_BASE_PATH",
|
||||
@@ -48,9 +46,6 @@
|
||||
"DOCS_REVALIDATION_OVERRIDE_KEYS",
|
||||
"FORCE_ASSET_CDN",
|
||||
"OPENAI_API_KEY",
|
||||
"SEARCH_GITHUB_APP_ID",
|
||||
"SEARCH_GITHUB_APP_INSTALLATION_ID",
|
||||
"SEARCH_GITHUB_APP_PRIVATE_KEY",
|
||||
"SITE_NAME",
|
||||
"SUPABASE_SECRET_KEY"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
|
||||
// The SerializeOptions is not exported from next-mdx-remote/serialize, so we need to
|
||||
// manually define it here.
|
||||
export type SerializeOptions = Parameters<typeof serialize>[1]
|
||||
@@ -1,9 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
exclude: ['examples/**/*', '**/node_modules/**'],
|
||||
setupFiles: ['vitest.setup.ts'],
|
||||
},
|
||||
plugins: [tsconfigPaths({ root: import.meta.dirname })],
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { afterAll, beforeAll, vi } from 'vitest'
|
||||
|
||||
let oldEnv: NodeJS.ProcessEnv
|
||||
|
||||
beforeAll(() => {
|
||||
// Use local Supabase to run e2e tests
|
||||
oldEnv = { ...process.env }
|
||||
process.env = {
|
||||
...process.env,
|
||||
NEXT_PUBLIC_SUPABASE_URL: 'http://localhost:54321',
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY:
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0',
|
||||
}
|
||||
|
||||
// Prevent errors about importing server-only modules from Client Components
|
||||
vi.mock('server-only', () => {
|
||||
return {}
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env = oldEnv
|
||||
vi.doUnmock('server-only')
|
||||
})
|
||||
@@ -340,6 +340,7 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
|
||||
className="!mb-0 mt-0 overflow-hidden h-96 w-full"
|
||||
title={id}
|
||||
srcDoc={bodyValue}
|
||||
sandbox="allow-scripts allow-forms"
|
||||
/>
|
||||
<Admonition
|
||||
type="default"
|
||||
|
||||
@@ -78,10 +78,10 @@ const PolicyRow = ({
|
||||
<div className="text-foreground-lighter text-sm">
|
||||
Applied to:{' '}
|
||||
{policy.roles.slice(0, 3).map((role, i) => (
|
||||
<code key={`policy-${role}-${i}`} className="text-foreground-light text-xs">
|
||||
{role}
|
||||
{policy.roles.length > 1 ? '' : ' '}
|
||||
</code>
|
||||
<span key={`policy-${role}-${i}`}>
|
||||
<code className="text-foreground-light text-xs">{role}</code>
|
||||
{i < Math.min(policy.roles.length, 3) - 1 && ', '}
|
||||
</span>
|
||||
))}
|
||||
{policy.roles.length > 1 ? 'roles' : 'role'}
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ const ContentFile = ({ projectKeys }: ContentFileProps) => {
|
||||
<SimpleCodeBlock className="bash" parentClassName="min-h-72">
|
||||
{`
|
||||
SUPABASE_URL=${projectKeys.apiUrl ?? 'your-project-url'}
|
||||
SUPABASE_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
|
||||
SUPABASE_ANON_KEY=${projectKeys.anonKey ?? 'your-anon-key'}
|
||||
`}
|
||||
</SimpleCodeBlock>
|
||||
</ConnectTabContent>
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { motion } from 'framer-motion'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ArrowUpRight, Circle, Database, MoreVertical, Plus, Search } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import ReactFlow, { Background, Handle, Position, ReactFlowProvider } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { Button, Input } from 'ui'
|
||||
import { NODE_WIDTH } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
|
||||
const STATIC_NODES = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'primary',
|
||||
data: {
|
||||
label: 'Primary Database',
|
||||
region: 'East US (Ohio)',
|
||||
provider: 'AWS',
|
||||
regionIcon: 'EAST_US',
|
||||
},
|
||||
position: { x: 825, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'replica',
|
||||
data: {
|
||||
label: 'Iceberg',
|
||||
details: '3 tables',
|
||||
regionIcon: 'WEST_US',
|
||||
},
|
||||
position: { x: 875, y: 110 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'replica',
|
||||
data: {
|
||||
label: 'BigQuery',
|
||||
details: '5 tables',
|
||||
regionIcon: 'WEST_US',
|
||||
},
|
||||
position: { x: 875, y: 200 },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'blank',
|
||||
position: { x: 875, y: 290 },
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'cta',
|
||||
position: { x: 125, y: 20 },
|
||||
data: {},
|
||||
},
|
||||
]
|
||||
|
||||
const STATIC_EDGES = [
|
||||
{ id: 'e1-2', source: '1', target: '2', type: 'smoothstep', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3', type: 'smoothstep', animated: true },
|
||||
{ id: 'e1-4', source: '1', target: '4', type: 'smoothstep', animated: true },
|
||||
]
|
||||
|
||||
const ReplicationStaticMockup = () => {
|
||||
const nodes = useMemo(() => STATIC_NODES, [])
|
||||
const edges = useMemo(() => STATIC_EDGES, [])
|
||||
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
const backgroundPatternColor =
|
||||
resolvedTheme === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.4)'
|
||||
|
||||
const nodeTypes = useMemo(
|
||||
() => ({
|
||||
primary: PrimaryNode,
|
||||
replica: ReplicaNode,
|
||||
blank: BlankNode,
|
||||
cta: CTANode,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="relative border-t">
|
||||
<div className="h-[500px] w-full relative">
|
||||
<ReactFlow
|
||||
fitView
|
||||
fitViewOptions={{ minZoom: 0.9, maxZoom: 0.9 }}
|
||||
className="instance-configuration"
|
||||
zoomOnPinch={false}
|
||||
zoomOnScroll={false}
|
||||
nodesDraggable={true}
|
||||
nodesConnectable={false}
|
||||
zoomOnDoubleClick={false}
|
||||
edgesFocusable={false}
|
||||
edgesUpdatable={false}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background color={backgroundPatternColor} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<StaticDestinations />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ReplicationComingSoon = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReplicationStaticMockup />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReplicationComingSoon
|
||||
|
||||
const PrimaryNode = ({
|
||||
data,
|
||||
}: {
|
||||
data: { label: string; region: string; provider: string; regionIcon: string }
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col rounded bg-surface-100 border border-default">
|
||||
<div className="flex items-start justify-between p-3" style={{ width: NODE_WIDTH / 2 + 55 }}>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="w-8 h-8 bg-brand-500 border border-brand-600 rounded-md flex items-center justify-center">
|
||||
<Database size={16} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="text-sm">{data.label}</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-sm text-foreground-light">{data.region}</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-sm text-foreground-light">{data.provider}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-6 rounded-sm mt-0.5"
|
||||
src={`${BASE_PATH}/img/regions/${data.regionIcon}.svg`}
|
||||
/>
|
||||
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
<Handle type="source" position={Position.Left} className="opacity-25" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const ReplicaNode = ({
|
||||
data,
|
||||
}: {
|
||||
data: { label: string; details: string; regionIcon: string }
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col rounded bg-surface-100 border border-default px-2">
|
||||
<div className="flex items-start justify-between p-3" style={{ width: NODE_WIDTH / 2 - 10 }}>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="">{data.label}</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-sm text-foreground-light">{data.details}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-6 rounded-sm mt-0.5"
|
||||
src={`${BASE_PATH}/img/regions/${data.regionIcon}.svg`}
|
||||
/>
|
||||
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Handle type="target" position={Position.Left} className="opacity-25" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BlankNode = () => {
|
||||
return (
|
||||
<div className="flex flex-col rounded bg-surface-100 border border-default px-1">
|
||||
<div className="flex items-start justify-between p-3" style={{ width: NODE_WIDTH / 2 }}>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<Plus size={16} />
|
||||
<span className="text-sm">Add new</span>
|
||||
</div>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} className="opacity-25" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CTANode = () => {
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-surface-100 rounded-lg p-8 shadow-lg"
|
||||
style={{
|
||||
border: '1px solid',
|
||||
borderColor: 'hsl(var(--foreground-default) / var(--border-opacity, 0.6))',
|
||||
}}
|
||||
animate={{
|
||||
'--border-opacity': [0.6, 0.4, 0.2, 0.1, 0.2, 0.4, 0.6],
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
ease: 'linear',
|
||||
repeat: Number.POSITIVE_INFINITY,
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-4 w-[425px] relative">
|
||||
<span className="text-xs uppercase text-foreground-light">Early Access</span>
|
||||
<h2 className="text-lg">Replicate Your Data in Real Time</h2>
|
||||
<p>
|
||||
Stream changes from your Postgres database into your data warehouse—no manual exports, no
|
||||
lag.
|
||||
</p>
|
||||
<p>
|
||||
We're rolling this out to a limited group of early adopters. Sign up to get early access.
|
||||
</p>
|
||||
<p>
|
||||
<Button asChild type="secondary">
|
||||
<Link href="https://forms.supabase.com/pg_replicate" target="_blank" rel="noreferrer">
|
||||
<span className="flex items-center gap-x-1">
|
||||
Request Early Access
|
||||
<ArrowUpRight size={16} />
|
||||
</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
const StaticDestinations = () => {
|
||||
const mockRows = [
|
||||
{ name: 'BigQuery', tables: 4, lag: '55ms', status: 'Enabled' },
|
||||
{ name: 'Iceberg', tables: 4, lag: '85ms', status: 'Enabled' },
|
||||
{ name: 'US East', tables: 4, lag: '125ms', status: 'Enabled' },
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col bg-surface-100 px-6 pt-6 border-t relative h-full">
|
||||
<div className="bg-surface-300 w-full h-full absolute top-0 left-0 opacity-30"></div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
size="small"
|
||||
className="w-52 bg-transparent"
|
||||
iconContainerClassName="pl-2"
|
||||
icon={<Search size={14} className="text-foreground-lighter" />}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<Button
|
||||
icon={<Plus size={16} />}
|
||||
type="primary"
|
||||
className="flex items-center"
|
||||
onClick={() => {}}
|
||||
>
|
||||
New destination
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="publication">Publication</Table.th>,
|
||||
<Table.th key="lag">Lag</Table.th>,
|
||||
<Table.th key="status">Status</Table.th>,
|
||||
<Table.th key="actions"></Table.th>,
|
||||
]}
|
||||
className="mt-4"
|
||||
body={mockRows.map((row, i) => (
|
||||
<Table.tr key={i}>
|
||||
<Table.td>{row.name}</Table.td>
|
||||
<Table.td>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-bold">All</span>
|
||||
<span className="text-sm text-foreground-lighter">{row.tables} tables</span>
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td>{row.lag}</Table.td>
|
||||
<Table.td>
|
||||
<span className="flex items-center gap-3">
|
||||
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
|
||||
{row.status}
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<button className="p-1">
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PostgresSchema } from '@supabase/postgres-meta'
|
||||
import { toPng, toSvg } from 'html-to-image'
|
||||
import { Download, Loader2 } from 'lucide-react'
|
||||
import { Check, Download, Loader2, Clipboard, Info } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow'
|
||||
@@ -21,7 +21,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
||||
import { SchemaGraphLegend } from './SchemaGraphLegend'
|
||||
import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.utils'
|
||||
import { TableNode } from './SchemaTableNode'
|
||||
|
||||
import { copyToClipboard } from 'ui'
|
||||
// [Joshen] Persisting logic: Only save positions to local storage WHEN a node is moved OR when explicitly clicked to reset layout
|
||||
|
||||
export const SchemaGraph = () => {
|
||||
@@ -30,6 +30,13 @@ export const SchemaGraph = () => {
|
||||
const { project } = useProjectContext()
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
|
||||
const [copied, setCopied] = useState(false)
|
||||
useEffect(() => {
|
||||
if (copied) {
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
}, [copied])
|
||||
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
|
||||
const miniMapNodeColor = '#111318'
|
||||
@@ -160,6 +167,59 @@ export const SchemaGraph = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function tablesToSQL(t: typeof tables) {
|
||||
if (!Array.isArray(t)) return ''
|
||||
const warning =
|
||||
'-- WARNING: This schema is for context only and is not meant to be run.\n-- Table order and constraints may not be valid for execution.\n\n'
|
||||
const sql = t
|
||||
.map((table) => {
|
||||
if (!table || !Array.isArray((table as any).columns)) return ''
|
||||
|
||||
const columns = (table as { columns?: any[] }).columns ?? []
|
||||
const columnLines = columns.map((c) => {
|
||||
let line = ` ${c.name} ${c.data_type}`
|
||||
if (c.is_identity) {
|
||||
line += ' GENERATED ALWAYS AS IDENTITY'
|
||||
}
|
||||
if (c.is_nullable === false) {
|
||||
line += ' NOT NULL'
|
||||
}
|
||||
if (c.default_value !== null && c.default_value !== undefined) {
|
||||
line += ` DEFAULT ${c.default_value}`
|
||||
}
|
||||
if (c.is_unique) {
|
||||
line += ' UNIQUE'
|
||||
}
|
||||
if (c.check) {
|
||||
line += ` CHECK (${c.check})`
|
||||
}
|
||||
return line
|
||||
})
|
||||
|
||||
const constraints: string[] = []
|
||||
|
||||
if (Array.isArray(table.primary_keys) && table.primary_keys.length > 0) {
|
||||
const pkCols = table.primary_keys.map((pk) => pk.name).join(', ')
|
||||
constraints.push(` CONSTRAINT ${table.name}_pkey PRIMARY KEY (${pkCols})`)
|
||||
}
|
||||
|
||||
if (Array.isArray(table.relationships)) {
|
||||
table.relationships.forEach((rel) => {
|
||||
if (rel && rel.source_table_name === table.name) {
|
||||
constraints.push(
|
||||
` CONSTRAINT ${rel.constraint_name} FOREIGN KEY (${rel.source_column_name}) REFERENCES ${rel.target_table_schema}.${rel.target_table_name}(${rel.target_column_name})`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const allLines = [...columnLines, ...constraints]
|
||||
return `CREATE TABLE ${table.schema}.${table.name} (\n${allLines.join(',\n')}\n);`
|
||||
})
|
||||
.join('\n')
|
||||
return warning + sql
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccessTables && isSuccessSchemas && tables.length > 0) {
|
||||
const schema = schemas.find((s) => s.name === selectedSchema) as PostgresSchema
|
||||
@@ -192,6 +252,40 @@ export const SchemaGraph = () => {
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<ButtonTooltip
|
||||
type="outline"
|
||||
icon={copied ? <Check /> : <Clipboard />}
|
||||
onClick={() => {
|
||||
if (tables) {
|
||||
copyToClipboard(tablesToSQL(tables))
|
||||
setCopied(true)
|
||||
}
|
||||
}}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: (
|
||||
<div className="max-w-[180px] space-y-2 text-foreground-light">
|
||||
<p className="text-foreground">Note</p>
|
||||
<p>
|
||||
This schema is for context or debugging only. Table order and constraints
|
||||
may be invalid. Not meant to be run as-is.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
}}
|
||||
>
|
||||
Copy as SQL
|
||||
</ButtonTooltip>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
loading={isDownloading}
|
||||
className="px-1.5"
|
||||
icon={<Download />}
|
||||
onClick={() => downloadImage('png')}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Download current view as PNG' } }}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonTooltip
|
||||
|
||||
@@ -25,20 +25,20 @@ const SERVICE_STATUS_THRESHOLD = 5 // minutes
|
||||
const StatusMessage = ({
|
||||
status,
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isHealthy,
|
||||
isProjectNew,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isSuccess: boolean
|
||||
isHealthy: boolean
|
||||
isProjectNew: boolean
|
||||
status?: ProjectServiceStatus
|
||||
}) => {
|
||||
if (isHealthy) return 'Healthy'
|
||||
if (isLoading) return 'Checking status'
|
||||
if (status === 'UNHEALTHY') return 'Unhealthy'
|
||||
if (status === 'COMING_UP') return 'Coming up...'
|
||||
if (status === 'ACTIVE_HEALTHY') return 'Healthy'
|
||||
if (isProjectNew) return 'Coming up...'
|
||||
if (isSuccess) return 'Healthy'
|
||||
return 'Unable to connect'
|
||||
}
|
||||
|
||||
@@ -52,21 +52,21 @@ const CheckIcon = () => <CheckCircle2 {...iconProps} className="text-brand" />
|
||||
|
||||
const StatusIcon = ({
|
||||
isLoading,
|
||||
isSuccess,
|
||||
isHealthy,
|
||||
isProjectNew,
|
||||
projectStatus,
|
||||
}: {
|
||||
isLoading: boolean
|
||||
isSuccess: boolean
|
||||
isHealthy: boolean
|
||||
isProjectNew: boolean
|
||||
projectStatus?: ProjectServiceStatus
|
||||
}) => {
|
||||
if (isHealthy) return <CheckIcon />
|
||||
if (isLoading) return <LoaderIcon />
|
||||
if (projectStatus === 'UNHEALTHY') return <AlertIcon />
|
||||
if (projectStatus === 'COMING_UP') return <LoaderIcon />
|
||||
if (projectStatus === 'ACTIVE_HEALTHY') return <CheckIcon />
|
||||
if (isProjectNew) return <LoaderIcon />
|
||||
if (isSuccess) return <CheckIcon />
|
||||
return <AlertIcon />
|
||||
}
|
||||
|
||||
@@ -124,17 +124,17 @@ export const ServiceStatus = () => {
|
||||
error?: string
|
||||
docsUrl?: string
|
||||
isLoading: boolean
|
||||
isSuccess?: boolean
|
||||
isHealthy: boolean
|
||||
status: ProjectServiceStatus
|
||||
logsUrl: string
|
||||
status?: ProjectServiceStatus
|
||||
}[] = [
|
||||
{
|
||||
name: 'Database',
|
||||
error: undefined,
|
||||
docsUrl: undefined,
|
||||
isLoading: isLoading,
|
||||
isSuccess: dbStatus?.healthy,
|
||||
status: dbStatus?.status,
|
||||
isHealthy: !!dbStatus?.healthy,
|
||||
status: dbStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/postgres-logs',
|
||||
},
|
||||
{
|
||||
@@ -142,8 +142,8 @@ export const ServiceStatus = () => {
|
||||
error: restStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isSuccess: restStatus?.healthy,
|
||||
status: restStatus?.status,
|
||||
isHealthy: !!restStatus?.healthy,
|
||||
status: restStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/postgrest-logs',
|
||||
},
|
||||
...(authEnabled
|
||||
@@ -153,8 +153,8 @@ export const ServiceStatus = () => {
|
||||
error: authStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isSuccess: authStatus?.healthy,
|
||||
status: authStatus?.status,
|
||||
isHealthy: !!authStatus?.healthy,
|
||||
status: authStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/auth-logs',
|
||||
},
|
||||
]
|
||||
@@ -166,8 +166,8 @@ export const ServiceStatus = () => {
|
||||
error: realtimeStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isSuccess: realtimeStatus?.healthy,
|
||||
status: realtimeStatus?.status,
|
||||
isHealthy: !!realtimeStatus?.healthy,
|
||||
status: realtimeStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/realtime-logs',
|
||||
},
|
||||
]
|
||||
@@ -179,8 +179,8 @@ export const ServiceStatus = () => {
|
||||
error: storageStatus?.error,
|
||||
docsUrl: undefined,
|
||||
isLoading,
|
||||
isSuccess: storageStatus?.healthy,
|
||||
status: storageStatus?.status,
|
||||
isHealthy: !!storageStatus?.healthy,
|
||||
status: storageStatus?.status ?? 'UNHEALTHY',
|
||||
logsUrl: '/logs/storage-logs',
|
||||
},
|
||||
]
|
||||
@@ -192,7 +192,12 @@ export const ServiceStatus = () => {
|
||||
error: undefined,
|
||||
docsUrl: 'https://supabase.com/docs/guides/functions/troubleshooting',
|
||||
isLoading,
|
||||
isSuccess: edgeFunctionsStatus?.healthy,
|
||||
isHealthy: !!edgeFunctionsStatus?.healthy,
|
||||
status: edgeFunctionsStatus?.healthy
|
||||
? 'ACTIVE_HEALTHY'
|
||||
: isLoading
|
||||
? 'COMING_UP'
|
||||
: ('UNHEALTHY' as ProjectServiceStatus),
|
||||
logsUrl: '/logs/edge-functions-logs',
|
||||
},
|
||||
]
|
||||
@@ -200,7 +205,7 @@ export const ServiceStatus = () => {
|
||||
]
|
||||
|
||||
const isLoadingChecks = services.some((service) => service.isLoading)
|
||||
const allServicesOperational = services.every((service) => service.isSuccess)
|
||||
const allServicesOperational = services.every((service) => service.isHealthy)
|
||||
|
||||
// If the project is less than 5 minutes old, and status is not operational, then it's likely the service is still starting up
|
||||
const isProjectNew =
|
||||
@@ -234,7 +239,7 @@ export const ServiceStatus = () => {
|
||||
<Button
|
||||
type="default"
|
||||
icon={
|
||||
isLoadingChecks || isProjectNew ? (
|
||||
isLoadingChecks || (!allServicesOperational && isProjectNew) ? (
|
||||
<LoaderIcon />
|
||||
) : (
|
||||
<div
|
||||
@@ -258,7 +263,7 @@ export const ServiceStatus = () => {
|
||||
<div className="flex gap-x-2">
|
||||
<StatusIcon
|
||||
isLoading={service.isLoading}
|
||||
isSuccess={!!service.isSuccess}
|
||||
isHealthy={!!service.isHealthy}
|
||||
isProjectNew={isProjectNew}
|
||||
projectStatus={service.status}
|
||||
/>
|
||||
@@ -267,7 +272,7 @@ export const ServiceStatus = () => {
|
||||
<p className="text-foreground-light flex items-center gap-1">
|
||||
<StatusMessage
|
||||
isLoading={service.isLoading}
|
||||
isSuccess={!!service.isSuccess}
|
||||
isHealthy={!!service.isHealthy}
|
||||
isProjectNew={isProjectNew}
|
||||
status={service.status}
|
||||
/>
|
||||
|
||||
@@ -87,7 +87,7 @@ export const PostgresVersionSelector = ({
|
||||
type === 'create'
|
||||
? createVersions?.available_versions ?? []
|
||||
: unpauseVersions?.available_versions ?? []
|
||||
const availableVersions = versions.sort((a, b) => a.version.localeCompare(b.version))
|
||||
const availableVersions = versions.sort((a, b) => a.version.localeCompare(b.version)).reverse()
|
||||
const { postgresVersionSelection } = form.watch()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
+10
-1
@@ -3,12 +3,14 @@ import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { LOCAL_STORAGE_KEYS } from 'common'
|
||||
import { CANCELLATION_REASONS } from 'components/interfaces/Billing/Billing.constants'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useSendDowngradeFeedbackMutation } from 'data/feedback/exit-survey-send'
|
||||
import { useProjectDeleteMutation } from 'data/projects/project-delete-mutation'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { Input } from 'ui'
|
||||
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
|
||||
@@ -18,6 +20,11 @@ const DeleteProjectModal = ({ visible, onClose }: { visible: boolean; onClose: (
|
||||
const { project } = useProjectContext()
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
const [lastVisitedOrganization] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
|
||||
''
|
||||
)
|
||||
|
||||
const projectRef = project?.ref
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
||||
const projectPlan = subscription?.plan?.id ?? 'free'
|
||||
@@ -43,7 +50,9 @@ const DeleteProjectModal = ({ visible, onClose }: { visible: boolean; onClose: (
|
||||
}
|
||||
|
||||
toast.success(`Successfully deleted ${project?.name}`)
|
||||
router.push(`/projects`)
|
||||
|
||||
if (lastVisitedOrganization) router.push(`/org/${lastVisitedOrganization}`)
|
||||
else router.push('/organizations')
|
||||
},
|
||||
})
|
||||
const { mutateAsync: sendExitSurvey, isLoading: isSending } = useSendDowngradeFeedbackMutation()
|
||||
|
||||
+27
-40
@@ -1,18 +1,11 @@
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { FormHeader } from 'components/ui/Forms/FormHeader'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import {
|
||||
Alert,
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
CriticalIcon,
|
||||
} from 'ui'
|
||||
import DeleteProjectButton from './DeleteProjectButton'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import PartnerManagedResource from 'components/ui/PartnerManagedResource'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, CriticalIcon } from 'ui'
|
||||
import DeleteProjectButton from './DeleteProjectButton'
|
||||
|
||||
const DeleteProjectPanel = () => {
|
||||
export const DeleteProjectPanel = () => {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
@@ -21,35 +14,29 @@ const DeleteProjectPanel = () => {
|
||||
return (
|
||||
<section id="delete-project">
|
||||
<FormHeader title="Delete Project" description="" />
|
||||
<Panel>
|
||||
<Panel.Content>
|
||||
{selectedOrganization?.managed_by !== 'vercel-marketplace' ? (
|
||||
<Alert_Shadcn_ variant="destructive">
|
||||
<CriticalIcon />
|
||||
<AlertTitle_Shadcn_>
|
||||
Deleting this project will also remove your database.
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</AlertDescription_Shadcn_>
|
||||
<div className="mt-2">
|
||||
<DeleteProjectButton />
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
) : (
|
||||
<PartnerManagedResource
|
||||
partner="vercel-marketplace"
|
||||
resource="Projects"
|
||||
cta={{
|
||||
installationId: selectedOrganization?.partner_id,
|
||||
message: 'Delete project in Vercel Marketplace',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
{selectedOrganization?.managed_by !== 'vercel-marketplace' ? (
|
||||
<Alert_Shadcn_ variant="destructive">
|
||||
<CriticalIcon />
|
||||
<AlertTitle_Shadcn_>
|
||||
Deleting this project will also remove your database.
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Make sure you have made a backup if you want to keep your data.
|
||||
</AlertDescription_Shadcn_>
|
||||
<div className="mt-2">
|
||||
<DeleteProjectButton />
|
||||
</div>
|
||||
</Alert_Shadcn_>
|
||||
) : (
|
||||
<PartnerManagedResource
|
||||
partner="vercel-marketplace"
|
||||
resource="Projects"
|
||||
cta={{
|
||||
installationId: selectedOrganization?.partner_id,
|
||||
message: 'Delete project in Vercel Marketplace',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeleteProjectPanel
|
||||
@@ -1,7 +1,6 @@
|
||||
export { default as General } from './General'
|
||||
export { default as ComplianceConfig } from './ComplianceConfig/ProjectComplianceMode'
|
||||
export { default as CustomDomainConfig } from './CustomDomainConfig/CustomDomainConfig'
|
||||
export { default as DeleteProjectPanel } from './DeleteProjectPanel/DeleteProjectPanel'
|
||||
export { default as DeleteProjectButton } from './DeleteProjectPanel/DeleteProjectButton'
|
||||
export { default as General } from './General'
|
||||
export { default as TransferProjectButton } from './TransferProjectPanel/TransferProjectButton'
|
||||
export { default as TransferProjectPanel } from './TransferProjectPanel/TransferProjectPanel'
|
||||
export { default as ComplianceConfig } from './ComplianceConfig/ProjectComplianceMode'
|
||||
@@ -146,6 +146,9 @@ const SignInMfaForm = () => {
|
||||
}?`}</a>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<Link href="/logout">Force sign out and clear cookies</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
target="_blank"
|
||||
|
||||
@@ -69,7 +69,7 @@ const DeleteConfirmationDialogs = ({
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: deleteRows } = useTableRowDeleteMutation({
|
||||
const { mutate: deleteRows, isLoading: isDeletingRows } = useTableRowDeleteMutation({
|
||||
onSuccess: () => {
|
||||
if (snap.confirmationDialog?.type === 'row') {
|
||||
snap.confirmationDialog.callback?.()
|
||||
@@ -81,7 +81,7 @@ const DeleteConfirmationDialogs = ({
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: deleteAllRows } = useTableRowDeleteAllMutation({
|
||||
const { mutate: deleteAllRows, isLoading: isDeletingAllRows } = useTableRowDeleteAllMutation({
|
||||
onSuccess: () => {
|
||||
if (snap.confirmationDialog?.type === 'row') {
|
||||
snap.confirmationDialog.callback?.()
|
||||
@@ -96,7 +96,7 @@ const DeleteConfirmationDialogs = ({
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: truncateRows } = useTableRowTruncateMutation({
|
||||
const { mutate: truncateRows, isLoading: isTruncatingRows } = useTableRowTruncateMutation({
|
||||
onSuccess: () => {
|
||||
if (snap.confirmationDialog?.type === 'row') {
|
||||
snap.confirmationDialog.callback?.()
|
||||
@@ -310,6 +310,7 @@ const DeleteConfirmationDialogs = ({
|
||||
confirmLabelLoading="Deleting"
|
||||
onCancel={() => snap.closeConfirmationDialog()}
|
||||
onConfirm={() => onConfirmDeleteRow()}
|
||||
loading={isTruncatingRows || isDeletingRows || isDeletingAllRows}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-foreground-light">
|
||||
|
||||
@@ -30,7 +30,6 @@ const DatabaseProductMenu = () => {
|
||||
const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined
|
||||
const pitrEnabled = addons?.selected_addons.find((addon) => addon.type === 'pitr') !== undefined
|
||||
const columnLevelPrivileges = useIsColumnLevelPrivilegesEnabled()
|
||||
const enablePgReplicate = useFlag('enablePgReplicate')
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -40,7 +39,6 @@ const DatabaseProductMenu = () => {
|
||||
pgNetExtensionExists,
|
||||
pitrEnabled,
|
||||
columnLevelPrivileges,
|
||||
enablePgReplicate,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -9,12 +9,10 @@ export const generateDatabaseMenu = (
|
||||
pgNetExtensionExists: boolean
|
||||
pitrEnabled: boolean
|
||||
columnLevelPrivileges: boolean
|
||||
enablePgReplicate: boolean
|
||||
}
|
||||
): ProductMenuGroup[] => {
|
||||
const ref = project?.ref ?? 'default'
|
||||
const { pgNetExtensionExists, pitrEnabled, columnLevelPrivileges, enablePgReplicate } =
|
||||
flags || {}
|
||||
const { pgNetExtensionExists, pitrEnabled, columnLevelPrivileges } = flags || {}
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -64,16 +62,13 @@ export const generateDatabaseMenu = (
|
||||
url: `/project/${ref}/database/publications`,
|
||||
items: [],
|
||||
},
|
||||
...(enablePgReplicate
|
||||
? [
|
||||
{
|
||||
name: 'Replication',
|
||||
key: 'replication',
|
||||
url: `/project/${ref}/database/replication`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Replication',
|
||||
key: 'replication',
|
||||
url: `/project/${ref}/database/replication`,
|
||||
label: 'Coming Soon',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -160,7 +160,7 @@ export const PauseDisabledState = () => {
|
||||
</DropdownMenuItem> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
<Button asChild type="default" icon={<ExternalLink />} className="my-3">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -169,7 +169,7 @@ export const PauseDisabledState = () => {
|
||||
Restore backup to a new Supabase project guide
|
||||
</a>
|
||||
</Button>
|
||||
<Button asChild type="default" icon={<ExternalLink />}>
|
||||
<Button asChild type="default" icon={<ExternalLink />} className="mb-3">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
|
||||
@@ -51,9 +51,11 @@ export const useProjectSettingsV2Query = <TData = ProjectSettingsData>(
|
||||
({ signal }) => getProjectSettings({ projectRef }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined',
|
||||
refetchInterval(data) {
|
||||
const apiKeys = (data as ProjectSettings)?.service_api_keys ?? []
|
||||
const interval = canReadAPIKeys && apiKeys.length === 0 ? 2000 : 0
|
||||
refetchInterval(_data) {
|
||||
const data = _data as ProjectSettings | undefined
|
||||
const apiKeys = data?.service_api_keys ?? []
|
||||
const interval =
|
||||
canReadAPIKeys && data?.status !== 'INACTIVE' && apiKeys.length === 0 ? 2000 : 0
|
||||
return interval
|
||||
},
|
||||
...options,
|
||||
|
||||
@@ -39,7 +39,11 @@ export async function executeSql<T = any>(
|
||||
| 'isRoleImpersonationEnabled'
|
||||
>,
|
||||
signal?: AbortSignal,
|
||||
headersInit?: HeadersInit
|
||||
headersInit?: HeadersInit,
|
||||
fetcherOverride?: (
|
||||
sql: string,
|
||||
headers?: HeadersInit
|
||||
) => Promise<{ data: T } | { error: ResponseError }>
|
||||
): Promise<{ result: T }> {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
@@ -52,21 +56,37 @@ export async function executeSql<T = any>(
|
||||
let headers = new Headers(headersInit)
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
let { data, error } = await post('/platform/pg-meta/{ref}/query', {
|
||||
signal,
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString ?? '' },
|
||||
path: { ref: projectRef },
|
||||
// @ts-expect-error: This is just a client side thing to identify queries better
|
||||
query: {
|
||||
key:
|
||||
queryKey?.filter((seg) => typeof seg === 'string' || typeof seg === 'number').join('-') ??
|
||||
'',
|
||||
let data
|
||||
let error
|
||||
|
||||
if (fetcherOverride) {
|
||||
const result = await fetcherOverride(sql, headers)
|
||||
if ('data' in result) {
|
||||
data = result.data
|
||||
} else {
|
||||
error = result.error
|
||||
}
|
||||
} else {
|
||||
const result = await post('/platform/pg-meta/{ref}/query', {
|
||||
signal,
|
||||
params: {
|
||||
header: { 'x-connection-encrypted': connectionString ?? '' },
|
||||
path: { ref: projectRef },
|
||||
// @ts-expect-error: This is just a client side thing to identify queries better
|
||||
query: {
|
||||
key:
|
||||
queryKey
|
||||
?.filter((seg) => typeof seg === 'string' || typeof seg === 'number')
|
||||
.join('-') ?? '',
|
||||
},
|
||||
},
|
||||
},
|
||||
body: { query: sql },
|
||||
headers,
|
||||
})
|
||||
body: { query: sql },
|
||||
headers,
|
||||
})
|
||||
|
||||
data = result.data
|
||||
error = result.error
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function createBucket({
|
||||
|
||||
const payload: Partial<CreateStorageBucketBody> = { id, public: isPublic }
|
||||
if (file_size_limit) payload.file_size_limit = file_size_limit
|
||||
if (allowed_mime_types) payload.allowed_mime_types
|
||||
if (allowed_mime_types) payload.allowed_mime_types = allowed_mime_types
|
||||
|
||||
const { data, error } = await post('/platform/storage/{ref}/buckets', {
|
||||
params: { path: { ref: projectRef } },
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { constructHeaders } from 'lib/api/apiHelpers'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { PG_META_URL } from 'lib/constants'
|
||||
import type { ResponseError } from 'types'
|
||||
|
||||
export async function queryPgMetaSelfHosted(sql: string, headersInit?: { [prop: string]: any }) {
|
||||
const headers = constructHeaders(headersInit ?? {})
|
||||
const response = await post(`${PG_META_URL}/query`, { query: sql }, { headers })
|
||||
|
||||
if (response.error) {
|
||||
return { error: response.error as ResponseError }
|
||||
} else {
|
||||
return { data: response.data }
|
||||
}
|
||||
}
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
|
||||
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
|
||||
@@ -164,7 +164,6 @@
|
||||
"@vitest/coverage-v8": "^3.0.9",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"api-types": "workspace:*",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"common": "workspace:*",
|
||||
"config": "workspace:*",
|
||||
"date-fns": "^2.30.0",
|
||||
|
||||
Loaded 100 of 280 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user