mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
Merge branch 'master' into feature/branching-gitless-merge
This commit is contained in:
commit
1ebce69137
109 files changed
+1040
-698
No files matched your search
@@ -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" />
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||

|
||||
|
||||
@@ -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!
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`),
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'))
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -62,7 +62,13 @@ const InfoTooltip = ({
|
||||
[_setTooltipOpen, id, isMobile]
|
||||
)
|
||||
|
||||
useEffect(() => () => clearTimeout(timeout.current), [])
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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,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({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -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' }),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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 || '')
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+2
-1
@@ -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
Reference in new issue
Block a user