Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2025-02-26 15:54:01 -03:30
commit f58cb222dd
37 files changed
+817 -208

No files matched your search

+61 -21
View File
@@ -5,7 +5,8 @@ import type { HTMLAttributes } from 'react'
import { useEffect, useState } from 'react'
import { useBreakpoint } from 'common'
import { cn, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui'
import { cn, Separator, Sheet, SheetContent, SheetHeader, SheetTrigger } from 'ui'
import { Feedback } from '~/components/Feedback'
interface TocItem extends HTMLAttributes<HTMLElement> {
label: string
@@ -18,7 +19,7 @@ export function ContributingToc({ className }: { className?: string }) {
useEffect(() => {
const headings = [
...document.querySelectorAll('article.prose > h2,h3'),
...document.querySelectorAll('article.prose > h2,h3:not(#feedback-title)'),
] as Array<HTMLHeadingElement>
const tocItems = headings
.filter((heading) => !!heading.id)
@@ -30,20 +31,16 @@ export function ContributingToc({ className }: { className?: string }) {
}, [])
return mobileToc ? (
<MobileToc
items={tocItems}
className={cn(
'[--local-top-spacing:2rem]',
'fixed top-[calc(var(--header-height)+var(--local-top-spacing))] right-8'
)}
/>
<MobileToc items={tocItems} />
) : (
<TocBase
items={tocItems}
className={cn(
'[--local-top-spacing:5rem]',
'fixed top-[calc(var(--header-height)+var(--local-top-spacing))] right-8 w-36',
'xl:border-l border-foreground-muted'
'border-l thin-scrollbar overflow-y-auto px-2 hidden lg:block',
'col-span-3 self-start sticky',
'top-[calc(var(--header-height)+1px+2rem)] max-h-[calc(100vh-var(--header-height)-3rem)]',
className
)}
/>
)
@@ -52,31 +49,74 @@ export function ContributingToc({ className }: { className?: string }) {
function MobileToc({ items, className }: { items: Array<TocItem>; className?: string }) {
const [open, setOpen] = useState(false)
useEffect(() => {
const onHashChanged = () => setOpen(false)
window.addEventListener('hashchange', onHashChanged)
return () => {
window.removeEventListener('hashchange', onHashChanged)
}
}, [])
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen}>
<PopoverTrigger_Shadcn_ className={cn('border rounded p-2', className)}>
<Menu />
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger
className={cn(
'fixed z-0 inset-0 top-auto w-full rounded-t-lg border border-b-0 p-4 bg-studio flex items-center gap-2 text-foreground-light text-sm',
className
)}
>
<Menu size={16} strokeWidth={1.5} className="text-foreground-light" />
<span className="sr-only">
{open ? 'Close table of contents' : 'Open table of contents'}
</span>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ align="end" className="w-48">
<TocBase items={items} />
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
<span>On this page</span>
</SheetTrigger>
<SheetContent
side="bottom"
size="lg"
className={cn(
'w-full flex flex-col gap-0 p-0 rounded-t-lg overflow-hidden',
!open && 'top-[calc(100vh-100px)]'
)}
>
<SheetHeader className="py-0 px-4">
<SheetTrigger
className={cn(
'w-full py-4 flex items-center gap-2 text-foreground-light text-sm',
className
)}
>
<Menu size={16} strokeWidth={1.5} className="text-foreground-light" />
<span className="sr-only">
{open ? 'Close table of contents' : 'Open table of contents'}
</span>
<span>On this page</span>
</SheetTrigger>
</SheetHeader>
<div className="w-full flex-1 p-4 pb-8 overflow-y-auto thin-scrollbar">
<TocBase items={items} />
</div>
</SheetContent>
</Sheet>
)
}
function TocBase({ items, className }: { items: Array<TocItem>; className?: string }) {
return (
<nav aria-label="Table of contents" className={cn('px-4 text-foreground-light', className)}>
<ul className="flex flex-col gap-1">
<nav aria-label="Table of contents" className={cn('text-foreground-lighter', className)}>
<span className="hidden lg:block font-mono text-xs uppercase text-foreground px-5 mb-6">
On this page
</span>
<ul className="toc-menu list-none lg:pl-5 text-[0.8rem] grid gap-2">
{items.map((item) => (
<li key={item.anchor} className="overflow-hidden truncate">
<a href={`#${item.anchor}`}>{item.label}</a>
</li>
))}
</ul>
<Separator className="lg:w-[calc(100%-2rem)] lg:ml-5 my-4 lg:my-8" />
<Feedback className="pl-0 lg:pl-5" />
</nav>
)
}
+16 -8
View File
@@ -5,22 +5,30 @@ import { fileURLToPath } from 'node:url'
import { ContributingToc } from '~/app/contributing/ContributingToC'
import { MDXProviderGuides } from '~/features/docs/GuidesMdx.client'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import { LayoutMainContent } from '~/layouts/DefaultLayout'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
import Breadcrumbs from '~/components/Breadcrumbs'
export default async function ContributingPage() {
const contentFile = join(dirname(fileURLToPath(import.meta.url)), 'content.mdx')
const content = await readFile(contentFile, 'utf-8')
return (
<SidebarSkeleton hideSideNav>
<div className="px-8 py-16">
<article className="prose mx-auto">
<MDXProviderGuides>
<MDXRemoteBase source={content} />
</MDXProviderGuides>
</article>
<SidebarSkeleton>
<LayoutMainContent className="pb-0 grid grid-cols-12 relative gap-4">
<div className="col-span-12 lg:col-span-9">
<Breadcrumbs className="mb-2 col-span-full" />
<article
id="contributing"
className="prose max-w-none relative transition-all ease-out duration-100"
>
<MDXProviderGuides>
<MDXRemoteBase source={content} />
</MDXProviderGuides>
</article>
</div>
<ContributingToc />
</div>
</LayoutMainContent>
</SidebarSkeleton>
)
}
+54 -43
View File
@@ -33,9 +33,9 @@ const FeedbackButton = forwardRef<
ref={ref}
className={cn(
'mt-0',
'flex items-center gap-2',
'flex items-center gap-1',
'text-xs text-foreground-lighter',
'hover:text-foreground',
'hover:text-foreground text-left',
!visible && 'opacity-0 invisible',
'[transition-property:opacity,color]',
'[transition-delay:700ms,0ms]'
@@ -141,72 +141,83 @@ function Feedback({ className }: { className?: string }) {
}
return (
<section className={cn('@container px-5 mb-6', className)} aria-labelledby="feedback-title">
<h3 id="feedback-title" className="block font-mono text-xs uppercase text-foreground mb-4">
<section className={cn('@container', className)} aria-labelledby="feedback-title">
<h3 id="feedback-title" className="block font-mono text-xs text-foreground-light mb-3">
Is this helpful?
</h3>
<div className="relative flex flex-col gap-y-4 mb-2 @[12rem]:flex-row @[12rem]:items-center">
<div className="relative flex flex-col gap-2 @[12rem]:gap-4 @[12rem]:flex-row @[12rem]:items-center">
<div
style={{ '--container-flex-gap': '0.5rem' } as CSSProperties}
className={`relative flex gap-[var(--container-flex-gap)] items-center`}
className="relative flex gap-2 items-center"
>
<Button
type="outline"
rounded
className={cn(
'px-1',
!isYes && 'hover:text-brand-600 hover:border-brand-500',
isYes && 'bg-brand text-brand-200 !border-brand disabled:opacity-100',
!showYes && 'opacity-0 invisible',
'[transition-property:opacity,transform,color] [transition-duration:150ms,300ms,300ms]',
'motion-reduce:[transition-duration:150ms,1ms,300ms]'
)}
onClick={() => handleVote('yes')}
disabled={state.type === StateType.Followup}
>
<Check size={14} strokeWidth={3} />
<span className="sr-only">Yes</span>
</Button>
<Button
type="outline"
rounded
className={cn(
'px-1',
'px-1 w-7 h-7',
'text-foreground-light',
'[transition-property:opacity,transform,color] [transition-duration:150ms,250ms,250ms]',
'motion-reduce:[transition-duration:150ms,1ms,300ms]',
'[transition-timing-function:cubic-bezier(.76,0,.23,1)]',
!isNo && 'hover:text-warning-600 hover:border-warning-500',
isNo &&
`bg-warning text-warning-200 !border-warning -translate-x-[calc(100%+var(--container-flex-gap,0.5rem))] disabled:opacity-100`,
!showNo && 'opacity-0 invisible',
'[transition-property:opacity,transform,color] [transition-duration:150ms,300ms,300ms]',
'motion-reduce:[transition-duration:150ms,1ms,300ms]'
isNo && `bg-warning text-warning-200 !border-warning disabled:opacity-100`,
!showNo && 'opacity-0 invisible'
)}
onClick={() => handleVote('no')}
disabled={state.type === StateType.Followup}
>
<X size={14} strokeWidth={3} />
<X size={14} strokeWidth={2} className="text-current" />
<span className="sr-only">No</span>
</Button>
<Button
type="outline"
rounded
className={cn(
'px-1 w-7 h-7',
'text-foreground-light',
'[transition-property:opacity,transform,color] [transition-duration:150ms,250ms,250ms]',
'motion-reduce:[transition-duration:150ms,1ms,300ms]',
'[transition-timing-function:cubic-bezier(.76,0,.23,1)]',
!isYes && 'hover:text-brand-600 hover:border-brand-500',
isYes &&
'bg-brand text-brand-200 !border-brand disabled:opacity-100 -translate-x-[calc(100%+var(--container-inline-flex-gap,0.5rem))]',
!showYes && 'opacity-0 invisible'
)}
onClick={() => handleVote('yes')}
disabled={state.type === StateType.Followup}
>
<Check size={14} strokeWidth={2} />
<span className="sr-only">Yes</span>
</Button>
</div>
<div
className={cn(
'flex flex-col gap-1',
'flex flex-col gap-0.5',
'@[12rem]:absolute @[12rem]:left-9',
'text-xs',
'opacity-0 invisible',
'text-left',
'-translate-x-[0.25rem] @[12rem]:-translate-x-[1.25rem]',
'[transition-property:opacity,transform] [transition-duration:150ms,300ms]',
'-translate-x-2',
'[transition-property:opacity,transform]',
'[transition-duration:450ms,300ms]',
'[transition-delay:200ms,0ms]',
'[transition-timing-function:cubic-bezier(.76,0,.23,1)]',
'motion-reduce:[transition-duration:150ms,1ms]',
'delay-300',
state.type === StateType.Followup &&
'opacity-100 visible -translate-x-0 @[12rem]:-translate-x-[1rem]'
'!ease-out',
state.type === StateType.Followup && 'opacity-100 visible -translate-x-0'
)}
>
<span className="text-foreground-light">Thanks for your feedback!</span>
<FeedbackButton
ref={feedbackButtonRef}
onClick={() => setModalOpen(true)}
isYes={isYes}
visible={!unanswered}
/>
{state.type === StateType.Followup && (
<>
<span className="text-foreground-light">Thanks for your feedback!</span>
<FeedbackButton
ref={feedbackButtonRef}
onClick={() => setModalOpen(true)}
isYes={isYes}
visible={true}
/>
</>
)}
</div>
</div>
<FeedbackModal
+15 -6
View File
@@ -131,16 +131,25 @@ const GuidesTableOfContents = ({
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
return (
<div className={cn('border-l', 'thin-scrollbar overflow-y-auto', 'px-2', className)}>
<div
className={cn(
'border-l flex flex-col gap-6 lg:gap-8',
'thin-scrollbar overflow-y-auto',
'px-2',
className
)}
>
{video && (
<div className="relative mb-6 pl-5">
<div className="relative pl-5">
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
</div>
)}
<Feedback key={pathname} />
<div className="pl-5">
<Feedback key={pathname} />
</div>
{displayedList.length > 0 && (
<>
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-6">
<div>
<span className="block font-mono text-xs uppercase text-foreground px-5 mb-3">
On this page
</span>
<ul className="toc-menu list-none pl-5 text-[0.8rem] grid gap-2">
@@ -163,7 +172,7 @@ const GuidesTableOfContents = ({
</li>
))}
</ul>
</>
</div>
)}
</div>
)
@@ -1953,6 +1953,13 @@ export const local_development: NavMenuConstant = {
],
}
export const contributing: NavMenuConstant = {
icon: 'contributing',
title: 'Contributing',
url: '/contributing',
items: [{ name: 'Overview', url: '/contributing' }],
}
export const MIGRATION_PAGES: Partial<NavMenuSection & ComponentProps<typeof IconPanel>>[] = [
{
name: 'Auth0',
@@ -24,6 +24,7 @@ enum MenuId {
SelfHosting = 'self_hosting',
Integrations = 'integrations',
LocalDevelopment = 'local_development',
Contributing = 'contributing',
RefJavaScriptV1 = 'reference_javascript_v1',
RefJavaScriptV2 = 'reference_javascript_v2',
RefDartV1 = 'reference_dart_v1',
@@ -128,6 +129,10 @@ const menus: Menu[] = [
id: MenuId.LocalDevelopment,
type: 'guide',
},
{
id: MenuId.Contributing,
type: 'guide',
},
{
id: MenuId.Deployment,
type: 'guide',
@@ -139,6 +139,8 @@ export const getMenuId = (pathname: string | null) => {
return MenuId.SelfHosting
case pathname.startsWith('storage'):
return MenuId.Storage
case pathname.startsWith('/contributing'):
return MenuId.Contributing
default:
return MenuId.GettingStarted
}
@@ -90,7 +90,7 @@ The session mode connection string connects to your Postgres instance via a prox
The connection string looks like this:
```
postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-ca-central-1.pooler.supabase.com:5432/postgres
postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
```
Get your project's session mode string from the [Database Settings](/dashboard/project/_/settings/database) page:
@@ -112,7 +112,7 @@ Transaction mode does not support [prepared statements](https://postgresql.org/d
The connection string looks like this:
```
postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-ca-central-1.pooler.supabase.com:6543/postgres
postgres://postgres.apbkobhfnmcqqzqeeqss:[YOUR-PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
```
Get your project's transaction mode string from the [Database Settings](/dashboard/project/_/settings/database) page:
@@ -115,7 +115,7 @@ const GuideTemplate = ({ meta, content, children, editLink, mdxOptions }: GuideT
<GuidesTableOfContents
video={meta?.tocVideo}
className={cn(
'hidden md:block',
'hidden md:flex',
'col-span-3 self-start',
'sticky',
/**
@@ -94,7 +94,7 @@ export default async function TroubleshootingPage({ entry }: { entry: ITroublesh
<hr className="my-6" aria-hidden />
</>
)}
<Feedback className="px-0" />
<Feedback className="px-0 mb-6 lg:mb-8" />
</aside>
</div>
</article>
+6
View File
@@ -82,6 +82,10 @@ const levelsData = {
icon: 'platform',
name: 'Platform',
},
contributing: {
icon: 'contributing',
name: 'Contributing',
},
resources: {
icon: 'resources',
name: 'Resources',
@@ -377,6 +381,8 @@ function SidebarSkeleton({
const mobileMenuOpen = useMenuMobileOpen()
console.log('menuName', menuName, menuId)
return (
<div className={cn('flex flex-row h-full relative', className)}>
{!hideSideNav && (
@@ -82,7 +82,10 @@ const AddNewSecretForm = () => {
}
if (pairs.length) {
form.setValue('secrets', pairs)
const currentSecrets = form.getValues('secrets')
// Filter out any empty pairs before combining
const nonEmptySecrets = currentSecrets.filter((secret) => secret.name || secret.value)
form.setValue('secrets', [...nonEmptySecrets, ...pairs])
}
}
@@ -7,29 +7,7 @@ import TerminalInstructions from './TerminalInstructions'
const FunctionsEmptyState = () => {
return (
<>
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col gap-4">
<p className="max-w-lg text-base text-foreground">
Scalable functions to run your code with no server management.
</p>
<p className="max-w-lg text-sm text-foreground-light">
Edge Functions are server-side Typescript functions, distributed globally at the edge -
close to your users. They can be used for listening to webhooks or integrating your
Supabase project with third-parties.
</p>
<div className="flex gap-x-2">
<DocsButton href="https://supabase.com/docs/guides/functions" />
<Button asChild type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href="https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions"
>
Examples
</a>
</Button>
</div>
</div>
<div className="w-full mt-8">
<div className="col-span-8 bg-surface-100 px-5 py-4 border rounded-md">
<TerminalInstructions />
</div>
@@ -2,11 +2,13 @@ import type { CloudProvider, Region } from 'shared-data'
import { AWS_REGIONS, FLY_REGIONS } from 'shared-data'
export function getAvailableRegions(cloudProvider: CloudProvider): Region {
if (cloudProvider === 'AWS') {
return AWS_REGIONS
} else if (cloudProvider === 'FLY') {
return FLY_REGIONS
switch (cloudProvider) {
case 'AWS':
case 'AWS_NEW':
return AWS_REGIONS
case 'FLY':
return FLY_REGIONS
default:
throw new Error('Invalid cloud provider')
}
throw new Error('Invalid cloud provider')
}
+71 -7
View File
@@ -16,11 +16,15 @@ import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectConte
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
import { useHideSidebar } from 'hooks/misc/useHideSidebar'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLints } from 'hooks/misc/useLints'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useFlag } from 'hooks/ui/useFlag'
import { Home } from 'icons'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
@@ -39,6 +43,7 @@ import {
useSidebar,
} from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { useIsAPIDocsSidePanelEnabled } from './App/FeaturePreview/FeaturePreviewContext'
import { ThemeDropdown } from './ThemeDropdown'
import { UserDropdown } from './UserDropdown'
@@ -237,10 +242,29 @@ export function SideBarNavLink({
)
}
const ActiveDot = (errorArray: any[], warningArray: any[]) => {
return (
<div
className={cn(
'absolute flex h-2 w-2 left-[21px] group-data-[state=expanded]:left-[22.25px] top-2 z-10 rounded-full',
errorArray.length > 0
? 'bg-destructive-600'
: warningArray.length > 0
? 'bg-warning-600'
: 'bg-transparent'
)}
/>
)
}
function ProjectLinks() {
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const snap = useAppStateSnapshot()
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const { securityLints, errorLints } = useLints()
const showWarehouse = useFlag('warehouse')
const activeRoute = router.pathname.split('/')[3]
@@ -302,13 +326,53 @@ function ProjectLinks() {
</SidebarGroup>
<Separator className="w-[calc(100%-1rem)] mx-auto" />
<SidebarGroup className="gap-0.5">
{otherRoutes.map((route, i) => (
<SideBarNavLink
key={`other-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
))}
{otherRoutes.map((route, i) => {
if (route.key === 'api' && isNewAPIDocsEnabled) {
return (
<SideBarNavLink
key={`other-routes-${i}`}
route={{
label: route.label,
icon: route.icon,
key: route.key,
}}
onClick={() => {
snap.setShowProjectApiDocs(true)
}}
/>
)
} else if (route.key === 'advisors') {
return (
<div className="relative" key={route.key}>
{ActiveDot(errorLints, securityLints)}
<SideBarNavLink
key={`other-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
</div>
)
} else if (route.key === 'logs') {
// TODO: Undo this when warehouse flag is removed
const label = showWarehouse ? 'Logs & Analytics' : route.label
const newRoute = { ...route, label }
return (
<SideBarNavLink
key={`other-routes-${i}`}
route={newRoute}
active={activeRoute === newRoute.key}
/>
)
} else {
return (
<SideBarNavLink
key={`other-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
)
}
})}
</SidebarGroup>
{/* Settings routes to be added in with project/org nav */}
{/* <SidebarGroup className="gap-0.5">
@@ -0,0 +1,51 @@
import { useParams } from 'common'
import { ProductMenu } from 'components/ui/ProductMenu'
import { withAuth } from 'hooks/misc/withAuth'
import { useRouter } from 'next/router'
import { PropsWithChildren } from 'react'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
const EdgeFunctionsProductMenu = () => {
const { ref: projectRef = 'default' } = useParams()
const router = useRouter()
const page = router.pathname.split('/')[4]
const menuItems = [
{
name: 'Edge Functions',
key: 'functions',
url: `/project/${projectRef}/functions`,
items: [
{
name: 'Functions',
key: 'main',
url: `/project/${projectRef}/functions`,
items: [],
},
{
name: 'Secrets',
key: 'secrets',
url: `/project/${projectRef}/functions/secrets`,
items: [],
},
],
},
]
return <ProductMenu page={page} menu={menuItems} />
}
const EdgeFunctionsLayout = ({ children }: PropsWithChildren<{}>) => {
return (
<ProjectLayout
title="Edge Functions"
product="Edge Functions"
productMenu={<EdgeFunctionsProductMenu />}
isBlocking={false}
>
{children}
</ProjectLayout>
)
}
export default withAuth(EdgeFunctionsLayout)
@@ -27,7 +27,7 @@ interface PageLayoutProps {
secondaryActions?: ReactNode
navigationItems?: NavigationItem[]
className?: string
size?: 'default' | 'full'
size?: 'default' | 'full' | 'large' | 'small'
isCompact?: boolean
}
@@ -72,6 +72,7 @@ export const PageLayout = ({
return (
<div className="w-full">
<ScaffoldContainer
size={size}
className={cn(
'w-full mx-auto',
size === 'full' &&
@@ -129,8 +129,9 @@ export const generateSettingsMenu = (
{
name: 'Edge Functions',
key: 'functions',
url: `/project/${ref}/settings/functions`,
url: `/project/${ref}/functions/secrets`,
items: [],
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
},
]
: []),
+15 -3
View File
@@ -19,16 +19,28 @@ export const MAX_WIDTH_CLASSES_COLUMN = 'min-w-[420px]'
*/
export const ScaffoldContainer = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement> & { bottomPadding?: boolean }
>(({ className, bottomPadding, ...props }, ref) => {
HTMLAttributes<HTMLDivElement> & {
bottomPadding?: boolean
size?: 'small' | 'default' | 'large' | 'full'
}
>(({ className, bottomPadding, size = 'default', ...props }, ref) => {
const { aiAssistantPanel } = useAppStateSnapshot()
const { open } = aiAssistantPanel
const maxWidthClass = {
small: 'max-w-[768px]',
default: 'max-w-[1200px]',
large: 'max-w-[1600px]',
full: 'max-w-none',
}[size]
return (
<div
ref={ref}
{...props}
className={cn(
MAX_WIDTH_CLASSES,
'mx-auto w-full',
maxWidthClass,
PADDING_CLASSES,
bottomPadding && 'pb-16',
open ? 'xl:px-6' : '',
@@ -30,7 +30,7 @@ const ProductMenu = ({ page, menu }: ProductMenuProps) => {
{group.items.map((item) => {
const isActive = !!item.pages
? item.pages.includes(page ?? '')
: page === item.key
: page === item.key || (page === undefined && item.key === 'main')
return (
<ProductMenuItem
@@ -2,7 +2,8 @@ import { ReactNode } from 'react'
export interface ProductMenuGroup {
title?: string
key?: string
/** Set to "main" if page is on a '/' route */
key?: string | 'main'
isPreview?: boolean
items: ProductMenuGroupItem[]
}
@@ -20,7 +20,7 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.01344,
priceMonthly: 10,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
small: {
label: 'Small',
@@ -28,7 +28,7 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.0206,
priceMonthly: 15,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
medium: {
label: 'Medium',
@@ -36,7 +36,7 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.0822,
priceMonthly: 60,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
large: {
label: 'Large',
@@ -44,7 +44,7 @@ export const instanceSizeSpecs: Record<
cpu: '2-core',
priceHourly: 0.1517,
priceMonthly: 110,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
xlarge: {
label: 'XL',
@@ -52,7 +52,7 @@ export const instanceSizeSpecs: Record<
cpu: '4-core',
priceHourly: 0.2877,
priceMonthly: 210,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
'2xlarge': {
label: '2XL',
@@ -60,7 +60,7 @@ export const instanceSizeSpecs: Record<
cpu: '8-core',
priceHourly: 0.562,
priceMonthly: 410,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
'4xlarge': {
label: '4XL',
@@ -68,7 +68,7 @@ export const instanceSizeSpecs: Record<
cpu: '16-core',
priceHourly: 1.32,
priceMonthly: 960,
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.FLY.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id, PROVIDERS.FLY.id],
},
'8xlarge': {
label: '8XL',
@@ -76,7 +76,7 @@ export const instanceSizeSpecs: Record<
cpu: '32-core',
priceHourly: 2.562,
priceMonthly: 1870,
cloud_providers: [PROVIDERS.AWS.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id],
},
'12xlarge': {
label: '12XL',
@@ -84,7 +84,7 @@ export const instanceSizeSpecs: Record<
cpu: '48-core',
priceHourly: 3.836,
priceMonthly: 2800,
cloud_providers: [PROVIDERS.AWS.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id],
},
'16xlarge': {
label: '16XL',
@@ -92,6 +92,6 @@ export const instanceSizeSpecs: Record<
cpu: '64-core',
priceHourly: 5.12,
priceMonthly: 3730,
cloud_providers: [PROVIDERS.AWS.id],
cloud_providers: [PROVIDERS.AWS.id, PROVIDERS.AWS_NEW.id],
},
}
+25
View File
@@ -0,0 +1,25 @@
import { useParams } from 'common'
import { useProjectLintsQuery } from 'data/lint/lint-query'
/**
* Hook to fetch and filter project lints
*
* Retrieves all lints for the current project and filters them by:
* - Security-related lints
* - Error-level security lints
*
* @returns {Object} Object containing filtered lint arrays
* @returns {Array} securityLints - All security-related lints
* @returns {Array} errorLints - Security lints with ERROR level
*/
export const useLints = () => {
const { ref } = useParams()
const { data } = useProjectLintsQuery({
projectRef: ref,
})
const securityLints = (data ?? []).filter((lint) => lint.categories.includes('SECURITY'))
const errorLints = securityLints.filter((lint) => lint.level === 'ERROR')
return { securityLints, errorLints }
}
@@ -39,6 +39,13 @@ export const PROVIDERS = {
default_region: AWS_REGIONS_DEFAULT,
regions: { ...AWS_REGIONS },
},
AWS_NEW: {
id: 'AWS_NEW',
name: 'AWS (Revamped)',
DEFAULT_SSH_KEY: 'supabase-app-instance',
default_region: AWS_REGIONS_DEFAULT,
regions: { ...AWS_REGIONS },
},
} as const
export const PROJECT_STATUS: {
+5
View File
@@ -460,6 +460,11 @@ const nextConfig = {
source: '/project/:ref/settings/warehouse',
destination: '/project/:ref/settings/general',
},
{
permanent: true,
source: '/project/:ref/settings/functions',
destination: '/project/:ref/functions/secrets',
},
...(process.env.NEXT_PUBLIC_BASE_PATH?.length
? [
{
@@ -1,21 +1,28 @@
import { useParams } from 'common'
import { Button, Dialog, DialogContent, DialogSection, DialogTrigger } from 'ui'
import { AiIconAnimation, Button, Dialog, DialogContent, DialogSection, DialogTrigger } from 'ui'
import {
EdgeFunctionsListItem,
FunctionsEmptyState,
TerminalInstructions,
} from 'components/interfaces/Functions'
import FunctionsLayout from 'components/layouts/FunctionsLayout/FunctionsLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DocsButton } from 'components/ui/DocsButton'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { useAppStateSnapshot } from 'state/app-state'
import { ExternalLink } from 'lucide-react'
import type { NextPageWithLayout } from 'types'
const PageLayout: NextPageWithLayout = () => {
const FunctionsPage: NextPageWithLayout = () => {
const { ref } = useParams()
const { setAiAssistantPanel } = useAppStateSnapshot()
const {
data: functions,
@@ -27,10 +34,77 @@ const PageLayout: NextPageWithLayout = () => {
const hasFunctions = (functions ?? []).length > 0
const deployButton = (
<Dialog>
<DialogTrigger asChild>
<Button type="primary">Deploy a new function</Button>
</DialogTrigger>
<DialogContent size="large">
<DialogSection padding="small">
<TerminalInstructions />
</DialogSection>
</DialogContent>
</Dialog>
)
const secondaryActions = [
<DocsButton key="docs" href="https://supabase.com/docs/guides/functions" />,
<ButtonTooltip
type="default"
className="px-1 pointer-events-auto"
icon={<AiIconAnimation size={16} />}
onClick={() =>
setAiAssistantPanel({
open: true,
initialInput: `Create a new edge function that ...`,
suggestions: {
title:
'I can help you create a new edge function. Here are a few example prompts to get you started:',
prompts: [
'Create a new edge function that processes payments with Stripe',
'Create a new edge function that sends emails with Resend',
'Create a new edge function that generates PDFs from HTML templates',
],
},
})
}
tooltip={{
content: {
side: 'bottom',
text: 'Create with Supabase Assistant',
},
}}
/>,
]
if (!hasFunctions) {
secondaryActions.unshift(
<Button asChild type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href="https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions"
>
Examples
</a>
</Button>
)
}
return (
<>
<div className="py-6">
{isLoading && <GenericSkeletonLoader />}
<PageLayout
size="large"
title="Edge Functions"
subtitle="Server-side TypeScript functions distributed globally at the edge"
primaryActions={deployButton}
secondaryActions={secondaryActions}
>
<ScaffoldContainer size="large">
{isLoading && (
<div className="pt-8">
<GenericSkeletonLoader />
</div>
)}
{isError && <AlertError error={error} subject="Failed to retrieve edge functions" />}
@@ -38,21 +112,6 @@ const PageLayout: NextPageWithLayout = () => {
<>
{hasFunctions ? (
<div className="py-6 space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-foreground-lighter">{`${functions.length} function${
functions.length > 1 ? 's' : ''
} deployed`}</span>
<Dialog>
<DialogTrigger asChild>
<Button type="primary">Deploy a new function</Button>
</DialogTrigger>
<DialogContent size={'large'}>
<DialogSection padding="small">
<TerminalInstructions />
</DialogSection>
</DialogContent>
</Dialog>
</div>
<Table
head={
<>
@@ -78,15 +137,17 @@ const PageLayout: NextPageWithLayout = () => {
)}
</>
)}
</div>
</>
</ScaffoldContainer>
</PageLayout>
)
}
PageLayout.getLayout = (page) => (
<DefaultLayout>
<FunctionsLayout>{page}</FunctionsLayout>
</DefaultLayout>
)
FunctionsPage.getLayout = (page) => {
return (
<DefaultLayout>
<EdgeFunctionsLayout>{page}</EdgeFunctionsLayout>
</DefaultLayout>
)
}
export default PageLayout
export default FunctionsPage
@@ -0,0 +1,32 @@
import EdgeFunctionSecrets from 'components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets'
import DefaultLayout from 'components/layouts/DefaultLayout'
import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
import type { NextPageWithLayout } from 'types'
const SecretsPage: NextPageWithLayout = () => {
return (
<ScaffoldContainer size="large" className="pt-6">
<EdgeFunctionSecrets />
</ScaffoldContainer>
)
}
SecretsPage.getLayout = (page) => {
return (
<DefaultLayout>
<EdgeFunctionsLayout>
<PageLayout
size="large"
title="Edge Function Secrets"
subtitle="Manage the secrets for your project's edge functions"
>
{page}
</PageLayout>
</EdgeFunctionsLayout>
</DefaultLayout>
)
}
export default SecretsPage
@@ -1,42 +0,0 @@
import EdgeFunctionSecrets from 'components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets'
import DefaultLayout from 'components/layouts/DefaultLayout'
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import {
ScaffoldContainer,
ScaffoldDescription,
ScaffoldHeader,
ScaffoldTitle,
} from 'components/layouts/Scaffold'
import type { NextPageWithLayout } from 'types'
import { DocsButton } from 'components/ui/DocsButton'
const PageLayout: NextPageWithLayout = () => {
return (
<>
<ScaffoldContainer>
<ScaffoldHeader className="flex flex-row justify-between">
<div>
<ScaffoldTitle>Edge Function Secrets</ScaffoldTitle>
<ScaffoldDescription>
Manage the secrets (environment variables) for your project's Edge Functions
</ScaffoldDescription>
</div>
<div className="flex items-center space-x-2">
<DocsButton href="https://supabase.com/docs/guides/functions/secrets" />
</div>
</ScaffoldHeader>
</ScaffoldContainer>
<ScaffoldContainer bottomPadding>
<EdgeFunctionSecrets />
</ScaffoldContainer>
</>
)
}
PageLayout.getLayout = (page) => (
<DefaultLayout>
<SettingsLayout title="Settings">{page}</SettingsLayout>
</DefaultLayout>
)
export default PageLayout
@@ -0,0 +1,320 @@
---
title: 'Postgres as a Graph Database: (Ab)using pgRouting'
description:
author: paul_copplestone
image: pgrouting-graph/pgrouting-og.png
thumb: pgrouting-graph/pgrouting-og.png
categories:
- developers
tags:
- postgres
date: '2025-02-25'
toc_depth: 3
---
[pgRouting](https://github.com/pgRouting/pgrouting) is a Postgres extension. It's often used for finding the “shortest path” between two locations, however it's a hidden gem in Postgres and can be used for basic graph functionality.
![pgrouting on HN](/images/blog/pgrouting-graph/pgrouting-hn.png)
pgRouting is typically combined with PostGIS for working with geospatial data, but it can also be [useful beyond that](https://news.ycombinator.com/item?id=26350454) as a lightweight alternative to Graph extensions like [Apache AGE](https://age.apache.org/), or specialized graph databases like [Neo4j](https://neo4j.com/).
Let's explore some useful applications of pgRouting and graphs.
## What is pgRouting?
pgRouting is an extension of PostGIS that provides geospatial routing functionality. You can use it to calculate the shortest path, perform network analysis, and solve complex routing problems on a graph-based structure. Most commonly, this is used in Geographic Information Systems (GIS) for tasks like determining the fastest route between two locations.
![pgrouting map](/images/blog/pgrouting-graph/pgrouting-map.png)
## Working with Graphs
The power of pgRouting lies in its ability to work with any data structured as a graph. A graph is essentially a network of interconnected points, where:
- **Nodes** represent entities.
- **Edges** represent relationships or paths between those nodes.
In maps / [GIS](https://en.wikipedia.org/wiki/Geographic_information_system), nodes and edges represent intersections and roads respectively. However, this structure can also be applied to abstract systems like a social networks, where users are nodes and friendships are edges.
<Img
alt="create API key on Cal.com"
src={{
dark: '/images/blog/pgrouting-graph/graph-dark.png',
light: '/images/blog/pgrouting-graph/graph-light.png',
}}
/>
## Non-GIS Use Cases for pgRouting
Let's explore how pgRouting can be applied to a few non-[GIS](https://en.wikipedia.org/wiki/Geographic_information_system) problems.
### Task scheduling
In any project, tasks have dependencies. For example, task B can only start after task A is completed. This creates a [directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph) (DAG), where:
- nodes represent tasks
- edges represent dependencies
One of the most challenging aspects of managing projects is determining the “critical path” — the project's overall duration, determined by the longest sequence of dependencies.
<Img
alt="create API key on Cal.com"
src={{
dark: '/images/blog/pgrouting-graph/path-dark.png',
light: '/images/blog/pgrouting-graph/path-light.png',
}}
/>
Using pgRouting, you can model your task's dependencies, using graph algorithms to find the critical path. Suppose we have a table tasks with task dependencies modeled as a graph:
```sql
-- Create the tasks table with dependencies
create table tasks (
id serial primary key,
name text not null
);
-- insert tasks into the table
insert into tasks (name)
values
('Start Project'),
('Task A'),
('Task B'),
('Task C'),
('Task D'),
('End Project');
-- create the dependencies table
create table dependencies (
id serial primary key,
source integer not null, -- task id where the dependency starts
target integer not null, -- task id where the dependency ends
duration integer not null, -- duration of the task in days
constraint fk_source foreign key (source) references tasks (id),
constraint fk_target foreign key (target) references tasks (id)
);
-- insert dependencies with durations (directed edges)
insert into dependencies (source, target, duration)
values
(1, 2, 3), -- start project -> task a (3 days)
(2, 3, 4), -- task a -> task b (4 days)
(3, 4, 5), -- task b -> task c (5 days)
(4, 5, 2), -- task c -> task d (2 days)
(5, 6, 6);
-- task d -> end project (6 days)
```
You can then use the [`pgr_dijkstra()`](https://docs.pgrouting.org/latest/en/pgr_dijkstra.html) function to find the shortest (or longest) path through the tasks, allowing you to map out the project schedule effectively:
```sql
create schema if not exists extensions;
create extension pgrouting schema extensions cascade;
-- find the longest path using pgr_dijkstra()
-- (as it calculates shortest path, use negative weights)
select * FROM extensions.pgr_dijkstra(
'select id, source, target, duration as cost from dependencies',
1, -- Start Project (Task ID 1)
6 -- End Project (Task ID 6)
);
```
Which returns a table showing that this project will take 20 days from start to finish:
| **seq** | **path_seq** | **node** | **edge** | **cost** | **agg_cost** |
| ------- | ------------ | -------- | -------- | -------- | ------------ |
| 1 | 1 | 1 | 1 | 3 | 0 |
| 2 | 2 | 2 | 2 | 4 | 3 |
| 3 | 3 | 3 | 3 | 5 | 7 |
| 4 | 4 | 4 | 4 | 2 | 12 |
| 5 | 5 | 5 | 5 | 6 | 14 |
| 6 | 6 | 6 | -1 | 0 | 20 |
<details>
<summary>Tangent: the Dijkstra algorithm</summary>
<div>
The `pgr_dijkstra()` function implements [Dijkstra's
algorithm](https://en.wikipedia.org/wiki/Dijkstra's_algorithm), which is used to find the
shortest path between nodes in a graph. This algorithm guarantees the shortest path from a
source node to a target node (or all other nodes), based on the cost of edges connecting the
nodes.
</div>
<div>
Fun fact: Dijkstra's algorithm was published in 1959 by Dutch computer scientist Edsger
Dijkstra. It's a “greedy” algorithm, meaning it always picks the closest, cheapest node to
explore next.
</div>
</details>
### Reverse proxy routing based on resource allocation
Distributed systems usually involve allocating resources efficiently across a network of nodes. Each node might represent a physical location or a computing process, and the edges represent the available pathways to move resources between them. For example, in a cloud infrastructure, pgRouting could help determine how to allocate compute tasks across a set of distributed servers by finding the shortest or least-congested path to route data.
Suppose you have a network of servers represented by nodes and their data connections as edges in a table servers.
```sql
-- create the servers table representing the nodes
create table servers (
id serial primary key,
name text,
x double precision, -- x coordinate for spatial data (latitude)
y double precision -- y coordinate for spatial data (longitude)
);
-- insert some sample servers
insert into servers (name, x, y)
values
('server a', 0, 0),
('server b', 2, 1),
('server c', 4, 3),
('server d', 3, 5);
-- create the server_connections table representing the edges
create table server_latency (
id serial primary key,
source integer,
target integer,
cost double precision, -- cost could represent latency or bandwidth
x1 double precision, -- x coordinate of source
y1 double precision, -- y coordinate of source
x2 double precision, -- x coordinate of target
y2 double precision, -- y coordinate of target,
constraint fk_source foreign key (source) references servers (id),
constraint fk_target foreign key (target) references servers (id)
);
-- insert connections between servers
insert into server_latency (source, target, cost, x1, y1, x2, y2)
values
(1, 2, 1.5, 0, 0, 2, 1), -- server a -> server b with a cost of 1.5 (could be latency)
(2, 3, 2.0, 2, 1, 4, 3), -- server b -> server c with a cost of 2.0
(2, 4, 1.8, 2, 1, 3, 5), -- server b -> server d with a cost of 1.8
(4, 3, 1.0, 3, 5, 4, 3);
-- server d -> server c with a cost of 1.0
```
You can then use [`pgr_astar`](https://docs.pgrouting.org/latest/en/pgr_aStar.html)() to find the most efficient path for data or compute tasks to travel through this network, optimizing for speed or load:
```sql
-- Query to find the most efficient path (using pgr_astar)
select *
from
extensions.pgr_astar(
'select id, source, target, cost, x1, y1, x2, y2 from server_latency',
1,
3 -- Start from Server A (id=1) to Server C (id=3)
);
```
<details>
<summary>Tangent: the A\* algorithm</summary>
<div>
The `pgr_astar()` function is an implementation of the [A\* (A-star)
algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm). It's used to find the most
efficient (shortest) path between two points in a graph. A\* is commonly used in navigation and
routing because it is more efficient than Dijkstra's algorithm in many scenarios, especially
when you have spatial data with coordinates (e.g., X, Y positions).
</div>
<div>
Fun fact: A\* was originally designed in the 1960s for artificial intelligence applications and
pathfinding in games. Today, it's one of the most widely used algorithms in video game
development to help characters navigate complex environments efficiently.
</div>
</details>
### Recommendation engines like YouTube
In recommendation engines or search algorithms that use knowledge graphs, pgRouting can be used to build relationships between entities and events. Take YouTube's recommendation algorithm, we can structure this data as a graph where:
- **Nodes** represent entities like users, videos, or categories.
- **Edges** represent relationships or interactions between those entities, such as a user liking a video or videos being part of the same category.
Let's create a list of “nodes”:
```sql
create table categories (
id serial primary key,
name text
);
insert into categories (name)
values
('Graph Theory'),
('AI & Machine Learning'),
('Python Programming');
create table videos (
id serial primary key,
title text,
category_id int references categories (id)
);
insert into videos (title, category_id)
values
('Intro to Graph Theory', 1),
('Advanced Graph Algorithms', 1),
('Graph Neural Networks', 2),
('Beginner Python Tutorial', 3),
('Advanced Python Techniques', 3);
```
And some “edges”:
```sql
create table video_relationships (
source_video_id int references videos (id),
target_video_id int references videos (id),
relationship_type text, -- 'same_category', 'watched_by_same_users', etc.
weight int default 1 -- strength of the relationship
);
insert into video_relationships (source_video_id, target_video_id, relationship_type, weight)
values
(1, 2, 'same_category', 5), -- "Intro to Graph Theory" and "Advanced Graph Algorithms" are in the same category
(2, 3, 'watched_by_same_users', 3), -- "Advanced Graph Algorithms" and "Graph Neural Networks" are often watched together
(4, 5, 'same_category', 5); -- "Beginner Python Tutorial
create table interactions (
user_id int references auth.users (id),
video_id int references videos (id),
interaction_type text, -- 'liked', 'viewed', etc.
weight int default 1 -- strength of the interaction
);
insert into interactions (user_id, video_id, interaction_type, weight)
values
('user_01', 1, 'viewed', 5), -- "User 01" watched "Intro to Graph Theory" to the end (weight = 5)
('user_01', 2, 'liked', 5), -- "User 01" liked "Advanced Graph Algorithms"
('user_02', 3, 'viewed', 2), -- "User 02" watched "Graph Neural Networks" and bounced halfway through (weight = 2)
('user_03', 4, 'liked', 5), -- "User 03" liked "Beginner Python Tutorial"
('user_03', 5, 'viewed', 2);
-- "User 03" watched "Advanced Python Techniques" and bounced halfway through (weight = 2)
```
Now we can use the `pgr_dijkstra()` function to find the shortest or most relevant path between a user and new videos. For example, let's find videos that are most relevant to `user_01` considering their past interactions:
<details>
<summary>Tangent: ranking recommendations</summary>
<div>
Since it's “just postgres” it's simple enough to rank the results using an `order by` clause. For example, if we stored the `pgr_dijkstra()` results above in a table called “recommendations”, we use this a query like this to sort the paths by the highest ranking:
</div>
<div>
```sql
select videos.title, sum(weight) as recommendation_score
from
recommendations
join videos on recommendations.target = videos.id
group by videos.title
order by recommendation_score desc;
```
</div>
</details>
## Get Started
pgRouting is a powerful extension for Postgres that can be used to solve a wide range of graph-based problems. Check out the [pgRouting docs](https://docs.pgrouting.org/latest/en/index.html) for more information on how to use it. You can also use it on Supabase:
- Docs: [pgrouting: Geospatial Routing](/docs/guides/database/extensions/pgrouting)
- Launch a new Postgres database: [database.new](https://database.new)
Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
@@ -1,4 +1,4 @@
export type CloudProvider = 'FLY' | 'AWS'
export type CloudProvider = 'FLY' | 'AWS' | 'AWS_NEW'
export type Region = typeof AWS_REGIONS | typeof FLY_REGIONS
export const AWS_REGIONS = {