diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index c954de07f..facf63c97 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -33,12 +33,15 @@ "microsoft_planner", "microsoft_teams", "mistral_parse", + "mysql", "notion", "onedrive", "openai", "outlook", + "parallel_ai", "perplexity", "pinecone", + "postgresql", "qdrant", "reddit", "s3", diff --git a/apps/docs/content/docs/tools/mysql.mdx b/apps/docs/content/docs/tools/mysql.mdx new file mode 100644 index 000000000..14125a962 --- /dev/null +++ b/apps/docs/content/docs/tools/mysql.mdx @@ -0,0 +1,180 @@ +--- +title: MySQL +description: Connect to MySQL database +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +The [MySQL](https://www.mysql.com/) tool enables you to connect to any MySQL database and perform a wide range of database operations directly within your agentic workflows. With secure connection handling and flexible configuration, you can easily manage and interact with your data. + +With the MySQL tool, you can: + +- **Query data**: Execute SELECT queries to retrieve data from your MySQL tables using the `mysql_query` operation. +- **Insert records**: Add new rows to your tables with the `mysql_insert` operation by specifying the table and data to insert. +- **Update records**: Modify existing data in your tables using the `mysql_update` operation, providing the table, new data, and WHERE conditions. +- **Delete records**: Remove rows from your tables with the `mysql_delete` operation, specifying the table and WHERE conditions. +- **Execute raw SQL**: Run any custom SQL command using the `mysql_execute` operation for advanced use cases. + +The MySQL tool is ideal for scenarios where your agents need to interact with structured data—such as automating reporting, syncing data between systems, or powering data-driven workflows. It streamlines database access, making it easy to read, write, and manage your MySQL data programmatically. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect to any MySQL database to execute queries, manage data, and perform database operations. Supports SELECT, INSERT, UPDATE, DELETE operations with secure connection handling. + + + +## Tools + +### `mysql_query` + +Execute SELECT query on MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | SQL SELECT query to execute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | + +### `mysql_insert` + +Insert new record into MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to insert into | +| `data` | object | Yes | Data to insert as key-value pairs | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of inserted rows | +| `rowCount` | number | Number of rows inserted | + +### `mysql_update` + +Update existing records in MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to update | +| `data` | object | Yes | Data to update as key-value pairs | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of updated rows | +| `rowCount` | number | Number of rows updated | + +### `mysql_delete` + +Delete records from MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to delete from | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of deleted rows | +| `rowCount` | number | Number of rows deleted | + +### `mysql_execute` + +Execute raw SQL query on MySQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | Raw SQL query to execute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows affected | + + + +## Notes + +- Category: `tools` +- Type: `mysql` diff --git a/apps/docs/content/docs/tools/parallel_ai.mdx b/apps/docs/content/docs/tools/parallel_ai.mdx new file mode 100644 index 000000000..39b8730dd --- /dev/null +++ b/apps/docs/content/docs/tools/parallel_ai.mdx @@ -0,0 +1,106 @@ +--- +title: Parallel AI +description: Search with Parallel AI +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +[Parallel AI](https://parallel.ai/) is an advanced web search and content extraction platform designed to deliver comprehensive, high-quality results for any query. By leveraging intelligent processing and large-scale data extraction, Parallel AI enables users and agents to access, analyze, and synthesize information from across the web with speed and accuracy. + +With Parallel AI, you can: + +- **Search the web intelligently**: Retrieve relevant, up-to-date information from a wide range of sources +- **Extract and summarize content**: Get concise, meaningful excerpts from web pages and documents +- **Customize search objectives**: Tailor queries to specific needs or questions for targeted results +- **Process results at scale**: Handle large volumes of search results with advanced processing options +- **Integrate with workflows**: Use Parallel AI within Sim to automate research, content gathering, and knowledge extraction +- **Control output granularity**: Specify the number of results and the amount of content per result +- **Secure API access**: Protect your searches and data with API key authentication + +In Sim, the Parallel AI integration empowers your agents to perform web searches and extract content programmatically. This enables powerful automation scenarios such as real-time research, competitive analysis, content monitoring, and knowledge base creation. By connecting Sim with Parallel AI, you unlock the ability for agents to gather, process, and utilize web data as part of your automated workflows. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Search the web using Parallel AI's advanced search capabilities. Get comprehensive results with intelligent processing and content extraction. + + + +## Tools + +### `parallel_search` + +Search the web using Parallel AI. Provides comprehensive search results with intelligent processing and content extraction. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `objective` | string | Yes | The search objective or question to answer | +| `search_queries` | string | No | Optional comma-separated list of search queries to execute | +| `processor` | string | No | Processing method: base or pro \(default: base\) | +| `max_results` | number | No | Maximum number of results to return \(default: 5\) | +| `max_chars_per_result` | number | No | Maximum characters per result \(default: 1500\) | +| `apiKey` | string | Yes | Parallel AI API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | array | Search results with excerpts from relevant pages | + + + +## Notes + +- Category: `tools` +- Type: `parallel_ai` diff --git a/apps/docs/content/docs/tools/postgresql.mdx b/apps/docs/content/docs/tools/postgresql.mdx new file mode 100644 index 000000000..e79d5661e --- /dev/null +++ b/apps/docs/content/docs/tools/postgresql.mdx @@ -0,0 +1,188 @@ +--- +title: PostgreSQL +description: Connect to PostgreSQL database +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +The [PostgreSQL](https://www.postgresql.org/) tool enables you to connect to any PostgreSQL database and perform a wide range of database operations directly within your agentic workflows. With secure connection handling and flexible configuration, you can easily manage and interact with your data. + +With the PostgreSQL tool, you can: + +- **Query data**: Execute SELECT queries to retrieve data from your PostgreSQL tables using the `postgresql_query` operation. +- **Insert records**: Add new rows to your tables with the `postgresql_insert` operation by specifying the table and data to insert. +- **Update records**: Modify existing data in your tables using the `postgresql_update` operation, providing the table, new data, and WHERE conditions. +- **Delete records**: Remove rows from your tables with the `postgresql_delete` operation, specifying the table and WHERE conditions. +- **Execute raw SQL**: Run any custom SQL command using the `postgresql_execute` operation for advanced use cases. + +The PostgreSQL tool is ideal for scenarios where your agents need to interact with structured data—such as automating reporting, syncing data between systems, or powering data-driven workflows. It streamlines database access, making it easy to read, write, and manage your PostgreSQL data programmatically. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect to any PostgreSQL database to execute queries, manage data, and perform database operations. Supports SELECT, INSERT, UPDATE, DELETE operations with secure connection handling. + + + +## Tools + +### `postgresql_query` + +Execute a SELECT query on PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | SQL SELECT query to execute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows returned | + +### `postgresql_insert` + +Insert data into PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to insert data into | +| `data` | object | Yes | Data object to insert \(key-value pairs\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Inserted data \(if RETURNING clause used\) | +| `rowCount` | number | Number of rows inserted | + +### `postgresql_update` + +Update data in PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to update data in | +| `data` | object | Yes | Data object with fields to update \(key-value pairs\) | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Updated data \(if RETURNING clause used\) | +| `rowCount` | number | Number of rows updated | + +### `postgresql_delete` + +Delete data from PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `table` | string | Yes | Table name to delete data from | +| `where` | string | Yes | WHERE clause condition \(without WHERE keyword\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Deleted data \(if RETURNING clause used\) | +| `rowCount` | number | Number of rows deleted | + +### `postgresql_execute` + +Execute raw SQL query on PostgreSQL database + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `query` | string | Yes | Raw SQL query to execute | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `rows` | array | Array of rows returned from the query | +| `rowCount` | number | Number of rows affected | + + + +## Notes + +- Category: `tools` +- Type: `postgresql` diff --git a/apps/sim/app/api/tools/mysql/delete/route.ts b/apps/sim/app/api/tools/mysql/delete/route.ts new file mode 100644 index 000000000..d473dae9d --- /dev/null +++ b/apps/sim/app/api/tools/mysql/delete/route.ts @@ -0,0 +1,67 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils' + +const logger = createLogger('MySQLDeleteAPI') + +const DeleteSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + table: z.string().min(1, 'Table name is required'), + where: z.string().min(1, 'WHERE clause is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = DeleteSchema.parse(body) + + logger.info( + `[${requestId}] Deleting data from ${params.table} on ${params.host}:${params.port}/${params.database}` + ) + + const connection = await createMySQLConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const { query, values } = buildDeleteQuery(params.table, params.where) + const result = await executeQuery(connection, query, values) + + logger.info(`[${requestId}] Delete executed successfully, ${result.rowCount} row(s) deleted`) + + return NextResponse.json({ + message: `Data deleted successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await connection.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MySQL delete failed:`, error) + + return NextResponse.json({ error: `MySQL delete failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/mysql/execute/route.ts b/apps/sim/app/api/tools/mysql/execute/route.ts new file mode 100644 index 000000000..30d59025c --- /dev/null +++ b/apps/sim/app/api/tools/mysql/execute/route.ts @@ -0,0 +1,75 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils' + +const logger = createLogger('MySQLExecuteAPI') + +const ExecuteSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + query: z.string().min(1, 'Query is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = ExecuteSchema.parse(body) + + logger.info( + `[${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}`) + return NextResponse.json( + { error: `Query validation failed: ${validation.error}` }, + { status: 400 } + ) + } + + const connection = await createMySQLConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const result = await executeQuery(connection, params.query) + + logger.info(`[${requestId}] SQL executed successfully, ${result.rowCount} row(s) affected`) + + return NextResponse.json({ + message: `SQL executed successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await connection.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MySQL execute failed:`, error) + + return NextResponse.json({ error: `MySQL execute failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/mysql/insert/route.ts b/apps/sim/app/api/tools/mysql/insert/route.ts new file mode 100644 index 000000000..497d8cf5f --- /dev/null +++ b/apps/sim/app/api/tools/mysql/insert/route.ts @@ -0,0 +1,91 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils' + +const logger = createLogger('MySQLInsertAPI') + +const InsertSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + table: z.string().min(1, 'Table name is required'), + data: z.union([ + z + .record(z.unknown()) + .refine((obj) => Object.keys(obj).length > 0, 'Data object cannot be empty'), + z + .string() + .min(1) + .transform((str) => { + try { + const parsed = JSON.parse(str) + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error('Data must be a JSON object') + } + return parsed + } catch (e) { + const errorMsg = e instanceof Error ? e.message : 'Unknown error' + throw new Error( + `Invalid JSON format in data field: ${errorMsg}. Received: ${str.substring(0, 100)}...` + ) + } + }), + ]), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.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( + `[${requestId}] Inserting data into ${params.table} on ${params.host}:${params.port}/${params.database}` + ) + + const connection = await createMySQLConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const { query, values } = buildInsertQuery(params.table, params.data) + const result = await executeQuery(connection, query, values) + + logger.info(`[${requestId}] Insert executed successfully, ${result.rowCount} row(s) inserted`) + + return NextResponse.json({ + message: `Data inserted successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await connection.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MySQL insert failed:`, error) + + return NextResponse.json({ error: `MySQL insert failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/mysql/query/route.ts b/apps/sim/app/api/tools/mysql/query/route.ts new file mode 100644 index 000000000..56b6f2960 --- /dev/null +++ b/apps/sim/app/api/tools/mysql/query/route.ts @@ -0,0 +1,75 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils' + +const logger = createLogger('MySQLQueryAPI') + +const QuerySchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + query: z.string().min(1, 'Query is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = QuerySchema.parse(body) + + logger.info( + `[${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}`) + return NextResponse.json( + { error: `Query validation failed: ${validation.error}` }, + { status: 400 } + ) + } + + const connection = await createMySQLConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const result = await executeQuery(connection, params.query) + + logger.info(`[${requestId}] Query executed successfully, returned ${result.rowCount} rows`) + + return NextResponse.json({ + message: `Query executed successfully. ${result.rowCount} row(s) returned.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await connection.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MySQL query failed:`, error) + + return NextResponse.json({ error: `MySQL query failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/mysql/update/route.ts b/apps/sim/app/api/tools/mysql/update/route.ts new file mode 100644 index 000000000..dcf5fd507 --- /dev/null +++ b/apps/sim/app/api/tools/mysql/update/route.ts @@ -0,0 +1,86 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils' + +const logger = createLogger('MySQLUpdateAPI') + +const UpdateSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + table: z.string().min(1, 'Table name is required'), + data: z.union([ + z + .record(z.unknown()) + .refine((obj) => Object.keys(obj).length > 0, 'Data object cannot be empty'), + z + .string() + .min(1) + .transform((str) => { + try { + const parsed = JSON.parse(str) + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error('Data must be a JSON object') + } + return parsed + } catch (e) { + throw new Error('Invalid JSON format in data field') + } + }), + ]), + where: z.string().min(1, 'WHERE clause is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = UpdateSchema.parse(body) + + logger.info( + `[${requestId}] Updating data in ${params.table} on ${params.host}:${params.port}/${params.database}` + ) + + const connection = await createMySQLConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const { query, values } = buildUpdateQuery(params.table, params.data, params.where) + const result = await executeQuery(connection, query, values) + + logger.info(`[${requestId}] Update executed successfully, ${result.rowCount} row(s) updated`) + + return NextResponse.json({ + message: `Data updated successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await connection.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MySQL update failed:`, error) + + return NextResponse.json({ error: `MySQL update failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/mysql/utils.ts b/apps/sim/app/api/tools/mysql/utils.ts new file mode 100644 index 000000000..29d84339f --- /dev/null +++ b/apps/sim/app/api/tools/mysql/utils.ts @@ -0,0 +1,159 @@ +import mysql from 'mysql2/promise' + +export interface MySQLConnectionConfig { + host: string + port: number + database: string + username: string + password: string + ssl?: string +} + +export async function createMySQLConnection(config: MySQLConnectionConfig) { + const connectionConfig: mysql.ConnectionOptions = { + host: config.host, + port: config.port, + database: config.database, + user: config.username, + 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) +} + +export async function executeQuery( + connection: mysql.Connection, + query: string, + values?: unknown[] +) { + const [rows, fields] = await connection.execute(query, values) + + if (Array.isArray(rows)) { + return { + rows: rows as unknown[], + rowCount: rows.length, + fields, + } + } + + return { + rows: [], + rowCount: (rows as mysql.ResultSetHeader).affectedRows || 0, + fields, + } +} + +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, + /drop\s+user/i, + /create\s+user/i, + /grant\s+/i, + /revoke\s+/i, + /alter\s+user/i, + /set\s+global/i, + /set\s+session/i, + /load\s+data/i, + /into\s+outfile/i, + /into\s+dumpfile/i, + /load_file\s*\(/i, + /system\s+/i, + /exec\s+/i, + /execute\s+immediate/i, + /xp_cmdshell/i, + /sp_configure/i, + /information_schema\.tables/i, + /mysql\.user/i, + /mysql\.db/i, + /mysql\.host/i, + /performance_schema/i, + /sys\./i, + ] + + for (const pattern of dangerousPatterns) { + if (pattern.test(query)) { + return { + isValid: false, + error: `Query contains potentially dangerous operation: ${pattern.source}`, + } + } + } + + // 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 { + isValid: false, + error: + 'Only SELECT, INSERT, UPDATE, DELETE, WITH, SHOW, DESCRIBE, and EXPLAIN statements are allowed', + } + } + + return { isValid: true } +} + +export function buildInsertQuery(table: string, data: Record) { + const sanitizedTable = sanitizeIdentifier(table) + const columns = Object.keys(data) + const values = Object.values(data) + const placeholders = columns.map(() => '?').join(', ') + + const query = `INSERT INTO ${sanitizedTable} (${columns.map(sanitizeIdentifier).join(', ')}) VALUES (${placeholders})` + + return { query, values } +} + +export function buildUpdateQuery(table: string, data: Record, where: string) { + const sanitizedTable = sanitizeIdentifier(table) + const columns = Object.keys(data) + const values = Object.values(data) + + const setClause = columns.map((col) => `${sanitizeIdentifier(col)} = ?`).join(', ') + const query = `UPDATE ${sanitizedTable} SET ${setClause} WHERE ${where}` + + return { query, values } +} + +export function buildDeleteQuery(table: string, where: string) { + const sanitizedTable = sanitizeIdentifier(table) + const query = `DELETE FROM ${sanitizedTable} WHERE ${where}` + + return { query, values: [] } +} + +export function sanitizeIdentifier(identifier: string): string { + // Handle schema.table format + if (identifier.includes('.')) { + const parts = identifier.split('.') + return parts.map((part) => sanitizeSingleIdentifier(part)).join('.') + } + + return sanitizeSingleIdentifier(identifier) +} + +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 new file mode 100644 index 000000000..da13eabb5 --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/delete/route.ts @@ -0,0 +1,74 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + buildDeleteQuery, + createPostgresConnection, + executeQuery, +} from '@/app/api/tools/postgresql/utils' + +const logger = createLogger('PostgreSQLDeleteAPI') + +const DeleteSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + table: z.string().min(1, 'Table name is required'), + where: z.string().min(1, 'WHERE clause is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = DeleteSchema.parse(body) + + logger.info( + `[${requestId}] Deleting data from ${params.table} on ${params.host}:${params.port}/${params.database}` + ) + + const client = await createPostgresConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const { query, values } = buildDeleteQuery(params.table, params.where) + const result = await executeQuery(client, query, values) + + logger.info(`[${requestId}] Delete executed successfully, ${result.rowCount} row(s) deleted`) + + return NextResponse.json({ + message: `Data deleted successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] PostgreSQL delete failed:`, error) + + return NextResponse.json( + { error: `PostgreSQL delete failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/postgresql/execute/route.ts b/apps/sim/app/api/tools/postgresql/execute/route.ts new file mode 100644 index 000000000..a1eeb247d --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/execute/route.ts @@ -0,0 +1,82 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + createPostgresConnection, + executeQuery, + validateQuery, +} from '@/app/api/tools/postgresql/utils' + +const logger = createLogger('PostgreSQLExecuteAPI') + +const ExecuteSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + query: z.string().min(1, 'Query is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = ExecuteSchema.parse(body) + + logger.info( + `[${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}`) + return NextResponse.json( + { error: `Query validation failed: ${validation.error}` }, + { status: 400 } + ) + } + + const client = await createPostgresConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const result = await executeQuery(client, params.query) + + logger.info(`[${requestId}] SQL executed successfully, ${result.rowCount} row(s) affected`) + + return NextResponse.json({ + message: `SQL executed successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] PostgreSQL execute failed:`, error) + + return NextResponse.json( + { error: `PostgreSQL execute failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/postgresql/insert/route.ts b/apps/sim/app/api/tools/postgresql/insert/route.ts new file mode 100644 index 000000000..aa8cffaf6 --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/insert/route.ts @@ -0,0 +1,99 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + buildInsertQuery, + createPostgresConnection, + executeQuery, +} from '@/app/api/tools/postgresql/utils' + +const logger = createLogger('PostgreSQLInsertAPI') + +const InsertSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + table: z.string().min(1, 'Table name is required'), + data: z.union([ + z + .record(z.unknown()) + .refine((obj) => Object.keys(obj).length > 0, 'Data object cannot be empty'), + z + .string() + .min(1) + .transform((str) => { + try { + const parsed = JSON.parse(str) + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error('Data must be a JSON object') + } + return parsed + } catch (e) { + const errorMsg = e instanceof Error ? e.message : 'Unknown error' + throw new Error( + `Invalid JSON format in data field: ${errorMsg}. Received: ${str.substring(0, 100)}...` + ) + } + }), + ]), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + + // Debug: Log the data field to see what we're getting + logger.info(`[${requestId}] Received data field type: ${typeof body.data}, value:`, body.data) + + const params = InsertSchema.parse(body) + + logger.info( + `[${requestId}] Inserting data into ${params.table} on ${params.host}:${params.port}/${params.database}` + ) + + const client = await createPostgresConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const { query, values } = buildInsertQuery(params.table, params.data) + const result = await executeQuery(client, query, values) + + logger.info(`[${requestId}] Insert executed successfully, ${result.rowCount} row(s) inserted`) + + return NextResponse.json({ + message: `Data inserted successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] PostgreSQL insert failed:`, error) + + return NextResponse.json( + { error: `PostgreSQL insert failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/postgresql/query/route.ts b/apps/sim/app/api/tools/postgresql/query/route.ts new file mode 100644 index 000000000..88dc9be1f --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/query/route.ts @@ -0,0 +1,65 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils' + +const logger = createLogger('PostgreSQLQueryAPI') + +const QuerySchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + query: z.string().min(1, 'Query is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = QuerySchema.parse(body) + + logger.info( + `[${requestId}] Executing PostgreSQL query on ${params.host}:${params.port}/${params.database}` + ) + + const client = await createPostgresConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const result = await executeQuery(client, params.query) + + logger.info(`[${requestId}] Query executed successfully, returned ${result.rowCount} rows`) + + return NextResponse.json({ + message: `Query executed successfully. ${result.rowCount} row(s) returned.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] PostgreSQL query failed:`, error) + + return NextResponse.json({ error: `PostgreSQL query failed: ${errorMessage}` }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/tools/postgresql/update/route.ts b/apps/sim/app/api/tools/postgresql/update/route.ts new file mode 100644 index 000000000..fe6616727 --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/update/route.ts @@ -0,0 +1,93 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createLogger } from '@/lib/logs/console/logger' +import { + buildUpdateQuery, + createPostgresConnection, + executeQuery, +} from '@/app/api/tools/postgresql/utils' + +const logger = createLogger('PostgreSQLUpdateAPI') + +const UpdateSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('required'), + table: z.string().min(1, 'Table name is required'), + data: z.union([ + z + .record(z.unknown()) + .refine((obj) => Object.keys(obj).length > 0, 'Data object cannot be empty'), + z + .string() + .min(1) + .transform((str) => { + try { + const parsed = JSON.parse(str) + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error('Data must be a JSON object') + } + return parsed + } catch (e) { + throw new Error('Invalid JSON format in data field') + } + }), + ]), + where: z.string().min(1, 'WHERE clause is required'), +}) + +export async function POST(request: NextRequest) { + const requestId = crypto.randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = UpdateSchema.parse(body) + + logger.info( + `[${requestId}] Updating data in ${params.table} on ${params.host}:${params.port}/${params.database}` + ) + + const client = await createPostgresConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const { query, values } = buildUpdateQuery(params.table, params.data, params.where) + const result = await executeQuery(client, query, values) + + logger.info(`[${requestId}] Update executed successfully, ${result.rowCount} row(s) updated`) + + return NextResponse.json({ + message: `Data updated successfully. ${result.rowCount} row(s) affected.`, + rows: result.rows, + rowCount: result.rowCount, + }) + } finally { + await client.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] PostgreSQL update failed:`, error) + + return NextResponse.json( + { error: `PostgreSQL update failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/postgresql/utils.ts b/apps/sim/app/api/tools/postgresql/utils.ts new file mode 100644 index 000000000..6d655da02 --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/utils.ts @@ -0,0 +1,173 @@ +import { Client } from 'pg' +import type { PostgresConnectionConfig } from '@/tools/postgresql/types' + +export async function createPostgresConnection(config: PostgresConnectionConfig): Promise { + const client = new Client({ + host: config.host, + port: config.port, + database: config.database, + user: config.username, + password: config.password, + ssl: + config.ssl === 'disabled' + ? false + : config.ssl === 'required' + ? true + : config.ssl === 'preferred' + ? { rejectUnauthorized: false } + : false, + connectionTimeoutMillis: 10000, // 10 seconds + query_timeout: 30000, // 30 seconds + }) + + try { + await client.connect() + return client + } catch (error) { + await client.end() + throw error + } +} + +export async function executeQuery( + client: Client, + query: string, + params: unknown[] = [] +): Promise<{ rows: unknown[]; rowCount: number }> { + const result = await client.query(query, params) + return { + rows: result.rows || [], + rowCount: result.rowCount || 0, + } +} + +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, + /drop\s+user/i, + /create\s+user/i, + /create\s+role/i, + /grant\s+/i, + /revoke\s+/i, + /alter\s+user/i, + /alter\s+role/i, + /set\s+role/i, + /reset\s+role/i, + /copy\s+.*from/i, + /copy\s+.*to/i, + /lo_import/i, + /lo_export/i, + /pg_read_file/i, + /pg_write_file/i, + /pg_ls_dir/i, + /information_schema\.tables/i, + /pg_catalog/i, + /pg_user/i, + /pg_shadow/i, + /pg_roles/i, + /pg_authid/i, + /pg_stat_activity/i, + /dblink/i, + /\\\\copy/i, + ] + + for (const pattern of dangerousPatterns) { + if (pattern.test(query)) { + return { + isValid: false, + error: `Query contains potentially dangerous operation: ${pattern.source}`, + } + } + } + + // 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 { + isValid: false, + error: + 'Only SELECT, INSERT, UPDATE, DELETE, WITH, EXPLAIN, ANALYZE, and SHOW statements are allowed', + } + } + + return { isValid: true } +} + +export function sanitizeIdentifier(identifier: string): string { + // Handle schema.table format + if (identifier.includes('.')) { + const parts = identifier.split('.') + return parts.map((part) => sanitizeSingleIdentifier(part)).join('.') + } + + return sanitizeSingleIdentifier(identifier) +} + +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}"` +} + +export function buildInsertQuery( + table: string, + data: Record +): { + query: string + values: unknown[] +} { + const sanitizedTable = sanitizeIdentifier(table) + const columns = Object.keys(data) + const sanitizedColumns = columns.map((col) => sanitizeIdentifier(col)) + const placeholders = columns.map((_, index) => `$${index + 1}`) + const values = columns.map((col) => data[col]) + + const query = `INSERT INTO ${sanitizedTable} (${sanitizedColumns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *` + + return { query, values } +} + +export function buildUpdateQuery( + table: string, + data: Record, + where: string +): { + query: string + values: unknown[] +} { + const sanitizedTable = sanitizeIdentifier(table) + const columns = Object.keys(data) + const sanitizedColumns = columns.map((col) => sanitizeIdentifier(col)) + const setClause = sanitizedColumns.map((col, index) => `${col} = $${index + 1}`).join(', ') + const values = columns.map((col) => data[col]) + + const query = `UPDATE ${sanitizedTable} SET ${setClause} WHERE ${where} RETURNING *` + + return { query, values } +} + +export function buildDeleteQuery( + table: string, + where: string +): { + query: string + values: unknown[] +} { + const sanitizedTable = sanitizeIdentifier(table) + const query = `DELETE FROM ${sanitizedTable} WHERE ${where} RETURNING *` + + return { query, values: [] } +} diff --git a/apps/sim/blocks/blocks/mysql.ts b/apps/sim/blocks/blocks/mysql.ts new file mode 100644 index 000000000..957665dfd --- /dev/null +++ b/apps/sim/blocks/blocks/mysql.ts @@ -0,0 +1,255 @@ +import { MySQLIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { MySQLResponse } from '@/tools/mysql/types' + +export const MySQLBlock: BlockConfig = { + type: 'mysql', + name: 'MySQL', + description: 'Connect to MySQL database', + longDescription: + 'Connect to any MySQL database to execute queries, manage data, and perform database operations. Supports SELECT, INSERT, UPDATE, DELETE operations with secure connection handling.', + docsLink: 'https://docs.sim.ai/tools/mysql', + category: 'tools', + bgColor: '#E0E0E0', + icon: MySQLIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Query (SELECT)', id: 'query' }, + { label: 'Insert Data', id: 'insert' }, + { label: 'Update Data', id: 'update' }, + { label: 'Delete Data', id: 'delete' }, + { label: 'Execute Raw SQL', id: 'execute' }, + ], + value: () => 'query', + }, + { + id: 'host', + title: 'Host', + type: 'short-input', + layout: 'full', + placeholder: 'localhost or your.database.host', + required: true, + }, + { + id: 'port', + title: 'Port', + type: 'short-input', + layout: 'full', + placeholder: '3306', + value: () => '3306', + required: true, + }, + { + id: 'database', + title: 'Database Name', + type: 'short-input', + layout: 'full', + placeholder: 'your_database', + required: true, + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + layout: 'full', + placeholder: 'root', + required: true, + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + layout: 'full', + password: true, + placeholder: 'Your database password', + required: true, + }, + { + id: 'ssl', + title: 'SSL Mode', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Disabled', id: 'disabled' }, + { label: 'Required', id: 'required' }, + { label: 'Preferred', id: 'preferred' }, + ], + value: () => 'preferred', + }, + // Table field for insert/update/delete operations + { + id: 'table', + title: 'Table Name', + type: 'short-input', + layout: 'full', + placeholder: 'users', + condition: { field: 'operation', value: 'insert' }, + required: true, + }, + { + id: 'table', + title: 'Table Name', + type: 'short-input', + layout: 'full', + placeholder: 'users', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + { + id: 'table', + title: 'Table Name', + type: 'short-input', + layout: 'full', + placeholder: 'users', + condition: { field: 'operation', value: 'delete' }, + required: true, + }, + // SQL Query field + { + id: 'query', + title: 'SQL Query', + type: 'code', + layout: 'full', + placeholder: 'SELECT * FROM users WHERE active = true', + condition: { field: 'operation', value: 'query' }, + required: true, + }, + { + id: 'query', + title: 'SQL Query', + type: 'code', + layout: 'full', + placeholder: 'SELECT * FROM table_name', + condition: { field: 'operation', value: 'execute' }, + required: true, + }, + // Data for insert operations + { + id: 'data', + title: 'Data (JSON)', + type: 'code', + layout: 'full', + placeholder: '{\n "name": "John Doe",\n "email": "john@example.com",\n "active": true\n}', + condition: { field: 'operation', value: 'insert' }, + required: true, + }, + // Set clause for updates + { + id: 'data', + title: 'Update Data (JSON)', + type: 'code', + layout: 'full', + placeholder: '{\n "name": "Jane Doe",\n "email": "jane@example.com"\n}', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + // Where clause for update/delete + { + id: 'where', + title: 'WHERE Condition', + type: 'short-input', + layout: 'full', + placeholder: 'id = 1', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + { + id: 'where', + title: 'WHERE Condition', + type: 'short-input', + layout: 'full', + placeholder: 'id = 1', + condition: { field: 'operation', value: 'delete' }, + required: true, + }, + ], + tools: { + access: ['mysql_query', 'mysql_insert', 'mysql_update', 'mysql_delete', 'mysql_execute'], + config: { + tool: (params) => { + switch (params.operation) { + case 'query': + return 'mysql_query' + case 'insert': + return 'mysql_insert' + case 'update': + return 'mysql_update' + case 'delete': + return 'mysql_delete' + case 'execute': + return 'mysql_execute' + default: + throw new Error(`Invalid MySQL operation: ${params.operation}`) + } + }, + params: (params) => { + const { operation, data, ...rest } = params + + // Parse JSON data if it's a string + let parsedData + if (data && typeof data === 'string' && data.trim()) { + try { + parsedData = JSON.parse(data) + } catch (parseError) { + const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown JSON error' + throw new Error(`Invalid JSON data format: ${errorMsg}. Please check your JSON syntax.`) + } + } else if (data && typeof data === 'object') { + parsedData = data + } + + // Build connection config + const connectionConfig = { + host: rest.host, + port: typeof rest.port === 'string' ? Number.parseInt(rest.port, 10) : rest.port || 3306, + database: rest.database, + username: rest.username, + password: rest.password, + ssl: rest.ssl || 'preferred', + } + + // Build params object + const result: any = { ...connectionConfig } + + if (rest.table) result.table = rest.table + if (rest.query) result.query = rest.query + if (rest.where) result.where = rest.where + if (parsedData !== undefined) result.data = parsedData + + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Database operation to perform' }, + host: { type: 'string', description: 'Database host' }, + port: { type: 'string', description: 'Database port' }, + database: { type: 'string', description: 'Database name' }, + username: { type: 'string', description: 'Database username' }, + password: { type: 'string', description: 'Database password' }, + ssl: { type: 'string', description: 'SSL mode' }, + table: { type: 'string', description: 'Table name' }, + query: { type: 'string', description: 'SQL query to execute' }, + data: { type: 'json', description: 'Data for insert/update operations' }, + where: { type: 'string', description: 'WHERE clause for update/delete' }, + }, + outputs: { + message: { + type: 'string', + description: 'Success or error message describing the operation outcome', + }, + rows: { + type: 'array', + description: 'Array of rows returned from the query', + }, + rowCount: { + type: 'number', + description: 'Number of rows affected by the operation', + }, + }, +} diff --git a/apps/sim/blocks/blocks/parallel.ts b/apps/sim/blocks/blocks/parallel.ts new file mode 100644 index 000000000..5f3851958 --- /dev/null +++ b/apps/sim/blocks/blocks/parallel.ts @@ -0,0 +1,109 @@ +import { ParallelIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { ToolResponse } from '@/tools/types' + +export const ParallelBlock: BlockConfig = { + type: 'parallel_ai', + name: 'Parallel AI', + description: 'Search with Parallel AI', + longDescription: + "Search the web using Parallel AI's advanced search capabilities. Get comprehensive results with intelligent processing and content extraction.", + docsLink: 'https://docs.parallel.ai/search-api/search-quickstart', + category: 'tools', + bgColor: '#E0E0E0', + icon: ParallelIcon, + subBlocks: [ + { + id: 'objective', + title: 'Search Objective', + type: 'long-input', + layout: 'full', + placeholder: "When was the United Nations established? Prefer UN's websites.", + required: true, + }, + { + id: 'search_queries', + title: 'Search Queries', + type: 'long-input', + layout: 'full', + placeholder: + 'Enter search queries separated by commas (e.g., "Founding year UN", "Year of founding United Nations")', + required: false, + }, + { + id: 'processor', + title: 'Processor', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Base', id: 'base' }, + { label: 'Pro', id: 'pro' }, + ], + value: () => 'base', + }, + { + id: 'max_results', + title: 'Max Results', + type: 'short-input', + layout: 'half', + placeholder: '5', + }, + { + id: 'max_chars_per_result', + title: 'Max Chars', + type: 'short-input', + layout: 'half', + placeholder: '1500', + }, + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + layout: 'full', + placeholder: 'Enter your Parallel AI API key', + password: true, + required: true, + }, + ], + tools: { + access: ['parallel_search'], + config: { + tool: (params) => { + // Convert search_queries from comma-separated string to array (if provided) + if (params.search_queries && typeof params.search_queries === 'string') { + const queries = params.search_queries + .split(',') + .map((query: string) => query.trim()) + .filter((query: string) => query.length > 0) + // Only set if we have actual queries + if (queries.length > 0) { + params.search_queries = queries + } else { + params.search_queries = undefined + } + } + + // Convert numeric parameters + if (params.max_results) { + params.max_results = Number(params.max_results) + } + if (params.max_chars_per_result) { + params.max_chars_per_result = Number(params.max_chars_per_result) + } + + return 'parallel_search' + }, + }, + }, + inputs: { + objective: { type: 'string', description: 'Search objective or question' }, + search_queries: { type: 'string', description: 'Comma-separated search queries' }, + processor: { type: 'string', description: 'Processing method' }, + max_results: { type: 'number', description: 'Maximum number of results' }, + max_chars_per_result: { type: 'number', description: 'Maximum characters per result' }, + apiKey: { type: 'string', description: 'Parallel AI API key' }, + }, + outputs: { + results: { type: 'array', description: 'Search results with excerpts from relevant pages' }, + }, +} diff --git a/apps/sim/blocks/blocks/postgresql.ts b/apps/sim/blocks/blocks/postgresql.ts new file mode 100644 index 000000000..1f489c984 --- /dev/null +++ b/apps/sim/blocks/blocks/postgresql.ts @@ -0,0 +1,261 @@ +import { PostgresIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import type { PostgresResponse } from '@/tools/postgresql/types' + +export const PostgreSQLBlock: BlockConfig = { + type: 'postgresql', + name: 'PostgreSQL', + description: 'Connect to PostgreSQL database', + longDescription: + 'Connect to any PostgreSQL database to execute queries, manage data, and perform database operations. Supports SELECT, INSERT, UPDATE, DELETE operations with secure connection handling.', + docsLink: 'https://docs.sim.ai/tools/postgresql', + category: 'tools', + bgColor: '#336791', + icon: PostgresIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Query (SELECT)', id: 'query' }, + { label: 'Insert Data', id: 'insert' }, + { label: 'Update Data', id: 'update' }, + { label: 'Delete Data', id: 'delete' }, + { label: 'Execute Raw SQL', id: 'execute' }, + ], + value: () => 'query', + }, + { + id: 'host', + title: 'Host', + type: 'short-input', + layout: 'full', + placeholder: 'localhost or your.database.host', + required: true, + }, + { + id: 'port', + title: 'Port', + type: 'short-input', + layout: 'full', + placeholder: '5432', + value: () => '5432', + required: true, + }, + { + id: 'database', + title: 'Database Name', + type: 'short-input', + layout: 'full', + placeholder: 'your_database', + required: true, + }, + { + id: 'username', + title: 'Username', + type: 'short-input', + layout: 'full', + placeholder: 'postgres', + required: true, + }, + { + id: 'password', + title: 'Password', + type: 'short-input', + layout: 'full', + password: true, + placeholder: 'Your database password', + required: true, + }, + { + id: 'ssl', + title: 'SSL Mode', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Disabled', id: 'disabled' }, + { label: 'Required', id: 'required' }, + { label: 'Preferred', id: 'preferred' }, + ], + value: () => 'preferred', + }, + // Table field for insert/update/delete operations + { + id: 'table', + title: 'Table Name', + type: 'short-input', + layout: 'full', + placeholder: 'users', + condition: { field: 'operation', value: 'insert' }, + required: true, + }, + { + id: 'table', + title: 'Table Name', + type: 'short-input', + layout: 'full', + placeholder: 'users', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + { + id: 'table', + title: 'Table Name', + type: 'short-input', + layout: 'full', + placeholder: 'users', + condition: { field: 'operation', value: 'delete' }, + required: true, + }, + // SQL Query field + { + id: 'query', + title: 'SQL Query', + type: 'code', + layout: 'full', + placeholder: 'SELECT * FROM users WHERE active = true', + condition: { field: 'operation', value: 'query' }, + required: true, + }, + { + id: 'query', + title: 'SQL Query', + type: 'code', + layout: 'full', + placeholder: 'SELECT * FROM table_name', + condition: { field: 'operation', value: 'execute' }, + required: true, + }, + // Data for insert operations + { + id: 'data', + title: 'Data (JSON)', + type: 'code', + layout: 'full', + placeholder: '{\n "name": "John Doe",\n "email": "john@example.com",\n "active": true\n}', + condition: { field: 'operation', value: 'insert' }, + required: true, + }, + // Set clause for updates + { + id: 'data', + title: 'Update Data (JSON)', + type: 'code', + layout: 'full', + placeholder: '{\n "name": "Jane Doe",\n "email": "jane@example.com"\n}', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + // Where clause for update/delete + { + id: 'where', + title: 'WHERE Condition', + type: 'short-input', + layout: 'full', + placeholder: 'id = 1', + condition: { field: 'operation', value: 'update' }, + required: true, + }, + { + id: 'where', + title: 'WHERE Condition', + type: 'short-input', + layout: 'full', + placeholder: 'id = 1', + condition: { field: 'operation', value: 'delete' }, + required: true, + }, + ], + tools: { + access: [ + 'postgresql_query', + 'postgresql_insert', + 'postgresql_update', + 'postgresql_delete', + 'postgresql_execute', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'query': + return 'postgresql_query' + case 'insert': + return 'postgresql_insert' + case 'update': + return 'postgresql_update' + case 'delete': + return 'postgresql_delete' + case 'execute': + return 'postgresql_execute' + default: + throw new Error(`Invalid PostgreSQL operation: ${params.operation}`) + } + }, + params: (params) => { + const { operation, data, ...rest } = params + + // Parse JSON data if it's a string + let parsedData + if (data && typeof data === 'string' && data.trim()) { + try { + parsedData = JSON.parse(data) + } catch (parseError) { + const errorMsg = parseError instanceof Error ? parseError.message : 'Unknown JSON error' + throw new Error(`Invalid JSON data format: ${errorMsg}. Please check your JSON syntax.`) + } + } else if (data && typeof data === 'object') { + parsedData = data + } + + // Build connection config + const connectionConfig = { + host: rest.host, + port: typeof rest.port === 'string' ? Number.parseInt(rest.port, 10) : rest.port || 5432, + database: rest.database, + username: rest.username, + password: rest.password, + ssl: rest.ssl || 'preferred', + } + + // Build params object + const result: any = { ...connectionConfig } + + if (rest.table) result.table = rest.table + if (rest.query) result.query = rest.query + if (rest.where) result.where = rest.where + if (parsedData !== undefined) result.data = parsedData + + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Database operation to perform' }, + host: { type: 'string', description: 'Database host' }, + port: { type: 'string', description: 'Database port' }, + database: { type: 'string', description: 'Database name' }, + username: { type: 'string', description: 'Database username' }, + password: { type: 'string', description: 'Database password' }, + ssl: { type: 'string', description: 'SSL mode' }, + table: { type: 'string', description: 'Table name' }, + query: { type: 'string', description: 'SQL query to execute' }, + data: { type: 'json', description: 'Data for insert/update operations' }, + where: { type: 'string', description: 'WHERE clause for update/delete' }, + }, + outputs: { + message: { + type: 'string', + description: 'Success or error message describing the operation outcome', + }, + rows: { + type: 'array', + description: 'Array of rows returned from the query', + }, + rowCount: { + type: 'number', + description: 'Number of rows affected by the operation', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index b052b80ae..a27716b74 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -40,12 +40,15 @@ import { MicrosoftExcelBlock } from '@/blocks/blocks/microsoft_excel' import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner' import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' import { MistralParseBlock } from '@/blocks/blocks/mistral_parse' +import { MySQLBlock } from '@/blocks/blocks/mysql' import { NotionBlock } from '@/blocks/blocks/notion' import { OneDriveBlock } from '@/blocks/blocks/onedrive' import { OpenAIBlock } from '@/blocks/blocks/openai' import { OutlookBlock } from '@/blocks/blocks/outlook' +import { ParallelBlock } from '@/blocks/blocks/parallel' import { PerplexityBlock } from '@/blocks/blocks/perplexity' import { PineconeBlock } from '@/blocks/blocks/pinecone' +import { PostgreSQLBlock } from '@/blocks/blocks/postgresql' import { QdrantBlock } from '@/blocks/blocks/qdrant' import { RedditBlock } from '@/blocks/blocks/reddit' import { ResponseBlock } from '@/blocks/blocks/response' @@ -113,12 +116,15 @@ export const registry: Record = { microsoft_planner: MicrosoftPlannerBlock, microsoft_teams: MicrosoftTeamsBlock, mistral_parse: MistralParseBlock, + mysql: MySQLBlock, notion: NotionBlock, openai: OpenAIBlock, outlook: OutlookBlock, onedrive: OneDriveBlock, + parallel_ai: ParallelBlock, perplexity: PerplexityBlock, pinecone: PineconeBlock, + postgresql: PostgreSQLBlock, qdrant: QdrantBlock, memory: MemoryBlock, reddit: RedditBlock, diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 69c940ab0..4917633c4 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -4,7 +4,7 @@ import type { ToolResponse } from '@/tools/types' // Basic types export type BlockIcon = (props: SVGProps) => JSX.Element export type ParamType = 'string' | 'number' | 'boolean' | 'json' -export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'any' +export type PrimitiveValueType = 'string' | 'number' | 'boolean' | 'json' | 'array' | 'any' // Block classification export type BlockCategory = 'blocks' | 'tools' | 'triggers' diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 13471d594..93a1a3c1a 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3344,3 +3344,95 @@ export function MicrosoftPlannerIcon(props: SVGProps) { ) } + +export function ParallelIcon(props: SVGProps) { + return ( + + + + + + + + + + + ) +} + +export function PostgresIcon(props: SVGProps) { + return ( + + + + + + + ) +} + +export function MySQLIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/sim/package.json b/apps/sim/package.json index 2ae444d90..aee18f0cf 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -69,6 +69,7 @@ "@react-email/components": "^0.0.34", "@sentry/nextjs": "^9.15.0", "@trigger.dev/sdk": "4.0.0", + "@types/pg": "8.15.5", "@types/three": "0.177.0", "@vercel/og": "^0.6.5", "@vercel/speed-insights": "^1.2.0", @@ -96,11 +97,13 @@ "lenis": "^1.2.3", "lucide-react": "^0.479.0", "mammoth": "^1.9.0", + "mysql2": "3.14.3", "next": "^15.3.2", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", "openai": "^4.91.1", "pdf-parse": "^1.1.1", + "pg": "8.16.3", "postgres": "^3.4.5", "prismjs": "^1.30.0", "react": "19.1.0", diff --git a/apps/sim/tools/mysql/delete.ts b/apps/sim/tools/mysql/delete.ts new file mode 100644 index 000000000..d140f1f98 --- /dev/null +++ b/apps/sim/tools/mysql/delete.ts @@ -0,0 +1,102 @@ +import type { MySQLDeleteParams, MySQLResponse } from '@/tools/mysql/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteTool: ToolConfig = { + id: 'mysql_delete', + name: 'MySQL Delete', + description: 'Delete records from MySQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MySQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'MySQL server port (default: 3306)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name to delete from', + }, + where: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'WHERE clause condition (without WHERE keyword)', + }, + }, + + request: { + url: '/api/tools/mysql/delete', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'preferred', + table: params.table, + where: params.where, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'MySQL delete failed') + } + + return { + success: true, + output: { + message: data.message || 'Data deleted successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of deleted rows' }, + rowCount: { type: 'number', description: 'Number of rows deleted' }, + }, +} diff --git a/apps/sim/tools/mysql/execute.ts b/apps/sim/tools/mysql/execute.ts new file mode 100644 index 000000000..27c7f6faf --- /dev/null +++ b/apps/sim/tools/mysql/execute.ts @@ -0,0 +1,95 @@ +import type { MySQLExecuteParams, MySQLResponse } from '@/tools/mysql/types' +import type { ToolConfig } from '@/tools/types' + +export const executeTool: ToolConfig = { + id: 'mysql_execute', + name: 'MySQL Execute', + description: 'Execute raw SQL query on MySQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MySQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'MySQL server port (default: 3306)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Raw SQL query to execute', + }, + }, + + request: { + url: '/api/tools/mysql/execute', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'preferred', + query: params.query, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'MySQL execute failed') + } + + return { + success: true, + output: { + message: data.message || 'Query executed successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of rows returned from the query' }, + rowCount: { type: 'number', description: 'Number of rows affected' }, + }, +} diff --git a/apps/sim/tools/mysql/index.ts b/apps/sim/tools/mysql/index.ts new file mode 100644 index 000000000..b532035cb --- /dev/null +++ b/apps/sim/tools/mysql/index.ts @@ -0,0 +1,6 @@ +export { deleteTool } from './delete' +export { executeTool } from './execute' +export { insertTool } from './insert' +export { queryTool } from './query' +export * from './types' +export { updateTool } from './update' diff --git a/apps/sim/tools/mysql/insert.ts b/apps/sim/tools/mysql/insert.ts new file mode 100644 index 000000000..4c6845e8e --- /dev/null +++ b/apps/sim/tools/mysql/insert.ts @@ -0,0 +1,102 @@ +import type { MySQLInsertParams, MySQLResponse } from '@/tools/mysql/types' +import type { ToolConfig } from '@/tools/types' + +export const insertTool: ToolConfig = { + id: 'mysql_insert', + name: 'MySQL Insert', + description: 'Insert new record into MySQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MySQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'MySQL server port (default: 3306)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name to insert into', + }, + data: { + type: 'object', + required: true, + visibility: 'user-or-llm', + description: 'Data to insert as key-value pairs', + }, + }, + + request: { + url: '/api/tools/mysql/insert', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'preferred', + table: params.table, + data: params.data, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'MySQL insert failed') + } + + return { + success: true, + output: { + message: data.message || 'Data inserted successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of inserted rows' }, + rowCount: { type: 'number', description: 'Number of rows inserted' }, + }, +} diff --git a/apps/sim/tools/mysql/query.ts b/apps/sim/tools/mysql/query.ts new file mode 100644 index 000000000..cac2a2dd1 --- /dev/null +++ b/apps/sim/tools/mysql/query.ts @@ -0,0 +1,95 @@ +import type { MySQLQueryParams, MySQLResponse } from '@/tools/mysql/types' +import type { ToolConfig } from '@/tools/types' + +export const queryTool: ToolConfig = { + id: 'mysql_query', + name: 'MySQL Query', + description: 'Execute SELECT query on MySQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MySQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'MySQL server port (default: 3306)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'SQL SELECT query to execute', + }, + }, + + request: { + url: '/api/tools/mysql/query', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'preferred', + query: params.query, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'MySQL query failed') + } + + return { + success: true, + output: { + message: data.message || 'Query executed successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of rows returned from the query' }, + rowCount: { type: 'number', description: 'Number of rows returned' }, + }, +} diff --git a/apps/sim/tools/mysql/types.ts b/apps/sim/tools/mysql/types.ts new file mode 100644 index 000000000..97edaf0ce --- /dev/null +++ b/apps/sim/tools/mysql/types.ts @@ -0,0 +1,50 @@ +import type { ToolResponse } from '@/tools/types' + +export interface MySQLConnectionConfig { + host: string + port: number + database: string + username: string + password: string + ssl: 'disabled' | 'required' | 'preferred' +} + +export interface MySQLQueryParams extends MySQLConnectionConfig { + query: string +} + +export interface MySQLInsertParams extends MySQLConnectionConfig { + table: string + data: Record +} + +export interface MySQLUpdateParams extends MySQLConnectionConfig { + table: string + data: Record + where: string +} + +export interface MySQLDeleteParams extends MySQLConnectionConfig { + table: string + where: string +} + +export interface MySQLExecuteParams extends MySQLConnectionConfig { + query: string +} + +export interface MySQLBaseResponse extends ToolResponse { + output: { + message: string + rows: unknown[] + rowCount: number + } + error?: string +} + +export interface MySQLQueryResponse extends MySQLBaseResponse {} +export interface MySQLInsertResponse extends MySQLBaseResponse {} +export interface MySQLUpdateResponse extends MySQLBaseResponse {} +export interface MySQLDeleteResponse extends MySQLBaseResponse {} +export interface MySQLExecuteResponse extends MySQLBaseResponse {} +export interface MySQLResponse extends MySQLBaseResponse {} diff --git a/apps/sim/tools/mysql/update.ts b/apps/sim/tools/mysql/update.ts new file mode 100644 index 000000000..85859ca09 --- /dev/null +++ b/apps/sim/tools/mysql/update.ts @@ -0,0 +1,109 @@ +import type { MySQLResponse, MySQLUpdateParams } from '@/tools/mysql/types' +import type { ToolConfig } from '@/tools/types' + +export const updateTool: ToolConfig = { + id: 'mysql_update', + name: 'MySQL Update', + description: 'Update existing records in MySQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MySQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'MySQL server port (default: 3306)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name to update', + }, + data: { + type: 'object', + required: true, + visibility: 'user-or-llm', + description: 'Data to update as key-value pairs', + }, + where: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'WHERE clause condition (without WHERE keyword)', + }, + }, + + request: { + url: '/api/tools/mysql/update', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'preferred', + table: params.table, + data: params.data, + where: params.where, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'MySQL update failed') + } + + return { + success: true, + output: { + message: data.message || 'Data updated successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of updated rows' }, + rowCount: { type: 'number', description: 'Number of rows updated' }, + }, +} diff --git a/apps/sim/tools/parallel/index.ts b/apps/sim/tools/parallel/index.ts new file mode 100644 index 000000000..471319e21 --- /dev/null +++ b/apps/sim/tools/parallel/index.ts @@ -0,0 +1,3 @@ +import { searchTool } from '@/tools/parallel/search' + +export const parallelSearchTool = searchTool diff --git a/apps/sim/tools/parallel/search.ts b/apps/sim/tools/parallel/search.ts new file mode 100644 index 000000000..3059d65d0 --- /dev/null +++ b/apps/sim/tools/parallel/search.ts @@ -0,0 +1,108 @@ +import type { ParallelSearchParams } from '@/tools/parallel/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const searchTool: ToolConfig = { + id: 'parallel_search', + name: 'Parallel AI Search', + description: + 'Search the web using Parallel AI. Provides comprehensive search results with intelligent processing and content extraction.', + version: '1.0.0', + + params: { + objective: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The search objective or question to answer', + }, + search_queries: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Optional comma-separated list of search queries to execute', + }, + processor: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Processing method: base or pro (default: base)', + }, + max_results: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum number of results to return (default: 5)', + }, + max_chars_per_result: { + type: 'number', + required: false, + visibility: 'user-only', + description: 'Maximum characters per result (default: 1500)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Parallel AI API Key', + }, + }, + + request: { + url: 'https://api.parallel.ai/v1beta/search', + method: 'POST', + headers: (params) => ({ + 'Content-Type': 'application/json', + 'x-api-key': params.apiKey, + }), + body: (params) => { + const body: Record = { + objective: params.objective, + search_queries: params.search_queries, + } + + // Add optional parameters if provided + if (params.processor) body.processor = params.processor + if (params.max_results) body.max_results = params.max_results + if (params.max_chars_per_result) body.max_chars_per_result = params.max_chars_per_result + + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + results: data.results.map((result: unknown) => { + const resultObj = result as Record + return { + url: resultObj.url || '', + title: resultObj.title || '', + excerpts: resultObj.excerpts || [], + } + }), + }, + } + }, + + outputs: { + results: { + type: 'array', + description: 'Search results with excerpts from relevant pages', + items: { + type: 'object', + properties: { + url: { type: 'string', description: 'The URL of the search result' }, + title: { type: 'string', description: 'The title of the search result' }, + excerpts: { + type: 'array', + description: 'Text excerpts from the page', + items: { type: 'string' }, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/parallel/types.ts b/apps/sim/tools/parallel/types.ts new file mode 100644 index 000000000..48720ec79 --- /dev/null +++ b/apps/sim/tools/parallel/types.ts @@ -0,0 +1,18 @@ +export interface ParallelSearchParams { + objective: string + search_queries: string[] + processor?: string + max_results?: number + max_chars_per_result?: number + apiKey: string +} + +export interface ParallelSearchResult { + url: string + title: string + excerpts: string[] +} + +export interface ParallelSearchResponse { + results: ParallelSearchResult[] +} diff --git a/apps/sim/tools/postgresql/delete.ts b/apps/sim/tools/postgresql/delete.ts new file mode 100644 index 000000000..7a629d746 --- /dev/null +++ b/apps/sim/tools/postgresql/delete.ts @@ -0,0 +1,102 @@ +import type { PostgresDeleteParams, PostgresDeleteResponse } from '@/tools/postgresql/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteTool: ToolConfig = { + id: 'postgresql_delete', + name: 'PostgreSQL Delete', + description: 'Delete data from PostgreSQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server port (default: 5432)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name to delete data from', + }, + where: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'WHERE clause condition (without WHERE keyword)', + }, + }, + + request: { + url: '/api/tools/postgresql/delete', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'required', + table: params.table, + where: params.where, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'PostgreSQL delete failed') + } + + return { + success: true, + output: { + message: data.message || 'Data deleted successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Deleted data (if RETURNING clause used)' }, + rowCount: { type: 'number', description: 'Number of rows deleted' }, + }, +} diff --git a/apps/sim/tools/postgresql/execute.ts b/apps/sim/tools/postgresql/execute.ts new file mode 100644 index 000000000..7a6d28ee4 --- /dev/null +++ b/apps/sim/tools/postgresql/execute.ts @@ -0,0 +1,95 @@ +import type { PostgresExecuteParams, PostgresExecuteResponse } from '@/tools/postgresql/types' +import type { ToolConfig } from '@/tools/types' + +export const executeTool: ToolConfig = { + id: 'postgresql_execute', + name: 'PostgreSQL Execute', + description: 'Execute raw SQL query on PostgreSQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server port (default: 5432)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Raw SQL query to execute', + }, + }, + + request: { + url: '/api/tools/postgresql/execute', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'required', + query: params.query, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'PostgreSQL execute failed') + } + + return { + success: true, + output: { + message: data.message || 'Query executed successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of rows returned from the query' }, + rowCount: { type: 'number', description: 'Number of rows affected' }, + }, +} diff --git a/apps/sim/tools/postgresql/index.ts b/apps/sim/tools/postgresql/index.ts new file mode 100644 index 000000000..651026e93 --- /dev/null +++ b/apps/sim/tools/postgresql/index.ts @@ -0,0 +1,7 @@ +import { deleteTool } from './delete' +import { executeTool } from './execute' +import { insertTool } from './insert' +import { queryTool } from './query' +import { updateTool } from './update' + +export { deleteTool, executeTool, insertTool, queryTool, updateTool } diff --git a/apps/sim/tools/postgresql/insert.ts b/apps/sim/tools/postgresql/insert.ts new file mode 100644 index 000000000..4f26dda3c --- /dev/null +++ b/apps/sim/tools/postgresql/insert.ts @@ -0,0 +1,102 @@ +import type { PostgresInsertParams, PostgresInsertResponse } from '@/tools/postgresql/types' +import type { ToolConfig } from '@/tools/types' + +export const insertTool: ToolConfig = { + id: 'postgresql_insert', + name: 'PostgreSQL Insert', + description: 'Insert data into PostgreSQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server port (default: 5432)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name to insert data into', + }, + data: { + type: 'object', + required: true, + visibility: 'user-or-llm', + description: 'Data object to insert (key-value pairs)', + }, + }, + + request: { + url: '/api/tools/postgresql/insert', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'required', + table: params.table, + data: params.data, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'PostgreSQL insert failed') + } + + return { + success: true, + output: { + message: data.message || 'Data inserted successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Inserted data (if RETURNING clause used)' }, + rowCount: { type: 'number', description: 'Number of rows inserted' }, + }, +} diff --git a/apps/sim/tools/postgresql/query.ts b/apps/sim/tools/postgresql/query.ts new file mode 100644 index 000000000..3f4e79434 --- /dev/null +++ b/apps/sim/tools/postgresql/query.ts @@ -0,0 +1,95 @@ +import type { PostgresQueryParams, PostgresQueryResponse } from '@/tools/postgresql/types' +import type { ToolConfig } from '@/tools/types' + +export const queryTool: ToolConfig = { + id: 'postgresql_query', + name: 'PostgreSQL Query', + description: 'Execute a SELECT query on PostgreSQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server port (default: 5432)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'SQL SELECT query to execute', + }, + }, + + request: { + url: '/api/tools/postgresql/query', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'required', + query: params.query, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'PostgreSQL query failed') + } + + return { + success: true, + output: { + message: data.message || 'Query executed successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Array of rows returned from the query' }, + rowCount: { type: 'number', description: 'Number of rows returned' }, + }, +} diff --git a/apps/sim/tools/postgresql/types.ts b/apps/sim/tools/postgresql/types.ts new file mode 100644 index 000000000..18d1ad9d2 --- /dev/null +++ b/apps/sim/tools/postgresql/types.ts @@ -0,0 +1,50 @@ +import type { ToolResponse } from '@/tools/types' + +export interface PostgresConnectionConfig { + host: string + port: number + database: string + username: string + password: string + ssl: 'disabled' | 'required' | 'preferred' +} + +export interface PostgresQueryParams extends PostgresConnectionConfig { + query: string +} + +export interface PostgresInsertParams extends PostgresConnectionConfig { + table: string + data: Record +} + +export interface PostgresUpdateParams extends PostgresConnectionConfig { + table: string + data: Record + where: string +} + +export interface PostgresDeleteParams extends PostgresConnectionConfig { + table: string + where: string +} + +export interface PostgresExecuteParams extends PostgresConnectionConfig { + query: string +} + +export interface PostgresBaseResponse extends ToolResponse { + output: { + message: string + rows: unknown[] + rowCount: number + } + error?: string +} + +export interface PostgresQueryResponse extends PostgresBaseResponse {} +export interface PostgresInsertResponse extends PostgresBaseResponse {} +export interface PostgresUpdateResponse extends PostgresBaseResponse {} +export interface PostgresDeleteResponse extends PostgresBaseResponse {} +export interface PostgresExecuteResponse extends PostgresBaseResponse {} +export interface PostgresResponse extends PostgresBaseResponse {} diff --git a/apps/sim/tools/postgresql/update.ts b/apps/sim/tools/postgresql/update.ts new file mode 100644 index 000000000..9bd09cc5a --- /dev/null +++ b/apps/sim/tools/postgresql/update.ts @@ -0,0 +1,109 @@ +import type { PostgresUpdateParams, PostgresUpdateResponse } from '@/tools/postgresql/types' +import type { ToolConfig } from '@/tools/types' + +export const updateTool: ToolConfig = { + id: 'postgresql_update', + name: 'PostgreSQL Update', + description: 'Update data in PostgreSQL database', + version: '1.0', + + params: { + host: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server hostname or IP address', + }, + port: { + type: 'number', + required: true, + visibility: 'user-only', + description: 'PostgreSQL server port (default: 5432)', + }, + database: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database name to connect to', + }, + username: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database username', + }, + password: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Database password', + }, + ssl: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'SSL connection mode (disabled, required, preferred)', + }, + table: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Table name to update data in', + }, + data: { + type: 'object', + required: true, + visibility: 'user-or-llm', + description: 'Data object with fields to update (key-value pairs)', + }, + where: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'WHERE clause condition (without WHERE keyword)', + }, + }, + + request: { + url: '/api/tools/postgresql/update', + method: 'POST', + headers: () => ({ + 'Content-Type': 'application/json', + }), + body: (params) => ({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl || 'required', + table: params.table, + data: params.data, + where: params.where, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + throw new Error(data.error || 'PostgreSQL update failed') + } + + return { + success: true, + output: { + message: data.message || 'Data updated successfully', + rows: data.rows || [], + rowCount: data.rowCount || 0, + }, + error: undefined, + } + }, + + outputs: { + message: { type: 'string', description: 'Operation status message' }, + rows: { type: 'array', description: 'Updated data (if RETURNING clause used)' }, + rowCount: { type: 'number', description: 'Number of rows updated' }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 4665e4436..1c1b6b932 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -91,6 +91,13 @@ import { microsoftTeamsWriteChatTool, } from '@/tools/microsoft_teams' import { mistralParserTool } from '@/tools/mistral' +import { + deleteTool as mysqlDeleteTool, + executeTool as mysqlExecuteTool, + insertTool as mysqlInsertTool, + queryTool as mysqlQueryTool, + updateTool as mysqlUpdateTool, +} from '@/tools/mysql' import { notionCreateDatabaseTool, notionCreatePageTool, @@ -103,6 +110,7 @@ import { import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive' import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai' import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook' +import { parallelSearchTool } from '@/tools/parallel' import { perplexityChatTool } from '@/tools/perplexity' import { pineconeFetchTool, @@ -111,6 +119,13 @@ import { pineconeSearchVectorTool, pineconeUpsertTextTool, } from '@/tools/pinecone' +import { + deleteTool as postgresDeleteTool, + executeTool as postgresExecuteTool, + insertTool as postgresInsertTool, + queryTool as postgresQueryTool, + updateTool as postgresUpdateTool, +} from '@/tools/postgresql' import { qdrantFetchTool, qdrantSearchTool, qdrantUpsertTool } from '@/tools/qdrant' import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from '@/tools/reddit' import { s3GetObjectTool } from '@/tools/s3' @@ -217,6 +232,16 @@ export const tools: Record = { pinecone_search_text: pineconeSearchTextTool, pinecone_search_vector: pineconeSearchVectorTool, pinecone_upsert_text: pineconeUpsertTextTool, + postgresql_query: postgresQueryTool, + postgresql_insert: postgresInsertTool, + postgresql_update: postgresUpdateTool, + postgresql_delete: postgresDeleteTool, + postgresql_execute: postgresExecuteTool, + mysql_query: mysqlQueryTool, + mysql_insert: mysqlInsertTool, + mysql_update: mysqlUpdateTool, + mysql_delete: mysqlDeleteTool, + mysql_execute: mysqlExecuteTool, github_pr: githubPrTool, github_comment: githubCommentTool, exa_search: exaSearchTool, @@ -224,6 +249,7 @@ export const tools: Record = { exa_find_similar_links: exaFindSimilarLinksTool, exa_answer: exaAnswerTool, exa_research: exaResearchTool, + parallel_search: parallelSearchTool, reddit_hot_posts: redditHotPostsTool, reddit_get_posts: redditGetPostsTool, reddit_get_comments: redditGetCommentsTool, diff --git a/bun.lock b/bun.lock index efad3b979..4584b80ef 100644 --- a/bun.lock +++ b/bun.lock @@ -98,6 +98,7 @@ "@react-email/components": "^0.0.34", "@sentry/nextjs": "^9.15.0", "@trigger.dev/sdk": "4.0.0", + "@types/pg": "8.15.5", "@types/three": "0.177.0", "@vercel/og": "^0.6.5", "@vercel/speed-insights": "^1.2.0", @@ -125,11 +126,13 @@ "lenis": "^1.2.3", "lucide-react": "^0.479.0", "mammoth": "^1.9.0", + "mysql2": "3.14.3", "next": "^15.3.2", "next-runtime-env": "3.3.0", "next-themes": "^0.4.6", "openai": "^4.91.1", "pdf-parse": "^1.1.1", + "pg": "8.16.3", "postgres": "^3.4.5", "prismjs": "^1.30.0", "react": "19.1.0", @@ -1399,7 +1402,7 @@ "@types/normalize-path": ["@types/normalize-path@3.0.2", "", {}, "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA=="], - "@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + "@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="], "@types/pg-pool": ["@types/pg-pool@2.0.6", "", { "dependencies": { "@types/pg": "*" } }, "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ=="], @@ -1559,6 +1562,8 @@ "autoprefixer": ["autoprefixer@10.4.21", "", { "dependencies": { "browserslist": "^4.24.4", "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], @@ -2017,6 +2022,8 @@ "geist": ["geist@1.4.2", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-OQUga/KUc8ueijck6EbtT07L4tZ5+TZgjw8PyWfxo16sL5FWk7gNViPNU8hgCFjy6bJi9yuTP+CRpywzaGN8zw=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -2169,6 +2176,8 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], @@ -2293,7 +2302,7 @@ "log-update": ["log-update@5.0.1", "", { "dependencies": { "ansi-escapes": "^5.0.0", "cli-cursor": "^4.0.0", "slice-ansi": "^5.0.0", "strip-ansi": "^7.0.1", "wrap-ansi": "^8.0.1" } }, "sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw=="], - "long": ["long@2.4.0", "", {}, "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], @@ -2307,6 +2316,8 @@ "lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="], + "lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="], + "lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -2475,8 +2486,12 @@ "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], + "mysql2": ["mysql2@3.14.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], + "nano-spawn": ["nano-spawn@1.0.2", "", {}, "sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -2581,12 +2596,22 @@ "peberminta": ["peberminta@0.9.0", "", {}, "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ=="], + "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], + + "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], + + "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], + "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -2821,6 +2846,8 @@ "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], @@ -2891,6 +2918,8 @@ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -3239,8 +3268,6 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@grpc/proto-loader/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], @@ -3365,6 +3392,8 @@ "@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="], + "@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + "@opentelemetry/instrumentation-redis-4/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="], "@opentelemetry/instrumentation-tedious/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="], @@ -3589,6 +3618,8 @@ "@types/pg/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@types/pg-pool/@types/pg": ["@types/pg@8.6.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w=="], + "@types/tedious/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], "@types/through/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], @@ -3699,6 +3730,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "named-placeholders/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "next/@next/env": ["@next/env@15.4.1", "", {}, "sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -3731,8 +3764,6 @@ "protobufjs/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], - "protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "react-email/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], "react-email/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], @@ -3789,6 +3820,8 @@ "test-exclude/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + "thriftrw/long": ["long@2.4.0", "", {}, "sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ=="], + "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], "unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], @@ -3927,6 +3960,8 @@ "@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@opentelemetry/instrumentation-redis-4/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], "@opentelemetry/instrumentation-tedious/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], @@ -4153,6 +4188,8 @@ "@types/node-fetch/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@types/pg-pool/@types/pg/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], "@types/tedious/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], @@ -4303,6 +4340,8 @@ "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@radix-ui/react-toggle-group/@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w=="], "@react-email/preview-server/@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.0", "@radix-ui/react-slot": "1.2.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg=="], @@ -4353,6 +4392,8 @@ "@trigger.dev/core/socket.io/engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], + "@types/pg-pool/@types/pg/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.0", "", {}, "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg=="], "gaxios/node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],