@naganpm/snowflake-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.continue/prompts/new-prompt.md +14 -0
- package/.env.example +7 -0
- package/Dockerfile +21 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +174 -0
- package/README.md +205 -0
- package/SETUP_COMPLETE.md +226 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +129 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/middleware/error.middleware.d.ts +3 -0
- package/dist/middleware/error.middleware.d.ts.map +1 -0
- package/dist/middleware/error.middleware.js +18 -0
- package/dist/middleware/error.middleware.js.map +1 -0
- package/dist/routes/snowflake.routes.d.ts +3 -0
- package/dist/routes/snowflake.routes.d.ts.map +1 -0
- package/dist/routes/snowflake.routes.js +120 -0
- package/dist/routes/snowflake.routes.js.map +1 -0
- package/dist/services/snowflake.service.d.ts +32 -0
- package/dist/services/snowflake.service.d.ts.map +1 -0
- package/dist/services/snowflake.service.js +126 -0
- package/dist/services/snowflake.service.js.map +1 -0
- package/docker-compose.yml +16 -0
- package/examples/usage.ts +84 -0
- package/package.json +45 -0
- package/postman_collection.json +148 -0
- package/src/index.ts +51 -0
- package/src/mcp-server.ts +137 -0
- package/src/middleware/error.middleware.ts +22 -0
- package/src/routes/snowflake.routes.ts +123 -0
- package/src/services/snowflake.service.ts +149 -0
- package/test-api.sh +39 -0
- package/tsconfig.json +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@naganpm/snowflake-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Snowflake database integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"snowflake-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "ts-node src/index.ts",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"watch": "tsc --watch",
|
|
15
|
+
"clean": "rm -rf dist",
|
|
16
|
+
"prepare": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"mcp-server",
|
|
21
|
+
"snowflake",
|
|
22
|
+
"database",
|
|
23
|
+
"express",
|
|
24
|
+
"typescript"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
30
|
+
"cors": "^2.8.5",
|
|
31
|
+
"dotenv": "^16.4.1",
|
|
32
|
+
"express": "^4.18.2",
|
|
33
|
+
"snowflake-sdk": "^1.11.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/cors": "^2.8.17",
|
|
37
|
+
"@types/express": "^4.17.21",
|
|
38
|
+
"@types/node": "^20.11.5",
|
|
39
|
+
"ts-node": "^10.9.2",
|
|
40
|
+
"typescript": "^5.3.3"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"info": {
|
|
3
|
+
"name": "Snowflake MCP Server",
|
|
4
|
+
"description": "API collection for Snowflake MCP Server",
|
|
5
|
+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
6
|
+
},
|
|
7
|
+
"item": [
|
|
8
|
+
{
|
|
9
|
+
"name": "Health Check",
|
|
10
|
+
"request": {
|
|
11
|
+
"method": "GET",
|
|
12
|
+
"header": [],
|
|
13
|
+
"url": {
|
|
14
|
+
"raw": "{{base_url}}/health",
|
|
15
|
+
"host": ["{{base_url}}"],
|
|
16
|
+
"path": ["health"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "Connection Status",
|
|
22
|
+
"request": {
|
|
23
|
+
"method": "GET",
|
|
24
|
+
"header": [],
|
|
25
|
+
"url": {
|
|
26
|
+
"raw": "{{base_url}}/api/snowflake/status",
|
|
27
|
+
"host": ["{{base_url}}"],
|
|
28
|
+
"path": ["api", "snowflake", "status"]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "Execute Query",
|
|
34
|
+
"request": {
|
|
35
|
+
"method": "POST",
|
|
36
|
+
"header": [
|
|
37
|
+
{
|
|
38
|
+
"key": "Content-Type",
|
|
39
|
+
"value": "application/json"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"body": {
|
|
43
|
+
"mode": "raw",
|
|
44
|
+
"raw": "{\n \"query\": \"SELECT CURRENT_DATE(), CURRENT_USER(), CURRENT_ROLE()\"\n}"
|
|
45
|
+
},
|
|
46
|
+
"url": {
|
|
47
|
+
"raw": "{{base_url}}/api/snowflake/query",
|
|
48
|
+
"host": ["{{base_url}}"],
|
|
49
|
+
"path": ["api", "snowflake", "query"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "List Databases",
|
|
55
|
+
"request": {
|
|
56
|
+
"method": "GET",
|
|
57
|
+
"header": [],
|
|
58
|
+
"url": {
|
|
59
|
+
"raw": "{{base_url}}/api/snowflake/databases",
|
|
60
|
+
"host": ["{{base_url}}"],
|
|
61
|
+
"path": ["api", "snowflake", "databases"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "List Schemas",
|
|
67
|
+
"request": {
|
|
68
|
+
"method": "GET",
|
|
69
|
+
"header": [],
|
|
70
|
+
"url": {
|
|
71
|
+
"raw": "{{base_url}}/api/snowflake/schemas?database=YOUR_DATABASE",
|
|
72
|
+
"host": ["{{base_url}}"],
|
|
73
|
+
"path": ["api", "snowflake", "schemas"],
|
|
74
|
+
"query": [
|
|
75
|
+
{
|
|
76
|
+
"key": "database",
|
|
77
|
+
"value": "YOUR_DATABASE"
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "List Tables",
|
|
85
|
+
"request": {
|
|
86
|
+
"method": "GET",
|
|
87
|
+
"header": [],
|
|
88
|
+
"url": {
|
|
89
|
+
"raw": "{{base_url}}/api/snowflake/tables?database=YOUR_DATABASE&schema=PUBLIC",
|
|
90
|
+
"host": ["{{base_url}}"],
|
|
91
|
+
"path": ["api", "snowflake", "tables"],
|
|
92
|
+
"query": [
|
|
93
|
+
{
|
|
94
|
+
"key": "database",
|
|
95
|
+
"value": "YOUR_DATABASE"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"key": "schema",
|
|
99
|
+
"value": "PUBLIC"
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"name": "Describe Table",
|
|
107
|
+
"request": {
|
|
108
|
+
"method": "GET",
|
|
109
|
+
"header": [],
|
|
110
|
+
"url": {
|
|
111
|
+
"raw": "{{base_url}}/api/snowflake/tables/YOUR_TABLE/describe",
|
|
112
|
+
"host": ["{{base_url}}"],
|
|
113
|
+
"path": ["api", "snowflake", "tables", "YOUR_TABLE", "describe"]
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "List Warehouses",
|
|
119
|
+
"request": {
|
|
120
|
+
"method": "GET",
|
|
121
|
+
"header": [],
|
|
122
|
+
"url": {
|
|
123
|
+
"raw": "{{base_url}}/api/snowflake/warehouses",
|
|
124
|
+
"host": ["{{base_url}}"],
|
|
125
|
+
"path": ["api", "snowflake", "warehouses"]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"name": "Get Current Session",
|
|
131
|
+
"request": {
|
|
132
|
+
"method": "GET",
|
|
133
|
+
"header": [],
|
|
134
|
+
"url": {
|
|
135
|
+
"raw": "{{base_url}}/api/snowflake/session",
|
|
136
|
+
"host": ["{{base_url}}"],
|
|
137
|
+
"path": ["api", "snowflake", "session"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
],
|
|
142
|
+
"variable": [
|
|
143
|
+
{
|
|
144
|
+
"key": "base_url",
|
|
145
|
+
"value": "http://localhost:3000"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import express, { Express, Request, Response } from 'express';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { SnowflakeService } from './services/snowflake.service';
|
|
6
|
+
import { errorHandler } from './middleware/error.middleware';
|
|
7
|
+
import { snowflakeRoutes } from './routes/snowflake.routes';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
const app: Express = express();
|
|
12
|
+
const port = process.env.PORT || 3000;
|
|
13
|
+
|
|
14
|
+
// Middleware
|
|
15
|
+
app.use(cors());
|
|
16
|
+
app.use(express.json());
|
|
17
|
+
app.use(express.urlencoded({ extended: true }));
|
|
18
|
+
|
|
19
|
+
// Health check
|
|
20
|
+
app.get('/health', (req: Request, res: Response) => {
|
|
21
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Snowflake routes
|
|
25
|
+
app.use('/api/snowflake', snowflakeRoutes);
|
|
26
|
+
|
|
27
|
+
// Error handler
|
|
28
|
+
app.use(errorHandler);
|
|
29
|
+
|
|
30
|
+
// Initialize Snowflake connection
|
|
31
|
+
const snowflakeService = SnowflakeService.getInstance();
|
|
32
|
+
|
|
33
|
+
app.listen(port, () => {
|
|
34
|
+
console.log(`🚀 Snowflake MCP Server running on port ${port}`);
|
|
35
|
+
console.log(`📊 Health check: http://localhost:${port}/health`);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Graceful shutdown
|
|
39
|
+
process.on('SIGINT', async () => {
|
|
40
|
+
console.log('\n🛑 Shutting down gracefully...');
|
|
41
|
+
await snowflakeService.disconnect();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
process.on('SIGTERM', async () => {
|
|
46
|
+
console.log('\n🛑 Shutting down gracefully...');
|
|
47
|
+
await snowflakeService.disconnect();
|
|
48
|
+
process.exit(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default app;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import { SnowflakeService } from './services/snowflake.service';
|
|
9
|
+
import dotenv from 'dotenv';
|
|
10
|
+
|
|
11
|
+
dotenv.config();
|
|
12
|
+
|
|
13
|
+
const snowflakeService = SnowflakeService.getInstance();
|
|
14
|
+
|
|
15
|
+
const server = new Server(
|
|
16
|
+
{
|
|
17
|
+
name: 'snowflake-mcp-server',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
28
|
+
return {
|
|
29
|
+
tools: [
|
|
30
|
+
{
|
|
31
|
+
name: 'execute_query',
|
|
32
|
+
description: 'Execute a SQL query on Snowflake database',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
query: { type: 'string', description: 'SQL query to execute' },
|
|
37
|
+
binds: { type: 'array', description: 'Optional bind parameters' },
|
|
38
|
+
},
|
|
39
|
+
required: ['query'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'list_databases',
|
|
44
|
+
description: 'List all databases',
|
|
45
|
+
inputSchema: { type: 'object', properties: {} },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'list_schemas',
|
|
49
|
+
description: 'List schemas',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: { database: { type: 'string' } },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'list_tables',
|
|
57
|
+
description: 'List tables',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: { database: { type: 'string' }, schema: { type: 'string' } },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'describe_table',
|
|
65
|
+
description: 'Describe table structure',
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: { tableName: { type: 'string' } },
|
|
69
|
+
required: ['tableName'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'list_warehouses',
|
|
74
|
+
description: 'List warehouses',
|
|
75
|
+
inputSchema: { type: 'object', properties: {} },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'get_session_info',
|
|
79
|
+
description: 'Get session info',
|
|
80
|
+
inputSchema: { type: 'object', properties: {} },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
87
|
+
try {
|
|
88
|
+
const { name, arguments: args } = request.params;
|
|
89
|
+
let result;
|
|
90
|
+
|
|
91
|
+
switch (name) {
|
|
92
|
+
case 'execute_query':
|
|
93
|
+
result = await snowflakeService.executeQuery((args?.query as string) || '', args?.binds as any);
|
|
94
|
+
break;
|
|
95
|
+
case 'list_databases':
|
|
96
|
+
result = await snowflakeService.listDatabases();
|
|
97
|
+
break;
|
|
98
|
+
case 'list_schemas':
|
|
99
|
+
result = await snowflakeService.listSchemas(args?.database as string);
|
|
100
|
+
break;
|
|
101
|
+
case 'list_tables':
|
|
102
|
+
result = await snowflakeService.listTables(args?.database as string, args?.schema as string);
|
|
103
|
+
break;
|
|
104
|
+
case 'describe_table':
|
|
105
|
+
result = await snowflakeService.describeTable((args?.tableName as string) || '');
|
|
106
|
+
break;
|
|
107
|
+
case 'list_warehouses':
|
|
108
|
+
result = await snowflakeService.listWarehouses();
|
|
109
|
+
break;
|
|
110
|
+
case 'get_session_info':
|
|
111
|
+
result = await snowflakeService.getCurrentSession();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
119
|
+
};
|
|
120
|
+
} catch (error: any) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
async function main() {
|
|
129
|
+
const transport = new StdioServerTransport();
|
|
130
|
+
await server.connect(transport);
|
|
131
|
+
console.error('Snowflake MCP server running on stdio');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
main().catch((error) => {
|
|
135
|
+
console.error('Failed to start MCP server:', error);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export const errorHandler = (
|
|
4
|
+
err: any,
|
|
5
|
+
req: Request,
|
|
6
|
+
res: Response,
|
|
7
|
+
next: NextFunction
|
|
8
|
+
) => {
|
|
9
|
+
console.error('❌ Error:', err);
|
|
10
|
+
|
|
11
|
+
const statusCode = err.statusCode || 500;
|
|
12
|
+
const message = err.message || 'Internal Server Error';
|
|
13
|
+
|
|
14
|
+
res.status(statusCode).json({
|
|
15
|
+
success: false,
|
|
16
|
+
error: {
|
|
17
|
+
message,
|
|
18
|
+
code: err.code || 'INTERNAL_ERROR',
|
|
19
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { SnowflakeService } from '../services/snowflake.service';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
const snowflakeService = SnowflakeService.getInstance();
|
|
6
|
+
|
|
7
|
+
// Execute query
|
|
8
|
+
router.post('/query', async (req: Request, res: Response, next: NextFunction) => {
|
|
9
|
+
try {
|
|
10
|
+
const { query, binds } = req.body;
|
|
11
|
+
|
|
12
|
+
if (!query) {
|
|
13
|
+
return res.status(400).json({ error: 'Query is required' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const result = await snowflakeService.executeQuery(query, binds);
|
|
17
|
+
res.json({
|
|
18
|
+
success: true,
|
|
19
|
+
data: result,
|
|
20
|
+
});
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
next(error);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// List databases
|
|
27
|
+
router.get('/databases', async (req: Request, res: Response, next: NextFunction) => {
|
|
28
|
+
try {
|
|
29
|
+
const result = await snowflakeService.listDatabases();
|
|
30
|
+
res.json({
|
|
31
|
+
success: true,
|
|
32
|
+
data: result,
|
|
33
|
+
});
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
next(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// List schemas
|
|
40
|
+
router.get('/schemas', async (req: Request, res: Response, next: NextFunction) => {
|
|
41
|
+
try {
|
|
42
|
+
const { database } = req.query;
|
|
43
|
+
const result = await snowflakeService.listSchemas(database as string);
|
|
44
|
+
res.json({
|
|
45
|
+
success: true,
|
|
46
|
+
data: result,
|
|
47
|
+
});
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
next(error);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// List tables
|
|
54
|
+
router.get('/tables', async (req: Request, res: Response, next: NextFunction) => {
|
|
55
|
+
try {
|
|
56
|
+
const { database, schema } = req.query;
|
|
57
|
+
const result = await snowflakeService.listTables(
|
|
58
|
+
database as string,
|
|
59
|
+
schema as string
|
|
60
|
+
);
|
|
61
|
+
res.json({
|
|
62
|
+
success: true,
|
|
63
|
+
data: result,
|
|
64
|
+
});
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
next(error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Describe table
|
|
71
|
+
router.get('/tables/:tableName/describe', async (req: Request, res: Response, next: NextFunction) => {
|
|
72
|
+
try {
|
|
73
|
+
const { tableName } = req.params;
|
|
74
|
+
const result = await snowflakeService.describeTable(tableName);
|
|
75
|
+
res.json({
|
|
76
|
+
success: true,
|
|
77
|
+
data: result,
|
|
78
|
+
});
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
next(error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// List warehouses
|
|
85
|
+
router.get('/warehouses', async (req: Request, res: Response, next: NextFunction) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await snowflakeService.listWarehouses();
|
|
88
|
+
res.json({
|
|
89
|
+
success: true,
|
|
90
|
+
data: result,
|
|
91
|
+
});
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
next(error);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Get current session
|
|
98
|
+
router.get('/session', async (req: Request, res: Response, next: NextFunction) => {
|
|
99
|
+
try {
|
|
100
|
+
const result = await snowflakeService.getCurrentSession();
|
|
101
|
+
res.json({
|
|
102
|
+
success: true,
|
|
103
|
+
data: result,
|
|
104
|
+
});
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
next(error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Connection status
|
|
111
|
+
router.get('/status', async (req: Request, res: Response, next: NextFunction) => {
|
|
112
|
+
try {
|
|
113
|
+
const isConnected = snowflakeService.isConnected();
|
|
114
|
+
res.json({
|
|
115
|
+
success: true,
|
|
116
|
+
connected: isConnected,
|
|
117
|
+
});
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
next(error);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export { router as snowflakeRoutes };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import snowflake from 'snowflake-sdk';
|
|
2
|
+
|
|
3
|
+
export interface SnowflakeConfig {
|
|
4
|
+
account: string;
|
|
5
|
+
username: string;
|
|
6
|
+
password: string;
|
|
7
|
+
warehouse?: string;
|
|
8
|
+
database?: string;
|
|
9
|
+
schema?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QueryResult {
|
|
13
|
+
columns: string[];
|
|
14
|
+
rows: any[];
|
|
15
|
+
rowCount: number;
|
|
16
|
+
executionTime: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SnowflakeService {
|
|
20
|
+
private static instance: SnowflakeService;
|
|
21
|
+
private connection: snowflake.Connection | null = null;
|
|
22
|
+
private config: SnowflakeConfig;
|
|
23
|
+
|
|
24
|
+
private constructor() {
|
|
25
|
+
this.config = {
|
|
26
|
+
account: process.env.SNOWFLAKE_ACCOUNT || '',
|
|
27
|
+
username: process.env.SNOWFLAKE_USERNAME || '',
|
|
28
|
+
password: process.env.SNOWFLAKE_PASSWORD || '',
|
|
29
|
+
warehouse: process.env.SNOWFLAKE_WAREHOUSE,
|
|
30
|
+
database: process.env.SNOWFLAKE_DATABASE,
|
|
31
|
+
schema: process.env.SNOWFLAKE_SCHEMA,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static getInstance(): SnowflakeService {
|
|
36
|
+
if (!SnowflakeService.instance) {
|
|
37
|
+
SnowflakeService.instance = new SnowflakeService();
|
|
38
|
+
}
|
|
39
|
+
return SnowflakeService.instance;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private async connect(): Promise<snowflake.Connection> {
|
|
43
|
+
if (this.connection) {
|
|
44
|
+
return this.connection;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const connectionOptions: any = {
|
|
49
|
+
account: this.config.account,
|
|
50
|
+
username: this.config.username,
|
|
51
|
+
password: this.config.password,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (this.config.warehouse) connectionOptions.warehouse = this.config.warehouse;
|
|
55
|
+
if (this.config.database) connectionOptions.database = this.config.database;
|
|
56
|
+
if (this.config.schema) connectionOptions.schema = this.config.schema;
|
|
57
|
+
|
|
58
|
+
this.connection = snowflake.createConnection(connectionOptions);
|
|
59
|
+
|
|
60
|
+
this.connection.connect((err, conn) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
console.error('❌ Failed to connect to Snowflake:', err.message);
|
|
63
|
+
reject(err);
|
|
64
|
+
} else {
|
|
65
|
+
console.log('✅ Successfully connected to Snowflake');
|
|
66
|
+
resolve(conn);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public async executeQuery(sqlText: string, binds?: any[]): Promise<QueryResult> {
|
|
73
|
+
const startTime = Date.now();
|
|
74
|
+
const conn = await this.connect();
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
conn.execute({
|
|
78
|
+
sqlText,
|
|
79
|
+
binds,
|
|
80
|
+
complete: (err, stmt, rows) => {
|
|
81
|
+
const executionTime = Date.now() - startTime;
|
|
82
|
+
|
|
83
|
+
if (err) {
|
|
84
|
+
reject(err);
|
|
85
|
+
} else {
|
|
86
|
+
const columns = stmt.getColumns().map(col => col.getName());
|
|
87
|
+
resolve({
|
|
88
|
+
columns,
|
|
89
|
+
rows: rows || [],
|
|
90
|
+
rowCount: rows?.length || 0,
|
|
91
|
+
executionTime,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public async listDatabases(): Promise<QueryResult> {
|
|
100
|
+
return this.executeQuery('SHOW DATABASES');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public async listSchemas(database?: string): Promise<QueryResult> {
|
|
104
|
+
if (database) {
|
|
105
|
+
return this.executeQuery(`SHOW SCHEMAS IN DATABASE ${database}`);
|
|
106
|
+
}
|
|
107
|
+
return this.executeQuery('SHOW SCHEMAS');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async listTables(database?: string, schema?: string): Promise<QueryResult> {
|
|
111
|
+
if (database && schema) {
|
|
112
|
+
return this.executeQuery(`SHOW TABLES IN ${database}.${schema}`);
|
|
113
|
+
}
|
|
114
|
+
return this.executeQuery('SHOW TABLES');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public async describeTable(tableName: string): Promise<QueryResult> {
|
|
118
|
+
return this.executeQuery(`DESCRIBE TABLE ${tableName}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public async listWarehouses(): Promise<QueryResult> {
|
|
122
|
+
return this.executeQuery('SHOW WAREHOUSES');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public async getCurrentSession(): Promise<QueryResult> {
|
|
126
|
+
return this.executeQuery('SELECT CURRENT_USER(), CURRENT_ROLE(), CURRENT_DATABASE(), CURRENT_SCHEMA(), CURRENT_WAREHOUSE()');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public async disconnect(): Promise<void> {
|
|
130
|
+
if (this.connection) {
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
this.connection!.destroy((err) => {
|
|
133
|
+
if (err) {
|
|
134
|
+
console.error('❌ Error disconnecting from Snowflake:', err.message);
|
|
135
|
+
reject(err);
|
|
136
|
+
} else {
|
|
137
|
+
console.log('✅ Disconnected from Snowflake');
|
|
138
|
+
this.connection = null;
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public isConnected(): boolean {
|
|
147
|
+
return this.connection !== null;
|
|
148
|
+
}
|
|
149
|
+
}
|