feat(api gateway logs): add error code explanation (#36315)
Add the ability to look up error code explanation in API Gateway logs. Also a bunch of GraphQL-related utilities and generated types for calling the Content API.
This commit is contained in:
@@ -37,6 +37,31 @@ export const preferredRegion = [
|
||||
|
||||
const MAX_DEPTH = 5
|
||||
|
||||
function isAllowedCorsOrigin(origin: string): boolean {
|
||||
const exactMatches = IS_DEV
|
||||
? ['http://localhost:8082', 'https://supabase.com']
|
||||
: ['https://supabase.com']
|
||||
if (exactMatches.includes(origin)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return /^https:\/\/[\w-]+\w-supabase.vercel.app$/.test(origin)
|
||||
}
|
||||
|
||||
function getCorsHeaders(request: Request): Record<string, string> {
|
||||
const origin = request.headers.get('Origin')
|
||||
|
||||
if (origin && isAllowedCorsOrigin(origin)) {
|
||||
return {
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Accept',
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
const validationRules = [
|
||||
...specifiedRules,
|
||||
createQueryDepthLimiter(MAX_DEPTH),
|
||||
@@ -78,13 +103,18 @@ async function handleGraphQLRequest(request: Request): Promise<NextResponse> {
|
||||
const { query, variables, operationName } = parsedBody.data
|
||||
const validationErrors = validateGraphQLRequest(query, isDevGraphiQL(request))
|
||||
if (validationErrors.length > 0) {
|
||||
return NextResponse.json({
|
||||
errors: validationErrors.map((error) => ({
|
||||
message: error.message,
|
||||
locations: error.locations,
|
||||
path: error.path,
|
||||
})),
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
errors: validationErrors.map((error) => ({
|
||||
message: error.message,
|
||||
locations: error.locations,
|
||||
path: error.path,
|
||||
})),
|
||||
},
|
||||
{
|
||||
headers: getCorsHeaders(request),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const result = await graphql({
|
||||
@@ -94,7 +124,9 @@ async function handleGraphQLRequest(request: Request): Promise<NextResponse> {
|
||||
variableValues: variables,
|
||||
operationName,
|
||||
})
|
||||
return NextResponse.json(result)
|
||||
return NextResponse.json(result, {
|
||||
headers: getCorsHeaders(request),
|
||||
})
|
||||
}
|
||||
|
||||
function validateGraphQLRequest(query: string, isDevGraphiQL = false): ReadonlyArray<GraphQLError> {
|
||||
@@ -112,6 +144,14 @@ function validateGraphQLRequest(query: string, isDevGraphiQL = false): ReadonlyA
|
||||
return validate(rootGraphQLSchema, documentAST, rules)
|
||||
}
|
||||
|
||||
export async function OPTIONS(request: Request): Promise<NextResponse> {
|
||||
const corsHeaders = getCorsHeaders(request)
|
||||
return new NextResponse(null, {
|
||||
status: 204,
|
||||
headers: corsHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
try {
|
||||
const result = await handleGraphQLRequest(request)
|
||||
@@ -130,18 +170,28 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
// https://github.com/getsentry/sentry-javascript/issues/9626
|
||||
await Sentry.flush(2000)
|
||||
|
||||
return NextResponse.json({
|
||||
errors: [{ message: error.isPrivate() ? 'Internal Server Error' : error.message }],
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
errors: [{ message: error.isPrivate() ? 'Internal Server Error' : error.message }],
|
||||
},
|
||||
{
|
||||
headers: getCorsHeaders(request),
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Sentry.captureException(error)
|
||||
// Do not let Vercel close the process until Sentry has flushed
|
||||
// https://github.com/getsentry/sentry-javascript/issues/9626
|
||||
await Sentry.flush(2000)
|
||||
|
||||
return NextResponse.json({
|
||||
errors: [{ message: 'Internal Server Error' }],
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
errors: [{ message: 'Internal Server Error' }],
|
||||
},
|
||||
{
|
||||
headers: getCorsHeaders(request),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Alert_Shadcn_,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Badge,
|
||||
Button_Shadcn_,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from 'ui'
|
||||
import { useErrorCodesQuery } from 'data/content-api/docs-error-codes-query'
|
||||
import { type ErrorCodeQueryQuery, Service } from 'data/graphql/graphql'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
interface ErrorCodeDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
errorCode: string
|
||||
service?: Service
|
||||
}
|
||||
|
||||
export const ErrorCodeDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
errorCode,
|
||||
service,
|
||||
}: ErrorCodeDialogProps) => {
|
||||
const { data, isLoading, isSuccess, refetch } = useErrorCodesQuery(
|
||||
{ code: errorCode, service },
|
||||
{ enabled: open }
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-4">
|
||||
Help for error code <code>{errorCode}</code>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isLoading && <LoadingState />}
|
||||
{isSuccess && <SuccessState data={data} />}
|
||||
{!isLoading && !isSuccess && <ErrorState refetch={refetch} />}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingState = () => (
|
||||
<>
|
||||
<ShimmeringLoader className="w-3/4 mb-2" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</>
|
||||
)
|
||||
|
||||
const SuccessState = ({ data }: { data: ErrorCodeQueryQuery | undefined }) => {
|
||||
const errors = data?.errors?.nodes?.filter((error) => !!error.message)
|
||||
if (!errors || errors.length === 0) {
|
||||
return <>No information found for this error code.</>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4">Possible explanations for this error:</p>
|
||||
<div className="grid gap-2 grid-cols-[max-content_1fr]">
|
||||
{errors.map((error) => (
|
||||
<ErrorExplanation key={`${error.service}-${error.code}`} {...error} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorExplanation = ({
|
||||
code,
|
||||
service,
|
||||
message,
|
||||
}: {
|
||||
code: string
|
||||
service: Service
|
||||
message?: string | null
|
||||
}) => {
|
||||
if (!message) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Badge className="h-fit">{service}</Badge>
|
||||
<p>{message}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorState = ({ refetch }: { refetch?: () => void }) => (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<AlertTriangle />
|
||||
<AlertTitle_Shadcn_>Lookup failed</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
<p>Failed to look up error code help info</p>
|
||||
{refetch && (
|
||||
<Button_Shadcn_ variant="outline" size="sm" className="mt-2" onClick={refetch}>
|
||||
Try again
|
||||
</Button_Shadcn_>
|
||||
)}
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Service } from 'data/graphql/graphql'
|
||||
import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
Separator,
|
||||
} from 'ui'
|
||||
import { TimestampInfo } from 'ui-patterns'
|
||||
import { ErrorCodeDialog } from '../ErrorCodeDialog'
|
||||
import type { LogSearchCallback, PreviewLogData } from '../Logs.types'
|
||||
import { ResponseCodeFormatter } from '../LogsFormatters'
|
||||
|
||||
@@ -32,12 +34,18 @@ const PropertyRow = ({
|
||||
keyName,
|
||||
value,
|
||||
dataTestId,
|
||||
path,
|
||||
}: {
|
||||
keyName: string
|
||||
value: any
|
||||
dataTestId?: string
|
||||
path?: string
|
||||
}) => {
|
||||
const { setSearch } = useLogsUrlState()
|
||||
const [showErrorInfo, setShowErrorInfo] = useState(false)
|
||||
|
||||
const service = path?.startsWith('/auth/') ? Service.Auth : undefined
|
||||
|
||||
const handleSearch: LogSearchCallback = async (event: string, { query }: { query?: string }) => {
|
||||
setSearch(query || '')
|
||||
}
|
||||
@@ -118,88 +126,108 @@ const PropertyRow = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="group w-full" data-testid={dataTestId}>
|
||||
<div className="rounded-md w-full overflow-hidden">
|
||||
<div
|
||||
className={cn('flex h-10 w-full', {
|
||||
'flex-col gap-1.5 h-auto': isExpanded,
|
||||
'items-center group-hover:bg-surface-300 gap-4': !isExpanded,
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={cn('pl-3 text-foreground-lighter text-sm text-left', {
|
||||
'h-10 flex items-center': isExpanded,
|
||||
})}
|
||||
>
|
||||
{keyName}
|
||||
</h3>
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="group w-full" data-testid={dataTestId}>
|
||||
<div className="rounded-md w-full overflow-hidden">
|
||||
<div
|
||||
className={cn('text-xs flex-1 font-mono text-foreground pr-3', {
|
||||
'max-w-full text-left rounded-md p-2 bg-surface-300 text-xs w-full': isExpanded,
|
||||
'truncate text-right': !isExpanded,
|
||||
'text-brand-600': isCopied,
|
||||
className={cn('flex h-10 w-full', {
|
||||
'flex-col gap-1.5 h-auto': isExpanded,
|
||||
'items-center group-hover:bg-surface-300 gap-4': !isExpanded,
|
||||
})}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<LogRowCodeBlock value={value} />
|
||||
) : isTimestamp ? (
|
||||
<TimestampInfo className="text-sm" utcTimestamp={value} />
|
||||
) : isStatus ? (
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<ResponseCodeFormatter value={value} />
|
||||
</div>
|
||||
) : isMethod ? (
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<ResponseCodeFormatter value={value} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="truncate">{value}</div>
|
||||
)}
|
||||
<h3
|
||||
className={cn('pl-3 text-foreground-lighter text-sm text-left', {
|
||||
'h-10 flex items-center': isExpanded,
|
||||
})}
|
||||
>
|
||||
{keyName}
|
||||
</h3>
|
||||
<div
|
||||
className={cn('text-xs flex-1 font-mono text-foreground pr-3', {
|
||||
'max-w-full text-left rounded-md p-2 bg-surface-300 text-xs w-full': isExpanded,
|
||||
'truncate text-right': !isExpanded,
|
||||
'text-brand-600': isCopied,
|
||||
})}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<LogRowCodeBlock value={value} />
|
||||
) : isTimestamp ? (
|
||||
<TimestampInfo className="text-sm" utcTimestamp={value} />
|
||||
) : isStatus ? (
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<ResponseCodeFormatter value={value} />
|
||||
</div>
|
||||
) : isMethod ? (
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<ResponseCodeFormatter value={value} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="truncate">{value}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={handleCopy}>Copy {keyName}</DropdownMenuItem>
|
||||
{!isObject && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}}
|
||||
>
|
||||
{isExpanded ? 'Collapse' : 'Expand'} value
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(isMethod || isUserAgent || isStatus || isPath) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleSearch('search-input-change', { query: value })
|
||||
}}
|
||||
>
|
||||
Search by {keyName}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isSearch
|
||||
? getSearchPairs().map((pair) => (
|
||||
<DropdownMenuItem
|
||||
key={pair}
|
||||
onClick={() => {
|
||||
handleSearch('search-input-change', { query: pair })
|
||||
}}
|
||||
>
|
||||
Search by {pair}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
: null}
|
||||
</DropdownMenuContent>
|
||||
<LogRowSeparator />
|
||||
</DropdownMenu>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
{keyName === 'error_code' && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setShowErrorInfo(true)
|
||||
}}
|
||||
>
|
||||
More information
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={handleCopy}>Copy {keyName}</DropdownMenuItem>
|
||||
{!isObject && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}}
|
||||
>
|
||||
{isExpanded ? 'Collapse' : 'Expand'} value
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(isMethod || isUserAgent || isStatus || isPath) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleSearch('search-input-change', { query: value })
|
||||
}}
|
||||
>
|
||||
Search by {keyName}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isSearch
|
||||
? getSearchPairs().map((pair) => (
|
||||
<DropdownMenuItem
|
||||
key={pair}
|
||||
onClick={() => {
|
||||
handleSearch('search-input-change', { query: pair })
|
||||
}}
|
||||
>
|
||||
Search by {pair}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
: null}
|
||||
</DropdownMenuContent>
|
||||
<LogRowSeparator />
|
||||
</DropdownMenu>
|
||||
{keyName === 'error_code' && (
|
||||
<ErrorCodeDialog
|
||||
open={showErrorInfo}
|
||||
onOpenChange={setShowErrorInfo}
|
||||
errorCode={String(value)}
|
||||
service={service}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DefaultPreviewSelectionRenderer = ({ log }: { log: PreviewLogData }) => {
|
||||
const { timestamp, event_message, metadata, id, status, ...rest } = log
|
||||
const path = typeof log.path === 'string' ? log.path : undefined
|
||||
const log_file = log?.metadata?.[0]?.log_file
|
||||
|
||||
return (
|
||||
@@ -212,7 +240,7 @@ const DefaultPreviewSelectionRenderer = ({ log }: { log: PreviewLogData }) => {
|
||||
<PropertyRow key={'timestamp'} keyName={'timestamp'} value={log.timestamp} />
|
||||
)}
|
||||
{Object.entries(rest).map(([key, value]) => {
|
||||
return <PropertyRow key={key} keyName={key} value={value} />
|
||||
return <PropertyRow key={key} keyName={key} value={value} path={path} />
|
||||
})}
|
||||
|
||||
{log?.event_message && (
|
||||
|
||||
43
apps/studio/data/content-api/docs-error-codes-query.ts
Normal file
43
apps/studio/data/content-api/docs-error-codes-query.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useQuery, type UseQueryOptions } from '@tanstack/react-query'
|
||||
import { graphql } from 'data/graphql'
|
||||
import { executeGraphQL } from 'data/graphql/execute'
|
||||
import { Service } from 'data/graphql/graphql'
|
||||
import { contentApiKeys } from './keys'
|
||||
|
||||
const ErrorCodeQuery = graphql(`
|
||||
query ErrorCodeQuery($code: String!, $service: Service) {
|
||||
errors(code: $code, service: $service) {
|
||||
nodes {
|
||||
code
|
||||
service
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
interface Variables {
|
||||
code: string
|
||||
service?: Service
|
||||
}
|
||||
|
||||
async function getErrorCodeDescriptions({ code, service }: Variables, signal?: AbortSignal) {
|
||||
return await executeGraphQL(ErrorCodeQuery, { variables: { code, service }, signal })
|
||||
}
|
||||
|
||||
type ErrorCodeDescriptionsData = Awaited<ReturnType<typeof getErrorCodeDescriptions>>
|
||||
type ErrorCodeDescriptionsError = unknown
|
||||
|
||||
export const useErrorCodesQuery = <TData = ErrorCodeDescriptionsData>(
|
||||
variables: Variables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ErrorCodeDescriptionsData, ErrorCodeDescriptionsError, TData> = {}
|
||||
) => {
|
||||
return useQuery<ErrorCodeDescriptionsData, ErrorCodeDescriptionsError, TData>(
|
||||
contentApiKeys.errorCodes(variables),
|
||||
({ signal }) => getErrorCodeDescriptions(variables, signal),
|
||||
{ enabled, ...options }
|
||||
)
|
||||
}
|
||||
4
apps/studio/data/content-api/keys.ts
Normal file
4
apps/studio/data/content-api/keys.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const contentApiKeys = {
|
||||
errorCodes: ({ code, service }: { code: string; service?: string }) =>
|
||||
['content-api', 'error-codes', { code, service }] as const,
|
||||
}
|
||||
36
apps/studio/data/graphql/execute.ts
Normal file
36
apps/studio/data/graphql/execute.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { handleError } from 'data/fetchers'
|
||||
import type { TypedDocumentString } from './graphql'
|
||||
|
||||
const CONTENT_API_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL!
|
||||
|
||||
export async function executeGraphQL<TResult, TVariables>(
|
||||
query: TypedDocumentString<TResult, TVariables>,
|
||||
{ variables, signal }: { variables?: TVariables; signal?: AbortSignal }
|
||||
) {
|
||||
try {
|
||||
const response = await fetch(CONTENT_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed network response from Content API')
|
||||
}
|
||||
|
||||
const { data, errors } = await response.json()
|
||||
if (errors) {
|
||||
throw errors
|
||||
}
|
||||
|
||||
return data as TResult
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
}
|
||||
84
apps/studio/data/graphql/fragment-masking.ts
Normal file
84
apps/studio/data/graphql/fragment-masking.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/* eslint-disable */
|
||||
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core'
|
||||
import { Incremental, TypedDocumentString } from './graphql'
|
||||
|
||||
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
|
||||
TDocumentType extends DocumentTypeDecoration<infer TType, any>
|
||||
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
|
||||
? TKey extends string
|
||||
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
|
||||
: never
|
||||
: never
|
||||
: never
|
||||
|
||||
// return non-nullable if `fragmentType` is non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
): TType
|
||||
// return nullable if `fragmentType` is undefined
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
|
||||
): TType | undefined
|
||||
// return nullable if `fragmentType` is nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
|
||||
): TType | null
|
||||
// return nullable if `fragmentType` is nullable or undefined
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
|
||||
): TType | null | undefined
|
||||
// return array of non-nullable if `fragmentType` is array of non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
): Array<TType>
|
||||
// return array of nullable if `fragmentType` is array of nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||
): Array<TType> | null | undefined
|
||||
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
): ReadonlyArray<TType>
|
||||
// return readonly array of nullable if `fragmentType` is array of nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
|
||||
): ReadonlyArray<TType> | null | undefined
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType:
|
||||
| FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| null
|
||||
| undefined
|
||||
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
|
||||
return fragmentType as any
|
||||
}
|
||||
|
||||
export function makeFragmentData<
|
||||
F extends DocumentTypeDecoration<any, any>,
|
||||
FT extends ResultOf<F>,
|
||||
>(data: FT, _fragment: F): FragmentType<F> {
|
||||
return data as FragmentType<F>
|
||||
}
|
||||
export function isFragmentReady<TQuery, TFrag>(
|
||||
queryNode: TypedDocumentString<TQuery, any>,
|
||||
fragmentNode: TypedDocumentString<TFrag, any>,
|
||||
data: FragmentType<TypedDocumentString<Incremental<TFrag>, any>> | null | undefined
|
||||
): data is FragmentType<typeof fragmentNode> {
|
||||
const deferredFields = queryNode.__meta__?.deferredFields as Record<string, (keyof TFrag)[]>
|
||||
const fragName = fragmentNode.__meta__?.fragmentName as string | undefined
|
||||
|
||||
if (!deferredFields || !fragName) return true
|
||||
|
||||
const fields = deferredFields[fragName] ?? []
|
||||
return fields.length > 0 && fields.every((field) => data && field in data)
|
||||
}
|
||||
32
apps/studio/data/graphql/gql.ts
Normal file
32
apps/studio/data/graphql/gql.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable */
|
||||
import * as types from './graphql'
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
*
|
||||
* This map has several performance disadvantages:
|
||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
||||
*
|
||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
|
||||
*/
|
||||
type Documents = {
|
||||
'\n query ErrorCodeQuery($code: String!, $service: Service) {\n errors(code: $code, service: $service) {\n nodes {\n code\n service\n message\n }\n }\n }\n': typeof types.ErrorCodeQueryDocument
|
||||
}
|
||||
const documents: Documents = {
|
||||
'\n query ErrorCodeQuery($code: String!, $service: Service) {\n errors(code: $code, service: $service) {\n nodes {\n code\n service\n message\n }\n }\n }\n':
|
||||
types.ErrorCodeQueryDocument,
|
||||
}
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: '\n query ErrorCodeQuery($code: String!, $service: Service) {\n errors(code: $code, service: $service) {\n nodes {\n code\n service\n message\n }\n }\n }\n'
|
||||
): typeof import('./graphql').ErrorCodeQueryDocument
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {}
|
||||
}
|
||||
266
apps/studio/data/graphql/graphql.ts
Normal file
266
apps/studio/data/graphql/graphql.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/* eslint-disable */
|
||||
import { DocumentTypeDecoration } from '@graphql-typed-document-node/core'
|
||||
export type Maybe<T> = T | null
|
||||
export type InputMaybe<T> = Maybe<T>
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
|
||||
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }
|
||||
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }
|
||||
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = {
|
||||
[_ in K]?: never
|
||||
}
|
||||
export type Incremental<T> =
|
||||
| T
|
||||
| { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }
|
||||
/** All built-in and custom scalars, mapped to their actual values */
|
||||
export type Scalars = {
|
||||
ID: { input: string; output: string }
|
||||
String: { input: string; output: string }
|
||||
Boolean: { input: boolean; output: boolean }
|
||||
Int: { input: number; output: number }
|
||||
Float: { input: number; output: number }
|
||||
}
|
||||
|
||||
/** A reference document containing a description of a Supabase CLI command */
|
||||
export type CliCommandReference = SearchResult & {
|
||||
__typename?: 'CLICommandReference'
|
||||
/** The content of the reference document, as text */
|
||||
content?: Maybe<Scalars['String']['output']>
|
||||
/** The URL of the document */
|
||||
href?: Maybe<Scalars['String']['output']>
|
||||
/** The title of the document */
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** A reference document containing a description of a function from a Supabase client library */
|
||||
export type ClientLibraryFunctionReference = SearchResult & {
|
||||
__typename?: 'ClientLibraryFunctionReference'
|
||||
/** The content of the reference document, as text */
|
||||
content?: Maybe<Scalars['String']['output']>
|
||||
/** The URL of the document */
|
||||
href?: Maybe<Scalars['String']['output']>
|
||||
/** The programming language for which the function is written */
|
||||
language: Language
|
||||
/** The name of the function or method */
|
||||
methodName?: Maybe<Scalars['String']['output']>
|
||||
/** The title of the document */
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** An error returned by a Supabase service */
|
||||
export type Error = {
|
||||
__typename?: 'Error'
|
||||
/** The unique code identifying the error. The code is stable, and can be used for string matching during error handling. */
|
||||
code: Scalars['String']['output']
|
||||
/** The HTTP status code returned with this error. */
|
||||
httpStatusCode?: Maybe<Scalars['Int']['output']>
|
||||
/** A human-readable message describing the error. The message is not stable, and should not be used for string matching during error handling. Use the code instead. */
|
||||
message?: Maybe<Scalars['String']['output']>
|
||||
/** The Supabase service that returns this error. */
|
||||
service: Service
|
||||
}
|
||||
|
||||
/** A collection of Errors */
|
||||
export type ErrorCollection = {
|
||||
__typename?: 'ErrorCollection'
|
||||
/** A list of edges containing nodes in this collection */
|
||||
edges: Array<ErrorEdge>
|
||||
/** The nodes in this collection, directly accessible */
|
||||
nodes: Array<Error>
|
||||
/** Pagination information */
|
||||
pageInfo: PageInfo
|
||||
/** The total count of items available in this collection */
|
||||
totalCount: Scalars['Int']['output']
|
||||
}
|
||||
|
||||
/** An edge in a collection of Errors */
|
||||
export type ErrorEdge = {
|
||||
__typename?: 'ErrorEdge'
|
||||
/** A cursor for use in pagination */
|
||||
cursor: Scalars['String']['output']
|
||||
/** The Error at the end of the edge */
|
||||
node: Error
|
||||
}
|
||||
|
||||
/** A document containing content from the Supabase docs. This is a guide, which might describe a concept, or explain the steps for using or implementing a feature. */
|
||||
export type Guide = SearchResult & {
|
||||
__typename?: 'Guide'
|
||||
/** The full content of the document, including all subsections (both those matching and not matching any query string) and possibly more content */
|
||||
content?: Maybe<Scalars['String']['output']>
|
||||
/** The URL of the document */
|
||||
href?: Maybe<Scalars['String']['output']>
|
||||
/** The subsections of the document. If the document is returned from a search match, only matching content chunks are returned. For the full content of the original document, use the content field in the parent Guide. */
|
||||
subsections?: Maybe<SubsectionCollection>
|
||||
/** The title of the document */
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
export enum Language {
|
||||
Csharp = 'CSHARP',
|
||||
Dart = 'DART',
|
||||
Javascript = 'JAVASCRIPT',
|
||||
Kotlin = 'KOTLIN',
|
||||
Python = 'PYTHON',
|
||||
Swift = 'SWIFT',
|
||||
}
|
||||
|
||||
/** Pagination information for a collection */
|
||||
export type PageInfo = {
|
||||
__typename?: 'PageInfo'
|
||||
/** Cursor pointing to the end of the current page */
|
||||
endCursor?: Maybe<Scalars['String']['output']>
|
||||
/** Whether there are more items after the current page */
|
||||
hasNextPage: Scalars['Boolean']['output']
|
||||
/** Whether there are more items before the current page */
|
||||
hasPreviousPage: Scalars['Boolean']['output']
|
||||
/** Cursor pointing to the start of the current page */
|
||||
startCursor?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
export type RootQueryType = {
|
||||
__typename?: 'RootQueryType'
|
||||
/** Get the details of an error code returned from a Supabase service */
|
||||
error?: Maybe<Error>
|
||||
/** Get error codes that can potentially be returned by Supabase services */
|
||||
errors?: Maybe<ErrorCollection>
|
||||
/** Get the GraphQL schema for this endpoint */
|
||||
schema: Scalars['String']['output']
|
||||
/** Search the Supabase docs for content matching a query string */
|
||||
searchDocs?: Maybe<SearchResultCollection>
|
||||
}
|
||||
|
||||
export type RootQueryTypeErrorArgs = {
|
||||
code: Scalars['String']['input']
|
||||
service: Service
|
||||
}
|
||||
|
||||
export type RootQueryTypeErrorsArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>
|
||||
before?: InputMaybe<Scalars['String']['input']>
|
||||
code?: InputMaybe<Scalars['String']['input']>
|
||||
first?: InputMaybe<Scalars['Int']['input']>
|
||||
last?: InputMaybe<Scalars['Int']['input']>
|
||||
service?: InputMaybe<Service>
|
||||
}
|
||||
|
||||
export type RootQueryTypeSearchDocsArgs = {
|
||||
limit?: InputMaybe<Scalars['Int']['input']>
|
||||
query: Scalars['String']['input']
|
||||
}
|
||||
|
||||
/** Document that matches a search query */
|
||||
export type SearchResult = {
|
||||
/** The full content of the matching result */
|
||||
content?: Maybe<Scalars['String']['output']>
|
||||
/** The URL of the matching result */
|
||||
href?: Maybe<Scalars['String']['output']>
|
||||
/** The title of the matching result */
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** A collection of search results containing content from Supabase docs */
|
||||
export type SearchResultCollection = {
|
||||
__typename?: 'SearchResultCollection'
|
||||
/** A list of edges containing nodes in this collection */
|
||||
edges: Array<SearchResultEdge>
|
||||
/** The nodes in this collection, directly accessible */
|
||||
nodes: Array<SearchResult>
|
||||
/** The total count of items available in this collection */
|
||||
totalCount: Scalars['Int']['output']
|
||||
}
|
||||
|
||||
/** An edge in a collection of SearchResults */
|
||||
export type SearchResultEdge = {
|
||||
__typename?: 'SearchResultEdge'
|
||||
/** The SearchResult at the end of the edge */
|
||||
node: SearchResult
|
||||
}
|
||||
|
||||
export enum Service {
|
||||
Auth = 'AUTH',
|
||||
Realtime = 'REALTIME',
|
||||
Storage = 'STORAGE',
|
||||
}
|
||||
|
||||
/** A content chunk taken from a larger document in the Supabase docs */
|
||||
export type Subsection = {
|
||||
__typename?: 'Subsection'
|
||||
/** The content of the subsection */
|
||||
content?: Maybe<Scalars['String']['output']>
|
||||
/** The URL of the subsection */
|
||||
href?: Maybe<Scalars['String']['output']>
|
||||
/** The title of the subsection */
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
/** A collection of content chunks from a larger document in the Supabase docs. */
|
||||
export type SubsectionCollection = {
|
||||
__typename?: 'SubsectionCollection'
|
||||
/** A list of edges containing nodes in this collection */
|
||||
edges: Array<SubsectionEdge>
|
||||
/** The nodes in this collection, directly accessible */
|
||||
nodes: Array<Subsection>
|
||||
/** The total count of items available in this collection */
|
||||
totalCount: Scalars['Int']['output']
|
||||
}
|
||||
|
||||
/** An edge in a collection of Subsections */
|
||||
export type SubsectionEdge = {
|
||||
__typename?: 'SubsectionEdge'
|
||||
/** The Subsection at the end of the edge */
|
||||
node: Subsection
|
||||
}
|
||||
|
||||
/** A document describing how to troubleshoot an issue when using Supabase */
|
||||
export type TroubleshootingGuide = SearchResult & {
|
||||
__typename?: 'TroubleshootingGuide'
|
||||
/** The full content of the troubleshooting guide */
|
||||
content?: Maybe<Scalars['String']['output']>
|
||||
/** The URL of the troubleshooting guide */
|
||||
href?: Maybe<Scalars['String']['output']>
|
||||
/** The title of the troubleshooting guide */
|
||||
title?: Maybe<Scalars['String']['output']>
|
||||
}
|
||||
|
||||
export type ErrorCodeQueryQueryVariables = Exact<{
|
||||
code: Scalars['String']['input']
|
||||
service?: InputMaybe<Service>
|
||||
}>
|
||||
|
||||
export type ErrorCodeQueryQuery = {
|
||||
__typename?: 'RootQueryType'
|
||||
errors?: {
|
||||
__typename?: 'ErrorCollection'
|
||||
nodes: Array<{ __typename?: 'Error'; code: string; service: Service; message?: string | null }>
|
||||
} | null
|
||||
}
|
||||
|
||||
export class TypedDocumentString<TResult, TVariables>
|
||||
extends String
|
||||
implements DocumentTypeDecoration<TResult, TVariables>
|
||||
{
|
||||
__apiType?: DocumentTypeDecoration<TResult, TVariables>['__apiType']
|
||||
private value: string
|
||||
public __meta__?: Record<string, any> | undefined
|
||||
|
||||
constructor(value: string, __meta__?: Record<string, any> | undefined) {
|
||||
super(value)
|
||||
this.value = value
|
||||
this.__meta__ = __meta__
|
||||
}
|
||||
|
||||
toString(): string & DocumentTypeDecoration<TResult, TVariables> {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
|
||||
export const ErrorCodeQueryDocument = new TypedDocumentString(`
|
||||
query ErrorCodeQuery($code: String!, $service: Service) {
|
||||
errors(code: $code, service: $service) {
|
||||
nodes {
|
||||
code
|
||||
service
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`) as unknown as TypedDocumentString<ErrorCodeQueryQuery, ErrorCodeQueryQueryVariables>
|
||||
2
apps/studio/data/graphql/index.ts
Normal file
2
apps/studio/data/graphql/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './fragment-masking'
|
||||
export * from './gql'
|
||||
@@ -29,6 +29,11 @@ const SUPABASE_DOCS_PROJECT_URL = process.env.NEXT_PUBLIC_SUPABASE_URL
|
||||
? new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).origin
|
||||
: ''
|
||||
|
||||
// Needed to test docs content API in local dev
|
||||
const SUPABASE_CONTENT_API_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL
|
||||
? new URL(process.env.NEXT_PUBLIC_CONTENT_API_URL).origin
|
||||
: ''
|
||||
|
||||
const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red'
|
||||
const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red'
|
||||
const SUPABASE_COM_URL = 'https://supabase.com'
|
||||
@@ -78,7 +83,7 @@ const csp = [
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
||||
? [
|
||||
`default-src 'self' ${DEFAULT_SRC_URLS} ${SUPABASE_STAGING_PROJECTS_URL} ${SUPABASE_STAGING_PROJECTS_URL_WS} ${VERCEL_LIVE_URL} ${PUSHER_URL_WS} ${SUPABASE_DOCS_PROJECT_URL} ${SENTRY_URL};`,
|
||||
`default-src 'self' ${DEFAULT_SRC_URLS} ${SUPABASE_STAGING_PROJECTS_URL} ${SUPABASE_STAGING_PROJECTS_URL_WS} ${VERCEL_LIVE_URL} ${PUSHER_URL_WS} ${SUPABASE_DOCS_PROJECT_URL} ${SUPABASE_CONTENT_API_URL} ${SENTRY_URL};`,
|
||||
`script-src 'self' 'unsafe-eval' 'unsafe-inline' ${SCRIPT_SRC_URLS} ${VERCEL_LIVE_URL} ${PUSHER_URL};`,
|
||||
`frame-src 'self' ${FRAME_SRC_URLS} ${VERCEL_LIVE_URL};`,
|
||||
`img-src 'self' blob: data: ${IMG_SRC_URLS} ${SUPABASE_STAGING_PROJECTS_URL} ${VERCEL_URL};`,
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prettier:check": "prettier --check .",
|
||||
"prettier:write": "prettier --write .",
|
||||
"build:deno-types": "tsx scripts/deno-types.ts"
|
||||
"build:deno-types": "tsx scripts/deno-types.ts",
|
||||
"build:graphql-types": "tsx scripts/download-graphql-schema.mts && pnpm graphql-codegen --config scripts/codegen.ts",
|
||||
"build:graphql-types:watch": "pnpm graphql-codegen --config scripts/codegen.ts --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
@@ -136,6 +138,8 @@
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.5",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@radix-ui/react-use-escape-keydown": "^1.0.3",
|
||||
"@supabase/postgres-meta": "^0.64.4",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
|
||||
17
apps/studio/scripts/codegen.ts
Normal file
17
apps/studio/scripts/codegen.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli'
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: 'scripts/schema.graphql',
|
||||
documents: ['data/**/*.ts'],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
'data/graphql/': {
|
||||
preset: 'client',
|
||||
config: {
|
||||
documentMode: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
42
apps/studio/scripts/download-graphql-schema.mts
Normal file
42
apps/studio/scripts/download-graphql-schema.mts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { stripIndent } from 'common-tags'
|
||||
import { writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
async function downloadGraphQLSchema() {
|
||||
const schemaEndpoint = 'https://supabase.com/docs/api/graphql'
|
||||
const outputPath = path.join(__dirname, './schema.graphql')
|
||||
|
||||
const schemaQuery = stripIndent`
|
||||
query SchemaQuery {
|
||||
schema
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const response = await fetch(schemaEndpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
query: schemaQuery.trim(),
|
||||
}),
|
||||
})
|
||||
const { data, errors } = await response.json()
|
||||
|
||||
if (errors) {
|
||||
throw errors
|
||||
}
|
||||
|
||||
writeFileSync(outputPath, data.schema, 'utf8')
|
||||
|
||||
console.log(`✅ Successfully downloaded GraphQL schema to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error('🚨 Error generating GraphQL schema:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
downloadGraphQLSchema()
|
||||
}
|
||||
237
apps/studio/scripts/schema.graphql
Normal file
237
apps/studio/scripts/schema.graphql
Normal file
@@ -0,0 +1,237 @@
|
||||
schema {
|
||||
query: RootQueryType
|
||||
}
|
||||
|
||||
"""
|
||||
A document containing content from the Supabase docs. This is a guide, which might describe a concept, or explain the steps for using or implementing a feature.
|
||||
"""
|
||||
type Guide implements SearchResult {
|
||||
"""The title of the document"""
|
||||
title: String
|
||||
|
||||
"""The URL of the document"""
|
||||
href: String
|
||||
|
||||
"""
|
||||
The full content of the document, including all subsections (both those matching and not matching any query string) and possibly more content
|
||||
"""
|
||||
content: String
|
||||
|
||||
"""
|
||||
The subsections of the document. If the document is returned from a search match, only matching content chunks are returned. For the full content of the original document, use the content field in the parent Guide.
|
||||
"""
|
||||
subsections: SubsectionCollection
|
||||
}
|
||||
|
||||
"""Document that matches a search query"""
|
||||
interface SearchResult {
|
||||
"""The title of the matching result"""
|
||||
title: String
|
||||
|
||||
"""The URL of the matching result"""
|
||||
href: String
|
||||
|
||||
"""The full content of the matching result"""
|
||||
content: String
|
||||
}
|
||||
|
||||
"""
|
||||
A collection of content chunks from a larger document in the Supabase docs.
|
||||
"""
|
||||
type SubsectionCollection {
|
||||
"""A list of edges containing nodes in this collection"""
|
||||
edges: [SubsectionEdge!]!
|
||||
|
||||
"""The nodes in this collection, directly accessible"""
|
||||
nodes: [Subsection!]!
|
||||
|
||||
"""The total count of items available in this collection"""
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
"""An edge in a collection of Subsections"""
|
||||
type SubsectionEdge {
|
||||
"""The Subsection at the end of the edge"""
|
||||
node: Subsection!
|
||||
}
|
||||
|
||||
"""A content chunk taken from a larger document in the Supabase docs"""
|
||||
type Subsection {
|
||||
"""The title of the subsection"""
|
||||
title: String
|
||||
|
||||
"""The URL of the subsection"""
|
||||
href: String
|
||||
|
||||
"""The content of the subsection"""
|
||||
content: String
|
||||
}
|
||||
|
||||
"""
|
||||
A reference document containing a description of a Supabase CLI command
|
||||
"""
|
||||
type CLICommandReference implements SearchResult {
|
||||
"""The title of the document"""
|
||||
title: String
|
||||
|
||||
"""The URL of the document"""
|
||||
href: String
|
||||
|
||||
"""The content of the reference document, as text"""
|
||||
content: String
|
||||
}
|
||||
|
||||
"""
|
||||
A reference document containing a description of a function from a Supabase client library
|
||||
"""
|
||||
type ClientLibraryFunctionReference implements SearchResult {
|
||||
"""The title of the document"""
|
||||
title: String
|
||||
|
||||
"""The URL of the document"""
|
||||
href: String
|
||||
|
||||
"""The content of the reference document, as text"""
|
||||
content: String
|
||||
|
||||
"""The programming language for which the function is written"""
|
||||
language: Language!
|
||||
|
||||
"""The name of the function or method"""
|
||||
methodName: String
|
||||
}
|
||||
|
||||
enum Language {
|
||||
JAVASCRIPT
|
||||
SWIFT
|
||||
DART
|
||||
CSHARP
|
||||
KOTLIN
|
||||
PYTHON
|
||||
}
|
||||
|
||||
"""A document describing how to troubleshoot an issue when using Supabase"""
|
||||
type TroubleshootingGuide implements SearchResult {
|
||||
"""The title of the troubleshooting guide"""
|
||||
title: String
|
||||
|
||||
"""The URL of the troubleshooting guide"""
|
||||
href: String
|
||||
|
||||
"""The full content of the troubleshooting guide"""
|
||||
content: String
|
||||
}
|
||||
|
||||
type RootQueryType {
|
||||
"""Get the GraphQL schema for this endpoint"""
|
||||
schema: String!
|
||||
|
||||
"""Search the Supabase docs for content matching a query string"""
|
||||
searchDocs(query: String!, limit: Int): SearchResultCollection
|
||||
|
||||
"""Get the details of an error code returned from a Supabase service"""
|
||||
error(code: String!, service: Service!): Error
|
||||
|
||||
"""Get error codes that can potentially be returned by Supabase services"""
|
||||
errors(
|
||||
"""Returns the first n elements from the list"""
|
||||
first: Int
|
||||
|
||||
"""Returns elements that come after the specified cursor"""
|
||||
after: String
|
||||
|
||||
"""Returns the last n elements from the list"""
|
||||
last: Int
|
||||
|
||||
"""Returns elements that come before the specified cursor"""
|
||||
before: String
|
||||
|
||||
"""Filter errors by a specific Supabase service"""
|
||||
service: Service
|
||||
|
||||
"""Filter errors by a specific error code"""
|
||||
code: String
|
||||
): ErrorCollection
|
||||
}
|
||||
|
||||
"""A collection of search results containing content from Supabase docs"""
|
||||
type SearchResultCollection {
|
||||
"""A list of edges containing nodes in this collection"""
|
||||
edges: [SearchResultEdge!]!
|
||||
|
||||
"""The nodes in this collection, directly accessible"""
|
||||
nodes: [SearchResult!]!
|
||||
|
||||
"""The total count of items available in this collection"""
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
"""An edge in a collection of SearchResults"""
|
||||
type SearchResultEdge {
|
||||
"""The SearchResult at the end of the edge"""
|
||||
node: SearchResult!
|
||||
}
|
||||
|
||||
"""An error returned by a Supabase service"""
|
||||
type Error {
|
||||
"""
|
||||
The unique code identifying the error. The code is stable, and can be used for string matching during error handling.
|
||||
"""
|
||||
code: String!
|
||||
|
||||
"""The Supabase service that returns this error."""
|
||||
service: Service!
|
||||
|
||||
"""The HTTP status code returned with this error."""
|
||||
httpStatusCode: Int
|
||||
|
||||
"""
|
||||
A human-readable message describing the error. The message is not stable, and should not be used for string matching during error handling. Use the code instead.
|
||||
"""
|
||||
message: String
|
||||
}
|
||||
|
||||
enum Service {
|
||||
AUTH
|
||||
REALTIME
|
||||
STORAGE
|
||||
}
|
||||
|
||||
"""A collection of Errors"""
|
||||
type ErrorCollection {
|
||||
"""A list of edges containing nodes in this collection"""
|
||||
edges: [ErrorEdge!]!
|
||||
|
||||
"""The nodes in this collection, directly accessible"""
|
||||
nodes: [Error!]!
|
||||
|
||||
"""Pagination information"""
|
||||
pageInfo: PageInfo!
|
||||
|
||||
"""The total count of items available in this collection"""
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
"""An edge in a collection of Errors"""
|
||||
type ErrorEdge {
|
||||
"""The Error at the end of the edge"""
|
||||
node: Error!
|
||||
|
||||
"""A cursor for use in pagination"""
|
||||
cursor: String!
|
||||
}
|
||||
|
||||
"""Pagination information for a collection"""
|
||||
type PageInfo {
|
||||
"""Whether there are more items after the current page"""
|
||||
hasNextPage: Boolean!
|
||||
|
||||
"""Whether there are more items before the current page"""
|
||||
hasPreviousPage: Boolean!
|
||||
|
||||
"""Cursor pointing to the start of the current page"""
|
||||
startCursor: String
|
||||
|
||||
"""Cursor pointing to the end of the current page"""
|
||||
endCursor: String
|
||||
}
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
@@ -985,6 +985,12 @@ importers:
|
||||
specifier: ^4.4.2
|
||||
version: 4.4.2
|
||||
devDependencies:
|
||||
'@graphql-codegen/cli':
|
||||
specifier: 5.0.5
|
||||
version: 5.0.5(@parcel/watcher@2.5.1)(@types/node@22.13.14)(encoding@0.1.13)(graphql-sock@1.0.1(graphql@16.10.0))(graphql@16.10.0)(supports-color@8.1.1)(typescript@5.5.2)
|
||||
'@graphql-typed-document-node/core':
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(graphql@16.10.0)
|
||||
'@radix-ui/react-use-escape-keydown':
|
||||
specifier: ^1.0.3
|
||||
version: 1.1.0(@types/react@18.3.3)(react@18.3.1)
|
||||
@@ -12611,9 +12617,6 @@ packages:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
|
||||
jose@5.2.1:
|
||||
resolution: {integrity: sha512-qiaQhtQRw6YrOaOj0v59h3R6hUY9NvxBmmnMfKemkqYmBB0tEc97NbLP7ix44VP5p9/0YHG8Vyhzuo5YBNwviA==}
|
||||
|
||||
jose@5.9.6:
|
||||
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
||||
|
||||
@@ -20381,12 +20384,12 @@ snapshots:
|
||||
'@whatwg-node/fetch': 0.10.6
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
dotenv: 16.4.7
|
||||
dotenv: 16.5.0
|
||||
graphql: 16.10.0
|
||||
graphql-request: 6.1.0(encoding@0.1.13)(graphql@16.10.0)
|
||||
http-proxy-agent: 7.0.2(supports-color@8.1.1)
|
||||
https-proxy-agent: 7.0.6(supports-color@8.1.1)
|
||||
jose: 5.2.1
|
||||
jose: 5.9.6
|
||||
js-yaml: 4.1.0
|
||||
lodash: 4.17.21
|
||||
scuid: 1.1.0
|
||||
@@ -31464,8 +31467,6 @@ snapshots:
|
||||
|
||||
jiti@2.4.2: {}
|
||||
|
||||
jose@5.2.1: {}
|
||||
|
||||
jose@5.9.6: {}
|
||||
|
||||
jotai@2.8.1(@types/react@18.3.3)(react@18.3.1):
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"env": [
|
||||
"ANALYZE",
|
||||
"NEXT_PUBLIC_SUPPORT_API_URL",
|
||||
"NEXT_PUBLIC_CONTENT_API_URL",
|
||||
"NEXT_PUBLIC_BASE_PATH",
|
||||
"NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
|
||||
"NEXT_PUBLIC_SUPPORT_ANON_KEY",
|
||||
|
||||
Reference in New Issue
Block a user