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:
Waleed
2025-08-29 18:32:07 -07:00
committed by GitHub
parent 813a0fb741
commit 4310dd6c15
23 changed files with 754 additions and 126 deletions

View File

@@ -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()

View File

@@ -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}`)

View File

@@ -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(

View File

@@ -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}`)

View File

@@ -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()

View File

@@ -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}\``
}

View File

@@ -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()

View File

@@ -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}`)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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, [])

View File

@@ -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'

View File

@@ -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>
</>
)
}

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -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
{

View File

@@ -17,6 +17,8 @@ export type GenerationType =
| 'json-object'
| 'system-prompt'
| 'custom-tool-schema'
| 'sql-query'
| 'postgrest'
// SubBlock types
export type SubBlockType =

View File

@@ -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,
}),

View File

@@ -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,
}),
},

View File

@@ -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,
}),

View File

@@ -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,
}),
},

View File

@@ -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,