mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(docs): ai prompts section (#30342)
Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
This commit is contained in:
1 parent
72910fc30f
commit
2fb8b9f74c
18 files changed
+438
-50
No files matched your search
+96
@@ -0,0 +1,96 @@
|
||||
import type { Code, Heading, Root } from 'mdast'
|
||||
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||
import { toMarkdown } from 'mdast-util-to-markdown'
|
||||
import { readdir, readFile, stat } from 'node:fs/promises'
|
||||
import { basename, extname, join } from 'node:path'
|
||||
import { cache } from 'react'
|
||||
import { visit, EXIT } from 'unist-util-visit'
|
||||
|
||||
import { EXAMPLES_DIRECTORY } from '~/lib/docs'
|
||||
|
||||
const PROMPTS_DIRECTORY = join(EXAMPLES_DIRECTORY, 'prompts')
|
||||
|
||||
function parseMarkdown(markdown: string) {
|
||||
const mdast = fromMarkdown(markdown)
|
||||
|
||||
let heading = ''
|
||||
visit(mdast, 'heading', (node: Heading) => {
|
||||
if (node.depth === 1) {
|
||||
if ('value' in node.children[0]) {
|
||||
heading = node.children[0].value
|
||||
}
|
||||
return EXIT
|
||||
}
|
||||
})
|
||||
|
||||
const codeBlock: Code = {
|
||||
type: 'code',
|
||||
lang: 'markdown',
|
||||
value: markdown,
|
||||
}
|
||||
const root: Root = {
|
||||
type: 'root',
|
||||
children: [codeBlock],
|
||||
}
|
||||
const content = toMarkdown(root)
|
||||
|
||||
return { heading, content }
|
||||
}
|
||||
|
||||
async function getAiPromptsImpl() {
|
||||
const directoryContents = await readdir(PROMPTS_DIRECTORY)
|
||||
|
||||
const prompts = directoryContents
|
||||
.filter(async (file) => {
|
||||
if (extname(file) !== '.md') {
|
||||
return false
|
||||
}
|
||||
|
||||
const fileStats = await stat(join(PROMPTS_DIRECTORY, file))
|
||||
const isFile = fileStats.isFile()
|
||||
return isFile
|
||||
})
|
||||
.map(async (filename) => {
|
||||
const rawContent = await readFile(join(PROMPTS_DIRECTORY, filename), 'utf-8')
|
||||
const { heading, content } = parseMarkdown(rawContent)
|
||||
|
||||
return {
|
||||
filename: basename(filename, '.md'),
|
||||
heading,
|
||||
content,
|
||||
}
|
||||
})
|
||||
|
||||
return (await Promise.all(prompts)).sort((a, b) => b.filename.localeCompare(a.filename))
|
||||
}
|
||||
export const getAiPrompts = cache(getAiPromptsImpl)
|
||||
|
||||
async function getAiPromptImpl(prompt: string) {
|
||||
const filePath = join(PROMPTS_DIRECTORY, `${prompt}.md`)
|
||||
try {
|
||||
const rawContent = await readFile(filePath, 'utf-8')
|
||||
const { heading, content } = parseMarkdown(rawContent)
|
||||
return { heading, content }
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch prompt from repo: %o', err)
|
||||
}
|
||||
}
|
||||
export const getAiPrompt = cache(getAiPromptImpl)
|
||||
|
||||
export async function generateAiPromptMetadata({ params: { slug } }: { params: { slug: string } }) {
|
||||
const prompt = await getAiPrompt(slug)
|
||||
|
||||
return {
|
||||
title: `AI Prompt: ${prompt.heading} | Supabase Docs`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateAiPromptsStaticParams() {
|
||||
const prompts = await getAiPrompts()
|
||||
|
||||
return prompts.map((prompt) => {
|
||||
return {
|
||||
slug: prompt.filename,
|
||||
}
|
||||
})
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { GlassPanel } from 'ui-patterns'
|
||||
|
||||
import { getAiPrompts } from './AiPrompts.utils'
|
||||
|
||||
export async function AiPromptsIndex() {
|
||||
const prompts = await getAiPrompts()
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-6 not-prose">
|
||||
{prompts.map((prompt) => (
|
||||
<Link
|
||||
key={prompt.filename}
|
||||
href={`/guides/getting-started/ai-prompts/${prompt.filename}`}
|
||||
passHref
|
||||
>
|
||||
<GlassPanel
|
||||
key={prompt.filename}
|
||||
title={prompt.heading ?? prompt.filename.replaceAll('-', ' ')}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
|
||||
import {
|
||||
generateAiPromptMetadata,
|
||||
generateAiPromptsStaticParams,
|
||||
getAiPrompt,
|
||||
} from './AiPrompts.utils'
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
export default async function AiPromptsPage({ params: { slug } }: { params: { slug: string } }) {
|
||||
let { heading, content } = await getAiPrompt(slug)
|
||||
content = `
|
||||
## How to use
|
||||
|
||||
Copy the prompt to a file in your repo.
|
||||
|
||||
Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use \`#<filename>\`, in Cursor, use \`@Files\`, and in Zed, use \`/file\`.
|
||||
|
||||
## Prompt
|
||||
|
||||
${content}
|
||||
`.trim()
|
||||
|
||||
return (
|
||||
<GuideTemplate
|
||||
meta={{ title: `API Prompt: ${heading}` }}
|
||||
content={content}
|
||||
editLink={newEditLink(`supabase/supabase/blob/master/examples/prompts/${slug}.md`)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const generateMetadata = generateAiPromptMetadata
|
||||
export const generateStaticParams = generateAiPromptsStaticParams
|
||||
@@ -2,25 +2,41 @@ import { cache, type PropsWithChildren } from 'react'
|
||||
|
||||
import { IS_PLATFORM } from 'common'
|
||||
|
||||
import { supabaseMisc } from '~/lib/supabaseMisc'
|
||||
import { NavMenuSection } from '~/components/Navigation/Navigation.types'
|
||||
import Layout from '~/layouts/guides'
|
||||
import { supabaseMisc } from '~/lib/supabaseMisc'
|
||||
import { getAiPrompts } from './getting-started/ai-prompts/[slug]/AiPrompts.utils'
|
||||
|
||||
// Revalidate occasionally to pick up changes to partners
|
||||
// 60 seconds/minute * 60 minutes/hour * 24 hours/day
|
||||
export const revalidate = 86_400
|
||||
|
||||
const GuidesLayout = async ({ children }: PropsWithChildren) => {
|
||||
const partners = IS_PLATFORM ? await getPartners() : []
|
||||
const partnerNavItems = partners.map((partner) => ({
|
||||
name: partner.title,
|
||||
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
|
||||
}))
|
||||
const [partnerNavItems, promptNavItems] = await Promise.all([getPartners(), getPrompts()])
|
||||
|
||||
return <Layout additionalNavItems={partnerNavItems}>{children}</Layout>
|
||||
const additionalNavItems =
|
||||
partnerNavItems.length > 0 || promptNavItems.length > 0
|
||||
? { integrations: partnerNavItems, prompts: promptNavItems }
|
||||
: undefined
|
||||
|
||||
return <Layout additionalNavItems={additionalNavItems}>{children}</Layout>
|
||||
}
|
||||
|
||||
async function getPrompts() {
|
||||
const prompts = await getAiPrompts()
|
||||
return prompts.map(
|
||||
(prompt) =>
|
||||
({
|
||||
name: prompt.heading,
|
||||
url: `/guides/getting-started/ai-prompts/${prompt.filename}`,
|
||||
}) as Partial<NavMenuSection>
|
||||
)
|
||||
}
|
||||
|
||||
const getPartners = cache(getPartnersImpl)
|
||||
async function getPartnersImpl() {
|
||||
if (!IS_PLATFORM) return []
|
||||
|
||||
const { data, error } = await supabaseMisc()
|
||||
.from('partners')
|
||||
.select('slug, title')
|
||||
@@ -31,7 +47,15 @@ async function getPartnersImpl() {
|
||||
console.error(new Error('Error fetching partners', { cause: error }))
|
||||
}
|
||||
|
||||
return data ?? []
|
||||
const partnerNavItems = (data ?? []).map(
|
||||
(partner) =>
|
||||
({
|
||||
name: partner.title,
|
||||
url: `https://supabase.com/partners/integrations/${partner.slug}` as `https://${string}`,
|
||||
}) as Partial<NavMenuSection>
|
||||
)
|
||||
|
||||
return partnerNavItems
|
||||
}
|
||||
|
||||
export default GuidesLayout
|
||||
@@ -166,6 +166,15 @@ function useBreadcrumbs() {
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const isAiPromptsPage = pathname.startsWith('/guides/getting-started/ai-prompts')
|
||||
if (isAiPromptsPage) {
|
||||
const breadcrumbs = [
|
||||
{ name: 'Getting started', url: '/guides/getting-started' },
|
||||
{ name: 'AI Prompts', url: '/guides/getting-started/ai-prompts' },
|
||||
]
|
||||
return breadcrumbs
|
||||
}
|
||||
|
||||
const menuId = getMenuId(pathname)
|
||||
const menu = NavItems[menuId]
|
||||
return findMenuItemByUrl(menu, pathname, [])
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { useBreakpoint } from 'common'
|
||||
import { ChevronRight, Play, Sparkles } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { IconBackground } from 'ui'
|
||||
|
||||
import { useBreakpoint } from 'common'
|
||||
import { cn, IconBackground } from 'ui'
|
||||
import { IconPanel } from 'ui-patterns/IconPanel'
|
||||
|
||||
import DocsCoverLogo from './DocsCoverLogo'
|
||||
import { Play } from 'lucide-react'
|
||||
|
||||
function AiPrompt({ className }: { className?: string }) {
|
||||
return (
|
||||
<Link
|
||||
className={cn(
|
||||
'group',
|
||||
'w-fit rounded-full border px-3 py-1 flex gap-2 items-center text-foreground-light text-sm',
|
||||
'hover:border-brand hover:text-brand focus-visible:text-brand',
|
||||
'transition-colors',
|
||||
className
|
||||
)}
|
||||
href="/guides/getting-started/ai-prompts"
|
||||
>
|
||||
<Sparkles size={14} />
|
||||
Start with Supabase AI prompts
|
||||
<ChevronRight size={14} className="group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const HomePageCover = (props) => {
|
||||
const isXs = useBreakpoint(639)
|
||||
@@ -74,7 +95,7 @@ const HomePageCover = (props) => {
|
||||
p-5 md:p-8
|
||||
"
|
||||
>
|
||||
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-1 md:gap-3">
|
||||
<div className="col-span-full flex flex-col md:flex-row xl:flex-col justify-between gap-3">
|
||||
<div className="md:max-w-xs shrink w-fit xl:max-w-none">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<IconBackground>
|
||||
@@ -83,20 +104,23 @@ const HomePageCover = (props) => {
|
||||
<h2 className="text-2xl m-0 text-foreground">Getting Started</h2>
|
||||
</div>
|
||||
<p className="text-foreground-light text-sm">
|
||||
Discover how to set up a database to an app making queries in just a few minutes.
|
||||
Set up and connect a database in just a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
|
||||
{frameworks.map((framework, i) => (
|
||||
<Link key={i} href={framework.href} passHref className="no-underline">
|
||||
<IconPanel
|
||||
iconSize={iconSize}
|
||||
hideArrow
|
||||
tooltip={framework.tooltip}
|
||||
icon={framework.icon}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
<div className="shrink-0">
|
||||
<div className="flex flex-wrap md:grid md:grid-cols-5 gap-2 sm:gap-3">
|
||||
{frameworks.map((framework, i) => (
|
||||
<Link key={i} href={framework.href} passHref className="no-underline">
|
||||
<IconPanel
|
||||
iconSize={iconSize}
|
||||
hideArrow
|
||||
tooltip={framework.tooltip}
|
||||
icon={framework.icon}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<AiPrompt className="mt-6" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -349,6 +349,16 @@ export const gettingstarted: NavMenuConstant = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'AI Prompts',
|
||||
url: undefined,
|
||||
items: [
|
||||
{
|
||||
name: 'Overview',
|
||||
url: '/guides/getting-started/ai-prompts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ const NavigationMenu = ({
|
||||
additionalNavItems,
|
||||
}: {
|
||||
menuId: MenuId
|
||||
additionalNavItems?: Partial<NavMenuSection>[]
|
||||
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
|
||||
}) => {
|
||||
const level = menuId
|
||||
const menu = getMenuById(level)
|
||||
|
||||
@@ -6,20 +6,23 @@ import { type NavMenuSection } from '../Navigation.types'
|
||||
import * as NavItems from './NavigationMenu.constants'
|
||||
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { MenuId } from './NavigationMenu'
|
||||
|
||||
const NavigationMenuGuideList = ({
|
||||
id,
|
||||
additionalNavItems,
|
||||
}: {
|
||||
id: string
|
||||
additionalNavItems?: Partial<NavMenuSection>[]
|
||||
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
|
||||
}) => {
|
||||
const pathname = usePathname()
|
||||
const firstLevelRoute = pathname?.split('/')?.slice(0, 4)?.join('/')
|
||||
|
||||
// eslint-disable-next-line import/namespace -- dynamic access, can't lint properly
|
||||
let menu = NavItems[id]
|
||||
|
||||
if (id === 'integrations' && additionalNavItems) {
|
||||
if (id === MenuId.Integrations && additionalNavItems?.integrations) {
|
||||
const integrationsListIndex = menu.items.findIndex((item) => item.name === 'Integrations')
|
||||
if (integrationsListIndex !== -1) {
|
||||
menu = {
|
||||
@@ -28,7 +31,7 @@ const NavigationMenuGuideList = ({
|
||||
...menu.items.slice(0, integrationsListIndex),
|
||||
{
|
||||
...menu.items[integrationsListIndex],
|
||||
items: [...menu.items[integrationsListIndex].items, ...additionalNavItems],
|
||||
items: [...menu.items[integrationsListIndex].items, ...additionalNavItems.integrations],
|
||||
},
|
||||
...menu.items.slice(integrationsListIndex + 1),
|
||||
],
|
||||
@@ -36,6 +39,38 @@ const NavigationMenuGuideList = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (id === MenuId.GettingStarted && additionalNavItems?.prompts) {
|
||||
const promptsSectionIndex = menu.items.findIndex((item) => item.name === 'AI Prompts')
|
||||
if (promptsSectionIndex !== -1) {
|
||||
menu = {
|
||||
...menu,
|
||||
items: [
|
||||
...menu.items.slice(0, promptsSectionIndex),
|
||||
{
|
||||
...menu.items[promptsSectionIndex],
|
||||
items: [...menu.items[promptsSectionIndex].items, ...additionalNavItems.prompts],
|
||||
},
|
||||
...menu.items.slice(promptsSectionIndex + 1),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuGuideListWrapper id={id} firstLevelRoute={firstLevelRoute}>
|
||||
<NavigationMenuGuideListItems menu={menu} id={id} />
|
||||
</NavigationMenuGuideListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function NavigationMenuGuideListWrapper({
|
||||
id,
|
||||
firstLevelRoute,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
id: string
|
||||
firstLevelRoute?: string
|
||||
}>) {
|
||||
return (
|
||||
<Accordion.Root
|
||||
collapsible={true}
|
||||
@@ -44,7 +79,7 @@ const NavigationMenuGuideList = ({
|
||||
value={firstLevelRoute}
|
||||
className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150"
|
||||
>
|
||||
<NavigationMenuGuideListItems menu={menu} id={id} />
|
||||
{children}
|
||||
</Accordion.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: AI Prompts
|
||||
subtitle: Prompts for working with Supabase using AI-powered IDE tools
|
||||
---
|
||||
|
||||
We've curated a selection of prompts to help you work with Supabase using your favorite AI-powered IDE tools, such as GitHub Copilot or Cursor.
|
||||
|
||||
## How to use
|
||||
|
||||
Copy the prompt to a file in your repo.
|
||||
|
||||
Use the "include file" feature from your AI tool to include the prompt when chatting with your AI assistant. For example, with GitHub Copilot, use `#<filename>`, in Cursor, use `@Files`, and in Zed, use `/file`.
|
||||
|
||||
## Prompts
|
||||
|
||||
<AiPromptsIndex />
|
||||
@@ -7,9 +7,14 @@ import remarkGfm from 'remark-gfm'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import remarkMath from 'remark-math'
|
||||
|
||||
import { AiPromptsIndex } from '~/app/guides/(with-sidebar)/getting-started/ai-prompts/[slug]/AiPromptsIndex'
|
||||
import { preprocessMdxWithDefaults } from '~/features/directives/utils'
|
||||
import { components } from '~/features/docs/MdxBase.shared'
|
||||
|
||||
const serverOnlyComponents = {
|
||||
AiPromptsIndex,
|
||||
}
|
||||
|
||||
const codeHikeOptions: CodeHikeConfig = {
|
||||
theme: codeHikeTheme,
|
||||
lineNumbers: true,
|
||||
@@ -66,7 +71,7 @@ const MDXRemoteBase = async ({
|
||||
return (
|
||||
<MDXRemote
|
||||
source={preprocessedSource}
|
||||
components={components}
|
||||
components={{ ...components, ...serverOnlyComponents }}
|
||||
options={finalOptions}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -336,7 +336,7 @@ interface SkeletonProps extends PropsWithChildren {
|
||||
NavigationMenu?: ReactNode
|
||||
hideFooter?: boolean
|
||||
className?: string
|
||||
additionalNavItems?: Partial<NavMenuSection>[]
|
||||
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
|
||||
}
|
||||
|
||||
function TopNavSkeleton({ children }) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'katex/dist/katex.min.css'
|
||||
import { type PropsWithChildren } from 'react'
|
||||
import type { ReactNode, PropsWithChildren } from 'react'
|
||||
|
||||
import { type NavMenuSection } from '~/components/Navigation/Navigation.types'
|
||||
import { LayoutMainContent } from '~/layouts/DefaultLayout'
|
||||
@@ -8,9 +8,13 @@ import { SidebarSkeleton } from '~/layouts/MainSkeleton'
|
||||
const Layout = ({
|
||||
children,
|
||||
additionalNavItems,
|
||||
}: PropsWithChildren<{ additionalNavItems?: Partial<NavMenuSection>[] }>) => {
|
||||
NavigationMenu,
|
||||
}: PropsWithChildren<{
|
||||
additionalNavItems?: Record<string, Partial<NavMenuSection>[]>
|
||||
NavigationMenu?: ReactNode
|
||||
}>) => {
|
||||
return (
|
||||
<SidebarSkeleton additionalNavItems={additionalNavItems}>
|
||||
<SidebarSkeleton NavigationMenu={NavigationMenu} additionalNavItems={additionalNavItems}>
|
||||
<LayoutMainContent className="pb-0">{children}</LayoutMainContent>
|
||||
</SidebarSkeleton>
|
||||
)
|
||||
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
@@ -101,7 +101,6 @@
|
||||
"unist-builder": "^3.0.1",
|
||||
"unist-util-filter": "^4.0.1",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"unist-util-visit-parents": "^5.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"valtio": "^1.12.0",
|
||||
"yaml": "^2.4.5",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# PostgreSQL SQL Style Guide
|
||||
# Postgres SQL Style Guide
|
||||
|
||||
## General
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
- Avoid SQL reserved words and ensure names are unique and under 63 characters.
|
||||
- Use snake_case for tables and columns.
|
||||
- Prefer plurals for table names
|
||||
- Prefer plurals for table names
|
||||
- Prefer singular names for columns.
|
||||
|
||||
## Tables
|
||||
@@ -62,12 +62,12 @@ where employee_id = 1001;
|
||||
Larger queries:
|
||||
|
||||
```sql
|
||||
select
|
||||
first_name,
|
||||
select
|
||||
first_name,
|
||||
last_name
|
||||
from
|
||||
from
|
||||
employees
|
||||
where
|
||||
where
|
||||
start_date between '2021-01-01' and '2021-12-31'
|
||||
and
|
||||
status = 'employed';
|
||||
@@ -80,14 +80,14 @@ and
|
||||
- Prefer full table names when referencing tables. This helps for readability.
|
||||
|
||||
```sql
|
||||
select
|
||||
employees.employee_name,
|
||||
select
|
||||
employees.employee_name,
|
||||
departments.department_name
|
||||
from
|
||||
from
|
||||
employees
|
||||
join
|
||||
join
|
||||
departments on employees.department_id = departments.department_id
|
||||
where
|
||||
where
|
||||
employees.start_date > '2022-01-01';
|
||||
```
|
||||
|
||||
@@ -104,7 +104,7 @@ where end_date is null;
|
||||
|
||||
## Complex queries and CTEs
|
||||
|
||||
- If a query is extremely complex, prefer a CTE.
|
||||
- If a query is extremely complex, prefer a CTE.
|
||||
- Make sure the CTE is clear and linear. Prefer readability over performance.
|
||||
- Add comments to each block.
|
||||
|
||||
@@ -138,4 +138,4 @@ from
|
||||
employee_counts
|
||||
order by
|
||||
department_name;
|
||||
```
|
||||
```
|
||||
@@ -0,0 +1,108 @@
|
||||
# Bootstrap Next.js app with Supabase Auth
|
||||
|
||||
Create a Next.js app that uses App Router with Supabase Auth.
|
||||
|
||||
Follow Supabase's guidelines for using the `@supabase/ssr` package and Server-Side Auth. Specifically, there should be:
|
||||
|
||||
- A utility function to create a client on the client side
|
||||
- A utility function create a client on the server side, using the Next.js `cookies` API to access the cookies. Use the latest version of the API, where `cookies` must be awaited.
|
||||
- A utility function to handle refreshing the user session in middleware.
|
||||
|
||||
## Working with cookies
|
||||
|
||||
Use the latest version of `@supabase/ssr`, where cookie options are defined with the `getAll` and `setAll` functions, like so:
|
||||
|
||||
```
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
No other cookie options should be provided.
|
||||
|
||||
## Middleware
|
||||
|
||||
The middleware should use the following `updateSession` function:
|
||||
|
||||
```
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// IMPORTANT: Avoid writing any logic between createServerClient and
|
||||
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith('/login') &&
|
||||
!request.nextUrl.pathname.startsWith('/auth')
|
||||
) {
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = '/login'
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
|
||||
// creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
```
|
||||
Generated
-1
@@ -931,7 +931,6 @@
|
||||
"unist-builder": "^3.0.1",
|
||||
"unist-util-filter": "^4.0.1",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"unist-util-visit-parents": "^5.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
"valtio": "^1.12.0",
|
||||
"yaml": "^2.4.5",
|
||||
|
||||
Reference in new issue
Block a user