mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-19 20:08:04 -05:00
Compare commits
12 Commits
fix/copilo
...
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')
|
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.update).toHaveBeenCalled()
|
||||||
expect(mockDb.set).toHaveBeenCalled()
|
expect(mockDb.set).toHaveBeenCalled()
|
||||||
expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
|
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')
|
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.update).toHaveBeenCalled()
|
||||||
expect(mockDb.set).toHaveBeenCalled()
|
expect(mockDb.set).toHaveBeenCalled()
|
||||||
expect(token).toBe('new-token')
|
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 { ShopifyBlock } from '@/blocks/blocks/shopify'
|
||||||
import { SlackBlock } from '@/blocks/blocks/slack'
|
import { SlackBlock } from '@/blocks/blocks/slack'
|
||||||
import { SmtpBlock } from '@/blocks/blocks/smtp'
|
import { SmtpBlock } from '@/blocks/blocks/smtp'
|
||||||
|
import { SnowflakeBlock } from '@/blocks/blocks/snowflake'
|
||||||
import { SSHBlock } from '@/blocks/blocks/ssh'
|
import { SSHBlock } from '@/blocks/blocks/ssh'
|
||||||
import { StagehandBlock } from '@/blocks/blocks/stagehand'
|
import { StagehandBlock } from '@/blocks/blocks/stagehand'
|
||||||
import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent'
|
import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent'
|
||||||
@@ -234,6 +235,7 @@ export const registry: Record<string, BlockConfig> = {
|
|||||||
shopify: ShopifyBlock,
|
shopify: ShopifyBlock,
|
||||||
slack: SlackBlock,
|
slack: SlackBlock,
|
||||||
smtp: SmtpBlock,
|
smtp: SmtpBlock,
|
||||||
|
snowflake: SnowflakeBlock,
|
||||||
ssh: SSHBlock,
|
ssh: SSHBlock,
|
||||||
stagehand: StagehandBlock,
|
stagehand: StagehandBlock,
|
||||||
stagehand_agent: StagehandAgentBlock,
|
stagehand_agent: StagehandAgentBlock,
|
||||||
|
|||||||
@@ -4089,3 +4089,18 @@ export function PolymarketIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</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'
|
| 'shopify'
|
||||||
| 'zoom'
|
| 'zoom'
|
||||||
| 'wordpress'
|
| 'wordpress'
|
||||||
|
|
||||||
export interface OAuthProviderConfig {
|
export interface OAuthProviderConfig {
|
||||||
id: OAuthProvider
|
id: OAuthProvider
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1010,6 +1010,20 @@ import {
|
|||||||
} from '@/tools/slack'
|
} from '@/tools/slack'
|
||||||
import { smsSendTool } from '@/tools/sms'
|
import { smsSendTool } from '@/tools/sms'
|
||||||
import { smtpSendMailTool } from '@/tools/smtp'
|
import { smtpSendMailTool } from '@/tools/smtp'
|
||||||
|
import {
|
||||||
|
snowflakeDeleteRowsTool,
|
||||||
|
snowflakeDescribeTableTool,
|
||||||
|
snowflakeExecuteQueryTool,
|
||||||
|
snowflakeInsertRowsTool,
|
||||||
|
snowflakeListDatabasesTool,
|
||||||
|
snowflakeListFileFormatsTool,
|
||||||
|
snowflakeListSchemasTool,
|
||||||
|
snowflakeListStagesTool,
|
||||||
|
snowflakeListTablesTool,
|
||||||
|
snowflakeListViewsTool,
|
||||||
|
snowflakeListWarehousesTool,
|
||||||
|
snowflakeUpdateRowsTool,
|
||||||
|
} from '@/tools/snowflake'
|
||||||
import {
|
import {
|
||||||
checkCommandExistsTool as sshCheckCommandExistsTool,
|
checkCommandExistsTool as sshCheckCommandExistsTool,
|
||||||
checkFileExistsTool as sshCheckFileExistsTool,
|
checkFileExistsTool as sshCheckFileExistsTool,
|
||||||
@@ -2425,4 +2439,16 @@ export const tools: Record<string, ToolConfig> = {
|
|||||||
zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool,
|
zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool,
|
||||||
zoom_delete_recording: zoomDeleteRecordingTool,
|
zoom_delete_recording: zoomDeleteRecordingTool,
|
||||||
zoom_list_past_participants: zoomListPastParticipantsTool,
|
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