mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
imporvement(pg): added wand config for writing sql queries for generic db blocks & supabase postgrest syntax (#1197)
* add parallel ai, postgres, mysql, slight modifications to dark mode styling * bun install frozen lockfile * new deps * improve security, add wand to short input and update wand config
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -17,7 +18,7 @@ const DeleteSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -16,7 +17,7 @@ const ExecuteSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
@@ -26,7 +27,6 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Executing raw SQL on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
// Validate query before execution
|
||||
const validation = validateQuery(params.query)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Query validation failed: ${validation.error}`)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -38,13 +39,10 @@ const InsertSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
logger.info(`[${requestId}] Received data field type: ${typeof body.data}, value:`, body.data)
|
||||
|
||||
const params = InsertSchema.parse(body)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -16,7 +17,7 @@ const QuerySchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
@@ -26,7 +27,6 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Executing MySQL query on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
// Validate query before execution
|
||||
const validation = validateQuery(params.query)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Query validation failed: ${validation.error}`)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -36,7 +37,7 @@ const UpdateSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
@@ -18,13 +18,11 @@ export async function createMySQLConnection(config: MySQLConnectionConfig) {
|
||||
password: config.password,
|
||||
}
|
||||
|
||||
// Handle SSL configuration
|
||||
if (config.ssl === 'required') {
|
||||
connectionConfig.ssl = { rejectUnauthorized: true }
|
||||
} else if (config.ssl === 'preferred') {
|
||||
connectionConfig.ssl = { rejectUnauthorized: false }
|
||||
}
|
||||
// For 'disabled', we don't set the ssl property at all
|
||||
|
||||
return mysql.createConnection(connectionConfig)
|
||||
}
|
||||
@@ -54,7 +52,6 @@ export async function executeQuery(
|
||||
export function validateQuery(query: string): { isValid: boolean; error?: string } {
|
||||
const trimmedQuery = query.trim().toLowerCase()
|
||||
|
||||
// Block dangerous SQL operations
|
||||
const dangerousPatterns = [
|
||||
/drop\s+database/i,
|
||||
/drop\s+schema/i,
|
||||
@@ -91,7 +88,6 @@ export function validateQuery(query: string): { isValid: boolean; error?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow specific statement types for execute endpoint
|
||||
const allowedStatements = /^(select|insert|update|delete|with|show|describe|explain)\s+/i
|
||||
if (!allowedStatements.test(trimmedQuery)) {
|
||||
return {
|
||||
@@ -116,6 +112,8 @@ export function buildInsertQuery(table: string, data: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
export function buildUpdateQuery(table: string, data: Record<string, unknown>, where: string) {
|
||||
validateWhereClause(where)
|
||||
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const columns = Object.keys(data)
|
||||
const values = Object.values(data)
|
||||
@@ -127,14 +125,33 @@ export function buildUpdateQuery(table: string, data: Record<string, unknown>, w
|
||||
}
|
||||
|
||||
export function buildDeleteQuery(table: string, where: string) {
|
||||
validateWhereClause(where)
|
||||
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const query = `DELETE FROM ${sanitizedTable} WHERE ${where}`
|
||||
|
||||
return { query, values: [] }
|
||||
}
|
||||
|
||||
function validateWhereClause(where: string): void {
|
||||
const dangerousPatterns = [
|
||||
/;\s*(drop|delete|insert|update|create|alter|grant|revoke)/i,
|
||||
/union\s+select/i,
|
||||
/into\s+outfile/i,
|
||||
/load_file/i,
|
||||
/--/,
|
||||
/\/\*/,
|
||||
/\*\//,
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(where)) {
|
||||
throw new Error('WHERE clause contains potentially dangerous operation')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeIdentifier(identifier: string): string {
|
||||
// Handle schema.table format
|
||||
if (identifier.includes('.')) {
|
||||
const parts = identifier.split('.')
|
||||
return parts.map((part) => sanitizeSingleIdentifier(part)).join('.')
|
||||
@@ -144,16 +161,13 @@ export function sanitizeIdentifier(identifier: string): string {
|
||||
}
|
||||
|
||||
function sanitizeSingleIdentifier(identifier: string): string {
|
||||
// Remove any existing backticks to prevent double-escaping
|
||||
const cleaned = identifier.replace(/`/g, '')
|
||||
|
||||
// Validate identifier contains only safe characters
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(cleaned)) {
|
||||
throw new Error(
|
||||
`Invalid identifier: ${identifier}. Identifiers must start with a letter or underscore and contain only letters, numbers, and underscores.`
|
||||
)
|
||||
}
|
||||
|
||||
// Wrap in backticks for MySQL
|
||||
return `\`${cleaned}\``
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -17,7 +18,7 @@ const DeleteSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -20,7 +21,7 @@ const ExecuteSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
@@ -30,7 +31,6 @@ export async function POST(request: NextRequest) {
|
||||
`[${requestId}] Executing raw SQL on ${params.host}:${params.port}/${params.database}`
|
||||
)
|
||||
|
||||
// Validate query before execution
|
||||
const validation = validateQuery(params.query)
|
||||
if (!validation.isValid) {
|
||||
logger.warn(`[${requestId}] Query validation failed: ${validation.error}`)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -38,7 +39,7 @@ const InsertSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -16,7 +17,7 @@ const QuerySchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
@@ -36,7 +37,7 @@ const UpdateSchema = z.object({
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const requestId = randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
|
||||
@@ -82,7 +82,6 @@ export function validateQuery(query: string): { isValid: boolean; error?: string
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow specific statement types for execute endpoint
|
||||
const allowedStatements = /^(select|insert|update|delete|with|explain|analyze|show)\s+/i
|
||||
if (!allowedStatements.test(trimmedQuery)) {
|
||||
return {
|
||||
@@ -96,7 +95,6 @@ export function validateQuery(query: string): { isValid: boolean; error?: string
|
||||
}
|
||||
|
||||
export function sanitizeIdentifier(identifier: string): string {
|
||||
// Handle schema.table format
|
||||
if (identifier.includes('.')) {
|
||||
const parts = identifier.split('.')
|
||||
return parts.map((part) => sanitizeSingleIdentifier(part)).join('.')
|
||||
@@ -105,18 +103,33 @@ export function sanitizeIdentifier(identifier: string): string {
|
||||
return sanitizeSingleIdentifier(identifier)
|
||||
}
|
||||
|
||||
function validateWhereClause(where: string): void {
|
||||
const dangerousPatterns = [
|
||||
/;\s*(drop|delete|insert|update|create|alter|grant|revoke)/i,
|
||||
/union\s+select/i,
|
||||
/into\s+outfile/i,
|
||||
/load_file/i,
|
||||
/--/,
|
||||
/\/\*/,
|
||||
/\*\//,
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(where)) {
|
||||
throw new Error('WHERE clause contains potentially dangerous operation')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeSingleIdentifier(identifier: string): string {
|
||||
// Remove any existing double quotes to prevent double-escaping
|
||||
const cleaned = identifier.replace(/"/g, '')
|
||||
|
||||
// Validate identifier contains only safe characters
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(cleaned)) {
|
||||
throw new Error(
|
||||
`Invalid identifier: ${identifier}. Identifiers must start with a letter or underscore and contain only letters, numbers, and underscores.`
|
||||
)
|
||||
}
|
||||
|
||||
// Wrap in double quotes for PostgreSQL
|
||||
return `"${cleaned}"`
|
||||
}
|
||||
|
||||
@@ -146,6 +159,8 @@ export async function executeUpdate(
|
||||
data: Record<string, unknown>,
|
||||
where: string
|
||||
): Promise<{ rows: unknown[]; rowCount: number }> {
|
||||
validateWhereClause(where)
|
||||
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const columns = Object.keys(data)
|
||||
const sanitizedColumns = columns.map((col) => sanitizeIdentifier(col))
|
||||
@@ -166,6 +181,8 @@ export async function executeDelete(
|
||||
table: string,
|
||||
where: string
|
||||
): Promise<{ rows: unknown[]; rowCount: number }> {
|
||||
validateWhereClause(where)
|
||||
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const query = `DELETE FROM ${sanitizedTable} WHERE ${where} RETURNING *`
|
||||
const result = await sql.unsafe(query, [])
|
||||
|
||||
@@ -385,7 +385,7 @@ export function Code({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'group relative min-h-[100px] rounded-md border-2 border-border bg-background font-mono text-sm transition-colors',
|
||||
'group relative min-h-[100px] rounded-md border border-input bg-background font-mono text-sm transition-colors',
|
||||
isConnecting && 'ring-2 ring-blue-500 ring-offset-2',
|
||||
!isValidJson && 'border-destructive bg-destructive/10'
|
||||
)}
|
||||
@@ -394,7 +394,7 @@ export function Code({
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className='absolute top-2 right-3 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
|
||||
{!isCollapsed && !isAiStreaming && !isPreview && (
|
||||
{wandConfig?.enabled && !isCollapsed && !isAiStreaming && !isPreview && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Wand2 } from 'lucide-react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
|
||||
import { formatDisplayText } from '@/components/ui/formatted-text'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { checkTagTrigger, TagDropdown } from '@/components/ui/tag-dropdown'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { WandPromptBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/wand-prompt-bar/wand-prompt-bar'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
|
||||
import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-wand'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { useTagSelection } from '@/hooks/use-tag-selection'
|
||||
import { useOperationQueueStore } from '@/stores/operation-queue/store'
|
||||
@@ -40,19 +44,39 @@ export function ShortInput({
|
||||
previewValue,
|
||||
disabled = false,
|
||||
}: ShortInputProps) {
|
||||
// Local state for immediate UI updates during streaming
|
||||
const [localContent, setLocalContent] = useState<string>('')
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const [showEnvVars, setShowEnvVars] = useState(false)
|
||||
const [showTags, setShowTags] = useState(false)
|
||||
const validatePropValue = (value: any): string => {
|
||||
if (value === undefined || value === null) return ''
|
||||
if (typeof value === 'string') return value
|
||||
try {
|
||||
return String(value)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId)
|
||||
|
||||
// Wand functionality (only if wandConfig is enabled)
|
||||
const wandHook = config.wandConfig?.enabled
|
||||
? useWand({
|
||||
wandConfig: config.wandConfig,
|
||||
currentValue: localContent,
|
||||
onStreamStart: () => {
|
||||
// Clear the content when streaming starts
|
||||
setLocalContent('')
|
||||
},
|
||||
onStreamChunk: (chunk) => {
|
||||
// Update local content with each chunk as it arrives
|
||||
setLocalContent((current) => current + chunk)
|
||||
},
|
||||
onGeneratedContent: (content) => {
|
||||
// Final content update
|
||||
setLocalContent(content)
|
||||
},
|
||||
})
|
||||
: null
|
||||
// State management - useSubBlockValue with explicit streaming control
|
||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId, false, {
|
||||
isStreaming: wandHook?.isStreaming || false,
|
||||
onStreamingEnd: () => {
|
||||
logger.debug('Wand streaming ended, value persisted', { blockId, subBlockId })
|
||||
},
|
||||
})
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [cursorPosition, setCursorPosition] = useState(0)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -65,7 +89,29 @@ export function ShortInput({
|
||||
const reactFlowInstance = useReactFlow()
|
||||
|
||||
// Use preview value when in preview mode, otherwise use store value or prop value
|
||||
const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||
const baseValue = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue
|
||||
|
||||
// During streaming, use local content; otherwise use base value
|
||||
const value = wandHook?.isStreaming ? localContent : baseValue
|
||||
|
||||
// Sync local content with base value when not streaming
|
||||
useEffect(() => {
|
||||
if (!wandHook?.isStreaming) {
|
||||
const baseValueString = baseValue?.toString() ?? ''
|
||||
if (baseValueString !== localContent) {
|
||||
setLocalContent(baseValueString)
|
||||
}
|
||||
}
|
||||
}, [baseValue, wandHook?.isStreaming])
|
||||
|
||||
// Update store value during streaming (but won't persist until streaming ends)
|
||||
useEffect(() => {
|
||||
if (wandHook?.isStreaming && localContent !== '') {
|
||||
if (!isPreview && !disabled) {
|
||||
setStoreValue(localContent)
|
||||
}
|
||||
}
|
||||
}, [localContent, wandHook?.isStreaming, isPreview, disabled, setStoreValue])
|
||||
|
||||
// Check if this input is API key related
|
||||
const isApiKeyField = useMemo(() => {
|
||||
@@ -297,91 +343,130 @@ export function ShortInput({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative w-full'>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
'allow-scroll w-full overflow-auto text-transparent caret-foreground placeholder:text-muted-foreground/50',
|
||||
isConnecting &&
|
||||
config?.connectionDroppable !== false &&
|
||||
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
|
||||
)}
|
||||
placeholder={placeholder ?? ''}
|
||||
type='text'
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
onFocus={() => {
|
||||
setIsFocused(true)
|
||||
<>
|
||||
<WandPromptBar
|
||||
isVisible={wandHook?.isPromptVisible || false}
|
||||
isLoading={wandHook?.isLoading || false}
|
||||
isStreaming={wandHook?.isStreaming || false}
|
||||
promptValue={wandHook?.promptInputValue || ''}
|
||||
onSubmit={(prompt: string) => wandHook?.generateStream({ prompt }) || undefined}
|
||||
onCancel={
|
||||
wandHook?.isStreaming
|
||||
? wandHook?.cancelGeneration
|
||||
: wandHook?.hidePromptInline || (() => {})
|
||||
}
|
||||
onChange={(value: string) => wandHook?.updatePromptValue?.(value)}
|
||||
placeholder={config.wandConfig?.placeholder || 'Describe what you want to generate...'}
|
||||
/>
|
||||
|
||||
// If this is an API key field, automatically show env vars dropdown
|
||||
if (isApiKeyField) {
|
||||
setShowEnvVars(true)
|
||||
setSearchTerm('')
|
||||
<div className='group relative w-full'>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
'allow-scroll w-full overflow-auto text-transparent caret-foreground placeholder:text-muted-foreground/50',
|
||||
isConnecting &&
|
||||
config?.connectionDroppable !== false &&
|
||||
'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
|
||||
)}
|
||||
placeholder={placeholder ?? ''}
|
||||
type='text'
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
onFocus={() => {
|
||||
setIsFocused(true)
|
||||
|
||||
// Set cursor position to the end of the input
|
||||
const inputLength = value?.toString().length ?? 0
|
||||
setCursorPosition(inputLength)
|
||||
} else {
|
||||
// If this is an API key field, automatically show env vars dropdown
|
||||
if (isApiKeyField) {
|
||||
setShowEnvVars(true)
|
||||
setSearchTerm('')
|
||||
|
||||
// Set cursor position to the end of the input
|
||||
const inputLength = value?.toString().length ?? 0
|
||||
setCursorPosition(inputLength)
|
||||
} else {
|
||||
setShowEnvVars(false)
|
||||
setShowTags(false)
|
||||
setSearchTerm('')
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false)
|
||||
setShowEnvVars(false)
|
||||
setShowTags(false)
|
||||
setSearchTerm('')
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false)
|
||||
setShowEnvVars(false)
|
||||
try {
|
||||
useOperationQueueStore.getState().flushDebouncedForBlock(blockId)
|
||||
} catch {}
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onScroll={handleScroll}
|
||||
onPaste={handlePaste}
|
||||
onWheel={handleWheel}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoComplete='off'
|
||||
style={{ overflowX: 'auto' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div
|
||||
ref={overlayRef}
|
||||
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-3 text-sm'
|
||||
style={{ overflowX: 'auto' }}
|
||||
>
|
||||
try {
|
||||
useOperationQueueStore.getState().flushDebouncedForBlock(blockId)
|
||||
} catch {}
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onScroll={handleScroll}
|
||||
onPaste={handlePaste}
|
||||
onWheel={handleWheel}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoComplete='off'
|
||||
style={{ overflowX: 'auto' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div
|
||||
className='w-full whitespace-pre'
|
||||
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
|
||||
ref={overlayRef}
|
||||
className='pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-3 text-sm'
|
||||
style={{ overflowX: 'auto' }}
|
||||
>
|
||||
{password && !isFocused
|
||||
? '•'.repeat(value?.toString().length ?? 0)
|
||||
: formatDisplayText(value?.toString() ?? '', true)}
|
||||
<div
|
||||
className='w-full whitespace-pre'
|
||||
style={{ scrollbarWidth: 'none', minWidth: 'fit-content' }}
|
||||
>
|
||||
{password && !isFocused
|
||||
? '•'.repeat(value?.toString().length ?? 0)
|
||||
: formatDisplayText(value?.toString() ?? '', true)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={searchTerm}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
/>
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={handleEnvVarSelect}
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Wand Button */}
|
||||
{wandHook && !isPreview && !wandHook.isStreaming && (
|
||||
<div className='-translate-y-1/2 absolute top-1/2 right-3 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={
|
||||
wandHook.isPromptVisible ? wandHook.hidePromptInline : wandHook.showPromptInline
|
||||
}
|
||||
disabled={wandHook.isLoading || wandHook.isStreaming || disabled}
|
||||
aria-label='Generate content with AI'
|
||||
className='h-8 w-8 rounded-full border border-transparent bg-muted/80 text-muted-foreground shadow-sm transition-all duration-200 hover:border-primary/20 hover:bg-muted hover:text-primary hover:shadow'
|
||||
>
|
||||
<Wand2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!wandHook?.isStreaming && (
|
||||
<>
|
||||
<EnvVarDropdown
|
||||
visible={showEnvVars}
|
||||
onSelect={handleEnvVarSelect}
|
||||
searchTerm={searchTerm}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowEnvVars(false)
|
||||
setSearchTerm('')
|
||||
}}
|
||||
/>
|
||||
<TagDropdown
|
||||
visible={showTags}
|
||||
onSelect={handleEnvVarSelect}
|
||||
blockId={blockId}
|
||||
activeSourceBlockId={activeSourceBlockId}
|
||||
inputValue={value?.toString() ?? ''}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => {
|
||||
setShowTags(false)
|
||||
setActiveSourceBlockId(null)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,6 +118,72 @@ export const MySQLBlock: BlockConfig<MySQLResponse> = {
|
||||
placeholder: 'SELECT * FROM users WHERE active = true',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert MySQL database developer. Write MySQL SQL queries based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
|
||||
|
||||
### QUERY GUIDELINES
|
||||
1. **Syntax**: Use MySQL-specific syntax and functions
|
||||
2. **Performance**: Write efficient queries with proper indexing considerations
|
||||
3. **Security**: Use parameterized queries when applicable
|
||||
4. **Readability**: Format queries with proper indentation and spacing
|
||||
5. **Best Practices**: Follow MySQL naming conventions
|
||||
|
||||
### MYSQL FEATURES
|
||||
- Use MySQL-specific functions (IFNULL, DATE_FORMAT, CONCAT, etc.)
|
||||
- Leverage MySQL features like GROUP_CONCAT, AUTO_INCREMENT
|
||||
- Use proper MySQL data types (VARCHAR, DATETIME, DECIMAL, JSON, etc.)
|
||||
- Include appropriate LIMIT clauses for large result sets
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple Select**: "Get all active users"
|
||||
→ SELECT id, name, email, created_at
|
||||
FROM users
|
||||
WHERE active = 1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
**Complex Join**: "Get users with their order counts and total spent"
|
||||
→ SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
COUNT(o.id) as order_count,
|
||||
IFNULL(SUM(o.total), 0) as total_spent
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.active = 1
|
||||
GROUP BY u.id, u.name, u.email
|
||||
HAVING COUNT(o.id) > 0
|
||||
ORDER BY total_spent DESC;
|
||||
|
||||
**With Subquery**: "Get top 10 products by sales"
|
||||
→ SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
(SELECT SUM(oi.quantity * oi.price)
|
||||
FROM order_items oi
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id
|
||||
AND o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
) as total_sales
|
||||
FROM products p
|
||||
WHERE p.active = 1
|
||||
ORDER BY total_sales DESC
|
||||
LIMIT 10;
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the SQL query you need...',
|
||||
generationType: 'sql-query',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
@@ -127,6 +193,72 @@ export const MySQLBlock: BlockConfig<MySQLResponse> = {
|
||||
placeholder: 'SELECT * FROM table_name',
|
||||
condition: { field: 'operation', value: 'execute' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert MySQL database developer. Write MySQL SQL queries based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
|
||||
|
||||
### QUERY GUIDELINES
|
||||
1. **Syntax**: Use MySQL-specific syntax and functions
|
||||
2. **Performance**: Write efficient queries with proper indexing considerations
|
||||
3. **Security**: Use parameterized queries when applicable
|
||||
4. **Readability**: Format queries with proper indentation and spacing
|
||||
5. **Best Practices**: Follow MySQL naming conventions
|
||||
|
||||
### MYSQL FEATURES
|
||||
- Use MySQL-specific functions (IFNULL, DATE_FORMAT, CONCAT, etc.)
|
||||
- Leverage MySQL features like GROUP_CONCAT, AUTO_INCREMENT
|
||||
- Use proper MySQL data types (VARCHAR, DATETIME, DECIMAL, JSON, etc.)
|
||||
- Include appropriate LIMIT clauses for large result sets
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple Select**: "Get all active users"
|
||||
→ SELECT id, name, email, created_at
|
||||
FROM users
|
||||
WHERE active = 1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
**Complex Join**: "Get users with their order counts and total spent"
|
||||
→ SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
COUNT(o.id) as order_count,
|
||||
IFNULL(SUM(o.total), 0) as total_spent
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.active = 1
|
||||
GROUP BY u.id, u.name, u.email
|
||||
HAVING COUNT(o.id) > 0
|
||||
ORDER BY total_spent DESC;
|
||||
|
||||
**With Subquery**: "Get top 10 products by sales"
|
||||
→ SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
(SELECT SUM(oi.quantity * oi.price)
|
||||
FROM order_items oi
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
WHERE oi.product_id = p.id
|
||||
AND o.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||||
) as total_sales
|
||||
FROM products p
|
||||
WHERE p.active = 1
|
||||
ORDER BY total_sales DESC
|
||||
LIMIT 10;
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the SQL query you need...',
|
||||
generationType: 'sql-query',
|
||||
},
|
||||
},
|
||||
// Data for insert operations
|
||||
{
|
||||
|
||||
@@ -118,6 +118,73 @@ export const PostgreSQLBlock: BlockConfig<PostgresResponse> = {
|
||||
placeholder: 'SELECT * FROM users WHERE active = true',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert PostgreSQL database developer. Write PostgreSQL SQL queries based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
|
||||
|
||||
### QUERY GUIDELINES
|
||||
1. **Syntax**: Use PostgreSQL-specific syntax and functions
|
||||
2. **Performance**: Write efficient queries with proper indexing considerations
|
||||
3. **Security**: Use parameterized queries when applicable
|
||||
4. **Readability**: Format queries with proper indentation and spacing
|
||||
5. **Best Practices**: Follow PostgreSQL naming conventions
|
||||
|
||||
### POSTGRESQL FEATURES
|
||||
- Use PostgreSQL-specific functions (COALESCE, EXTRACT, etc.)
|
||||
- Leverage advanced features like CTEs, window functions, arrays
|
||||
- Use proper PostgreSQL data types (TEXT, TIMESTAMPTZ, JSONB, etc.)
|
||||
- Include appropriate LIMIT clauses for large result sets
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple Select**: "Get all active users"
|
||||
→ SELECT id, name, email, created_at
|
||||
FROM users
|
||||
WHERE active = true
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
**Complex Join**: "Get users with their order counts and total spent"
|
||||
→ SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
COUNT(o.id) as order_count,
|
||||
COALESCE(SUM(o.total), 0) as total_spent
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.active = true
|
||||
GROUP BY u.id, u.name, u.email
|
||||
HAVING COUNT(o.id) > 0
|
||||
ORDER BY total_spent DESC;
|
||||
|
||||
**With CTE**: "Get top 10 products by sales"
|
||||
→ WITH product_sales AS (
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
SUM(oi.quantity * oi.price) as total_sales
|
||||
FROM products p
|
||||
JOIN order_items oi ON p.id = oi.product_id
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY p.id, p.name
|
||||
)
|
||||
SELECT * FROM product_sales
|
||||
ORDER BY total_sales DESC
|
||||
LIMIT 10;
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the SQL query you need...',
|
||||
generationType: 'sql-query',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
@@ -127,6 +194,73 @@ export const PostgreSQLBlock: BlockConfig<PostgresResponse> = {
|
||||
placeholder: 'SELECT * FROM table_name',
|
||||
condition: { field: 'operation', value: 'execute' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert PostgreSQL database developer. Write PostgreSQL SQL queries based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query.
|
||||
|
||||
### QUERY GUIDELINES
|
||||
1. **Syntax**: Use PostgreSQL-specific syntax and functions
|
||||
2. **Performance**: Write efficient queries with proper indexing considerations
|
||||
3. **Security**: Use parameterized queries when applicable
|
||||
4. **Readability**: Format queries with proper indentation and spacing
|
||||
5. **Best Practices**: Follow PostgreSQL naming conventions
|
||||
|
||||
### POSTGRESQL FEATURES
|
||||
- Use PostgreSQL-specific functions (COALESCE, EXTRACT, etc.)
|
||||
- Leverage advanced features like CTEs, window functions, arrays
|
||||
- Use proper PostgreSQL data types (TEXT, TIMESTAMPTZ, JSONB, etc.)
|
||||
- Include appropriate LIMIT clauses for large result sets
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple Select**: "Get all active users"
|
||||
→ SELECT id, name, email, created_at
|
||||
FROM users
|
||||
WHERE active = true
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
**Complex Join**: "Get users with their order counts and total spent"
|
||||
→ SELECT
|
||||
u.id,
|
||||
u.name,
|
||||
u.email,
|
||||
COUNT(o.id) as order_count,
|
||||
COALESCE(SUM(o.total), 0) as total_spent
|
||||
FROM users u
|
||||
LEFT JOIN orders o ON u.id = o.user_id
|
||||
WHERE u.active = true
|
||||
GROUP BY u.id, u.name, u.email
|
||||
HAVING COUNT(o.id) > 0
|
||||
ORDER BY total_spent DESC;
|
||||
|
||||
**With CTE**: "Get top 10 products by sales"
|
||||
→ WITH product_sales AS (
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
SUM(oi.quantity * oi.price) as total_sales
|
||||
FROM products p
|
||||
JOIN order_items oi ON p.id = oi.product_id
|
||||
JOIN orders o ON oi.order_id = o.id
|
||||
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30 days'
|
||||
GROUP BY p.id, p.name
|
||||
)
|
||||
SELECT * FROM product_sales
|
||||
ORDER BY total_sales DESC
|
||||
LIMIT 10;
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the SQL query - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the SQL query you need...',
|
||||
generationType: 'sql-query',
|
||||
},
|
||||
},
|
||||
// Data for insert operations
|
||||
{
|
||||
|
||||
@@ -94,6 +94,66 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
placeholder: 'id=eq.123',
|
||||
condition: { field: 'operation', value: 'get_row' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert in PostgREST API syntax. Generate PostgREST filter expressions based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the PostgREST filter expression. Do not include any explanations, markdown formatting, or additional text. Just the raw filter expression.
|
||||
|
||||
### POSTGREST FILTER SYNTAX
|
||||
PostgREST uses a specific syntax for filtering data. The format is:
|
||||
column=operator.value
|
||||
|
||||
### OPERATORS
|
||||
- **eq** - equals: \`id=eq.123\`
|
||||
- **neq** - not equals: \`status=neq.inactive\`
|
||||
- **gt** - greater than: \`age=gt.18\`
|
||||
- **gte** - greater than or equal: \`score=gte.80\`
|
||||
- **lt** - less than: \`price=lt.100\`
|
||||
- **lte** - less than or equal: \`rating=lte.5\`
|
||||
- **like** - pattern matching: \`name=like.*john*\`
|
||||
- **ilike** - case-insensitive like: \`email=ilike.*@gmail.com\`
|
||||
- **in** - in list: \`category=in.(tech,science,art)\`
|
||||
- **is** - is null/not null: \`deleted_at=is.null\`
|
||||
- **not** - negation: \`not.and=(status.eq.active,verified.eq.true)\`
|
||||
|
||||
### COMBINING FILTERS
|
||||
- **AND**: Use \`&\` or \`and=(...)\`: \`id=eq.123&status=eq.active\`
|
||||
- **OR**: Use \`or=(...)\`: \`or=(status.eq.active,status.eq.pending)\`
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple equality**: "Find user with ID 123"
|
||||
→ id=eq.123
|
||||
|
||||
**Text search**: "Find users with Gmail addresses"
|
||||
→ email=ilike.*@gmail.com
|
||||
|
||||
**Range filter**: "Find products under $50"
|
||||
→ price=lt.50
|
||||
|
||||
**Multiple conditions**: "Find active users over 18"
|
||||
→ age=gt.18&status=eq.active
|
||||
|
||||
**OR condition**: "Find active or pending orders"
|
||||
→ or=(status.eq.active,status.eq.pending)
|
||||
|
||||
**In list**: "Find posts in specific categories"
|
||||
→ category=in.(tech,science,health)
|
||||
|
||||
**Null check**: "Find users without a profile picture"
|
||||
→ profile_image=is.null
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the PostgREST filter expression - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the filter condition you need...',
|
||||
generationType: 'postgrest',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'filter',
|
||||
@@ -103,6 +163,66 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
placeholder: 'id=eq.123',
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert in PostgREST API syntax. Generate PostgREST filter expressions based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the PostgREST filter expression. Do not include any explanations, markdown formatting, or additional text. Just the raw filter expression.
|
||||
|
||||
### POSTGREST FILTER SYNTAX
|
||||
PostgREST uses a specific syntax for filtering data. The format is:
|
||||
column=operator.value
|
||||
|
||||
### OPERATORS
|
||||
- **eq** - equals: \`id=eq.123\`
|
||||
- **neq** - not equals: \`status=neq.inactive\`
|
||||
- **gt** - greater than: \`age=gt.18\`
|
||||
- **gte** - greater than or equal: \`score=gte.80\`
|
||||
- **lt** - less than: \`price=lt.100\`
|
||||
- **lte** - less than or equal: \`rating=lte.5\`
|
||||
- **like** - pattern matching: \`name=like.*john*\`
|
||||
- **ilike** - case-insensitive like: \`email=ilike.*@gmail.com\`
|
||||
- **in** - in list: \`category=in.(tech,science,art)\`
|
||||
- **is** - is null/not null: \`deleted_at=is.null\`
|
||||
- **not** - negation: \`not.and=(status.eq.active,verified.eq.true)\`
|
||||
|
||||
### COMBINING FILTERS
|
||||
- **AND**: Use \`&\` or \`and=(...)\`: \`id=eq.123&status=eq.active\`
|
||||
- **OR**: Use \`or=(...)\`: \`or=(status.eq.active,status.eq.pending)\`
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple equality**: "Find user with ID 123"
|
||||
→ id=eq.123
|
||||
|
||||
**Text search**: "Find users with Gmail addresses"
|
||||
→ email=ilike.*@gmail.com
|
||||
|
||||
**Range filter**: "Find products under $50"
|
||||
→ price=lt.50
|
||||
|
||||
**Multiple conditions**: "Find active users over 18"
|
||||
→ age=gt.18&status=eq.active
|
||||
|
||||
**OR condition**: "Find active or pending orders"
|
||||
→ or=(status.eq.active,status.eq.pending)
|
||||
|
||||
**In list**: "Find posts in specific categories"
|
||||
→ category=in.(tech,science,health)
|
||||
|
||||
**Null check**: "Find users without a profile picture"
|
||||
→ profile_image=is.null
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the PostgREST filter expression - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the filter condition you need...',
|
||||
generationType: 'postgrest',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'filter',
|
||||
@@ -112,6 +232,66 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
placeholder: 'id=eq.123',
|
||||
condition: { field: 'operation', value: 'delete' },
|
||||
required: true,
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert in PostgREST API syntax. Generate PostgREST filter expressions based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the PostgREST filter expression. Do not include any explanations, markdown formatting, or additional text. Just the raw filter expression.
|
||||
|
||||
### POSTGREST FILTER SYNTAX
|
||||
PostgREST uses a specific syntax for filtering data. The format is:
|
||||
column=operator.value
|
||||
|
||||
### OPERATORS
|
||||
- **eq** - equals: \`id=eq.123\`
|
||||
- **neq** - not equals: \`status=neq.inactive\`
|
||||
- **gt** - greater than: \`age=gt.18\`
|
||||
- **gte** - greater than or equal: \`score=gte.80\`
|
||||
- **lt** - less than: \`price=lt.100\`
|
||||
- **lte** - less than or equal: \`rating=lte.5\`
|
||||
- **like** - pattern matching: \`name=like.*john*\`
|
||||
- **ilike** - case-insensitive like: \`email=ilike.*@gmail.com\`
|
||||
- **in** - in list: \`category=in.(tech,science,art)\`
|
||||
- **is** - is null/not null: \`deleted_at=is.null\`
|
||||
- **not** - negation: \`not.and=(status.eq.active,verified.eq.true)\`
|
||||
|
||||
### COMBINING FILTERS
|
||||
- **AND**: Use \`&\` or \`and=(...)\`: \`id=eq.123&status=eq.active\`
|
||||
- **OR**: Use \`or=(...)\`: \`or=(status.eq.active,status.eq.pending)\`
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple equality**: "Find user with ID 123"
|
||||
→ id=eq.123
|
||||
|
||||
**Text search**: "Find users with Gmail addresses"
|
||||
→ email=ilike.*@gmail.com
|
||||
|
||||
**Range filter**: "Find products under $50"
|
||||
→ price=lt.50
|
||||
|
||||
**Multiple conditions**: "Find active users over 18"
|
||||
→ age=gt.18&status=eq.active
|
||||
|
||||
**OR condition**: "Find active or pending orders"
|
||||
→ or=(status.eq.active,status.eq.pending)
|
||||
|
||||
**In list**: "Find posts in specific categories"
|
||||
→ category=in.(tech,science,health)
|
||||
|
||||
**Null check**: "Find users without a profile picture"
|
||||
→ profile_image=is.null
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the PostgREST filter expression - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the filter condition you need...',
|
||||
generationType: 'postgrest',
|
||||
},
|
||||
},
|
||||
// Optional filter for query operation
|
||||
{
|
||||
@@ -121,6 +301,66 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
|
||||
layout: 'full',
|
||||
placeholder: 'status=eq.active',
|
||||
condition: { field: 'operation', value: 'query' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert in PostgREST API syntax. Generate PostgREST filter expressions based on the user's request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the PostgREST filter expression. Do not include any explanations, markdown formatting, or additional text. Just the raw filter expression.
|
||||
|
||||
### POSTGREST FILTER SYNTAX
|
||||
PostgREST uses a specific syntax for filtering data. The format is:
|
||||
column=operator.value
|
||||
|
||||
### OPERATORS
|
||||
- **eq** - equals: \`id=eq.123\`
|
||||
- **neq** - not equals: \`status=neq.inactive\`
|
||||
- **gt** - greater than: \`age=gt.18\`
|
||||
- **gte** - greater than or equal: \`score=gte.80\`
|
||||
- **lt** - less than: \`price=lt.100\`
|
||||
- **lte** - less than or equal: \`rating=lte.5\`
|
||||
- **like** - pattern matching: \`name=like.*john*\`
|
||||
- **ilike** - case-insensitive like: \`email=ilike.*@gmail.com\`
|
||||
- **in** - in list: \`category=in.(tech,science,art)\`
|
||||
- **is** - is null/not null: \`deleted_at=is.null\`
|
||||
- **not** - negation: \`not.and=(status.eq.active,verified.eq.true)\`
|
||||
|
||||
### COMBINING FILTERS
|
||||
- **AND**: Use \`&\` or \`and=(...)\`: \`id=eq.123&status=eq.active\`
|
||||
- **OR**: Use \`or=(...)\`: \`or=(status.eq.active,status.eq.pending)\`
|
||||
|
||||
### EXAMPLES
|
||||
|
||||
**Simple equality**: "Find user with ID 123"
|
||||
→ id=eq.123
|
||||
|
||||
**Text search**: "Find users with Gmail addresses"
|
||||
→ email=ilike.*@gmail.com
|
||||
|
||||
**Range filter**: "Find products under $50"
|
||||
→ price=lt.50
|
||||
|
||||
**Multiple conditions**: "Find active users over 18"
|
||||
→ age=gt.18&status=eq.active
|
||||
|
||||
**OR condition**: "Find active or pending orders"
|
||||
→ or=(status.eq.active,status.eq.pending)
|
||||
|
||||
**In list**: "Find posts in specific categories"
|
||||
→ category=in.(tech,science,health)
|
||||
|
||||
**Null check**: "Find users without a profile picture"
|
||||
→ profile_image=is.null
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the PostgREST filter expression - no explanations, no markdown, no extra text.`,
|
||||
placeholder: 'Describe the filter condition...',
|
||||
generationType: 'postgrest',
|
||||
},
|
||||
},
|
||||
// Optional order by for query operation
|
||||
{
|
||||
|
||||
@@ -17,6 +17,8 @@ export type GenerationType =
|
||||
| 'json-object'
|
||||
| 'system-prompt'
|
||||
| 'custom-tool-schema'
|
||||
| 'sql-query'
|
||||
| 'postgrest'
|
||||
|
||||
// SubBlock types
|
||||
export type SubBlockType =
|
||||
|
||||
@@ -70,7 +70,7 @@ export const deleteTool: ToolConfig<MySQLDeleteParams, MySQLResponse> = {
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
ssl: params.ssl || 'preferred',
|
||||
ssl: params.ssl || 'required',
|
||||
table: params.table,
|
||||
where: params.where,
|
||||
}),
|
||||
|
||||
@@ -64,7 +64,7 @@ export const executeTool: ToolConfig<MySQLExecuteParams, MySQLResponse> = {
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
ssl: params.ssl || 'preferred',
|
||||
ssl: params.ssl || 'required',
|
||||
query: params.query,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@ export const insertTool: ToolConfig<MySQLInsertParams, MySQLResponse> = {
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
ssl: params.ssl || 'preferred',
|
||||
ssl: params.ssl || 'required',
|
||||
table: params.table,
|
||||
data: params.data,
|
||||
}),
|
||||
|
||||
@@ -64,7 +64,7 @@ export const queryTool: ToolConfig<MySQLQueryParams, MySQLResponse> = {
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
ssl: params.ssl || 'preferred',
|
||||
ssl: params.ssl || 'required',
|
||||
query: params.query,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -76,7 +76,7 @@ export const updateTool: ToolConfig<MySQLUpdateParams, MySQLResponse> = {
|
||||
database: params.database,
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
ssl: params.ssl || 'preferred',
|
||||
ssl: params.ssl || 'required',
|
||||
table: params.table,
|
||||
data: params.data,
|
||||
where: params.where,
|
||||
|
||||
Reference in New Issue
Block a user