Merge branch 'master' into feature/branching-gitless-merge

This commit is contained in:
Saxon Fletcher committed 2025-06-05 11:20:03 +10:00
commit 1ebce69137
109 files changed
+1040 -698

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
---
description: Docs Testing Procedure
globs: apps/docs/**/*.test.ts
alwaysApply: false
---
# Docs Test Requirements
Rules for running tests in the docs application, ensuring proper Supabase setup and test execution.
<rule>
name: docs_test_requirements
description: Standards for running tests in the docs application with proper Supabase setup
filters:
# Match test files in the docs app
- type: file_extension
pattern: "\\.(test|spec)\\.(ts|tsx)$"
- type: path
pattern: "^apps/docs/.*"
# Match test execution events
- type: event
pattern: "test_execution"
actions:
- type: suggest
message: |
Before running tests in the docs app:
1. Check Supabase status:
```bash
pnpm supabase status
```
2. If Supabase is not running:
```bash
pnpm supabase start
```
3. Reset the database to ensure clean state:
```bash
pnpm supabase db reset --local
```
4. Run the tests:
```bash
pnpm run -F docs test:local:unwatch
```
Important notes:
- Always ensure Supabase is running before tests
- Database must be reset to ensure clean state
- Use test:local:unwatch to run tests without watch mode
- Tests are located in apps/docs/**/*.{test,spec}.{ts,tsx}
examples:
- input: |
# Bad: Running tests without proper setup
pnpm run -F docs test
pnpm run -F docs test:local
# Good: Proper test execution sequence
pnpm supabase status
pnpm supabase start # if not running
pnpm supabase db reset --local
pnpm run -F docs test:local:unwatch
output: "Correctly executed docs tests with proper Supabase setup"
metadata:
priority: high
version: 1.0
</rule>
@@ -38,7 +38,7 @@ We use surfaces in 2 different ways. In the ./www and ./docs sites, we use a dar
#### `./apps/studio`
For the studio (dashbaord) we can use `bg-surface-100`, `bg-surface-200`, `bg-surface-300`
For the studio (dashboard) we can use `bg-surface-100`, `bg-surface-200`, `bg-surface-300`
<CodeFragment name="color-usage-surface-studio" />
+2 -2
View File
@@ -29,8 +29,8 @@ export async function GET(request: Request) {
slug = maybeVersion
}
let section: AbbrevApiReferenceSection
let sectionsWithUrl: Array<AbbrevApiReferenceSection & { url: URL }>
let section: AbbrevApiReferenceSection | undefined
let sectionsWithUrl: Array<AbbrevApiReferenceSection & { url: URL }> = []
try {
const flattenedSections = (await getFlattenedSections(lib, version)) ?? []
sectionsWithUrl = flattenedSections.map((section) => {
@@ -22,9 +22,9 @@ export function ContributingToc({ className }: { className?: string }) {
...document.querySelectorAll('article.prose > h2,h3:not(#feedback-title)'),
] as Array<HTMLHeadingElement>
const tocItems = headings
.filter((heading) => !!heading.id)
.filter((heading) => !!heading.id && heading.textContent)
.map((heading) => ({
label: heading.textContent.substring(0, heading.textContent.length - 1), // Remove ending `#`
label: heading.textContent!.substring(0, heading.textContent!.length - 1), // Remove ending `#`
anchor: heading.id,
}))
setTocItems(tocItems)
@@ -83,6 +83,12 @@ export async function generateAiPromptMetadata(props: { params: Promise<{ slug:
const { slug } = await props.params
const prompt = await getAiPrompt(slug)
if (!prompt) {
return {
title: 'AI Prompt | Supabase Docs',
}
}
return {
title: `AI Prompt: ${prompt.heading} | Supabase Docs`,
}
@@ -1,3 +1,4 @@
import { notFound } from 'next/navigation'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import {
generateAiPromptMetadata,
@@ -12,7 +13,12 @@ export default async function AiPromptsPage(props: { params: Promise<{ slug: str
const { slug } = params
let { heading, content } = await getAiPrompt(slug)
const prompt = await getAiPrompt(slug)
if (!prompt) {
notFound()
}
let { heading, content } = prompt
content = `
## How to use
+16 -10
View File
@@ -15,8 +15,12 @@ const generateMetadata = async (_, parent: ResolvingMetadata): Promise<Metadata>
return {
alternates: {
...parentAlternates,
canonical: `${BASE_PATH}`,
...(parentAlternates && {
languages: parentAlternates.languages || undefined,
media: parentAlternates.media || undefined,
types: parentAlternates.types || undefined,
}),
},
}
}
@@ -251,15 +255,17 @@ const HomePage = () => (
</div>
<ul className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{MIGRATION_PAGES.sort((a, b) => a.name.localeCompare(b.name)).map((guide) => {
return (
<li key={guide.name} className="col-span-6 md:col-span-4">
<Link href={guide.url} passHref>
<IconPanel {...guide} title={guide.name} background={true} showLink={false} />
</Link>
</li>
)
})}
{MIGRATION_PAGES.sort((a, b) => (a.name || '').localeCompare(b.name || '')).map(
(guide) => {
return (
<li key={guide.name} className="col-span-6 md:col-span-4">
<Link href={guide.url || '#'} passHref>
<IconPanel {...guide} title={guide.name} background={true} showLink={false} />
</Link>
</li>
)
}
)}
</ul>
</div>
+2 -2
View File
@@ -6,11 +6,11 @@ import { Tabs, TabPanel } from '~/features/ui/Tabs'
type IParamProps = any
const ApiSchema = ({ schema, id }: IParamProps) => {
let example: string
let example: string | undefined
try {
example = sample(schema, { skipReadOnly: true, quiet: true })
} catch {
// ignore
example = undefined
}
return (
<Tabs
@@ -133,7 +133,7 @@ const AppleSecretGenerator = () => {
<input
type="file"
onChange={(e) => {
setFile({ file: e.target.files[0] })
setFile({ file: e.target.files?.[0] || null })
}}
/>
</div>
@@ -156,7 +156,7 @@ const AppleSecretGenerator = () => {
keyID,
teamID,
serviceID,
file.file
file.file!
)
setKeyID(kid)
setSecretKey(jwt)
+1 -1
View File
@@ -181,7 +181,7 @@ function useBreadcrumbs() {
return findMenuItemByUrl(menu, pathname, [])
}
function findMenuItemByUrl(menu, targetUrl, parents = []) {
function findMenuItemByUrl(menu: any, targetUrl: string, parents: any[] = []) {
// If the menu has items, recursively search through them
if (menu.items) {
for (let item of menu.items) {
@@ -24,7 +24,7 @@ function getLinkTarget(link: string): LinkTarget {
}
function getUniqueTags(json: Extension[]): string[] {
const tags = []
const tags: string[] = []
for (const item of json) {
if (item.tags) {
tags.push(...item.tags)
@@ -96,6 +96,8 @@ function Feedback({ className }: { className?: string }) {
const showNo = unanswered || isNo
async function sendFeedbackVote(response: Response) {
if (!supabase) return
const { error } = await supabase.from('feedback').insert({
vote: response,
page: pathname,
@@ -59,7 +59,7 @@ export default function JwtGenerator() {
const [jwtSecret, setJwtSecret] = useState(secret)
const [token, setToken] = useState(anonToken)
const [signedToken, setSignedToken] = useState('')
const [err, setErr] = useState<string>(null)
const [err, setErr] = useState<string>('')
const handleKeySelection = (e: ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value
@@ -71,7 +71,7 @@ export default function JwtGenerator() {
try {
const newTok = JSON.parse(e.target.value)
setToken(newTok)
setErr(null)
setErr('')
} catch (err) {
const errMessage =
!!err && typeof err === 'object' && 'message' in err && typeof err.message === 'string'
-196
View File
@@ -1,196 +0,0 @@
'use client'
import { ChevronsUpDown } from 'lucide-react'
import {
createContext,
forwardRef,
useCallback,
useContext,
useId,
useState,
PropsWithChildren,
useRef,
useEffect,
} from 'react'
import {
cn,
Badge,
Checkbox_Shadcn_ as Checkbox,
Popover_Shadcn_ as Popover,
PopoverContent_Shadcn_ as PopoverContent,
PopoverTrigger_Shadcn_ as PopoverTrigger,
} from 'ui'
interface Item {
value: string
label: string
}
const MultiSelectContext = createContext<{
selected: Item[]
handleSelect: (item: Item) => void
setOpen: (open: boolean) => void
}>({
selected: [],
handleSelect: () => {},
setOpen: () => {},
})
function MultiSelectProvider({
selected,
onSelectedChange,
setOpen,
children,
}: PropsWithChildren<{
selected: Item[]
onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void
setOpen: (open: boolean) => void
}>) {
const handleSelect = useCallback(
(item: Item) => {
onSelectedChange((current: Item[]) => {
const isSelected = current.some((currItem) => currItem.value === item.value)
if (isSelected) {
return current.filter((currItem) => currItem.value !== item.value)
} else {
return [...current, item]
}
})
},
[onSelectedChange]
)
return (
<MultiSelectContext.Provider value={{ selected, handleSelect, setOpen }}>
{children}
</MultiSelectContext.Provider>
)
}
function useMultiSelect() {
const context = useContext(MultiSelectContext)
if (!context) {
throw new Error('useMultiSelect must be used within a MultiSelectProvider')
}
return context
}
export function MultiSelect({
open: _controlledOpen,
setOpen: _setControlledOpen,
selected,
onSelectedChange,
children,
...props
}: PropsWithChildren<
{
open?: boolean
setOpen?: React.Dispatch<React.SetStateAction<boolean>>
selected?: Item[]
onSelectedChange: (selected: Item[] | ((selected: Item[]) => Item[])) => void
} & React.ComponentProps<typeof Popover>
>) {
const [_internalOpen, _setInternalOpen] = useState(false)
const open = _controlledOpen ?? _internalOpen
const setOpen = _setControlledOpen ?? _setInternalOpen
return (
<MultiSelectProvider selected={selected} onSelectedChange={onSelectedChange} setOpen={setOpen}>
<Popover open={open} onOpenChange={setOpen} {...props}>
{children}
</Popover>
</MultiSelectProvider>
)
}
const Trigger = forwardRef<
HTMLButtonElement,
{ label?: string; className?: string } & React.ComponentProps<typeof PopoverTrigger>
>(({ label, className, ...props }, ref) => {
const { selected } = useMultiSelect()
const [measuredWidth, setMeasuredWidth] = useState(0)
const measuredRef = (node: HTMLElement) => {
if (node) {
setMeasuredWidth(node.getBoundingClientRect().width)
}
}
const badgeWidth = 100 // Approximate width of a badge in pixels
const maxBadges = Math.max(1, Math.floor((measuredWidth || 200) / badgeWidth))
const visibleBadges = selected.slice(0, maxBadges)
const extraBadgesCount = Math.max(0, selected.length - maxBadges)
const badgeClasses = 'bg-200'
return (
<PopoverTrigger asChild ref={ref}>
<button
role="combobox"
className={cn(
'flex w-full min-w-[200px] items-center justify-between rounded-md border',
'border-alternative bg-foreground/[.026] px-3 py-2 text-sm',
'ring-offset-background placeholder:text-muted-foreground',
'focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'disabled:cursor-not-allowed disabled:opacity-50',
'hover:border-primary transition-colors duration-200',
className
)}
{...props}
>
{selected.length === 0 ? (
// Leading prevents shift when switching to badges
<span className="text-muted-foreground leading-[1.375rem]">{label}</span>
) : (
<div ref={measuredRef} className="flex gap-1 overflow-hidden">
{visibleBadges.map((item) => (
<Badge key={item.value} className={cn('shrink-0', badgeClasses)}>
{item.label}
</Badge>
))}
{extraBadgesCount > 0 && <Badge className={badgeClasses}>+{extraBadgesCount}</Badge>}
</div>
)}
<ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" />
</button>
</PopoverTrigger>
)
})
Trigger.displayName = 'Trigger'
MultiSelect.Trigger = Trigger
const Content = forwardRef<
HTMLDivElement,
PropsWithChildren<{ className?: string } & React.ComponentProps<typeof PopoverContent>>
>(({ children, className, ...props }, ref) => {
return (
<PopoverContent ref={ref} className={cn('w-full p-4 space-y-4', className)} {...props}>
{children}
</PopoverContent>
)
})
Content.displayName = 'Content'
MultiSelect.Content = Content
export function Item({ item, className }: { item: Item; className?: string }) {
const id = useId()
const { selected, handleSelect } = useMultiSelect()
return (
<div className={cn('flex items-center space-x-2', className)}>
<Checkbox
id={`${id}-checkbox-${item.value}`}
checked={selected.some((sel) => sel.value === item.value)}
onCheckedChange={() => handleSelect(item)}
/>
<label
htmlFor={`${id}-checkbox-${item.value}`}
className="text-sm leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{item.label}
</label>
</div>
)
}
MultiSelect.Item = Item
@@ -71,7 +71,7 @@ const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => {
</Accordion.Item>
) : (
<Link
href={section[0].href}
href={section[0].href || '#'}
className={cn(activeLabel === section[0].label && '!text-foreground', itemClassName)}
>
{section[0].label}
@@ -81,7 +81,7 @@ const GlobalNavigationMenu: FC = () => {
)}
>
{section[0].label === 'Home' ? (
<MenuIconPicker icon={section[0].icon} />
<MenuIconPicker icon={section[0].icon || ''} />
) : (
section[0].label
)}
@@ -125,7 +125,7 @@ const GlobalNavigationMenu: FC = () => {
>
<NavigationMenuLink asChild>
<Link
href={section[0].href}
href={section[0].href || '#'}
className={cn(
navigationMenuTriggerStyle(),
triggerClassName,
@@ -133,7 +133,7 @@ const GlobalNavigationMenu: FC = () => {
)}
>
{section[0].label === 'Home' ? (
<MenuIconPicker icon={section[0].icon} />
<MenuIconPicker icon={section[0].icon || ''} />
) : (
section[0].label
)}
@@ -3,7 +3,7 @@
import { createContext, useContext } from 'react'
interface ContextProps {
activeRefItem: string
activeRefItem: string | undefined
setActiveRefItem: (x: string) => void
}
@@ -15,7 +15,7 @@ interface Provider extends ContextProps {
// createContext matches the shape that the consumers expect!
const NavMenuContext = createContext<ContextProps>({
activeRefItem: undefined,
setActiveRefItem: undefined,
setActiveRefItem: () => {},
})
export const NavigationMenuContextProvider = (props: Provider) => {
@@ -1,9 +1,9 @@
import { memo } from 'react'
import type { NavMenuSection } from '../Navigation.types'
import { useCloseMenuOnRouteChange } from './NavigationMenu.utils'
import NavigationMenuGuideList from './NavigationMenuGuideList'
import NavigationMenuRefList from './NavigationMenuRefList'
import { useCloseMenuOnRouteChange } from './NavigationMenu.utils'
enum MenuId {
GettingStarted = 'gettingstarted',
@@ -251,6 +251,8 @@ function getMenuById(id: MenuId) {
}
function getMenuElement(menu: Menu | undefined, props?: any) {
if (!menu) return null
const menuType = menu?.type
switch (menuType) {
case 'guide':
@@ -260,7 +262,7 @@ function getMenuElement(menu: Menu | undefined, props?: any) {
<NavigationMenuRefList
id={menu.id}
basePath={menu.path}
commonSectionsFile={menu.commonSectionsFile}
commonSectionsFile={menu.commonSectionsFile || ''}
/>
)
default:
@@ -283,5 +285,5 @@ const NavigationMenu = ({
return getMenuElement(menu, { additionalNavItems })
}
export { MenuId, getMenuById }
export { getMenuById, MenuId }
export default memo(NavigationMenu)
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import type { ICommonItem } from '~/components/reference/Reference.types'
import type { Json } from '~/features/helpers.types'
@@ -52,7 +52,10 @@ export function deepFilterSections<T extends ICommonItem>(
*
* See https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import
*/
export function useCommonSections(commonSectionsFile: string) {
export function useCommonSections(
commonSectionsFile: string,
{ enabled = true }: { enabled: boolean }
) {
const [commonSections, setCommonSections] = useState<ICommonItem[]>()
useEffect(() => {
@@ -67,6 +70,10 @@ export function useCommonSections(commonSectionsFile: string) {
fetchCommonSections()
}, [commonSectionsFile])
if (!enabled) {
return null
}
return commonSections
}
@@ -16,7 +16,7 @@ const NavigationMenuRefList = ({
commonSectionsFile,
specFile,
}: NavigationMenuRefListProps) => {
const commonSections = useCommonSections(commonSectionsFile)
const commonSections = useCommonSections(commonSectionsFile, { enabled: !!commonSectionsFile })
const spec = useSpec(specFile)
if (!commonSections) {
+3 -3
View File
@@ -28,7 +28,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
if (foundItem) return group
})
const currentSubSection: NavMenuSection =
const currentSubSection: NavMenuSection | undefined =
currentSection !== undefined
? currentSection.items.find((section) => {
if (section.items.length === 0) {
@@ -113,7 +113,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
{group.items.map((section: NavMenuSection) => {
if (section.items.length === 0) {
return (
<Link href={section.url} key={section.name}>
<Link href={section.url || '#'} key={section.name}>
<div
className={[
'py-1.5 px-5 rounded text-sm transition',
@@ -150,7 +150,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
</Accordion.Trigger>
<Accordion.Content className="my-2 data-open:animate-slide-down data-closed:animate-slide-up">
{section.items.map((item: NavMenuSection) => (
<Link key={item.name} href={item.url}>
<Link key={item.name} href={item.url || '#'}>
<div
key={item.name}
className={[
@@ -8,7 +8,7 @@ import type {
} from '~/components/ProjectConfigVariables/ProjectConfigVariables.utils'
import type { ProjectApiData } from '~/lib/fetch/projectApi'
import { Copy, Check } from 'lucide-react'
import { Check, Copy } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useMemo } from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
@@ -16,7 +16,7 @@ import { withErrorBoundary } from 'react-error-boundary'
import { proxy, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsUserLoading } from 'common'
import { Button_Shadcn_ as Button, Input_Shadcn_ as Input, cn } from 'ui'
import { Button_Shadcn_ as Button, cn, Input_Shadcn_ as Input } from 'ui'
import {
ComboBox,
@@ -35,7 +35,7 @@ import { useBranchesQuery } from '~/lib/fetch/branches'
import { useOrganizationsQuery } from '~/lib/fetch/organizations'
import { type SupavisorConfigData, useSupavisorConfigQuery } from '~/lib/fetch/pooler'
import { useProjectApiQuery } from '~/lib/fetch/projectApi'
import { isProjectPaused, type ProjectsData, useProjectsQuery } from '~/lib/fetch/projects'
import { isProjectPaused, useProjectsQuery } from '~/lib/fetch/projects'
import { retrieve, storeOrRemoveNull } from '~/lib/storage'
import { useOnLogout } from '~/lib/userAuth'
@@ -122,14 +122,16 @@ function OrgProjectSelector() {
() =>
stateSummary !== 'loggedIn.dataSuccess.hasData'
? []
: projects.map((project) => {
const organization = organizations.find((org) => org.id === project.organization_id)
return {
id: project.ref,
value: toOrgProjectValue(organization, project),
displayName: toDisplayNameOrgProject(organization, project),
}
}),
: (projects!
.map((project) => {
const organization = organizations!.find((org) => org.id === project.organization_id)!
return {
id: project.ref,
value: toOrgProjectValue(organization, project),
displayName: toDisplayNameOrgProject(organization, project),
}
})
.filter(Boolean) as ComboBoxOption[]),
[organizations, projects, stateSummary]
)
@@ -138,18 +140,18 @@ function OrgProjectSelector() {
const storedMaybeOrgId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_ORG)
const storedMaybeProjectRef = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_PROJECT)
let storedOrg: Org
let storedProject: Project
let storedOrg: Org | undefined
let storedProject: Project | undefined
if (storedMaybeOrgId && storedMaybeProjectRef) {
storedOrg = organizations.find((org) => org.id === Number(storedMaybeOrgId))
storedProject = projects.find((project) => project.ref === storedMaybeProjectRef)
storedOrg = organizations!.find((org) => org.id === Number(storedMaybeOrgId))
storedProject = projects!.find((project) => project.ref === storedMaybeProjectRef)
}
if (storedOrg && storedProject && storedProject.organization_id === storedOrg.id) {
setSelectedOrgProject(storedOrg, storedProject)
} else {
const firstProject = projects[0]
const matchingOrg = organizations.find((org) => org.id === firstProject.organization_id)
} else if (projects!.length > 0) {
const firstProject = projects![0]
const matchingOrg = organizations!.find((org) => org.id === firstProject.organization_id)
if (matchingOrg) setSelectedOrgProject(matchingOrg, firstProject)
}
}
@@ -172,8 +174,8 @@ function OrgProjectSelector() {
const [orgId, projectRef] = fromOrgProjectValue(optionValue)
if (!orgId || !projectRef) return
const org = organizations.find((org) => org.id === orgId)
const project = projects.find((project) => project.ref === projectRef)
const org = organizations?.find((org) => org.id === orgId)
const project = projects?.find((project) => project.ref === projectRef)
if (org && project && project.organization_id === org.id) {
setSelectedOrgProject(org, project)
@@ -214,7 +216,7 @@ function BranchSelector() {
const formattedData: ComboBoxOption[] =
stateSummary !== 'loggedIn.branches.dataSuccess.hasData'
? []
: data.map((branch) => ({
: data!.map((branch) => ({
id: branch.id,
displayName: branch.name,
value: toBranchValue(branch),
@@ -224,18 +226,18 @@ function BranchSelector() {
if (stateSummary === 'loggedIn.branches.dataSuccess.hasData' && !selectedBranch) {
const storedMaybeBranchId = retrieve('local', LOCAL_STORAGE_KEYS.SAVED_BRANCH)
let storedBranch: Branch
let storedBranch: Branch | undefined
if (storedMaybeBranchId) {
storedBranch = data.find((branch) => branch.id === storedMaybeBranchId)
storedBranch = data!.find((branch) => branch.id === storedMaybeBranchId)
}
if (storedBranch) {
setSelectedBranch(storedBranch)
} else {
const productionBranch = data.find(
const productionBranch = data!.find(
(branch) => branch.project_ref === branch.parent_project_ref
)
setSelectedBranch(productionBranch ?? data[0])
setSelectedBranch(productionBranch ?? data![0])
}
}
}, [data, selectedBranch, setSelectedBranch, stateSummary])
@@ -255,7 +257,7 @@ function BranchSelector() {
onSelectOption={(option) => {
const [branchId] = fromBranchValue(option)
if (branchId) {
const branch = data.find((branch) => branch.id === branchId)
const branch = data?.find((branch) => branch.id === branchId)
if (branch) setSelectedBranch(branch)
}
}}
@@ -322,23 +324,23 @@ function VariableView({ variable, className }: { variable: Variable; className?:
? 'loggedIn.selectedProject.dataPending'
: (
needsApiQuery
? isApiError || isInvalidApiData(apiData)
: isSupavisorError || isInvalidSupavisorData(supavisorConfig)
? isApiError || isInvalidApiData(apiData!)
: isSupavisorError || isInvalidSupavisorData(supavisorConfig!)
)
? 'loggedIn.selectedProject.dataError'
: 'loggedIn.selectedProject.dataSuccess'
let variableValue: string = null
let variableValue: string = ''
if (stateSummary === 'loggedIn.selectedProject.dataSuccess') {
switch (variable) {
case 'url':
variableValue = `https://${apiData.app_config!.endpoint}`
variableValue = `https://${apiData?.app_config?.endpoint}`
break
case 'anonKey':
variableValue = apiData.service_api_keys!.find((key) => key.tags === 'anon')!.api_key
variableValue = apiData?.service_api_keys?.find((key) => key.tags === 'anon')?.api_key || ''
break
case 'sessionPooler':
variableValue = supavisorConfig[0].connection_string
variableValue = supavisorConfig?.[0]?.connection_string || ''
}
}
@@ -137,7 +137,11 @@ export default function RealtimeLimitsEstimater({}) {
<Button
type="text"
icon={
<ChevronDown size={18} strokeWidth={2} className={expandPreview && 'rotate-180'} />
<ChevronDown
size={18}
strokeWidth={2}
className={expandPreview ? 'rotate-180' : undefined}
/>
}
className="px-1"
onClick={() => setExpandPreview(!expandPreview)}
@@ -2,7 +2,10 @@ import { createContext } from 'react'
// Make sure the shape of the default value passed to
// createContext matches the shape that the consumers expect!
export const StepHikeContext = createContext({
export const StepHikeContext = createContext<{
activeStep: { titleId: string; step: number } | undefined
steps: any[] | undefined
}>({
activeStep: undefined,
steps: undefined,
})
+11 -7
View File
@@ -2,21 +2,25 @@ import { useEffect, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { StepHikeContext } from './StepHikeContext'
const StepHike = ({ children, title }) => {
const [activeStep, setActiveStep] = useState(undefined)
const StepHike = ({ children }) => {
const [activeStep, setActiveStep] = useState<{ titleId: string; step: number } | undefined>(
undefined
)
// check if there are any children
if (!children) throw 'StepHike component requires <StepHike.Step> children'
const steps = children.filter((x) => {
const steps = children.filter((x: any) => {
return x.type.name === 'Step'
})
useEffect(() => {
setActiveStep({
titleId: steps[0].props.title.replaceAll(' ', '-').toLowerCase(),
step: 0,
})
if (steps.length > 0) {
setActiveStep({
titleId: steps[0].props.title.replaceAll(' ', '-').toLowerCase(),
step: 0,
})
}
}, [])
// check if there is at least 1 StepHike subcomponent
+6 -3
View File
@@ -4,7 +4,7 @@ import { cn } from 'ui'
type TableProps = TableHTMLAttributes<HTMLTableElement>
const Table = ({ children, ...props }: TableProps) => {
const containerRef = useRef(null)
const containerRef = useRef<HTMLDivElement>(null)
const [showShadow, setShowShadow] = useState(true)
const handleScroll = () => {
@@ -18,8 +18,11 @@ const Table = ({ children, ...props }: TableProps) => {
}
useEffect(() => {
containerRef?.current?.addEventListener('scroll', handleScroll)
return () => containerRef?.current?.removeEventListener('scroll', handleScroll)
const container = containerRef.current
if (container) {
container.addEventListener('scroll', handleScroll)
return () => container.removeEventListener('scroll', handleScroll)
}
}, [])
return (
@@ -1,10 +1,10 @@
---
title: 'Connect to your database'
description: 'Connect to Postgres from your frontend, backend, or serverless environment'
subtitle: 'Connect to Postgres from your frontend, backend, or serverless environment'
subtitle: 'Supabase provides multiple methods to connect to your Postgres database, whether you’re working on the frontend, backend, or utilizing serverless functions.'
---
## Quick summary
## How to connect to your Postgres databases
How you connect to your database depends on where you're connecting from:
@@ -52,7 +52,7 @@ The Data APIs allow you to interact with your database using REST or GraphQL req
- [REST](/docs/guides/api)
- [GraphQL](/docs/guides/graphql/api)
For convenience, you can also use the Supabase client libraries, which wrap the Data APIs with a developer-friendly interface and automatically handle authentication:
For convenience, you can also use the [Supabase client libraries](/docs/reference), which wrap the Data APIs with a developer-friendly interface and automatically handle authentication:
- [JavaScript](/docs/reference/javascript)
- [Flutter](/docs/reference/dart)
@@ -167,3 +167,43 @@ You can obtain your connection info and Server root certificate from your applic
## Resources
- [Connection management](/docs/guides/database/connection-management)
- [Connecting with psql](/docs/guides/database/psql)
- [Importing data into Supabase](/docs/guides/database/import-data)
## Troubleshooting and Postgres connection string FAQs
Below are answers to common challenges and queries.
### What is a “connection refused” error?
A “Connection refused” error typically means your database isn’t reachable. Ensure your Supabase project is running, confirm your database’s connection string, check firewall settings, and validate network permissions.
### What is the “FATAL: Password authentication failed” error?
This error occurs when your credentials are incorrect. Double-check your username and password from the Supabase dashboard. If the problem persists, reset your database password from the project settings.
### How do you connect using IPv4?
Supabase’s default direct connection supports IPv6 only. To connect over IPv4, consider using the Supavisor session or transaction modes, or a connection pooler (shared or dedicated), which support both IPv4 and IPv6.
### How do you choose a connection method?
- Direct connection: Persistent backend services (IPv6 only)
- Supavisor session mode: Persistent backend needing IPv4
- Supavisor transaction mode: Serverless functions
- Shared pooler: General-purpose connections with IPv4 and IPv6
- Dedicated pooler: High-performance apps requiring dedicated resources (paid tier)
### Where is the Postgres connection string in Supabase?
Your connection string is located in the Supabase Dashboard. Click the "Connect" button at the top of the page.
### Can you use `psql` with a Supabase database?
Yes. Use the following command structure, replacing `your_connection_string` with the string from your Supabase dashboard:
```
psql "your_connection_string"
```
Ensure you have `psql` installed locally before running this command.
@@ -5,8 +5,12 @@ breadcrumb: 'GUI Quickstarts'
hideToc: true
---
## What is pgAdmin?
[`pgAdmin`](https://www.pgadmin.org/) is a GUI tool for managing Postgres databases. You can use it to connect to your database via SSL.
## Connecting pgAdmin with your Postgres database
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
@@ -83,3 +87,11 @@ hideToc: true
</StepHikeCompact.Step>
</StepHikeCompact>
## Why connect to pgAdmin
Connecting your Postgres instance to `pgAdmin` gives you a free, cross-platform GUI that makes tasks such as browsing objects, writing queries with autocomplete, running backups, and monitoring performance much faster and safer than using `psql` alone.
It acts as a single control panel where you can manage multiple servers, inspect locks and slow queries in real time, and perform maintenance operations with a click.
For scripted migrations or ultra-light remote work you’ll still lean on plain SQL or CLI tools, but most teams find `pgAdmin` invaluable for exploration and routine administration.
@@ -82,6 +82,12 @@ This time, we are revoking the column-level `UPDATE` privilege of the `title` co
## Manage column privileges in the Dashboard
<Admonition type="caution">
Column-level privileges are a powerful tool, but they're also quite advanced and in many cases, not the best fit for common access control needs. For that reason, we've intentionally moved the UI for this feature under the Feature Preview section in the dashboard.
</Admonition>
You can view and edit the privileges in the [Supabase Studio](https://supabase.com/dashboard/project/_/database/column-privileges).
![Column level privileges](/docs/img/guides/privileges/column-level-privileges-2.png)
@@ -0,0 +1,69 @@
---
title = "Database API 42501 errors"
topics = [
"database"
]
github_url = "https://github.com/orgs/supabase/discussions/31293"
[[errors]]
http_status_code = 401
code = "42501"
[[errors]]
http_status_code = 403
code = "42501"
---
[Postgres 42501 errors](https://www.postgresql.org/docs/current/errcodes-appendix.html), often reported by clients as 401 or 403 errors, imply the request lacked adequate privileges. They can be viewed in the [log explorer](https://supabase.com/dashboard/project/_/logs/explorer?q=select%0A++++cast%28postgres_logs.timestamp+as+datetime%29+as+timestamp%2C%0A++++event_message%2C%0A++++parsed.error_severity%2C%0A++++parsed.user_name%2C%0A++++parsed.query%2C%0A++++parsed.detail%2C%0A++++parsed.hint%2C%0A++++parsed.sql_state_code%2C%0A++++parsed.backend_type%0Afrom%0A++++postgres_logs%0A++++cross+join+unnest%28metadata%29+as+metadata%0A++++cross+join+unnest%28metadata.parsed%29+as+parsed%0Awhere%0A++++parsed.sql_state_code+%3D+%2742501%27%0Aorder+by%0A++++timestamp+desc%0Alimit+100%3B%0A) by running:
```sql
select
cast(postgres_logs.timestamp as datetime) as timestamp,
event_message,
parsed.error_severity,
parsed.user_name,
parsed.query,
parsed.detail,
parsed.hint
from
postgres_logs
cross join unnest(metadata) as metadata
cross join unnest(metadata.parsed) as parsed
where
regexp_contains(parsed.error_severity, 'ERROR|FATAL|PANIC')
and parsed.sql_state_code = '42501'
order by timestamp desc
limit 100;
```
They tend to be caused by one of the following factors.
### Attempted to access a forbidden schema
API roles cannot access certain schemas, most notably `auth` and `vault`. This restriction extends to Foreign Data Wrappers relying on `vault`. While you can bypass it using a [security definer function](https://supabase.com/docs/guides/database/functions?queryGroups=language&language=sql&queryGroups=example-view&example-view=sql#security-definer-vs-invoker), these schemas are intentionally restricted for security reasons.
### Attempted to access a custom schema
If you created a custom schema, you will have to give the Database API permission to query it. Follow our [Using Custom Schemas guide](https://supabase.com/docs/guides/api/using-custom-schemas) for more directions.
### Revoked object level access:
In rare cases, users may accidentally revoke object-level access in `public` from their API roles. To regrant full visibility, run the below code:
```sql
grant usage on schema public to anon, authenticated, service_role;
grant all on all tables in schema public to anon, authenticated, service_role;
grant all on all routines in schema public to anon, authenticated, service_role;
grant all ON all sequences in schema public to anon, authenticated, service_role;
alter default privileges for role postgres in schema public grant all on tables to anon, authenticated, service_role;
alter default privileges for role postgres in schema public grant all on routines to anon, authenticated, service_role;
alter default privileges for role postgres in schema public grant all on sequences to anon, authenticated, service_role;
```
### Configured column-level restrictions
If you've set column-based access in the [Dashboard](https://supabase.com/dashboard/project/_/database/column-privileges) or via SQL, queries will fail with a `42501` error when accessing restricted columns. This includes using `select *`, as it expands to include forbidden columns.
### RLS:
If the anon or authenticated roles attempt to UPDATE or INSERT values without the necessary RLS permissions, Postgres will return a 42501 error.
@@ -0,0 +1,107 @@
---
title = "Implementing soft deletes with supabase-js"
topics = []
keywords = ["supabase-js", "sdk", "javascript"]
github_url="https://github.com/orgs/supabase/discussions/32523"
---
When building modern applications, soft deletes are a common feature that lets you "delete" data while retaining it for potential recovery or historical tracking. This is especially useful in audit trails or when accidental deletions need undoing. Supabase makes this process seamless with Postgres views and the supabase-js library.
In this post, we’ll show how to implement soft deletes using Supabase and how to use Postgres views to manage your data efficiently.
## What are soft deletes?
Soft deletes don’t remove a record from the database. Instead, they mark it as "deleted" by updating a specific column, often named `deleted_at`, with a timestamp. This keeps the data in the database but excludes it from most queries unless explicitly required.
## Step 1: Add the `deleted_at` column
To implement soft deletes, start by adding a `deleted_at` column to your table.
Run this SQL in your Supabase SQL editor:
```sql
alter table items
add column deleted_at timestamptz;
```
This column will store the timestamp when a record is "deleted."
## Step 2: Create a view for active records
To ensure you only fetch non-deleted records by default, create a Postgres view that filters out rows where `deleted_at` is not null.
```sql
create view active_items as
select *
from items
where deleted_at is null;
```
With this view, you can now query `active_items` instead of `items` to get only active (non-deleted) rows.
## Step 3: Soft delete a record
Instead of deleting a record, update its `deleted_at` column with the current timestamp. Using `supabase-js`, it looks like this:
```javascript
await supabase.from('items').update({ deleted_at: new Date().toISOString() }).eq('id', 123)
```
This sets the `deleted_at` column for the item with `id` 123.
## Step 4: Query active records
To fetch only non-deleted rows, query the `active_items` view instead of the `items` table. Here's how you do it with `supabase-js`:
```javascript
const { data, error } = await supabase
.from('active_items') // Query the view, not the table
.select('*')
if (error) console.error('Error fetching active items:', error)
else console.log('Active items:', data)
```
## Step 5: Restore a soft-deleted record (optional)
To "restore" a soft-deleted record, set the `deleted_at` column back to `null`:
```javascript
await supabase.from('items').update({ deleted_at: null }).eq('id', 123)
```
This effectively un-deletes the record.
## Benefits of using views for soft deletes
- **Cleaner Queries:** No need to add `WHERE deleted_at IS NULL` to every query. Just query the view (`active_items`).
- **Separation of Concerns:** Views abstract the logic of filtering deleted records from your application code.
- **Efficiency:** Postgres handles the filtering in the view, reducing the complexity in your app.
## Full example with supabase-js
Here’s a complete example of implementing soft deletes with `supabase-js`:
```javascript
// 1. Soft delete an item
await supabase.from('items').update({ deleted_at: new Date().toISOString() }).eq('id', 123)
// 2. Query active (non-deleted) items
const { data, error } = await supabase
.from('active_items') // Query the view, not the table
.select('*')
if (error) console.error('Error fetching active items:', error)
else console.log('Active items:', data)
// 3. Restore a soft-deleted item
await supabase.from('items').update({ deleted_at: null }).eq('id', 123)
```
---
## Conclusion
Soft deletes are easy to implement with Supabase and Postgres. By combining a `deleted_at` column with views, you can cleanly separate active and deleted records, keeping your application logic simple and maintainable.
This approach provides the flexibility of retaining data for audits or recovery while keeping your app's interface clean and efficient. Start using soft deletes in your Supabase projects today!
+18 -6
View File
@@ -236,7 +236,7 @@ async function fetchSourceCodeContent(tree: Root, deps: Dependencies) {
function rewriteNodes(contentMap: Map<MdxJsxFlowElement, [CodeSampleMeta, string]>) {
for (const [node, [meta, content]] of contentMap) {
const lang = matchLang(meta.path.split('.').pop())
const lang = matchLang(meta.path.split('.').pop() || '')
const source = isExternalSource(meta)
? `https://github.com/${meta.org}/${meta.repo}/blob/${meta.commit}${meta.path}`
@@ -286,6 +286,7 @@ function rewriteNodes(contentMap: Map<MdxJsxFlowElement, [CodeSampleMeta, string
existingWrapper.attributes[0].value = newSource
} else if (
typeof existingSource !== 'string' &&
existingSource &&
existingSource.type === 'mdxJsxAttributeValueExpression'
) {
const existingSourceArray =
@@ -405,7 +406,10 @@ export function _createElidedLine(
function isContainedInJsx(tree: acorn.Node, line: number) {
const acornNodeContainsLine = (node: acorn.Node, line) =>
node.loc?.start.line <= line && node.loc?.end.line >= line
node.loc?.start?.line != null &&
node.loc?.end?.line != null &&
node.loc.start.line <= line &&
node.loc.end.line >= line
if (!acornNodeContainsLine(tree, line)) {
return false
}
@@ -430,10 +434,18 @@ function isContainedInJsx(tree: acorn.Node, line: number) {
continue
} else {
if (
child.loc?.start?.line > candidateNarrowestContainingNode.loc?.start?.line ||
child.loc.end.line < candidateNarrowestContainingNode.loc?.end?.line ||
child.loc.start.column > candidateNarrowestContainingNode.loc?.start.column ||
child.loc.end.column < candidateNarrowestContainingNode.loc?.end.column
(child.loc?.start?.line != null &&
candidateNarrowestContainingNode.loc?.start?.line != null &&
child.loc.start.line > candidateNarrowestContainingNode.loc.start.line) ||
(child.loc?.end?.line != null &&
candidateNarrowestContainingNode.loc?.end?.line != null &&
child.loc.end.line < candidateNarrowestContainingNode.loc.end.line) ||
(child.loc?.start?.column != null &&
candidateNarrowestContainingNode.loc?.start?.column != null &&
child.loc.start.column > candidateNarrowestContainingNode.loc.start.column) ||
(child.loc?.end?.column != null &&
candidateNarrowestContainingNode.loc?.end?.column != null &&
child.loc.end.column < candidateNarrowestContainingNode.loc.end.column)
) {
candidateNarrowestContainingNode = child
getNarrowestContainingNode(child, line)
+1 -1
View File
@@ -35,7 +35,7 @@ const TocAnchorsProvider = ({ children }: PropsWithChildren) => {
const newHeadings = headings
.filter((heading) => heading.id)
.map((heading) => {
const text = heading.textContent.replace('#', '')
const text = heading.textContent?.replace('#', '') || ''
const link = heading.querySelector('a')?.getAttribute('href')
if (!link) return null
+2 -2
View File
@@ -92,14 +92,14 @@ const getGuidesMarkdown = cache_fullProcess_withDevCacheBust(
const genGuidesStaticParams = (directory?: string) => async () => {
const promises = directory
? (await readdir(join(GUIDES_DIRECTORY, directory), { recursive: true }))
.filter((file) => extname(file) === '.mdx' && !file.split(sep).at(-1).startsWith('_'))
.filter((file) => extname(file) === '.mdx' && !file.split(sep).at(-1)?.startsWith('_'))
.map((file) => ({ slug: file.replace(/\.mdx$/, '').split(sep) }))
.concat(
(await existsFile(join(GUIDES_DIRECTORY, `${directory}.mdx`))) ? [{ slug: [] }] : []
)
: PUBLISHED_SECTIONS.map(async (section) =>
(await readdir(join(GUIDES_DIRECTORY, section), { recursive: true }))
.filter((file) => extname(file) === '.mdx' && !file.split(sep).at(-1).startsWith('_'))
.filter((file) => extname(file) === '.mdx' && !file.split(sep).at(-1)?.startsWith('_'))
.map((file) => ({
slug: [section, ...file.replace(/\.mdx$/, '').split(sep)],
}))
@@ -78,12 +78,12 @@ function mapEndpointsById(
const endpoints = spec.paths
const endpointsById = new Map<string, IApiEndPoint>()
Object.entries(endpoints).forEach(([path, methods]) => {
Object.entries(methods).forEach(([method, details]) => {
Object.entries(endpoints as Record<string, any>).forEach(([path, methods]) => {
Object.entries(methods as Record<string, any>).forEach(([method, details]) => {
endpointsById.set(getId(details), {
id: getId(details),
path,
method,
method: method as 'get' | 'post' | 'put' | 'delete' | 'patch',
...details,
})
})
@@ -92,13 +92,16 @@ function mapEndpointsById(
return endpointsById
}
function genClientSdkSectionTree(fns: Array<{ id: unknown }>, excludeName: string) {
function genClientSdkSectionTree(
fns: Array<{ id: unknown }>,
excludeName: string
): AbbrevApiReferenceSection[] {
const validSections = deepFilterRec(
commonClientLibSections as Array<AbbrevApiReferenceSection>,
commonClientLibSections as AbbrevApiReferenceSection[],
'items',
(section) =>
section.type === 'markdown' || section.type === 'category'
? !('excludes' in section && section.excludes.includes(excludeName))
? !('excludes' in section && section.excludes?.includes(excludeName))
: section.type === 'function'
? fns.some(({ id }) => section.id === id)
: true
@@ -106,11 +109,11 @@ function genClientSdkSectionTree(fns: Array<{ id: unknown }>, excludeName: strin
return validSections
}
async function genCliSectionTree() {
async function genCliSectionTree(): Promise<AbbrevApiReferenceSection[]> {
const cliSpec = await getSpec('cli_v1_commands', { ext: 'yaml' })
const validSections = deepFilterRec(
cliCommonSections as Array<AbbrevApiReferenceSection>,
cliCommonSections as AbbrevApiReferenceSection[],
'items',
(section) =>
section.type === 'cli-command' ? cliSpec.commands.some(({ id }) => id === section.id) : true
@@ -118,9 +121,9 @@ async function genCliSectionTree() {
return validSections
}
function genApiSectionTree(endpointsById: Map<string, IApiEndPoint>) {
function genApiSectionTree(endpointsById: Map<string, IApiEndPoint>): AbbrevApiReferenceSection[] {
const validSections = deepFilterRec(
apiCommonSections as Array<AbbrevApiReferenceSection>,
apiCommonSections as AbbrevApiReferenceSection[],
'items',
(section) => (section.type === 'operation' ? endpointsById.has(section.id) : true)
)
@@ -131,7 +134,7 @@ function genSelfHostedSectionTree(
spec: Array<AbbrevApiReferenceSection>,
endpointsById: Map<string, IApiEndPoint>
) {
const validSections = deepFilterRec(spec, 'items', (section) =>
const validSections = deepFilterRec(spec as any, 'items', (section: any) =>
section.type === 'self-hosted-operation' ? endpointsById.has(section.id) : true
)
return validSections
@@ -143,7 +146,7 @@ export function flattenCommonClientLibSections(tree: Array<AbbrevApiReferenceSec
const prunedElem = { ...elem }
delete prunedElem.items
acc.push(prunedElem)
acc.push(...flattenCommonClientLibSections(elem.items))
acc.push(...flattenCommonClientLibSections(elem.items || []))
} else {
acc.push(elem)
}
@@ -289,7 +292,7 @@ async function writeSelfHostingReferenceSections() {
selfHostingSpecs.flatMap((service) => {
let tasks: Promise<any>[] = []
let endpointsById: Map<string, IApiEndPoint>
let endpointsById: Map<string, IApiEndPoint> = new Map()
if (service.spec) {
endpointsById = mapEndpointsById(service.spec, (details) =>
slugify(details.summary || `dummy-id-${String(id++)}`, {
@@ -313,7 +316,9 @@ async function writeSelfHostingReferenceSections() {
)
)
const flattenedSelfHostedSections = flattenCommonClientLibSections(selfHostedSectionTree)
const flattenedSelfHostedSections = flattenCommonClientLibSections(
selfHostedSectionTree as AbbrevApiReferenceSection[]
)
tasks.push(
writeFile(
join(GENERATED_DIRECTORY, `${service.id}.latest.flat.json`),
+1 -1
View File
@@ -22,7 +22,7 @@ function ClientLibHeader({ menuData, className }: ClientLibHeaderProps) {
<div className={cn('flex items-start gap-6', className)}>
{'icon' in menuData && (
<MenuIconPicker
icon={menuData.icon}
icon={menuData.icon || ''}
width={35}
height={35}
className="text-foreground-light"
@@ -61,7 +61,7 @@ export function ReferenceNavigationScrollHandler({
...rest
}: PropsWithChildren & HTMLAttributes<HTMLDivElement>) {
const parentRef = useRef<HTMLElement>()
const ref = useRef<HTMLDivElement>()
const ref = useRef<HTMLDivElement>(null)
const initialScrollHappened = useContext(ReferenceContentInitiallyScrolledContext)
useEffect(() => {
@@ -70,7 +70,9 @@ export function ReferenceNavigationScrollHandler({
let scrollingParent: HTMLElement = ref.current
while (scrollingParent && !(scrollingParent.scrollHeight > scrollingParent.clientHeight)) {
scrollingParent = scrollingParent.parentElement
const parent = scrollingParent.parentElement
if (!parent) break
scrollingParent = parent
}
parentRef.current = scrollingParent
@@ -180,7 +182,7 @@ export function RefLink({
skipChildren?: boolean
className?: string
}) {
const ref = useRef<HTMLAnchorElement>()
const ref = useRef<HTMLAnchorElement>(null)
const pathname = usePathname()
const href = deriveHref(basePath, section)
@@ -189,7 +191,7 @@ export function RefLink({
useEffect(() => {
if (ref.current) {
ref.current.ariaCurrent = isActive ? 'page' : undefined
ref.current.ariaCurrent = isActive ? 'page' : null
ref.current.className = getLinkStyles(isActive, className)
}
}, [isActive, className])
@@ -201,7 +203,8 @@ export function RefLink({
if (!('title' in section)) return null
const isCompoundSection = !skipChildren && 'items' in section && section.items.length > 0
const isCompoundSection =
!skipChildren && 'items' in section && section.items && section.items.length > 0
return (
<>
@@ -232,7 +235,7 @@ function useCompoundRefLinkActive(basePath: string, section: AbbrevApiReferenceS
const isParentActive = pathname === parentHref
const childHrefs = useMemo(
() => new Set(section.items.map((item) => deriveHref(basePath, item))),
() => new Set((section.items || []).map((item) => deriveHref(basePath, item))),
[basePath, section]
)
const isChildActive = childHrefs.has(pathname)
@@ -288,7 +291,7 @@ function CompoundRefLink({
>
<ul className="space-y-2">
<RefLink basePath={basePath} section={section} skipChildren />
{section.items.map((item, idx) => {
{(section.items || []).map((item, idx) => {
return (
<li key={`${section.id}-${idx}`}>
<RefLink basePath={basePath} section={item} />
@@ -35,12 +35,12 @@ export async function ReferenceNavigation({
return (
<ReferenceNavigationScrollHandler className="w-full flex flex-col pt-3 pb-5 gap-3">
<div className="flex items-center gap-3">
{'icon' in menuData && <MenuIconPicker icon={menuData.icon} width={21} height={21} />}
{'icon' in menuData && <MenuIconPicker icon={menuData.icon || ''} width={21} height={21} />}
<span className="text-base text-brand-600">{name}</span>
<RefVersionDropdown library={libPath} currentVersion={version} />
</div>
<ul className="flex flex-col gap-2">
{navSections.map((section) => (
{navSections?.map((section) => (
<Fragment key={section.id}>
{section.type === 'category' ? (
<li>
@@ -67,14 +67,14 @@ function RefCategory({
basePath: string
section: AbbrevApiReferenceSection
}) {
if (!('items' in section && section.items.length > 0)) return null
if (!('items' in section && section.items && section.items.length > 0)) return null
return (
<>
<Divider />
{'title' in section && <SideMenuTitle className="py-2">{section.title}</SideMenuTitle>}
<ul className="space-y-2">
{section.items.map((item) => (
{section.items?.map((item) => (
<li key={item.id} className={topLvlRefNavItemStyles}>
<RefLink basePath={basePath} section={item} />
</li>
@@ -38,11 +38,13 @@ type RefSectionsProps = {
async function RefSections({ libraryId, version }: RefSectionsProps) {
let flattenedSections = await getFlattenedSections(libraryId, version)
flattenedSections = trimIntro(flattenedSections)
if (flattenedSections) {
flattenedSections = trimIntro(flattenedSections)
}
return (
<div className="flex flex-col my-16 gap-16">
{flattenedSections
{(flattenedSections || [])
.filter((section) => section.type !== 'category')
.map((section, idx) => (
<Fragment key={`${section.id}-${idx}`}>
@@ -405,7 +407,7 @@ async function FunctionSection({
}: FunctionSectionProps) {
const fns = await getFunctionsList(sdkId, version)
const fn = fns.find((fn) => fn.id === section.id)
const fn = fns?.find((fn) => fn.id === section.id)
if (!fn) return null
let types: MethodTypes | undefined
@@ -194,7 +194,7 @@ interface CommentBlockTag {
content: CommentKind[]
}
function normalizeComment(original: TypedocComment | Comment): Comment {
function normalizeComment(original: TypedocComment | Comment | undefined): Comment | undefined {
if (!original) return
if ('shortText' in original || 'text' in original) {
@@ -299,7 +299,7 @@ function parseModInternal(
case 'property':
case 'reference':
default:
return undefined
return
}
}
@@ -379,7 +379,7 @@ function parseSignature(
map: Map<number, any>
): {
params: Array<FunctionParameterType>
ret: ReturnType
ret: ReturnType | undefined
comment?: Comment
} {
const params: Array<FunctionParameterType> = (signature.parameters ?? []).map((param: any) => {
@@ -401,7 +401,7 @@ function parseSignature(
return res
})
let ret: ReturnType
let ret: ReturnType | undefined
if ('type' in signature) {
const retType = parseType(signature.type, map)
if (retType) {
@@ -643,10 +643,10 @@ function parsePickType(type: any, map: Map<number, any>) {
return undefined
}
function parseReflectionType(type: any, map: Map<number, any>) {
function parseReflectionType(type: any, map: Map<number, any>): TypeDetails | undefined {
if (!type.declaration) return undefined
let res: TypeDetails
let res: TypeDetails | undefined
switch ((type.declaration.kindString ?? type.declaration.variant).toLowerCase()) {
case 'type literal':
res = parseTypeLiteral(type, map)
@@ -662,7 +662,7 @@ function parseReflectionType(type: any, map: Map<number, any>) {
return res
}
function parseTypeLiteral(type: any, map: Map<number, any>): TypeDetails {
function parseTypeLiteral(type: any, map: Map<number, any>): TypeDetails | undefined {
const name = nameOrAnonymous(type)
if ('children' in type.declaration) {
@@ -67,7 +67,7 @@ export function ApiOperationBodySchemeSelector({
requestBody: IApiEndPoint['requestBody']
className?: string
}) {
const availableSchemes = Object.keys(requestBody.content) as Array<
const availableSchemes = Object.keys(requestBody?.content || {}) as Array<
'application/json' | 'application/x-www-form-urlencoded'
>
const [selectedScheme, setSelectedScheme] = useState(availableSchemes[0])
@@ -75,11 +75,10 @@ export function ApiOperationBodySchemeSelector({
const containerRef = useRef<HTMLDivElement>(null)
const allSchemeDetails = useRef<HTMLUListElement[]>([])
useEffect(() => {
allSchemeDetails.current = Array.from(
containerRef.current?.querySelectorAll(
`[${API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY}]`
)
const elements = containerRef.current?.querySelectorAll(
`[${API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY}]`
)
allSchemeDetails.current = elements ? (Array.from(elements) as HTMLUListElement[]) : []
}, [])
useEffect(() => {
+47 -32
View File
@@ -31,7 +31,7 @@ function Section({ slug, link, columns = 'single', children }: SectionProps) {
return (
<ReferenceSectionWrapper
id={slug}
id={slug || ''}
link={link}
className={cn(
'grid grid-cols-[1fr] gap-x-16 gap-y-8',
@@ -61,7 +61,7 @@ function Examples({ children }: PropsWithChildren) {
function EducationSection({ children, slug, ...props }: SectionProps) {
return (
<ReferenceSectionWrapper id={slug} className={'prose max-w-none'} {...props}>
<ReferenceSectionWrapper id={slug || ''} className={'prose max-w-none'} {...props}>
{children}
</ReferenceSectionWrapper>
)
@@ -231,7 +231,7 @@ function ParamOrTypeDetails({ paramOrType }: { paramOrType: object }) {
</div>
)}
{subContent && subContent.length > 0 && (
<TypeSubDetails details={subContent} defaultOpen={defaultOpen} />
<TypeSubDetails details={subContent} defaultOpen={defaultOpen || false} />
)}
</>
)
@@ -240,27 +240,29 @@ function ParamOrTypeDetails({ paramOrType }: { paramOrType: object }) {
export function ReturnTypeDetails({ returnType }: { returnType: MethodTypes['ret'] }) {
// These custom names that aren't defined aren't particularly meaningful, so
// just don't display them.
const isNameOnlyType = returnType.type.type === 'nameOnly'
const isNameOnlyType = returnType?.type?.type === 'nameOnly'
if (isNameOnlyType) return
const subContent = getSubDetails(returnType)
const isDefaultOpen = isDefaultExpanded(returnType)
const isDefaultOpen = returnType ? isDefaultExpanded(returnType) : false
return (
<div>
<h3 className="mb-3 text-base text-foreground">Return Type</h3>
<div className="border-t border-b py-5 flex flex-col gap-3">
<div className="text-xs text-foreground-muted">{getTypeName(returnType)}</div>
{returnType.comment?.shortText && (
<div className="text-xs text-foreground-muted">
{returnType ? getTypeName(returnType) : ''}
</div>
{returnType?.comment?.shortText && (
<div className="prose text-sm">
<MDXRemoteBase
source={returnType.comment?.shortText}
source={returnType?.comment?.shortText}
customPreprocess={normalizeMarkdown}
/>
</div>
)}
{subContent && subContent.length > 0 && (
<TypeSubDetails defaultOpen={isDefaultOpen} details={subContent} />
<TypeSubDetails defaultOpen={isDefaultOpen || false} details={subContent} />
)}
</div>
</div>
@@ -371,7 +373,7 @@ export function ApiOperationRequestBodyDetails({
}: {
requestBody: IApiEndPoint['requestBody']
}) {
const availableSchemes = Object.keys(requestBody.content) as Array<
const availableSchemes = Object.keys(requestBody?.content || {}) as Array<
'application/json' | 'application/x-www-form-urlencoded'
>
@@ -380,7 +382,7 @@ export function ApiOperationRequestBodyDetails({
{availableSchemes.map((scheme, index) => (
<ApiOperationRequestBodyDetailsInternal
key={index}
schema={requestBody.content[scheme].schema}
schema={requestBody?.content?.[scheme]?.schema || ({} as ISchema)}
hidden={index > 0}
{...{
[API_REFERENCE_REQUEST_BODY_SCHEMA_DATA_ATTRIBUTES.KEY]: scheme,
@@ -452,12 +454,12 @@ function ApiOperationRequestBodyDetailsInternal({
} else if (schema.type === 'object') {
return (
<ul {...props}>
{Object.keys(schema.properties)
{Object.keys(schema.properties || {})
.map((property) => ({
name: property,
required: schema.required?.includes(property),
required: schema.required?.includes(property) || false,
in: 'body' as const,
schema: schema.properties[property],
schema: schema.properties?.[property] || ({} as ISchema),
}))
.map((property, index) => (
<ApiSchemaParamDetails key={index} param={property} />
@@ -599,7 +601,11 @@ function getTypeName(parameter: object): string {
return parameter.type
}
if (typeof parameter.type !== 'object' || !('type' in parameter.type)) {
if (
typeof parameter.type !== 'object' ||
parameter.type === null ||
!('type' in parameter.type)
) {
return ''
}
@@ -645,28 +651,28 @@ function nameOrDefault(node: object, fallback: string) {
}
function getSubDetails(parentType: MethodTypes['params'][number] | MethodTypes['ret']) {
let subDetails: Array<any>
let subDetails: Array<any> = []
switch (parentType.type?.type) {
switch (parentType?.type?.type) {
case 'object':
subDetails = parentType.type.properties
subDetails = parentType?.type?.properties
break
case 'function':
subDetails = [
...(parentType.type.params.length === 0
...(parentType?.type?.params?.length === 0
? []
: [
{
name: 'Parameters',
type: 'callback parameters',
isOptional: 'NA',
params: parentType.type.params.map((param) => ({
params: parentType?.type?.params?.map((param) => ({
...param,
isOptional: 'NA',
})),
},
]),
{ name: 'Return', type: parentType.type.ret.type, isOptional: 'NA' },
{ name: 'Return', type: parentType?.type?.ret?.type, isOptional: 'NA' },
]
break
// @ts-ignore -- Adding these fake types to take advantage of existing recursion
@@ -675,27 +681,34 @@ function getSubDetails(parentType: MethodTypes['params'][number] | MethodTypes['
subDetails = parentType.params
break
case 'union':
subDetails = parentType.type.subTypes.map((subType, index) => ({
subDetails = parentType?.type?.subTypes?.map((subType, index) => ({
name: `Option ${index + 1}`,
type: { ...subType },
isOptional: 'NA',
}))
break
case 'promise':
if (parentType.type.awaited.type === 'union') {
subDetails = parentType.type.awaited.subTypes.map((subType, index) => ({
if (parentType?.type?.awaited?.type === 'union') {
subDetails = parentType?.type?.awaited?.subTypes?.map((subType, index) => ({
name: `Option ${index + 1}`,
type: { ...subType },
isOptional: 'NA',
}))
} else if (parentType.type.awaited.type === 'object') {
subDetails = parentType.type.awaited.properties.map((property) => ({
} else if (
parentType?.type?.awaited?.type === 'object' &&
'properties' in parentType.type.awaited
) {
subDetails = (parentType.type.awaited as any).properties?.map((property) => ({
...property,
isOptional: 'NA',
}))
} else if (parentType.type.awaited.type === 'array') {
} else if (parentType?.type?.awaited?.type === 'array') {
subDetails = [
{ name: 'array element', type: parentType.type.awaited.elemType, isOptional: 'NA' },
{
name: 'array element',
type: (parentType?.type?.awaited as any)?.elemType,
isOptional: 'NA',
},
]
}
break
@@ -836,7 +849,7 @@ function applyParameterMergeStrategy(
if (clonedParametersByName.has(key)) {
clonedParametersByName.set(
key,
applyParameterMergeStrategy(clonedParametersByName.get(key), alternateValue)
applyParameterMergeStrategy(clonedParametersByName.get(key)!, alternateValue)
)
} else {
clonedParametersByName.set(key, alternateValue)
@@ -856,7 +869,7 @@ function applyParameterMergeStrategy(
*********/
function mergeIntoUnion() {
if (alternateParameter.type.type === 'union') {
if (alternateParameter.type?.type === 'union') {
const originalType = clonedParameter.type
if (
@@ -866,7 +879,7 @@ function applyParameterMergeStrategy(
} else {
clonedParameter.type = {
type: 'union',
subTypes: [originalType, ...alternateParameter.type.subTypes],
subTypes: [originalType as TypeDetails, ...(alternateParameter.type?.subTypes || [])],
}
}
} else {
@@ -874,7 +887,7 @@ function applyParameterMergeStrategy(
if (!isEqual(originalType, alternateParameter.type)) {
clonedParameter.type = {
type: 'union',
subTypes: [originalType, alternateParameter.type],
subTypes: [originalType as TypeDetails, alternateParameter.type!],
}
}
}
@@ -885,11 +898,13 @@ function isDefaultExpanded(meta: object) {
return (
'type' in meta &&
typeof meta.type === 'object' &&
meta.type &&
'type' in meta.type &&
(meta.type.type == 'union' ||
(meta.type.type === 'promise' &&
'awaited' in meta.type &&
typeof meta.type.awaited === 'object' &&
meta.type.awaited &&
'type' in meta.type.awaited &&
meta.type.awaited.type === 'union'))
)
+13 -8
View File
@@ -30,15 +30,21 @@ export function parseReferencePath(slug: Array<string>) {
const isSelfHostingReference = slug[0].startsWith('self-hosting-')
if (isClientSdkReference) {
let [sdkId, maybeVersion, maybeCrawlers, ...path] = slug
let sdkId: string
let maybeVersion: string | null
let maybeCrawlers: string | null
let path: string[]
;[sdkId, maybeVersion, maybeCrawlers, ...path] = slug
if (!/v\d+/.test(maybeVersion)) {
maybeVersion = null
maybeCrawlers = maybeVersion
path = [maybeCrawlers, ...path]
maybeCrawlers = maybeVersion
}
if (maybeCrawlers !== 'crawlers') {
if (typeof maybeCrawlers === 'string') {
path = [maybeCrawlers, ...path]
}
maybeCrawlers = null
path = [maybeCrawlers, ...path]
}
return {
@@ -75,7 +81,7 @@ export function parseReferencePath(slug: Array<string>) {
async function generateStaticParamsForSdkVersion(sdkId: string, version: string) {
const flattenedSections = await getFlattenedSections(sdkId, version)
return flattenedSections
return (flattenedSections || [])
.filter((section) => section.type !== 'category' && !!section.slug)
.map((section) => ({
slug: [
@@ -139,7 +145,7 @@ export async function generateReferenceMetadata(
const displayName = REFERENCES[sdkId].name
const sectionTitle =
slug.length > 0
? flattenedSections.find((section) => section.slug === slug[0])?.title
? flattenedSections?.find((section) => section.slug === slug[0])?.title
: undefined
const url = [BASE_PATH, 'reference', sdkId, path[0]].filter(Boolean).join('/')
@@ -154,7 +160,6 @@ export async function generateReferenceMetadata(
...(slug.length > 0
? {
alternates: {
...parentAlternates,
canonical: url,
},
}
@@ -219,8 +224,8 @@ export function normalizeMarkdown(markdownUnescaped: string): string {
visit(markdownTree, ['code', 'inlineCode'], (node) => {
codeBlocks.push({
type: node.type,
start: node.position.start.offset,
end: node.position.end.offset,
start: node.position?.start?.offset || 0,
end: node.position?.end?.offset || 0,
})
})
// Sort code blocks by start offset in descending order
@@ -58,7 +58,7 @@ export default async function TroubleshootingPage({ entry }: { entry: ITroublesh
<hr className="my-6" aria-hidden />
</>
)}
{entry.data.errors?.length > 0 && (
{entry.data.errors?.length && entry.data.errors.length > 0 && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Related error codes</h3>
<div className="flex flex-wrap gap-0.5">
@@ -76,9 +76,9 @@ export default async function TroubleshootingPage({ entry }: { entry: ITroublesh
<hr className="my-6" aria-hidden />
</>
)}
{entry.data.keywords?.length > 0 && (
{entry.data.keywords?.length && entry.data.keywords.length > 0 && (
<>
<h3 className="text-sm text-foreground-lighter mb-3">Tags</h3>
<h3 className="text-sm text-foreground-lighter mb-3">Keywords</h3>
<div className="flex flex-wrap gap-0.5">
{entry.data.keywords.map((keyword) => (
<Link
@@ -49,15 +49,15 @@ export async function TroubleshootingPreview({ entry }: { entry: ITroubleshootin
{entry.data.title}
</h3>
</Link>
{entry.data.errors?.length > 0 && (
{entry.data.errors?.length && entry.data.errors.length > 0 && (
<span className="text-xs text-foreground-lighter ml-1 @4xl/troubleshooting:ml-0">
{entry.data.errors
{(entry.data.errors || [])
.map(formatError)
.filter(Boolean)
.map((error, index) => (
<>
<code key={index}>{error}</code>
{index < entry.data.errors.length - 1 ? ', ' : ''}
{index < (entry.data.errors?.length || 0) - 1 ? ', ' : ''}
</>
))}
</span>
@@ -11,7 +11,7 @@ export const TROUBLESHOOTING_DATA_ATTRIBUTES = {
ERRORS_LIST_ATTRIBUTE: 'data-errors',
}
export function formatError(error: ITroubleshootingEntry['data']['errors'][number]) {
export function formatError(error: NonNullable<ITroubleshootingEntry['data']['errors']>[number]) {
return `${error.http_status_code ?? ''}${!!error.http_status_code && !!error.code ? ' ' : ''}${error.code ?? ''}`
}
@@ -67,7 +67,7 @@ export async function getAllTroubleshootingErrors() {
const entries = await getAllTroubleshootingEntries()
const allErrors = new Set(
entries
.flatMap((entry) => entry.data.errors)
.flatMap((entry) => entry.data.errors ?? [])
.filter((error) => error?.http_status_code || error?.code)
)
@@ -81,8 +81,8 @@ export async function getAllTroubleshootingErrors() {
}
function sortErrors(
a: ITroubleshootingMetadata['errors'][number],
b: ITroubleshootingMetadata['errors'][number]
a: NonNullable<ITroubleshootingMetadata['errors']>[number],
b: NonNullable<ITroubleshootingMetadata['errors']>[number]
) {
return formatError(a).localeCompare(formatError(b))
}
+25 -30
View File
@@ -1,32 +1,27 @@
type RecObj<T extends object, K extends keyof T> = T[K] extends Array<T> ? T : never
const EMPTY_ARRAY = new Array(0)
export function getEmptyArray() {
return EMPTY_ARRAY
}
export function deepFilterRec<T extends object, K extends keyof T>(
arr: Array<RecObj<T, K>>,
recKey: K,
export function deepFilterRec<T extends object>(
arr: Array<T>,
recKey: string,
filterFn: (item: T) => boolean
) {
return arr.reduce(
(acc, elem) => {
if (!filterFn(elem)) return acc
): Array<T> {
return arr.reduce((acc, elem) => {
if (!filterFn(elem)) return acc
if (recKey in elem) {
const newSubitems = deepFilterRec(elem[recKey] as Array<RecObj<T, K>>, recKey, filterFn)
const newElem = { ...elem, [recKey]: newSubitems }
if (recKey in elem && elem[recKey as keyof T]) {
const newSubitems = deepFilterRec(elem[recKey as keyof T] as Array<T>, recKey, filterFn)
const newElem = { ...elem, [recKey]: newSubitems }
if (newSubitems.length > 0 || filterFn(elem)) acc.push(newElem)
} else {
acc.push(elem)
}
if (newSubitems.length > 0 || filterFn(elem)) acc.push(newElem)
} else {
acc.push(elem)
}
return acc
},
[] as Array<RecObj<T, K>>
)
return acc
}, [] as Array<T>)
}
export async function pluckPromise<T, K extends keyof T>(promise: Promise<T>, key: K) {
@@ -37,12 +32,12 @@ export async function pluckPromise<T, K extends keyof T>(promise: Promise<T>, ke
export class Result<Ok, Error> {
constructor(private internal: { data: Ok; error: null } | { data: null; error: Error }) {}
static ok<Ok, Error>(data: Ok): Result<Ok, Error> {
return new Result({ data, error: null })
static ok<Error = unknown, Ok = unknown>(data: Ok): Result<Ok, Error> {
return new Result<Ok, Error>({ data, error: null })
}
static error<Ok, Error>(error: Error): Result<Ok, Error> {
return new Result({ data: null, error })
static error<Ok = unknown, Error = unknown>(error: Error): Result<Ok, Error> {
return new Result<Ok, Error>({ data: null, error })
}
static async tryCatchFlat<
@@ -67,24 +62,24 @@ export class Result<Ok, Error> {
}
map<Mapped>(fn: (data: Ok) => Mapped): Result<Mapped, Error> {
if (this.isOk()) return Result.ok(fn(this.internal.data))
if (this.isOk()) return Result.ok(fn(this.internal.data!))
return this as unknown as Result<Mapped, Error>
}
mapError<MappedError>(fn: (error: Error) => MappedError): Result<Ok, MappedError> {
if (this.isOk()) return this as unknown as Result<Ok, MappedError>
return Result.error(fn(this.internal.error))
return Result.error(fn(this.internal.error!))
}
flatMap<Mapped>(fn: (data: Ok) => Result<Mapped, Error>): Result<Mapped, Error> {
if (this.isOk()) return fn(this.internal.data)
if (this.isOk()) return fn(this.internal.data!)
return this as unknown as Result<Mapped, Error>
}
flatMapAsync<Mapped>(
fn: (data: Ok) => Promise<Result<Mapped, Error>>
): Promise<Result<Mapped, Error>> {
if (this.isOk()) return fn(this.internal.data)
if (this.isOk()) return fn(this.internal.data!)
return Promise.resolve(this as unknown as Result<Mapped, Error>)
}
@@ -92,7 +87,7 @@ export class Result<Ok, Error> {
onOk: (data: Ok) => Mapped,
onError: (error: Error) => MappedError
): Mapped | MappedError {
if (this.isOk()) return onOk(this.internal.data)
return onError(this.internal.error)
if (this.isOk()) return onOk(this.internal.data!)
return onError(this.internal.error!)
}
}
@@ -34,10 +34,10 @@ export async function CodeBlock({
skipTypeGeneration?: boolean
}>) {
let code = (contents || extractCode(children)).trim()
const lang = tryToBundledLanguage(langSetting) || extractLang(children)
const lang = tryToBundledLanguage(langSetting || '') || extractLang(children)
let twoslashed = null as null | Map<number, Map<number, Array<NodeHover>>>
if (!skipTypeGeneration && TWOSLASHABLE_LANGS.includes(lang)) {
if (!skipTypeGeneration && lang && TWOSLASHABLE_LANGS.includes(lang)) {
try {
const { code: editedCode, nodes } = twoslasher(code)
const hoverNodes: Array<NodeHover> = nodes.filter((node) => node.type === 'hover')
@@ -53,7 +53,7 @@ export async function CodeBlock({
}
const { tokens } = highlighter.codeToTokens(code, {
lang,
lang: lang || undefined,
theme: 'Supabase Theme',
})
@@ -121,7 +121,10 @@ function CodeLine({
annotations={twoslash.get(token.offset)!}
/>
) : (
<span key={token.offset} style={{ color: token.color, ...getFontStyle(token.fontStyle) }}>
<span
key={token.offset}
style={{ color: token.color, ...getFontStyle(token.fontStyle || 0) }}
>
{token.content}
</span>
)
@@ -12,7 +12,7 @@ enum FontStyle {
}
export function getFontStyle(styleFlags: number): CSSProperties {
let style: CSSProperties = null
let style: CSSProperties = {}
if (styleFlags & FontStyle.Italic) {
;(style ??= {}).fontStyle = 'italic'
+7 -1
View File
@@ -62,7 +62,13 @@ const InfoTooltip = ({
[_setTooltipOpen, id, isMobile]
)
useEffect(() => () => clearTimeout(timeout.current), [])
useEffect(() => {
return () => {
if (timeout.current) {
clearTimeout(timeout.current)
}
}
}, [])
return (
<>
+3 -3
View File
@@ -13,10 +13,10 @@ const cliSpec = yaml.load(fs.readFileSync(`spec/cli_v1_commands.yaml`, 'utf8'))
* @returns {Array<{link: string}>} - An array of CLI page links.
*/
export function generateCLIPages() {
let cliPages = []
let cliPages: Array<{ link: string }> = []
cliSpec.commands.map((section) => {
const slug = flatCLISections.find((item) => item.id === section.id)?.slug
cliSpec.commands.map((section: any) => {
const slug = (flatCLISections as any[]).find((item: any) => item.id === section.id)?.slug
if (slug) cliPages.push({ link: `reference/cli/${slug}` })
})
return cliPages
+2 -2
View File
@@ -1,5 +1,5 @@
export function flattenSections(sections) {
let a = []
export function flattenSections(sections: any[]): any[] {
let a: any[] = []
for (let i = 0; i < sections.length; i++) {
if (sections[i].id) {
// only push a section that has an id
+1 -1
View File
@@ -89,7 +89,7 @@ const StickyHeader: FC<StickyHeader> = ({ icon, ...props }) => {
// we're serving search bots a different file (/crawlers/[...slug])
// and need to modify content to suit that
const isCrawlerPage = router.route.includes('/crawlers/[...slug]')
const isCrawlerPage = router?.route.includes('/crawlers/[...slug]') || false
const { ref } = useInView({
threshold: 1,
+1 -1
View File
@@ -7,7 +7,7 @@ import remarkGfm from 'remark-gfm'
import type { ICommonMarkdown } from '~/components/reference/Reference.types'
async function generateRefMarkdown(sections: ICommonMarkdown[], slug: string) {
let markdownContent = []
let markdownContent: any[] = []
/**
* Read all the markdown files that might have
* - custom text
@@ -6,7 +6,7 @@ import { visit } from 'unist-util-visit'
export type UrlTransformFunction = (url: string, node: Element) => string
function modify(node: Element, prop: string, fn?: UrlTransformFunction) {
if (hasProperty(node, prop)) {
if (node.properties && hasProperty(node, prop)) {
const property = node.properties[prop]
if (typeof property !== 'string') {
return
@@ -25,7 +25,9 @@ const remarkMkDocsAdmonition = function () {
// Extract the admonition type, title, and remaining text
const [, , typeTitle, value] = match
const [, type, title] = typeTitle.match(/^(.+?) ?(?:"(.*)")?$/)
const typeTitleMatch = typeTitle.match(/^(.+?) ?(?:"(.*)")?$/)
if (!typeTitleMatch) return
const [, type, title] = typeTitleMatch
// Rewrite the node's value to remove the admonition syntax
firstChild.value = value
@@ -76,7 +78,7 @@ const remarkMkDocsAdmonition = function () {
* Splices the discovered siblings out of the original parent and returns them.
*/
function extractLinkedSiblings(parent: Root, node: Node, index: number, indentAmount = 4) {
const { column } = node.position.start
const { column } = node.position?.start || { column: 0 }
let nextSibling: Content
let i = index
+1
View File
@@ -125,6 +125,7 @@ const remarkPyMdownTabs = function () {
* Splices the discovered siblings out of the original parent and returns them.
*/
function extractLinkedSiblings(parent: Root, node: Node, index: number, indentAmount = 4) {
if (!node.position) return []
const { column } = node.position.start
let nextSibling: Content
+15 -10
View File
@@ -10,13 +10,16 @@ let octokitInstance: Octokit
export function octokit() {
if (!octokitInstance) {
const privateKey = process.env.DOCS_GITHUB_APP_PRIVATE_KEY
if (!privateKey) {
throw new Error('DOCS_GITHUB_APP_PRIVATE_KEY environment variable is required')
}
// https://github.com/gr2m/universal-github-app-jwt?tab=readme-ov-file#converting-pkcs1-to-pkcs8
const privateKeyPkcs8 = crypto
.createPrivateKey(process.env.DOCS_GITHUB_APP_PRIVATE_KEY)
.export({
type: 'pkcs8',
format: 'pem',
})
const privateKeyPkcs8 = crypto.createPrivateKey(privateKey).export({
type: 'pkcs8',
format: 'pem',
})
octokitInstance = new Octokit({
authStrategy: createAppAuth,
@@ -78,6 +81,7 @@ async function getGitHubFileContents({
} catch (err) {
console.error('Error fetching GitHub file: %o', err)
onError?.(err)
return ''
}
}
@@ -93,10 +97,10 @@ export async function getGitHubFileContentsImmutableOnly({
branch: string
path: string
options: {
onError: (err?: unknown) => void
fetch?: (info: RequestInfo, init?: RequestInit) => Promise<Response>
onError: (error: unknown) => void
fetch: (url: string) => Promise<Response>
}
}) {
}): Promise<string> {
const isImmutableCommit = await checkForImmutableCommit({
org,
repo,
@@ -106,13 +110,14 @@ export async function getGitHubFileContentsImmutableOnly({
throw Error('The commit is not an immutable commit SHA. Tags and branch names are not allowed.')
}
return getGitHubFileContents({
const result = await getGitHubFileContents({
org,
repo,
branch,
path,
options: { onError, fetch },
})
return result || ''
}
async function checkForImmutableCommit({
+5 -5
View File
@@ -24,17 +24,17 @@ export function extractTsDocNode(nodeToFind: string, definition: any) {
}
export function generateParameters(tsDefinition: any) {
let functionDeclaration = null
let functionDeclaration: any = null
if (tsDefinition.kindString == 'Method') {
functionDeclaration = tsDefinition
} else if (tsDefinition.kindString == 'Constructor') {
functionDeclaration = tsDefinition
} else functionDeclaration = tsDefinition?.type?.declaration
if (!functionDeclaration) return ''
if (!functionDeclaration || !functionDeclaration.signatures) return ''
// Functions can have multiple signatures - select the last one since that
// tends to be closer to primitive types (citation needed).
const paramDefinitions: TsDoc.TypeDefinition[] = functionDeclaration.signatures.at(-1).parameters
const paramDefinitions: TsDoc.TypeDefinition[] = functionDeclaration.signatures.at(-1)?.parameters
if (!paramDefinitions) return ''
// const paramsComments: TsDoc.CommentTag = tsDefinition.comment?.tags?.filter(x => x.tag == 'param')
@@ -46,7 +46,7 @@ function recurseThroughParams(paramDefinition: any) {
const param = { ...paramDefinition }
const labelParams = generateLabelParam(param)
let children: any[]
let children: any[] | undefined
if (param.type?.type === 'literal') {
// skip: literal types have no children
} else if (param.type?.type === 'intrinsic') {
@@ -241,7 +241,7 @@ export function gen_v3(
toArrayWithKey(val!, 'operation').forEach((o) => {
const operation = o as v3OperationWithPath
const operationId =
type === 'mgmt-api' && isValidSlug(operation.operationId)
type === 'mgmt-api' && operation.operationId && isValidSlug(operation.operationId)
? operation.operationId
: slugify(operation.summary!)
const enriched = {
+8 -4
View File
@@ -8,10 +8,14 @@ let supabaseAdminClient: SupabaseClient<Database> | null = null
export function supabaseAdmin() {
if (!supabaseAdminClient) {
supabaseAdminClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY
)
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
const key = process.env.SUPABASE_SECRET_KEY
if (!url || !key) {
throw new Error('Missing required environment variables for Supabase admin client')
}
supabaseAdminClient = createClient(url, key)
}
return supabaseAdminClient
@@ -26,10 +26,14 @@ export abstract class SearchResultModel {
await supabase().rpc('search_content', {
embedding,
include_full_content: includeFullContent,
max_result: args.limit,
max_result: args.limit ?? undefined,
})
)
.map((matches) => matches.map(createModelFromMatch).filter(Boolean))
.map((matches) =>
matches
.map(createModelFromMatch)
.filter((item): item is SearchResultInterface => item !== null)
)
.mapError(convertPostgrestToApiError)
return matchResult
@@ -55,7 +59,7 @@ function createModelFromMatch({
})
case 'reference':
const { language } = metadata
if (SDKLanguageValues.includes(language)) {
if (language && SDKLanguageValues.includes(language)) {
return new ReferenceSDKFunctionModel({
title: page_title,
href,
@@ -71,7 +75,7 @@ function createModelFromMatch({
subsections,
})
} else {
break
return null
}
case 'github-discussions':
return new TroubleshootingModel({
+6 -6
View File
@@ -269,7 +269,7 @@ export class GraphQLCollectionBuilder {
const result = await fetch(args)
const { items: fetchedItems, totalCount, hasNextPage = false, hasPreviousPage = false } = result
const edges = fetchedItems.map((item) => {
return { node: item, cursor: getCursor(item) }
return { node: item, cursor: getCursor?.(item) ?? '' }
})
return {
@@ -291,7 +291,7 @@ export class GraphQLCollectionBuilder {
return { node: item, cursor: getCursor(item, idx) }
})
const { edges, hasPreviousPage, hasNextPage } = getRequestedSlice(allEdges, args)
const { edges, hasPreviousPage, hasNextPage } = getRequestedSlice(allEdges, args ?? {})
return {
edges,
@@ -323,12 +323,12 @@ function getRequestedSlice<ItemType>(
let beforeIndex = allEdges.length
let afterIndex = -1
const requestedBefore = toNumber(pageArgs.before)
const requestedBefore = pageArgs.before ? toNumber(pageArgs.before) : undefined
if (requestedBefore && requestedBefore >= 0 && requestedBefore < beforeIndex) {
beforeIndex = requestedBefore
hasNextPage = true
}
const requestedAfter = toNumber(pageArgs.after)
const requestedAfter = pageArgs.after ? toNumber(pageArgs.after) : undefined
if (requestedAfter && requestedAfter >= 0) {
afterIndex = requestedAfter
hasPreviousPage = true
@@ -336,10 +336,10 @@ function getRequestedSlice<ItemType>(
let edges = allEdges.slice(afterIndex + 1, beforeIndex)
if (pageArgs.first >= 0 && edges.length > pageArgs.first) {
if (pageArgs.first != null && pageArgs.first >= 0 && edges.length > pageArgs.first) {
edges = edges.slice(0, pageArgs.first)
hasNextPage = true
} else if (pageArgs.last >= 0 && edges.length > pageArgs.last) {
} else if (pageArgs.last != null && pageArgs.last >= 0 && edges.length > pageArgs.last) {
edges = edges.slice(edges.length - pageArgs.last)
hasPreviousPage = true
}
+25 -20
View File
@@ -19,7 +19,7 @@ import {
type ValueNode,
} from 'graphql'
let options = { processArguments: false, excludedFields: [] }
let options = { processArguments: false, excludedFields: [] as string[] }
function getSelections(ast: FieldNode | FragmentDefinitionNode | InlineFragmentNode) {
if (
@@ -47,7 +47,7 @@ function getAST(ast: FragmentSpreadNode | InlineFragmentNode, info: GraphQLResol
}
function getArguments(ast: FieldNode, info: GraphQLResolveInfo) {
return ast.arguments.map((argument) => {
return ast.arguments?.map((argument) => {
const argumentValue = getArgumentValue(argument.value, info)
return {
@@ -81,7 +81,8 @@ function getArgumentValue(arg: ValueNode, info: GraphQLResolveInfo) {
}
function getDirectiveValue(directive: DirectiveNode, info: GraphQLResolveInfo) {
const arg = directive.arguments[0]
const arg = directive.arguments?.[0]
if (!arg) return undefined
if (arg.value.kind !== 'Variable') {
return arg.value.kind === 'BooleanValue' ? arg.value.value : undefined
}
@@ -93,22 +94,26 @@ function getDirectiveResults(ast: SelectionNode, info: GraphQLResolveInfo) {
shouldInclude: true,
shouldSkip: false,
}
return ast.directives.reduce((result, directive) => {
switch (directive.name.value) {
case 'include':
const directiveValue = getDirectiveValue(directive, info)
if (directiveValue != undefined) {
return { ...result, shouldInclude: directiveValue }
}
case 'skip':
const directiveValule = getDirectiveValue(directive, info)
if (directiveValue != undefined) {
return { ...result, shouldSkip: directiveValue }
}
default:
return result
}
}, directiveResult)
return (
ast.directives?.reduce((result, directive) => {
switch (directive.name.value) {
case 'include':
const directiveValue = getDirectiveValue(directive, info)
if (directiveValue != undefined) {
return { ...result, shouldInclude: directiveValue }
}
return result
case 'skip':
const directiveSkipValue = getDirectiveValue(directive, info)
if (directiveSkipValue != undefined) {
return { ...result, shouldSkip: directiveSkipValue }
}
return result
default:
return result
}
}, directiveResult) ?? directiveResult
)
}
function flattenAST(
@@ -128,7 +133,7 @@ function flattenAST(
flattened = flattenAST(getAST(a, info), info, flattened)
} else {
const name = a.name.value
if (options.excludedFields.indexOf(name) !== -1) {
if (options.excludedFields && options.excludedFields.indexOf(name) !== -1) {
return flattened
}
if (flattened[name] && flattened[name] !== '__arguments') {
+3 -2
View File
@@ -45,7 +45,7 @@ function processMdx(content: string, options?: { yaml?: boolean }): ProcessedMdx
mdastExtensions: [mdxFromMarkdown()],
})
let meta: Record<string, unknown>
let meta: Record<string, unknown> | undefined
if (options?.yaml) {
meta = frontmatter
} else {
@@ -83,7 +83,8 @@ function processMdx(content: string, options?: { yaml?: boolean }): ProcessedMdx
const [firstNode] = tree.children
const content = toMarkdown(tree)
const rawHeading: string = firstNode.type === 'heading' ? toString(firstNode) : undefined
const rawHeading: string | undefined =
firstNode.type === 'heading' ? toString(firstNode) : undefined
if (!rawHeading) {
return { content }
+7 -5
View File
@@ -11,7 +11,7 @@
* the last Git commit date.
*/
import _configureDotEnv from './utils/dotenv'
import _configureDotEnv from './utils/dotenv.js'
import { createClient, type SupabaseClient } from '@supabase/supabase-js'
import matter from 'gray-matter'
@@ -111,8 +111,8 @@ function parseOptions(): Options {
function createSupabaseClient() {
return createClient(
process.env[REQUIRED_ENV_VARS.SUPABASE_URL],
process.env[REQUIRED_ENV_VARS.SERVICE_ROLE_KEY]
process.env[REQUIRED_ENV_VARS.SUPABASE_URL]!,
process.env[REQUIRED_ENV_VARS.SERVICE_ROLE_KEY]!
)
}
@@ -125,7 +125,9 @@ async function updateContentDates({ reset, ctx }: { reset: boolean; ctx: Ctx })
const updateTasks: Array<Promise<void>> = []
for (const file of mdxFiles) {
const tasks = await updateTimestamps(file, { reset, timestamp, ctx })
updateTasks.push(...tasks)
if (tasks) {
updateTasks.push(...tasks)
}
}
await Promise.all(updateTasks)
}
@@ -183,7 +185,7 @@ function processMdx(rawContent: string): Array<SectionWithChecksum> {
let heading = rawHeading
if (seenHeadings.has(rawHeading)) {
const idx = seenHeadings.get(rawHeading) + 1
const idx = (seenHeadings.get(rawHeading) ?? 0) + 1
seenHeadings.set(rawHeading, idx)
heading = `${rawHeading} (__UNIQUE_MARKER__${idx})`
} else {
+32 -13
View File
@@ -1,4 +1,4 @@
import _configureDotEnv from './utils/dotenv'
import './utils/dotenv.js'
import 'dotenv/config'
import fs from 'node:fs/promises'
@@ -15,8 +15,6 @@ import {
type SearchSource,
} from './search/sources/index.js'
const _ = _configureDotEnv
interface Source {
title: string
/**
@@ -39,37 +37,58 @@ const SOURCES: Source[] = [
{
title: 'Supabase Reference (JavaScript)',
relPath: 'llms/js.txt',
fetch: fetchJsLibReferenceSource,
fetch: async () =>
(await fetchJsLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
{
title: 'Supabase Reference (Dart)',
relPath: 'llms/dart.txt',
fetch: fetchDartLibReferenceSource,
fetch: async () =>
(await fetchDartLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
{
title: 'Supabase Reference (Swift)',
relPath: 'llms/swift.txt',
fetch: fetchSwiftLibReferenceSource,
fetch: async () =>
(await fetchSwiftLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
{
title: 'Supabase Reference (Kotlin)',
relPath: 'llms/kt.txt',
fetch: fetchKtLibReferenceSource,
relPath: 'llms/kotlin.txt',
fetch: async () =>
(await fetchKtLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
{
title: 'Supabase Reference (Python)',
relPath: 'llms/py.txt',
fetch: fetchPythonLibReferenceSource,
relPath: 'llms/python.txt',
fetch: async () =>
(await fetchPythonLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
{
title: 'Supabase Reference (C#)',
relPath: 'llms/csharp.txt',
fetch: fetchCSharpLibReferenceSource,
fetch: async () =>
(await fetchCSharpLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
{
title: 'Supabase Reference (CLI)',
title: 'Supabase CLI Reference',
relPath: 'llms/cli.txt',
fetch: fetchCliLibReferenceSource,
fetch: async () =>
(await fetchCliLibReferenceSource()).filter(
(item): item is SearchSource => item !== undefined
),
},
]
+13 -11
View File
@@ -2,24 +2,25 @@ import { readdir } from 'node:fs/promises'
import { extname, join } from 'node:path'
import {
GLOBAL_MENU_ITEMS,
gettingstarted,
cli,
auth,
database,
api,
graphql,
functions,
realtime,
storage,
ai,
api,
auth,
cli,
database,
functions,
gettingstarted,
graphql,
platform,
realtime,
resources,
self_hosting,
} from '../../components/Navigation/NavigationMenu/NavigationMenu.constants'
storage,
} from '../../components/Navigation/NavigationMenu/NavigationMenu.constants.js'
const DOCS_ROOT_DIR = join(__dirname, '..', '..')
const DEFAULT_DOCS_CONTENT_DIR = join(DOCS_ROOT_DIR, 'content')
// eslint-disable-next-line turbo/no-undeclared-env-vars
const DOCS_CONTENT_DIR = process.env.DOCS_CONTENT_DIR || DEFAULT_DOCS_CONTENT_DIR
const IGNORE_LIST = [
@@ -47,7 +48,8 @@ type RefItem = {
const recGetUrl = (items: readonly RefItem[], acc: string[] = []) =>
items.reduce((acc, item) => {
if (item.href || item.url) acc.push(item.href || item.url)
const url = item.href || item.url
if (url) acc.push(url)
if (item.items) acc.push(...recGetUrl(item.items, acc))
return acc
}, acc)
@@ -1,4 +1,4 @@
import _configureDotenv from '../utils/dotenv'
import '../utils/dotenv.js'
import { createClient } from '@supabase/supabase-js'
import { parseArgs } from 'node:util'
@@ -7,8 +7,6 @@ import { v4 as uuidv4 } from 'uuid'
import type { Json, Section } from '../helpers.mdx.js'
import { fetchAllSources } from './sources/index.js'
const _ = _configureDotenv
const args = parseArgs({
options: {
refresh: {
@@ -41,8 +39,8 @@ async function generateEmbeddings() {
}
const supabaseClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY,
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SECRET_KEY!,
{
auth: {
persistSession: false,
@@ -38,7 +38,7 @@ export async function fetchDiscussions(owner: string, repo: string, categoryId:
auth: {
appId,
installationId,
privateKey: crypto.createPrivateKey(privateKey).export({ type: 'pkcs8', format: 'pem' }),
privateKey: crypto.createPrivateKey(privateKey!).export({ type: 'pkcs8', format: 'pem' }),
},
})
+5 -1
View File
@@ -139,7 +139,11 @@ export async function fetchAllSources() {
const partnerIntegrationSources = fetchPartners()
.then((partners) =>
Promise.all(partners.map((partner) => new IntegrationLoader(partner.slug, partner).load()))
partners
? Promise.all(
partners.map((partner) => new IntegrationLoader(partner.slug, partner).load())
)
: []
)
.then((data) => data.flat())
+1 -1
View File
@@ -46,6 +46,6 @@ export class MarkdownSource extends BaseSource {
const sections = this.sections ?? []
const sectionText = sections.map(({ content }) => content).join('\n\n')
return `# ${this.meta.title ?? ''}\n\n${this.meta.subtitle ?? ''}\n\n${sectionText}`
return `# ${this.meta?.title ?? ''}\n\n${this.meta?.subtitle ?? ''}\n\n${sectionText}`
}
}
@@ -8,15 +8,15 @@ type PartnerData = {
overview: string // The Markdown content for indexing
}
const supabaseUrl = process.env.NEXT_PUBLIC_MISC_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_MISC_ANON_KEY
const supabaseUrl = process.env.NEXT_PUBLIC_MISC_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_MISC_ANON_KEY!
let supabaseClient: SupabaseClient
function getSupabaseClient() {
if (!supabaseClient) {
supabaseClient = createClient(supabaseUrl, supabaseAnonKey)
return supabaseClient
}
return supabaseClient
}
export async function fetchPartners() {
@@ -27,7 +27,7 @@ export async function fetchPartners() {
.eq('approved', true)
// We want to show technology integrations, not agencies, in search
.neq('type', 'expert')
return partners
return partners ?? []
}
export class IntegrationLoader extends BaseLoader {
@@ -30,7 +30,7 @@ export abstract class ReferenceLoader<SpecSection> extends BaseLoader {
super(source, path)
}
async load() {
async load(): Promise<BaseSource[]> {
const specContents = await readFile(this.specFilePath, 'utf8')
const refSectionsContents = await readFile(this.sectionsFilePath, 'utf8')
@@ -55,13 +55,13 @@ export abstract class ReferenceLoader<SpecSection> extends BaseLoader {
this.enhanceMeta(specSection)
)
})
.filter(Boolean)
.filter((item): item is ReferenceSource<SpecSection> => item !== undefined)
return sections
return sections as BaseSource[]
}
abstract getSpecSections(specContents: string): SpecSection[]
abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection
abstract matchSpecSection(specSections: SpecSection[], id: string): SpecSection | undefined
enhanceMeta(section: SpecSection): Json {
return this.meta
}
@@ -136,7 +136,7 @@ export class OpenApiReferenceLoader extends ReferenceLoader<enrichedOperation> {
return generatedSpec.operations
}
matchSpecSection(operations: enrichedOperation[], id: string): enrichedOperation {
matchSpecSection(operations: enrichedOperation[], id: string): enrichedOperation | undefined {
return operations.find((operation) => operation.operationId === id)
}
}
@@ -166,9 +166,9 @@ export class OpenApiReferenceSource extends ReferenceSource<enrichedOperation> {
extractIndexedContent(): string {
const { summary, description, operation, tags } = this.specSection
return `# ${this.meta.title ?? ''}\n\n${summary ?? ''}\n\n${description ?? ''}\n\n${operation ?? ''}\n\n${tags.join(
', '
)}`
return `# ${this.meta.title ?? ''}\n\n${summary ?? ''}\n\n${description ?? ''}\n\n${operation ?? ''}\n\n${
tags?.join(', ') ?? ''
}`
}
}
@@ -190,7 +190,10 @@ export class ClientLibReferenceLoader extends ReferenceLoader<IFunctionDefinitio
return spec.functions
}
matchSpecSection(functionDefinitions: IFunctionDefinition[], id: string): IFunctionDefinition {
matchSpecSection(
functionDefinitions: IFunctionDefinition[],
id: string
): IFunctionDefinition | undefined {
return functionDefinitions.find((functionDefinition) => functionDefinition.id === id)
}
@@ -246,7 +249,7 @@ export class CliReferenceLoader extends ReferenceLoader<CliCommand> {
return spec.commands
}
matchSpecSection(cliCommands: CliCommand[], id: string): CliCommand {
matchSpecSection(cliCommands: CliCommand[], id: string): CliCommand | undefined {
return cliCommands.find((cliCommand) => cliCommand.id === id)
}
}
@@ -23,8 +23,19 @@ function extractSectionsFromOpenApi(filePath, outputPath) {
try {
const openApiJson = JSON.parse(data)
const categories = []
const sections = []
const categories: string[] = []
const sections: Array<{
type: string
title: string
id?: string
slug?: string
items: Array<{
type: string
title: string
id: string
slug: string
}>
}> = []
if (openApiJson.paths) {
for (const route in openApiJson.paths) {
@@ -47,7 +58,7 @@ function extractSectionsFromOpenApi(filePath, outputPath) {
}
const sectionCate = sections.find((i) => i.title === tag)
sectionCate.items.push({
sectionCate?.items.push({
id: operationId,
title: slugToTitle(operationId),
slug: operationId,
@@ -65,6 +76,7 @@ function extractSectionsFromOpenApi(filePath, outputPath) {
id: 'introduction',
slug: 'introduction',
type: 'markdown',
items: [],
})
fs.writeFile(outputPath, JSON.stringify(sections, null, 2), 'utf8', (err) => {
+1 -1
View File
@@ -25,7 +25,7 @@
"name": "next"
}
],
"strictNullChecks": false
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
@@ -123,7 +123,11 @@ export const Grid = memo(
>
{isLoading && <GenericSkeletonLoader />}
{isError && (
<AlertError error={error} subject="Failed to retrieve rows from table">
<AlertError
className="pointer-events-auto"
error={error}
subject="Failed to retrieve rows from table"
>
{filters.length > 0 && (
<p>
Verify that the filter values are correct, as the error may stem from an
@@ -1,10 +0,0 @@
import DisplayApiSettings from 'components/ui/ProjectSettings/DisplayApiSettings'
export function LegacyAPIKeys() {
return (
<div>
<DisplayApiSettings legacy showNotice={false} />
</div>
)
return
}
@@ -5,7 +5,6 @@ import CopyButton from 'components/ui/CopyButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { Link } from 'lucide-react'
import { useMemo } from 'react'
import { cn, EyeOffIcon, Input_Shadcn_, Skeleton, WarningIcon } from 'ui'
// to add in later with follow up PR
@@ -22,7 +22,7 @@ interface ApiKeysVisibilityState {
export function useApiKeysVisibility(): ApiKeysVisibilityState {
const { ref: projectRef } = useParams()
const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'api_keys')
const isInRollout = useFlag('basicApiKeys')
const isBasicApiKeysEnabled = useFlag('basicApiKeys')
const { data: apiKeysData, isLoading } = useAPIKeysQuery({
projectRef,
@@ -39,12 +39,13 @@ export function useApiKeysVisibility(): ApiKeysVisibilityState {
const hasApiKeys = publishableApiKeys.length > 0
// Can initialize API keys when in rollout, has permissions, not loading, and no API keys yet
const canInitApiKeys = hasApiKeys ? false : isInRollout && canReadAPIKeys && !isLoading
const canInitApiKeys = isBasicApiKeysEnabled && canReadAPIKeys && !isLoading && !hasApiKeys
const shouldDisableUI = hasApiKeys ? false : !isInRollout
// Disable UI for publishable keys and secrets keys if flag is not enabled OR no API keys created yet
const shouldDisableUI = !isBasicApiKeysEnabled || !hasApiKeys
return {
isInRollout,
isInRollout: isBasicApiKeysEnabled,
hasApiKeys,
isLoading,
canReadAPIKeys,
@@ -312,7 +312,8 @@ export const SubscriptionPlanUpdateDialog = ({
subscriptionPreview?.breakdown?.filter(
(item) =>
item.description?.toLowerCase().includes('compute') &&
item.breakdown?.length > 0
item.breakdown &&
item.breakdown.length > 0
) || []
const computeCreditsItem =
@@ -325,11 +326,11 @@ export const SubscriptionPlanUpdateDialog = ({
)
const allProjects = computeItems.flatMap((item) =>
item.breakdown.map((project) => ({
(item.breakdown || []).map((project) => ({
...project,
computeType: item.description,
computeCosts: Math.round(
item.total_price / item.breakdown.length
item.total_price / item.breakdown!.length
),
}))
)
@@ -412,7 +413,7 @@ export const SubscriptionPlanUpdateDialog = ({
<TableCell className="text-xs py-2 px-0">
<div className="flex items-center gap-1">
<span>{item.description ?? 'Unknown'}</span>
{item.breakdown.length > 0 && (
{item.breakdown && item.breakdown.length > 0 && (
<InfoTooltip className="max-w-sm">
<p>Projects using {item.description}:</p>
<ul className="ml-6 list-disc">
@@ -195,7 +195,7 @@ const Usage = () => {
title="Usage filtered by project"
description={
<div>
You are currently viewing usage for the "$
You are currently viewing usage for the "
{selectedProject?.name || selectedProjectRef}" project. Supabase uses{' '}
<Link
href="/docs/guides/platform/billing-on-supabase#organization-based-billing"
@@ -1,7 +1,6 @@
import Link from 'next/link'
import { ReactNode, useState } from 'react'
import { Popover, PopoverContent, PopoverTrigger } from '@ui/components/shadcn/ui/popover'
import { IS_PLATFORM } from 'common'
import Table from 'components/to-be-cleaned/Table'
import dayjs from 'dayjs'
@@ -234,31 +233,32 @@ const LogsQueryPanel = ({
/>
)}
<div className="overflow-hidden">
<div
data-testid="log-explorer-warnings"
className={` transition-all duration-300 ${
warnings.length > 0 ? 'opacity-100' : 'invisible h-0 w-0 opacity-0'
}`}
>
<Popover>
<PopoverTrigger>
<Badge variant="warning">
{warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'}
</Badge>
<PopoverContent className="p-0 divide-y">
{warnings.map((warning, index) => (
<p key={index} className="p-3 text-xs text-foreground-light text-left">
{warning.text}{' '}
{warning.link && (
<Link href={warning.link}>{warning.linkText || 'View'}</Link>
)}
</p>
))}
</PopoverContent>
</PopoverTrigger>
</Popover>
</div>
<div
data-testid="log-explorer-warnings"
className={`transition-all duration-300 h-full ${
warnings.length > 0 ? 'opacity-100' : 'invisible h-0 w-0 opacity-0'
}`}
>
<Tooltip>
<TooltipTrigger className="flex items-start">
<Badge variant="warning">
{warnings.length} {warnings.length > 1 ? 'warnings' : 'warning'}
</Badge>
<TooltipContent className="p-0 divide-y max-w-xs" side="bottom">
{warnings.map((warning, index) => (
<p
key={index}
className="px-3 py-1.5 text-xs text-foreground-light text-left"
>
{warning.text}{' '}
{warning.link && (
<Link href={warning.link}>{warning.linkText || 'View'}</Link>
)}
</p>
))}
</TooltipContent>
</TooltipTrigger>
</Tooltip>
</div>
</div>
{dataSource === 'logs' && (
@@ -218,22 +218,24 @@ export function LogsSidebarMenuV2() {
onChange={(e) => setSearchText(e.target.value)}
></InnerSideBarFilterSearchInput>
</InnerSideBarFilters>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<Plus className="text-foreground" />}
className="w-[26px]"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem className="gap-x-2" asChild>
<Link href={`/project/${ref}/logs/explorer`}>
<FilePlus size={14} />
Create query
</Link>
</DropdownMenuItem>
{warehouseEnabled && (
{warehouseEnabled ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={<Plus className="text-foreground" />}
className="w-[26px]"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem className="gap-x-2" asChild>
<Link href={`/project/${ref}/logs/explorer`}>
<FilePlus size={14} />
Create query
</Link>
</DropdownMenuItem>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuItem className="gap-x-2" asChild>
@@ -253,9 +255,17 @@ export function LogsSidebarMenuV2() {
</TooltipContent>
)}
</Tooltip>
)}
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Button
type="default"
icon={<Plus className="text-foreground" />}
className="w-[26px]"
onClick={() => router.push(`/project/${ref}/logs/explorer`)}
/>
)}
<CreateWarehouseCollectionModal
open={createCollectionOpen}
onOpenChange={setCreateCollectionOpen}
@@ -125,7 +125,7 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === 'i' && !e.altKey && !e.shiftKey) {
aiSnap.openAssistant()
aiSnap.toggleAssistant()
e.preventDefault()
e.stopPropagation()
}
@@ -7,7 +7,6 @@ import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
import { withAuth } from 'hooks/misc/withAuth'
import { useFlag } from 'hooks/ui/useFlag'
import { IS_PLATFORM } from 'lib/constants'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { generateSettingsMenu } from './SettingsMenu.utils'
@@ -46,14 +45,11 @@ const SettingsLayout = ({ title, children }: PropsWithChildren<SettingsLayoutPro
'billing:invoices',
])
const newApiKeys = useFlag('newApiKeys')
const menuRoutes = generateSettingsMenu(ref, project, organization, {
auth: authEnabled,
edgeFunctions: edgeFunctionsEnabled,
storage: storageEnabled,
invoices: invoicesEnabled,
newApiKeys,
})
return (
@@ -14,7 +14,6 @@ export const generateSettingsMenu = (
edgeFunctions?: boolean
storage?: boolean
invoices?: boolean
newApiKeys?: boolean
}
): ProductMenuGroup[] => {
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
@@ -1,6 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { IS_PLATFORM } from 'common'
import { useParams } from 'common/hooks/useParams'
import { useIsSQLEditorTabsEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useSQLSnippetFolderContentsQuery } from 'data/content/sql-folder-contents-query'
import { Snippet } from 'data/content/sql-folders-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
@@ -82,6 +83,7 @@ export const SQLEditorTreeViewItem = ({
const { profile } = useProfile()
const { className, onClick } = getNodeProps()
const snapV2 = useSqlEditorV2StateSnapshot()
const isSQLEditorTabsEnabled = useIsSQLEditorTabsEnabled()
const isOwner = profile?.id === element?.metadata.owner_id
const isSharedSnippet = element.metadata.visibility === 'project'
@@ -265,7 +267,7 @@ export const SQLEditorTreeViewItem = ({
>
<Link
href={`/project/${projectRef}/sql/${element.id}`}
target="_blank"
target={isSQLEditorTabsEnabled ? '_self' : '_blank'}
rel="noreferrer"
>
<ExternalLink size={14} />
@@ -1,4 +1,3 @@
import { useMemo } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { useParams } from 'common'
@@ -8,9 +7,10 @@ import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import { AlertCircle, Loader2 } from 'lucide-react'
import { Input } from 'ui'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { useLastUsedAPIKeysLogQuery, getLastUsedAPIKeys } from './DisplayApiSettings.utils'
import { Input } from 'ui'
import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils'
const DisplayApiSettings = ({
legacy,
@@ -21,7 +21,7 @@ const DisplayApiSettings = ({
}) => {
const { ref: projectRef } = useParams()
const newApiKeysFlag = useFlag('newApiKeys')
const newApiKeysInRollOut = useFlag('basicApiKeys')
const {
data: settings,
@@ -177,7 +177,7 @@ const DisplayApiSettings = ({
))
)}
{showNotice ? (
newApiKeysFlag ? (
newApiKeysInRollOut ? (
<Panel.Notice
className="border-t"
title="API keys have moved"
@@ -1,13 +1,15 @@
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRef } from 'react'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
dayjs.extend(duration)
dayjs.extend(relativeTime)
export function useLastUsedAPIKeysLogQuery(projectRef: string, now: Date = new Date()) {
export function useLastUsedAPIKeysLogQuery(projectRef: string) {
const now = useRef(new Date()).current
return useLogsQuery(projectRef, {
iso_timestamp_start: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
iso_timestamp_end: now.toISOString(),
@@ -16,7 +16,7 @@ export type OrganizationBillingSubscriptionPreviewResponse = {
unit_price_desc?: string
quantity?: number
total_price: number
breakdown: {
breakdown?: {
project_name: string
project_ref: string
usage: number
+17 -1
View File
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { Dispatch, SetStateAction, useState } from 'react'
import { Dispatch, SetStateAction, useState, useEffect } from 'react'
import { IS_PLATFORM } from 'common'
import {
@@ -46,6 +46,22 @@ const useLogsQuery = (
: defaultHelper.calcTo(),
})
useEffect(() => {
setParams((prev) => ({
...prev,
...initialParams,
project: projectRef,
sql: initialParams?.sql ?? prev.sql,
iso_timestamp_start: initialParams.iso_timestamp_start ?? prev.iso_timestamp_start,
iso_timestamp_end: initialParams.iso_timestamp_end ?? prev.iso_timestamp_end,
}))
}, [
projectRef,
initialParams.sql,
initialParams.iso_timestamp_start,
initialParams.iso_timestamp_end,
])
const _enabled = enabled && typeof projectRef !== 'undefined' && Boolean(params.sql)
const usesWith = checkForWithClause(params.sql || '')
+30 -19
View File
@@ -1,4 +1,5 @@
import dayjs from 'dayjs'
import Link from 'next/link'
import { useEffect, useRef } from 'react'
import { useParams } from 'common'
@@ -39,7 +40,7 @@ const Home: NextPageWithLayout = () => {
const project = useSelectedProject()
const isOrioleDb = useIsOrioleDb()
const snap = useAppStateSnapshot()
const { enableBranching } = useParams()
const { ref, enableBranching } = useParams()
const hasShownEnableBranchingModalRef = useRef(false)
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
@@ -70,9 +71,10 @@ const Home: NextPageWithLayout = () => {
projectRef: project?.ref,
})
const tablesCount = tablesData?.length ?? 0
const functionsCount = functionsData?.length ?? 0
const replicasCount = (replicasData?.length ?? 1) - 1
const tablesCount = Math.max(0, tablesData?.length ?? 0)
const functionsCount = Math.max(0, functionsData?.length ?? 0)
// [Joshen] JFYI minus 1 as the replicas endpoint returns the primary DB minimally
const replicasCount = Math.max(0, (replicasData?.length ?? 1) - 1)
return (
<div className="w-full">
@@ -108,44 +110,53 @@ const Home: NextPageWithLayout = () => {
<div className="flex items-center">
{project?.status === PROJECT_STATUS.ACTIVE_HEALTHY && (
<div className="flex items-center gap-x-6">
<div>
<div className="flex items-center gap-1.5 text-foreground-light text-sm mb-1">
<div className="flex flex-col gap-y-1">
<Link
href={`/project/${ref}/editor`}
className="transition text-foreground-light hover:text-foreground text-sm"
>
Tables
</div>
<span className="text-2xl tabular-nums">
</Link>
<p className="text-2xl tabular-nums">
{isLoadingTables ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
tablesCount
)}
</span>
</p>
</div>
<div>
<div className="flex items-center gap-1.5 text-foreground-light text-sm mb-1">
<div className="flex flex-col gap-y-1">
<Link
href={`/project/${ref}/functions`}
className="transition text-foreground-light hover:text-foreground text-sm"
>
Functions
</div>
<span className="text-2xl tabular-nums">
</Link>
<p className="text-2xl tabular-nums">
{isLoadingFunctions ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
functionsCount
)}
</span>
</p>
</div>
{IS_PLATFORM && (
<div>
<div className="flex items-center gap-1.5 text-foreground-light text-sm mb-1">
<div className="flex flex-col gap-y-1">
<Link
href={`/project/${ref}/settings/infrastructure`}
className="transition text-foreground-light hover:text-foreground text-sm"
>
Replicas
</div>
<span className="text-2xl tabular-nums">
</Link>
<p className="text-2xl tabular-nums">
{isLoadingReplicas ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
replicasCount
)}
</span>
</p>
</div>
)}
</div>
@@ -338,7 +338,7 @@ export const LogsExplorerPage: NextPageWithLayout = () => {
text: 'Querying large date ranges can be slow. Consider selecting a smaller date range.',
})
}
if (editorValue && !editorValue.includes('limit')) {
if (editorValue && !editorValue.toLowerCase().includes('limit')) {
newWarnings.push({ text: 'When querying large date ranges, include a LIMIT clause.' })
}
setWarnings(newWarnings)
@@ -1,25 +0,0 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
export default function ApiKeysLegacyPage() {
const router = useRouter()
const { ref } = useParams()
// For backward compatibility, check if there's a view query parameter
const view = router.query.view as string
// Get the view from local storage if no query parameter
const [storedView] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.API_KEYS_VIEW(ref ?? ''), 'new-keys')
// Determine which page to redirect to
const targetView = view || storedView || 'new-keys'
useEffect(() => {
// Redirect to the appropriate page
router.replace(`/project/${ref}/settings/api-keys/${targetView}`)
}, [])
return null
}
@@ -1,11 +1,11 @@
import { LegacyAPIKeys } from 'components/interfaces/APIKeys/LegacyAPIKeys'
import ApiKeysLayout from 'components/layouts/APIKeys/APIKeysLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import { DisplayApiSettings } from 'components/ui/ProjectSettings'
import type { NextPageWithLayout } from 'types'
const ApiKeysLegacyPage: NextPageWithLayout = () => {
return <LegacyAPIKeys />
return <DisplayApiSettings legacy showNotice={false} />
}
ApiKeysLegacyPage.getLayout = (page) => (
@@ -7,7 +7,8 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
const next = searchParams.get('next') ?? '/'
const _next = searchParams.get('next')
const next = _next?.startsWith('/') ? _next : '/'
if (token_hash && type) {
const supabase = await createClient()
@@ -6,7 +6,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
const requestUrl = new URL(request.url)
const token_hash = requestUrl.searchParams.get('token_hash')
const type = requestUrl.searchParams.get('type') as EmailOtpType | null
const next = requestUrl.searchParams.get('next') || '/'
const _next = requestUrl.searchParams.get('next')
const next = _next?.startsWith('/') ? _next : '/'
if (token_hash && type) {
const { supabase, headers } = createClient(request)
@@ -27,7 +27,8 @@ const confirmFn = createServerFn({ method: 'GET' })
const searchParams = ctx.data
const token_hash = searchParams['token_hash'] as string
const type = searchParams['type'] as EmailOtpType | null
const next = (searchParams['next'] ?? '/') as string
const _next = searchParams['next'] as string
const next = _next?.startsWith('/') ? _next : '/'
if (token_hash && type) {
const supabase = createClient()
@@ -4,7 +4,8 @@ import { type LoaderFunctionArgs, redirect } from 'react-router'
export async function loader({ request }: LoaderFunctionArgs) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
const next = requestUrl.searchParams.get('next') || '/'
const _next = requestUrl.searchParams.get('next')
const next = _next?.startsWith('/') ? _next : '/'
if (code) {
const { supabase, headers } = createClient(request)
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more