Compare commits

...

12 Commits

Author SHA1 Message Date
aadamgough
e7936f7e1d removed comments and utils 2025-12-06 15:35:08 -08:00
aadamgough
7671ec35e8 fix 2025-12-06 15:26:54 -08:00
aadamgough
addd05bb8e added env to env.ts 2025-12-06 14:47:24 -08:00
aadamgough
248ebac78b removed unnecessary file and reverted oauth.ts 2025-12-06 14:46:16 -08:00
aadamgough
89ba330846 reformatted to PAT from oauth 2025-12-06 14:45:44 -08:00
aadamgough
342c4a2081 fixed build 2025-12-06 14:44:31 -08:00
aadamgough
b3f6bffc55 added new test routes to fix build 2025-12-06 14:44:29 -08:00
aadamgough
883c70140a fixed vulnerability and lint 2025-12-06 14:43:51 -08:00
aadamgough
f093f97cc8 component fix 2025-12-06 14:43:33 -08:00
aadamgough
17a164508f client id and secret for individual projects, no more single client and secret 2025-12-06 14:42:10 -08:00
aadamgough
e3dca6635a working, but certain create, delete update queries are not 2025-12-06 14:40:00 -08:00
aadamgough
273d4cda2e first push 2025-12-06 14:38:40 -08:00
21 changed files with 2941 additions and 2 deletions

View File

@@ -163,7 +163,7 @@ describe('OAuth Utils', () => {
const result = await refreshTokenIfNeeded('request-id', mockCredential, 'credential-id')
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
expect(mockDb.update).toHaveBeenCalled()
expect(mockDb.set).toHaveBeenCalled()
expect(result).toEqual({ accessToken: 'new-token', refreshed: true })
@@ -251,7 +251,7 @@ describe('OAuth Utils', () => {
const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id')
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token')
expect(mockRefreshOAuthToken).toHaveBeenCalledWith('google', 'refresh-token', undefined)
expect(mockDb.update).toHaveBeenCalled()
expect(mockDb.set).toHaveBeenCalled()
expect(token).toBe('new-token')

View 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',
},
},
}

View File

@@ -97,6 +97,7 @@ import { SharepointBlock } from '@/blocks/blocks/sharepoint'
import { ShopifyBlock } from '@/blocks/blocks/shopify'
import { SlackBlock } from '@/blocks/blocks/slack'
import { SmtpBlock } from '@/blocks/blocks/smtp'
import { SnowflakeBlock } from '@/blocks/blocks/snowflake'
import { SSHBlock } from '@/blocks/blocks/ssh'
import { StagehandBlock } from '@/blocks/blocks/stagehand'
import { StagehandAgentBlock } from '@/blocks/blocks/stagehand_agent'
@@ -234,6 +235,7 @@ export const registry: Record<string, BlockConfig> = {
shopify: ShopifyBlock,
slack: SlackBlock,
smtp: SmtpBlock,
snowflake: SnowflakeBlock,
ssh: SSHBlock,
stagehand: StagehandBlock,
stagehand_agent: StagehandAgentBlock,

View File

@@ -4089,3 +4089,18 @@ export function PolymarketIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function SnowflakeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns='http://www.w3.org/2000/svg'
width='64'
height='64'
viewBox='0 0 64 64'
fill='#29b5e8'
>
<path d='M9.86 15.298l13.008 7.8a3.72 3.72 0 0 0 4.589-.601 4.01 4.01 0 0 0 1.227-2.908V3.956a3.81 3.81 0 0 0-1.861-3.42 3.81 3.81 0 0 0-3.893 0 3.81 3.81 0 0 0-1.861 3.42v8.896l-7.387-4.43a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.28 1.4 5.422m31.27 7.8l13.008-7.8c1.84-1.143 2.458-3.533 1.4-5.424a3.75 3.75 0 0 0-5.22-1.452l-7.3 4.37v-8.84a3.81 3.81 0 1 0-7.615 0v15.323a4.08 4.08 0 0 0 .494 2.367c.482.903 1.314 1.57 2.3 1.844a3.71 3.71 0 0 0 2.922-.4M29.552 31.97c.013-.25.108-.5.272-.68l1.52-1.58a1.06 1.06 0 0 1 .658-.282h.057a1.05 1.05 0 0 1 .656.282l1.52 1.58a1.12 1.12 0 0 1 .272.681v.06a1.13 1.13 0 0 1-.272.683l-1.52 1.58a1.04 1.04 0 0 1-.656.284h-.057c-.246-.014-.48-.115-.658-.284l-1.52-1.58a1.13 1.13 0 0 1-.272-.683zm-4.604-.65v1.364a1.54 1.54 0 0 0 .372.93l5.16 5.357a1.42 1.42 0 0 0 .895.386h1.312a1.42 1.42 0 0 0 .895-.386l5.16-5.357a1.54 1.54 0 0 0 .372-.93V31.32a1.54 1.54 0 0 0-.372-.93l-5.16-5.357a1.42 1.42 0 0 0-.895-.386h-1.312a1.42 1.42 0 0 0-.895.386L25.32 30.4a1.55 1.55 0 0 0-.372.93M3.13 27.62l7.365 4.417L3.13 36.45a4.06 4.06 0 0 0-1.399 5.424 3.75 3.75 0 0 0 2.3 1.844c.986.274 2.042.133 2.922-.392l13.008-7.8c1.2-.762 1.9-2.078 1.9-3.492a4.16 4.16 0 0 0-1.9-3.492l-13.008-7.8a3.79 3.79 0 0 0-2.922-.4c-.986.265-1.818.94-2.3 1.844-1.057 1.9-.44 4.278 1.4 5.422m38.995 4.442a4 4 0 0 0 1.91 3.477l13 7.8c.88.524 1.934.666 2.92.392s1.817-.94 2.3-1.843a4.05 4.05 0 0 0-1.4-5.424L53.5 32.038l7.365-4.417c1.84-1.143 2.457-3.53 1.4-5.422a3.74 3.74 0 0 0-2.3-1.844c-.987-.274-2.042-.134-2.92.4l-13 7.8a4 4 0 0 0-1.91 3.507M25.48 40.508a3.7 3.7 0 0 0-2.611.464l-13.008 7.8c-1.84 1.143-2.456 3.53-1.4 5.422.483.903 1.314 1.57 2.3 1.843a3.75 3.75 0 0 0 2.922-.392l7.387-4.43v8.83a3.81 3.81 0 1 0 7.614 0V44.4a3.91 3.91 0 0 0-3.205-3.903m28.66 8.276l-13.008-7.8a3.75 3.75 0 0 0-2.922-.392 3.74 3.74 0 0 0-2.3 1.843 4.09 4.09 0 0 0-.494 2.37v15.25a3.81 3.81 0 1 0 7.614 0V51.28l7.287 4.37a3.79 3.79 0 0 0 2.922.4c.986-.265 1.818-.94 2.3-1.844 1.057-1.9.44-4.28-1.4-5.422' />
</svg>
)
}

View File

@@ -109,6 +109,7 @@ export type OAuthService =
| 'shopify'
| 'zoom'
| 'wordpress'
export interface OAuthProviderConfig {
id: OAuthProvider
name: string

View File

@@ -1010,6 +1010,20 @@ import {
} from '@/tools/slack'
import { smsSendTool } from '@/tools/sms'
import { smtpSendMailTool } from '@/tools/smtp'
import {
snowflakeDeleteRowsTool,
snowflakeDescribeTableTool,
snowflakeExecuteQueryTool,
snowflakeInsertRowsTool,
snowflakeListDatabasesTool,
snowflakeListFileFormatsTool,
snowflakeListSchemasTool,
snowflakeListStagesTool,
snowflakeListTablesTool,
snowflakeListViewsTool,
snowflakeListWarehousesTool,
snowflakeUpdateRowsTool,
} from '@/tools/snowflake'
import {
checkCommandExistsTool as sshCheckCommandExistsTool,
checkFileExistsTool as sshCheckFileExistsTool,
@@ -2425,4 +2439,16 @@ export const tools: Record<string, ToolConfig> = {
zoom_get_meeting_recordings: zoomGetMeetingRecordingsTool,
zoom_delete_recording: zoomDeleteRecordingTool,
zoom_list_past_participants: zoomListPastParticipantsTool,
snowflake_execute_query: snowflakeExecuteQueryTool,
snowflake_insert_rows: snowflakeInsertRowsTool,
snowflake_update_rows: snowflakeUpdateRowsTool,
snowflake_delete_rows: snowflakeDeleteRowsTool,
snowflake_list_databases: snowflakeListDatabasesTool,
snowflake_list_schemas: snowflakeListSchemasTool,
snowflake_list_tables: snowflakeListTablesTool,
snowflake_list_views: snowflakeListViewsTool,
snowflake_list_warehouses: snowflakeListWarehousesTool,
snowflake_list_file_formats: snowflakeListFileFormatsTool,
snowflake_list_stages: snowflakeListStagesTool,
snowflake_describe_table: snowflakeDescribeTableTool,
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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,
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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',
},
},
}

View 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

View 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',
},
},
}

View 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}"`
}