diff --git a/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx
new file mode 100644
index 0000000000000..fd475eb5acb85
--- /dev/null
+++ b/apps/studio/components/interfaces/Integrations/Queues/SingleQueue/QueueSettings.tsx
@@ -0,0 +1,302 @@
+import { isEqual } from 'lodash'
+import { Settings } from 'lucide-react'
+import { useEffect, useState } from 'react'
+import { toast } from 'sonner'
+
+import { useParams } from 'common'
+import AlertError from 'components/ui/AlertError'
+import { ButtonTooltip } from 'components/ui/ButtonTooltip'
+import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
+import {
+ TablePrivilegesGrant,
+ useTablePrivilegesGrantMutation,
+} from 'data/privileges/table-privileges-grant-mutation'
+import { useTablePrivilegesQuery } from 'data/privileges/table-privileges-query'
+import {
+ TablePrivilegesRevoke,
+ useTablePrivilegesRevokeMutation,
+} from 'data/privileges/table-privileges-revoke-mutation'
+import { useTablesQuery } from 'data/tables/tables-query'
+import { useSelectedProject } from 'hooks/misc/useSelectedProject'
+import {
+ Button,
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetSection,
+ SheetTitle,
+ SheetTrigger,
+ Switch,
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from 'ui'
+import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
+
+const ACTIONS = ['select', 'insert', 'update', 'delete']
+type Privileges = { select?: boolean; insert?: boolean; update?: boolean; delete?: boolean }
+
+interface QueueSettingsProps {}
+
+export const QueueSettings = ({}: QueueSettingsProps) => {
+ const { childId: name } = useParams()
+ const project = useSelectedProject()
+
+ const [open, setOpen] = useState(false)
+ const [isSaving, setIsSaving] = useState(false)
+ const [privileges, setPrivileges] = useState<{ [key: string]: Privileges }>({})
+
+ const { data, error, isLoading, isSuccess, isError } = useDatabaseRolesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const roles = (data ?? []).sort((a, b) => a.name.localeCompare(b.name))
+
+ const { data: queueTables } = useTablesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ schema: 'pgmq',
+ })
+ const queueTable = queueTables?.find((x) => x.name === `q_${name}`)
+ const archiveTable = queueTables?.find((x) => x.name === `a_${name}`)
+
+ const { data: allTablePrivileges, isSuccess: isSuccessPrivileges } = useTablePrivilegesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const queuePrivileges = allTablePrivileges?.find(
+ (x) => x.schema === 'pgmq' && x.name === `q_${name}`
+ )
+
+ const { mutateAsync: grantPrivilege } = useTablePrivilegesGrantMutation()
+ const { mutateAsync: revokePrivilege } = useTablePrivilegesRevokeMutation()
+
+ const onTogglePrivilege = (role: string, action: string, value: boolean) => {
+ const updatedPrivileges = { ...privileges, [role]: { ...privileges[role], [action]: value } }
+ setPrivileges(updatedPrivileges)
+ }
+
+ const onSaveConfiguration = async () => {
+ if (!project) return console.error('Project is required')
+ if (!queueTable) return console.error('Unable to find queue table')
+ if (!archiveTable) return console.error('Unable to find archive table')
+
+ setIsSaving(true)
+ const revoke: { role: string; action: string }[] = []
+ const grant: { role: string; action: string }[] = []
+
+ Object.entries(privileges).forEach(([role, p]) => {
+ const originalRolePrivileges = queuePrivileges?.privileges.filter((x) => x.grantee === role)
+ Object.entries(p).forEach(([action, value]) => {
+ const originalValue = !!originalRolePrivileges?.find(
+ (x) => x.privilege_type.toLowerCase() === action
+ )
+ if (value !== originalValue) {
+ if (value) grant.push({ role, action })
+ else revoke.push({ role, action })
+ }
+ })
+ })
+
+ const rolesBeingGrantedPerms = [...new Set(grant.map((x) => x.role))]
+ const rolesBeingRevokedPerms = [...new Set(revoke.map((x) => x.role))]
+
+ const rolesNoLongerHavingPerms = rolesBeingRevokedPerms.filter((x) => {
+ const existingPrivileges = queuePrivileges?.privileges
+ .filter((y) => x === y.grantee)
+ .map((y) => y.privilege_type)
+ const privilegesGettingRevoked = revoke
+ .filter((y) => y.role === x)
+ .map((y) => y.action.toUpperCase())
+ const privilegesGettingGranted = grant.filter((y) => y.role === x)
+ return (
+ privilegesGettingGranted.length === 0 &&
+ isEqual(existingPrivileges, privilegesGettingRevoked)
+ )
+ })
+
+ try {
+ await Promise.all([
+ ...(revoke.length > 0
+ ? [
+ revokePrivilege({
+ projectRef: project.ref,
+ connectionString: project.connectionString,
+ revokes: revoke.map((x) => ({
+ grantee: x.role,
+ privilege_type: x.action.toUpperCase(),
+ relation_id: queueTable.id,
+ })) as TablePrivilegesRevoke[],
+ }),
+ ]
+ : []),
+ // Revoke select + insert on archive table only if role no longer has ANY perms on the queue table
+ ...(rolesNoLongerHavingPerms.length > 0
+ ? [
+ revokePrivilege({
+ projectRef: project.ref,
+ connectionString: project.connectionString,
+ revokes: [
+ ...rolesNoLongerHavingPerms.map((x) => ({
+ grantee: x,
+ privilege_type: 'INSERT' as 'INSERT',
+ relation_id: archiveTable.id,
+ })),
+ ...rolesNoLongerHavingPerms.map((x) => ({
+ grantee: x,
+ privilege_type: 'SELECT' as 'SELECT',
+ relation_id: archiveTable.id,
+ })),
+ ],
+ }),
+ ]
+ : []),
+ ...(grant.length > 0
+ ? [
+ grantPrivilege({
+ projectRef: project.ref,
+ connectionString: project.connectionString,
+ grants: grant.map((x) => ({
+ grantee: x.role,
+ privilege_type: x.action.toUpperCase(),
+ relation_id: queueTable.id,
+ })) as TablePrivilegesGrant[],
+ }),
+ // Just grant select + insert on archive table as long as we're granting any perms to the queue table for the role
+ grantPrivilege({
+ projectRef: project.ref,
+ connectionString: project.connectionString,
+ grants: [
+ ...rolesBeingGrantedPerms.map((x) => ({
+ grantee: x,
+ privilege_type: 'INSERT' as 'INSERT',
+ relation_id: archiveTable.id,
+ })),
+ ...rolesBeingGrantedPerms.map((x) => ({
+ grantee: x,
+ privilege_type: 'SELECT' as 'SELECT',
+ relation_id: archiveTable.id,
+ })),
+ ],
+ }),
+ ]
+ : []),
+ ])
+ toast.success('Successfully updated permissions')
+ setOpen(false)
+ } catch (error: any) {
+ toast.error(`Failed to update permissions: ${error.message}`)
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ useEffect(() => {
+ if (open && isSuccessPrivileges && queuePrivileges) {
+ const initialState = queuePrivileges.privileges.reduce((a, b) => {
+ return {
+ ...a,
+ [b.grantee]: { ...(a as any)[b.grantee], [b.privilege_type.toLowerCase()]: true },
+ }
+ }, {})
+ setPrivileges(initialState)
+ }
+ }, [open, isSuccessPrivileges])
+
+ return (
+
+
+ }
+ title="Settings"
+ tooltip={{ content: { side: 'bottom', text: 'Queue settings' } }}
+ />
+
+
+
+ Manage queue permissions on {name}
+
+ Configure permissions for each role to grant access to the relevant actions on the queue
+
+
+
+
+
+
+
+ Role
+ {ACTIONS.map((x) => (
+
+ {x}
+
+ ))}
+
+
+
+ {isLoading && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {isError && (
+
+
+
+
+
+ )}
+ {isSuccess &&
+ (roles ?? []).map((role) => {
+ return (
+
+ {role.name}
+ {ACTIONS.map((x) => (
+
+ onTogglePrivilege(role.name, x, value)}
+ />
+
+ ))}
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx b/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx
index 07a77e633d8fb..0fbf052477c17 100644
--- a/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/EmptyState.tsx
@@ -6,7 +6,7 @@ import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-que
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
export interface EmptyStateProps {}
@@ -14,7 +14,7 @@ export interface EmptyStateProps {}
const EmptyState = ({}: EmptyStateProps) => {
const snap = useTableEditorStateSnapshot()
const { selectedSchema } = useQuerySchemaState()
- const isProtectedSchema = EXCLUDED_SCHEMAS.includes(selectedSchema)
+ const isProtectedSchema = PROTECTED_SCHEMAS.includes(selectedSchema)
const canCreateTables =
useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') && !isProtectedSchema
diff --git a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
index 3562f5a8f2e8a..eaad8f754cf50 100644
--- a/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
@@ -1,11 +1,10 @@
-import type { PostgresTable } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { useParams } from 'common'
import { Lock, MousePointer2, PlusCircle, Unlock } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
+import { useParams } from 'common'
import { useTrackedState } from 'components/grid/store/Store'
import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
@@ -25,7 +24,7 @@ import {
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
-import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import {
Button,
PopoverContent_Shadcn_,
@@ -60,7 +59,7 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
const isMaterializedView = isTableLikeMaterializedView(table)
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
- const isLocked = EXCLUDED_SCHEMAS.includes(table.schema)
+ const isLocked = PROTECTED_SCHEMAS.includes(table.schema)
const { mutate: updateTable } = useTableUpdateMutation({
onError: (error) => {
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx
index d4a9cab8f335b..408f3ecce1f5e 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.tsx
@@ -17,7 +17,7 @@ import {
useForeignKeyConstraintsQuery,
} from 'data/database/foreign-key-constraints-query'
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
-import { EXCLUDED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
import type { Dictionary } from 'types'
import { Button, Checkbox, Input, SidePanel, Toggle } from 'ui'
import ActionBar from '../ActionBar'
@@ -84,7 +84,7 @@ const ColumnEditor = ({
connectionString: project?.connectionString,
})
const enumTypes = (types ?? []).filter(
- (type) => !EXCLUDED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
+ (type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
)
const { data: constraints } = useTableConstraintsQuery({
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
index cc792afd0310e..d4058972d45cb 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
@@ -21,7 +21,7 @@ import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useUrlState } from 'hooks/ui/useUrlState'
-import { EXCLUDED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import { Admonition } from 'ui-patterns'
import ActionBar from '../ActionBar'
@@ -97,7 +97,7 @@ const TableEditor = ({
connectionString: project?.connectionString,
})
const enumTypes = (types ?? []).filter(
- (type) => !EXCLUDED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
+ (type) => !PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS.includes(type.schema)
)
const { data: publications } = useDatabasePublicationsQuery({
diff --git a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
index ff4ef5d7127ef..288e602563568 100644
--- a/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/TableGridEditor.tsx
@@ -21,7 +21,7 @@ import { TableRowsData } from 'data/table-rows/table-rows-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import useLatest from 'hooks/misc/useLatest'
import { useUrlState } from 'hooks/ui/useUrlState'
-import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { EMPTY_ARR } from 'lib/void'
import { useGetImpersonatedRole } from 'state/role-impersonation-state'
import { useTableEditorStateSnapshot } from 'state/table-editor'
@@ -125,7 +125,7 @@ const TableGridEditor = ({
const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable)
const isTableSelected = isTableLike(selectedTable)
- const isLocked = EXCLUDED_SCHEMAS.includes(selectedTable?.schema ?? '')
+ const isLocked = PROTECTED_SCHEMAS.includes(selectedTable?.schema ?? '')
const canEditViaTableEditor = isTableSelected && !isLocked
const gridTable = parseSupaTable(selectedTable)
diff --git a/apps/studio/components/layouts/Integrations/tabs.tsx b/apps/studio/components/layouts/Integrations/tabs.tsx
index 1d53f7c856740..bc9c475d4a95a 100644
--- a/apps/studio/components/layouts/Integrations/tabs.tsx
+++ b/apps/studio/components/layouts/Integrations/tabs.tsx
@@ -71,7 +71,7 @@ export const IntegrationTabs = ({ scroll, isSticky }: IntegrationTabsProps) => {
const tabUrl = `/project/${project?.ref}/integrations/${integration?.id}/${tab.route}`
return (
-
+
{tab.label}
diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx
index 1366ec37662ab..b9496ba2ba6f1 100644
--- a/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx
+++ b/apps/studio/components/layouts/ProjectIntegrationsLayout/ProjectIntegrationsLayout.tsx
@@ -30,7 +30,6 @@ const ProjectIntegrationsMenu = () => {
const pgNetExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_net') !== undefined
const graphqlExtensionExists = (data ?? []).find((ext) => ext.name === 'pg_graphql') !== undefined
- // TODO: Change this to true for local development to work
const pgmqExtensionExists = (data ?? []).find((ext) => ext.name === 'pgmq') !== undefined
return (
diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
index 204958a3349af..acfd0705ee736 100644
--- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx
@@ -198,7 +198,11 @@ const ProjectLayout = forwardRef
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx
index 56761c03108ae..7e2a8253966a7 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SqlEditor.Commands.tsx
@@ -35,7 +35,7 @@ import {
useSetPage,
} from 'ui-patterns/CommandMenu'
import { usePrefetchTables, useTablesQuery, type TablesData } from 'data/tables/tables-query'
-import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useEffect, useRef } from 'react'
export function useSqlEditorGotoCommands(options?: CommandOptions) {
@@ -356,7 +356,7 @@ from ${formatTableIdentifier(table)}
}
function excludeSupabaseControlledSchemas(tables: TablesData) {
- return tables.filter((table) => !EXCLUDED_SCHEMAS.includes(table.schema))
+ return tables.filter((table) => !PROTECTED_SCHEMAS.includes(table.schema))
}
// Not a perfectly spec-compliant regex , since Postgres also allows non-Latin
diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
index eef43fb93f9e8..cbbf0cdad3e85 100644
--- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx
@@ -16,7 +16,7 @@ import { useTableEditorQuery } from 'data/table-editor/table-editor-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
-import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useTableEditorStateSnapshot } from 'state/table-editor'
import {
AlertDescription_Shadcn_,
@@ -93,7 +93,7 @@ const TableEditorMenu = () => {
const [protectedSchemas] = partition(
(schemas ?? []).sort((a, b) => a.name.localeCompare(b.name)),
- (schema) => EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
+ (schema) => PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const isLocked = protectedSchemas.some((s) => s.id === schema?.id)
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
index 74c2d00d3014e..050323869e2b1 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx
@@ -394,8 +394,9 @@ export const AIAssistant = ({
{suggestions.title && {suggestions.title}
}
- {suggestions?.prompts?.map((prompt) => (
+ {suggestions?.prompts?.map((prompt, idx) => (
}
type="text"
@@ -518,8 +519,8 @@ export const AIAssistant = ({
{sqlSnippets.map((snippet, index) => (
{
const newSnippets = [...sqlSnippets]
diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.tsx
index d88b7ee0749cc..98fd721b7ff05 100644
--- a/apps/studio/components/ui/AIAssistantPanel/Message.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/Message.tsx
@@ -3,7 +3,7 @@ import { PropsWithChildren } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
-import { cn, markdownComponents, WarningIcon } from 'ui'
+import { cn, markdownComponents, WarningIcon, CodeBlock } from 'ui'
import CollapsibleCodeBlock from './CollapsibleCodeBlock'
import { SqlSnippet } from './SqlSnippet'
@@ -56,20 +56,38 @@ export const Message = function Message({
components={{
...markdownComponents,
pre: (props: any) => {
- return readOnly ? (
-
-
+
+
+ ) : (
+
+ )
+ }
+
+ return (
+
+ code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap [&>code]:block [&>code>span]:text-foreground'
+ )}
/>
- ) : (
-
)
},
ol: (props: any) => {
diff --git a/apps/studio/data/config/project-postgrest-config-query.ts b/apps/studio/data/config/project-postgrest-config-query.ts
index fcf36806e3336..58b8956314505 100644
--- a/apps/studio/data/config/project-postgrest-config-query.ts
+++ b/apps/studio/data/config/project-postgrest-config-query.ts
@@ -2,6 +2,7 @@ import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { configKeys } from './keys'
+import { ResponseError } from 'types'
export type ProjectPostgrestConfigVariables = {
projectRef?: string
@@ -36,7 +37,7 @@ export async function getProjectPostgrestConfig(
}
export type ProjectPostgrestConfigData = Awaited>
-export type ProjectPostgrestConfigError = unknown
+export type ProjectPostgrestConfigError = ResponseError
export const useProjectPostgrestConfigQuery = (
{ projectRef }: ProjectPostgrestConfigVariables,
diff --git a/apps/studio/data/database-queues/database-queues-create-mutation.ts b/apps/studio/data/database-queues/database-queues-create-mutation.ts
index 7f08d7a9840db..92c9e43d3b028 100644
--- a/apps/studio/data/database-queues/database-queues-create-mutation.ts
+++ b/apps/studio/data/database-queues/database-queues-create-mutation.ts
@@ -4,22 +4,41 @@ import { toast } from 'sonner'
import { executeSql } from 'data/sql/execute-sql-query'
import type { ResponseError } from 'types'
import { databaseQueuesKeys } from './keys'
+import { tableKeys } from 'data/tables/keys'
export type DatabaseQueueCreateVariables = {
projectRef: string
connectionString?: string
- query: string
+ name: string
+ type: 'basic' | 'partitioned' | 'unlogged'
+ enableRls: boolean
+ configuration?: {
+ partitionInterval?: number
+ retentionInterval?: number
+ }
}
export async function createDatabaseQueue({
projectRef,
connectionString,
- query,
+ name,
+ type,
+ enableRls,
+ configuration,
}: DatabaseQueueCreateVariables) {
+ const { partitionInterval, retentionInterval } = configuration ?? {}
+
+ const query =
+ type === 'partitioned'
+ ? `select from pgmq.create_partitioned('${name}', '${partitionInterval}', '${retentionInterval}');`
+ : type === 'unlogged'
+ ? `SELECT pgmq.create_unlogged('${name}');`
+ : `SELECT pgmq.create('${name}');`
+
const { result } = await executeSql({
projectRef,
connectionString,
- sql: query,
+ sql: `${query} ${enableRls ? `alter table pgmq."q_${name}" enable row level security;` : ''}`.trim(),
queryKey: databaseQueuesKeys.create(),
})
@@ -44,6 +63,7 @@ export const useDatabaseQueueCreateMutation = ({
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(databaseQueuesKeys.list(projectRef))
+ queryClient.invalidateQueries(tableKeys.list(projectRef, 'pgmq'))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/data/database-queues/database-queues-delete-mutation.ts b/apps/studio/data/database-queues/database-queues-delete-mutation.ts
index 83c2c82835918..79a043428bc09 100644
--- a/apps/studio/data/database-queues/database-queues-delete-mutation.ts
+++ b/apps/studio/data/database-queues/database-queues-delete-mutation.ts
@@ -42,11 +42,8 @@ export const useDatabaseQueueDeleteMutation = ({
(vars) => deleteDatabaseQueue(vars),
{
async onSuccess(data, variables, context) {
- const { projectRef, queueName } = variables
+ const { projectRef } = variables
await queryClient.invalidateQueries(databaseQueuesKeys.list(projectRef))
- await queryClient.invalidateQueries(
- databaseQueuesKeys.getMessagesInfinite(projectRef, queueName)
- )
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
diff --git a/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts b/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts
new file mode 100644
index 0000000000000..353c162aa6212
--- /dev/null
+++ b/apps/studio/data/database-queues/database-queues-expose-postgrest-status-query.ts
@@ -0,0 +1,47 @@
+import { useQuery, UseQueryOptions } from '@tanstack/react-query'
+import minify from 'pg-minify'
+
+import { executeSql } from 'data/sql/execute-sql-query'
+import { ResponseError } from 'types'
+import { QUEUES_SCHEMA } from './database-queues-toggle-postgrest-mutation'
+import { databaseQueuesKeys } from './keys'
+
+export type DatabaseQueuesVariables = {
+ projectRef?: string
+ connectionString?: string
+}
+
+// [Joshen] Check if all the relevant functions exist to indicate whether PGMQ has been exposed through PostgREST
+const queueSqlQuery = minify(/**SQL */ `
+ SELECT exists (select schema_name FROM information_schema.schemata WHERE schema_name = '${QUEUES_SCHEMA}');
+`)
+
+export async function getDatabaseQueuesExposePostgrestStatus({
+ projectRef,
+ connectionString,
+}: DatabaseQueuesVariables) {
+ if (!projectRef) throw new Error('Project ref is required')
+
+ const { result } = await executeSql({
+ projectRef,
+ connectionString,
+ sql: queueSqlQuery,
+ })
+ return result[0].exists as boolean
+}
+
+export type DatabaseQueueData = boolean
+export type DatabaseQueueError = ResponseError
+
+export const useQueuesExposePostgrestStatusQuery = (
+ { projectRef, connectionString }: DatabaseQueuesVariables,
+ { enabled = true, ...options }: UseQueryOptions = {}
+) =>
+ useQuery(
+ databaseQueuesKeys.exposePostgrestStatus(projectRef),
+ () => getDatabaseQueuesExposePostgrestStatus({ projectRef, connectionString }),
+ {
+ enabled: enabled && typeof projectRef !== 'undefined',
+ ...options,
+ }
+ )
diff --git a/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts b/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts
new file mode 100644
index 0000000000000..5265c2527abd8
--- /dev/null
+++ b/apps/studio/data/database-queues/database-queues-toggle-postgrest-mutation.ts
@@ -0,0 +1,278 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { toast } from 'sonner'
+import minify from 'pg-minify'
+
+import { executeSql } from 'data/sql/execute-sql-query'
+import type { ResponseError } from 'types'
+import { databaseQueuesKeys } from './keys'
+import { databaseKeys } from 'data/database/keys'
+
+export type DatabaseQueueExposePostgrestVariables = {
+ projectRef: string
+ connectionString?: string
+ enable: boolean
+}
+
+export const QUEUES_SCHEMA = 'pgmq_public'
+
+const EXPOSE_QUEUES_TO_POSTGREST_SQL = minify(/* SQL */ `
+create schema if not exists ${QUEUES_SCHEMA};
+grant usage on schema ${QUEUES_SCHEMA} to postgres, anon, authenticated, service_role;
+
+create or replace function ${QUEUES_SCHEMA}.queue_pop(
+ queue_name text
+)
+ returns setof pgmq.message_record
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return query
+ select *
+ from pgmq.pop(
+ queue_name := queue_name
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_pop(queue_name text) is 'Retrieves and locks the next message from the specified queue.';
+
+
+create or replace function ${QUEUES_SCHEMA}.queue_send(
+ queue_name text,
+ message jsonb,
+ sleep_seconds integer default 0 -- renamed from 'delay'
+)
+ returns setof bigint
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return query
+ select *
+ from pgmq.send(
+ queue_name := queue_name,
+ msg := message,
+ delay := sleep_seconds
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_send(queue_name text, message jsonb, sleep_seconds integer) is 'Sends a message to the specified queue, optionally delaying its availability by a number of seconds.';
+
+
+create or replace function ${QUEUES_SCHEMA}.queue_send_batch(
+ queue_name text,
+ messages jsonb[],
+ sleep_seconds integer default 0 -- renamed from 'delay'
+)
+ returns setof bigint
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return query
+ select *
+ from pgmq.send_batch(
+ queue_name := queue_name,
+ msgs := messages,
+ delay := sleep_seconds
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_send_batch(queue_name text, messages jsonb[], sleep_seconds integer) is 'Sends a batch of messages to the specified queue, optionally delaying their availability by a number of seconds.';
+
+
+create or replace function ${QUEUES_SCHEMA}.queue_archive(
+ queue_name text,
+ message_id bigint
+)
+ returns boolean
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return
+ pgmq.archive(
+ queue_name := queue_name,
+ msg_id := message_id
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.';
+
+
+create or replace function ${QUEUES_SCHEMA}.queue_archive(
+ queue_name text,
+ message_id bigint
+)
+ returns boolean
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return
+ pgmq.archive(
+ queue_name := queue_name,
+ msg_id := message_id
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_archive(queue_name text, message_id bigint) is 'Archives a message by moving it from the queue to a permanent archive.';
+
+
+create or replace function ${QUEUES_SCHEMA}.queue_delete(
+ queue_name text,
+ message_id bigint
+)
+ returns boolean
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return
+ pgmq.delete(
+ queue_name := queue_name,
+ msg_id := message_id
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_delete(queue_name text, message_id bigint) is 'Permanently deletes a message from the specified queue.';
+
+create or replace function ${QUEUES_SCHEMA}.queue_read(
+ queue_name text,
+ sleep_seconds integer,
+ n integer
+)
+ returns setof pgmq.message_record
+ language plpgsql
+ set search_path = ''
+as $$
+begin
+ return query
+ select *
+ from pgmq.read(
+ queue_name := queue_name,
+ vt := sleep_seconds,
+ qty := n
+ );
+end;
+$$;
+
+comment on function ${QUEUES_SCHEMA}.queue_read(queue_name text, sleep_seconds integer, n integer) is 'Reads up to "n" messages from the specified queue with an optional "sleep_seconds" (visibility timeout).';
+
+-- Grant execute permissions on wrapper functions to roles
+grant execute on function ${QUEUES_SCHEMA}.queue_pop(text) to postgres, service_role, anon, authenticated;
+grant execute on function pgmq.pop(text) to postgres, service_role, anon, authenticated;
+
+grant execute on function ${QUEUES_SCHEMA}.queue_send(text, jsonb, integer) to postgres, service_role, anon, authenticated;
+grant execute on function pgmq.send(text, jsonb, integer) to postgres, service_role, anon, authenticated;
+
+grant execute on function ${QUEUES_SCHEMA}.queue_send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated;
+grant execute on function pgmq.send_batch(text, jsonb[], integer) to postgres, service_role, anon, authenticated;
+
+grant execute on function ${QUEUES_SCHEMA}.queue_archive(text, bigint) to postgres, service_role, anon, authenticated;
+grant execute on function pgmq.archive(text, bigint) to postgres, service_role, anon, authenticated;
+
+grant execute on function ${QUEUES_SCHEMA}.queue_delete(text, bigint) to postgres, service_role, anon, authenticated;
+grant execute on function pgmq.delete(text, bigint) to postgres, service_role, anon, authenticated;
+
+grant execute on function ${QUEUES_SCHEMA}.queue_read(text, integer, integer) to postgres, service_role, anon, authenticated;
+grant execute on function pgmq.read(text, integer, integer) to postgres, service_role, anon, authenticated;
+
+-- For the service role, we want full access
+-- Grant permissions on existing tables
+grant all privileges on all tables in schema pgmq to postgres, service_role;
+
+-- Ensure service_role has permissions on future tables
+alter default privileges in schema pgmq grant all privileges on tables to postgres, service_role;
+
+grant usage on schema pgmq to postgres, anon, authenticated, service_role;
+`)
+
+const HIDE_QUEUES_FROM_POSTGREST_SQL = minify(/* SQL */ `
+ drop function if exists
+ ${QUEUES_SCHEMA}.queue_pop(queue_name text),
+ ${QUEUES_SCHEMA}.queue_send(queue_name text, message jsonb, sleep_seconds integer),
+ ${QUEUES_SCHEMA}.queue_send_batch(queue_name text, message jsonb[], sleep_seconds integer),
+ ${QUEUES_SCHEMA}.queue_archive(queue_name text, message_id bigint),
+ ${QUEUES_SCHEMA}.queue_delete(queue_name text, message_id bigint),
+ ${QUEUES_SCHEMA}.queue_read(queue_name text, sleep integer, n integer)
+ ;
+
+ -- Revoke execute permissions on inner pgmq functions to roles (inverse of enabling)
+ do $$
+ begin
+ if exists (select 1 from pg_namespace where nspname = 'pgmq') then
+ -- Revoke privileges on the schema itself
+ revoke all on schema pgmq from anon, authenticated, service_role;
+
+ -- Revoke default privileges for future objects
+ alter default privileges in schema pgmq revoke all on tables from anon, authenticated, service_role;
+ alter default privileges in schema pgmq revoke all on sequences from anon, authenticated, service_role;
+ alter default privileges in schema pgmq revoke all on functions from anon, authenticated, service_role;
+ end if;
+ end $$;
+
+ drop schema if exists ${QUEUES_SCHEMA};
+`)
+
+export async function toggleQueuesExposurePostgrest({
+ projectRef,
+ connectionString,
+ enable,
+}: DatabaseQueueExposePostgrestVariables) {
+ const sql = enable ? EXPOSE_QUEUES_TO_POSTGREST_SQL : HIDE_QUEUES_FROM_POSTGREST_SQL
+
+ const { result } = await executeSql({
+ projectRef,
+ connectionString,
+ sql,
+ queryKey: ['toggle-queues-exposure'],
+ })
+
+ return result
+}
+
+type DatabaseQueueExposePostgrestData = Awaited>
+
+export const useDatabaseQueueToggleExposeMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseMutationOptions<
+ DatabaseQueueExposePostgrestData,
+ ResponseError,
+ DatabaseQueueExposePostgrestVariables
+ >,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation<
+ DatabaseQueueExposePostgrestData,
+ ResponseError,
+ DatabaseQueueExposePostgrestVariables
+ >((vars) => toggleQueuesExposurePostgrest(vars), {
+ async onSuccess(data, variables, context) {
+ const { projectRef } = variables
+ await queryClient.invalidateQueries(databaseQueuesKeys.exposePostgrestStatus(projectRef))
+ // [Joshen] Schemas can be invalidated without waiting
+ queryClient.invalidateQueries(databaseKeys.schemas(projectRef))
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(`Failed to toggle queue exposure via PostgREST: ${data.message}`)
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ })
+}
diff --git a/apps/studio/data/database-queues/keys.ts b/apps/studio/data/database-queues/keys.ts
index 228c5caa6944d..c902274df00fb 100644
--- a/apps/studio/data/database-queues/keys.ts
+++ b/apps/studio/data/database-queues/keys.ts
@@ -3,9 +3,10 @@ export const databaseQueuesKeys = {
delete: (name: string) => ['queues', name, 'delete'] as const,
purge: (name: string) => ['queues', name, 'purge'] as const,
getMessagesInfinite: (projectRef: string | undefined, queueName: string, options?: object) =>
- ['projects', projectRef, 'queues', queueName, options].filter(Boolean),
+ ['projects', projectRef, 'queue-messages', queueName, options].filter(Boolean),
list: (projectRef: string | undefined) => ['projects', projectRef, 'queues'] as const,
- // invalidating queues.list will also invalidate queues.metrics
metrics: (projectRef: string | undefined, queueName: string) =>
- ['projects', projectRef, 'queues', 'metrics', queueName] as const,
+ ['projects', projectRef, 'queue-metrics', queueName] as const,
+ exposePostgrestStatus: (projectRef: string | undefined) =>
+ ['projects', projectRef, 'queue-expose-status'] as const,
}
diff --git a/apps/studio/lib/constants/schemas.ts b/apps/studio/lib/constants/schemas.ts
index c59e5025b4b78..0d15f8ff19a39 100644
--- a/apps/studio/lib/constants/schemas.ts
+++ b/apps/studio/lib/constants/schemas.ts
@@ -1,7 +1,9 @@
+import { QUEUES_SCHEMA } from 'data/database-queues/database-queues-toggle-postgrest-mutation'
+
/**
* A list of system schemas that users should not interact with
*/
-export const EXCLUDED_SCHEMAS = [
+export const PROTECTED_SCHEMAS = [
'auth',
'cron',
'extensions',
@@ -18,8 +20,9 @@ export const EXCLUDED_SCHEMAS = [
'vault',
'graphql',
'graphql_public',
+ QUEUES_SCHEMA,
]
-export const EXCLUDED_SCHEMAS_WITHOUT_EXTENSIONS = EXCLUDED_SCHEMAS.filter(
+export const PROTECTED_SCHEMAS_WITHOUT_EXTENSIONS = PROTECTED_SCHEMAS.filter(
(x) => x !== 'extensions'
)
diff --git a/apps/studio/package.json b/apps/studio/package.json
index 9d446523616ca..44208f8399ff1 100644
--- a/apps/studio/package.json
+++ b/apps/studio/package.json
@@ -42,6 +42,7 @@
"@supabase/pg-meta": "*",
"@supabase/realtime-js": "2.10.2",
"@supabase/shared-types": "0.1.74",
+ "@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "^2.44.3",
"@tanstack/react-query": "4.35.7",
"@tanstack/react-query-devtools": "4.35.7",
diff --git a/apps/studio/pages/api/ai/sql/generate-v3.ts b/apps/studio/pages/api/ai/sql/generate-v3.ts
index 1b5f3dce19815..343cde4a462da 100644
--- a/apps/studio/pages/api/ai/sql/generate-v3.ts
+++ b/apps/studio/pages/api/ai/sql/generate-v3.ts
@@ -68,7 +68,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
model: openai('gpt-4o-mini'),
maxSteps: 5,
system: `
- You are a Supabase Postgres expert who can do three things.
+ You are a Supabase Postgres expert who can do the following things.
# You generate and debug SQL
The generated SQL (must be valid SQL), and must adhere to the following:
@@ -105,6 +105,9 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
- 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
+ # You convert sql to supabase-js client code
+ Use the convertSqlToSupabaseJs tool to convert select sql to supabase-js client code. If conversion isn't supported, build a postgres function instead and suggest using supabase-js to call it via "const { data, error } = await supabase.rpc('echo', { say: '👋'})"
+
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.
diff --git a/apps/studio/pages/api/ai/sql/tools.ts b/apps/studio/pages/api/ai/sql/tools.ts
index 862e18ed457f6..de4b5f80ad83e 100644
--- a/apps/studio/pages/api/ai/sql/tools.ts
+++ b/apps/studio/pages/api/ai/sql/tools.ts
@@ -5,6 +5,7 @@ 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'
+import { processSql, renderSupabaseJs } from '@supabase/sql-to-rest'
export const getTools = ({
projectRef,
@@ -47,6 +48,25 @@ export const getTools = ({
}
},
}),
+ convertSqlToSupabaseJs: tool({
+ description: 'Convert an sql query into supabase-js client code',
+ parameters: z.object({
+ sql: z
+ .string()
+ .describe(
+ 'The sql statement to convert. Only a subset of statements are supported currently. '
+ ),
+ }),
+ execute: async ({ sql }) => {
+ try {
+ const statement = await processSql(sql)
+ const { code } = await renderSupabaseJs(statement)
+ return code
+ } catch (error) {
+ return `Failed to convert SQL: ${error}`
+ }
+ },
+ }),
getRlsKnowledge: tool({
description:
'Get existing policies and examples and instructions on how to write RLS policies',
diff --git a/apps/studio/pages/project/[ref]/auth/policies.tsx b/apps/studio/pages/project/[ref]/auth/policies.tsx
index 297fd46921ecc..b6e38e3e40d31 100644
--- a/apps/studio/pages/project/[ref]/auth/policies.tsx
+++ b/apps/studio/pages/project/[ref]/auth/policies.tsx
@@ -19,7 +19,7 @@ import { useSchemasQuery } from 'data/database/schemas-query'
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 { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { Input } from 'ui'
@@ -79,7 +79,7 @@ const AuthPoliciesPage: NextPageWithLayout = () => {
})
const [protectedSchemas] = partition(
schemas,
- (schema) => schema?.name !== 'realtime' && EXCLUDED_SCHEMAS.includes(schema?.name ?? '')
+ (schema) => schema?.name !== 'realtime' && PROTECTED_SCHEMAS.includes(schema?.name ?? '')
)
const selectedSchema = schemas?.find((s) => s.name === schema)
const isLocked = protectedSchemas.some((s) => s.id === selectedSchema?.id)
diff --git a/apps/studio/pages/project/[ref]/database/column-privileges.tsx b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
index a8a881a5c96ee..666008e679f13 100644
--- a/apps/studio/pages/project/[ref]/database/column-privileges.tsx
+++ b/apps/studio/pages/project/[ref]/database/column-privileges.tsx
@@ -27,7 +27,7 @@ import { useTablesQuery } from 'data/tables/tables-query'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
-import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
+import { PROTECTED_SCHEMAS } from 'lib/constants/schemas'
import { useAppStateSnapshot } from 'state/app-state'
import type { NextPageWithLayout } from 'types'
import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui'
@@ -133,7 +133,7 @@ const PrivilegesPage: NextPageWithLayout = () => {
const table = tableList?.find(
(table) => table.schema === selectedSchema && table.name === selectedTable
)
- const isLocked = EXCLUDED_SCHEMAS.includes(selectedSchema)
+ const isLocked = PROTECTED_SCHEMAS.includes(selectedSchema)
const {
tableCheckedStates,
diff --git a/package-lock.json b/package-lock.json
index 2bdf6f2e7eab5..05a306abd373b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1802,6 +1802,7 @@
"@supabase/pg-meta": "*",
"@supabase/realtime-js": "2.10.2",
"@supabase/shared-types": "0.1.74",
+ "@supabase/sql-to-rest": "^0.1.6",
"@supabase/supabase-js": "^2.44.3",
"@tanstack/react-query": "4.35.7",
"@tanstack/react-query-devtools": "4.35.7",
@@ -16242,6 +16243,7 @@
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@supabase/sql-to-rest/-/sql-to-rest-0.1.6.tgz",
"integrity": "sha512-06KgjeINtc6405XQvfnchBE1azEsU8G2NElfadmvVHKmHa5l2bFzjbtFbpaYgpgTzccHlcDmBaCgedVf2Gyl8Q==",
+ "license": "MIT",
"dependencies": {
"@babel/parser": "^7.24.5",
"libpg-query": "^15.1.0",
diff --git a/packages/ui/src/components/shadcn/ui/form.tsx b/packages/ui/src/components/shadcn/ui/form.tsx
index e480d4545832f..37271f4d3b2e2 100644
--- a/packages/ui/src/components/shadcn/ui/form.tsx
+++ b/packages/ui/src/components/shadcn/ui/form.tsx
@@ -134,7 +134,7 @@ const FormDescription = React.forwardRef<
const { formDescriptionId } = useFormField()
return (
-