From 4310dd6c15593cdf9a976ee99f15ed3e6dbcdb1d Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 29 Aug 2025 18:32:07 -0700 Subject: [PATCH] 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 --- apps/sim/app/api/tools/mysql/delete/route.ts | 3 +- apps/sim/app/api/tools/mysql/execute/route.ts | 4 +- apps/sim/app/api/tools/mysql/insert/route.ts | 6 +- apps/sim/app/api/tools/mysql/query/route.ts | 4 +- apps/sim/app/api/tools/mysql/update/route.ts | 3 +- apps/sim/app/api/tools/mysql/utils.ts | 30 +- .../app/api/tools/postgresql/delete/route.ts | 3 +- .../app/api/tools/postgresql/execute/route.ts | 4 +- .../app/api/tools/postgresql/insert/route.ts | 3 +- .../app/api/tools/postgresql/query/route.ts | 3 +- .../app/api/tools/postgresql/update/route.ts | 3 +- apps/sim/app/api/tools/postgresql/utils.ts | 27 +- .../components/sub-block/components/code.tsx | 4 +- .../sub-block/components/short-input.tsx | 265 ++++++++++++------ apps/sim/blocks/blocks/mysql.ts | 132 +++++++++ apps/sim/blocks/blocks/postgresql.ts | 134 +++++++++ apps/sim/blocks/blocks/supabase.ts | 240 ++++++++++++++++ apps/sim/blocks/types.ts | 2 + apps/sim/tools/mysql/delete.ts | 2 +- apps/sim/tools/mysql/execute.ts | 2 +- apps/sim/tools/mysql/insert.ts | 2 +- apps/sim/tools/mysql/query.ts | 2 +- apps/sim/tools/mysql/update.ts | 2 +- 23 files changed, 754 insertions(+), 126 deletions(-) diff --git a/apps/sim/app/api/tools/mysql/delete/route.ts b/apps/sim/app/api/tools/mysql/delete/route.ts index d473dae9d..583219439 100644 --- a/apps/sim/app/api/tools/mysql/delete/route.ts +++ b/apps/sim/app/api/tools/mysql/delete/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/mysql/execute/route.ts b/apps/sim/app/api/tools/mysql/execute/route.ts index 30d59025c..cd4dd52f9 100644 --- a/apps/sim/app/api/tools/mysql/execute/route.ts +++ b/apps/sim/app/api/tools/mysql/execute/route.ts @@ -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}`) diff --git a/apps/sim/app/api/tools/mysql/insert/route.ts b/apps/sim/app/api/tools/mysql/insert/route.ts index 497d8cf5f..e071b250a 100644 --- a/apps/sim/app/api/tools/mysql/insert/route.ts +++ b/apps/sim/app/api/tools/mysql/insert/route.ts @@ -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( diff --git a/apps/sim/app/api/tools/mysql/query/route.ts b/apps/sim/app/api/tools/mysql/query/route.ts index 56b6f2960..d2465f4e0 100644 --- a/apps/sim/app/api/tools/mysql/query/route.ts +++ b/apps/sim/app/api/tools/mysql/query/route.ts @@ -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}`) diff --git a/apps/sim/app/api/tools/mysql/update/route.ts b/apps/sim/app/api/tools/mysql/update/route.ts index dcf5fd507..d391e2ba8 100644 --- a/apps/sim/app/api/tools/mysql/update/route.ts +++ b/apps/sim/app/api/tools/mysql/update/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/mysql/utils.ts b/apps/sim/app/api/tools/mysql/utils.ts index 29d84339f..36d6128cc 100644 --- a/apps/sim/app/api/tools/mysql/utils.ts +++ b/apps/sim/app/api/tools/mysql/utils.ts @@ -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) { } export function buildUpdateQuery(table: string, data: Record, 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, 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}\`` } diff --git a/apps/sim/app/api/tools/postgresql/delete/route.ts b/apps/sim/app/api/tools/postgresql/delete/route.ts index 29670f45c..2270381b1 100644 --- a/apps/sim/app/api/tools/postgresql/delete/route.ts +++ b/apps/sim/app/api/tools/postgresql/delete/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/postgresql/execute/route.ts b/apps/sim/app/api/tools/postgresql/execute/route.ts index b7c529602..f60a65030 100644 --- a/apps/sim/app/api/tools/postgresql/execute/route.ts +++ b/apps/sim/app/api/tools/postgresql/execute/route.ts @@ -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}`) diff --git a/apps/sim/app/api/tools/postgresql/insert/route.ts b/apps/sim/app/api/tools/postgresql/insert/route.ts index 95a2e70af..9bd322e03 100644 --- a/apps/sim/app/api/tools/postgresql/insert/route.ts +++ b/apps/sim/app/api/tools/postgresql/insert/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/postgresql/query/route.ts b/apps/sim/app/api/tools/postgresql/query/route.ts index 5dfe10c2d..85dd56b6c 100644 --- a/apps/sim/app/api/tools/postgresql/query/route.ts +++ b/apps/sim/app/api/tools/postgresql/query/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/postgresql/update/route.ts b/apps/sim/app/api/tools/postgresql/update/route.ts index b6d4b2054..32812e883 100644 --- a/apps/sim/app/api/tools/postgresql/update/route.ts +++ b/apps/sim/app/api/tools/postgresql/update/route.ts @@ -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() diff --git a/apps/sim/app/api/tools/postgresql/utils.ts b/apps/sim/app/api/tools/postgresql/utils.ts index 78f5561fa..98771d382 100644 --- a/apps/sim/app/api/tools/postgresql/utils.ts +++ b/apps/sim/app/api/tools/postgresql/utils.ts @@ -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, 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, []) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx index da05303d3..bc6714211 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/code.tsx @@ -385,7 +385,7 @@ export function Code({
- {!isCollapsed && !isAiStreaming && !isPreview && ( + {wandConfig?.enabled && !isCollapsed && !isAiStreaming && !isPreview && ( +
+ )} + + {!wandHook?.isStreaming && ( + <> + { + setShowEnvVars(false) + setSearchTerm('') + }} + /> + { + setShowTags(false) + setActiveSourceBlockId(null) + }} + /> + + )} +
+ ) } diff --git a/apps/sim/blocks/blocks/mysql.ts b/apps/sim/blocks/blocks/mysql.ts index 957665dfd..6bfb41115 100644 --- a/apps/sim/blocks/blocks/mysql.ts +++ b/apps/sim/blocks/blocks/mysql.ts @@ -118,6 +118,72 @@ export const MySQLBlock: BlockConfig = { 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 = { 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 { diff --git a/apps/sim/blocks/blocks/postgresql.ts b/apps/sim/blocks/blocks/postgresql.ts index 1f489c984..6aa5e174c 100644 --- a/apps/sim/blocks/blocks/postgresql.ts +++ b/apps/sim/blocks/blocks/postgresql.ts @@ -118,6 +118,73 @@ export const PostgreSQLBlock: BlockConfig = { 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 = { 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 { diff --git a/apps/sim/blocks/blocks/supabase.ts b/apps/sim/blocks/blocks/supabase.ts index 121f644dc..2b45deeee 100644 --- a/apps/sim/blocks/blocks/supabase.ts +++ b/apps/sim/blocks/blocks/supabase.ts @@ -94,6 +94,66 @@ export const SupabaseBlock: BlockConfig = { 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 = { 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 = { 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 = { 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 { diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 4917633c4..2a417dbb5 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -17,6 +17,8 @@ export type GenerationType = | 'json-object' | 'system-prompt' | 'custom-tool-schema' + | 'sql-query' + | 'postgrest' // SubBlock types export type SubBlockType = diff --git a/apps/sim/tools/mysql/delete.ts b/apps/sim/tools/mysql/delete.ts index d140f1f98..35bcb5341 100644 --- a/apps/sim/tools/mysql/delete.ts +++ b/apps/sim/tools/mysql/delete.ts @@ -70,7 +70,7 @@ export const deleteTool: ToolConfig = { database: params.database, username: params.username, password: params.password, - ssl: params.ssl || 'preferred', + ssl: params.ssl || 'required', table: params.table, where: params.where, }), diff --git a/apps/sim/tools/mysql/execute.ts b/apps/sim/tools/mysql/execute.ts index 27c7f6faf..933162e56 100644 --- a/apps/sim/tools/mysql/execute.ts +++ b/apps/sim/tools/mysql/execute.ts @@ -64,7 +64,7 @@ export const executeTool: ToolConfig = { database: params.database, username: params.username, password: params.password, - ssl: params.ssl || 'preferred', + ssl: params.ssl || 'required', query: params.query, }), }, diff --git a/apps/sim/tools/mysql/insert.ts b/apps/sim/tools/mysql/insert.ts index 4c6845e8e..00f1dba7e 100644 --- a/apps/sim/tools/mysql/insert.ts +++ b/apps/sim/tools/mysql/insert.ts @@ -70,7 +70,7 @@ export const insertTool: ToolConfig = { database: params.database, username: params.username, password: params.password, - ssl: params.ssl || 'preferred', + ssl: params.ssl || 'required', table: params.table, data: params.data, }), diff --git a/apps/sim/tools/mysql/query.ts b/apps/sim/tools/mysql/query.ts index cac2a2dd1..4da704057 100644 --- a/apps/sim/tools/mysql/query.ts +++ b/apps/sim/tools/mysql/query.ts @@ -64,7 +64,7 @@ export const queryTool: ToolConfig = { database: params.database, username: params.username, password: params.password, - ssl: params.ssl || 'preferred', + ssl: params.ssl || 'required', query: params.query, }), }, diff --git a/apps/sim/tools/mysql/update.ts b/apps/sim/tools/mysql/update.ts index 85859ca09..17fbf9ae6 100644 --- a/apps/sim/tools/mysql/update.ts +++ b/apps/sim/tools/mysql/update.ts @@ -76,7 +76,7 @@ export const updateTool: ToolConfig = { 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,