mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -05:00
Compare commits
12 Commits
main
...
feat/snowf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7936f7e1d | ||
|
|
7671ec35e8 | ||
|
|
addd05bb8e | ||
|
|
248ebac78b | ||
|
|
89ba330846 | ||
|
|
342c4a2081 | ||
|
|
b3f6bffc55 | ||
|
|
883c70140a | ||
|
|
f093f97cc8 | ||
|
|
17a164508f | ||
|
|
e3dca6635a | ||
|
|
273d4cda2e |
@@ -163,7 +163,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
|
||||
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
expect(mockDb.set).toHaveBeenCalled()
|
||||
expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
|
||||
@@ -251,7 +251,7 @@ describe('OAuth Utils', () => {
|
||||
|
||||
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
|
||||
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
|
||||
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
|
||||
expect(mockDb.update).toHaveBeenCalled()
|
||||
expect(mockDb.set).toHaveBeenCalled()
|
||||
expect(token).toBe('new-token')
|
||||
|
||||
594
apps/sim/blocks/blocks/snowflake.ts
Normal file
594
apps/sim/blocks/blocks/snowflake.ts
Normal file
@@ -0,0 +1,594 @@
|
||||
import { SnowflakeIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { SnowflakeResponse } from '@/tools/snowflake/types'
|
||||
|
||||
export const SnowflakeBlock: BlockConfig<SnowflakeResponse> = {
|
||||
type: 'snowflake',
|
||||
name: 'Snowflake',
|
||||
description: 'Execute queries on Snowflake data warehouse',
|
||||
authMode: AuthMode.ApiKey,
|
||||
longDescription:
|
||||
'Integrate Snowflake into your workflow. Execute SQL queries, insert, update, and delete rows, list databases, schemas, and tables, and describe table structures in your Snowflake data warehouse.',
|
||||
docsLink: 'https://docs.sim.ai/tools/snowflake',
|
||||
category: 'tools',
|
||||
bgColor: '#E0E0E0',
|
||||
icon: SnowflakeIcon,
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'Execute Query', id: 'execute_query' },
|
||||
{ label: 'Insert Rows', id: 'insert_rows' },
|
||||
{ label: 'Update Rows', id: 'update_rows' },
|
||||
{ label: 'Delete Rows', id: 'delete_rows' },
|
||||
{ label: 'List Databases', id: 'list_databases' },
|
||||
{ label: 'List Schemas', id: 'list_schemas' },
|
||||
{ label: 'List Tables', id: 'list_tables' },
|
||||
{ label: 'List Views', id: 'list_views' },
|
||||
{ label: 'List Warehouses', id: 'list_warehouses' },
|
||||
{ label: 'List File Formats', id: 'list_file_formats' },
|
||||
{ label: 'List Stages', id: 'list_stages' },
|
||||
{ label: 'Describe Table', id: 'describe_table' },
|
||||
],
|
||||
value: () => 'execute_query',
|
||||
},
|
||||
{
|
||||
id: 'accountUrl',
|
||||
title: 'Account URL',
|
||||
type: 'short-input',
|
||||
placeholder: 'your-account.snowflakecomputing.com',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'accessToken',
|
||||
title: 'Personal Access Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your Snowflake PAT',
|
||||
description: 'Generate a PAT in Snowflake Snowsight',
|
||||
password: true,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'warehouse',
|
||||
title: 'Warehouse',
|
||||
type: 'short-input',
|
||||
placeholder: 'Warehouse name',
|
||||
},
|
||||
{
|
||||
id: 'role',
|
||||
title: 'Role',
|
||||
type: 'short-input',
|
||||
placeholder: 'Role name',
|
||||
},
|
||||
{
|
||||
id: 'query',
|
||||
title: 'SQL Query',
|
||||
type: 'long-input',
|
||||
required: true,
|
||||
placeholder: 'Enter SQL query (e.g., SELECT * FROM database.schema.table LIMIT 10)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'execute_query',
|
||||
},
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
prompt: `You are an expert Snowflake SQL developer. Generate Snowflake SQL queries based on the user's natural language request.
|
||||
|
||||
### CONTEXT
|
||||
{context}
|
||||
|
||||
### CRITICAL INSTRUCTION
|
||||
Return ONLY the SQL query. Do not include any explanations, markdown formatting, comments, or additional text. Just the raw SQL query that can be executed directly in Snowflake.
|
||||
|
||||
### SNOWFLAKE SQL GUIDELINES
|
||||
1. **Syntax**: Use standard Snowflake SQL syntax and functions
|
||||
2. **Fully Qualified Names**: Use database.schema.table format when possible
|
||||
3. **Case Sensitivity**: Identifiers are case-insensitive unless quoted
|
||||
4. **Performance**: Consider using LIMIT clauses for large datasets
|
||||
5. **Data Types**: Use appropriate Snowflake data types (VARIANT for JSON, TIMESTAMP_NTZ, etc.)
|
||||
|
||||
### COMMON SNOWFLAKE SQL PATTERNS
|
||||
|
||||
**Basic SELECT**:
|
||||
SELECT * FROM database.schema.table LIMIT 100;
|
||||
|
||||
**Filtered Query**:
|
||||
SELECT column1, column2
|
||||
FROM database.schema.table
|
||||
WHERE status = 'active'
|
||||
AND created_at > DATEADD(day, -7, CURRENT_DATE())
|
||||
LIMIT 100;
|
||||
|
||||
**Aggregate Functions**:
|
||||
SELECT
|
||||
category,
|
||||
COUNT(*) as total_count,
|
||||
AVG(amount) as avg_amount,
|
||||
SUM(amount) as total_amount
|
||||
FROM database.schema.sales
|
||||
GROUP BY category
|
||||
ORDER BY total_amount DESC;
|
||||
|
||||
**JOIN Operations**:
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.name,
|
||||
o.order_id,
|
||||
o.total
|
||||
FROM database.schema.users u
|
||||
INNER JOIN database.schema.orders o
|
||||
ON u.user_id = o.user_id
|
||||
WHERE o.created_at > CURRENT_DATE() - 30;
|
||||
|
||||
**Window Functions**:
|
||||
SELECT
|
||||
user_id,
|
||||
order_date,
|
||||
amount,
|
||||
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) as row_num
|
||||
FROM database.schema.orders;
|
||||
|
||||
**JSON/VARIANT Queries**:
|
||||
SELECT
|
||||
id,
|
||||
json_data:field::STRING as field_value,
|
||||
json_data:nested.value::NUMBER as nested_value
|
||||
FROM database.schema.json_table
|
||||
WHERE json_data:status::STRING = 'active';
|
||||
|
||||
**FLATTEN for Arrays**:
|
||||
SELECT
|
||||
id,
|
||||
f.value::STRING as array_item
|
||||
FROM database.schema.table,
|
||||
LATERAL FLATTEN(input => array_column) f;
|
||||
|
||||
**CTE (Common Table Expression)**:
|
||||
WITH active_users AS (
|
||||
SELECT user_id, name
|
||||
FROM database.schema.users
|
||||
WHERE status = 'active'
|
||||
)
|
||||
SELECT
|
||||
au.name,
|
||||
COUNT(o.order_id) as order_count
|
||||
FROM active_users au
|
||||
LEFT JOIN database.schema.orders o ON au.user_id = o.user_id
|
||||
GROUP BY au.name;
|
||||
|
||||
**Date/Time Functions**:
|
||||
SELECT
|
||||
DATE_TRUNC('month', order_date) as month,
|
||||
COUNT(*) as orders
|
||||
FROM database.schema.orders
|
||||
WHERE order_date >= DATEADD(year, -1, CURRENT_DATE())
|
||||
GROUP BY month
|
||||
ORDER BY month DESC;
|
||||
|
||||
**INSERT Statement**:
|
||||
INSERT INTO database.schema.table (column1, column2, column3)
|
||||
VALUES ('value1', 123, CURRENT_TIMESTAMP());
|
||||
|
||||
**UPDATE Statement**:
|
||||
UPDATE database.schema.table
|
||||
SET status = 'processed', updated_at = CURRENT_TIMESTAMP()
|
||||
WHERE id = 123;
|
||||
|
||||
**DELETE Statement**:
|
||||
DELETE FROM database.schema.table
|
||||
WHERE created_at < DATEADD(year, -2, CURRENT_DATE());
|
||||
|
||||
**MERGE Statement (Upsert)**:
|
||||
MERGE INTO database.schema.target t
|
||||
USING database.schema.source s
|
||||
ON t.id = s.id
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET t.value = s.value, t.updated_at = CURRENT_TIMESTAMP()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (id, value, created_at) VALUES (s.id, s.value, CURRENT_TIMESTAMP());
|
||||
|
||||
### SNOWFLAKE SPECIFIC FEATURES
|
||||
|
||||
**SAMPLE Clause** (for testing with large tables):
|
||||
SELECT * FROM database.schema.large_table SAMPLE (1000 ROWS);
|
||||
|
||||
**QUALIFY Clause** (filter window functions):
|
||||
SELECT
|
||||
user_id,
|
||||
order_date,
|
||||
amount
|
||||
FROM database.schema.orders
|
||||
QUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) = 1;
|
||||
|
||||
**Time Travel**:
|
||||
SELECT * FROM database.schema.table AT (TIMESTAMP => '2024-01-01 00:00:00'::TIMESTAMP);
|
||||
|
||||
### BEST PRACTICES
|
||||
1. Always use LIMIT when exploring data
|
||||
2. Use WHERE clauses to filter data efficiently
|
||||
3. Index commonly queried columns
|
||||
4. Use appropriate date functions (DATEADD, DATE_TRUNC, DATEDIFF)
|
||||
5. For JSON data, use proper casting (::STRING, ::NUMBER, etc.)
|
||||
6. Use CTEs for complex queries to improve readability
|
||||
|
||||
### REMEMBER
|
||||
Return ONLY the SQL query - no explanations, no markdown code blocks, no extra text. The query should be ready to execute.`,
|
||||
placeholder:
|
||||
'Describe the SQL query you need (e.g., "Get all orders from the last 7 days with customer names")...',
|
||||
generationType: 'sql-query',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'database',
|
||||
title: 'Database',
|
||||
type: 'short-input',
|
||||
placeholder: 'Database name',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_schemas',
|
||||
'list_tables',
|
||||
'list_views',
|
||||
'list_file_formats',
|
||||
'list_stages',
|
||||
'describe_table',
|
||||
'insert_rows',
|
||||
'update_rows',
|
||||
'delete_rows',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'schema',
|
||||
title: 'Schema',
|
||||
type: 'short-input',
|
||||
placeholder: 'Schema name',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_tables',
|
||||
'list_views',
|
||||
'list_file_formats',
|
||||
'list_stages',
|
||||
'describe_table',
|
||||
'insert_rows',
|
||||
'update_rows',
|
||||
'delete_rows',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'table',
|
||||
title: 'Table',
|
||||
type: 'short-input',
|
||||
placeholder: 'Table name',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['describe_table', 'insert_rows', 'update_rows', 'delete_rows'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'columns',
|
||||
title: 'Columns',
|
||||
type: 'long-input',
|
||||
placeholder: '["column1", "column2", "column3"]',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'insert_rows',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
placeholder: '[["value1", "value2", "value3"], ["value4", "value5", "value6"]]',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'insert_rows',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'updates',
|
||||
title: 'Updates',
|
||||
type: 'long-input',
|
||||
placeholder: '{"column1": "new_value", "column2": 123, "updated_at": "2024-01-01"}',
|
||||
required: true,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'update_rows',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'whereClause',
|
||||
title: 'WHERE Clause',
|
||||
type: 'long-input',
|
||||
placeholder: 'id = 123 (leave empty to update/delete ALL rows)',
|
||||
required: false,
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['update_rows', 'delete_rows'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'timeout',
|
||||
title: 'Timeout (seconds)',
|
||||
type: 'short-input',
|
||||
placeholder: '60',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'execute_query',
|
||||
},
|
||||
},
|
||||
],
|
||||
tools: {
|
||||
access: [
|
||||
'snowflake_execute_query',
|
||||
'snowflake_insert_rows',
|
||||
'snowflake_update_rows',
|
||||
'snowflake_delete_rows',
|
||||
'snowflake_list_databases',
|
||||
'snowflake_list_schemas',
|
||||
'snowflake_list_tables',
|
||||
'snowflake_list_views',
|
||||
'snowflake_list_warehouses',
|
||||
'snowflake_list_file_formats',
|
||||
'snowflake_list_stages',
|
||||
'snowflake_describe_table',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => {
|
||||
switch (params.operation) {
|
||||
case 'execute_query':
|
||||
return 'snowflake_execute_query'
|
||||
case 'insert_rows':
|
||||
return 'snowflake_insert_rows'
|
||||
case 'update_rows':
|
||||
return 'snowflake_update_rows'
|
||||
case 'delete_rows':
|
||||
return 'snowflake_delete_rows'
|
||||
case 'list_databases':
|
||||
return 'snowflake_list_databases'
|
||||
case 'list_schemas':
|
||||
return 'snowflake_list_schemas'
|
||||
case 'list_tables':
|
||||
return 'snowflake_list_tables'
|
||||
case 'list_views':
|
||||
return 'snowflake_list_views'
|
||||
case 'list_warehouses':
|
||||
return 'snowflake_list_warehouses'
|
||||
case 'list_file_formats':
|
||||
return 'snowflake_list_file_formats'
|
||||
case 'list_stages':
|
||||
return 'snowflake_list_stages'
|
||||
case 'describe_table':
|
||||
return 'snowflake_describe_table'
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${params.operation}`)
|
||||
}
|
||||
},
|
||||
params: (params) => {
|
||||
const { operation, ...rest } = params
|
||||
|
||||
// Build base params - use PAT directly as accessToken
|
||||
const baseParams: Record<string, any> = {
|
||||
accessToken: params.accessToken,
|
||||
accountUrl: params.accountUrl,
|
||||
}
|
||||
|
||||
// Add optional warehouse and role if provided
|
||||
if (params.warehouse) {
|
||||
baseParams.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
baseParams.role = params.role
|
||||
}
|
||||
|
||||
// Operation-specific params
|
||||
switch (operation) {
|
||||
case 'execute_query': {
|
||||
if (!params.query) {
|
||||
throw new Error('Query is required for execute_query operation')
|
||||
}
|
||||
baseParams.query = params.query
|
||||
if (params.database) baseParams.database = params.database
|
||||
if (params.schema) baseParams.schema = params.schema
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_databases': {
|
||||
// No additional params needed
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_schemas': {
|
||||
if (!params.database) {
|
||||
throw new Error('Database is required for list_schemas operation')
|
||||
}
|
||||
baseParams.database = params.database
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_tables': {
|
||||
if (!params.database || !params.schema) {
|
||||
throw new Error('Database and Schema are required for list_tables operation')
|
||||
}
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_views': {
|
||||
if (!params.database || !params.schema) {
|
||||
throw new Error('Database and Schema are required for list_views operation')
|
||||
}
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_warehouses': {
|
||||
// No additional params needed
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_file_formats': {
|
||||
if (!params.database || !params.schema) {
|
||||
throw new Error('Database and Schema are required for list_file_formats operation')
|
||||
}
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
break
|
||||
}
|
||||
|
||||
case 'list_stages': {
|
||||
if (!params.database || !params.schema) {
|
||||
throw new Error('Database and Schema are required for list_stages operation')
|
||||
}
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
break
|
||||
}
|
||||
|
||||
case 'describe_table': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error(
|
||||
'Database, Schema, and Table are required for describe_table operation'
|
||||
)
|
||||
}
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
break
|
||||
}
|
||||
|
||||
case 'insert_rows': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error('Database, Schema, and Table are required for insert_rows operation')
|
||||
}
|
||||
if (!params.columns || !params.values) {
|
||||
throw new Error('Columns and Values are required for insert_rows operation')
|
||||
}
|
||||
|
||||
// Parse columns and values if they are strings
|
||||
let columns = params.columns
|
||||
let values = params.values
|
||||
|
||||
if (typeof columns === 'string') {
|
||||
try {
|
||||
columns = JSON.parse(columns)
|
||||
} catch (e) {
|
||||
throw new Error('Columns must be a valid JSON array')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof values === 'string') {
|
||||
try {
|
||||
values = JSON.parse(values)
|
||||
} catch (e) {
|
||||
throw new Error('Values must be a valid JSON array of arrays')
|
||||
}
|
||||
}
|
||||
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
baseParams.columns = columns
|
||||
baseParams.values = values
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
case 'update_rows': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error('Database, Schema, and Table are required for update_rows operation')
|
||||
}
|
||||
if (!params.updates) {
|
||||
throw new Error('Updates object is required for update_rows operation')
|
||||
}
|
||||
|
||||
// Parse updates if it's a string
|
||||
let updates = params.updates
|
||||
if (typeof updates === 'string') {
|
||||
try {
|
||||
updates = JSON.parse(updates)
|
||||
} catch (e) {
|
||||
throw new Error('Updates must be a valid JSON object')
|
||||
}
|
||||
}
|
||||
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
baseParams.updates = updates
|
||||
if (params.whereClause) baseParams.whereClause = params.whereClause
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
case 'delete_rows': {
|
||||
if (!params.database || !params.schema || !params.table) {
|
||||
throw new Error('Database, Schema, and Table are required for delete_rows operation')
|
||||
}
|
||||
|
||||
baseParams.database = params.database
|
||||
baseParams.schema = params.schema
|
||||
baseParams.table = params.table
|
||||
if (params.whereClause) baseParams.whereClause = params.whereClause
|
||||
if (params.timeout) baseParams.timeout = Number.parseInt(params.timeout)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown operation: ${operation}`)
|
||||
}
|
||||
|
||||
return baseParams
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
description: 'Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
warehouse: { type: 'string', description: 'Warehouse name' },
|
||||
role: { type: 'string', description: 'Role name' },
|
||||
query: { type: 'string', description: 'SQL query to execute' },
|
||||
database: { type: 'string', description: 'Database name' },
|
||||
schema: { type: 'string', description: 'Schema name' },
|
||||
table: { type: 'string', description: 'Table name' },
|
||||
columns: { type: 'json', description: 'Array of column names for insert operation' },
|
||||
values: { type: 'json', description: 'Array of arrays containing values for insert operation' },
|
||||
updates: {
|
||||
type: 'json',
|
||||
description: 'Object containing column-value pairs for update operation',
|
||||
},
|
||||
whereClause: { type: 'string', description: 'WHERE clause for update/delete operations' },
|
||||
timeout: { type: 'string', description: 'Query timeout in seconds' },
|
||||
},
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
output: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Operation results containing query data, databases, schemas, tables, or column definitions based on the selected operation',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -97,6 +97,7 @@ import { SharepointBlock } from '@/blocks/blocks/sharepoint'
|
||||
import { ShopifyBlock } from '@/blocks/blocks/shopify'
|
||||
import { SlackBlock } from '@/blocks/blocks/slack'
|
||||
import { SmtpBlock } from '@/blocks/blocks/smtp'
|
||||
import { SnowflakeBlock } from '@/blocks/blocks/snowflake'
|
||||
import { SSHBlock } from '@/blocks/blocks/ssh'
|
||||
import { StagehandBlock } from '@/blocks/blocks/stagehand'
|
||||
import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent'
|
||||
@@ -234,6 +235,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
shopify: ShopifyBlock,
|
||||
slack: SlackBlock,
|
||||
smtp: SmtpBlock,
|
||||
snowflake: SnowflakeBlock,
|
||||
ssh: SSHBlock,
|
||||
stagehand: StagehandBlock,
|
||||
stagehand_agent: StagehandAgentBlock,
|
||||
|
||||
@@ -4089,3 +4089,18 @@ export function PolymarketIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SnowflakeIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='64'
|
||||
height='64'
|
||||
viewBox='0 0 64 64'
|
||||
fill='#29b5e8'
|
||||
>
|
||||
<path d='M9.86 15.298l13.008 7.8a3.72 3.72 0 0 0 4.589-.601 4.01 4.01 0 0 0 1.227-2.908V3.956a3.81 3.81 0 0 0-1.861-3.42 3.81 3.81 0 0 0-3.893 0 3.81 3.81 0 0 0-1.861 3.42v8.896l-7.387-4.43a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.28 1.4 5.422m31.27 7.8l13.008-7.8c1.84-1.143 2.458-3.533 1.4-5.424a3.75 3.75 0 0 0-5.22-1.452l-7.3 4.37v-8.84a3.81 3.81 0 1 0-7.615 0v15.323a4.08 4.08 0 0 0 .494 2.367c.482.903 1.314 1.57 2.3 1.844a3.71 3.71 0 0 0 2.922-.4M29.552 31.97c.013-.25.108-.5.272-.68l1.52-1.58a1.06 1.06 0 0 1 .658-.282h.057a1.05 1.05 0 0 1 .656.282l1.52 1.58a1.12 1.12 0 0 1 .272.681v.06a1.13 1.13 0 0 1-.272.683l-1.52 1.58a1.04 1.04 0 0 1-.656.284h-.057c-.246-.014-.48-.115-.658-.284l-1.52-1.58a1.13 1.13 0 0 1-.272-.683zm-4.604-.65v1.364a1.54 1.54 0 0 0 .372.93l5.16 5.357a1.42 1.42 0 0 0 .895.386h1.312a1.42 1.42 0 0 0 .895-.386l5.16-5.357a1.54 1.54 0 0 0 .372-.93V31.32a1.54 1.54 0 0 0-.372-.93l-5.16-5.357a1.42 1.42 0 0 0-.895-.386h-1.312a1.42 1.42 0 0 0-.895.386L25.32 30.4a1.55 1.55 0 0 0-.372.93M3.13 27.62l7.365 4.417L3.13 36.45a4.06 4.06 0 0 0-1.399 5.424 3.75 3.75 0 0 0 2.3 1.844c.986.274 2.042.133 2.922-.392l13.008-7.8c1.2-.762 1.9-2.078 1.9-3.492a4.16 4.16 0 0 0-1.9-3.492l-13.008-7.8a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.278 1.4 5.422m38.995 4.442a4 4 0 0 0 1.91 3.477l13 7.8c.88.524 1.934.666 2.92.392s1.817-.94 2.3-1.843a4.05 4.05 0 0 0-1.4-5.424L53.5 32.038l7.365-4.417c1.84-1.143 2.457-3.53 1.4-5.422a3.74 3.74 0 0 0-2.3-1.844c-.987-.274-2.042-.134-2.92.4l-13 7.8a4 4 0 0 0-1.91 3.507M25.48 40.508a3.7 3.7 0 0 0-2.611.464l-13.008 7.8c-1.84 1.143-2.456 3.53-1.4 5.422.483.903 1.314 1.57 2.3 1.843a3.75 3.75 0 0 0 2.922-.392l7.387-4.43v8.83a3.81 3.81 0 1 0 7.614 0V44.4a3.91 3.91 0 0 0-3.205-3.903m28.66 8.276l-13.008-7.8a3.75 3.75 0 0 0-2.922-.392 3.74 3.74 0 0 0-2.3 1.843 4.09 4.09 0 0 0-.494 2.37v15.25a3.81 3.81 0 1 0 7.614 0V51.28l7.287 4.37a3.79 3.79 0 0 0 2.922.4c.986-.265 1.818-.94 2.3-1.844 1.057-1.9.44-4.28-1.4-5.422' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ export type OAuthService =
|
||||
| 'shopify'
|
||||
| 'zoom'
|
||||
| 'wordpress'
|
||||
|
||||
export interface OAuthProviderConfig {
|
||||
id: OAuthProvider
|
||||
name: string
|
||||
|
||||
@@ -1010,6 +1010,20 @@ import {
|
||||
} from '@/tools/slack'
|
||||
import { smsSendTool } from '@/tools/sms'
|
||||
import { smtpSendMailTool } from '@/tools/smtp'
|
||||
import {
|
||||
snowflakeDeleteRowsTool,
|
||||
snowflakeDescribeTableTool,
|
||||
snowflakeExecuteQueryTool,
|
||||
snowflakeInsertRowsTool,
|
||||
snowflakeListDatabasesTool,
|
||||
snowflakeListFileFormatsTool,
|
||||
snowflakeListSchemasTool,
|
||||
snowflakeListStagesTool,
|
||||
snowflakeListTablesTool,
|
||||
snowflakeListViewsTool,
|
||||
snowflakeListWarehousesTool,
|
||||
snowflakeUpdateRowsTool,
|
||||
} from '@/tools/snowflake'
|
||||
import {
|
||||
checkCommandExistsTool as sshCheckCommandExistsTool,
|
||||
checkFileExistsTool as sshCheckFileExistsTool,
|
||||
@@ -2425,4 +2439,16 @@ export const tools: Record<string, ToolConfig> = {
|
||||
zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool,
|
||||
zoom_delete_recording: zoomDeleteRecordingTool,
|
||||
zoom_list_past_participants: zoomListPastParticipantsTool,
|
||||
snowflake_execute_query: snowflakeExecuteQueryTool,
|
||||
snowflake_insert_rows: snowflakeInsertRowsTool,
|
||||
snowflake_update_rows: snowflakeUpdateRowsTool,
|
||||
snowflake_delete_rows: snowflakeDeleteRowsTool,
|
||||
snowflake_list_databases: snowflakeListDatabasesTool,
|
||||
snowflake_list_schemas: snowflakeListSchemasTool,
|
||||
snowflake_list_tables: snowflakeListTablesTool,
|
||||
snowflake_list_views: snowflakeListViewsTool,
|
||||
snowflake_list_warehouses: snowflakeListWarehousesTool,
|
||||
snowflake_list_file_formats: snowflakeListFileFormatsTool,
|
||||
snowflake_list_stages: snowflakeListStagesTool,
|
||||
snowflake_describe_table: snowflakeDescribeTableTool,
|
||||
}
|
||||
|
||||
192
apps/sim/tools/snowflake/delete_rows.ts
Normal file
192
apps/sim/tools/snowflake/delete_rows.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeDeleteRowsParams,
|
||||
SnowflakeDeleteRowsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { parseAccountUrl, sanitizeIdentifier, validateWhereClause } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeDeleteRowsTool')
|
||||
|
||||
/**
|
||||
* Build DELETE SQL statement from parameters
|
||||
*/
|
||||
function buildDeleteSQL(
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
whereClause?: string
|
||||
): string {
|
||||
const sanitizedDatabase = sanitizeIdentifier(database)
|
||||
const sanitizedSchema = sanitizeIdentifier(schema)
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const fullTableName = `${sanitizedDatabase}.${sanitizedSchema}.${sanitizedTable}`
|
||||
|
||||
let sql = `DELETE FROM ${fullTableName}`
|
||||
|
||||
if (whereClause?.trim()) {
|
||||
validateWhereClause(whereClause)
|
||||
sql += ` WHERE ${whereClause}`
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export const snowflakeDeleteRowsTool: ToolConfig<
|
||||
SnowflakeDeleteRowsParams,
|
||||
SnowflakeDeleteRowsResponse
|
||||
> = {
|
||||
id: 'snowflake_delete_rows',
|
||||
name: 'Snowflake Delete Rows',
|
||||
description: 'Delete rows from a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name',
|
||||
},
|
||||
whereClause: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'WHERE clause to filter rows to delete (e.g., "id = 123" or "status = \'inactive\' AND created_at < \'2024-01-01\'"). WARNING: If not provided, ALL rows will be deleted.',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeDeleteRowsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeDeleteRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeDeleteRowsParams) => {
|
||||
// Build DELETE SQL
|
||||
const deleteSQL = buildDeleteSQL(
|
||||
params.database,
|
||||
params.schema,
|
||||
params.table,
|
||||
params.whereClause
|
||||
)
|
||||
|
||||
logger.info('Building DELETE statement', {
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
hasWhereClause: !!params.whereClause,
|
||||
})
|
||||
|
||||
// Log warning if no WHERE clause provided
|
||||
if (!params.whereClause) {
|
||||
logger.warn('DELETE statement has no WHERE clause - ALL rows will be deleted', {
|
||||
table: `${params.database}.${params.schema}.${params.table}`,
|
||||
})
|
||||
}
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: deleteSQL,
|
||||
timeout: params.timeout || 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeDeleteRowsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to delete rows from Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
table: params ? `${params.database}.${params.schema}.${params.table}` : 'unknown',
|
||||
})
|
||||
throw new Error(`Failed to delete rows: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract number of rows deleted from response
|
||||
const rowsDeleted = data.statementStatusUrl ? 'unknown' : 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
rowsDeleted,
|
||||
message: `Successfully deleted rows from ${params?.database}.${params?.schema}.${params?.table}`,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Delete operation result',
|
||||
},
|
||||
},
|
||||
}
|
||||
133
apps/sim/tools/snowflake/describe_table.ts
Normal file
133
apps/sim/tools/snowflake/describe_table.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeDescribeTableParams,
|
||||
SnowflakeDescribeTableResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl, sanitizeIdentifier } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeDescribeTableTool')
|
||||
|
||||
export const snowflakeDescribeTableTool: ToolConfig<
|
||||
SnowflakeDescribeTableParams,
|
||||
SnowflakeDescribeTableResponse
|
||||
> = {
|
||||
id: 'snowflake_describe_table',
|
||||
name: 'Snowflake Describe Table',
|
||||
description: 'Get the schema and structure of a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name to describe',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeDescribeTableParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeDescribeTableParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeDescribeTableParams) => {
|
||||
const sanitizedDatabase = sanitizeIdentifier(params.database)
|
||||
const sanitizedSchema = sanitizeIdentifier(params.schema)
|
||||
const sanitizedTable = sanitizeIdentifier(params.table)
|
||||
const fullTableName = `${sanitizedDatabase}.${sanitizedSchema}.${sanitizedTable}`
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: `DESCRIBE TABLE ${fullTableName}`,
|
||||
timeout: 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeDescribeTableParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to describe Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to describe table: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
columns: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Table column definitions and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
150
apps/sim/tools/snowflake/execute_query.ts
Normal file
150
apps/sim/tools/snowflake/execute_query.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeExecuteQueryParams,
|
||||
SnowflakeExecuteQueryResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import {
|
||||
extractColumnMetadata,
|
||||
extractResponseData,
|
||||
parseAccountUrl,
|
||||
} from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeExecuteQueryTool')
|
||||
|
||||
export const snowflakeExecuteQueryTool: ToolConfig<
|
||||
SnowflakeExecuteQueryParams,
|
||||
SnowflakeExecuteQueryResponse
|
||||
> = {
|
||||
id: 'snowflake_execute_query',
|
||||
name: 'Snowflake Execute Query',
|
||||
description: 'Execute a SQL query on your Snowflake data warehouse',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'SQL query to execute (SELECT, INSERT, UPDATE, DELETE, etc.)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Database to use for the query (optional)',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema to use for the query (optional)',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use for query execution (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use for query execution (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeExecuteQueryParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeExecuteQueryParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeExecuteQueryParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: params.query,
|
||||
timeout: params.timeout || 60,
|
||||
}
|
||||
|
||||
if (params.database) {
|
||||
requestBody.database = params.database
|
||||
}
|
||||
|
||||
if (params.schema) {
|
||||
requestBody.schema = params.schema
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeExecuteQueryParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to execute Snowflake query', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to execute query: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const extractedData = extractResponseData(data)
|
||||
const columns = extractColumnMetadata(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
data: extractedData,
|
||||
rowCount: extractedData.length,
|
||||
columns,
|
||||
message: data.message || 'Query executed successfully',
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Query execution results and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
27
apps/sim/tools/snowflake/index.ts
Normal file
27
apps/sim/tools/snowflake/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { snowflakeDeleteRowsTool } from '@/tools/snowflake/delete_rows'
|
||||
import { snowflakeDescribeTableTool } from '@/tools/snowflake/describe_table'
|
||||
import { snowflakeExecuteQueryTool } from '@/tools/snowflake/execute_query'
|
||||
import { snowflakeInsertRowsTool } from '@/tools/snowflake/insert_rows'
|
||||
import { snowflakeListDatabasesTool } from '@/tools/snowflake/list_databases'
|
||||
import { snowflakeListFileFormatsTool } from '@/tools/snowflake/list_file_formats'
|
||||
import { snowflakeListSchemasTool } from '@/tools/snowflake/list_schemas'
|
||||
import { snowflakeListStagesTool } from '@/tools/snowflake/list_stages'
|
||||
import { snowflakeListTablesTool } from '@/tools/snowflake/list_tables'
|
||||
import { snowflakeListViewsTool } from '@/tools/snowflake/list_views'
|
||||
import { snowflakeListWarehousesTool } from '@/tools/snowflake/list_warehouses'
|
||||
import { snowflakeUpdateRowsTool } from '@/tools/snowflake/update_rows'
|
||||
|
||||
export {
|
||||
snowflakeExecuteQueryTool,
|
||||
snowflakeListDatabasesTool,
|
||||
snowflakeListSchemasTool,
|
||||
snowflakeListTablesTool,
|
||||
snowflakeDescribeTableTool,
|
||||
snowflakeListViewsTool,
|
||||
snowflakeListWarehousesTool,
|
||||
snowflakeListFileFormatsTool,
|
||||
snowflakeListStagesTool,
|
||||
snowflakeInsertRowsTool,
|
||||
snowflakeUpdateRowsTool,
|
||||
snowflakeDeleteRowsTool,
|
||||
}
|
||||
226
apps/sim/tools/snowflake/insert_rows.ts
Normal file
226
apps/sim/tools/snowflake/insert_rows.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeInsertRowsParams,
|
||||
SnowflakeInsertRowsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { parseAccountUrl, sanitizeIdentifier } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeInsertRowsTool')
|
||||
|
||||
/**
|
||||
* Build INSERT SQL statement from parameters with proper identifier quoting
|
||||
*/
|
||||
function buildInsertSQL(
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
columns: string[],
|
||||
values: any[][]
|
||||
): string {
|
||||
const sanitizedDatabase = sanitizeIdentifier(database)
|
||||
const sanitizedSchema = sanitizeIdentifier(schema)
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const fullTableName = `${sanitizedDatabase}.${sanitizedSchema}.${sanitizedTable}`
|
||||
|
||||
const columnList = columns.map((col) => sanitizeIdentifier(col)).join(', ')
|
||||
|
||||
const valuesClause = values
|
||||
.map((rowValues) => {
|
||||
const formattedValues = rowValues.map((val) => {
|
||||
if (val === null || val === undefined) {
|
||||
return 'NULL'
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
return `'${val.replace(/'/g, "''")}'`
|
||||
}
|
||||
if (typeof val === 'boolean') {
|
||||
return val ? 'TRUE' : 'FALSE'
|
||||
}
|
||||
return String(val)
|
||||
})
|
||||
return `(${formattedValues.join(', ')})`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return `INSERT INTO ${fullTableName} (${columnList}) VALUES ${valuesClause}`
|
||||
}
|
||||
|
||||
export const snowflakeInsertRowsTool: ToolConfig<
|
||||
SnowflakeInsertRowsParams,
|
||||
SnowflakeInsertRowsResponse
|
||||
> = {
|
||||
id: 'snowflake_insert_rows',
|
||||
name: 'Snowflake Insert Rows',
|
||||
description: 'Insert rows into a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name',
|
||||
},
|
||||
columns: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Array of column names to insert data into',
|
||||
},
|
||||
values: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Array of arrays containing values to insert. Each inner array represents one row and must match the order of columns.',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeInsertRowsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeInsertRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeInsertRowsParams) => {
|
||||
// Validate inputs
|
||||
if (!Array.isArray(params.columns) || params.columns.length === 0) {
|
||||
throw new Error('Columns must be a non-empty array')
|
||||
}
|
||||
|
||||
if (!Array.isArray(params.values) || params.values.length === 0) {
|
||||
throw new Error('Values must be a non-empty array')
|
||||
}
|
||||
|
||||
// Validate each row has correct number of values
|
||||
for (let i = 0; i < params.values.length; i++) {
|
||||
if (!Array.isArray(params.values[i])) {
|
||||
throw new Error(`Values row ${i} must be an array`)
|
||||
}
|
||||
if (params.values[i].length !== params.columns.length) {
|
||||
throw new Error(
|
||||
`Values row ${i} has ${params.values[i].length} values but ${params.columns.length} columns were specified`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const insertSQL = buildInsertSQL(
|
||||
params.database,
|
||||
params.schema,
|
||||
params.table,
|
||||
params.columns,
|
||||
params.values
|
||||
)
|
||||
|
||||
logger.info('Building INSERT statement', {
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
columnCount: params.columns.length,
|
||||
rowCount: params.values.length,
|
||||
})
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: insertSQL,
|
||||
timeout: params.timeout || 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeInsertRowsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to insert rows into Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
table: params ? `${params.database}.${params.schema}.${params.table}` : 'unknown',
|
||||
})
|
||||
throw new Error(`Failed to insert rows: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const rowsInserted = data.statementStatusUrl ? params?.values.length || 0 : 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
rowsInserted,
|
||||
message: `Successfully inserted ${rowsInserted} row(s) into ${params?.database}.${params?.schema}.${params?.table}`,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Insert operation result with row count',
|
||||
},
|
||||
},
|
||||
}
|
||||
108
apps/sim/tools/snowflake/list_databases.ts
Normal file
108
apps/sim/tools/snowflake/list_databases.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeListDatabasesParams,
|
||||
SnowflakeListDatabasesResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListDatabasesTool')
|
||||
|
||||
export const snowflakeListDatabasesTool: ToolConfig<
|
||||
SnowflakeListDatabasesParams,
|
||||
SnowflakeListDatabasesResponse
|
||||
> = {
|
||||
id: 'snowflake_list_databases',
|
||||
name: 'Snowflake List Databases',
|
||||
description: 'List all databases in your Snowflake account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListDatabasesParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListDatabasesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListDatabasesParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: 'SHOW DATABASES',
|
||||
timeout: 60,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListDatabasesParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake databases', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list databases: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
databases: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of databases and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
120
apps/sim/tools/snowflake/list_file_formats.ts
Normal file
120
apps/sim/tools/snowflake/list_file_formats.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeListFileFormatsParams,
|
||||
SnowflakeListFileFormatsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListFileFormatsTool')
|
||||
|
||||
export const snowflakeListFileFormatsTool: ToolConfig<
|
||||
SnowflakeListFileFormatsParams,
|
||||
SnowflakeListFileFormatsResponse
|
||||
> = {
|
||||
id: 'snowflake_list_file_formats',
|
||||
name: 'Snowflake List File Formats',
|
||||
description: 'List all file formats in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name to list file formats from',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListFileFormatsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListFileFormatsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListFileFormatsParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: `SHOW FILE FORMATS IN ${params.database}.${params.schema}`,
|
||||
timeout: 60,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListFileFormatsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake file formats', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list file formats: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
fileFormats: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of file formats and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
117
apps/sim/tools/snowflake/list_schemas.ts
Normal file
117
apps/sim/tools/snowflake/list_schemas.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeListSchemasParams,
|
||||
SnowflakeListSchemasResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl, sanitizeIdentifier } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListSchemasTool')
|
||||
|
||||
export const snowflakeListSchemasTool: ToolConfig<
|
||||
SnowflakeListSchemasParams,
|
||||
SnowflakeListSchemasResponse
|
||||
> = {
|
||||
id: 'snowflake_list_schemas',
|
||||
name: 'Snowflake List Schemas',
|
||||
description: 'List all schemas in a Snowflake database',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name to list schemas from',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListSchemasParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListSchemasParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListSchemasParams) => {
|
||||
const sanitizedDatabase = sanitizeIdentifier(params.database)
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: `SHOW SCHEMAS IN DATABASE ${sanitizedDatabase}`,
|
||||
timeout: 60,
|
||||
database: params.database,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListSchemasParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake schemas', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list schemas: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
schemas: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of schemas and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
120
apps/sim/tools/snowflake/list_stages.ts
Normal file
120
apps/sim/tools/snowflake/list_stages.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeListStagesParams,
|
||||
SnowflakeListStagesResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListStagesTool')
|
||||
|
||||
export const snowflakeListStagesTool: ToolConfig<
|
||||
SnowflakeListStagesParams,
|
||||
SnowflakeListStagesResponse
|
||||
> = {
|
||||
id: 'snowflake_list_stages',
|
||||
name: 'Snowflake List Stages',
|
||||
description: 'List all stages in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name to list stages from',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListStagesParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListStagesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListStagesParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: `SHOW STAGES IN ${params.database}.${params.schema}`,
|
||||
timeout: 60,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListStagesParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake stages', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list stages: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
stages: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of stages and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
125
apps/sim/tools/snowflake/list_tables.ts
Normal file
125
apps/sim/tools/snowflake/list_tables.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeListTablesParams,
|
||||
SnowflakeListTablesResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl, sanitizeIdentifier } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListTablesTool')
|
||||
|
||||
export const snowflakeListTablesTool: ToolConfig<
|
||||
SnowflakeListTablesParams,
|
||||
SnowflakeListTablesResponse
|
||||
> = {
|
||||
id: 'snowflake_list_tables',
|
||||
name: 'Snowflake List Tables',
|
||||
description: 'List all tables in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name to list tables from',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListTablesParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListTablesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListTablesParams) => {
|
||||
const sanitizedDatabase = sanitizeIdentifier(params.database)
|
||||
const sanitizedSchema = sanitizeIdentifier(params.schema)
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: `SHOW TABLES IN ${sanitizedDatabase}.${sanitizedSchema}`,
|
||||
timeout: 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListTablesParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake tables', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list tables: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tables: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of tables and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
117
apps/sim/tools/snowflake/list_views.ts
Normal file
117
apps/sim/tools/snowflake/list_views.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { SnowflakeListViewsParams, SnowflakeListViewsResponse } from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListViewsTool')
|
||||
|
||||
export const snowflakeListViewsTool: ToolConfig<
|
||||
SnowflakeListViewsParams,
|
||||
SnowflakeListViewsResponse
|
||||
> = {
|
||||
id: 'snowflake_list_views',
|
||||
name: 'Snowflake List Views',
|
||||
description: 'List all views in a Snowflake schema',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name to list views from',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListViewsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListViewsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListViewsParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: `SHOW VIEWS IN ${params.database}.${params.schema}`,
|
||||
timeout: 60,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListViewsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake views', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list views: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
views: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of views and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
108
apps/sim/tools/snowflake/list_warehouses.ts
Normal file
108
apps/sim/tools/snowflake/list_warehouses.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeListWarehousesParams,
|
||||
SnowflakeListWarehousesResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { extractResponseData, parseAccountUrl } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeListWarehousesTool')
|
||||
|
||||
export const snowflakeListWarehousesTool: ToolConfig<
|
||||
SnowflakeListWarehousesParams,
|
||||
SnowflakeListWarehousesResponse
|
||||
> = {
|
||||
id: 'snowflake_list_warehouses',
|
||||
name: 'Snowflake List Warehouses',
|
||||
description: 'List all warehouses in the Snowflake account',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeListWarehousesParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeListWarehousesParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeListWarehousesParams) => {
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: 'SHOW WAREHOUSES',
|
||||
timeout: 60,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeListWarehousesParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to list Snowflake warehouses', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Failed to list warehouses: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const extractedData = extractResponseData(data)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
warehouses: extractedData,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'List of warehouses and metadata',
|
||||
},
|
||||
},
|
||||
}
|
||||
342
apps/sim/tools/snowflake/types.ts
Normal file
342
apps/sim/tools/snowflake/types.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Snowflake tool types and interfaces
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base parameters for Snowflake operations
|
||||
*/
|
||||
export interface SnowflakeBaseParams {
|
||||
accessToken: string
|
||||
accountUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for executing a SQL query
|
||||
*/
|
||||
export interface SnowflakeExecuteQueryParams extends SnowflakeBaseParams {
|
||||
query: string
|
||||
database?: string
|
||||
schema?: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing databases
|
||||
*/
|
||||
export interface SnowflakeListDatabasesParams extends SnowflakeBaseParams {
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing schemas
|
||||
*/
|
||||
export interface SnowflakeListSchemasParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing tables
|
||||
*/
|
||||
export interface SnowflakeListTablesParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for describing a table
|
||||
*/
|
||||
export interface SnowflakeDescribeTableParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing views
|
||||
*/
|
||||
export interface SnowflakeListViewsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing warehouses
|
||||
*/
|
||||
export interface SnowflakeListWarehousesParams extends SnowflakeBaseParams {
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing file formats
|
||||
*/
|
||||
export interface SnowflakeListFileFormatsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for listing stages
|
||||
*/
|
||||
export interface SnowflakeListStagesParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for execute query operations
|
||||
*/
|
||||
export interface SnowflakeExecuteQueryResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
message?: string
|
||||
data?: any[]
|
||||
rowCount?: number
|
||||
columns?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list databases operation
|
||||
*/
|
||||
export interface SnowflakeListDatabasesResponse extends ToolResponse {
|
||||
output: {
|
||||
databases?: Array<{
|
||||
name: string
|
||||
created_on: string
|
||||
owner: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list schemas operation
|
||||
*/
|
||||
export interface SnowflakeListSchemasResponse extends ToolResponse {
|
||||
output: {
|
||||
schemas?: Array<{
|
||||
name: string
|
||||
database_name: string
|
||||
created_on: string
|
||||
owner: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list tables operation
|
||||
*/
|
||||
export interface SnowflakeListTablesResponse extends ToolResponse {
|
||||
output: {
|
||||
tables?: Array<{
|
||||
name: string
|
||||
database_name: string
|
||||
schema_name: string
|
||||
kind: string
|
||||
created_on: string
|
||||
row_count: number
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for describe table operation
|
||||
*/
|
||||
export interface SnowflakeDescribeTableResponse extends ToolResponse {
|
||||
output: {
|
||||
columns?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
kind: string
|
||||
null: string
|
||||
default: string | null
|
||||
primary_key: string
|
||||
unique_key: string
|
||||
check: string | null
|
||||
expression: string | null
|
||||
comment: string | null
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list views operation
|
||||
*/
|
||||
export interface SnowflakeListViewsResponse extends ToolResponse {
|
||||
output: {
|
||||
views?: Array<{
|
||||
name: string
|
||||
database_name: string
|
||||
schema_name: string
|
||||
created_on: string
|
||||
owner: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list warehouses operation
|
||||
*/
|
||||
export interface SnowflakeListWarehousesResponse extends ToolResponse {
|
||||
output: {
|
||||
warehouses?: Array<{
|
||||
name: string
|
||||
state: string
|
||||
size: string
|
||||
created_on: string
|
||||
owner: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list file formats operation
|
||||
*/
|
||||
export interface SnowflakeListFileFormatsResponse extends ToolResponse {
|
||||
output: {
|
||||
fileFormats?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
owner: string
|
||||
created_on: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for list stages operation
|
||||
*/
|
||||
export interface SnowflakeListStagesResponse extends ToolResponse {
|
||||
output: {
|
||||
stages?: Array<{
|
||||
name: string
|
||||
type: string
|
||||
url: string
|
||||
created_on: string
|
||||
owner: string
|
||||
}>
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for inserting rows
|
||||
*/
|
||||
export interface SnowflakeInsertRowsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
columns: string[]
|
||||
values: any[][]
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for updating rows
|
||||
*/
|
||||
export interface SnowflakeUpdateRowsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
updates: Record<string, any>
|
||||
whereClause?: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for deleting rows
|
||||
*/
|
||||
export interface SnowflakeDeleteRowsParams extends SnowflakeBaseParams {
|
||||
database: string
|
||||
schema: string
|
||||
table: string
|
||||
whereClause?: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for insert rows operation
|
||||
*/
|
||||
export interface SnowflakeInsertRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
rowsInserted?: number
|
||||
message?: string
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for update rows operation
|
||||
*/
|
||||
export interface SnowflakeUpdateRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
rowsUpdated?: number | string
|
||||
message?: string
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for delete rows operation
|
||||
*/
|
||||
export interface SnowflakeDeleteRowsResponse extends ToolResponse {
|
||||
output: {
|
||||
statementHandle?: string
|
||||
rowsDeleted?: number | string
|
||||
message?: string
|
||||
ts: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic Snowflake response type for the block
|
||||
*/
|
||||
export type SnowflakeResponse =
|
||||
| SnowflakeExecuteQueryResponse
|
||||
| SnowflakeListDatabasesResponse
|
||||
| SnowflakeListSchemasResponse
|
||||
| SnowflakeListTablesResponse
|
||||
| SnowflakeDescribeTableResponse
|
||||
| SnowflakeListViewsResponse
|
||||
| SnowflakeListWarehousesResponse
|
||||
| SnowflakeListFileFormatsResponse
|
||||
| SnowflakeListStagesResponse
|
||||
| SnowflakeInsertRowsResponse
|
||||
| SnowflakeUpdateRowsResponse
|
||||
| SnowflakeDeleteRowsResponse
|
||||
232
apps/sim/tools/snowflake/update_rows.ts
Normal file
232
apps/sim/tools/snowflake/update_rows.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type {
|
||||
SnowflakeUpdateRowsParams,
|
||||
SnowflakeUpdateRowsResponse,
|
||||
} from '@/tools/snowflake/types'
|
||||
import { parseAccountUrl, sanitizeIdentifier, validateWhereClause } from '@/tools/snowflake/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('SnowflakeUpdateRowsTool')
|
||||
|
||||
/**
|
||||
* Build UPDATE SQL statement from parameters with proper identifier quoting
|
||||
*/
|
||||
function buildUpdateSQL(
|
||||
database: string,
|
||||
schema: string,
|
||||
table: string,
|
||||
updates: Record<string, any>,
|
||||
whereClause?: string
|
||||
): string {
|
||||
const sanitizedDatabase = sanitizeIdentifier(database)
|
||||
const sanitizedSchema = sanitizeIdentifier(schema)
|
||||
const sanitizedTable = sanitizeIdentifier(table)
|
||||
const fullTableName = `${sanitizedDatabase}.${sanitizedSchema}.${sanitizedTable}`
|
||||
|
||||
const setClause = Object.entries(updates)
|
||||
.map(([column, value]) => {
|
||||
const sanitizedColumn = sanitizeIdentifier(column)
|
||||
|
||||
let formattedValue: string
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
formattedValue = 'NULL'
|
||||
} else if (typeof value === 'string') {
|
||||
// Escape single quotes by doubling them
|
||||
formattedValue = `'${value.replace(/'/g, "''")}'`
|
||||
} else if (typeof value === 'boolean') {
|
||||
formattedValue = value ? 'TRUE' : 'FALSE'
|
||||
} else {
|
||||
formattedValue = String(value)
|
||||
}
|
||||
|
||||
return `${sanitizedColumn} = ${formattedValue}`
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
let sql = `UPDATE ${fullTableName} SET ${setClause}`
|
||||
|
||||
if (whereClause?.trim()) {
|
||||
validateWhereClause(whereClause)
|
||||
sql += ` WHERE ${whereClause}`
|
||||
}
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export const snowflakeUpdateRowsTool: ToolConfig<
|
||||
SnowflakeUpdateRowsParams,
|
||||
SnowflakeUpdateRowsResponse
|
||||
> = {
|
||||
id: 'snowflake_update_rows',
|
||||
name: 'Snowflake Update Rows',
|
||||
description: 'Update rows in a Snowflake table',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'Snowflake Personal Access Token (PAT)',
|
||||
},
|
||||
accountUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Your Snowflake account URL (e.g., xy12345.us-east-1.snowflakecomputing.com)',
|
||||
},
|
||||
database: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Database name',
|
||||
},
|
||||
schema: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Schema name',
|
||||
},
|
||||
table: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Table name',
|
||||
},
|
||||
updates: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Object containing column-value pairs to update (e.g., {"status": "active", "updated_at": "2024-01-01"})',
|
||||
},
|
||||
whereClause: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'WHERE clause to filter rows to update (e.g., "id = 123" or "status = \'pending\' AND created_at < \'2024-01-01\'"). If not provided, all rows will be updated.',
|
||||
},
|
||||
warehouse: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Warehouse to use (optional)',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Role to use (optional)',
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description: 'Query timeout in seconds (default: 60)',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params: SnowflakeUpdateRowsParams) => {
|
||||
const cleanUrl = parseAccountUrl(params.accountUrl)
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
},
|
||||
method: 'POST',
|
||||
headers: (params: SnowflakeUpdateRowsParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
}),
|
||||
body: (params: SnowflakeUpdateRowsParams) => {
|
||||
// Validate inputs
|
||||
if (
|
||||
!params.updates ||
|
||||
typeof params.updates !== 'object' ||
|
||||
Object.keys(params.updates).length === 0
|
||||
) {
|
||||
throw new Error('Updates must be a non-empty object with column-value pairs')
|
||||
}
|
||||
|
||||
// Build UPDATE SQL
|
||||
const updateSQL = buildUpdateSQL(
|
||||
params.database,
|
||||
params.schema,
|
||||
params.table,
|
||||
params.updates,
|
||||
params.whereClause
|
||||
)
|
||||
|
||||
logger.info('Building UPDATE statement', {
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
updateColumnCount: Object.keys(params.updates).length,
|
||||
hasWhereClause: !!params.whereClause,
|
||||
})
|
||||
|
||||
// Log warning if no WHERE clause provided
|
||||
if (!params.whereClause) {
|
||||
logger.warn('UPDATE statement has no WHERE clause - all rows will be updated', {
|
||||
table: `${params.database}.${params.schema}.${params.table}`,
|
||||
})
|
||||
}
|
||||
|
||||
const requestBody: Record<string, any> = {
|
||||
statement: updateSQL,
|
||||
timeout: params.timeout || 60,
|
||||
database: params.database,
|
||||
schema: params.schema,
|
||||
}
|
||||
|
||||
if (params.warehouse) {
|
||||
requestBody.warehouse = params.warehouse
|
||||
}
|
||||
|
||||
if (params.role) {
|
||||
requestBody.role = params.role
|
||||
}
|
||||
|
||||
return requestBody
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response, params?: SnowflakeUpdateRowsParams) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Failed to update rows in Snowflake table', {
|
||||
status: response.status,
|
||||
errorText,
|
||||
table: params ? `${params.database}.${params.schema}.${params.table}` : 'unknown',
|
||||
})
|
||||
throw new Error(`Failed to update rows: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Extract number of rows updated from response
|
||||
const rowsUpdated = data.statementStatusUrl ? 'unknown' : 0
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
statementHandle: data.statementHandle,
|
||||
rowsUpdated,
|
||||
message: `Successfully updated rows in ${params?.database}.${params?.schema}.${params?.table}`,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: 'Operation success status',
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
description: 'Update operation result',
|
||||
},
|
||||
},
|
||||
}
|
||||
184
apps/sim/tools/snowflake/utils.ts
Normal file
184
apps/sim/tools/snowflake/utils.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('Snowflake Utils')
|
||||
|
||||
/**
|
||||
* Build the base Snowflake SQL API URL
|
||||
*/
|
||||
export function buildSnowflakeSQLAPIUrl(accountUrl: string): string {
|
||||
// Remove https:// if present
|
||||
const cleanUrl = accountUrl.replace(/^https?:\/\//, '')
|
||||
return `https://${cleanUrl}/api/v2/statements`
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Snowflake SQL statement
|
||||
*/
|
||||
export async function executeSnowflakeStatement(
|
||||
accountUrl: string,
|
||||
accessToken: string,
|
||||
query: string,
|
||||
options?: {
|
||||
database?: string
|
||||
schema?: string
|
||||
warehouse?: string
|
||||
role?: string
|
||||
timeout?: number
|
||||
async?: boolean
|
||||
}
|
||||
): Promise<any> {
|
||||
const apiUrl = buildSnowflakeSQLAPIUrl(accountUrl)
|
||||
|
||||
const requestBody: any = {
|
||||
statement: query,
|
||||
timeout: options?.timeout || 60,
|
||||
}
|
||||
|
||||
if (options?.database) {
|
||||
requestBody.database = options.database
|
||||
}
|
||||
|
||||
if (options?.schema) {
|
||||
requestBody.schema = options.schema
|
||||
}
|
||||
|
||||
if (options?.warehouse) {
|
||||
requestBody.warehouse = options.warehouse
|
||||
}
|
||||
|
||||
if (options?.role) {
|
||||
requestBody.role = options.role
|
||||
}
|
||||
|
||||
if (options?.async) {
|
||||
requestBody.async = true
|
||||
}
|
||||
|
||||
logger.info('Executing Snowflake statement', {
|
||||
accountUrl,
|
||||
hasAccessToken: !!accessToken,
|
||||
database: options?.database,
|
||||
schema: options?.schema,
|
||||
})
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'X-Snowflake-Authorization-Token-Type': 'PROGRAMMATIC_ACCESS_TOKEN',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
logger.error('Snowflake API error', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
errorText,
|
||||
})
|
||||
throw new Error(`Snowflake API error: ${response.status} - ${errorText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
logger.info('Snowflake statement executed successfully')
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Snowflake account URL to ensure proper format
|
||||
*/
|
||||
export function parseAccountUrl(accountUrl: string): string {
|
||||
// Remove protocol if present
|
||||
let cleanUrl = accountUrl.replace(/^https?:\/\//, '')
|
||||
|
||||
// Remove trailing slash if present
|
||||
cleanUrl = cleanUrl.replace(/\/$/, '')
|
||||
|
||||
// If it doesn't contain snowflakecomputing.com, append it
|
||||
if (!cleanUrl.includes('snowflakecomputing.com')) {
|
||||
cleanUrl = `${cleanUrl}.snowflakecomputing.com`
|
||||
}
|
||||
|
||||
return cleanUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract data from Snowflake API response
|
||||
*/
|
||||
export function extractResponseData(response: any): any[] {
|
||||
if (!response.data || response.data.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rows: any[] = []
|
||||
|
||||
for (const row of response.data) {
|
||||
const rowData: any = {}
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const columnName = response.resultSetMetaData?.rowType?.[i]?.name || `column_${i}`
|
||||
rowData[columnName] = row[i]
|
||||
}
|
||||
rows.push(rowData)
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract column metadata from Snowflake API response
|
||||
*/
|
||||
export function extractColumnMetadata(response: any): Array<{ name: string; type: string }> {
|
||||
if (!response.resultSetMetaData?.rowType) {
|
||||
return []
|
||||
}
|
||||
|
||||
return response.resultSetMetaData.rowType.map((col: any) => ({
|
||||
name: col.name,
|
||||
type: col.type,
|
||||
}))
|
||||
}
|
||||
|
||||
export function sanitizeIdentifier(identifier: string): string {
|
||||
if (identifier.includes('.')) {
|
||||
const parts = identifier.split('.')
|
||||
return parts.map((part) => sanitizeSingleIdentifier(part)).join('.')
|
||||
}
|
||||
|
||||
return sanitizeSingleIdentifier(identifier)
|
||||
}
|
||||
|
||||
export function validateWhereClause(where: string): void {
|
||||
const dangerousPatterns = [
|
||||
/;\s*(drop|delete|insert|update|create|alter|grant|revoke|truncate)/i,
|
||||
/union\s+select/i,
|
||||
/into\s+outfile/i,
|
||||
/load_file/i,
|
||||
/--/,
|
||||
/\/\*/,
|
||||
/\*\//,
|
||||
/xp_cmdshell/i,
|
||||
/exec\s*\(/i,
|
||||
/execute\s+immediate/i,
|
||||
]
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(where)) {
|
||||
throw new Error('WHERE clause contains potentially dangerous operation')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeSingleIdentifier(identifier: string): string {
|
||||
const cleaned = identifier.replace(/"/g, '')
|
||||
|
||||
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.`
|
||||
)
|
||||
}
|
||||
|
||||
return `"${cleaned}"`
|
||||
}
|
||||
Reference in New Issue
Block a user