Assistant V2 (#30523)
* start * added panels * remove stuff * fixes and refinements * clean up * remove old assistant panel * resizable assistant kinda * use icon * Add missing package * remove canvas * add suggestions * updated empty state if no tables exist * fix table condition * Implement diffing if using assistant in sql editor * Reinstate old assistant in SQL editor if feature preview is off * pane size adjustment * assistant button corners * Add SQL snippet content to assistant if opening assistant in sql editor * Add the necessary checks for opt in and hipaa * revert adding snippet to assistant when opening assistant in sql editor * Add cmd i shortcut * Add admonitions for when disablePrompt is toggled on, and if no api key is set. Add footer note RE rate limitation * Bump ai package in packages * some fixes for backwards compability depending on feature preview toggled * Rename feature preview property for new assistant * Smol fix * Prevent SQL snippet from running until message is finished * only loading last message * fix z-index * save chat state to global state * add debug to failed ai queries * Add basic contextual invalidation * Add explain code action to SQL editor * Add link to abort ongoing queries from SqlSnippet * Update feature preview content * Fix * Fix * Fix * Te4st * Fix tests * ONly show ai button within a project * Fix PH tracking * Beef up a bit more event tracking * Rough fix to padding when assistant is open * A bit more telemetry stuff * Update prompts * fix rls editing via assistant * Update generate-v3.ts prompt to get auth schema too * Add policy satement to assistant when editing * Address all comments * fixc * Fix SqlSnippet not taking full width on larger viewports * Adjust max width --------- Co-authored-by: Saxon Fletcher <saxonafletcher@gmail.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { detectOS } from 'lib/helpers'
|
||||
|
||||
export const AssistantV2Preview = () => {
|
||||
const os = detectOS()
|
||||
const { ref } = useParams()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-foreground-light text-sm mb-4">
|
||||
We're changing the way our AI Assistant integrates with the dashboard by making it shared
|
||||
and accessible universally across the whole dashboard. This hopes to make using the
|
||||
Assistant as a supporting tool more seamless while you build your project.
|
||||
</p>
|
||||
<Image
|
||||
src={`${BASE_PATH}/img/previews/assistant-v2.png`}
|
||||
width={1860}
|
||||
height={970}
|
||||
alt="api-docs-side-panel-preview"
|
||||
className="rounded border"
|
||||
/>
|
||||
<p className="text-foreground-light text-sm mb-4">
|
||||
The Assistant will also be automatically provided with contexts depending on where you are
|
||||
in the dashboard to generate more relevant and higher quality outputs. You may also ask for
|
||||
insights on your own data apart from help with SQL and Postgres!
|
||||
</p>
|
||||
<p className="text-foreground-light text-sm mb-4">
|
||||
We believe that this should further lower the barrier of working with databases especially
|
||||
if you're not well acquainted with Postgres (yet!), so please do feel free to let us know
|
||||
what you think in the attached GitHub discussion above!
|
||||
</p>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm">Enabling this preview will:</p>
|
||||
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
|
||||
<li>
|
||||
<Markdown
|
||||
className="text-foreground-light"
|
||||
content={`Add a button in the top navigation bar where you can access the AI Assistant from anywhere in the dashboard`}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Markdown
|
||||
className="text-foreground-light"
|
||||
content={`Add a keyboard shortcut (${os === 'macos' ? 'Cmd' : 'Ctrl'} + I) that can also open the Assistant from anywhere in the dashboard`}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,13 +5,13 @@ import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { EMPTY_OBJ } from 'lib/void'
|
||||
import { APISidePanelPreview } from './APISidePanelPreview'
|
||||
import { CLSPreview } from './CLSPreview'
|
||||
import { FunctionsAssistantPreview } from './FunctionsAssistantPreview'
|
||||
import { AssistantV2Preview } from './AssistantV2Preview'
|
||||
|
||||
export const FEATURE_PREVIEWS = [
|
||||
{
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT,
|
||||
name: 'Database Functions Assistant',
|
||||
content: <FunctionsAssistantPreview />,
|
||||
key: LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2,
|
||||
name: 'Supabase AI Assistant V2',
|
||||
content: <AssistantV2Preview />,
|
||||
discussionsUrl: undefined,
|
||||
isNew: true,
|
||||
},
|
||||
@@ -86,7 +86,7 @@ export const useIsColumnLevelPrivilegesEnabled = () => {
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_CLS]
|
||||
}
|
||||
|
||||
export const useIsDatabaseFunctionsAssistantEnabled = () => {
|
||||
export const useIsAssistantV2Enabled = () => {
|
||||
const { flags } = useFeaturePreviewContext()
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_FUNCTIONS_ASSISTANT]
|
||||
return flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2]
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { TELEMETRY_EVENTS } from 'lib/constants/telemetry'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { Badge, Button, Modal, ScrollArea, cn } from 'ui'
|
||||
import { FEATURE_PREVIEWS, useFeaturePreviewContext } from './FeaturePreviewContext'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
|
||||
const FeaturePreviewModal = () => {
|
||||
const snap = useAppStateSnapshot()
|
||||
@@ -36,10 +38,17 @@ const FeaturePreviewModal = () => {
|
||||
const toggleFeature = () => {
|
||||
onUpdateFlag(selectedFeatureKey, !isSelectedFeatureEnabled)
|
||||
sendEvent({
|
||||
category: 'ui_feature_previews',
|
||||
action: isSelectedFeatureEnabled ? 'disabled' : 'enabled',
|
||||
action: TELEMETRY_EVENTS.FEATURE_PREVIEWS,
|
||||
label: selectedFeatureKey,
|
||||
value: isSelectedFeatureEnabled ? 'disabled' : 'enabled',
|
||||
})
|
||||
|
||||
if (
|
||||
selectedFeatureKey === LOCAL_STORAGE_KEYS.UI_PREVIEW_ASSISTANT_V2 &&
|
||||
isSelectedFeatureEnabled
|
||||
) {
|
||||
snap.setAiAssistantPanel({ open: false })
|
||||
}
|
||||
}
|
||||
|
||||
function handleCloseFeaturePreviewModal() {
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import Image from 'next/image'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { detectOS } from 'lib/helpers'
|
||||
|
||||
export const FunctionsAssistantPreview = () => {
|
||||
const os = detectOS()
|
||||
const { ref } = useParams()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-foreground-light text-sm mb-4">
|
||||
We're providing an additional alternative UX to creating database functions through the
|
||||
dashboard with the integration of our AI Assistant that you might have seen in the Auth
|
||||
Policies section.
|
||||
</p>
|
||||
<Image
|
||||
src={`${BASE_PATH}/img/previews/functions-assistant.png`}
|
||||
width={1860}
|
||||
height={970}
|
||||
alt="api-docs-side-panel-preview"
|
||||
className="rounded border"
|
||||
/>
|
||||
<p className="text-foreground-light text-sm mb-4">
|
||||
This preview also shares an improved Assistant interface where you may provide the Assistant
|
||||
with contexts in hopes to generate more relevant and higher quality outputs. Contexts that
|
||||
you may provide include specific schemas, and / or specific tables from your database.
|
||||
</p>
|
||||
<p className="text-foreground-light text-sm mb-4">
|
||||
We'd hope to use this as a consistent pattern throughout the dashboard eventually if this
|
||||
feature preview proves itself to benefit most of our users, so as usual please do feel free
|
||||
to let us know what you think if the attached GitHub discussion above!
|
||||
</p>
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<p className="text-sm">Enabling this preview will:</p>
|
||||
<ul className="list-disc pl-6 text-sm text-foreground-light space-y-1">
|
||||
<li>
|
||||
<Markdown
|
||||
className="text-foreground-light"
|
||||
content={`Add a button beside the "Create new function" button on the [Database Functions page](/project/${ref}/database/functions) that will open up a code editor paired with a contextualized AI assistant in a side panel.`}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Markdown
|
||||
className="text-foreground-light"
|
||||
content={`Add a keyboard shortcut (${os === 'macos' ? 'Cmd' : 'Ctrl'} + I) that will open the Assistant in an uncontextualized mode, along with a quick SQL Editor that you can run queries with from anywhere in the dashboard`}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -21,8 +21,9 @@ import {
|
||||
TooltipContent_Shadcn_,
|
||||
TooltipTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { generatePolicyCreateSQL } from './PolicyTableRow.utils'
|
||||
|
||||
interface PolicyRowProps {
|
||||
policy: PostgresPolicy
|
||||
@@ -38,7 +39,7 @@ const PolicyRow = ({
|
||||
onSelectDeletePolicy = noop,
|
||||
}: PolicyRowProps) => {
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
|
||||
|
||||
const { project } = useProjectContext()
|
||||
@@ -102,21 +103,21 @@ const PolicyRow = ({
|
||||
<DropdownMenuContent
|
||||
side="bottom"
|
||||
align="end"
|
||||
className={cn(enableAssistantV2 ? 'w-52' : 'w-40')}
|
||||
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-40')}
|
||||
>
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectEditPolicy(policy)}>
|
||||
<Edit size={14} />
|
||||
<p>Edit policy</p>
|
||||
</DropdownMenuItem>
|
||||
{enableAssistantV2 && (
|
||||
{isAssistantV2Enabled && (
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => {
|
||||
const sql = generatePolicyCreateSQL(policy)
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
editor: 'rls-policies',
|
||||
entity: policy,
|
||||
tables: [{ schema: policy.schema, name: policy.table }],
|
||||
sqlSnippets: [sql],
|
||||
initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { PostgresPolicy } from '@supabase/postgres-meta'
|
||||
|
||||
export const generatePolicyCreateSQL = (policy: PostgresPolicy) => {
|
||||
let expression = ''
|
||||
if (policy.definition !== null && policy.definition !== undefined) {
|
||||
expression += `USING (${policy.definition})${
|
||||
policy.check === null || policy.check === undefined ? ';' : ''
|
||||
}\n`
|
||||
}
|
||||
if (policy.check !== null && policy.check !== undefined) {
|
||||
expression += `WITH CHECK (${policy.check});\n`
|
||||
}
|
||||
|
||||
return `
|
||||
CREATE POLICY "${policy.name}"
|
||||
ON "${policy.schema}"."${policy.table}"
|
||||
AS ${policy.action}
|
||||
FOR ${policy.command}
|
||||
TO ${policy.roles.join(', ')}
|
||||
${expression}
|
||||
`.trim()
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Lock, Unlock } from 'lucide-react'
|
||||
import { useQueryState } from 'nuqs'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
@@ -37,7 +37,8 @@ const PolicyTableRowHeader = ({
|
||||
const { ref } = useParams()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
|
||||
const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled()
|
||||
const enableAssistantV2 = useIsAssistantV2Enabled()
|
||||
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
|
||||
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
|
||||
const isRealtimeSchema = table.schema === 'realtime'
|
||||
@@ -96,13 +97,13 @@ const PolicyTableRowHeader = ({
|
||||
)}
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
disabled={!canToggleRLS}
|
||||
disabled={!canToggleRLS || !canCreatePolicies}
|
||||
onClick={() => onSelectCreatePolicy()}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canToggleRLS
|
||||
? !canToggleRLS
|
||||
? !canToggleRLS || !canCreatePolicies
|
||||
? 'You need additional permissions to create RLS policies'
|
||||
: undefined
|
||||
: undefined,
|
||||
@@ -119,9 +120,7 @@ const PolicyTableRowHeader = ({
|
||||
if (enableAssistantV2) {
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
editor: 'rls-policies',
|
||||
entity: undefined,
|
||||
tables: [{ schema: table.schema, name: table.name }],
|
||||
initialInput: `Create a new policy for the ${table.schema} schema on the ${table.name} table that ...`,
|
||||
})
|
||||
} else {
|
||||
onSelectCreatePolicy()
|
||||
@@ -131,9 +130,10 @@ const PolicyTableRowHeader = ({
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: !canToggleRLS
|
||||
? 'You need additional permissions to create RLS policies'
|
||||
: 'Create with Supabase Assistant',
|
||||
text:
|
||||
!canToggleRLS || !canCreatePolicies
|
||||
? 'You need additional permissions to create RLS policies'
|
||||
: 'Create with Supabase Assistant',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -63,16 +63,16 @@ const EnumeratedTypes = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
<Input
|
||||
size="small"
|
||||
size="tiny"
|
||||
value={search}
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search for a type"
|
||||
icon={<Search size={14} />}
|
||||
|
||||
@@ -57,11 +57,11 @@ const Extensions = () => {
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
size="small"
|
||||
size="tiny"
|
||||
placeholder="Search for an extension"
|
||||
value={filterString}
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
icon={<Search size={14} />}
|
||||
/>
|
||||
<DocsButton href="https://supabase.com/docs/guides/database/extensions" />
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
|
||||
interface FunctionListProps {
|
||||
schema: string
|
||||
@@ -37,8 +37,8 @@ const FunctionList = ({
|
||||
}: FunctionListProps) => {
|
||||
const router = useRouter()
|
||||
const { project: selectedProject } = useProjectContext()
|
||||
const enableAssistantV2 = useIsDatabaseFunctionsAssistantEnabled()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
|
||||
const { data: functions } = useDatabaseFunctionsQuery({
|
||||
projectRef: selectedProject?.ref,
|
||||
@@ -113,7 +113,7 @@ const FunctionList = ({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="left"
|
||||
className={cn(enableAssistantV2 ? 'w-52' : 'w-40')}
|
||||
className={cn(isAssistantV2Enabled ? 'w-52' : 'w-40')}
|
||||
>
|
||||
{isApiDocumentAvailable && (
|
||||
<DropdownMenuItem
|
||||
@@ -128,11 +128,24 @@ const FunctionList = ({
|
||||
<Edit2 size={14} />
|
||||
<p>Edit function</p>
|
||||
</DropdownMenuItem>
|
||||
{enableAssistantV2 && (
|
||||
{isAssistantV2Enabled && (
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => {
|
||||
setAiAssistantPanel({ open: true, editor: 'functions', entity: x })
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
initialInput: 'Update this function to do...',
|
||||
suggestions: {
|
||||
title:
|
||||
'I can help you make a change to this function, here are a few example prompts to get you started:',
|
||||
prompts: [
|
||||
'Rename this function to ...',
|
||||
'Modify this function so that it ...',
|
||||
'Add a trigger for this function that calls it when ...',
|
||||
],
|
||||
},
|
||||
sqlSnippets: [x.complete_statement],
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Edit size={14} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Search } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
@@ -38,7 +38,7 @@ const FunctionsList = ({
|
||||
const { search } = useParams()
|
||||
const { project } = useProjectContext()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const enableFunctionsAssistant = useIsDatabaseFunctionsAssistantEnabled()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
|
||||
const filterString = search ?? ''
|
||||
@@ -104,10 +104,10 @@ const FunctionsList = ({
|
||||
) : (
|
||||
<div className="w-full space-y-4">
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={(schema) => {
|
||||
@@ -119,10 +119,10 @@ const FunctionsList = ({
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search for a function"
|
||||
size="small"
|
||||
size="tiny"
|
||||
icon={<Search size={14} />}
|
||||
value={filterString}
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -144,7 +144,7 @@ const FunctionsList = ({
|
||||
>
|
||||
Create a new function
|
||||
</ButtonTooltip>
|
||||
{enableFunctionsAssistant && (
|
||||
{isAssistantV2Enabled && (
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
disabled={!canCreateFunctions}
|
||||
@@ -155,8 +155,7 @@ const FunctionsList = ({
|
||||
onClick={() =>
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
editor: 'functions',
|
||||
entity: undefined,
|
||||
initialInput: `Create a new function for the schema ${selectedSchema} that does ...`,
|
||||
})
|
||||
}
|
||||
tooltip={{
|
||||
|
||||
@@ -49,10 +49,10 @@ const HooksList = ({ createHook = noop, editHook = noop, deleteHook = noop }: Ho
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
placeholder="Search for a webhook"
|
||||
size="small"
|
||||
size="tiny"
|
||||
icon={<Search size="14" />}
|
||||
value={filterString}
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
<div className="flex items-center gap-x-2">
|
||||
|
||||
@@ -109,17 +109,17 @@ const Indexes = () => {
|
||||
)}
|
||||
{isSuccessSchemas && (
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
)}
|
||||
<Input
|
||||
size="small"
|
||||
size="tiny"
|
||||
value={search}
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search for an index"
|
||||
icon={<Search size={14} />}
|
||||
|
||||
@@ -43,23 +43,15 @@ const PrivilegesHead = ({
|
||||
}: PrivilegesHeadProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<SchemaSelector
|
||||
className="bg-control rounded-md w-[200px] [&>button]:py-[5px]"
|
||||
className="bg-control rounded-md w-[180px] [&>button]:py-[5px]"
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={onChangeSchema}
|
||||
/>
|
||||
<div className="w-[200px]">
|
||||
<TablesSelect
|
||||
selectedTable={selectedTable}
|
||||
tables={tables}
|
||||
onChangeTable={onChangeTable}
|
||||
/>
|
||||
</div>
|
||||
<TablesSelect selectedTable={selectedTable} tables={tables} onChangeTable={onChangeTable} />
|
||||
<div className="h-[20px] w-px border-r border-scale-600"></div>
|
||||
<div className="w-[200px]">
|
||||
<RolesSelect selectedRole={selectedRole} roles={roles} onChangeRole={onChangeRole} />
|
||||
</div>
|
||||
<RolesSelect selectedRole={selectedRole} roles={roles} onChangeRole={onChangeRole} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -96,14 +88,14 @@ const RolesSelect = ({
|
||||
}) => {
|
||||
return (
|
||||
<Select_Shadcn_ value={selectedRole} onValueChange={onChangeRole}>
|
||||
<SelectTrigger_Shadcn_>
|
||||
<SelectTrigger_Shadcn_ size="tiny" className="w-40">
|
||||
<SelectValue_Shadcn_ placeholder="Select a role" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
<SelectGroup_Shadcn_>
|
||||
{roles.map((role) => (
|
||||
<SelectItem_Shadcn_ key={role} value={role}>
|
||||
<span className="text-foreground-light">role</span> {role}
|
||||
<SelectItem_Shadcn_ key={role} value={role} className="text-xs">
|
||||
<span className="text-foreground-light mr-1">role</span> {role}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectGroup_Shadcn_>
|
||||
@@ -125,7 +117,7 @@ const TablesSelect = ({
|
||||
}) => {
|
||||
return (
|
||||
<Select_Shadcn_ value={selectedTable?.name} onValueChange={onChangeTable}>
|
||||
<SelectTrigger_Shadcn_>
|
||||
<SelectTrigger_Shadcn_ size="tiny" className="w-44">
|
||||
<SelectValue_Shadcn_ placeholder="Select a table" />
|
||||
</SelectTrigger_Shadcn_>
|
||||
<SelectContent_Shadcn_>
|
||||
@@ -136,8 +128,8 @@ const TablesSelect = ({
|
||||
</div>
|
||||
) : null}
|
||||
{tables.map((table) => (
|
||||
<SelectItem_Shadcn_ key={table} value={table}>
|
||||
<span className="text-foreground-light">table</span> {table}
|
||||
<SelectItem_Shadcn_ key={table} value={table} className="text-xs">
|
||||
<span className="text-foreground-light mr-1">table</span> {table}
|
||||
</SelectItem_Shadcn_>
|
||||
))}
|
||||
</SelectGroup_Shadcn_>
|
||||
|
||||
@@ -81,7 +81,7 @@ const PublicationsList = ({ onSelectPublication = noop }: PublicationsListProps)
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Input
|
||||
size="small"
|
||||
size="tiny"
|
||||
icon={<Search size="14" />}
|
||||
placeholder={'Filter'}
|
||||
value={filterString}
|
||||
|
||||
@@ -63,7 +63,8 @@ const RolesList = () => {
|
||||
<div className="mb-4 flex items-center justify-between gap-2 flex-wrap">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Input
|
||||
size="small"
|
||||
size="tiny"
|
||||
className="w-52"
|
||||
placeholder="Search for a role"
|
||||
icon={<Search size={12} />}
|
||||
value={filterString}
|
||||
@@ -81,10 +82,10 @@ const RolesList = () => {
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center border border-strong rounded-full w-min h-[34px]">
|
||||
<div className="flex items-center border border-strong rounded-full w-min h-[26px]">
|
||||
<button
|
||||
className={[
|
||||
'text-xs w-[90px] h-full text-center rounded-l-full flex items-center justify-center transition',
|
||||
'text-xs w-[80px] h-full text-center rounded-l-full flex items-center justify-center transition',
|
||||
filterType === 'all'
|
||||
? 'bg-overlay-hover text-foreground'
|
||||
: 'hover:bg-surface-200 text-foreground-light',
|
||||
@@ -96,7 +97,7 @@ const RolesList = () => {
|
||||
<div className="h-full w-[1px] border-r border-strong"></div>
|
||||
<button
|
||||
className={[
|
||||
'text-xs w-[90px] h-full text-center rounded-r-full flex items-center justify-center transition',
|
||||
'text-xs w-[80px] h-full text-center rounded-r-full flex items-center justify-center transition',
|
||||
filterType === 'active'
|
||||
? 'bg-overlay-hover text-foreground'
|
||||
: 'hover:bg-surface-200 text-foreground-light',
|
||||
@@ -131,7 +132,9 @@ const RolesList = () => {
|
||||
? `${totalActiveConnections}/${maxConnectionLimit}`
|
||||
: `${totalActiveConnections}`
|
||||
}
|
||||
labelTopClass="text-xs"
|
||||
labelBottom="Active connections"
|
||||
labelBottomClass="text-xs"
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
|
||||
@@ -8,12 +8,12 @@ import 'reactflow/dist/style.css'
|
||||
import { useParams } from 'common'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { Button, Tooltip_Shadcn_, TooltipContent_Shadcn_, TooltipTrigger_Shadcn_ } from 'ui'
|
||||
import { SchemaGraphLegend } from './SchemaGraphLegend'
|
||||
import { getGraphDataFromTables, getLayoutedElementsViaDagre } from './Schemas.utils'
|
||||
import { TableNode } from './SchemaTableNode'
|
||||
@@ -117,22 +117,21 @@ export const SchemaGraph = () => {
|
||||
{isSuccessSchemas && (
|
||||
<>
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<Button type="default" onClick={resetLayout}>
|
||||
Auto layout
|
||||
</Button>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom">
|
||||
Automatically arrange the layout of all nodes
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
<ButtonTooltip
|
||||
type="default"
|
||||
onClick={resetLayout}
|
||||
tooltip={{
|
||||
content: { side: 'bottom', text: 'Automatically arrange the layout of all nodes' },
|
||||
}}
|
||||
>
|
||||
Auto layout
|
||||
</ButtonTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -203,11 +203,11 @@ const TableList = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center gap-x-2 flex-wrap">
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
@@ -215,8 +215,9 @@ const TableList = ({
|
||||
<Popover_Shadcn_>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type={visibleTypes.length !== 5 ? 'default' : 'dashed'}
|
||||
className="py-4 px-2"
|
||||
className="px-1"
|
||||
icon={<Filter />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
@@ -259,8 +260,8 @@ const TableList = ({
|
||||
</Popover_Shadcn_>
|
||||
|
||||
<Input
|
||||
size="small"
|
||||
className="w-64"
|
||||
size="tiny"
|
||||
className="w-52"
|
||||
placeholder="Search for a table"
|
||||
value={filterString}
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
@@ -294,7 +295,7 @@ const TableList = ({
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
|
||||
|
||||
{isSuccess && (
|
||||
<div className="my-4 w-full">
|
||||
<div className="w-full">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="icon" className="!px-0" />,
|
||||
|
||||
@@ -89,18 +89,18 @@ const TriggersList = ({
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={selectedSchema}
|
||||
onSelectSchema={setSelectedSchema}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Search for a trigger"
|
||||
size="small"
|
||||
size="tiny"
|
||||
icon={<Search size="14" />}
|
||||
value={filterString}
|
||||
className="w-64"
|
||||
className="w-52"
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
{!isLocked && (
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import { ReactMarkdown, ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
import { cn } from 'ui'
|
||||
|
||||
interface Props extends Omit<ReactMarkdownOptions, 'children' | 'node'> {
|
||||
@@ -14,11 +16,17 @@ const Markdown = ({ className, content = '', extLinks = false, ...props }: Props
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h3: ({ children }) => <h3 className="mb-1">{children}</h3>,
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} target={extLinks ? '_blank' : ''} rel={extLinks ? 'noreferrer' : ''}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
a: ({ href, children }) => {
|
||||
if (extLinks) {
|
||||
return (
|
||||
<a href={href} target="_blank" rel="noreferrer noopener">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return <Link href={href ?? '/'}>{children}</Link>
|
||||
}
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
className={cn('prose text-sm', className)}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { cn } from 'ui'
|
||||
|
||||
/**
|
||||
* Standardized padding and width layout for non-custom reports
|
||||
*/
|
||||
const ReportPadding = ({ children }: PropsWithChildren<{}>) => (
|
||||
<div className="flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 xl:px-24 2xl:px-32 w-full">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
const ReportPadding = ({ children }: PropsWithChildren<{}>) => {
|
||||
const { aiAssistantPanel } = useAppStateSnapshot()
|
||||
const { open } = aiAssistantPanel
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4 px-5 py-6 mx-auto 1xl:px-28 lg:px-16 2xl:px-32 w-full',
|
||||
open ? 'xl:px-6' : 'xl:px-24'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ReportPadding
|
||||
|
||||
@@ -94,13 +94,13 @@ export const AiAssistantPanel = ({
|
||||
// Use chat id because useChat doesn't have a reset function to clear all messages
|
||||
const {
|
||||
messages: chatMessages,
|
||||
append,
|
||||
isLoading,
|
||||
append,
|
||||
} = useChat({
|
||||
id: chatId,
|
||||
api: `${BASE_PATH}/api/ai/sql/generate-v2`,
|
||||
body: {
|
||||
existingSql: existingSql,
|
||||
existingSql,
|
||||
entityDefinitions: entityDefinitions,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -13,6 +13,8 @@ import { cn } from 'ui'
|
||||
import { untitledSnippetTitle } from './SQLEditor.constants'
|
||||
import type { IStandaloneCodeEditor } from './SQLEditor.types'
|
||||
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
|
||||
import { useIsAssistantV2Enabled } from '../App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
|
||||
export type MonacoEditorProps = {
|
||||
id: string
|
||||
@@ -39,6 +41,9 @@ const MonacoEditor = ({
|
||||
const project = useSelectedProject()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
|
||||
const [intellisenseEnabled] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
|
||||
true
|
||||
@@ -69,6 +74,25 @@ const MonacoEditor = ({
|
||||
},
|
||||
})
|
||||
|
||||
if (isAssistantV2Enabled) {
|
||||
editor.addAction({
|
||||
id: 'explain-code',
|
||||
label: 'Explain Code',
|
||||
contextMenuGroupId: 'operation',
|
||||
contextMenuOrder: 1,
|
||||
run: () => {
|
||||
const selectedValue = (editorRef?.current as any)
|
||||
.getModel()
|
||||
.getValueInRange((editorRef?.current as any)?.getSelection())
|
||||
setAiAssistantPanel({
|
||||
open: true,
|
||||
sqlSnippets: [selectedValue],
|
||||
initialInput: 'Can you explain this section to me in more detail?',
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
editor.onDidChangeCursorSelection(({ selection }) => {
|
||||
const noSelection =
|
||||
selection.startLineNumber === selection.endLineNumber &&
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
||||
import { useQueryAbortMutation } from 'data/sql/abort-query-mutation'
|
||||
import { useOngoingQueriesQuery } from 'data/sql/ongoing-queries-query'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { ResponseError } from 'types'
|
||||
@@ -33,6 +34,7 @@ interface OngoingQueriesPanel {
|
||||
}
|
||||
|
||||
export const OngoingQueriesPanel = ({ visible, onClose }: OngoingQueriesPanel) => {
|
||||
const [_, setParams] = useUrlState({ replace: true })
|
||||
const project = useSelectedProject()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
const [selectedId, setSelectedId] = useState<number>()
|
||||
@@ -66,9 +68,14 @@ export const OngoingQueriesPanel = ({ visible, onClose }: OngoingQueriesPanel) =
|
||||
},
|
||||
})
|
||||
|
||||
const closePanel = () => {
|
||||
setParams({ viewOngoingQueries: undefined })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={visible} onOpenChange={() => onClose()}>
|
||||
<Sheet open={visible} onOpenChange={() => closePanel()}>
|
||||
<SheetContent size="lg">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-x-2">
|
||||
|
||||
@@ -52,9 +52,10 @@ import {
|
||||
cn,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { useIsAssistantV2Enabled } from '../App/FeaturePreview/FeaturePreviewContext'
|
||||
import { subscriptionHasHipaaAddon } from '../Billing/Subscription/Subscription.utils'
|
||||
import AISchemaSuggestionPopover from './AISchemaSuggestionPopover'
|
||||
import { AiAssistantPanel } from './AiAssistantPanel'
|
||||
import AISchemaSuggestionPopover from './AISchemaSuggestionPopover'
|
||||
import { DiffActionBar } from './DiffActionBar'
|
||||
import {
|
||||
ROWS_PER_PAGE_OPTIONS,
|
||||
@@ -87,8 +88,8 @@ const DiffEditor = dynamic(
|
||||
)
|
||||
|
||||
const SQLEditor = () => {
|
||||
const { ref, id: urlId } = useParams()
|
||||
const router = useRouter()
|
||||
const { ref, id: urlId } = useParams()
|
||||
|
||||
// generate an id to be used for new snippets. The dependency on urlId is to avoid a bug which
|
||||
// shows up when clicking on the SQL Editor while being in the SQL editor on a random snippet.
|
||||
@@ -105,11 +106,13 @@ const SQLEditor = () => {
|
||||
const databaseSelectorState = useDatabaseSelectorStateSnapshot()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
const { open } = appSnap.aiAssistantPanel
|
||||
|
||||
const { mutate: formatQuery } = useFormatQueryMutation()
|
||||
const { mutateAsync: generateSqlTitle } = useSqlTitleGenerateMutation()
|
||||
const { mutateAsync: debugSql, isLoading: isDebugSqlLoading } = useSqlDebugMutation()
|
||||
|
||||
const [selectedMessage, setSelectedMessage] = useState<string>()
|
||||
const [debugSolution, setDebugSolution] = useState<string>()
|
||||
const [sourceSqlDiff, setSourceSqlDiff] = useState<ContentDiff>()
|
||||
const [pendingTitle, setPendingTitle] = useState<string>()
|
||||
@@ -118,6 +121,7 @@ const SQLEditor = () => {
|
||||
const editorRef = useRef<IStandaloneCodeEditor | null>(null)
|
||||
const monacoRef = useRef<Monaco | null>(null)
|
||||
const diffEditorRef = useRef<IStandaloneDiffEditor | null>(null)
|
||||
const aiPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
||||
const { data: databases, isSuccess: isSuccessReadReplicas } = useReadReplicasQuery({
|
||||
@@ -127,7 +131,9 @@ const SQLEditor = () => {
|
||||
// Customers on HIPAA plans should not have access to Supabase AI
|
||||
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
|
||||
|
||||
// [Joshen] This eventually needs to be in the app state
|
||||
const [isAiOpen, setIsAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true)
|
||||
const isAssistantOpen = (!isAssistantV2Enabled && isAiOpen) || (isAssistantV2Enabled && open)
|
||||
|
||||
const [showPotentialIssuesModal, setShowPotentialIssuesModal] = useState(false)
|
||||
const [queryHasDestructiveOperations, setQueryHasDestructiveOperations] = useState(false)
|
||||
@@ -376,7 +382,7 @@ const SQLEditor = () => {
|
||||
)
|
||||
|
||||
const updateEditorWithCheckForDiff = useCallback(
|
||||
({ id, diffType, sql }: { id: string; diffType: DiffType; sql: string }) => {
|
||||
({ diffType, sql }: { diffType: DiffType; sql: string }) => {
|
||||
const editorModel = editorRef.current?.getModel()
|
||||
if (!editorModel) return
|
||||
|
||||
@@ -392,7 +398,6 @@ const SQLEditor = () => {
|
||||
},
|
||||
])
|
||||
} else {
|
||||
setSelectedMessage(id)
|
||||
const currentSql = editorRef.current?.getValue()
|
||||
const diff = { original: currentSql || '', modified: sql }
|
||||
setSourceSqlDiff(diff)
|
||||
@@ -406,26 +411,33 @@ const SQLEditor = () => {
|
||||
try {
|
||||
const snippet = snapV2.snippets[id]
|
||||
const result = snapV2.results[id]?.[0]
|
||||
|
||||
const { solution, sql } = await debugSql({
|
||||
sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(),
|
||||
errorMessage: result.error.message,
|
||||
entityDefinitions,
|
||||
})
|
||||
|
||||
const formattedSql =
|
||||
sqlAiDisclaimerComment +
|
||||
'\n\n' +
|
||||
format(sql, {
|
||||
language: 'postgresql',
|
||||
keywordCase: 'lower',
|
||||
if (isAssistantV2Enabled) {
|
||||
appSnap.setAiAssistantPanel({
|
||||
open: true,
|
||||
sqlSnippets: [snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim()],
|
||||
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${result.error.message}`,
|
||||
})
|
||||
setDebugSolution(solution)
|
||||
setSourceSqlDiff({
|
||||
original: snippet.snippet.content.sql,
|
||||
modified: formattedSql,
|
||||
})
|
||||
setSelectedDiffType(DiffType.Modification)
|
||||
} else {
|
||||
const { solution, sql } = await debugSql({
|
||||
sql: snippet.snippet.content.sql.replace(sqlAiDisclaimerComment, '').trim(),
|
||||
errorMessage: result.error.message,
|
||||
entityDefinitions,
|
||||
})
|
||||
|
||||
const formattedSql =
|
||||
sqlAiDisclaimerComment +
|
||||
'\n\n' +
|
||||
format(sql, {
|
||||
language: 'postgresql',
|
||||
keywordCase: 'lower',
|
||||
})
|
||||
setDebugSolution(solution)
|
||||
setSourceSqlDiff({
|
||||
original: snippet.snippet.content.sql,
|
||||
modified: formattedSql,
|
||||
})
|
||||
setSelectedDiffType(DiffType.Modification)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// [Joshen] There's a tendency for the SQL debug to chuck a lengthy error message
|
||||
// that's not relevant for the user - so we prettify it here by avoiding to return the
|
||||
@@ -482,7 +494,6 @@ const SQLEditor = () => {
|
||||
label: debugSolution ? 'debug_snippet' : 'edit_snippet',
|
||||
})
|
||||
|
||||
setSelectedMessage(undefined)
|
||||
setSelectedDiffType(DiffType.Modification)
|
||||
setDebugSolution(undefined)
|
||||
setSourceSqlDiff(undefined)
|
||||
@@ -509,7 +520,6 @@ const SQLEditor = () => {
|
||||
label: debugSolution ? 'debug_snippet' : 'edit_snippet',
|
||||
})
|
||||
|
||||
setSelectedMessage(undefined)
|
||||
setDebugSolution(undefined)
|
||||
setSourceSqlDiff(undefined)
|
||||
setPendingTitle(undefined)
|
||||
@@ -588,6 +598,12 @@ const SQLEditor = () => {
|
||||
}
|
||||
}, [isSuccessReadReplicas, databases, ref])
|
||||
|
||||
useEffect(() => {
|
||||
if (snapV2.diffContent !== undefined) {
|
||||
updateEditorWithCheckForDiff(snapV2.diffContent)
|
||||
}
|
||||
}, [snapV2.diffContent])
|
||||
|
||||
const defaultSqlDiff = useMemo(() => {
|
||||
if (!sourceSqlDiff) {
|
||||
return { original: '', modified: '' }
|
||||
@@ -610,8 +626,6 @@ const SQLEditor = () => {
|
||||
}
|
||||
}, [selectedDiffType, sourceSqlDiff])
|
||||
|
||||
const aiPanelRef = useRef<ImperativePanelHandle>(null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmationModal
|
||||
@@ -679,7 +693,7 @@ const SQLEditor = () => {
|
||||
direction="vertical"
|
||||
autoSaveId={LOCAL_STORAGE_KEYS.SQL_EDITOR_SPLIT_SIZE}
|
||||
>
|
||||
{(isAiOpen || isDiffOpen) && !hasHipaaAddon && (
|
||||
{(isAssistantOpen || isDiffOpen) && !hasHipaaAddon && (
|
||||
<AISchemaSuggestionPopover
|
||||
onClickSettings={() => {
|
||||
appSnap.setShowAiSettingsModal(true)
|
||||
@@ -715,14 +729,36 @@ const SQLEditor = () => {
|
||||
)}
|
||||
<ResizablePanel maxSize={70}>
|
||||
<div className="flex-grow overflow-y-auto border-b h-full">
|
||||
{!isAiOpen && (
|
||||
{!isAssistantV2Enabled && !isAiOpen && (
|
||||
<motion.button
|
||||
layoutId="ask-ai-input-icon"
|
||||
transition={{ duration: 0.1 }}
|
||||
onClick={() => aiPanelRef.current?.expand()}
|
||||
className={cn(
|
||||
'group absolute z-10 rounded-lg right-[24px] top-4 transition-all duration-200 ease-out'
|
||||
)}
|
||||
className="group absolute z-10 rounded-lg right-[24px] top-4 transition-all duration-200 ease-out"
|
||||
onClick={() => {
|
||||
if (isAssistantV2Enabled) {
|
||||
const state = getSqlEditorV2StateSnapshot()
|
||||
const snippet = state.snippets[id]
|
||||
const editor = editorRef.current
|
||||
const selection = editor?.getSelection()
|
||||
const selectedValue = selection
|
||||
? editor?.getModel()?.getValueInRange(selection)
|
||||
: undefined
|
||||
const sql = snippet
|
||||
? (selectedValue || editorRef.current?.getValue()) ??
|
||||
snippet.snippet.content.sql
|
||||
: selectedValue || editorRef.current?.getValue()
|
||||
|
||||
appSnap.setAiAssistantPanel({
|
||||
open: true,
|
||||
sqlSnippets: sql ? [sql] : [],
|
||||
initialInput: sql
|
||||
? `Help me make a change to the attached sql snippet`
|
||||
: '',
|
||||
})
|
||||
} else {
|
||||
aiPanelRef.current?.expand()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AiIconAnimation loading={false} allowHoverEffect />
|
||||
</motion.button>
|
||||
@@ -860,25 +896,27 @@ const SQLEditor = () => {
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
|
||||
<ResizableHandle withHandle />
|
||||
|
||||
<ResizablePanel
|
||||
ref={aiPanelRef}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
minSize={31}
|
||||
maxSize={40}
|
||||
onCollapse={() => setIsAiOpen(false)}
|
||||
onExpand={() => setIsAiOpen(true)}
|
||||
>
|
||||
<AiAssistantPanel
|
||||
selectedMessage={selectedMessage}
|
||||
existingSql={editorRef.current?.getValue() || ''}
|
||||
includeSchemaMetadata={includeSchemaMetadata}
|
||||
onDiff={updateEditorWithCheckForDiff}
|
||||
onClose={() => aiPanelRef.current?.collapse()}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
{!isAssistantV2Enabled && (
|
||||
<>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel
|
||||
ref={aiPanelRef}
|
||||
collapsible
|
||||
collapsedSize={0}
|
||||
minSize={31}
|
||||
maxSize={40}
|
||||
onCollapse={() => setIsAiOpen(false)}
|
||||
onExpand={() => setIsAiOpen(true)}
|
||||
>
|
||||
<AiAssistantPanel
|
||||
existingSql={editorRef.current?.getValue() || ''}
|
||||
includeSchemaMetadata={includeSchemaMetadata}
|
||||
onDiff={updateEditorWithCheckForDiff}
|
||||
onClose={() => aiPanelRef.current?.collapse()}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</ResizablePanelGroup>
|
||||
</>
|
||||
)
|
||||
|
||||
53
apps/studio/components/interfaces/SQLEditor/hooks.ts
Normal file
53
apps/studio/components/interfaces/SQLEditor/hooks.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useRouter } from 'next/router'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
||||
import { createSqlSnippetSkeletonV2 } from './SQLEditor.utils'
|
||||
|
||||
export const useNewQuery = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { profile } = useProfile()
|
||||
const { project } = useProjectContext()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
|
||||
const canCreateSQLSnippet = useCheckPermissions(PermissionAction.CREATE, 'user_content', {
|
||||
resource: { type: 'sql', owner_id: profile?.id },
|
||||
subject: { id: profile?.id },
|
||||
})
|
||||
|
||||
const newQuery = async (sql: string, name: string) => {
|
||||
if (!ref) return console.error('Project ref is required')
|
||||
if (!project) return console.error('Project is required')
|
||||
if (!profile) return console.error('Profile is required')
|
||||
|
||||
if (!canCreateSQLSnippet) {
|
||||
return toast('Your queries will not be saved as you do not have sufficient permissions')
|
||||
}
|
||||
|
||||
try {
|
||||
const snippet = createSqlSnippetSkeletonV2({
|
||||
id: uuidv4(),
|
||||
name,
|
||||
sql,
|
||||
owner_id: profile?.id,
|
||||
project_id: project?.id,
|
||||
})
|
||||
snapV2.addSnippet({ projectRef: ref, snippet })
|
||||
snapV2.addNeedsSaving(snippet.id)
|
||||
router.push(`/project/${ref}/sql/${snippet.id}`)
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to create new query: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { newQuery }
|
||||
}
|
||||
|
||||
export default useNewQuery
|
||||
@@ -94,8 +94,8 @@ const EncryptionKeysManagement = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
className="w-64 input-clear"
|
||||
size="small"
|
||||
className="w-52 input-clear"
|
||||
size="tiny"
|
||||
placeholder="Search by name or ID"
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
@@ -116,7 +116,7 @@ const EncryptionKeysManagement = () => {
|
||||
}
|
||||
/>
|
||||
<div className="w-44">
|
||||
<Listbox size="small" value={selectedSort} onChange={setSelectedSort}>
|
||||
<Listbox size="tiny" value={selectedSort} onChange={setSelectedSort}>
|
||||
<Listbox.Option
|
||||
id="created"
|
||||
className="max-w-[180px]"
|
||||
|
||||
@@ -60,8 +60,8 @@ const SecretsManagement = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
className="w-64"
|
||||
size="small"
|
||||
className="w-52"
|
||||
size="tiny"
|
||||
placeholder="Search by name or key ID"
|
||||
value={searchValue}
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
@@ -82,7 +82,7 @@ const SecretsManagement = () => {
|
||||
}
|
||||
/>
|
||||
<div className="w-44">
|
||||
<Listbox size="small" value={selectedSort} onChange={setSelectedSort}>
|
||||
<Listbox size="tiny" value={selectedSort} onChange={setSelectedSort}>
|
||||
<Listbox.Option
|
||||
id="updated_at"
|
||||
className="max-w-[180px]"
|
||||
|
||||
@@ -13,6 +13,7 @@ import OrganizationDropdown from './OrganizationDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import UserSettingsDropdown from './UserSettingsDropdown'
|
||||
import AssistantButton from './AssistantButton'
|
||||
|
||||
// [Joshen] Just FYI this is only for Nav V2 which is still going through design iteration
|
||||
// Component is not currently in use
|
||||
@@ -53,6 +54,7 @@ const AppHeader = () => {
|
||||
<FeedbackDropdown />
|
||||
<NotificationsPopoverV2 />
|
||||
<HelpPopover />
|
||||
<AssistantButton />
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
20
apps/studio/components/layouts/AppLayout/AssistantButton.tsx
Normal file
20
apps/studio/components/layouts/AppLayout/AssistantButton.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { AiIconAnimation, Button } from 'ui'
|
||||
|
||||
const AssistantButton = () => {
|
||||
const { setAiAssistantPanel, aiAssistantPanel } = useAppStateSnapshot()
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
id="assistant-trigger"
|
||||
className="w-6 h-6"
|
||||
onClick={() => {
|
||||
setAiAssistantPanel({ open: !aiAssistantPanel.open })
|
||||
}}
|
||||
icon={<AiIconAnimation allowHoverEffect className="w-4 h-4 text-foreground-light" />}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssistantButton
|
||||
@@ -17,11 +17,14 @@ import BreadcrumbsView from './BreadcrumbsView'
|
||||
import { FeedbackDropdown } from './FeedbackDropdown'
|
||||
import HelpPopover from './HelpPopover'
|
||||
import NotificationsPopoverV2 from './NotificationsPopoverV2/NotificationsPopover'
|
||||
import AssistantButton from 'components/layouts/AppLayout/AssistantButton'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
|
||||
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
const isBranchingEnabled = selectedProject?.is_branch_enabled === true
|
||||
|
||||
const { data: subscription } = useOrgSubscriptionQuery({
|
||||
@@ -117,6 +120,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
<FeedbackDropdown />
|
||||
<NotificationsPopoverV2 />
|
||||
<HelpPopover />
|
||||
{isAssistantV2Enabled && !!projectRef && <AssistantButton />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useState } from 'react'
|
||||
|
||||
import {
|
||||
useIsAPIDocsSidePanelEnabled,
|
||||
useIsDatabaseFunctionsAssistantEnabled,
|
||||
useIsAssistantV2Enabled,
|
||||
} from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { useProjectLintsQuery } from 'data/lint/lint-query'
|
||||
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
|
||||
@@ -73,7 +73,7 @@ const NavigationBar = () => {
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
|
||||
const isFunctionsAssistantEnabled = useIsDatabaseFunctionsAssistantEnabled()
|
||||
const isFunctionsAssistantEnabled = useIsAssistantV2Enabled()
|
||||
const [userDropdownOpen, setUserDropdownOpenState] = useState(false)
|
||||
|
||||
const [allowNavPanelToExpand] = useLocalStorageQuery(
|
||||
@@ -181,7 +181,9 @@ const NavigationBar = () => {
|
||||
}
|
||||
label="Assistant"
|
||||
shortcut="I"
|
||||
onClick={() => snap.setAiAssistantPanel({ open: true, editor: null })}
|
||||
onClick={() => {
|
||||
snap.setAiAssistantPanel({ open: !snap.aiAssistantPanel.open })
|
||||
}}
|
||||
/>
|
||||
</HoverCardContent_Shadcn_>
|
||||
</HoverCard_Shadcn_>
|
||||
|
||||
@@ -31,6 +31,9 @@ import RestoreFailedState from './RestoreFailedState'
|
||||
import RestoringState from './RestoringState'
|
||||
import { UpgradingState } from './UpgradingState'
|
||||
import { ResizingState } from './ResizingState'
|
||||
import { AiAssistantPanel } from 'components/ui/AIAssistantPanel/AIAssistantPanel'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
|
||||
// [Joshen] This is temporary while we unblock users from managing their project
|
||||
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
|
||||
@@ -85,10 +88,14 @@ const ProjectLayout = ({
|
||||
const { ref: projectRef } = useParams()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const selectedProject = useSelectedProject()
|
||||
const projectName = selectedProject?.name
|
||||
const organizationName = selectedOrganization?.name
|
||||
const { aiAssistantPanel, setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const { open } = aiAssistantPanel
|
||||
|
||||
const navLayoutV2 = useFlag('navigationLayoutV2')
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
|
||||
const projectName = selectedProject?.name
|
||||
const organizationName = selectedOrganization?.name
|
||||
|
||||
const isPaused = selectedProject?.status === PROJECT_STATUS.INACTIVE
|
||||
const showProductMenu = selectedProject
|
||||
@@ -101,6 +108,15 @@ const ProjectLayout = ({
|
||||
router.pathname === '/project/[ref]' || router.pathname.includes('/project/[ref]/settings')
|
||||
const showPausedState = isPaused && !ignorePausedState
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.code === 'KeyI') setAiAssistantPanel({ open: !open })
|
||||
}
|
||||
if (isAssistantV2Enabled) window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isAssistantV2Enabled, open])
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<ProjectContextProvider projectRef={projectRef}>
|
||||
@@ -149,20 +165,39 @@ const ProjectLayout = ({
|
||||
/>
|
||||
<ResizablePanel id="panel-right" className="h-full flex flex-col">
|
||||
{!navLayoutV2 && !hideHeader && IS_PLATFORM && <LayoutHeader />}
|
||||
<main className="h-full flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
|
||||
<ResourceExhaustionWarningBanner />
|
||||
{children}
|
||||
</ContentWrapper>
|
||||
<ResizablePanelGroup
|
||||
className="h-full w-full overflow-x-hidden flex-1"
|
||||
direction="horizontal"
|
||||
autoSaveId="project-layout-content"
|
||||
>
|
||||
<ResizablePanel id="panel-content" className=" w-full min-w-[600px]">
|
||||
<main className="h-full flex flex-col flex-1 w-full overflow-x-hidden">
|
||||
{showPausedState ? (
|
||||
<div className="mx-auto my-16 w-full h-full max-w-7xl flex items-center">
|
||||
<div className="w-full">
|
||||
<ProjectPausedState product={product} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ContentWrapper isLoading={isLoading} isBlocking={isBlocking}>
|
||||
<ResourceExhaustionWarningBanner />
|
||||
{children}
|
||||
</ContentWrapper>
|
||||
)}
|
||||
</main>
|
||||
</ResizablePanel>
|
||||
{aiAssistantPanel.open && (
|
||||
<>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel
|
||||
id="panel-assistant"
|
||||
className="min-w-[400px] max-w-[500px] bg 2xl:max-w-[600px] xl:relative xl:top-0 absolute right-0 top-[48px] bottom-0"
|
||||
>
|
||||
<AiAssistantPanel />
|
||||
</ResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</ResizablePanelGroup>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { OngoingQueriesPanel } from 'components/interfaces/SQLEditor/OngoingQueriesPanel'
|
||||
import { withAuth } from 'hooks/misc/withAuth'
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
import ProjectLayout from '../ProjectLayout/ProjectLayout'
|
||||
import { SQLEditorMenu } from './SQLEditorMenu'
|
||||
import { useParams } from 'common'
|
||||
|
||||
export interface SQLEditorLayoutProps {
|
||||
title: string
|
||||
@@ -10,7 +12,9 @@ export interface SQLEditorLayoutProps {
|
||||
}
|
||||
|
||||
const SQLEditorLayout = ({ title, children }: SQLEditorLayoutProps) => {
|
||||
const { viewOngoingQueries } = useParams()
|
||||
const [showOngoingQueries, setShowOngoingQueries] = useState(false)
|
||||
|
||||
const productMenu = useMemo(
|
||||
() => (
|
||||
<SQLEditorMenu
|
||||
@@ -21,6 +25,10 @@ const SQLEditorLayout = ({ title, children }: SQLEditorLayoutProps) => {
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (viewOngoingQueries === 'true') setShowOngoingQueries(true)
|
||||
}, [viewOngoingQueries])
|
||||
|
||||
return (
|
||||
<ProjectLayout
|
||||
title={title || 'SQL'}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { cn } from 'ui'
|
||||
|
||||
export const MAX_WIDTH_CLASSES = 'mx-auto w-full max-w-[1200px]'
|
||||
@@ -35,11 +36,19 @@ const ScaffoldContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { bottomPadding?: boolean }
|
||||
>(({ className, bottomPadding, ...props }, ref) => {
|
||||
const { aiAssistantPanel } = useAppStateSnapshot()
|
||||
const { open } = aiAssistantPanel
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={cn(MAX_WIDTH_CLASSES, PADDING_CLASSES, bottomPadding && 'pb-16', className)}
|
||||
className={cn(
|
||||
MAX_WIDTH_CLASSES,
|
||||
PADDING_CLASSES,
|
||||
bottomPadding && 'pb-16',
|
||||
open ? 'xl:px-6' : '',
|
||||
className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,17 @@
|
||||
import { authKeys } from 'data/auth/keys'
|
||||
import { databaseExtensionsKeys } from 'data/database-extensions/keys'
|
||||
import { databasePoliciesKeys } from 'data/database-policies/keys'
|
||||
import { databaseTriggerKeys } from 'data/database-triggers/keys'
|
||||
import { databaseKeys } from 'data/database/keys'
|
||||
import { enumeratedTypesKeys } from 'data/enumerated-types/keys'
|
||||
import { tableKeys } from 'data/tables/keys'
|
||||
import { CommonDatabaseEntity } from 'state/app-state'
|
||||
import { SupportedAssistantEntities, SupportedAssistantQuickPromptTypes } from './AIAssistant.types'
|
||||
import { SupportedAssistantEntities } from './AIAssistant.types'
|
||||
|
||||
const PLACEHOLDER_PREFIX = `-- Press tab to use this code
|
||||
\n \n`
|
||||
|
||||
const PLACEHOLDER_LIMIT = `Just three examples will do.`
|
||||
|
||||
export const generateTitle = (
|
||||
editor?: SupportedAssistantEntities | null,
|
||||
entity?: CommonDatabaseEntity
|
||||
) => {
|
||||
switch (editor) {
|
||||
case 'functions':
|
||||
if (entity === undefined) return 'Create a new function'
|
||||
else return `Edit function: ${entity.name}`
|
||||
case 'rls-policies':
|
||||
if (entity === undefined) return 'Create a new RLS policy'
|
||||
else return `Edit RLS policy: ${entity.name}`
|
||||
default:
|
||||
return 'SQL Scratch Pad'
|
||||
}
|
||||
}
|
||||
|
||||
export const generateCTA = (editor?: SupportedAssistantEntities | null) => {
|
||||
switch (editor) {
|
||||
case 'functions':
|
||||
return 'Save function'
|
||||
case 'rls-policies':
|
||||
return 'Save policy'
|
||||
default:
|
||||
return 'Run query'
|
||||
}
|
||||
}
|
||||
|
||||
// [Joshen] Not used but keeping this for now in case we do an inline editor
|
||||
export const generatePlaceholder = (
|
||||
editor?: SupportedAssistantEntities | null,
|
||||
entity?: CommonDatabaseEntity,
|
||||
@@ -117,68 +96,91 @@ COMMIT;
|
||||
}
|
||||
}
|
||||
|
||||
export const retrieveDocsUrl = (editor?: SupportedAssistantEntities | null) => {
|
||||
switch (editor) {
|
||||
case 'functions':
|
||||
return 'https://supabase.com/docs/guides/database/functions'
|
||||
case 'rls-policies':
|
||||
return 'https://supabase.com/docs/guides/database/postgres/row-level-security'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// [Joshen] This is just very basic validation, but possible can extend perhaps
|
||||
export const validateQuery = (editor: SupportedAssistantEntities | null, query: string) => {
|
||||
// [Joshen] This is just very basic identification, but possible can extend perhaps
|
||||
export const identifyQueryType = (query: string) => {
|
||||
const formattedQuery = query.toLowerCase().replaceAll('\n', ' ')
|
||||
|
||||
switch (editor) {
|
||||
case 'functions':
|
||||
return (
|
||||
formattedQuery.includes('create function') ||
|
||||
formattedQuery.includes('create or replace function')
|
||||
)
|
||||
case 'rls-policies':
|
||||
return formattedQuery.includes('create policy') || formattedQuery.includes('alter policy')
|
||||
default:
|
||||
return true
|
||||
if (
|
||||
formattedQuery.includes('create function') ||
|
||||
formattedQuery.includes('create or replace function')
|
||||
) {
|
||||
return 'functions'
|
||||
} else if (formattedQuery.includes('create policy') || formattedQuery.includes('alter policy')) {
|
||||
return 'rls-policies'
|
||||
}
|
||||
}
|
||||
|
||||
export const generatePrompt = ({
|
||||
type,
|
||||
context,
|
||||
schemas,
|
||||
tables,
|
||||
}: {
|
||||
type: SupportedAssistantQuickPromptTypes
|
||||
context: SupportedAssistantEntities
|
||||
schemas: string[]
|
||||
tables: readonly { schema: string; name: string }[]
|
||||
}) => {
|
||||
if (type === 'examples') {
|
||||
return `What are some common examples of user-defined database ${context}? ${PLACEHOLDER_LIMIT}`
|
||||
} else if (type === 'ask') {
|
||||
return `Could you explain to me what are used-defined database ${context}?`
|
||||
} else if (type === 'suggest') {
|
||||
const output =
|
||||
context === 'functions'
|
||||
? 'user-defined database functions'
|
||||
: context === 'rls-policies'
|
||||
? 'RLS policies'
|
||||
: ''
|
||||
export const isReadOnlySelect = (query: string): boolean => {
|
||||
const normalizedQuery = query.trim().toLowerCase()
|
||||
|
||||
const suffix =
|
||||
context === 'functions' ? 'Let me know for which tables each function will be useful' : ''
|
||||
// Check if it starts with SELECT
|
||||
if (!normalizedQuery.startsWith('select')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const basePrompt = `Suggest some ${output} that might be useful`
|
||||
// List of keywords that indicate write operations or function calls
|
||||
const disallowedPatterns = [
|
||||
// Write operations
|
||||
'insert',
|
||||
'update',
|
||||
'delete',
|
||||
'alter',
|
||||
'drop',
|
||||
'create',
|
||||
'truncate',
|
||||
'replace',
|
||||
'with',
|
||||
|
||||
if (tables.length > 0 && schemas.length > 0) {
|
||||
return `${basePrompt} for the following tables within this database: ${tables.map((x) => `${x.schema}.${x.name}`)}. ${PLACEHOLDER_LIMIT} ${suffix}`.trim()
|
||||
} else if (schemas.length > 0) {
|
||||
return `${basePrompt} for the tables in the following schemas within this database: ${schemas.join(', ')}. ${suffix}`.trim()
|
||||
// Function patterns
|
||||
'function',
|
||||
'procedure',
|
||||
]
|
||||
|
||||
const allowedPatterns = ['created', 'inserted', 'updated', 'deleted']
|
||||
|
||||
// Check if query contains any disallowed patterns, but allow if part of allowedPatterns
|
||||
return !disallowedPatterns.some((pattern) => {
|
||||
// Check if the found disallowed pattern is actually part of an allowed pattern
|
||||
const isPartOfAllowedPattern = allowedPatterns.some(
|
||||
(allowed) => normalizedQuery.includes(allowed) && allowed.includes(pattern)
|
||||
)
|
||||
|
||||
if (isPartOfAllowedPattern) {
|
||||
return false
|
||||
}
|
||||
|
||||
return basePrompt
|
||||
}
|
||||
return normalizedQuery.includes(pattern)
|
||||
})
|
||||
}
|
||||
|
||||
const getContextKey = (pathname: string) => {
|
||||
const [_, __, ___, ...rest] = pathname.split('/')
|
||||
const key = rest.join('/')
|
||||
return key
|
||||
}
|
||||
|
||||
export const getContextualInvalidationKeys = ({
|
||||
ref,
|
||||
pathname,
|
||||
schema = 'public',
|
||||
}: {
|
||||
ref: string
|
||||
pathname: string
|
||||
schema?: string
|
||||
}) => {
|
||||
const key = getContextKey(pathname)
|
||||
|
||||
return (
|
||||
(
|
||||
{
|
||||
'auth/users': [authKeys.usersInfinite(ref)],
|
||||
'auth/policies': [databasePoliciesKeys.list(ref)],
|
||||
'database/functions': [databaseKeys.databaseFunctions(ref)],
|
||||
'database/tables': [tableKeys.list(ref, schema, true), tableKeys.list(ref, schema, false)],
|
||||
'database/triggers': [databaseTriggerKeys.list(ref)],
|
||||
'database/types': [enumeratedTypesKeys.list(ref)],
|
||||
'database/extensions': [databaseExtensionsKeys.list(ref)],
|
||||
'database/indexes': [databaseKeys.indexes(ref, schema)],
|
||||
} as const
|
||||
)[key] ?? []
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,464 +1,43 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { uniqBy } from 'lodash'
|
||||
import { Command, CornerDownLeft, Loader2, X } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useChat } from 'ai/react'
|
||||
import { useParams } from 'common'
|
||||
import { useIsDatabaseFunctionsAssistantEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { generateThreadMessage } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel/AIPolicyEditorPanel.utils'
|
||||
import { MessageWithDebug } from 'components/interfaces/SQLEditor/AiAssistantPanel'
|
||||
import { sqlAiDisclaimerComment } from 'components/interfaces/SQLEditor/SQLEditor.constants'
|
||||
import { DiffType, IStandaloneCodeEditor } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { suffixWithLimit } from 'components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results'
|
||||
import { useSqlDebugMutation } from 'data/ai/sql-debug-mutation'
|
||||
import { databasePoliciesKeys } from 'data/database-policies/keys'
|
||||
import { useEntityDefinitionQuery } from 'data/database/entity-definition-query'
|
||||
import { databaseKeys } from 'data/database/keys'
|
||||
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { usePrevious } from 'hooks/deprecated'
|
||||
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { SqlEditor } from 'icons'
|
||||
import {
|
||||
BASE_PATH,
|
||||
LOCAL_STORAGE_KEYS,
|
||||
TELEMETRY_ACTIONS,
|
||||
TELEMETRY_CATEGORIES,
|
||||
TELEMETRY_LABELS,
|
||||
} from 'lib/constants'
|
||||
import { detectOS, uuidv4 } from 'lib/helpers'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
Tooltip_Shadcn_,
|
||||
TooltipContent_Shadcn_,
|
||||
TooltipTrigger_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import CodeEditor from '../CodeEditor/CodeEditor'
|
||||
import { cn } from 'ui'
|
||||
import { AIAssistant } from './AIAssistant'
|
||||
import { generateCTA, generatePlaceholder, generateTitle, validateQuery } from './AIAssistant.utils'
|
||||
import { ASSISTANT_SUPPORT_ENTITIES } from './AiAssistant.constants'
|
||||
import type { Message as MessageType } from 'ai/react'
|
||||
|
||||
export const AiAssistantPanel = () => {
|
||||
const os = detectOS()
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const project = useSelectedProject()
|
||||
const queryClient = useQueryClient()
|
||||
const { aiAssistantPanel, setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const isEnabled = useIsDatabaseFunctionsAssistantEnabled()
|
||||
const { aiAssistantPanel, resetAiAssistantPanel } = useAppStateSnapshot()
|
||||
const [initialMessages, setInitialMessages] = useState<MessageType[] | undefined>(undefined)
|
||||
|
||||
const { open, editor, content, entity } = aiAssistantPanel
|
||||
const previousEditor = usePrevious(editor)
|
||||
const previousEntity = usePrevious(entity)
|
||||
|
||||
const [isAcknowledged, setIsAcknowledged] = useLocalStorage(
|
||||
LOCAL_STORAGE_KEYS.SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED,
|
||||
false
|
||||
)
|
||||
useEffect(() => {
|
||||
// set initial state of local messages to the global state if it exists
|
||||
if (aiAssistantPanel.messages) {
|
||||
const messagesCopy = aiAssistantPanel.messages.map((msg) => ({
|
||||
content: msg.content,
|
||||
createdAt: msg.createdAt,
|
||||
role: msg.role,
|
||||
id: msg.id,
|
||||
}))
|
||||
setInitialMessages(messagesCopy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const { open } = aiAssistantPanel
|
||||
const [chatId, setChatId] = useState(() => uuidv4())
|
||||
const [error, setError] = useState<QueryResponseError>()
|
||||
const [results, setResults] = useState<undefined | any[]>(undefined)
|
||||
const [showResults, setShowResults] = useState(false)
|
||||
const [showWarning, setShowWarning] = useState<boolean>(false)
|
||||
const [debugThread, setDebugThread] = useState<MessageWithDebug[]>([])
|
||||
|
||||
const { data: existingDefinition } = useEntityDefinitionQuery({
|
||||
id: entity?.id,
|
||||
type: editor,
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
// [Joshen] JFYI I'm opting to just have the assistant always show and not toggle-able
|
||||
// I don't really see any negatives of keeping it open (or benefits from hiding) tbh
|
||||
// const [showAssistant, setShowAssistant] = useState(true)
|
||||
const title = generateTitle(editor, entity)
|
||||
const placeholder = generatePlaceholder(editor, entity, existingDefinition)
|
||||
const ctaText = generateCTA(editor)
|
||||
const editorRef = useRef<IStandaloneCodeEditor | undefined>()
|
||||
const editorModel = editorRef.current?.getModel()
|
||||
|
||||
const numResults = (results ?? []).length
|
||||
const [errorHeader, ...errorContent] =
|
||||
(error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? []
|
||||
const entityContext = ASSISTANT_SUPPORT_ENTITIES.find((x) => x.id === editor)
|
||||
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
const sendTelemetryEvent = (action: string) => {
|
||||
sendEvent({
|
||||
action,
|
||||
category: TELEMETRY_CATEGORIES.AI_ASSISTANT,
|
||||
label: TELEMETRY_LABELS.QUICK_SQL_EDITOR,
|
||||
})
|
||||
const handleReset = () => {
|
||||
// reset local and global state
|
||||
setChatId(uuidv4())
|
||||
setInitialMessages(undefined)
|
||||
resetAiAssistantPanel()
|
||||
}
|
||||
|
||||
const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({
|
||||
onSuccess: async (res) => {
|
||||
// [Joshen] If in a specific editor context mode, assume that intent was to create/update
|
||||
// a database entity - so close it once success. Otherwise it's in Quick SQL mode and we
|
||||
// show the results.
|
||||
if (editor !== null) {
|
||||
switch (editor) {
|
||||
case 'functions':
|
||||
await queryClient.invalidateQueries(databaseKeys.databaseFunctions(ref))
|
||||
break
|
||||
case 'rls-policies':
|
||||
await queryClient.invalidateQueries(databasePoliciesKeys.list(ref))
|
||||
break
|
||||
}
|
||||
|
||||
toast.success(
|
||||
`Successfully ${entity === undefined ? 'created' : 'updated'} ${entityContext?.name}!`
|
||||
)
|
||||
setAiAssistantPanel({ open: false })
|
||||
} else {
|
||||
setShowResults(true)
|
||||
setResults(res.result)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error)
|
||||
setResults([])
|
||||
},
|
||||
})
|
||||
|
||||
const { append } = useChat({
|
||||
id: chatId,
|
||||
api: `${BASE_PATH}/api/ai/sql/generate-v2`,
|
||||
// [Joshen] Don't need entity definitions if calling here cause support action
|
||||
// via AI here is just to explain code segment, no need for entity definitions context
|
||||
body: {},
|
||||
})
|
||||
|
||||
const { mutateAsync: debugSql } = useSqlDebugMutation({ onError: () => {} })
|
||||
|
||||
const onExecuteSql = (skipValidation = false) => {
|
||||
setError(undefined)
|
||||
setShowWarning(false)
|
||||
|
||||
const query = editorRef.current?.getValue() ?? ''
|
||||
if (query.length === 0) return
|
||||
|
||||
if (editor !== undefined && !skipValidation) {
|
||||
// [Joshen] Some basic validation logic
|
||||
const validated = validateQuery(editor, query)
|
||||
if (!validated) {
|
||||
return setShowWarning(true)
|
||||
}
|
||||
}
|
||||
|
||||
executeSql({
|
||||
sql: suffixWithLimit(query, 100),
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
handleError: (error) => {
|
||||
throw error
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onFixWithAssistant = async () => {
|
||||
if (!error) return
|
||||
|
||||
const messageId = uuidv4()
|
||||
const query = editorRef.current?.getValue() ?? ''
|
||||
|
||||
const assistantMessageBefore = generateThreadMessage({
|
||||
id: messageId,
|
||||
content: 'Thinking...',
|
||||
isDebug: true,
|
||||
})
|
||||
|
||||
setDebugThread([...debugThread, assistantMessageBefore])
|
||||
sendTelemetryEvent(TELEMETRY_ACTIONS.FIX_WITH_ASSISTANT)
|
||||
|
||||
try {
|
||||
const { solution, sql } = await debugSql({
|
||||
sql: query,
|
||||
errorMessage: error.message,
|
||||
})
|
||||
|
||||
const assistantMessageAfter = generateThreadMessage({
|
||||
id: messageId,
|
||||
content: `${solution}\n\`\`\`sql\n${sql}\n\`\`\``,
|
||||
isDebug: true,
|
||||
})
|
||||
const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id)
|
||||
setDebugThread(cleanedMessages)
|
||||
} catch (error) {
|
||||
const assistantMessageAfter = generateThreadMessage({
|
||||
id: messageId,
|
||||
content: `Hmm, Sorry but I'm unable to find a solution for the error that you're facing`,
|
||||
isDebug: true,
|
||||
})
|
||||
const cleanedMessages = uniqBy([...debugThread, assistantMessageAfter], (m) => m.id)
|
||||
setDebugThread(cleanedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
const onExplainSql = (value: string) => {
|
||||
append({
|
||||
role: 'user',
|
||||
createdAt: new Date(),
|
||||
content: `
|
||||
Can you explain this section to me in more detail?\n
|
||||
${value}
|
||||
`.trim(),
|
||||
})
|
||||
sendTelemetryEvent(TELEMETRY_ACTIONS.EXPLAIN_CODE)
|
||||
}
|
||||
|
||||
const updateEditorWithCheckForDiff = useCallback(
|
||||
({ id, diffType, sql }: { id: string; diffType: DiffType; sql: string }) => {
|
||||
if (!editorModel) return
|
||||
|
||||
const existingValue = editorRef.current?.getValue() ?? ''
|
||||
if (existingValue.length === 0) {
|
||||
// if the editor is empty, just copy over the code
|
||||
editorRef.current?.executeEdits('apply-ai-message', [
|
||||
{
|
||||
text: `${sqlAiDisclaimerComment}\n\n${sql}`,
|
||||
range: editorModel.getFullModelRange(),
|
||||
},
|
||||
])
|
||||
} else {
|
||||
const currentSql = editorRef.current?.getValue()
|
||||
editorRef.current?.executeEdits('apply-ai-message', [
|
||||
{
|
||||
text: `${currentSql}\n\n${sqlAiDisclaimerComment}\n\n${sql}`,
|
||||
range: editorModel.getFullModelRange(),
|
||||
},
|
||||
])
|
||||
}
|
||||
},
|
||||
[editorModel]
|
||||
)
|
||||
|
||||
const onTogglePanel = () => {
|
||||
if (open) {
|
||||
// [Joshen] Save the code content when closing - should allow users to continue from
|
||||
// where they left off IF they are opening the assistant again in the same "editor" context
|
||||
const existingValue = editorRef.current?.getValue() ?? ''
|
||||
setAiAssistantPanel({ open: false, content: existingValue })
|
||||
} else {
|
||||
setAiAssistantPanel({ open: true, editor: null, entity: undefined })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
// [Joshen] Only reset the assistant if the editor changed or if the provided entity has changed
|
||||
if (previousEditor !== editor || previousEntity !== entity) {
|
||||
setChatId(uuidv4())
|
||||
setError(undefined)
|
||||
setShowWarning(false)
|
||||
setDebugThread([])
|
||||
setResults(undefined)
|
||||
setShowResults(false)
|
||||
} else {
|
||||
// [Joshen] Repopulate the code editor with the content from where the user left off
|
||||
// setTimeout is just to give time for the editorRef to get associated
|
||||
setTimeout(() => {
|
||||
const editorModel = editorRef.current?.getModel()
|
||||
if (content && editorModel) {
|
||||
editorRef.current?.executeEdits('apply-ai-message', [
|
||||
{ text: content, range: editorModel.getFullModelRange() },
|
||||
])
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open])
|
||||
|
||||
// [Joshen] Just to test the concept of a universal assistant of sorts
|
||||
useEffect(() => {
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.code === 'KeyI') onTogglePanel()
|
||||
}
|
||||
if (project !== undefined && isEnabled) window.addEventListener('keydown', handler)
|
||||
return () => window.removeEventListener('keydown', handler)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [project, isEnabled, open])
|
||||
|
||||
// [Joshen] Whenever the deps change recalculate the height of the editor
|
||||
useEffect(() => {
|
||||
editorRef.current?.layout({ width: 0, height: 0 })
|
||||
window.requestAnimationFrame(() => editorRef.current?.layout())
|
||||
}, [error, showWarning, isExecuting, numResults, showResults])
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={() => onTogglePanel()}>
|
||||
<SheetContent showClose={true} className={cn('flex gap-0 w-[1200px]')}>
|
||||
{/* Assistant */}
|
||||
<AIAssistant
|
||||
id={chatId}
|
||||
className="border-r w-1/2"
|
||||
debugThread={debugThread}
|
||||
onDiff={updateEditorWithCheckForDiff}
|
||||
onResetConversation={() => setChatId(uuidv4())}
|
||||
/>
|
||||
|
||||
{/* Editor */}
|
||||
<div className={cn('flex flex-col grow w-1/2')}>
|
||||
<SheetHeader className="flex items-center justify-between py-3 pr-12">
|
||||
{title}
|
||||
<Tooltip_Shadcn_>
|
||||
<TooltipTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="outline"
|
||||
icon={<SqlEditor />}
|
||||
className="h-[24px] w-[24px] px-1"
|
||||
onClick={() => {
|
||||
const content = editorRef.current?.getValue() ?? ''
|
||||
router.push(`/project/${ref}/sql/new?content=${encodeURIComponent(content)}`)
|
||||
setAiAssistantPanel({ open: false })
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom">Open in SQL Editor</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
</SheetHeader>
|
||||
{editor === null && !isAcknowledged && (
|
||||
<Admonition
|
||||
showIcon={false}
|
||||
type="default"
|
||||
title="This is a quick access SQL editor to run queries on your database"
|
||||
className="relative m-0 rounded-none border-x-0 border-t-0 [&>div]:m-0"
|
||||
>
|
||||
<span>
|
||||
Queries written here will not be saved and results are limited up to only 100 rows
|
||||
</span>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<X />}
|
||||
className="px-1.5 absolute top-2 right-2"
|
||||
onClick={() => setIsAcknowledged(true)}
|
||||
/>
|
||||
</Admonition>
|
||||
)}
|
||||
<div className="flex flex-col h-full justify-between">
|
||||
<div className="relative flex-grow block">
|
||||
<CodeEditor
|
||||
id="assistant-code-editor"
|
||||
language="pgsql"
|
||||
editorRef={editorRef}
|
||||
placeholder={placeholder}
|
||||
actions={{
|
||||
runQuery: { enabled: true, callback: () => onExecuteSql() },
|
||||
explainCode: { enabled: true, callback: onExplainSql },
|
||||
closeAssistant: { enabled: true, callback: () => onTogglePanel() },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
{error !== undefined && (
|
||||
<Admonition
|
||||
type="warning"
|
||||
className="m-0 rounded-none border-x-0 border-b-0 [&>div>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
|
||||
title={errorHeader || 'Error running SQL query'}
|
||||
description={
|
||||
<>
|
||||
<div>
|
||||
{errorContent.length > 0 ? (
|
||||
errorContent.map((errorText: string, i: number) => (
|
||||
<pre key={`err-${i}`} className="font-mono text-xs whitespace-pre-wrap">
|
||||
{errorText}
|
||||
</pre>
|
||||
))
|
||||
) : (
|
||||
<p className="font-mono text-xs">{error.error}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button type="default" className="w-min" onClick={() => onFixWithAssistant()}>
|
||||
Fix with Assistant
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{showWarning && (
|
||||
<Admonition
|
||||
type="default"
|
||||
className="m-0 rounded-none border-x-0 border-b-0 [&>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
|
||||
title={`Your query doesn't seem to be relevant to ${
|
||||
entityContext?.id === 'rls-policies'
|
||||
? entityContext?.label
|
||||
: `Database ${entityContext?.label}`
|
||||
}`}
|
||||
description={
|
||||
<>
|
||||
<p>Are you sure you want to run this query?</p>
|
||||
<Button type="default" className="w-min" onClick={() => onExecuteSql(true)}>
|
||||
Confirm run query
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{results !== undefined && results.length > 0 && (
|
||||
<>
|
||||
<div className={cn(showResults ? 'h-72 border-t' : 'h-0')}>
|
||||
<Results rows={results} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t bg-surface-100 py-2 pl-2 pr-5">
|
||||
<p className="text-xs text-foreground-light">
|
||||
{results.length} rows
|
||||
{results.length >= 100 && ` (Limited to only 100 rows)`}
|
||||
</p>
|
||||
<Button size="tiny" type="default" onClick={() => setShowResults(!showResults)}>
|
||||
{showResults ? 'Hide' : 'Show'} results
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{results !== undefined && results.length === 0 && (
|
||||
<div className="flex items-center justify-between border-t bg-surface-100 h-[43px] pl-2 pr-5">
|
||||
<p className="text-xs text-foreground-light">Success. No rows returned.</p>
|
||||
</div>
|
||||
)}
|
||||
<SheetFooter className="bg-surface-100 flex items-center !justify-end px-5 py-4 w-full border-t">
|
||||
<Button type="default" disabled={isExecuting} onClick={() => onTogglePanel()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
loading={isExecuting}
|
||||
onClick={() => onExecuteSql()}
|
||||
iconRight={
|
||||
isExecuting ? (
|
||||
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
|
||||
) : (
|
||||
<div className="flex items-center space-x-1">
|
||||
{os === 'macos' ? (
|
||||
<Command size={10} strokeWidth={1.5} />
|
||||
) : (
|
||||
<p className="text-xs text-foreground-light">CTRL</p>
|
||||
)}
|
||||
<CornerDownLeft size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{ctaText}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
return !open ? null : (
|
||||
<AIAssistant
|
||||
initialMessages={initialMessages}
|
||||
id={chatId}
|
||||
className={cn('w-full h-full')}
|
||||
onResetConversation={handleReset}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
245
apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx
Normal file
245
apps/studio/components/ui/AIAssistantPanel/AIOnboarding.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { motion } from 'framer-motion'
|
||||
import { Button } from 'ui'
|
||||
import { WandSparkles, FileText, MessageCircle, MessageCircleMore } from 'lucide-react'
|
||||
|
||||
interface AIOnboardingProps {
|
||||
setMessages: (messages: any[]) => void
|
||||
onSendMessage: (message: string) => void
|
||||
}
|
||||
|
||||
export default function AIOnboarding({ setMessages, onSendMessage }: AIOnboardingProps) {
|
||||
const sendMessageToAssistant = (message: string) => {
|
||||
onSendMessage(message)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-8 py-content flex flex-col flex-1 h-full">
|
||||
<div className="shrink-0 h-64 mb-5 w-auto overflow-hidden -mx-8 -mt-8 relative">
|
||||
<motion.div
|
||||
initial={{ height: '800%', bottom: 0 }}
|
||||
animate={{ height: '100%', bottom: 0, transition: { duration: 8 } }}
|
||||
className="absolute bottom-0 left-0 right-0 z-20 bg-gradient-to-b from-transparent to-background"
|
||||
/>
|
||||
<div className="h-full w-full relative">
|
||||
<motion.div
|
||||
initial={{ x: 350, rotate: -45 }}
|
||||
animate={{
|
||||
x: 400,
|
||||
rotate: -45,
|
||||
transition: { duration: 5, ease: 'easeInOut' },
|
||||
}}
|
||||
className="absolute -inset-full bg-gradient-to-b from-black/[0.05] dark:from-white/[0.08] to-transparent "
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ x: 380, rotate: -45 }}
|
||||
animate={{
|
||||
x: 500,
|
||||
rotate: -45,
|
||||
transition: { duration: 5, ease: 'easeInOut' },
|
||||
}}
|
||||
className="absolute -inset-full bg-gradient-to-b from-black/[0.05] dark:from-white/[0.08] to-transparent "
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ x: 410, rotate: -45 }}
|
||||
animate={{
|
||||
x: 600,
|
||||
rotate: -45,
|
||||
transition: { duration: 5, ease: 'easeInOut' },
|
||||
}}
|
||||
className="absolute -inset-full bg-gradient-to-b from-black/[0.05] dark:from-white/[0.08] to-transparent "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: 0 }}
|
||||
>
|
||||
<p className="text-base mb-8">How can I help you today?</p>
|
||||
</motion.div>
|
||||
<motion.div className="space-y-6 pb-16">
|
||||
<motion.section
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: 0 }}
|
||||
>
|
||||
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">Tables</h3>
|
||||
<div className="-mx-3">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<WandSparkles strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant(
|
||||
"Create a table of countries and a table of cities. The cities table should have a country column that's a foreign key to the countries table."
|
||||
)
|
||||
}
|
||||
>
|
||||
Create a new table
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FileText strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant(
|
||||
'Give me a list of new users from the auth.users table who signed up in the past week'
|
||||
)
|
||||
}
|
||||
>
|
||||
Query your data
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant(
|
||||
'Give me a chart showing the number of new sign ups in the auth.users table per day over the last week'
|
||||
)
|
||||
}
|
||||
>
|
||||
Chart your data
|
||||
</Button>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">RLS Policies</h3>
|
||||
<div className="-mx-3">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<WandSparkles strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant(
|
||||
'Suggest some database RLS policies I can add to my public schema'
|
||||
)
|
||||
}
|
||||
>
|
||||
Suggest RLS policies
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FileText strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant('Generate some examples of database RLS policies')
|
||||
}
|
||||
>
|
||||
Examples of RLS policies
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() => sendMessageToAssistant(`What are database RLS policies`)}
|
||||
>
|
||||
What are RLS policies?
|
||||
</Button>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">Functions</h3>
|
||||
<div className="-mx-3">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<WandSparkles strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant(
|
||||
'Suggest some database functions I can add to my public schema'
|
||||
)
|
||||
}
|
||||
>
|
||||
Suggest database functions
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FileText strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() => sendMessageToAssistant('Generate some examples of database functions')}
|
||||
>
|
||||
Examples of database functions
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() => sendMessageToAssistant('What are database functions')}
|
||||
>
|
||||
What are database functions?
|
||||
</Button>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<h3 className="text-foreground-light font-mono text-sm uppercase mb-3">Triggers</h3>
|
||||
<div className="-mx-3">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<WandSparkles strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() =>
|
||||
sendMessageToAssistant(
|
||||
'Suggest some database triggers I can add to my public schema'
|
||||
)
|
||||
}
|
||||
>
|
||||
Suggest database Triggers
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<FileText strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() => sendMessageToAssistant(`Generate some examples of database triggers`)}
|
||||
>
|
||||
Examples of database triggers
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MessageCircleMore strokeWidth={1.5} size={16} />}
|
||||
type="text"
|
||||
className="w-full justify-start py-1 h-auto"
|
||||
onClick={() => sendMessageToAssistant('What are database triggers')}
|
||||
>
|
||||
What are database triggers?
|
||||
</Button>
|
||||
</div>
|
||||
</motion.section>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Button, CodeBlock, CodeBlockProps, cn } from 'ui'
|
||||
|
||||
interface CollapsibleCodeBlockProps extends CodeBlockProps {
|
||||
onRemove?: () => void
|
||||
}
|
||||
|
||||
const CollapsibleCodeBlock = ({ onRemove, ...props }: CollapsibleCodeBlockProps) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const codeString = (props.value || props.children) as string
|
||||
const firstLine = isExpanded
|
||||
? codeString
|
||||
: codeString?.substring(0, codeString.indexOf('\n')) || codeString
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1 p-1 bg-surface-100 border border-default w-full overflow-hidden',
|
||||
'rounded-md'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-6 h-6"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
icon={isExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
/>
|
||||
<div className="flex-1 shrink-1 overflow-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||
<CodeBlock
|
||||
{...props}
|
||||
value={firstLine}
|
||||
hideCopy
|
||||
className={cn(
|
||||
'block !bg-transparent max-h-32 max-w-full !py-0 !px-0 !border-t-0 prose dark:prose-dark border-0 text-foreground !rounded-none w-full text-wrap whitespace-pre-wrap',
|
||||
// change the look of the code block. The flex hack is so that the code is wrapping since
|
||||
// every word is a separate span
|
||||
'[&>code]:m-0 [&>code>span]:flex border-t-0 [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground text-wrap whitespace-pre-wrap',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{onRemove && (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="shrink-0 w-6 h-6"
|
||||
onClick={onRemove}
|
||||
icon={<X size={14} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollapsibleCodeBlock
|
||||
@@ -1,146 +1,94 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { noop } from 'lodash'
|
||||
import Image from 'next/image'
|
||||
import { PropsWithChildren, memo, useMemo } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { AiIconAnimation, Badge, cn, markdownComponents, WarningIcon } from 'ui'
|
||||
|
||||
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { useProfile } from 'lib/profile'
|
||||
import { MessagePre } from './MessagePre'
|
||||
import { cn, markdownComponents, WarningIcon } from 'ui'
|
||||
import CollapsibleCodeBlock from './CollapsibleCodeBlock'
|
||||
import { SqlSnippet } from './SqlSnippet'
|
||||
|
||||
interface MessageProps {
|
||||
name?: string
|
||||
id: string
|
||||
role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'
|
||||
content?: string
|
||||
createdAt?: number
|
||||
isDebug?: boolean
|
||||
isSelected?: boolean
|
||||
isLoading: boolean
|
||||
readOnly?: boolean
|
||||
action?: React.ReactNode
|
||||
context?: { entity: string; schemas: string[]; tables: string[] }
|
||||
onDiff?: (type: DiffType, s: string) => void
|
||||
variant?: 'default' | 'warning'
|
||||
}
|
||||
|
||||
export const Message = memo(function Message({
|
||||
name,
|
||||
export const Message = function Message({
|
||||
id,
|
||||
role,
|
||||
content,
|
||||
createdAt,
|
||||
isDebug,
|
||||
isSelected = false,
|
||||
context,
|
||||
isLoading,
|
||||
readOnly,
|
||||
children,
|
||||
action = null,
|
||||
variant = 'default',
|
||||
onDiff = noop,
|
||||
}: PropsWithChildren<MessageProps>) {
|
||||
const { profile } = useProfile()
|
||||
const isUser = role === 'user'
|
||||
|
||||
const icon = useMemo(() => {
|
||||
return role === 'assistant' ? (
|
||||
<AiIconAnimation
|
||||
loading={content === 'Thinking...'}
|
||||
className="[&>div>div]:border-black dark:[&>div>div]:border-white"
|
||||
/>
|
||||
) : (
|
||||
<div className="relative border shadow-lg w-8 h-8 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={`https://github.com/${profile?.username}.png` || ''}
|
||||
width={30}
|
||||
height={30}
|
||||
alt="avatar"
|
||||
className="relative"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}, [content, profile?.username, role])
|
||||
|
||||
const formattedContext =
|
||||
context !== undefined
|
||||
? Object.entries(context)
|
||||
.filter(([_, value]) => value.length > 0)
|
||||
.map(([key, value]) => {
|
||||
return `${key.charAt(0).toUpperCase() + key.slice(1)}: ${Array.isArray(value) ? value.join(', ') : value}`
|
||||
})
|
||||
.join(' • ')
|
||||
: undefined
|
||||
|
||||
if (!content) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col py-4 gap-4 px-5 text-foreground-light text-sm border-t first:border-0',
|
||||
variant === 'warning' && 'bg-warning-200',
|
||||
isUser && 'bg-default'
|
||||
)}
|
||||
<motion.div
|
||||
layout="position"
|
||||
initial={{ y: 5, opacity: 0, scale: 0.99 }}
|
||||
animate={{ y: 0, opacity: 1, scale: 1 }}
|
||||
className="w-full flex flex-col"
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex gap-x-3 items-center">
|
||||
{variant === 'warning' ? <WarningIcon className="w-6 h-6" /> : icon}
|
||||
|
||||
<div className="flex flex-col -gap-y-1">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<span className="text-sm">{!isUser ? 'Assistant' : name ? name : 'You'}</span>
|
||||
{createdAt && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs text-foreground-muted',
|
||||
variant === 'warning' && 'text-warning-500'
|
||||
)}
|
||||
>
|
||||
{dayjs(createdAt).fromNow()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{role === 'user' && context !== undefined && (
|
||||
<span className="text-xs text-foreground-lighter">{formattedContext}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDebug && <Badge variant="warning">Debug request</Badge>}
|
||||
</div>{' '}
|
||||
{action}
|
||||
</div>
|
||||
|
||||
<ReactMarkdown
|
||||
className="gap-x-2.5 gap-y-4 flex flex-col [&>*>code]:text-xs [&>*>*>code]:text-xs"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
...markdownComponents,
|
||||
pre: (props: any) => {
|
||||
return (
|
||||
<MessagePre
|
||||
onDiff={onDiff}
|
||||
className={cn(
|
||||
'transition [&>div>pre]:max-w-full',
|
||||
isSelected ? '[&>div>pre]:!border-stronger [&>div>pre]:!bg-surface-200' : ''
|
||||
)}
|
||||
>
|
||||
{props.children[0].props.children}
|
||||
</MessagePre>
|
||||
)
|
||||
},
|
||||
ol: (props: any) => {
|
||||
return <ol className="flex flex-col gap-y-4">{props.children}</ol>
|
||||
},
|
||||
li: (props: any) => {
|
||||
return <li className="[&>pre]:mt-2">{props.children}</li>
|
||||
},
|
||||
h3: (props: any) => {
|
||||
return <h3 className="underline">{props.children}</h3>
|
||||
},
|
||||
code: (props: any) => {
|
||||
return <code className={cn('text-xs', props.className)}>{props.children}</code>
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'text-foreground-light text-sm mb max-w-full mb-6',
|
||||
variant === 'warning' && 'bg-warning-200',
|
||||
isUser ? 'px-5 py-3 rounded-lg bg-background-muted w-fit self-end' : 'mb-6'
|
||||
)}
|
||||
>
|
||||
{variant === 'warning' && <WarningIcon className="w-6 h-6" />}
|
||||
|
||||
{action}
|
||||
|
||||
<ReactMarkdown
|
||||
className="gap-x-2.5 gap-y-4 flex flex-col [&>*>code]:text-xs [&>*>*>code]:text-xs"
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
...markdownComponents,
|
||||
pre: (props: any) => {
|
||||
return readOnly ? (
|
||||
<div className="mb-1 -mt-2">
|
||||
<CollapsibleCodeBlock
|
||||
value={props.children[0].props.children[0]}
|
||||
language="sql"
|
||||
hideLineNumbers
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SqlSnippet
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
sql={props.children[0].props.children}
|
||||
/>
|
||||
)
|
||||
},
|
||||
ol: (props: any) => {
|
||||
return <ol className="flex flex-col gap-y-4">{props.children}</ol>
|
||||
},
|
||||
li: (props: any) => {
|
||||
return <li className="[&>pre]:mt-2">{props.children}</li>
|
||||
},
|
||||
h3: (props: any) => {
|
||||
return <h3 className="underline">{props.children}</h3>
|
||||
},
|
||||
code: (props: any) => {
|
||||
return <code className={cn('text-xs', props.className)}>{props.children}</code>
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InsertCode, ReplaceCode } from 'icons'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { InsertCode } from 'icons'
|
||||
import { Check, Copy, Edit } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { format } from 'sql-formatter'
|
||||
import {
|
||||
@@ -74,11 +74,11 @@ export const MessagePre = ({ onDiff, children, className }: MessagePreProps) =>
|
||||
})
|
||||
}}
|
||||
>
|
||||
<InsertCode className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
|
||||
<Edit className="h-4 w-4 text-foreground-light" strokeWidth={1.5} />
|
||||
</Button>
|
||||
</TooltipTrigger_Shadcn_>
|
||||
<TooltipContent_Shadcn_ side="bottom" className="font-sans">
|
||||
Insert code
|
||||
Edit
|
||||
</TooltipContent_Shadcn_>
|
||||
</Tooltip_Shadcn_>
|
||||
|
||||
|
||||
424
apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx
Normal file
424
apps/studio/components/ui/AIAssistantPanel/SqlSnippet.tsx
Normal file
@@ -0,0 +1,424 @@
|
||||
import { Code, DatabaseIcon, Edit, Play } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import useNewQuery from 'components/interfaces/SQLEditor/hooks'
|
||||
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
import { suffixWithLimit } from 'components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results'
|
||||
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
|
||||
import { useSelectedProject } from 'hooks/misc/useSelectedProject'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2'
|
||||
import {
|
||||
Button,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
CodeBlock,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
import { ButtonTooltip } from '../ButtonTooltip'
|
||||
import {
|
||||
getContextualInvalidationKeys,
|
||||
identifyQueryType,
|
||||
isReadOnlySelect,
|
||||
} from './AIAssistant.utils'
|
||||
import { useParams } from 'common'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
|
||||
import { TELEMETRY_EVENTS, TELEMETRY_VALUES } from 'lib/constants/telemetry'
|
||||
|
||||
interface SqlSnippetWrapperProps {
|
||||
sql: string
|
||||
isLoading?: boolean
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
const SqlSnippetWrapper = ({
|
||||
sql,
|
||||
isLoading = false,
|
||||
readOnly = false,
|
||||
}: SqlSnippetWrapperProps) => {
|
||||
const formatted = (sql || [''])[0]
|
||||
const propsMatch = formatted.match(/--\s*props:\s*(\{[^}]+\})/)
|
||||
const props = propsMatch ? JSON.parse(propsMatch[1]) : {}
|
||||
const title = props.title || 'SQL Query'
|
||||
const updatedFormatted = formatted?.replace(/--\s*props:\s*\{[^}]+\}/, '').trim()
|
||||
|
||||
return (
|
||||
<div className="-mx-8 my-3 mt-2 border-b overflow-hidden">
|
||||
<SqlCard
|
||||
sql={updatedFormatted}
|
||||
isChart={props.isChart === 'true'}
|
||||
xAxis={props.xAxis}
|
||||
yAxis={props.yAxis}
|
||||
title={title}
|
||||
readOnly={readOnly}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ParsedSqlProps {
|
||||
sql: string
|
||||
title: string
|
||||
isLoading?: boolean
|
||||
readOnly?: boolean
|
||||
isChart: boolean
|
||||
xAxis: string
|
||||
yAxis: string
|
||||
}
|
||||
|
||||
export const SqlCard = ({
|
||||
sql,
|
||||
isChart,
|
||||
xAxis,
|
||||
yAxis,
|
||||
title,
|
||||
readOnly = false,
|
||||
isLoading = false,
|
||||
}: ParsedSqlProps) => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const queryClient = useQueryClient()
|
||||
const project = useSelectedProject()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
const { newQuery } = useNewQuery()
|
||||
|
||||
const isInSQLEditor = router.pathname.includes('/sql')
|
||||
const isInNewSnippet = router.pathname.endsWith('/sql')
|
||||
|
||||
const [showCode, setShowCode] = useState(readOnly || !isReadOnlySelect(sql))
|
||||
const [showResults, setShowResults] = useState(false)
|
||||
const [results, setResults] = useState<any[]>()
|
||||
const [error, setError] = useState<QueryResponseError>()
|
||||
const [showWarning, setShowWarning] = useState(false)
|
||||
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
|
||||
const { mutate: executeSql, isLoading: isExecuting } = useExecuteSqlMutation({
|
||||
onSuccess: async (res) => {
|
||||
// [Joshen] Only do contextual invalidation within a project context
|
||||
if (!!ref) {
|
||||
const invalidationKeys = getContextualInvalidationKeys({ ref, pathname: router.pathname })
|
||||
await Promise.all(invalidationKeys?.map((x) => queryClient.invalidateQueries(x)))
|
||||
}
|
||||
|
||||
setShowResults(true)
|
||||
setResults(res.result)
|
||||
setShowWarning(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
setError(error)
|
||||
setResults([])
|
||||
setShowWarning(false)
|
||||
},
|
||||
})
|
||||
|
||||
const handleExecute = useCallback(() => {
|
||||
if (!project?.ref || !sql || readOnly) return
|
||||
|
||||
if (!isReadOnlySelect(sql)) {
|
||||
setShowCode(true)
|
||||
setShowWarning(true)
|
||||
return
|
||||
}
|
||||
|
||||
setError(undefined)
|
||||
executeSql({
|
||||
sql: suffixWithLimit(sql, 100),
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
handleError: (error: any) => {
|
||||
setError(error)
|
||||
setResults([])
|
||||
return { result: [] }
|
||||
},
|
||||
})
|
||||
}, [project?.ref, project?.connectionString, sql, executeSql, readOnly])
|
||||
|
||||
const handleEditInSQLEditor = () => {
|
||||
if (isInSQLEditor) {
|
||||
snapV2.setDiffContent(sql, DiffType.Addition)
|
||||
} else {
|
||||
newQuery(sql, title)
|
||||
}
|
||||
}
|
||||
|
||||
const [errorHeader, ...errorContent] =
|
||||
(error?.formattedError?.split('\n') ?? [])?.filter((x: string) => x.length > 0) ?? []
|
||||
|
||||
useEffect(() => {
|
||||
if (isReadOnlySelect(sql) && !results && !readOnly && !isLoading) {
|
||||
handleExecute()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sql, readOnly, isLoading])
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<div className={cn('flex items-center gap-2 border-t', showWarning ? '' : 'px-5 py-2')}>
|
||||
{showWarning ? (
|
||||
<Admonition type="warning" className="mb-0 rounded-none border-0">
|
||||
<p>This query contains write operations. Are you sure you want to execute it?</p>
|
||||
<div className="flex justify-stretch mt-2 gap-2">
|
||||
<Button
|
||||
type="outline"
|
||||
size="tiny"
|
||||
className="w-full flex-1"
|
||||
onClick={() => setShowWarning(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="outline"
|
||||
size="tiny"
|
||||
className="w-full flex-1"
|
||||
onClick={() => {
|
||||
setShowWarning(false)
|
||||
executeSql({
|
||||
sql: suffixWithLimit(sql, 100),
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
handleError: (error: any) => {
|
||||
setError(error)
|
||||
setResults([])
|
||||
return { result: [] }
|
||||
},
|
||||
})
|
||||
|
||||
sendEvent({
|
||||
action: TELEMETRY_EVENTS.AI_ASSISTANT_V2,
|
||||
value: TELEMETRY_VALUES.RAN_SQL_SUGGESTION,
|
||||
label: 'mutation',
|
||||
category: identifyQueryType(sql) ?? 'unknown',
|
||||
})
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
</div>
|
||||
</Admonition>
|
||||
) : (
|
||||
<>
|
||||
<DatabaseIcon size={16} strokeWidth={1.5} />
|
||||
<h3 className="text-sm font-medium flex-1">{title}</h3>
|
||||
|
||||
{!readOnly && (
|
||||
<div className="flex">
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Code size={14} />}
|
||||
onClick={() => setShowCode(!showCode)}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Show query' } }}
|
||||
/>
|
||||
|
||||
{!isInSQLEditor || isInNewSnippet ? (
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Edit size={14} />}
|
||||
onClick={() => {
|
||||
handleEditInSQLEditor()
|
||||
sendEvent({
|
||||
action: TELEMETRY_EVENTS.AI_ASSISTANT_V2,
|
||||
value: TELEMETRY_VALUES.EDIT_IN_SQL_EDITOR,
|
||||
})
|
||||
}}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Edit in SQL Editor' } }}
|
||||
/>
|
||||
) : (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Edit size={14} />}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Edit in SQL Editor' } }}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-36">
|
||||
<DropdownMenuItem
|
||||
onClick={() => snapV2.setDiffContent(sql, DiffType.Addition)}
|
||||
>
|
||||
Insert code
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => snapV2.setDiffContent(sql, DiffType.Modification)}
|
||||
>
|
||||
Replace code
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => snapV2.setDiffContent(sql, DiffType.NewSnippet)}
|
||||
>
|
||||
Create new snippet
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
icon={<Play size={14} />}
|
||||
loading={isExecuting}
|
||||
onClick={() => {
|
||||
handleExecute()
|
||||
if (isReadOnlySelect(sql)) {
|
||||
sendEvent({
|
||||
action: TELEMETRY_EVENTS.AI_ASSISTANT_V2,
|
||||
value: TELEMETRY_VALUES.RAN_SQL_SUGGESTION,
|
||||
label: 'select',
|
||||
})
|
||||
}
|
||||
}}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
className: 'max-w-56 text-center',
|
||||
text: isExecuting ? (
|
||||
<Markdown
|
||||
className="[&>p]:text-xs text-foreground"
|
||||
content={`Query is running. You may cancel ongoing queries via the [SQL Editor](/project/${ref}/sql?viewOngoingQueries=true).`}
|
||||
/>
|
||||
) : (
|
||||
'Run query'
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCode && (
|
||||
<CodeBlock
|
||||
hideLineNumbers
|
||||
value={sql}
|
||||
language="sql"
|
||||
className={cn(
|
||||
'max-w-full max-h-96 block !bg-transparent !py-3 !px-3.5 prose dark:prose-dark border-0 border-t text-foreground !rounded-none w-full',
|
||||
'[&>code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Results Section */}
|
||||
{results !== undefined && results.length > 0 && isChart && xAxis && yAxis ? (
|
||||
<div className="p-5 border-t">
|
||||
<ChartContainer config={{}} className="aspect-auto h-[250px] w-full">
|
||||
<BarChart
|
||||
data={results}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey={xAxis}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
minTickGap={32}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent className="w-[150px]" />} />
|
||||
<Bar dataKey={yAxis} fill="hsl(var(--chart-2))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{error !== undefined ? (
|
||||
<Admonition
|
||||
type="warning"
|
||||
className="m-0 rounded-none border-x-0 border-b-0 [&>div>div>pre]:text-sm [&>div]:flex [&>div]:flex-col [&>div]:gap-y-2"
|
||||
title={errorHeader || 'Error running SQL query'}
|
||||
description={
|
||||
<>
|
||||
<div>
|
||||
{errorContent.length > 0 ? (
|
||||
<div>
|
||||
{errorContent.map((errorText: string, i: number) => (
|
||||
<pre key={`err-${i}`} className="font-mono text-xs whitespace-pre-wrap">
|
||||
{errorText}
|
||||
</pre>
|
||||
))}
|
||||
<Button
|
||||
type="default"
|
||||
className="mt-2"
|
||||
onClick={() => {
|
||||
setAiAssistantPanel({
|
||||
sqlSnippets: [sql],
|
||||
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${errorHeader}\n${errorContent.join('\n')}`,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Debug
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="font-mono text-xs">{error.error}</p>
|
||||
<Button
|
||||
type="default"
|
||||
className="mt-2"
|
||||
onClick={() => {
|
||||
setAiAssistantPanel({
|
||||
sqlSnippets: [sql],
|
||||
initialInput: `Help me to debug the attached sql snippet which gives the following error: \n\n${error.error}`,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Debug
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : results !== undefined && results.length > 0 ? (
|
||||
<>
|
||||
<div className={cn(showResults ? 'h-auto max-h-64 overflow-auto border-t' : 'h-0')}>
|
||||
<Results rows={results} />
|
||||
</div>
|
||||
<div className="flex items-center justify-between border-t bg-background-muted py-2 pl-2 pr-5">
|
||||
<p className="text-xs text-foreground-light">
|
||||
{results.length} rows
|
||||
{results.length >= 100 && ` (Limited to only 100 rows)`}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
results !== undefined &&
|
||||
results.length === 0 && (
|
||||
<div className="flex items-center justify-between border-t bg-surface-100 h-[43px] pl-2 pr-5">
|
||||
<p className="text-xs text-foreground-light">Success. No rows returned.</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { SqlSnippetWrapper as SqlSnippet }
|
||||
@@ -14,11 +14,12 @@ export type DatabasePoliciesVariables = {
|
||||
|
||||
export async function getDatabasePolicies(
|
||||
{ projectRef, connectionString, schema }: DatabasePoliciesVariables,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
headersInit?: HeadersInit
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
let headers = new Headers()
|
||||
let headers = new Headers(headersInit)
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
const { data, error } = await get('/platform/pg-meta/{ref}/policies', {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
/* SQL */ `
|
||||
DROP TYPE IF EXISTS pg_temp.tabledefs CASCADE;
|
||||
CREATE TYPE pg_temp.tabledefs AS ENUM ('PKEY_INTERNAL','PKEY_EXTERNAL','FKEYS_INTERNAL', 'FKEYS_EXTERNAL', 'COMMENTS', 'FKEYS_NONE', 'INCLUDE_TRIGGERS', 'NO_TRIGGERS');
|
||||
|
||||
|
||||
-- SELECT * FROM pg_temp.pg_get_coldef('sample','orders','id');
|
||||
-- DROP FUNCTION pg_temp.pg_get_coldef(text,text,text,boolean);
|
||||
CREATE OR REPLACE FUNCTION pg_temp.pg_get_coldef(
|
||||
@@ -32,39 +32,39 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
v_dt2 text;
|
||||
v_dt3 text;
|
||||
v_nullable boolean;
|
||||
v_position int;
|
||||
v_identity text;
|
||||
v_generated text;
|
||||
v_hasdflt boolean;
|
||||
v_position int;
|
||||
v_identity text;
|
||||
v_generated text;
|
||||
v_hasdflt boolean;
|
||||
v_dfltexpr text;
|
||||
|
||||
|
||||
BEGIN
|
||||
IF oldway THEN
|
||||
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
|
||||
IF oldway THEN
|
||||
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
|
||||
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
|
||||
-- RAISE NOTICE 'DEBUG: oldway=%',v_coldef;
|
||||
ELSE
|
||||
-- a.attrelid::regclass::text, a.attname
|
||||
SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND
|
||||
pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid
|
||||
WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type
|
||||
INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
|
||||
SELECT CASE WHEN a.atttypid = ANY ('{int,int8,int2}'::regtype[]) AND EXISTS (SELECT FROM pg_attrdef ad WHERE ad.adrelid = a.attrelid AND ad.adnum = a.attnum AND
|
||||
pg_get_expr(ad.adbin, ad.adrelid) = 'nextval(''' || (pg_get_serial_sequence (a.attrelid::regclass::text, a.attname))::regclass || '''::regclass)') THEN CASE a.atttypid
|
||||
WHEN 'int'::regtype THEN 'serial' WHEN 'int8'::regtype THEN 'bigserial' WHEN 'int2'::regtype THEN 'smallserial' END ELSE format_type(a.atttypid, a.atttypmod) END AS data_type
|
||||
INTO v_coldef FROM pg_namespace n, pg_class c, pg_attribute a, pg_type t
|
||||
WHERE n.nspname = in_schema AND n.oid = c.relnamespace AND c.relname = in_table AND a.attname = in_column and a.attnum > 0 AND a.attrelid = c.oid AND a.atttypid = t.oid ORDER BY a.attnum;
|
||||
-- RAISE NOTICE 'DEBUG: newway=%',v_coldef;
|
||||
|
||||
-- Issue#24: not implemented yet
|
||||
|
||||
-- Issue#24: not implemented yet
|
||||
-- might replace with this below to do more detailed parsing...
|
||||
-- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable,
|
||||
-- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr
|
||||
-- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr
|
||||
-- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
|
||||
-- SELECT a.atttypid::regtype AS dt1, format_type(a.atttypid, a.atttypmod) as dt2, t.typname as dt3, CASE WHEN not(a.attnotnull) THEN True ELSE False END AS nullable,
|
||||
-- a.attnum, a.attidentity, a.attgenerated, a.atthasdef, pg_get_expr(ad.adbin, ad.adrelid) dfltexpr
|
||||
-- INTO v_dt1, v_dt2, v_dt3, v_nullable, v_position, v_identity, v_generated, v_hasdflt, v_dfltexpr
|
||||
-- FROM pg_attribute a JOIN pg_class c ON (a.attrelid = c.oid) JOIN pg_type t ON (a.atttypid = t.oid) LEFT JOIN pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
|
||||
-- WHERE c.relkind in ('r','p') AND a.attnum > 0 AND NOT a.attisdropped AND c.relnamespace::regnamespace::text = in_schema AND c.relname = in_table AND a.attname = in_column;
|
||||
-- RAISE NOTICE 'schema=% table=% column=% dt1=% dt2=% dt3=% nullable=% pos=% identity=% generated=% HasDefault=% DeftExpr=%', in_schema, in_table, in_column, v_dt1,v_dt2,v_dt3,v_nullable,v_position,v_identity,v_generated,v_hasdflt,v_dfltexpr;
|
||||
END IF;
|
||||
RETURN v_coldef;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
-- SELECT * FROM pg_temp.pg_get_tabledef('sample', 'address', false);
|
||||
DROP FUNCTION IF EXISTS pg_temp.pg_get_tabledef(character varying,character varying,boolean,tabledefs[]);
|
||||
CREATE OR REPLACE FUNCTION pg_temp.pg_get_tabledef(
|
||||
@@ -97,7 +97,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
v_parent text;
|
||||
v_parent_schema text;
|
||||
v_persist text;
|
||||
v_temp text := '';
|
||||
v_temp text := '';
|
||||
v_temp2 text;
|
||||
v_relopts text;
|
||||
v_tablespace text;
|
||||
@@ -116,8 +116,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
search_path_new text := '';
|
||||
v_partial boolean;
|
||||
v_pos integer;
|
||||
|
||||
-- assume defaults for ENUMs at the getgo
|
||||
|
||||
-- assume defaults for ENUMs at the getgo
|
||||
pkcnt int := 0;
|
||||
fkcnt int := 0;
|
||||
trigcnt int := 0;
|
||||
@@ -128,7 +128,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
arglen integer;
|
||||
vargs text;
|
||||
avarg pg_temp.tabledefs;
|
||||
|
||||
|
||||
-- exception variables
|
||||
v_ret text;
|
||||
v_diag1 text;
|
||||
@@ -137,14 +137,14 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
v_diag4 text;
|
||||
v_diag5 text;
|
||||
v_diag6 text;
|
||||
|
||||
|
||||
BEGIN
|
||||
SET client_min_messages = 'notice';
|
||||
IF _verbose THEN bVerbose = True; END IF;
|
||||
|
||||
-- v17 fix: handle case-sensitive
|
||||
|
||||
-- v17 fix: handle case-sensitive
|
||||
-- v_qualified = in_schema || '.' || in_table;
|
||||
|
||||
|
||||
arglen := array_length($4, 1);
|
||||
IF arglen IS NULL THEN
|
||||
-- nothing to do, so assume defaults
|
||||
@@ -164,44 +164,44 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
trigtype = avarg;
|
||||
ELSEIF avarg = 'PKEY_EXTERNAL' THEN
|
||||
pkcnt = pkcnt + 1;
|
||||
pktype = avarg;
|
||||
pktype = avarg;
|
||||
ELSEIF avarg = 'COMMENTS' THEN
|
||||
cmtcnt = cmtcnt + 1;
|
||||
|
||||
|
||||
END IF;
|
||||
END LOOP;
|
||||
IF fkcnt > 1 THEN
|
||||
IF fkcnt > 1 THEN
|
||||
RAISE WARNING 'Only one foreign key option can be provided. You provided %', fkcnt;
|
||||
RETURN '';
|
||||
ELSEIF trigcnt > 1 THEN
|
||||
ELSEIF trigcnt > 1 THEN
|
||||
RAISE WARNING 'Only one trigger option can be provided. You provided %', trigcnt;
|
||||
RETURN '';
|
||||
ELSEIF pkcnt > 1 THEN
|
||||
ELSEIF pkcnt > 1 THEN
|
||||
RAISE WARNING 'Only one pkey option can be provided. You provided %', pkcnt;
|
||||
RETURN '';
|
||||
ELSEIF cmtcnt > 1 THEN
|
||||
RETURN '';
|
||||
ELSEIF cmtcnt > 1 THEN
|
||||
RAISE WARNING 'Only one comments option can be provided. You provided %', cmtcnt;
|
||||
RETURN '';
|
||||
|
||||
END IF;
|
||||
RETURN '';
|
||||
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
|
||||
SELECT c.oid, (select setting from pg_settings where name = 'server_version_num') INTO v_table_oid, v_pgversion FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind in ('r','p') AND c.relname = in_table AND n.nspname = in_schema;
|
||||
|
||||
|
||||
-- set search_path = public before we do anything to force explicit schema qualification but dont forget to set it back before exiting...
|
||||
SELECT setting INTO search_path_old FROM pg_settings WHERE name = 'search_path';
|
||||
|
||||
|
||||
-- RAISE NOTICE 'DEBUG tableddl: saving old search_path: ***%***', search_path_old;
|
||||
EXECUTE 'SET search_path = "public"';
|
||||
SELECT setting INTO search_path_new FROM pg_settings WHERE name = 'search_path';
|
||||
-- RAISE NOTICE 'DEBUG tableddl: using new search path=***%***', search_path_new;
|
||||
|
||||
|
||||
-- throw an error if table was not found
|
||||
IF (v_table_oid IS NULL) THEN
|
||||
RAISE EXCEPTION 'table does not exist';
|
||||
END IF;
|
||||
|
||||
|
||||
-- get user-defined tablespaces if applicable
|
||||
SELECT tablespace INTO v_temp FROM pg_tables WHERE schemaname = in_schema and tablename = in_table and tablespace IS NOT NULL;
|
||||
IF v_temp IS NULL THEN
|
||||
@@ -209,16 +209,16 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
ELSE
|
||||
v_tablespace := 'TABLESPACE ' || v_temp;
|
||||
END IF;
|
||||
|
||||
|
||||
-- also see if there are any SET commands for this table, ie, autovacuum_enabled=off, fillfactor=70
|
||||
WITH relopts AS (SELECT unnest(c.reloptions) relopts FROM pg_class c, pg_namespace n WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table)
|
||||
WITH relopts AS (SELECT unnest(c.reloptions) relopts FROM pg_class c, pg_namespace n WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table)
|
||||
SELECT string_agg(r.relopts, ', ') as relopts INTO v_temp from relopts r;
|
||||
IF v_temp IS NULL THEN
|
||||
v_relopts := '';
|
||||
ELSE
|
||||
v_relopts := ' WITH (' || v_temp || ')';
|
||||
END IF;
|
||||
|
||||
|
||||
-- -----------------------------------------------------------------------------------
|
||||
-- Create table defs for partitions/children using inheritance or declarative methods.
|
||||
-- inheritance: pg_class.relkind = 'r' pg_class.relispartition=false pg_class.relpartbound is NULL
|
||||
@@ -230,7 +230,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
IF v_pgversion < 100000 THEN
|
||||
-- Issue#11: handle parent schema
|
||||
SELECT c2.relname parent, c2.relnamespace::regnamespace INTO v_parent, v_parent_schema from pg_class c1, pg_namespace n, pg_inherits i, pg_class c2
|
||||
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
|
||||
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.oid = i.inhrelid and i.inhparent = c2.oid and c1.relkind = 'r';
|
||||
IF (v_parent IS NOT NULL) THEN
|
||||
bPartition := True;
|
||||
bInheritance := True;
|
||||
@@ -250,24 +250,24 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
END IF;
|
||||
IF bPartition THEN
|
||||
--Issue#17 fix for case-sensitive tables
|
||||
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
|
||||
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
|
||||
|
||||
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
|
||||
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
|
||||
|
||||
--Issue#19 put double-quotes around SQL keyword column names
|
||||
-- Issue#121: fix keyword lookup for table name not column name that does not apply here
|
||||
-- SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
|
||||
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = in_table AND catcode = 'R';
|
||||
|
||||
|
||||
IF bInheritance THEN
|
||||
-- inheritance-based
|
||||
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
|
||||
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
|
||||
v_table_ddl := 'CREATE TABLE ' || in_schema || '."' || in_table || '"( '|| E'\\n';
|
||||
ELSE
|
||||
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
|
||||
v_table_ddl := 'CREATE TABLE ' || in_schema || '.' || in_table || '( '|| E'\\n';
|
||||
END IF;
|
||||
|
||||
|
||||
-- Jump to constraints section to add the check constraints
|
||||
ELSE
|
||||
-- declarative-based
|
||||
@@ -288,7 +288,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
END IF;
|
||||
END IF;
|
||||
IF bVerbose THEN RAISE NOTICE '(1)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
IF NOT bPartition THEN
|
||||
-- see if this is unlogged or temporary table
|
||||
select c.relpersistence into v_persist from pg_class c, pg_namespace n where n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r';
|
||||
@@ -300,29 +300,29 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
v_temp := '';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
|
||||
-- start the create definition for regular tables unless we are in progress creating an inheritance-based child table
|
||||
IF NOT bPartition THEN
|
||||
--Issue#17 fix for case-sensitive tables
|
||||
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
|
||||
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
|
||||
-- SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
-- WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = quote_ident(in_schema) AND t.table_name = quote_ident(in_table) AND t.table_type = 'BASE TABLE');
|
||||
SELECT count(*) INTO v_cnt1 FROM information_schema.tables t WHERE EXISTS (SELECT REGEXP_MATCHES(s.table_name, '([A-Z]+)','g') FROM information_schema.tables s
|
||||
WHERE t.table_schema=s.table_schema AND t.table_name=s.table_name AND t.table_schema = in_schema AND t.table_name = in_table AND t.table_type = 'BASE TABLE');
|
||||
IF v_cnt1 > 0 THEN
|
||||
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '."' || in_table || '" (' || E'\\n';
|
||||
ELSE
|
||||
v_table_ddl := 'CREATE ' || v_temp || ' TABLE ' || in_schema || '.' || in_table || ' (' || E'\\n';
|
||||
END IF;
|
||||
END IF;
|
||||
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
|
||||
-- RAISE NOTICE 'DEBUG2: tabledef so far: %', v_table_ddl;
|
||||
-- define all of the columns in the table unless we are in progress creating an inheritance-based child table
|
||||
IF NOT bPartition THEN
|
||||
FOR v_colrec IN
|
||||
SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression
|
||||
SELECT c.column_name, c.data_type, c.udt_name, c.udt_schema, c.character_maximum_length, c.is_nullable, c.column_default, c.numeric_precision, c.numeric_scale, c.is_identity, c.identity_generation, c.is_generated, c.generation_expression
|
||||
FROM information_schema.columns c WHERE (table_schema, table_name) = (in_schema, in_table) ORDER BY ordinal_position
|
||||
LOOP
|
||||
IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF;
|
||||
|
||||
IF bVerbose THEN RAISE NOTICE '(col loop) name=% type=% udt_name=% default=% is_generated=% gen_expr=%', v_colrec.column_name, v_colrec.data_type, v_colrec.udt_name, v_colrec.column_default, v_colrec.is_generated, v_colrec.generation_expression; END IF;
|
||||
|
||||
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
|
||||
-- SELECT CASE WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
|
||||
SELECT CASE WHEN pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN True ELSE False END into bSerial;
|
||||
@@ -335,20 +335,20 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
RAISE NOTICE 'DEBUG table: % Column: % datatype: % Serial=% serialval=% coldef=%', v_qualified, v_colrec.column_name, v_colrec.data_type, bSerial, v_temp, v_diag1;
|
||||
RAISE NOTICE 'DEBUG tabledef: %', v_table_ddl;
|
||||
END IF;
|
||||
|
||||
|
||||
--Issue#17 put double-quotes around case-sensitive column names
|
||||
SELECT COUNT(*) INTO v_cnt1 FROM information_schema.columns t WHERE EXISTS (SELECT REGEXP_MATCHES(s.column_name, '([A-Z]+)','g') FROM information_schema.columns s
|
||||
WHERE t.table_schema=s.table_schema and t.table_name=s.table_name and t.column_name=s.column_name AND t.table_schema = quote_ident(in_schema) AND column_name = v_colrec.column_name);
|
||||
|
||||
--Issue#19 put double-quotes around SQL keyword column names
|
||||
SELECT COUNT(*) INTO v_cnt1 FROM information_schema.columns t WHERE EXISTS (SELECT REGEXP_MATCHES(s.column_name, '([A-Z]+)','g') FROM information_schema.columns s
|
||||
WHERE t.table_schema=s.table_schema and t.table_name=s.table_name and t.column_name=s.column_name AND t.table_schema = quote_ident(in_schema) AND column_name = v_colrec.column_name);
|
||||
|
||||
--Issue#19 put double-quotes around SQL keyword column names
|
||||
SELECT COUNT(*) INTO v_cnt2 FROM pg_get_keywords() WHERE word = v_colrec.column_name AND catcode = 'R';
|
||||
|
||||
|
||||
IF v_cnt1 > 0 OR v_cnt2 > 0 THEN
|
||||
v_table_ddl := v_table_ddl || ' "' || v_colrec.column_name || '" ';
|
||||
ELSE
|
||||
v_table_ddl := v_table_ddl || ' ' || v_colrec.column_name || ' ';
|
||||
END IF;
|
||||
|
||||
|
||||
-- Issue#23: Handle autogenerated columns and rewrite as a simpler IF THEN ELSE branch instead of a much more complex embedded CASE STATEMENT
|
||||
IF v_colrec.is_generated = 'ALWAYS' and v_colrec.generation_expression IS NOT NULL THEN
|
||||
-- searchable tsvector GENERATED ALWAYS AS (to_tsvector('simple'::regconfig, COALESCE(translate(email, '@.-'::citext, ' '::text), ''::text)) ) STORED
|
||||
@@ -361,7 +361,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
-- Issue#6 fix: handle arrays
|
||||
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
|
||||
-- v17 fix: handle case-sensitive for pg_get_serial_sequence that requires SQL Identifier handling
|
||||
-- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL
|
||||
-- WHEN pg_get_serial_sequence(v_qualified, v_colrec.column_name) IS NOT NULL
|
||||
ELSEIF pg_get_serial_sequence(quote_ident(in_schema) || '.' || quote_ident(in_table), v_colrec.column_name) IS NOT NULL THEN
|
||||
-- Issue#8 fix: handle serial. Note: NOT NULL is implied so no need to declare it explicitly
|
||||
v_temp = pg_temp.pg_get_coldef(in_schema, in_table,v_colrec.column_name);
|
||||
@@ -369,31 +369,31 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
v_temp = v_colrec.data_type;
|
||||
END IF;
|
||||
-- RAISE NOTICE 'column def1=%', v_temp;
|
||||
|
||||
|
||||
-- handle IDENTITY columns
|
||||
IF v_colrec.is_identity = 'YES' THEN
|
||||
IF v_colrec.identity_generation = 'ALWAYS' THEN
|
||||
IF v_colrec.identity_generation = 'ALWAYS' THEN
|
||||
v_temp = v_temp || ' GENERATED ALWAYS AS IDENTITY';
|
||||
ELSE
|
||||
v_temp = v_temp || ' GENERATED BY DEFAULT AS IDENTITY';
|
||||
END IF;
|
||||
ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN
|
||||
ELSEIF v_colrec.character_maximum_length IS NOT NULL THEN
|
||||
v_temp = v_temp || ('(' || v_colrec.character_maximum_length || ')');
|
||||
ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN
|
||||
ELSEIF v_colrec.numeric_precision > 0 AND v_colrec.numeric_scale > 0 THEN
|
||||
v_temp = v_temp || '(' || v_colrec.numeric_precision || ',' || v_colrec.numeric_scale || ')';
|
||||
END IF;
|
||||
|
||||
|
||||
-- Handle NULL/NOT NULL
|
||||
IF bSerial THEN
|
||||
IF bSerial THEN
|
||||
v_temp = v_temp || ' NOT NULL';
|
||||
ELSEIF v_colrec.is_nullable = 'NO' THEN
|
||||
ELSEIF v_colrec.is_nullable = 'NO' THEN
|
||||
v_temp = v_temp || ' NOT NULL';
|
||||
ELSEIF v_colrec.is_nullable = 'YES' THEN
|
||||
v_temp = v_temp || ' NULL';
|
||||
END IF;
|
||||
|
||||
|
||||
-- Handle defaults
|
||||
IF v_colrec.column_default IS NOT null AND NOT bSerial THEN
|
||||
IF v_colrec.column_default IS NOT null AND NOT bSerial THEN
|
||||
-- RAISE NOTICE 'Setting default for column, %', v_colrec.column_name;
|
||||
v_temp = v_temp || (' DEFAULT ' || v_colrec.column_default);
|
||||
END IF;
|
||||
@@ -401,11 +401,11 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
-- RAISE NOTICE 'column def2=%', v_temp;
|
||||
v_table_ddl := v_table_ddl || v_temp;
|
||||
-- RAISE NOTICE 'tabledef=%', v_table_ddl;
|
||||
|
||||
|
||||
END LOOP;
|
||||
END IF;
|
||||
IF bVerbose THEN RAISE NOTICE '(2)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
-- define all the constraints: conparentid does not exist pre PGv11
|
||||
IF v_pgversion < 110000 THEN
|
||||
FOR v_constraintrec IN
|
||||
@@ -435,8 +435,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
|| ',' || E'\\n';
|
||||
ELSE
|
||||
-- Issue#16 handle external PG def
|
||||
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
|
||||
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
|
||||
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
|
||||
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table and r.conname = v_constraint_name;
|
||||
END IF;
|
||||
IF bPartition THEN
|
||||
continue;
|
||||
@@ -453,10 +453,10 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
|| 'CONSTRAINT' || ' '
|
||||
|| v_constraint_name || ' '
|
||||
|| v_constraint_def
|
||||
|| ',' || E'\\n';
|
||||
|| ',' || E'\\n';
|
||||
ELSE
|
||||
-- external def
|
||||
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
|
||||
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
|
||||
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table;
|
||||
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
|
||||
END IF;
|
||||
@@ -466,11 +466,11 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
|| 'CONSTRAINT' || ' '
|
||||
|| v_constraint_name || ' '
|
||||
|| v_constraint_def
|
||||
|| ',' || E'\\n';
|
||||
|| ',' || E'\\n';
|
||||
END IF;
|
||||
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
|
||||
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
|
||||
|
||||
|
||||
END LOOP;
|
||||
ELSE
|
||||
-- handle PG versions 11 and up
|
||||
@@ -486,9 +486,9 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
END as type_rank,
|
||||
pg_get_constraintdef(con.oid) as constraint_definition
|
||||
FROM pg_catalog.pg_constraint con JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid JOIN pg_catalog.pg_namespace nsp ON nsp.oid = connamespace
|
||||
WHERE nsp.nspname = in_schema AND rel.relname = in_table
|
||||
WHERE nsp.nspname = in_schema AND rel.relname = in_table
|
||||
--Issue#13 added this condition:
|
||||
AND con.conparentid = 0
|
||||
AND con.conparentid = 0
|
||||
ORDER BY type_rank
|
||||
LOOP
|
||||
v_constraint_name := v_constraintrec.constraint_name;
|
||||
@@ -505,8 +505,8 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
|| ',' || E'\\n';
|
||||
ELSE
|
||||
-- Issue#16 handle external PG def
|
||||
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
|
||||
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
|
||||
SELECT 'ALTER TABLE ONLY ' || in_schema || '.' || c.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_pkey_def
|
||||
FROM pg_catalog.pg_constraint r, pg_class c, pg_namespace n where r.conrelid = c.oid and r.contype = 'p' and n.oid = r.connamespace and n.nspname = in_schema AND c.relname = in_table;
|
||||
END IF;
|
||||
IF bPartition THEN
|
||||
continue;
|
||||
@@ -516,18 +516,18 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
--Issue#22 fix: added FKEY_NONE check
|
||||
IF fktype = 'FKEYS_NONE' THEN
|
||||
-- skip
|
||||
continue;
|
||||
continue;
|
||||
ELSIF fkcnt = 0 OR fktype = 'FKEYS_INTERNAL' THEN
|
||||
-- internal def
|
||||
v_table_ddl := v_table_ddl || ' ' -- note: two char spacer to start, to indent the column
|
||||
|| 'CONSTRAINT' || ' '
|
||||
|| v_constraint_name || ' '
|
||||
|| v_constraint_def
|
||||
|| ',' || E'\\n';
|
||||
|| ',' || E'\\n';
|
||||
ELSE
|
||||
-- external def
|
||||
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
|
||||
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
|
||||
SELECT 'ALTER TABLE ONLY ' || n.nspname || '.' || c2.relname || ' ADD CONSTRAINT ' || r.conname || ' ' || pg_catalog.pg_get_constraintdef(r.oid, true) || ';' INTO v_fkey_def
|
||||
FROM pg_constraint r, pg_class c1, pg_namespace n, pg_class c2 where r.conrelid = c1.oid and r.contype = 'f' and n.nspname = in_schema and n.oid = r.connamespace and r.conrelid = c2.oid and c2.relname = in_table and
|
||||
r.conname = v_constraint_name and r.conparentid = 0;
|
||||
v_fkey_defs = v_fkey_defs || v_fkey_def || E'\\n';
|
||||
END IF;
|
||||
@@ -537,14 +537,14 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
|| 'CONSTRAINT' || ' '
|
||||
|| v_constraint_name || ' '
|
||||
|| v_constraint_def
|
||||
|| ',' || E'\\n';
|
||||
|| ',' || E'\\n';
|
||||
END IF;
|
||||
if bVerbose THEN RAISE NOTICE 'DEBUG4: constraint name=% constraint_def=%', v_constraint_name,v_constraint_def; END IF;
|
||||
constraintarr := constraintarr || v_constraintrec.constraint_name:: text;
|
||||
|
||||
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
END IF;
|
||||
|
||||
-- drop the last comma before ending the create statement, which should be right before the carriage return character
|
||||
-- Issue#24: make sure the comma is there before removing it
|
||||
select substring(v_table_ddl, length(v_table_ddl) - 1, 1) INTO v_temp;
|
||||
@@ -552,12 +552,12 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
v_table_ddl = substr(v_table_ddl, 0, length(v_table_ddl) - 1) || E'\\n';
|
||||
END IF;
|
||||
IF bVerbose THEN RAISE NOTICE '(3)tabledef so far: %', trim(v_table_ddl); END IF;
|
||||
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- at this point we have everything up to the last table-enclosing parenthesis
|
||||
-- ---------------------------------------------------------------------------
|
||||
IF bVerbose THEN RAISE NOTICE '(4)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
-- See if this is an inheritance-based child table and finish up the table create.
|
||||
IF bPartition and bInheritance THEN
|
||||
-- Issue#11: handle parent schema
|
||||
@@ -565,132 +565,132 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
IF v_parent_schema = '' OR v_parent_schema IS NULL THEN v_parent_schema = in_schema; END IF;
|
||||
v_table_ddl := v_table_ddl || ') INHERITS (' || v_parent_schema || '.' || v_parent || ') ' || E'\\n' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
|
||||
END IF;
|
||||
|
||||
|
||||
IF v_pgversion >= 100000 AND NOT bPartition and NOT bInheritance THEN
|
||||
-- See if this is a partitioned table (pg_class.relkind = 'p') and add the partitioned key
|
||||
SELECT pg_get_partkeydef(c1.oid) as partition_key INTO v_partition_key FROM pg_class c1 JOIN pg_namespace n ON (n.oid = c1.relnamespace) LEFT JOIN pg_partitioned_table p ON (c1.oid = p.partrelid)
|
||||
-- See if this is a partitioned table (pg_class.relkind = 'p') and add the partitioned key
|
||||
SELECT pg_get_partkeydef(c1.oid) as partition_key INTO v_partition_key FROM pg_class c1 JOIN pg_namespace n ON (n.oid = c1.relnamespace) LEFT JOIN pg_partitioned_table p ON (c1.oid = p.partrelid)
|
||||
WHERE n.nspname = in_schema and n.oid = c1.relnamespace and c1.relname = in_table and c1.relkind = 'p';
|
||||
|
||||
|
||||
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
|
||||
-- add partition clause
|
||||
-- NOTE: cannot specify default tablespace for partitioned relations
|
||||
-- v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ' ' || v_tablespace || ';' || E'\\n';
|
||||
v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ';' || E'\\n';
|
||||
-- v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ' ' || v_tablespace || ';' || E'\\n';
|
||||
v_table_ddl := v_table_ddl || ') PARTITION BY ' || v_partition_key || ';' || E'\\n';
|
||||
ELSEIF v_relopts <> '' THEN
|
||||
v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
|
||||
v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || ';' || E'\\n';
|
||||
ELSE
|
||||
-- end the create definition
|
||||
v_table_ddl := v_table_ddl || ') ' || v_tablespace || ';' || E'\\n';
|
||||
END IF;
|
||||
v_table_ddl := v_table_ddl || ') ' || v_tablespace || ';' || E'\\n';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
|
||||
IF bVerbose THEN RAISE NOTICE '(5)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
-- Add closing paren for regular tables
|
||||
-- IF NOT bPartition THEN
|
||||
-- v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || E';\\n';
|
||||
-- v_table_ddl := v_table_ddl || ') ' || v_relopts || ' ' || v_tablespace || E';\\n';
|
||||
-- END IF;
|
||||
-- RAISE NOTICE 'ddlsofar3: %', v_table_ddl;
|
||||
|
||||
|
||||
-- Issue#16 create the external PKEY def if indicated
|
||||
IF v_pkey_def <> '' THEN
|
||||
v_table_ddl := v_table_ddl || v_pkey_def || E'\\n';
|
||||
v_table_ddl := v_table_ddl || v_pkey_def || E'\\n';
|
||||
END IF;
|
||||
|
||||
|
||||
-- Issue#20
|
||||
IF v_fkey_defs <> '' THEN
|
||||
v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n';
|
||||
v_table_ddl := v_table_ddl || v_fkey_defs || E'\\n';
|
||||
END IF;
|
||||
|
||||
|
||||
IF bVerbose THEN RAISE NOTICE '(6)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
-- create indexes
|
||||
FOR v_indexrec IN
|
||||
SELECT indexdef, COALESCE(tablespace, 'pg_default') as tablespace, indexname FROM pg_indexes WHERE (schemaname, tablename) = (in_schema, in_table)
|
||||
LOOP
|
||||
-- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef;
|
||||
-- RAISE NOTICE 'DEBUG6: indexname=% indexdef=%', v_indexrec.indexname, v_indexrec.indexdef;
|
||||
-- loop through constraints and skip ones already defined
|
||||
bSkip = False;
|
||||
FOREACH constraintelement IN ARRAY constraintarr
|
||||
LOOP
|
||||
LOOP
|
||||
IF constraintelement = v_indexrec.indexname THEN
|
||||
-- RAISE NOTICE 'DEBUG7: skipping index, %', v_indexrec.indexname;
|
||||
bSkip = True;
|
||||
EXIT;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
if bSkip THEN CONTINUE; END IF;
|
||||
|
||||
|
||||
-- Add IF NOT EXISTS clause so partition index additions will not be created if declarative partition in effect and index already created on parent
|
||||
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE INDEX', 'CREATE INDEX IF NOT EXISTS');
|
||||
-- Fix Issue#26: do it for unique/primary key indexes as well
|
||||
v_indexrec.indexdef := REPLACE(v_indexrec.indexdef, 'CREATE UNIQUE INDEX', 'CREATE UNIQUE INDEX IF NOT EXISTS');
|
||||
-- RAISE NOTICE 'DEBUG8: adding index, %', v_indexrec.indexname;
|
||||
|
||||
|
||||
-- NOTE: cannot specify default tablespace for partitioned relations
|
||||
IF v_partition_key IS NOT NULL AND v_partition_key <> '' THEN
|
||||
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ';' || E'\\n';
|
||||
ELSE
|
||||
-- Issue#25: see if partial index or not
|
||||
select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial
|
||||
FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid)
|
||||
WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname;
|
||||
select CASE WHEN i.indpred IS NOT NULL THEN True ELSE False END INTO v_partial
|
||||
FROM pg_index i JOIN pg_class c1 ON (i.indexrelid = c1.oid) JOIN pg_class c2 ON (i.indrelid = c2.oid)
|
||||
WHERE c1.relnamespace::regnamespace::text = in_schema AND c2.relnamespace::regnamespace::text = in_schema AND c2.relname = in_table AND c1.relname = v_indexrec.indexname;
|
||||
IF v_partial THEN
|
||||
-- Put tablespace def before WHERE CLAUSE
|
||||
v_temp = v_indexrec.indexdef;
|
||||
v_pos = POSITION(' WHERE ' IN v_temp);
|
||||
v_temp2 = SUBSTRING(v_temp, v_pos);
|
||||
v_temp = SUBSTRING(v_temp, 1, v_pos);
|
||||
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
|
||||
v_table_ddl := v_table_ddl || v_temp || ' TABLESPACE ' || v_indexrec.tablespace || v_temp2 || ';' || E'\\n';
|
||||
ELSE
|
||||
v_table_ddl := v_table_ddl || v_indexrec.indexdef || ' TABLESPACE ' || v_indexrec.tablespace || ';' || E'\\n';
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
|
||||
END LOOP;
|
||||
IF bVerbose THEN RAISE NOTICE '(7)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
-- Issue#20: added logic for table and column comments
|
||||
IF cmtcnt > 0 THEN
|
||||
IF cmtcnt > 0 THEN
|
||||
FOR v_rec IN
|
||||
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
|
||||
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
|
||||
SELECT c.relname, 'COMMENT ON ' || CASE WHEN c.relkind in ('r','p') AND a.attname IS NULL THEN 'TABLE ' WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN 'COLUMN ' WHEN c.relkind = 'f' THEN 'FOREIGN TABLE '
|
||||
WHEN c.relkind = 'm' THEN 'MATERIALIZED VIEW ' WHEN c.relkind = 'v' THEN 'VIEW ' WHEN c.relkind = 'i' THEN 'INDEX ' WHEN c.relkind = 'S' THEN 'SEQUENCE ' ELSE 'XX' END || n.nspname || '.' ||
|
||||
CASE WHEN c.relkind in ('r','p') AND a.attname IS NOT NULL THEN quote_ident(c.relname) || '.' || a.attname ELSE quote_ident(c.relname) END || ' IS ' || quote_literal(d.description) || ';' as ddl
|
||||
FROM pg_class c JOIN pg_namespace n ON (n.oid = c.relnamespace) LEFT JOIN pg_description d ON (c.oid = d.objoid) LEFT JOIN pg_attribute a ON (c.oid = a.attrelid AND a.attnum > 0 and a.attnum = d.objsubid)
|
||||
WHERE d.description IS NOT NULL AND n.nspname = in_schema AND c.relname = in_table ORDER BY 2 desc, ddl
|
||||
LOOP
|
||||
--RAISE NOTICE 'comments:%', v_rec.ddl;
|
||||
v_table_ddl = v_table_ddl || v_rec.ddl || E'\\n';
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END IF;
|
||||
IF bVerbose THEN RAISE NOTICE '(8)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
IF trigtype = 'INCLUDE_TRIGGERS' THEN
|
||||
-- Issue#14: handle multiple triggers for a table
|
||||
FOR v_trigrec IN
|
||||
select pg_get_triggerdef(t.oid, True) || ';' as triggerdef FROM pg_trigger t, pg_class c, pg_namespace n
|
||||
select pg_get_triggerdef(t.oid, True) || ';' as triggerdef FROM pg_trigger t, pg_class c, pg_namespace n
|
||||
WHERE n.nspname = in_schema and n.oid = c.relnamespace and c.relname = in_table and c.relkind = 'r' and t.tgrelid = c.oid and NOT t.tgisinternal
|
||||
LOOP
|
||||
v_table_ddl := v_table_ddl || v_trigrec.triggerdef;
|
||||
v_table_ddl := v_table_ddl || E'\\n';
|
||||
v_table_ddl := v_table_ddl || E'\\n';
|
||||
IF bVerbose THEN RAISE NOTICE 'triggerdef = %', v_trigrec.triggerdef; END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
|
||||
IF bVerbose THEN RAISE NOTICE '(9)tabledef so far: %', v_table_ddl; END IF;
|
||||
-- add empty line
|
||||
v_table_ddl := v_table_ddl || E'\\n';
|
||||
IF bVerbose THEN RAISE NOTICE '(10)tabledef so far: %', v_table_ddl; END IF;
|
||||
|
||||
|
||||
-- reset search_path back to what it was
|
||||
IF search_path_old = '' THEN
|
||||
SELECT set_config('search_path', '', false) into v_temp;
|
||||
ELSE
|
||||
EXECUTE 'SET search_path = ' || search_path_old;
|
||||
END IF;
|
||||
|
||||
|
||||
RETURN v_table_ddl;
|
||||
|
||||
|
||||
EXCEPTION
|
||||
WHEN others THEN
|
||||
BEGIN
|
||||
@@ -701,7 +701,7 @@ export const CREATE_PG_GET_TABLEDEF_SQL = minify(
|
||||
-- put additional coding here if necessarY
|
||||
RETURN '';
|
||||
END;
|
||||
|
||||
|
||||
END;
|
||||
$$;`,
|
||||
{ compress: true, removeAll: true }
|
||||
|
||||
@@ -25,7 +25,7 @@ with records as (
|
||||
'NO_TRIGGERS'
|
||||
)
|
||||
when 'v' then concat(
|
||||
'create view ', concat(nc.nspname, '.', c.relname), ' as',
|
||||
'create view ', concat(nc.nspname, '.', c.relname), ' as',
|
||||
pg_get_viewdef(concat(nc.nspname, '.', c.relname), true)
|
||||
)
|
||||
when 'm' then concat(
|
||||
|
||||
@@ -37,7 +37,8 @@ export async function executeSql(
|
||||
| 'handleError'
|
||||
| 'isRoleImpersonationEnabled'
|
||||
>,
|
||||
signal?: AbortSignal
|
||||
signal?: AbortSignal,
|
||||
headersInit?: HeadersInit
|
||||
): Promise<{ result: any }> {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
@@ -47,7 +48,7 @@ export async function executeSql(
|
||||
throw new Error('Query is too large to be run via the SQL Editor')
|
||||
}
|
||||
|
||||
let headers = new Headers()
|
||||
let headers = new Headers(headersInit)
|
||||
if (connectionString) headers.set('x-connection-encrypted', connectionString)
|
||||
|
||||
let { data, error } = await post('/platform/pg-meta/{ref}/query', {
|
||||
|
||||
@@ -10,10 +10,15 @@ import type { ResponseError } from 'types'
|
||||
type SendEvent = components['schemas']['TelemetryEventBodyV2']
|
||||
|
||||
export type SendEventVariables = {
|
||||
/** Defines the name of the event, refer to TELEMETRY_EVENTS in lib/constants */
|
||||
action: string
|
||||
category: string
|
||||
label: string
|
||||
/** These are all under the event's properties (customizable on the FE) */
|
||||
/** value: refer to TELEMETRY_VALUES in lib/constants */
|
||||
value?: string
|
||||
/** label: secondary tag to the event for further identification */
|
||||
label?: string
|
||||
/** To deprecate - seems unnecessary */
|
||||
category?: string
|
||||
}
|
||||
|
||||
type SendEventPayload = any
|
||||
|
||||
@@ -2,6 +2,11 @@ import { useDisallowHipaa } from 'hooks/misc/useDisallowHipaa'
|
||||
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
|
||||
import { OPT_IN_TAGS } from 'lib/constants'
|
||||
|
||||
/**
|
||||
* Checks if the organization has opted into sending anonymous data to OpenAI.
|
||||
* Also considers if the organization has the HIPAA addon.
|
||||
* @returns boolean (false if either not opted in or has the HIPAA addon)
|
||||
*/
|
||||
export function useOrgOptedIntoAi() {
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const optInTags = selectedOrganization?.opt_in_tags
|
||||
|
||||
@@ -33,7 +33,7 @@ export const LOCAL_STORAGE_KEYS = {
|
||||
UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout',
|
||||
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
|
||||
UI_PREVIEW_CLS: 'supabase-ui-cls',
|
||||
UI_PREVIEW_FUNCTIONS_ASSISTANT: 'supabase-ui-functions-assistant',
|
||||
UI_PREVIEW_ASSISTANT_V2: 'supabase-ui-assistant-v2',
|
||||
UI_ONBOARDING_NEW_PAGE_SHOWN: 'supabase-ui-onboarding-new-page-shown',
|
||||
|
||||
SQL_SCRATCH_PAD_BANNER_ACKNOWLEDGED: 'supabase-sql-scratch-pad-banner-acknowledged',
|
||||
@@ -83,22 +83,3 @@ export const OPT_IN_TAGS = {
|
||||
export const GB = 1024 * 1024 * 1024
|
||||
export const MB = 1024 * 1024
|
||||
export const KB = 1024
|
||||
|
||||
// [Joshen] Just adding these to start consolidating our telemetry configs
|
||||
// may change depending on how we choose to standardize across all apps
|
||||
export enum TELEMETRY_CATEGORIES {
|
||||
AI_ASSISTANT = 'ai-assistant',
|
||||
}
|
||||
|
||||
export enum TELEMETRY_LABELS {
|
||||
QUICK_SQL_EDITOR = 'quick-sql-editor',
|
||||
}
|
||||
|
||||
export const TELEMETRY_ACTIONS = {
|
||||
PROMPT_SUBMITTED: 'prompt-submitted',
|
||||
QUICK_PROMPT_SELECTED: (type: string) => `quick-prompt-selected-${type}`,
|
||||
SCHEMA_CONTEXT_ADDED: 'schema-context-added',
|
||||
TABLE_CONTEXT_ADDED: 'table-context-added',
|
||||
FIX_WITH_ASSISTANT: 'fix-with-assistant',
|
||||
EXPLAIN_CODE: 'explain-code',
|
||||
}
|
||||
|
||||
38
apps/studio/lib/constants/telemetry.ts
Normal file
38
apps/studio/lib/constants/telemetry.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// [Joshen] Just adding these to start consolidating our telemetry configs
|
||||
// may change depending on how we choose to standardize across all apps
|
||||
// Events define the name of the event and it'll be used as the primary identification
|
||||
export enum TELEMETRY_EVENTS {
|
||||
FEATURE_PREVIEWS = 'Dashboard UI Feature Previews',
|
||||
AI_ASSISTANT_V2 = 'AI Assistant V2',
|
||||
}
|
||||
|
||||
// [Joshen] Values refer to the "action" of the "event"
|
||||
// e.g prompt submitted (action) through the AI assistant (event)
|
||||
// e.g enabled feature x (action) via the feature preview (event)
|
||||
export enum TELEMETRY_VALUES {
|
||||
/**
|
||||
* Track whenever a prompt is submitted to the AI (excluding debug prompts)
|
||||
* @context AI Assistant V2
|
||||
* @purpose Indication of engagement with the feature, aid in prioritizing efforts into the assistant itself
|
||||
*/
|
||||
PROMPT_SUBMITTED = 'prompt-submitted',
|
||||
/**
|
||||
* Track whenever a debug prompt is submitted to the AI
|
||||
* @context AI Assistant V2
|
||||
* @purpose TBD
|
||||
*/
|
||||
DEBUG_SUBMITTED = 'debug-submitted',
|
||||
/**
|
||||
* Track running a SQL suggestion from AI Assistant
|
||||
* @context AI Assistant V2
|
||||
* @purpose Indication of usefulness of AI assistant response, aid in prioritizing tweak of Assistant prompts to adjust output quality
|
||||
* @details Broken down into "select" or "mutation", and for the latter further broken down to the type of query (e.g "functions" or "rls-policies", default to unknown otherwise)
|
||||
* */
|
||||
RAN_SQL_SUGGESTION = 'ran-sql-suggestion',
|
||||
/**
|
||||
* Track editing a SQL suggestion:
|
||||
* @context AI Assistant V2
|
||||
* @purpose Indication of interest for wanting to expand from a SQL suggestion, aid in deciding the priority for an inline editor
|
||||
* */
|
||||
EDIT_IN_SQL_EDITOR = 'edit-in-sql-editor',
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export const config = {
|
||||
const HOSTED_SUPPORTED_API_URLS = [
|
||||
'/ai/sql/suggest',
|
||||
'/ai/sql/generate-v2',
|
||||
'/ai/sql/generate-v3',
|
||||
'/ai/sql/title',
|
||||
'/ai/sql/debug',
|
||||
'/ai/sql/cron',
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"prettier:write": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
"@dagrejs/dagre": "^1.0.4",
|
||||
"@graphiql/react": "^0.19.4",
|
||||
"@graphiql/toolkit": "^0.9.1",
|
||||
@@ -45,10 +46,11 @@
|
||||
"@tanstack/react-query": "4.35.7",
|
||||
"@tanstack/react-query-devtools": "4.35.7",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"@vercel/flags": "^2.6.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@zip.js/zip.js": "^2.7.29",
|
||||
"ai": "^2.2.31",
|
||||
"ai": "^3.4.33",
|
||||
"ai-commands": "*",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
"common": "*",
|
||||
@@ -69,6 +71,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.436.0",
|
||||
"markdown-table": "^3.0.3",
|
||||
"markdown-to-jsx": "^7.5.0",
|
||||
"memoize-one": "^5.0.1",
|
||||
"mime-db": "^1.53.0",
|
||||
"mobx": "^6.10.2",
|
||||
@@ -124,6 +127,7 @@
|
||||
"yup": "^1.4.0",
|
||||
"yup-password": "^0.3.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^5.0.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -156,7 +156,6 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<StudioCommandMenu />
|
||||
<GenerateSql />
|
||||
<FeaturePreviewModal />
|
||||
<AiAssistantPanel />
|
||||
</FeaturePreviewContextProvider>
|
||||
</AppBannerWrapper>
|
||||
<SonnerToaster position="top-right" />
|
||||
|
||||
120
apps/studio/pages/api/ai/sql/generate-v3.ts
Normal file
120
apps/studio/pages/api/ai/sql/generate-v3.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { openai } from '@ai-sdk/openai'
|
||||
import { streamText } from 'ai'
|
||||
import { getTools } from './tools'
|
||||
import pgMeta from '@supabase/pg-meta'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export const maxDuration = 30
|
||||
const openAiKey = process.env.OPENAI_API_KEY
|
||||
const pgMetaSchemasList = pgMeta.schemas.list()
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (!openAiKey) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'No OPENAI_API_KEY set. Create this environment variable to use AI features.',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
return handlePost(req, res)
|
||||
default:
|
||||
return new Response(
|
||||
JSON.stringify({ data: null, error: { message: `Method ${method} Not Allowed` } }),
|
||||
{
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json', Allow: 'POST' },
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { messages, projectRef, connectionString, includeSchemaMetadata } = req.body
|
||||
|
||||
if (!projectRef) {
|
||||
return res.status(400).json({
|
||||
error: 'Missing project_ref in query parameters',
|
||||
})
|
||||
}
|
||||
|
||||
const authorization = req.headers.authorization
|
||||
|
||||
const { result: schemas } = includeSchemaMetadata
|
||||
? await executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: pgMetaSchemasList.sql,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
...(authorization && { Authorization: authorization }),
|
||||
}
|
||||
)
|
||||
: { result: [] }
|
||||
|
||||
const result = await streamText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
maxSteps: 5,
|
||||
system: `
|
||||
You are a Supabase Postgres expert who can do three things.
|
||||
|
||||
# You generate and debug SQL
|
||||
The generated SQL (must be valid SQL), and must adhere to the following:
|
||||
- Always use double apostrophe in SQL strings (eg. 'Night''s watch')
|
||||
- Always use semicolons
|
||||
- Output as markdown
|
||||
- Always include code snippets if available
|
||||
- If a code snippet is SQL, the first line of the snippet should always be -- props: {"title": "Query title", "isChart": "true", "xAxis": "columnName", "yAxis": "columnName"}
|
||||
- Explain what the snippet does in a sentence or two before showing it
|
||||
- Use vector(384) data type for any embedding/vector related query
|
||||
- When debugging, retrieve sql schema details to ensure sql is correct
|
||||
|
||||
When generating tables, do the following:
|
||||
- For primary keys, always use "id bigint primary key generated always as identity" (not serial)
|
||||
- Prefer creating foreign key references in the create statement
|
||||
- Prefer 'text' over 'varchar'
|
||||
- Prefer 'timestamp with time zone' over 'date'
|
||||
|
||||
Feel free to suggest corrections for suspected typos.
|
||||
|
||||
# You write row level security policies.
|
||||
|
||||
Your purpose is to generate a policy with the constraints given by the user.
|
||||
- First, use getSchema to retrieve more information about a schema or schemas that will contain policies, usually the public schema.
|
||||
- Then retrieve existing RLS policies and guidelines on how to write policies using the getRlsKnowledge tool .
|
||||
- Then write new policies or update existing policies based on the prompt
|
||||
- When asked to suggest policies, either alter existing policies or add new ones to the public schema.
|
||||
|
||||
# You write database functions
|
||||
Your purpose is to generate a database function with the constraints given by the user. The output may also include a database trigger
|
||||
if the function returns a type of trigger. When generating functions, do the following:
|
||||
- If the function returns a trigger type, ensure that it uses security definer, otherwise default to security invoker. Include this in the create functions SQL statement.
|
||||
- Ensure to set the search_path configuration parameter as '', include this in the create functions SQL statement.
|
||||
- Default to create or replace whenever possible for updating an existing function, otherwise use the alter function statement
|
||||
Please make sure that all queries are valid Postgres SQL queries
|
||||
|
||||
Follow these instructions:
|
||||
- First look at the list of provided schemas and if needed, get more information about a schema. You will almost always need to retrieve information about the public schema before answering a question. If the question is about users, also retrieve the auth schema.
|
||||
|
||||
Here are the existing database schema names you can retrieve: ${schemas}
|
||||
`,
|
||||
messages,
|
||||
tools: getTools({ projectRef, connectionString, authorization, includeSchemaMetadata }),
|
||||
})
|
||||
|
||||
// write the data stream to the response
|
||||
// Note: this is sent as a single response, not a stream
|
||||
result.pipeDataStreamToResponse(res)
|
||||
}
|
||||
326
apps/studio/pages/api/ai/sql/tools.ts
Normal file
326
apps/studio/pages/api/ai/sql/tools.ts
Normal file
@@ -0,0 +1,326 @@
|
||||
import { tool } from 'ai'
|
||||
import { stripIndent } from 'common-tags'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { getDatabasePolicies } from 'data/database-policies/database-policies-query'
|
||||
import { getEntityDefinitionsSql } from 'data/database/entity-definitions-query'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
|
||||
export const getTools = ({
|
||||
projectRef,
|
||||
connectionString,
|
||||
authorization,
|
||||
includeSchemaMetadata,
|
||||
}: {
|
||||
projectRef: string
|
||||
connectionString: string
|
||||
authorization?: string
|
||||
includeSchemaMetadata: boolean
|
||||
}) => {
|
||||
return {
|
||||
getSchema: tool({
|
||||
description: 'Get more information about one or more schemas',
|
||||
parameters: z.object({
|
||||
schemas: z.array(z.string()).describe('The schema names to get the definitions for'),
|
||||
}),
|
||||
execute: async ({ schemas }) => {
|
||||
try {
|
||||
const result = includeSchemaMetadata
|
||||
? await executeSql(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
sql: getEntityDefinitionsSql({ schemas }),
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
...(authorization && { Authorization: authorization }),
|
||||
}
|
||||
)
|
||||
: { result: [] }
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Failed to execute SQL:', error)
|
||||
return `Failed to fetch schema: ${error}`
|
||||
}
|
||||
},
|
||||
}),
|
||||
getRlsKnowledge: tool({
|
||||
description:
|
||||
'Get existing policies and examples and instructions on how to write RLS policies',
|
||||
parameters: z.object({
|
||||
schemas: z.array(z.string()).describe('The schema names to get the policies for'),
|
||||
}),
|
||||
execute: async ({ schemas }) => {
|
||||
const data = includeSchemaMetadata
|
||||
? await getDatabasePolicies(
|
||||
{
|
||||
projectRef,
|
||||
connectionString,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
'Content-Type': 'application/json',
|
||||
...(authorization && { Authorization: authorization }),
|
||||
}
|
||||
)
|
||||
: []
|
||||
|
||||
const formattedPolicies = data
|
||||
.map(
|
||||
(policy) => `
|
||||
Policy Name: "${policy.name}"
|
||||
Action: ${policy.action}
|
||||
Roles: ${policy.roles.join(', ')}
|
||||
Command: ${policy.command}
|
||||
Definition: ${policy.definition}
|
||||
${policy.check ? `Check: ${policy.check}` : ''}
|
||||
`
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
return stripIndent`
|
||||
You're a Supabase Postgres expert in writing row level security policies. Your purpose is to
|
||||
generate a policy with the constraints given by the user. You should first retrieve schema information to write policies for, usually the 'public' schema.
|
||||
|
||||
The output should use the following instructions:
|
||||
- The generated SQL must be valid SQL.
|
||||
- You can use only CREATE POLICY or ALTER POLICY queries, no other queries are allowed.
|
||||
- Always use double apostrophe in SQL strings (eg. 'Night''s watch')
|
||||
- You can add short explanations to your messages.
|
||||
- The result should be a valid markdown. The SQL code should be wrapped in \`\`\` (including sql language tag).
|
||||
- Always use "auth.uid()" instead of "current_user".
|
||||
- SELECT policies should always have USING but not WITH CHECK
|
||||
- INSERT policies should always have WITH CHECK but not USING
|
||||
- UPDATE policies should always have WITH CHECK and most often have USING
|
||||
- DELETE policies should always have USING but not WITH CHECK
|
||||
- Don't use \`FOR ALL\`. Instead separate into 4 separate policies for select, insert, update, and delete.
|
||||
- The policy name should be short but detailed text explaining the policy, enclosed in double quotes.
|
||||
- Always put explanations as separate text. Never use inline SQL comments.
|
||||
- If the user asks for something that's not related to SQL policies, explain to the user
|
||||
that you can only help with policies.
|
||||
- Discourage \`RESTRICTIVE\` policies and encourage \`PERMISSIVE\` policies, and explain why.
|
||||
|
||||
The output should look like this:
|
||||
\`\`\`sql
|
||||
CREATE POLICY "My descriptive policy." ON books FOR INSERT to authenticated USING ( (select auth.uid()) = author_id ) WITH ( true );
|
||||
\`\`\`
|
||||
|
||||
Since you are running in a Supabase environment, take note of these Supabase-specific additions:
|
||||
|
||||
## Authenticated and unauthenticated roles
|
||||
|
||||
Supabase maps every request to one of the roles:
|
||||
|
||||
- \`anon\`: an unauthenticated request (the user is not logged in)
|
||||
- \`authenticated\`: an authenticated request (the user is logged in)
|
||||
|
||||
These are actually [Postgres Roles](/docs/guides/database/postgres/roles). You can use these roles within your Policies using the \`TO\` clause:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Profiles are viewable by everyone"
|
||||
on profiles
|
||||
for select
|
||||
to authenticated, anon
|
||||
using ( true );
|
||||
|
||||
-- OR
|
||||
|
||||
create policy "Public profiles are viewable only by authenticated users"
|
||||
on profiles
|
||||
for select
|
||||
to authenticated
|
||||
using ( true );
|
||||
\`\`\`
|
||||
|
||||
Note that \`for ...\` must be added after the table but before the roles. \`to ...\` must be added after \`for ...\`:
|
||||
|
||||
### Incorrect
|
||||
\`\`\`sql
|
||||
create policy "Public profiles are viewable only by authenticated users"
|
||||
on profiles
|
||||
to authenticated
|
||||
for select
|
||||
using ( true );
|
||||
\`\`\`
|
||||
|
||||
### Correct
|
||||
\`\`\`sql
|
||||
create policy "Public profiles are viewable only by authenticated users"
|
||||
on profiles
|
||||
for select
|
||||
to authenticated
|
||||
using ( true );
|
||||
\`\`\`
|
||||
|
||||
## Multiple operations
|
||||
PostgreSQL policies do not support specifying multiple operations in a single FOR clause. You need to create separate policies for each operation.
|
||||
|
||||
### Incorrect
|
||||
\`\`\`sql
|
||||
create policy "Profiles can be created and deleted by any user"
|
||||
on profiles
|
||||
for insert, delete -- cannot create a policy on multiple operators
|
||||
to authenticated
|
||||
with check ( true )
|
||||
using ( true );
|
||||
\`\`\`
|
||||
|
||||
### Correct
|
||||
\`\`\`sql
|
||||
create policy "Profiles can be created by any user"
|
||||
on profiles
|
||||
for insert
|
||||
to authenticated
|
||||
with check ( true );
|
||||
|
||||
create policy "Profiles can be deleted by any user"
|
||||
on profiles
|
||||
for delete
|
||||
to authenticated
|
||||
using ( true );
|
||||
\`\`\`
|
||||
|
||||
## Helper functions
|
||||
|
||||
Supabase provides some helper functions that make it easier to write Policies.
|
||||
|
||||
### \`auth.uid()\`
|
||||
|
||||
Returns the ID of the user making the request.
|
||||
|
||||
### \`auth.jwt()\`
|
||||
|
||||
Returns the JWT of the user making the request. Anything that you store in the user's \`raw_app_meta_data\` column or the \`raw_user_meta_data\` column will be accessible using this function. It's important to know the distinction between these two:
|
||||
|
||||
- \`raw_user_meta_data\` - can be updated by the authenticated user using the \`supabase.auth.update()\` function. It is not a good place to store authorization data.
|
||||
- \`raw_app_meta_data\` - cannot be updated by the user, so it's a good place to store authorization data.
|
||||
|
||||
The \`auth.jwt()\` function is extremely versatile. For example, if you store some team data inside \`app_metadata\`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "User is in team"
|
||||
on my_table
|
||||
to authenticated
|
||||
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
|
||||
\`\`\`
|
||||
|
||||
### MFA
|
||||
|
||||
The \`auth.jwt()\` function can be used to check for [Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Restrict updates."
|
||||
on profiles
|
||||
as restrictive
|
||||
for update
|
||||
to authenticated using (
|
||||
(select auth.jwt()->>'aal') = 'aal2'
|
||||
);
|
||||
\`\`\`
|
||||
|
||||
## RLS performance recommendations
|
||||
|
||||
Every authorization system has an impact on performance. While row level security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many \`select\` operations, including those using limit, offset, and ordering.
|
||||
|
||||
Based on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS:
|
||||
|
||||
### Add indexes
|
||||
|
||||
Make sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access their own records" on test_table
|
||||
to authenticated
|
||||
using ( (select auth.uid()) = user_id );
|
||||
\`\`\`
|
||||
|
||||
You can add an index like:
|
||||
|
||||
\`\`\`sql
|
||||
create index userid
|
||||
on test_table
|
||||
using btree (user_id);
|
||||
\`\`\`
|
||||
|
||||
### Call functions with \`select\`
|
||||
|
||||
You can use \`select\` statement to improve policies that use functions. For example, instead of this:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access their own records" on test_table
|
||||
to authenticated
|
||||
using ( auth.uid() = user_id );
|
||||
\`\`\`
|
||||
|
||||
You can do:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access their own records" on test_table
|
||||
to authenticated
|
||||
using ( (select auth.uid()) = user_id );
|
||||
\`\`\`
|
||||
|
||||
This method works well for JWT functions like \`auth.uid()\` and \`auth.jwt()\` as well as \`security definer\` Functions. Wrapping the function causes an \`initPlan\` to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row.
|
||||
|
||||
Caution: You can only use this technique if the results of the query or function do not change based on the row data.
|
||||
|
||||
### Minimize joins
|
||||
|
||||
You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an \`IN\` or \`ANY\` operation in your filter.
|
||||
|
||||
For example, this is an example of a slow policy which joins the source \`test_table\` to the target \`team_user\`:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access records belonging to their teams" on test_table
|
||||
to authenticated
|
||||
using (
|
||||
(select auth.uid()) in (
|
||||
select user_id
|
||||
from team_user
|
||||
where team_user.team_id = team_id -- joins to the source "test_table.team_id"
|
||||
)
|
||||
);
|
||||
\`\`\`
|
||||
|
||||
We can rewrite this to avoid this join, and instead select the filter criteria into a set:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access records belonging to their teams" on test_table
|
||||
to authenticated
|
||||
using (
|
||||
team_id in (
|
||||
select team_id
|
||||
from team_user
|
||||
where user_id = (select auth.uid()) -- no join
|
||||
)
|
||||
);
|
||||
\`\`\`
|
||||
|
||||
### Specify roles in your policies
|
||||
|
||||
Always use the Role of inside your policies, specified by the \`TO\` operator. For example, instead of this query:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access their own records" on rls_test
|
||||
using ( auth.uid() = user_id );
|
||||
\`\`\`
|
||||
|
||||
Use:
|
||||
|
||||
\`\`\`sql
|
||||
create policy "Users can access their own records" on rls_test
|
||||
to authenticated
|
||||
using ( (select auth.uid()) = user_id );
|
||||
\`\`\`
|
||||
|
||||
This prevents the policy \`( (select auth.uid()) = user_id )\` from running for any \`anon\` users, since the execution stops at the \`to authenticated\` step.
|
||||
|
||||
${data.length > 0 ? `Here are my existing policies: ${formattedPolicies}` : ''}
|
||||
`
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition } from 'lodash'
|
||||
import { ExternalLink, Search } from 'lucide-react'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useIsAssistantV2Enabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { AIPolicyEditorPanel } from 'components/interfaces/Auth/Policies/AIPolicyEditorPanel'
|
||||
import Policies from 'components/interfaces/Auth/Policies/Policies'
|
||||
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
@@ -18,9 +20,9 @@ import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
|
||||
import { useUrlState } from 'hooks/ui/useUrlState'
|
||||
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import { Button, Input } from 'ui'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import { Input } from 'ui'
|
||||
|
||||
/**
|
||||
* Filter tables by table name and policy name
|
||||
@@ -64,6 +66,8 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
}>()
|
||||
const { schema = 'public', search: searchString = '' } = params
|
||||
const { project } = useProjectContext()
|
||||
const isAssistantV2Enabled = useIsAssistantV2Enabled()
|
||||
const { setAiAssistantPanel } = useAppStateSnapshot()
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<string>()
|
||||
const [showPolicyAiEditor, setShowPolicyAiEditor] = useState(false)
|
||||
@@ -99,9 +103,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
|
||||
const filteredTables = onFilterTables(tables ?? [], policies ?? [], searchString)
|
||||
const canReadPolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_READ, 'policies')
|
||||
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
|
||||
const isPermissionsLoaded = usePermissionsLoaded()
|
||||
const schemaHasNoTables = (tables ?? []).length === 0
|
||||
|
||||
if (isPermissionsLoaded && !canReadPolicies) {
|
||||
return <NoPermission isFullPage resourceText="view this project's RLS policies" />
|
||||
@@ -113,8 +115,8 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<SchemaSelector
|
||||
className="w-[260px]"
|
||||
size="small"
|
||||
className="w-[180px]"
|
||||
size="tiny"
|
||||
showError={false}
|
||||
selectedSchemaName={schema}
|
||||
onSelectSchema={(schema) => {
|
||||
@@ -122,9 +124,9 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
size="small"
|
||||
size="tiny"
|
||||
placeholder="Filter tables and policies"
|
||||
className="block w-64 text-sm placeholder-border-muted"
|
||||
className="block w-52 text-sm placeholder-border-muted"
|
||||
value={searchString || ''}
|
||||
onChange={(e) => {
|
||||
const str = e.target.value
|
||||
@@ -148,8 +150,8 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
|
||||
hasTables={tables.length > 0}
|
||||
isLocked={isLocked}
|
||||
onSelectCreatePolicy={(table: string) => {
|
||||
setShowPolicyAiEditor(true)
|
||||
setSelectedTable(table)
|
||||
setShowPolicyAiEditor(true)
|
||||
}}
|
||||
onSelectEditPolicy={(policy) => {
|
||||
setSelectedPolicyToEdit(policy)
|
||||
|
||||
@@ -36,7 +36,7 @@ const IndexesPage: NextPageWithLayout = () => {
|
||||
className="no-underline"
|
||||
href="https://supabase.com/docs/guides/database/extensions/index_advisor"
|
||||
>
|
||||
Optimization with index_advisor
|
||||
Index Advisor
|
||||
</a>
|
||||
</Button>
|
||||
</ScaffoldSectionDetail>
|
||||
|
||||
@@ -17,7 +17,7 @@ const DatabaseEnumeratedTypes: NextPageWithLayout = () => {
|
||||
return (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionContent>
|
||||
<ScaffoldSectionContent className="!col-span-12">
|
||||
<FormHeader
|
||||
className="!mb-0"
|
||||
title="Database Enumerated Types"
|
||||
|
||||
BIN
apps/studio/public/img/previews/assistant-v2.png
Normal file
BIN
apps/studio/public/img/previews/assistant-v2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 306 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 269 KiB |
@@ -3,6 +3,7 @@ import { proxy, snapshot, useSnapshot } from 'valtio'
|
||||
import { SupportedAssistantEntities } from 'components/ui/AIAssistantPanel/AIAssistant.types'
|
||||
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
|
||||
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
|
||||
import type { Message as MessageType } from 'ai/react'
|
||||
|
||||
const EMPTY_DASHBOARD_HISTORY: {
|
||||
sql?: string
|
||||
@@ -19,8 +20,17 @@ export type CommonDatabaseEntity = {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export type SuggestionsType = {
|
||||
title: string
|
||||
prompts: string[]
|
||||
}
|
||||
|
||||
type AiAssistantPanelType = {
|
||||
open: boolean
|
||||
messages?: MessageType[] | undefined
|
||||
initialInput: string
|
||||
sqlSnippets?: string[]
|
||||
suggestions?: SuggestionsType
|
||||
editor?: SupportedAssistantEntities | null
|
||||
// Raw string content for the monaco editor, currently used to retain where the user left off when toggling off the panel
|
||||
content?: string
|
||||
@@ -29,6 +39,18 @@ type AiAssistantPanelType = {
|
||||
tables: { schema: string; name: string }[]
|
||||
}
|
||||
|
||||
const INITIAL_AI_ASSISTANT: AiAssistantPanelType = {
|
||||
open: false,
|
||||
messages: undefined,
|
||||
sqlSnippets: undefined,
|
||||
initialInput: '',
|
||||
suggestions: undefined,
|
||||
editor: null,
|
||||
content: '',
|
||||
entity: undefined,
|
||||
tables: [],
|
||||
}
|
||||
|
||||
export const appState = proxy({
|
||||
// [Joshen] Last visited "entity" for any page that we wanna track
|
||||
dashboardHistory: EMPTY_DASHBOARD_HISTORY,
|
||||
@@ -108,13 +130,14 @@ export const appState = proxy({
|
||||
appState.navigationPanelJustClosed = value
|
||||
},
|
||||
|
||||
aiAssistantPanel: {
|
||||
open: false,
|
||||
editor: null,
|
||||
content: '',
|
||||
entity: undefined,
|
||||
tables: [],
|
||||
} as AiAssistantPanelType,
|
||||
resetAiAssistantPanel: () => {
|
||||
appState.aiAssistantPanel = {
|
||||
...INITIAL_AI_ASSISTANT,
|
||||
open: appState.aiAssistantPanel.open,
|
||||
}
|
||||
},
|
||||
|
||||
aiAssistantPanel: INITIAL_AI_ASSISTANT as AiAssistantPanelType,
|
||||
setAiAssistantPanel: (value: Partial<AiAssistantPanelType>) => {
|
||||
const hasEntityChanged = value.entity?.id !== appState.aiAssistantPanel.entity?.id
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Snippet, SnippetFolder, SnippetFolderResponse } from 'data/content/sql-
|
||||
import { SqlSnippet } from 'data/content/sql-snippets-query'
|
||||
import { getQueryClient } from 'data/query-client'
|
||||
import { getContentById } from 'data/content/content-id-query'
|
||||
import { DiffType } from 'components/interfaces/SQLEditor/SQLEditor.types'
|
||||
|
||||
export type StateSnippetFolder = {
|
||||
projectRef: string
|
||||
@@ -76,6 +77,9 @@ export const sqlEditorState = proxy({
|
||||
// For handling renaming folder failed
|
||||
lastUpdatedFolderName: '',
|
||||
|
||||
// For Assistant to render diffing into the editor
|
||||
diffContent: undefined as undefined | { sql: string; diffType: DiffType },
|
||||
|
||||
get allFolderNames() {
|
||||
return Object.values(sqlEditorState.folders).map((x) => x.folder.name)
|
||||
},
|
||||
@@ -121,6 +125,9 @@ export const sqlEditorState = proxy({
|
||||
})
|
||||
},
|
||||
|
||||
setDiffContent: (sql: string, diffType: DiffType) =>
|
||||
(sqlEditorState.diffContent = { sql, diffType }),
|
||||
|
||||
setOrder: (value: 'name' | 'inserted_at') => (sqlEditorState.order = value),
|
||||
|
||||
setPrivateSnippetCount: ({ projectRef, value }: { projectRef: string; value: number }) => {
|
||||
|
||||
686
package-lock.json
generated
686
package-lock.json
generated
@@ -1782,6 +1782,7 @@
|
||||
"apps/studio": {
|
||||
"version": "0.0.9",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
"@dagrejs/dagre": "^1.0.4",
|
||||
"@graphiql/react": "^0.19.4",
|
||||
"@graphiql/toolkit": "^0.9.1",
|
||||
@@ -1805,10 +1806,11 @@
|
||||
"@tanstack/react-query": "4.35.7",
|
||||
"@tanstack/react-query-devtools": "4.35.7",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"@vercel/flags": "^2.6.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@zip.js/zip.js": "^2.7.29",
|
||||
"ai": "^2.2.31",
|
||||
"ai": "^3.4.33",
|
||||
"ai-commands": "*",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
"common": "*",
|
||||
@@ -1829,6 +1831,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.436.0",
|
||||
"markdown-table": "^3.0.3",
|
||||
"markdown-to-jsx": "^7.5.0",
|
||||
"memoize-one": "^5.0.1",
|
||||
"mime-db": "^1.53.0",
|
||||
"mobx": "^6.10.2",
|
||||
@@ -1884,6 +1887,7 @@
|
||||
"yup": "^1.4.0",
|
||||
"yup-password": "^0.3.0",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^5.0.1",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1960,6 +1964,15 @@
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/@storybook/addon-backgrounds": {
|
||||
"version": "7.6.17",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-7.6.17.tgz",
|
||||
@@ -2810,6 +2823,76 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/ai": {
|
||||
"version": "3.4.33",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
|
||||
"integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/react": "0.0.70",
|
||||
"@ai-sdk/solid": "0.0.54",
|
||||
"@ai-sdk/svelte": "0.0.57",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"@ai-sdk/vue": "0.0.59",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"eventsource-parser": "1.1.2",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsondiffpatch": "0.6.0",
|
||||
"secure-json-parse": "^2.7.0",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openai": "^4.42.0",
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"sswr": "^2.1.0",
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openai": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"sswr": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/ai/node_modules/@ai-sdk/vue": {
|
||||
"version": "0.0.59",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
|
||||
"integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"swrv": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@@ -2845,6 +2928,15 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/eventsource-parser": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
|
||||
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/framer-motion": {
|
||||
"version": "11.3.31",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.31.tgz",
|
||||
@@ -2947,6 +3039,20 @@
|
||||
"sql-formatter": "bin/sql-formatter-cli.cjs"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/sswr": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
|
||||
"integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"swrev": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"apps/studio/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -2965,6 +3071,35 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true
|
||||
},
|
||||
"apps/studio/node_modules/zustand": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz",
|
||||
"integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps/www": {
|
||||
"version": "0.0.3",
|
||||
"license": "MIT",
|
||||
@@ -3625,6 +3760,202 @@
|
||||
"integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@ai-sdk/openai": {
|
||||
"version": "0.0.72",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-0.0.72.tgz",
|
||||
"integrity": "sha512-IKsgxIt6KJGkEHyMp975xW5VPmetwhI8g9H6dDmwvemBB41IRQa78YMNttiJqPcgmrZX2QfErOICv1gQvZ1gZg==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"@ai-sdk/provider-utils": "1.0.22"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider": {
|
||||
"version": "0.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-0.0.26.tgz",
|
||||
"integrity": "sha512-dQkfBDs2lTYpKM8389oopPdQgIU007GQyCbuPPrV+K6MtSII3HBfE0stUIMXUb44L+LK1t6GXPP7wjSzjO6uKg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"json-schema": "^0.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils": {
|
||||
"version": "1.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-1.0.22.tgz",
|
||||
"integrity": "sha512-YHK2rpj++wnLVc9vPGzGFP3Pjeld2MwhKinetA0zKXOoHAT/Jit5O8kZsxcSlJPu9wvcGT1UGZEjZrtO7PfFOQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"nanoid": "^3.3.7",
|
||||
"secure-json-parse": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils/node_modules/eventsource-parser": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
|
||||
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/provider-utils/node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react": {
|
||||
"version": "0.0.70",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-0.0.70.tgz",
|
||||
"integrity": "sha512-GnwbtjW4/4z7MleLiW+TOZC2M29eCg1tOUpuEiYFMmFNZK8mkrqM0PFZMo6UsYeUYMWqEOOcPOU9OQVJMJh7IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"swr": "^2.2.5",
|
||||
"throttleit": "2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/react/node_modules/swr": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
|
||||
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"client-only": "^0.0.1",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/solid": {
|
||||
"version": "0.0.54",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/solid/-/solid-0.0.54.tgz",
|
||||
"integrity": "sha512-96KWTVK+opdFeRubqrgaJXoNiDP89gNxFRWUp0PJOotZW816AbhUf4EnDjBjXTLjXL1n0h8tGSE9sZsRkj9wQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.7.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"solid-js": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/svelte": {
|
||||
"version": "0.0.57",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/svelte/-/svelte-0.0.57.tgz",
|
||||
"integrity": "sha512-SyF9ItIR9ALP9yDNAD+2/5Vl1IT6kchgyDH8xkmhysfJI6WrvJbtO1wdQ0nylvPLcsPoYu+cAlz1krU4lFHcYw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"sswr": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"svelte": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/svelte/node_modules/sswr": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
|
||||
"integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"swrev": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ai-sdk/ui-utils": {
|
||||
"version": "0.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/ui-utils/-/ui-utils-0.0.50.tgz",
|
||||
"integrity": "sha512-Z5QYJVW+5XpSaJ4jYCCAVG7zIAuKOOdikhgpksneNmKvx61ACFaf98pmOd+xnjahl0pIlc/QIe6O4yVaJ1sEaw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"json-schema": "^0.4.0",
|
||||
"secure-json-parse": "^2.7.0",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
"license": "MIT",
|
||||
@@ -16718,6 +17049,12 @@
|
||||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/doctrine": {
|
||||
"version": "0.0.3",
|
||||
"dev": true,
|
||||
@@ -17522,6 +17859,24 @@
|
||||
"version": "1.2.0",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@use-gesture/core": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/react": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@use-gesture/core": "10.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/flags": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/flags/-/flags-2.6.0.tgz",
|
||||
@@ -18201,43 +18556,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ai": {
|
||||
"version": "2.2.31",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-2.2.31.tgz",
|
||||
"integrity": "sha512-WQH13RxP+RYo9IE/FX8foNQh9gcKO/dhl9OGy5JL2bHJVBlnugPmH2CYJWaRt+mvjXHaU8txB+jzGo/fbtH2HA==",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "1.0.0",
|
||||
"nanoid": "3.3.6",
|
||||
"solid-swr-store": "0.10.7",
|
||||
"sswr": "2.0.0",
|
||||
"swr": "2.2.0",
|
||||
"swr-store": "0.10.6",
|
||||
"swrv": "1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.2.0",
|
||||
"solid-js": "^1.7.7",
|
||||
"svelte": "^3.0.0 || ^4.0.0",
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"solid-js": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ai-commands": {
|
||||
"resolved": "packages/ai-commands",
|
||||
"link": true
|
||||
@@ -19066,9 +19384,6 @@
|
||||
"version": "1.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base-64": {
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"funding": [
|
||||
@@ -19524,13 +19839,6 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/charenc": {
|
||||
"version": "0.0.2",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz",
|
||||
@@ -20336,13 +20644,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/crypt": {
|
||||
"version": "0.0.2",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-random-string": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
|
||||
@@ -21099,6 +21400,12 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/diff-sequences": {
|
||||
"version": "29.6.3",
|
||||
"license": "MIT",
|
||||
@@ -21106,14 +21413,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/digest-fetch": {
|
||||
"version": "1.3.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"md5": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dir-glob": {
|
||||
"version": "3.0.1",
|
||||
"license": "MIT",
|
||||
@@ -22873,14 +23172,6 @@
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz",
|
||||
"integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
@@ -28148,6 +28439,12 @@
|
||||
"foreach": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"license": "MIT"
|
||||
@@ -28183,6 +28480,35 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsondiffpatch": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz",
|
||||
"integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"chalk": "^5.3.0",
|
||||
"diff-match-patch": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"jsondiffpatch": "bin/jsondiffpatch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsondiffpatch/node_modules/chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonfile": {
|
||||
"version": "6.1.0",
|
||||
"dev": true,
|
||||
@@ -28911,10 +29237,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-to-jsx": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz",
|
||||
"integrity": "sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg==",
|
||||
"dev": true,
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.5.0.tgz",
|
||||
"integrity": "sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
@@ -28981,15 +29307,6 @@
|
||||
"version": "1.0.4",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/md5": {
|
||||
"version": "2.3.0",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "~1.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast": {
|
||||
"version": "3.0.0",
|
||||
"license": "MIT"
|
||||
@@ -34419,22 +34736,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "4.26.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.26.1.tgz",
|
||||
"integrity": "sha512-DvWbjhWbappsFRatOWmu4Dp1/Q4RG9oOz6CfOSjy0/Drb8G+5iAiqWAO4PfpGIkhOOKtvvNfQri2SItl+U7LhQ==",
|
||||
"version": "4.71.1",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-4.71.1.tgz",
|
||||
"integrity": "sha512-C6JNMaQ1eijM0lrjiRUL3MgThVP5RdwNAghpbJFdW0t11LzmyqON8Eh8MuUuEZ+CeD6bgYl2Fkn2BoptVxv9Ug==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.4",
|
||||
"abort-controller": "^3.0.0",
|
||||
"agentkeepalive": "^4.2.1",
|
||||
"digest-fetch": "^1.3.0",
|
||||
"form-data-encoder": "1.7.2",
|
||||
"formdata-node": "^4.3.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"web-streams-polyfill": "^3.2.1"
|
||||
"node-fetch": "^2.6.7"
|
||||
},
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/openai/node_modules/@types/node": {
|
||||
@@ -39240,6 +39564,12 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
|
||||
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"license": "ISC",
|
||||
@@ -39270,6 +39600,7 @@
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-0.14.1.tgz",
|
||||
"integrity": "sha512-ZlC9y1KVDhZFdEHLYZup1RjKDutyX1tt3ffOauqRbRURa2vRr2NU/bHuVEuNEqR3zE2uCU3WM6LqH6Oinc3tWg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -39713,24 +40044,13 @@
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.6.tgz",
|
||||
"integrity": "sha512-yiH6ZfBBZ3xj/aU/PBpVKB+8r8WWp100NGF7k/Z0IrK9Y8Lv0jwvFiJY1cHdc6Tj7GqXArKnMBabM0m1k+LzkA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "^0.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-swr-store": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/solid-swr-store/-/solid-swr-store-0.10.7.tgz",
|
||||
"integrity": "sha512-A6d68aJmRP471aWqKKPE2tpgOiR5fH4qXQNfKIec+Vap+MGQm3tvXlT8n0I8UgJSlNAsSAUuw2VTviH2h3Vv5g==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.2",
|
||||
"swr-store": "^0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/sonner": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz",
|
||||
@@ -39886,17 +40206,6 @@
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sswr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.0.0.tgz",
|
||||
"integrity": "sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==",
|
||||
"dependencies": {
|
||||
"swrev": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-generator": {
|
||||
"version": "2.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
|
||||
@@ -40932,28 +41241,6 @@
|
||||
"node": ">= 4.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz",
|
||||
"integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swr-store": {
|
||||
"version": "0.10.6",
|
||||
"resolved": "https://registry.npmjs.org/swr-store/-/swr-store-0.10.6.tgz",
|
||||
"integrity": "sha512-xPjB1hARSiRaNNlUQvWSVrG5SirCjk2TmaUyzzvk69SZQan9hCJqw/5rG9iL7xElHU784GxRPISClq4488/XVw==",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/swrev": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/swrev/-/swrev-4.0.0.tgz",
|
||||
@@ -41304,6 +41591,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||
"integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/through2": {
|
||||
"version": "2.0.5",
|
||||
"license": "MIT",
|
||||
@@ -44055,6 +44354,7 @@
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -44718,13 +45018,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.23.5",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz",
|
||||
"integrity": "sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.3"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "3.7.2",
|
||||
"license": "MIT",
|
||||
@@ -44758,7 +45068,7 @@
|
||||
"dependencies": {
|
||||
"@serafin/schema-builder": "^0.18.5",
|
||||
"@supabase/supabase-js": "*",
|
||||
"ai": "^2.2.29",
|
||||
"ai": "^3.4.33",
|
||||
"common-tags": "^1.8.2",
|
||||
"config": "*",
|
||||
"js-tiktoken": "^1.0.10",
|
||||
@@ -44766,6 +45076,7 @@
|
||||
"openai": "^4.26.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
"@babel/core": "^7.23.6",
|
||||
"@babel/preset-env": "^7.23.6",
|
||||
"@jest/globals": "^29.7.0",
|
||||
@@ -44784,6 +45095,82 @@
|
||||
"typescript": "~5.5.0"
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/ai": {
|
||||
"version": "3.4.33",
|
||||
"resolved": "https://registry.npmjs.org/ai/-/ai-3.4.33.tgz",
|
||||
"integrity": "sha512-plBlrVZKwPoRTmM8+D1sJac9Bq8eaa2jiZlHLZIWekKWI1yMWYZvCCEezY9ASPwRhULYDJB2VhKOBUUeg3S5JQ==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider": "0.0.26",
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/react": "0.0.70",
|
||||
"@ai-sdk/solid": "0.0.54",
|
||||
"@ai-sdk/svelte": "0.0.57",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"@ai-sdk/vue": "0.0.59",
|
||||
"@opentelemetry/api": "1.9.0",
|
||||
"eventsource-parser": "1.1.2",
|
||||
"json-schema": "^0.4.0",
|
||||
"jsondiffpatch": "0.6.0",
|
||||
"secure-json-parse": "^2.7.0",
|
||||
"zod-to-json-schema": "^3.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openai": "^4.42.0",
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"sswr": "^2.1.0",
|
||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"openai": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"sswr": {
|
||||
"optional": true
|
||||
},
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/ai/node_modules/@ai-sdk/vue": {
|
||||
"version": "0.0.59",
|
||||
"resolved": "https://registry.npmjs.org/@ai-sdk/vue/-/vue-0.0.59.tgz",
|
||||
"integrity": "sha512-+ofYlnqdc8c4F6tM0IKF0+7NagZRAiqBJpGDJ+6EYhDW8FHLUP/JFBgu32SjxSxC6IKFZxEnl68ZoP/Z38EMlw==",
|
||||
"dependencies": {
|
||||
"@ai-sdk/provider-utils": "1.0.22",
|
||||
"@ai-sdk/ui-utils": "0.0.50",
|
||||
"swrv": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -44802,6 +45189,14 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/eventsource-parser": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
|
||||
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
|
||||
"engines": {
|
||||
"node": ">=14.18"
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/sql-formatter": {
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.0.2.tgz",
|
||||
@@ -44816,6 +45211,19 @@
|
||||
"sql-formatter": "bin/sql-formatter-cli.cjs"
|
||||
}
|
||||
},
|
||||
"packages/ai-commands/node_modules/sswr": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sswr/-/sswr-2.1.0.tgz",
|
||||
"integrity": "sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"swrev": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^4.0.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"packages/api-types": {
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@serafin/schema-builder": "^0.18.5",
|
||||
"@supabase/supabase-js": "*",
|
||||
"ai": "^2.2.29",
|
||||
"ai": "^3.4.33",
|
||||
"common-tags": "^1.8.2",
|
||||
"config": "*",
|
||||
"js-tiktoken": "^1.0.10",
|
||||
@@ -22,6 +22,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"api-types": "*",
|
||||
"@ai-sdk/openai": "^0.0.72",
|
||||
"@babel/core": "^7.23.6",
|
||||
"@babel/preset-env": "^7.23.6",
|
||||
"@jest/globals": "^29.7.0",
|
||||
|
||||
@@ -28,10 +28,11 @@ export async function collectStream<R extends BufferSource>(stream: ReadableStre
|
||||
let content = ''
|
||||
|
||||
for await (const chunk of stream.pipeThrough(textDecoderStream)) {
|
||||
content += chunk
|
||||
const text = chunk.split('0:')[1]
|
||||
content += text.slice(1, text.length - 2)
|
||||
}
|
||||
|
||||
return content
|
||||
return content.replaceAll('\\n', '\n').replaceAll('\\"', '"')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,7 +174,7 @@ function Input({
|
||||
ref={inputRef}
|
||||
type={type}
|
||||
value={reveal && hidden ? HIDDEN_PLACEHOLDER : value}
|
||||
className={cn(inputClasses)}
|
||||
className={cn(inputClasses, size === 'tiny' && 'pl-8')}
|
||||
{...props}
|
||||
/>
|
||||
{icon && <InputIconContainer size={size} icon={icon} className={iconContainerClassName} />}
|
||||
|
||||
Reference in New Issue
Block a user