'use client' import Editor, { useMonaco } from '@monaco-editor/react' import { ChevronUp, GitPullRequest } from 'lucide-react' import type { editor } from 'monaco-editor' import { useTheme } from 'next-themes' import { PropsWithChildren, useCallback, useEffect, useLayoutEffect, useMemo, useState, } from 'react' import Markdown from 'react-markdown' import { format } from 'sql-formatter' import { HttpRequest, ParsingError, RenderError, Statement, SupabaseJsQuery, UnimplementedError, UnsupportedError, formatCurl, formatHttp, processSql, renderHttp, renderSupabaseJs, } from '@supabase/sql-to-rest' import { CodeBlock, Collapsible, Tabs, cn } from 'ui' import { Alert } from 'ui/src/components/shadcn/ui/alert' import { assumptions } from './assumptions' import { BaseUrlDialog } from './base-url-dialog' import { faqs } from './faqs' import { transformRenderer } from './syntax-highlighter/transform-renderer' import { ResultBundle } from './util' export interface SqlToRestProps { defaultValue?: string defaultBaseUrl?: string } export default function SqlToRest({ defaultValue, defaultBaseUrl = 'http://localhost:54321/rest/v1', }: SqlToRestProps) { const monaco = useMonaco() const { resolvedTheme } = useTheme() const isDark = resolvedTheme?.includes('dark') ?? true useLayoutEffect(() => { if (monaco) { const lightMode = getTheme(false) const darkMode = getTheme(true) monaco.editor.defineTheme('supabase-light', lightMode) monaco.editor.defineTheme('supabase-dark', darkMode) } }, [monaco]) const [sql, setSql] = useState(defaultValue ?? '') const [statement, setStatement] = useState() const [currentLanguage, setCurrentLanguage] = useState('curl') const [httpRequest, setHttpRequest] = useState() const [jsQuery, setJsQuery] = useState() const [parsingError, setParsingError] = useState() const [unimplementedError, setUnimplementedError] = useState() const [unsupportedError, setUnsupportedError] = useState() const [httpRenderError, setHttpRenderError] = useState() const [supabaseJsRenderError, setSupabaseJsRenderError] = useState() const [isBaseUrlDialogOpen, setIsBaseUrlDialogOpen] = useState(false) const [baseUrl, setBaseUrl] = useState(defaultBaseUrl) const baseUrlObject = useMemo(() => { try { return new URL(baseUrl) } catch (err) { return undefined } }, [baseUrl]) const codeBlockRenderer = useMemo( () => transformRenderer({ search: (text) => !!baseUrlObject && text.includes(baseUrlObject.host), wrapper: ({ children }) => ( setIsBaseUrlDialogOpen(true)} > {children} ), }), [baseUrlObject] ) const rawHttp = useMemo(() => { if (!httpRequest) { return } return formatHttp(baseUrl, httpRequest) }, [httpRequest, baseUrl]) const curlCommand = useMemo(() => { if (!httpRequest) { return } return formatCurl(baseUrl, httpRequest) }, [httpRequest, baseUrl]) const jsCommand = useMemo(() => { if (!jsQuery) { return } const { code } = jsQuery return code }, [jsQuery]) const relevantFaqs = useMemo(() => { if (!statement) { return [] } switch (currentLanguage) { case 'http': case 'curl': { if (!httpRequest) { return [] } const result: ResultBundle = { type: 'http', language: currentLanguage, statement, ...httpRequest, } return faqs.filter((faq) => faq.condition(result)) } case 'js': { if (!jsQuery) { return [] } const result: ResultBundle = { type: 'supabase-js', language: currentLanguage, statement, ...jsQuery, } return faqs.filter((faq) => faq.condition(result)) } default: return [] } }, [currentLanguage, statement, httpRequest, jsQuery]) const relevantAssumptions = useMemo(() => { if (!statement) { return [] } switch (currentLanguage) { case 'http': case 'curl': { if (!httpRequest) { return [] } const result: ResultBundle = { type: 'http', language: currentLanguage, statement, ...httpRequest, } return assumptions.filter((a) => a.condition(result)).flatMap((a) => a.assumptions(result)) } case 'js': { if (!jsQuery) { return [] } const result: ResultBundle = { type: 'supabase-js', language: currentLanguage, statement, ...jsQuery, } return assumptions.filter((a) => a.condition(result)).flatMap((a) => a.assumptions(result)) } default: return [] } }, [currentLanguage, statement, httpRequest, jsQuery]) const process = useCallback(async (sql: string) => { setSql(sql) try { const statement = await processSql(sql) const httpRequest = await renderHttp(statement) const jsQuery = await renderSupabaseJs(statement) setParsingError(undefined) setUnimplementedError(undefined) setUnsupportedError(undefined) setHttpRenderError(undefined) setSupabaseJsRenderError(undefined) setStatement(statement) setHttpRequest(httpRequest) setJsQuery(jsQuery) } catch (error) { setParsingError(undefined) setUnimplementedError(undefined) setUnsupportedError(undefined) setHttpRenderError(undefined) setSupabaseJsRenderError(undefined) if (error instanceof ParsingError) { setParsingError(error) } else if (error instanceof UnimplementedError) { setUnimplementedError(error) } else if (error instanceof UnsupportedError) { setUnsupportedError(error) } else if (error instanceof RenderError) { if (error.renderer === 'http') { setHttpRenderError(error) } else if (error.renderer === 'supabase-js') { setSupabaseJsRenderError(error) } else { console.error(error) } } else { console.error(error) } } }, []) // Process initial value only useEffect(() => { process(sql) }, [process]) return (
Enter SQL to translate
{ // Register pgsql formatter monaco.languages.registerDocumentFormattingEditProvider('pgsql', { async provideDocumentFormattingEdits(model) { const currentCode = editor.getValue() const formattedCode = format(currentCode, { language: 'postgresql', keywordCase: 'lower', }) return [ { range: model.getFullModelRange(), text: formattedCode, }, ] }, }) // Format on cmd+s editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, async () => { await editor.getAction('editor.action.formatDocument').run() }) // Run format on the initial value await editor.getAction('editor.action.formatDocument').run() }} onChange={async (sql) => { if (!sql) { return } await process(sql) }} />
{parsingError && ( {parsingError.message}. {parsingError.hint} )} {unsupportedError && (
{unsupportedError.message}. {unsupportedError.hint}
PostgREST doesn't support this query. If you're sure the syntax is correct and are unable to modify it, wrap it in a stored procedure and call it using the{' '} RPC {' '} endpoint.
)} {unimplementedError && ( {unimplementedError.message}. )}
setBaseUrl(value)} open={isBaseUrlDialogOpen} onOpenChange={(open) => setIsBaseUrlDialogOpen(open)} />
Choose language to translate to
setCurrentLanguage(id)}> {httpRenderError && {httpRenderError.message}} {curlCommand} {httpRenderError && {httpRenderError.message}} {rawHttp} {supabaseJsRenderError && ( {supabaseJsRenderError.message} )} {jsCommand}
{relevantAssumptions.length > 0 && (

Assumptions

    {relevantAssumptions.map((assumption) => (
  1. {assumption}
  2. ))}
)} {relevantFaqs.length > 0 && ( <>

FAQs

{relevantFaqs.map((faq) => (
, }} > {faq.answer}
))} )}
) } // TODO: this was copied from studio - find a way to share it between sites function getTheme(isDarkMode: boolean): editor.IStandaloneThemeData { return { base: isDarkMode ? 'vs-dark' : 'vs', // can also be vs-dark or hc-black inherit: true, // can also be false to completely replace the builtin rules rules: [ { token: '', background: isDarkMode ? '1f1f1f' : 'f0f0f0', foreground: isDarkMode ? 'd4d4d4' : '444444', }, { token: 'string.sql', foreground: '24b47e' }, { token: 'comment', foreground: '666666' }, { token: 'predefined.sql', foreground: isDarkMode ? 'D4D4D4' : '444444' }, ], colors: { 'editor.background': isDarkMode ? '#1f1f1f' : '#f0f0f0' }, } }