mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: refactor database advisors and database wrapper federated content (#48199)
This commit is contained in:
1 parent
cea246d195
commit
075caf314e
13 files changed
+488
-697
No files matched your search
@@ -48,6 +48,8 @@ public/docs/
|
||||
/content/guides/deployment/terraform/tutorial.mdx
|
||||
/content/guides/deployment/ci/
|
||||
/content/guides/ai/python/
|
||||
/content/guides/database/extensions/wrappers/*
|
||||
!/content/guides/database/extensions/wrappers/overview.mdx
|
||||
|
||||
# Downloaded TypeDoc dumps under spec/reference/<lib>/<ver>/. Regenerated by
|
||||
# `cd apps/docs/spec && make download.tsdoc.v2`. Hand-authored files in the
|
||||
|
||||
@@ -1,185 +1,13 @@
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import { TabPanel, Tabs } from '~/features/ui/Tabs'
|
||||
import { linkTransform, UrlTransformFunction } 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 { getGitHubFileContents, octokit, OCTOKIT_RETRY_OPTIONS } from '~/lib/octokit'
|
||||
import { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import { Heading } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
const repo = 'splinter'
|
||||
const branch = 'main'
|
||||
const docsDir = 'docs'
|
||||
|
||||
const meta = {
|
||||
title: 'Performance and Security Advisors',
|
||||
subtitle: 'Check your database for performance and security issues',
|
||||
}
|
||||
|
||||
const generateMetadata = genGuideMeta(() => ({
|
||||
pathname: '/guides/database/database-advisors',
|
||||
meta,
|
||||
}))
|
||||
|
||||
const editLink = newEditLink('supabase/splinter/tree/main/docs')
|
||||
|
||||
const markdownIntro = `
|
||||
You can use the Database Performance and Security Advisors to check your database for issues such as missing indexes and improperly set-up RLS policies.
|
||||
|
||||
## Using the Advisors
|
||||
|
||||
In the dashboard, navigate to [Security Advisor](https://supabase.com/dashboard/project/_/database/security-advisor) and [Performance Advisor](https://supabase.com/dashboard/project/_/database/performance-advisor) under Database. The advisors run automatically. You can also manually rerun them after you've resolved issues.
|
||||
`.trim()
|
||||
|
||||
const getBasename = (path: string) => path.split('/').at(-1)!.replace(/\.md$/, '')
|
||||
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
|
||||
import { genGuideMeta, getGuidesMarkdown } from '~/features/docs/GuidesMdx.utils'
|
||||
|
||||
const DatabaseAdvisorDocs = async () => {
|
||||
let lints: Awaited<ReturnType<typeof getLints>>['lints'] = []
|
||||
let lintsList: Awaited<ReturnType<typeof getLints>>['lintsList'] = []
|
||||
let fetchError: Error | null = null
|
||||
const data = await getGuidesMarkdown(['database', 'database-advisors'])
|
||||
|
||||
try {
|
||||
const data = await getLints()
|
||||
lints = data.lints
|
||||
lintsList = data.lintsList
|
||||
} catch (error) {
|
||||
fetchError = error instanceof Error ? error : new Error('Unknown error fetching advisor docs')
|
||||
console.error('[database-advisors] Failed to fetch advisor docs from GitHub', fetchError)
|
||||
}
|
||||
|
||||
const options = {
|
||||
mdxOptions: {
|
||||
remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]],
|
||||
rehypePlugins: [[linkTransform, urlTransform(lintsList)], rehypeSlug],
|
||||
},
|
||||
} as SerializeOptions
|
||||
|
||||
return (
|
||||
<GuideTemplate meta={meta} editLink={editLink} pathname="/guides/database/database-advisors">
|
||||
<MDXRemoteBase source={markdownIntro} />
|
||||
<Heading tag="h2">Available checks</Heading>
|
||||
|
||||
{fetchError ? (
|
||||
<Admonition type="note" title="Couldn’t load the full Advisor library">
|
||||
We fetch remediation guides straight from the <code>supabase/splinter</code> repository
|
||||
during the build. GitHub timed out just now, so we’re showing the overview only.
|
||||
<br />
|
||||
<br />
|
||||
You can check back in a few minutes or browse the
|
||||
{` `}
|
||||
<a
|
||||
className="underline decoration-dashed underline-offset-2"
|
||||
href="https://github.com/supabase/splinter/tree/main/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
latest Markdown on GitHub (opens in a new tab)
|
||||
</a>
|
||||
.
|
||||
</Admonition>
|
||||
) : (
|
||||
<Tabs listClassNames="flex flex-wrap gap-2 [&>button]:m-0!" queryGroup="lint">
|
||||
{lints.map((lint) => (
|
||||
<TabPanel
|
||||
key={lint.path}
|
||||
id={lint.path}
|
||||
label={capitalize(getBasename(lint.path).replace(/_/g, ' '))}
|
||||
>
|
||||
<section id={getBasename(lint.path)}>
|
||||
<MDXRemoteBase source={lint.content} options={options} />
|
||||
</section>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</GuideTemplate>
|
||||
)
|
||||
return <GuideTemplate {...data!} />
|
||||
}
|
||||
|
||||
/**
|
||||
* The GitHub repo uses relative links, which don't lead to the right locations
|
||||
* in docs.
|
||||
*
|
||||
* @param url The original link, as written in the Markdown file
|
||||
* @returns The rewritten link
|
||||
*/
|
||||
const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (lints) => (url) => {
|
||||
try {
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const relativePath = getBasename(pathname)
|
||||
const section = lints.find(({ path }) => path === relativePath)
|
||||
|
||||
if (section) {
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set('lint', relativePath)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to GitHub repo
|
||||
return `https://github.com/${org}/${repo}/blob/${branch}${pathname}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch lint remediation Markdown from external repo
|
||||
*/
|
||||
const getLints = async () => {
|
||||
const response = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner: org,
|
||||
repo: repo,
|
||||
path: docsDir,
|
||||
ref: branch,
|
||||
headers: {
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
},
|
||||
request: OCTOKIT_RETRY_OPTIONS,
|
||||
})
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(
|
||||
`Failed to fetch ${org}/${repo}/contents/${docsDir} docs from GitHub: ${response.status}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(response.data)) {
|
||||
throw Error(
|
||||
'Reading a directory, not a file. Should not reach this, solely to appease Typescript.'
|
||||
)
|
||||
}
|
||||
|
||||
const lintsList = response.data.filter(({ path }) => /docs\/\d+.+\.md$/.test(path))
|
||||
|
||||
const lints = await Promise.all(
|
||||
lintsList.map(async ({ path }) => {
|
||||
const content = await getGitHubFileContents({ org, repo, path, branch })
|
||||
|
||||
return {
|
||||
path: getBasename(path),
|
||||
content,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return { lints, lintsList }
|
||||
}
|
||||
const generateMetadata = genGuideMeta(() => getGuidesMarkdown(['database', 'database-advisors']))
|
||||
|
||||
export default DatabaseAdvisorDocs
|
||||
export { generateMetadata }
|
||||
@@ -1,345 +1,15 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join, relative } from 'node:path'
|
||||
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
|
||||
import {
|
||||
genGuideMeta,
|
||||
genGuidesStaticParams,
|
||||
removeRedundantH1,
|
||||
getGuidesMarkdown,
|
||||
} from '~/features/docs/GuidesMdx.utils'
|
||||
import { newEditLink } from '~/features/helpers.edit-link'
|
||||
import { Guide, GuideArticle, GuideFooter, GuideHeader, GuideMdxContent } from '~/features/ui/guide'
|
||||
// End of third-party imports
|
||||
|
||||
import { getEmptyArray } from '~/features/helpers.fn'
|
||||
import { IS_DEV } from '~/lib/constants'
|
||||
import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs'
|
||||
import { linkTransform, type UrlTransformFunction } 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 { getGitHubFileContents, octokit } from '~/lib/octokit'
|
||||
import type { SerializeOptions } from '~/types/next-mdx-remote-serialize'
|
||||
import { isFeatureEnabled } from 'common'
|
||||
import matter from 'gray-matter'
|
||||
import Link from 'next/link'
|
||||
import { notFound } from 'next/navigation'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import emoji from 'remark-emoji'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const org = 'supabase'
|
||||
const repo = 'wrappers'
|
||||
const docsDir = 'docs/catalog'
|
||||
const externalSite = 'https://supabase.github.io/wrappers'
|
||||
|
||||
type DocsTagsQueryResponse = {
|
||||
repository: {
|
||||
refs: {
|
||||
nodes: { name: string }[] | null
|
||||
pageInfo: { hasNextPage: boolean; endCursor: string | null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const docsTagsQuery = `
|
||||
query DocsTagsQuery($owner: String!, $name: String!, $after: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
refs(
|
||||
refPrefix: "refs/tags/",
|
||||
orderBy: { field: TAG_COMMIT_DATE, direction: DESC },
|
||||
first: 5,
|
||||
after: $after
|
||||
) {
|
||||
nodes { name }
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
async function getLatestDocsTag(after: string | null = null): Promise<string | null> {
|
||||
try {
|
||||
/**
|
||||
* We use GraphQL as it's the only way to use `orderBy` on Github API.
|
||||
*/
|
||||
const {
|
||||
repository: {
|
||||
refs: {
|
||||
nodes,
|
||||
pageInfo: { hasNextPage, endCursor },
|
||||
},
|
||||
},
|
||||
} = await octokit().graphql<DocsTagsQueryResponse>(docsTagsQuery, {
|
||||
owner: org,
|
||||
name: repo,
|
||||
after,
|
||||
})
|
||||
|
||||
return (
|
||||
nodes?.find(({ name }) => /^docs_v\d+\.\d+\.\d+/.test(name))?.name ??
|
||||
(hasNextPage && endCursor ? await getLatestDocsTag(endCursor) : null)
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error fetching docs tags for wrappers federated pages: ${error}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Each external docs page is mapped to a local page
|
||||
const pageMap = [
|
||||
{
|
||||
slug: 'airtable',
|
||||
meta: {
|
||||
title: 'Airtable',
|
||||
dashboardIntegrationPath: 'airtable_wrapper',
|
||||
},
|
||||
remoteFile: 'airtable.md',
|
||||
},
|
||||
{
|
||||
slug: 'auth0',
|
||||
meta: {
|
||||
title: 'Auth0',
|
||||
dashboardIntegrationPath: 'auth0_wrapper',
|
||||
},
|
||||
remoteFile: 'auth0.md',
|
||||
},
|
||||
{
|
||||
slug: 'bigquery',
|
||||
meta: {
|
||||
title: 'BigQuery',
|
||||
dashboardIntegrationPath: 'bigquery_wrapper',
|
||||
},
|
||||
remoteFile: 'bigquery.md',
|
||||
},
|
||||
{
|
||||
slug: 'cal',
|
||||
meta: {
|
||||
title: 'Cal.com',
|
||||
dashboardIntegrationPath: 'cal_wrapper',
|
||||
},
|
||||
remoteFile: 'cal.md',
|
||||
},
|
||||
{
|
||||
slug: 'calendly',
|
||||
meta: {
|
||||
title: 'Calendly',
|
||||
dashboardIntegrationPath: 'calendly_wrapper',
|
||||
},
|
||||
remoteFile: 'calendly.md',
|
||||
},
|
||||
{
|
||||
slug: 'clerk',
|
||||
meta: {
|
||||
title: 'Clerk',
|
||||
dashboardIntegrationPath: 'clerk_wrapper',
|
||||
},
|
||||
remoteFile: 'clerk.md',
|
||||
},
|
||||
{
|
||||
slug: 'clickhouse',
|
||||
meta: {
|
||||
title: 'ClickHouse',
|
||||
dashboardIntegrationPath: 'clickhouse_wrapper',
|
||||
},
|
||||
remoteFile: 'clickhouse.md',
|
||||
},
|
||||
{
|
||||
slug: 'cloudflare-d1',
|
||||
meta: {
|
||||
title: 'Cloudflare D1',
|
||||
dashboardIntegrationPath: 'cfd1_wrapper',
|
||||
},
|
||||
remoteFile: 'cfd1.md',
|
||||
},
|
||||
{
|
||||
slug: 'cognito',
|
||||
meta: {
|
||||
title: 'AWS Cognito',
|
||||
dashboardIntegrationPath: 'cognito_wrapper',
|
||||
},
|
||||
remoteFile: 'cognito.md',
|
||||
},
|
||||
{
|
||||
slug: 'duckdb',
|
||||
meta: {
|
||||
title: 'DuckDB',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'duckdb.md',
|
||||
},
|
||||
{
|
||||
slug: 'dynamodb',
|
||||
meta: {
|
||||
title: 'AWS DynamoDB',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'dynamodb.md',
|
||||
},
|
||||
{
|
||||
slug: 'firebase',
|
||||
meta: {
|
||||
title: 'Firebase',
|
||||
dashboardIntegrationPath: 'firebase_wrapper',
|
||||
},
|
||||
remoteFile: 'firebase.md',
|
||||
},
|
||||
{
|
||||
slug: 'gravatar',
|
||||
meta: {
|
||||
title: 'Gravatar',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'gravatar.md',
|
||||
},
|
||||
{
|
||||
slug: 'hubspot',
|
||||
meta: {
|
||||
title: 'HubSpot',
|
||||
dashboardIntegrationPath: 'hubspot_wrapper',
|
||||
},
|
||||
remoteFile: 'hubspot.md',
|
||||
},
|
||||
{
|
||||
slug: 'iceberg',
|
||||
meta: {
|
||||
title: 'Iceberg',
|
||||
dashboardIntegrationPath: 'iceberg_wrapper',
|
||||
},
|
||||
remoteFile: 'iceberg.md',
|
||||
},
|
||||
{
|
||||
slug: 'infura',
|
||||
meta: {
|
||||
title: 'Infura',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'infura.md',
|
||||
},
|
||||
{
|
||||
slug: 'logflare',
|
||||
meta: {
|
||||
title: 'Logflare',
|
||||
dashboardIntegrationPath: 'logflare_wrapper',
|
||||
},
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
{
|
||||
slug: 'mongodb',
|
||||
meta: {
|
||||
title: 'MongoDB',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'mongodb.md',
|
||||
},
|
||||
{
|
||||
slug: 'mssql',
|
||||
meta: {
|
||||
title: 'MSSQL',
|
||||
dashboardIntegrationPath: 'mssql_wrapper',
|
||||
},
|
||||
remoteFile: 'mssql.md',
|
||||
},
|
||||
{
|
||||
slug: 'mysql',
|
||||
meta: {
|
||||
title: 'MySQL',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'mysql.md',
|
||||
},
|
||||
{
|
||||
slug: 'notion',
|
||||
meta: {
|
||||
title: 'Notion',
|
||||
dashboardIntegrationPath: 'notion_wrapper',
|
||||
},
|
||||
remoteFile: 'notion.md',
|
||||
},
|
||||
{
|
||||
slug: 'openapi',
|
||||
meta: {
|
||||
title: 'OpenAPI',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'openapi.md',
|
||||
},
|
||||
{
|
||||
slug: 'orb',
|
||||
meta: {
|
||||
title: 'Orb',
|
||||
dashboardIntegrationPath: 'orb_wrapper',
|
||||
},
|
||||
remoteFile: 'orb.md',
|
||||
},
|
||||
{
|
||||
slug: 'paddle',
|
||||
meta: {
|
||||
title: 'Paddle',
|
||||
dashboardIntegrationPath: 'paddle_wrapper',
|
||||
},
|
||||
remoteFile: 'paddle.md',
|
||||
},
|
||||
{
|
||||
slug: 'redis',
|
||||
meta: {
|
||||
title: 'Redis',
|
||||
dashboardIntegrationPath: 'redis_wrapper',
|
||||
},
|
||||
remoteFile: 'redis.md',
|
||||
},
|
||||
{
|
||||
slug: 's3',
|
||||
meta: {
|
||||
title: 'AWS S3',
|
||||
dashboardIntegrationPath: 's3_wrapper',
|
||||
},
|
||||
remoteFile: 's3.md',
|
||||
},
|
||||
{
|
||||
slug: 's3_vectors',
|
||||
meta: {
|
||||
title: 'AWS S3 Vectors',
|
||||
dashboardIntegrationPath: 's3_vectors_wrapper',
|
||||
},
|
||||
remoteFile: 's3vectors.md',
|
||||
},
|
||||
{
|
||||
slug: 'shopify',
|
||||
meta: {
|
||||
title: 'Shopify',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'shopify.md',
|
||||
},
|
||||
{
|
||||
slug: 'slack',
|
||||
meta: {
|
||||
title: 'Slack',
|
||||
dashboardIntegrationPath: undefined,
|
||||
},
|
||||
remoteFile: 'slack.md',
|
||||
},
|
||||
{
|
||||
slug: 'snowflake',
|
||||
meta: {
|
||||
title: 'Snowflake',
|
||||
dashboardIntegrationPath: 'snowflake_wrapper',
|
||||
},
|
||||
remoteFile: 'snowflake.md',
|
||||
},
|
||||
{
|
||||
slug: 'stripe',
|
||||
meta: {
|
||||
title: 'Stripe',
|
||||
dashboardIntegrationPath: 'stripe_wrapper',
|
||||
},
|
||||
remoteFile: 'stripe.md',
|
||||
},
|
||||
]
|
||||
|
||||
interface Params {
|
||||
slug?: string[]
|
||||
}
|
||||
type Params = { slug?: string[] }
|
||||
|
||||
const WrappersDocs = async (props: { params: Promise<Params> }) => {
|
||||
if (!isFeatureEnabled('docs:fdw')) {
|
||||
@@ -347,191 +17,18 @@ const WrappersDocs = async (props: { params: Promise<Params> }) => {
|
||||
}
|
||||
|
||||
const params = await props.params
|
||||
const { isExternal, meta, assetsBaseUrl, ...data } = await getContent(params)
|
||||
const slug = ['database', 'extensions', 'wrappers', ...(params.slug ?? [])]
|
||||
const data = await getGuidesMarkdown(slug)
|
||||
|
||||
// Create a combined URL transformer that handles both regular URLs and asset URLs
|
||||
const combinedUrlTransformer: UrlTransformFunction = (url, node) => {
|
||||
// First try assets URL transformation (starts with ../assets/)
|
||||
const transformedUrl = assetUrlTransform(url, assetsBaseUrl)
|
||||
|
||||
// If URL wasn't changed proceed with regular URL transformation
|
||||
if (transformedUrl === url) {
|
||||
return urlTransform(url, node)
|
||||
}
|
||||
|
||||
return transformedUrl
|
||||
}
|
||||
|
||||
const options = isExternal
|
||||
? ({
|
||||
mdxOptions: {
|
||||
remarkPlugins: [
|
||||
remarkMkDocsAdmonition,
|
||||
emoji,
|
||||
remarkPyMdownTabs,
|
||||
[removeTitle, meta.title],
|
||||
],
|
||||
rehypePlugins: [[linkTransform, combinedUrlTransformer], rehypeSlug],
|
||||
},
|
||||
} as SerializeOptions)
|
||||
: undefined
|
||||
|
||||
const dashboardIntegrationURL = getDashboardIntegrationURL(meta.dashboardIntegrationPath)
|
||||
|
||||
return (
|
||||
<Guide meta={meta}>
|
||||
<GuideArticle>
|
||||
<GuideHeader />
|
||||
|
||||
{dashboardIntegrationURL && (
|
||||
<Admonition type="tip" className="mb-4">
|
||||
<p>You can enable the {meta.title} wrapper right from the Supabase dashboard.</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link href={dashboardIntegrationURL} className="no-underline">
|
||||
Open wrapper in dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)}
|
||||
|
||||
<GuideMdxContent content={data.content} mdxOptions={options} />
|
||||
|
||||
<GuideFooter editLink={data.editLink} />
|
||||
</GuideArticle>
|
||||
</Guide>
|
||||
)
|
||||
return <GuideTemplate {...data!} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch markdown from external repo
|
||||
*/
|
||||
const getContent = async (params: Params) => {
|
||||
const federatedPage = pageMap.find(
|
||||
({ slug }) => params && slug && params.slug && slug === params.slug.at(0)
|
||||
)
|
||||
|
||||
let isExternal: boolean
|
||||
let meta: any
|
||||
let content: string
|
||||
let editLink: string
|
||||
let assetsBaseUrl: string = ''
|
||||
|
||||
if (!federatedPage) {
|
||||
isExternal = false
|
||||
editLink = `supabase/supabase/apps/docs/content/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}.mdx`
|
||||
const rawContent = await readFile(
|
||||
join(
|
||||
GUIDES_DIRECTORY,
|
||||
'database',
|
||||
'extensions',
|
||||
`wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}.mdx`
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
;({ data: meta, content } = matter(rawContent))
|
||||
if (!isValidGuideFrontmatter(meta)) {
|
||||
throw Error(`Expected valid frontmatter, got ${JSON.stringify(meta, null, 2)}`)
|
||||
}
|
||||
} else {
|
||||
isExternal = true
|
||||
let remoteFile: string
|
||||
;({ remoteFile, meta } = federatedPage)
|
||||
|
||||
const tag = await getLatestDocsTag()
|
||||
|
||||
if (!tag) {
|
||||
throw new Error('No latest docs tag found for federated wrappers pages')
|
||||
}
|
||||
|
||||
editLink = `${org}/${repo}/blob/${tag}/${docsDir}/${remoteFile}`
|
||||
|
||||
let rawContent = await getGitHubFileContents({
|
||||
org,
|
||||
repo,
|
||||
path: `${docsDir}/${remoteFile}`,
|
||||
branch: tag,
|
||||
})
|
||||
|
||||
assetsBaseUrl = `https://raw.githubusercontent.com/${org}/${repo}/${tag}/docs/assets/`
|
||||
|
||||
const { content: contentWithoutFrontmatter } = matter(rawContent)
|
||||
content = removeRedundantH1(contentWithoutFrontmatter)
|
||||
}
|
||||
|
||||
return {
|
||||
pathname:
|
||||
`/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}` satisfies `/${string}`,
|
||||
isExternal,
|
||||
editLink: newEditLink(editLink),
|
||||
meta,
|
||||
content,
|
||||
assetsBaseUrl,
|
||||
}
|
||||
}
|
||||
|
||||
const getDashboardIntegrationURL = (wrapperPath?: string) => {
|
||||
return wrapperPath
|
||||
? `https://supabase.com/dashboard/project/_/integrations/${wrapperPath}/overview`
|
||||
: null
|
||||
}
|
||||
|
||||
const assetUrlTransform = (url: string, baseUrl: string): string => {
|
||||
const assetPattern = /(\.\.\/)+assets\//
|
||||
|
||||
if (assetPattern.test(url)) {
|
||||
return url.replace(assetPattern, baseUrl)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
const urlTransform: UrlTransformFunction = (url) => {
|
||||
try {
|
||||
const externalSiteUrl = new URL(externalSite)
|
||||
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
// Don't modify a url with a FQDN or a url that's only a hash
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
const relativePage = (
|
||||
pathname.endsWith('.md')
|
||||
? pathname.replace(/\.md$/, '')
|
||||
: relative(externalSiteUrl.pathname, pathname)
|
||||
).replace(/^\//, '')
|
||||
|
||||
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
|
||||
|
||||
// If we have a mapping for this page, use the mapped path
|
||||
if (page) {
|
||||
return page.slug + hash
|
||||
}
|
||||
|
||||
// If we don't have this page in our docs, link to original docs
|
||||
return `${externalSite}/${relativePage}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
const generateStaticParams = async () => {
|
||||
if (IS_DEV) {
|
||||
return []
|
||||
}
|
||||
|
||||
const mdxPaths = await genGuidesStaticParams('database/extensions/wrappers')()
|
||||
const federatedPaths = pageMap.map(({ slug }) => ({
|
||||
slug: [slug],
|
||||
}))
|
||||
|
||||
return [...mdxPaths, ...federatedPaths]
|
||||
}
|
||||
|
||||
const generateMetadata = genGuideMeta(getContent)
|
||||
const generateStaticParams = !IS_DEV
|
||||
? genGuidesStaticParams('database/extensions/wrappers')
|
||||
: getEmptyArray
|
||||
const generateMetadata = genGuideMeta((params: { slug?: string[] }) =>
|
||||
getGuidesMarkdown(['database', 'extensions', 'wrappers', ...(params.slug ?? [])])
|
||||
)
|
||||
|
||||
export default WrappersDocs
|
||||
export { generateMetadata, generateStaticParams }
|
||||
@@ -0,0 +1,34 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { MDXRemoteBase } from '~/features/docs/MdxBase'
|
||||
import { TabPanel, Tabs } from '~/features/ui/Tabs'
|
||||
import { GENERATED_DIRECTORY } from '~/lib/docs'
|
||||
import { capitalize } from 'lodash-es'
|
||||
|
||||
interface Lint {
|
||||
path: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export async function DatabaseAdvisorsIndex() {
|
||||
let lints: Lint[] = []
|
||||
|
||||
try {
|
||||
const raw = await readFile(join(GENERATED_DIRECTORY, 'database-advisors.json'), 'utf-8')
|
||||
lints = JSON.parse(raw)
|
||||
} catch (error) {
|
||||
throw error('[database-advisors] Failed to read generated advisor docs', error)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs listClassNames="flex flex-wrap gap-2 [&>button]:m-0!" queryGroup="lint">
|
||||
{lints.map((lint) => (
|
||||
<TabPanel key={lint.path} id={lint.path} label={capitalize(lint.path.replace(/_/g, ' '))}>
|
||||
<section id={lint.path}>
|
||||
<MDXRemoteBase source={lint.content} />
|
||||
</section>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
import { Admonition } from 'ui-patterns/admonition'
|
||||
|
||||
export function WrapperDashboardIntegration({ title, path }: { title: string; path: string }) {
|
||||
return (
|
||||
<Admonition type="tip" className="mb-4">
|
||||
<p>You can enable the {title} wrapper right from the Supabase dashboard.</p>
|
||||
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={`https://supabase.com/dashboard/project/_/integrations/${path}/overview`}
|
||||
className="no-underline"
|
||||
>
|
||||
Open wrapper in dashboard
|
||||
</Link>
|
||||
</Button>
|
||||
</Admonition>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Performance and Security Advisors
|
||||
subtitle: Check your database for performance and security issues
|
||||
---
|
||||
|
||||
You can use the Database Performance and Security Advisors to check your database for issues such as missing indexes and improperly set-up RLS policies.
|
||||
|
||||
## Using the advisors
|
||||
|
||||
In the dashboard, navigate to [Security Advisor](/dashboard/project/_/database/security-advisor) and [Performance Advisor](dashboard/project/_/database/performance-advisor) under Database. The advisors run automatically. You can also manually rerun them after you've resolved issues.
|
||||
|
||||
## Available checks
|
||||
|
||||
<DatabaseAdvisorsIndex />
|
||||
@@ -7,6 +7,7 @@ import ButtonCard from '~/components/ButtonCard'
|
||||
import { ComputeDiskLimitsTable } from '~/components/ComputeDiskLimitsTable'
|
||||
import { ContentListings } from '~/components/ContentListings'
|
||||
import { CustomContent } from '~/components/CustomContent'
|
||||
import { DatabaseAdvisorsIndex } from '~/components/DatabaseAdvisorsIndex'
|
||||
import { Extensions } from '~/components/Extensions'
|
||||
import Image, { type ImageProps } from '~/components/Image'
|
||||
import { McpCiConfigBlock } from '~/components/McpCiConfigBlock'
|
||||
@@ -20,6 +21,7 @@ import { RegionsList, SmartRegionsList } from '~/components/RegionsList'
|
||||
import { SharedData } from '~/components/SharedData'
|
||||
import StepHikeCompact from '~/components/StepHikeCompact'
|
||||
import { TerraformProviderSchema } from '~/components/TerraformProviderSchema'
|
||||
import { WrapperDashboardIntegration } from '~/components/WrapperDashboardIntegration'
|
||||
import { CodeSampleDummy, CodeSampleWrapper } from '~/features/directives/CodeSample.client'
|
||||
import { NamedCodeBlock } from '~/features/directives/CodeTabs.components'
|
||||
import { MdxAnchor } from '~/features/docs/MdxAnchor'
|
||||
@@ -82,6 +84,7 @@ const components = {
|
||||
ComputeDiskLimitsTable,
|
||||
CustomContent,
|
||||
ContentListings,
|
||||
DatabaseAdvisorsIndex,
|
||||
ErrorCodes,
|
||||
Extensions,
|
||||
GlassPanel,
|
||||
@@ -113,6 +116,7 @@ const components = {
|
||||
Tabs,
|
||||
TabPanel,
|
||||
TerraformProviderSchema,
|
||||
WrapperDashboardIntegration,
|
||||
InfoTooltip,
|
||||
a: MdxAnchor,
|
||||
h2: (props: ComponentPropsWithoutRef<'h2'>) => (
|
||||
|
||||
@@ -20,6 +20,7 @@ import { AuthProviders } from './markdown-schema/AuthProviders'
|
||||
import { ComputeDiskLimitsTable } from './markdown-schema/ComputeDiskLimitsTable'
|
||||
import { ContentListings } from './markdown-schema/ContentListings'
|
||||
import { CustomContent } from './markdown-schema/CustomContent'
|
||||
import { DatabaseAdvisorsIndex } from './markdown-schema/DatabaseAdvisorsIndex'
|
||||
import { ErrorCodes } from './markdown-schema/ErrorCodes'
|
||||
import { IconCheck, IconX } from './markdown-schema/Icons'
|
||||
import { Image } from './markdown-schema/Image'
|
||||
@@ -36,6 +37,7 @@ import { SharedData } from './markdown-schema/SharedData'
|
||||
import { StepHike } from './markdown-schema/StepHike'
|
||||
import { TabPanel } from './markdown-schema/TabPanel'
|
||||
import { TerraformProviderSchema } from './markdown-schema/TerraformProviderSchema'
|
||||
import { WrapperDashboardIntegration } from './markdown-schema/WrapperDashboardIntegration'
|
||||
import {
|
||||
collectMarkdownSources,
|
||||
type FrontmatterFormat,
|
||||
@@ -173,6 +175,7 @@ const SCHEMA: ComponentSchema = {
|
||||
AuthProviders,
|
||||
ComputeDiskLimitsTable,
|
||||
CustomContent,
|
||||
DatabaseAdvisorsIndex,
|
||||
ErrorCodes,
|
||||
Link,
|
||||
McpCiConfigBlock,
|
||||
@@ -191,6 +194,7 @@ const SCHEMA: ComponentSchema = {
|
||||
NavData,
|
||||
SharedData,
|
||||
TerraformProviderSchema,
|
||||
WrapperDashboardIntegration,
|
||||
}
|
||||
|
||||
function parseFrontmatter(raw: string, frontmatter: FrontmatterFormat) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const ADVISORS_PATH = path.join(process.cwd(), 'features/docs/generated/database-advisors.json')
|
||||
|
||||
interface Lint {
|
||||
path: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export const DatabaseAdvisorsIndex = (): string => {
|
||||
const lints: Lint[] = JSON.parse(readFileSync(ADVISORS_PATH, 'utf-8'))
|
||||
|
||||
return lints
|
||||
.map(
|
||||
(lint) => `### ${lint.path}
|
||||
|
||||
${lint.content}`
|
||||
)
|
||||
.join('\n\n')
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const WrapperDashboardIntegration = ({
|
||||
props,
|
||||
}: {
|
||||
props: Record<string, unknown>
|
||||
}): string => {
|
||||
const title = props.title ? String(props.title) : 'this'
|
||||
const path = String(props.path ?? '')
|
||||
return `> You can enable the ${title} wrapper right from the [Supabase dashboard](https://supabase.com/dashboard/project/_/integrations/${path}/overview).`
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import { mdxFromMarkdown, mdxToMarkdown } from 'mdast-util-mdx'
|
||||
import { toMarkdown } from 'mdast-util-to-markdown'
|
||||
import { gfm } from 'micromark-extension-gfm'
|
||||
import { mdxjs } from 'micromark-extension-mdxjs'
|
||||
import emoji from 'remark-emoji'
|
||||
import { visit } from 'unist-util-visit'
|
||||
|
||||
import type { FederatedContentSource, FederatedPage } from './types'
|
||||
@@ -34,6 +35,9 @@ const STRINGIFY_OPTIONS = {
|
||||
listItemIndent: 'one' as const,
|
||||
fences: true,
|
||||
}
|
||||
// remark-emoji's attacher is typed with a unified `this: Processor` context
|
||||
// it never actually uses; cast it to the plain transformer factory it is.
|
||||
const emojiTransform = (emoji as unknown as () => (tree: any) => void)()
|
||||
|
||||
/**
|
||||
* Discovers every `FederatedContentSource` under `./sources`.
|
||||
@@ -56,11 +60,76 @@ function remotePath(source: FederatedContentSource, page: FederatedPage): string
|
||||
return page.useRoot ? page.remoteFile : `${source.docsDir}/${page.remoteFile}`
|
||||
}
|
||||
|
||||
type LatestTagQueryResponse = {
|
||||
repository: {
|
||||
refs: {
|
||||
nodes: { name: string }[] | null
|
||||
pageInfo: { hasNextPage: boolean; endCursor: string | null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const LATEST_TAG_QUERY = `
|
||||
query LatestTagQuery($owner: String!, $name: String!, $after: String) {
|
||||
repository(owner: $owner, name: $name) {
|
||||
refs(
|
||||
refPrefix: "refs/tags/",
|
||||
orderBy: { field: TAG_COMMIT_DATE, direction: DESC },
|
||||
first: 20,
|
||||
after: $after
|
||||
) {
|
||||
nodes { name }
|
||||
pageInfo { hasNextPage endCursor }
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Resolves a source's `latestTag.pattern` to the newest matching tag name.
|
||||
* GraphQL is required here since the REST API can't order tags by date.
|
||||
*/
|
||||
async function resolveLatestTag(
|
||||
source: FederatedContentSource,
|
||||
after: string | null = null
|
||||
): Promise<string> {
|
||||
const pattern = new RegExp(source.latestTag!.pattern)
|
||||
|
||||
const {
|
||||
repository: {
|
||||
refs: {
|
||||
nodes,
|
||||
pageInfo: { hasNextPage, endCursor },
|
||||
},
|
||||
},
|
||||
} = await octokit().graphql<LatestTagQueryResponse>(LATEST_TAG_QUERY, {
|
||||
owner: source.org,
|
||||
name: source.repo,
|
||||
after,
|
||||
})
|
||||
|
||||
const tag = nodes?.find(({ name }) => pattern.test(name))?.name
|
||||
if (tag) return tag
|
||||
if (hasNextPage && endCursor) return resolveLatestTag(source, endCursor)
|
||||
|
||||
throw new Error(
|
||||
`No tag matching ${source.latestTag!.pattern} found for ${source.org}/${source.repo}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites a link URL: pages mapped in `source.pageMap` go to their local
|
||||
* `/guides/<section>` route, everything else falls back to `externalSite`.
|
||||
*/
|
||||
function transformUrl(source: FederatedContentSource, url: string): string {
|
||||
const assetPattern = /(\.\.\/)+assets\//
|
||||
if (source.assetsDir && assetPattern.test(url)) {
|
||||
return url.replace(
|
||||
assetPattern,
|
||||
`https://raw.githubusercontent.com/${source.org}/${source.repo}/${source.branch}/${source.assetsDir}/`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
@@ -104,9 +173,12 @@ async function fetchPage(source: FederatedContentSource, page: FederatedPage): P
|
||||
branch: source.branch,
|
||||
})
|
||||
|
||||
const tree = fromMarkdown(raw, PARSE_OPTIONS)
|
||||
// Strip the source file's own frontmatter, if any; we build our own from
|
||||
// `page.meta` below rather than merging it in.
|
||||
const tree = fromMarkdown(matter(raw).content, PARSE_OPTIONS)
|
||||
remarkMkDocsAdmonition()(tree)
|
||||
remarkPyMdownTabs()(tree)
|
||||
emojiTransform(tree)
|
||||
if (page.dropLeadingHeading) {
|
||||
const [firstNode] = tree.children
|
||||
if (firstNode?.type === 'heading' && firstNode.depth === 1) tree.children.splice(0, 1)
|
||||
@@ -116,7 +188,10 @@ async function fetchPage(source: FederatedContentSource, page: FederatedPage): P
|
||||
visit(tree, ['link', 'image', 'definition'], (node: any) => {
|
||||
node.url = transformUrl(source, node.url)
|
||||
})
|
||||
const content = toMarkdown(tree, STRINGIFY_OPTIONS).trim()
|
||||
let content = toMarkdown(tree, STRINGIFY_OPTIONS).trim()
|
||||
if (page.meta.dashboardIntegrationPath) {
|
||||
content = `<WrapperDashboardIntegration title="${page.meta.title}" path="${page.meta.dashboardIntegrationPath}" />\n\n${content}`
|
||||
}
|
||||
|
||||
const frontmatter: Record<string, string> = {
|
||||
title: page.meta.title,
|
||||
@@ -146,7 +221,11 @@ async function fetchRawFile(
|
||||
await writeFile(join(GENERATED_DIRECTORY, rawFile.outFile), content)
|
||||
}
|
||||
|
||||
async function fetchSource(source: FederatedContentSource): Promise<void> {
|
||||
async function fetchSource(baseSource: FederatedContentSource): Promise<void> {
|
||||
const source = baseSource.latestTag
|
||||
? { ...baseSource, branch: await resolveLatestTag(baseSource) }
|
||||
: baseSource
|
||||
|
||||
await mkdir(join(GUIDES_DIRECTORY, source.section), { recursive: true })
|
||||
|
||||
await Promise.all([
|
||||
@@ -214,10 +293,99 @@ async function fetchAiSkills(): Promise<void> {
|
||||
await writeFile(join(GENERATED_DIRECTORY, 'ai-skills.json'), JSON.stringify(skills, null, 2))
|
||||
}
|
||||
|
||||
const SPLINTER_REPO = {
|
||||
org: 'supabase',
|
||||
repo: 'splinter',
|
||||
branch: 'main',
|
||||
docsDir: 'docs',
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites a splinter lint doc's link: cross-references to other lints
|
||||
* become `?lint=<path>` (matching `Tabs`'s `queryGroup` tab-switching), and
|
||||
* everything else falls back to viewing the file on GitHub.
|
||||
*/
|
||||
function splinterUrlTransform(lintPaths: string[]) {
|
||||
return (url: string): string => {
|
||||
try {
|
||||
const placeholderHostname = 'placeholder'
|
||||
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
|
||||
|
||||
if (hostname !== placeholderHostname || pathname === '/') {
|
||||
return url
|
||||
}
|
||||
|
||||
const basename = pathname.split('/').at(-1)!.replace(/\.md$/, '')
|
||||
|
||||
if (lintPaths.includes(basename)) {
|
||||
return `?lint=${basename}${hash}`
|
||||
}
|
||||
|
||||
return `https://github.com/${SPLINTER_REPO.org}/${SPLINTER_REPO.repo}/blob/${SPLINTER_REPO.branch}${pathname}${hash}`
|
||||
} catch (err) {
|
||||
console.error('Error transforming markdown URL', err)
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the numbered lint docs in the splinter repo, transforms each the
|
||||
* same way as a guide page, and writes them to
|
||||
* `features/docs/generated/database-advisors.json` for the
|
||||
* `DatabaseAdvisorsIndex` component to read.
|
||||
*/
|
||||
async function fetchDatabaseAdvisors(): Promise<void> {
|
||||
const { data: contents } = await octokit().request('GET /repos/{owner}/{repo}/contents/{path}', {
|
||||
owner: SPLINTER_REPO.org,
|
||||
repo: SPLINTER_REPO.repo,
|
||||
path: SPLINTER_REPO.docsDir,
|
||||
ref: SPLINTER_REPO.branch,
|
||||
request: OCTOKIT_RETRY_OPTIONS,
|
||||
})
|
||||
|
||||
if (!Array.isArray(contents)) {
|
||||
throw new Error('Expected directory listing from GitHub splinter repo')
|
||||
}
|
||||
|
||||
const lintFiles = contents.filter(({ path }) => /docs\/\d+.+\.md$/.test(path))
|
||||
const lintPaths = lintFiles.map(({ path }) => path.split('/').at(-1)!.replace(/\.md$/, ''))
|
||||
const urlTransform = splinterUrlTransform(lintPaths)
|
||||
|
||||
const lints = await Promise.all(
|
||||
lintFiles.map(async ({ path }) => {
|
||||
const raw = await getGitHubFileContents({
|
||||
org: SPLINTER_REPO.org,
|
||||
repo: SPLINTER_REPO.repo,
|
||||
path,
|
||||
branch: SPLINTER_REPO.branch,
|
||||
})
|
||||
|
||||
const tree = fromMarkdown(matter(raw).content, PARSE_OPTIONS)
|
||||
remarkMkDocsAdmonition()(tree)
|
||||
remarkPyMdownTabs()(tree)
|
||||
visit(tree, ['link', 'image', 'definition'], (node: any) => {
|
||||
node.url = urlTransform(node.url)
|
||||
})
|
||||
|
||||
return {
|
||||
path: path.split('/').at(-1)!.replace(/\.md$/, ''),
|
||||
content: toMarkdown(tree, STRINGIFY_OPTIONS).trim(),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
await mkdir(GENERATED_DIRECTORY, { recursive: true })
|
||||
await writeFile(
|
||||
join(GENERATED_DIRECTORY, 'database-advisors.json'),
|
||||
JSON.stringify(lints, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchFederatedContent() {
|
||||
const sources = await loadSources()
|
||||
|
||||
await Promise.all([...sources.map(fetchSource), fetchAiSkills()])
|
||||
await Promise.all([...sources.map(fetchSource), fetchAiSkills(), fetchDatabaseAdvisors()])
|
||||
|
||||
const pageCount = sources.reduce((sum, source) => sum + source.pageMap.length, 0)
|
||||
console.log(
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import type { FederatedContentSource } from '../types'
|
||||
|
||||
// We fetch these docs at build time from an external repo
|
||||
const wrappers: FederatedContentSource = {
|
||||
section: 'database/extensions/wrappers',
|
||||
org: 'supabase',
|
||||
repo: 'wrappers',
|
||||
branch: 'main',
|
||||
// The wrappers repo tags its docs releases separately from code.
|
||||
latestTag: { pattern: '^docs_v\\d+\\.\\d+\\.\\d+' },
|
||||
docsDir: 'docs/catalog',
|
||||
assetsDir: 'docs/assets',
|
||||
externalSite: 'https://supabase.github.io/wrappers',
|
||||
pageMap: [
|
||||
{
|
||||
slug: 'airtable',
|
||||
meta: { title: 'Airtable', dashboardIntegrationPath: 'airtable_wrapper' },
|
||||
remoteFile: 'airtable.md',
|
||||
},
|
||||
{
|
||||
slug: 'auth0',
|
||||
meta: { title: 'Auth0', dashboardIntegrationPath: 'auth0_wrapper' },
|
||||
remoteFile: 'auth0.md',
|
||||
},
|
||||
{
|
||||
slug: 'bigquery',
|
||||
meta: { title: 'BigQuery', dashboardIntegrationPath: 'bigquery_wrapper' },
|
||||
remoteFile: 'bigquery.md',
|
||||
},
|
||||
{
|
||||
slug: 'cal',
|
||||
meta: { title: 'Cal.com', dashboardIntegrationPath: 'cal_wrapper' },
|
||||
remoteFile: 'cal.md',
|
||||
},
|
||||
{
|
||||
slug: 'calendly',
|
||||
meta: { title: 'Calendly', dashboardIntegrationPath: 'calendly_wrapper' },
|
||||
remoteFile: 'calendly.md',
|
||||
},
|
||||
{
|
||||
slug: 'clerk',
|
||||
meta: { title: 'Clerk', dashboardIntegrationPath: 'clerk_wrapper' },
|
||||
remoteFile: 'clerk.md',
|
||||
},
|
||||
{
|
||||
slug: 'clickhouse',
|
||||
meta: { title: 'ClickHouse', dashboardIntegrationPath: 'clickhouse_wrapper' },
|
||||
remoteFile: 'clickhouse.md',
|
||||
},
|
||||
{
|
||||
slug: 'cloudflare-d1',
|
||||
meta: { title: 'Cloudflare D1', dashboardIntegrationPath: 'cfd1_wrapper' },
|
||||
remoteFile: 'cfd1.md',
|
||||
},
|
||||
{
|
||||
slug: 'cognito',
|
||||
meta: { title: 'AWS Cognito', dashboardIntegrationPath: 'cognito_wrapper' },
|
||||
remoteFile: 'cognito.md',
|
||||
},
|
||||
{
|
||||
slug: 'duckdb',
|
||||
meta: { title: 'DuckDB' },
|
||||
remoteFile: 'duckdb.md',
|
||||
},
|
||||
{
|
||||
slug: 'dynamodb',
|
||||
meta: { title: 'AWS DynamoDB' },
|
||||
remoteFile: 'dynamodb.md',
|
||||
},
|
||||
{
|
||||
slug: 'firebase',
|
||||
meta: { title: 'Firebase', dashboardIntegrationPath: 'firebase_wrapper' },
|
||||
remoteFile: 'firebase.md',
|
||||
},
|
||||
{
|
||||
slug: 'gravatar',
|
||||
meta: { title: 'Gravatar' },
|
||||
remoteFile: 'gravatar.md',
|
||||
},
|
||||
{
|
||||
slug: 'hubspot',
|
||||
meta: { title: 'HubSpot', dashboardIntegrationPath: 'hubspot_wrapper' },
|
||||
remoteFile: 'hubspot.md',
|
||||
},
|
||||
{
|
||||
slug: 'iceberg',
|
||||
meta: { title: 'Iceberg', dashboardIntegrationPath: 'iceberg_wrapper' },
|
||||
remoteFile: 'iceberg.md',
|
||||
},
|
||||
{
|
||||
slug: 'infura',
|
||||
meta: { title: 'Infura' },
|
||||
remoteFile: 'infura.md',
|
||||
},
|
||||
{
|
||||
slug: 'logflare',
|
||||
meta: { title: 'Logflare', dashboardIntegrationPath: 'logflare_wrapper' },
|
||||
remoteFile: 'logflare.md',
|
||||
},
|
||||
{
|
||||
slug: 'mongodb',
|
||||
meta: { title: 'MongoDB' },
|
||||
remoteFile: 'mongodb.md',
|
||||
},
|
||||
{
|
||||
slug: 'mssql',
|
||||
meta: { title: 'MSSQL', dashboardIntegrationPath: 'mssql_wrapper' },
|
||||
remoteFile: 'mssql.md',
|
||||
},
|
||||
{
|
||||
slug: 'mysql',
|
||||
meta: { title: 'MySQL' },
|
||||
remoteFile: 'mysql.md',
|
||||
},
|
||||
{
|
||||
slug: 'notion',
|
||||
meta: { title: 'Notion', dashboardIntegrationPath: 'notion_wrapper' },
|
||||
remoteFile: 'notion.md',
|
||||
},
|
||||
{
|
||||
slug: 'openapi',
|
||||
meta: { title: 'OpenAPI' },
|
||||
remoteFile: 'openapi.md',
|
||||
},
|
||||
{
|
||||
slug: 'orb',
|
||||
meta: { title: 'Orb', dashboardIntegrationPath: 'orb_wrapper' },
|
||||
remoteFile: 'orb.md',
|
||||
},
|
||||
{
|
||||
slug: 'paddle',
|
||||
meta: { title: 'Paddle', dashboardIntegrationPath: 'paddle_wrapper' },
|
||||
remoteFile: 'paddle.md',
|
||||
},
|
||||
{
|
||||
slug: 'redis',
|
||||
meta: { title: 'Redis', dashboardIntegrationPath: 'redis_wrapper' },
|
||||
remoteFile: 'redis.md',
|
||||
},
|
||||
{
|
||||
slug: 's3',
|
||||
meta: { title: 'AWS S3', dashboardIntegrationPath: 's3_wrapper' },
|
||||
remoteFile: 's3.md',
|
||||
},
|
||||
{
|
||||
slug: 's3_vectors',
|
||||
meta: { title: 'AWS S3 Vectors', dashboardIntegrationPath: 's3_vectors_wrapper' },
|
||||
remoteFile: 's3vectors.md',
|
||||
},
|
||||
{
|
||||
slug: 'shopify',
|
||||
meta: { title: 'Shopify' },
|
||||
remoteFile: 'shopify.md',
|
||||
},
|
||||
{
|
||||
slug: 'slack',
|
||||
meta: { title: 'Slack' },
|
||||
remoteFile: 'slack.md',
|
||||
},
|
||||
{
|
||||
slug: 'snowflake',
|
||||
meta: { title: 'Snowflake', dashboardIntegrationPath: 'snowflake_wrapper' },
|
||||
remoteFile: 'snowflake.md',
|
||||
},
|
||||
{
|
||||
slug: 'stripe',
|
||||
meta: { title: 'Stripe', dashboardIntegrationPath: 'stripe_wrapper' },
|
||||
remoteFile: 'stripe.md',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export default wrappers
|
||||
@@ -9,6 +9,8 @@ export interface FederatedPage {
|
||||
subtitle?: string
|
||||
description?: string
|
||||
tocVideo?: string
|
||||
/** Path segment for the "Enable in dashboard" integration link, if any. */
|
||||
dashboardIntegrationPath?: string
|
||||
}
|
||||
/** Path of the file in the remote repo, relative to `docsDir`. */
|
||||
remoteFile: string
|
||||
@@ -39,6 +41,21 @@ export interface FederatedContentSource {
|
||||
docsDir: string
|
||||
/** Public site the remote docs are also published to, used to resolve unmapped links. */
|
||||
externalSite: string
|
||||
/**
|
||||
* Resolve `branch` to the latest matching release tag at fetch time,
|
||||
* instead of using a fixed branch name. Useful for repos that tag docs
|
||||
* releases separately from code (e.g. `docs_v1.2.3`).
|
||||
*/
|
||||
latestTag?: {
|
||||
/** Regex (as a string) tested against each tag name, newest first. */
|
||||
pattern: string
|
||||
}
|
||||
/**
|
||||
* Directory (relative to the repo root) holding image assets referenced by
|
||||
* pages via `../assets/...`-style relative links. When set, such links are
|
||||
* rewritten to `https://raw.githubusercontent.com/<org>/<repo>/<branch>/<assetsDir>/...`.
|
||||
*/
|
||||
assetsDir?: string
|
||||
/**
|
||||
* Unmapped links fall back to `${externalSite}/${relativePath}${hash}`. Set
|
||||
* this when `externalSite` is a source-controlled host (e.g. a GitHub blob
|
||||
|
||||
Reference in new issue
Block a user