crushdataai 1.2.14 → 1.2.15
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/dist/connectors/cloud/index.d.ts +3 -1
- package/dist/connectors/cloud/index.js +34 -6
- package/dist/connectors/index.d.ts +1 -0
- package/dist/connectors/mysql/index.d.ts +1 -0
- package/dist/connectors/mysql/index.js +23 -0
- package/dist/connectors/postgresql/index.d.ts +1 -0
- package/dist/connectors/postgresql/index.js +16 -0
- package/dist/routes/dashboard.js +67 -14
- package/dist/services/query-executor.d.ts +5 -0
- package/dist/services/query-executor.js +80 -0
- package/package.json +1 -1
- package/ui-dashboard-dist/assets/index-SkyAs8Zl.js +4185 -0
- package/ui-dashboard-dist/index.html +1 -1
- package/ui-dashboard-dist/assets/index-BhtUalwh.js +0 -112
|
@@ -8,11 +8,13 @@ export declare class BigQueryConnector implements Connector {
|
|
|
8
8
|
getData(connection: Connection, tableName: string, page: number, limit: number): Promise<TableData>;
|
|
9
9
|
getSchema(connection: Connection, tableName: string): Promise<ColumnInfo[]>;
|
|
10
10
|
getSnippet(connection: Connection, lang: string): string;
|
|
11
|
+
executeQuery(connection: Connection, query: string): Promise<any[]>;
|
|
11
12
|
}
|
|
12
13
|
export declare class SnowflakeConnector implements Connector {
|
|
13
14
|
type: string;
|
|
14
15
|
private createConnection;
|
|
15
|
-
private
|
|
16
|
+
private executeInternal;
|
|
17
|
+
executeQuery(connection: Connection, query: string): Promise<any[]>;
|
|
16
18
|
test(connection: Connection): Promise<boolean>;
|
|
17
19
|
getTables(connection: Connection): Promise<Table[]>;
|
|
18
20
|
getData(connection: Connection, tableName: string, page: number, limit: number): Promise<TableData>;
|
|
@@ -168,6 +168,18 @@ print(df.head())
|
|
|
168
168
|
}
|
|
169
169
|
return `# Language ${lang} not supported for BigQuery connector yet.`;
|
|
170
170
|
}
|
|
171
|
+
async executeQuery(connection, query) {
|
|
172
|
+
console.log(`[BigQuery] executeQuery called for ${connection.name}`);
|
|
173
|
+
const bigquery = this.createClient(connection);
|
|
174
|
+
try {
|
|
175
|
+
const [rows] = await bigquery.query(query);
|
|
176
|
+
return rows;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error(`[BigQuery] executeQuery failed:`, error.message);
|
|
180
|
+
throw new Error(`Failed to execute query: ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
171
183
|
}
|
|
172
184
|
exports.BigQueryConnector = BigQueryConnector;
|
|
173
185
|
class SnowflakeConnector {
|
|
@@ -193,7 +205,7 @@ class SnowflakeConnector {
|
|
|
193
205
|
});
|
|
194
206
|
});
|
|
195
207
|
}
|
|
196
|
-
|
|
208
|
+
executeInternal(conn, query) {
|
|
197
209
|
return new Promise((resolve, reject) => {
|
|
198
210
|
conn.execute({
|
|
199
211
|
sqlText: query,
|
|
@@ -208,6 +220,22 @@ class SnowflakeConnector {
|
|
|
208
220
|
});
|
|
209
221
|
});
|
|
210
222
|
}
|
|
223
|
+
async executeQuery(connection, query) {
|
|
224
|
+
console.log(`[Snowflake] executeQuery called for ${connection.name}`);
|
|
225
|
+
let conn = null;
|
|
226
|
+
try {
|
|
227
|
+
conn = await this.createConnection(connection);
|
|
228
|
+
return await this.executeInternal(conn, query);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error(`[Snowflake] executeQuery failed:`, error.message);
|
|
232
|
+
throw new Error(`Failed to execute query: ${error.message}`);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
if (conn)
|
|
236
|
+
conn.destroy(() => { });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
211
239
|
async test(connection) {
|
|
212
240
|
console.log(`[Snowflake] Testing connection for ${connection.name} (Account: ${connection.account})`);
|
|
213
241
|
// Validate required fields
|
|
@@ -226,7 +254,7 @@ class SnowflakeConnector {
|
|
|
226
254
|
let conn = null;
|
|
227
255
|
try {
|
|
228
256
|
conn = await this.createConnection(connection);
|
|
229
|
-
await this.
|
|
257
|
+
await this.executeInternal(conn, 'SELECT CURRENT_VERSION()');
|
|
230
258
|
console.log(`[Snowflake] Connection test successful for ${connection.name}`);
|
|
231
259
|
return true;
|
|
232
260
|
}
|
|
@@ -244,7 +272,7 @@ class SnowflakeConnector {
|
|
|
244
272
|
let conn = null;
|
|
245
273
|
try {
|
|
246
274
|
conn = await this.createConnection(connection);
|
|
247
|
-
const rows = await this.
|
|
275
|
+
const rows = await this.executeInternal(conn, 'SHOW TABLES');
|
|
248
276
|
return rows.map((row) => ({
|
|
249
277
|
name: row.name || row.TABLE_NAME,
|
|
250
278
|
type: 'table',
|
|
@@ -266,9 +294,9 @@ class SnowflakeConnector {
|
|
|
266
294
|
try {
|
|
267
295
|
conn = await this.createConnection(connection);
|
|
268
296
|
const offset = (page - 1) * limit;
|
|
269
|
-
const countRows = await this.
|
|
297
|
+
const countRows = await this.executeInternal(conn, `SELECT COUNT(*) as TOTAL FROM "${tableName}"`);
|
|
270
298
|
const totalRows = countRows[0]?.TOTAL || 0;
|
|
271
|
-
const rows = await this.
|
|
299
|
+
const rows = await this.executeInternal(conn, `SELECT * FROM "${tableName}" LIMIT ${limit} OFFSET ${offset}`);
|
|
272
300
|
const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
|
|
273
301
|
const totalPages = Math.ceil(totalRows / limit) || 1;
|
|
274
302
|
return {
|
|
@@ -298,7 +326,7 @@ class SnowflakeConnector {
|
|
|
298
326
|
let conn = null;
|
|
299
327
|
try {
|
|
300
328
|
conn = await this.createConnection(connection);
|
|
301
|
-
const rows = await this.
|
|
329
|
+
const rows = await this.executeInternal(conn, `DESCRIBE TABLE "${tableName}"`);
|
|
302
330
|
return rows.map((row) => ({
|
|
303
331
|
name: row.name || row.COLUMN_NAME,
|
|
304
332
|
type: row.type || row.DATA_TYPE,
|
|
@@ -28,6 +28,7 @@ export interface Connector {
|
|
|
28
28
|
getData(connection: Connection, tableName: string, page: number, limit: number): Promise<TableData>;
|
|
29
29
|
getSchema(connection: Connection, tableName: string): Promise<ColumnInfo[]>;
|
|
30
30
|
getSnippet(connection: Connection, lang: string): string;
|
|
31
|
+
executeQuery?(connection: Connection, query: string): Promise<any[]>;
|
|
31
32
|
}
|
|
32
33
|
export declare class ConnectorRegistry {
|
|
33
34
|
private static connectors;
|
|
@@ -7,4 +7,5 @@ export declare class MySQLConnector implements Connector {
|
|
|
7
7
|
getData(connection: Connection, tableName: string, page: number, limit: number): Promise<TableData>;
|
|
8
8
|
getSchema(connection: Connection, tableName: string): Promise<import('../index').ColumnInfo[]>;
|
|
9
9
|
getSnippet(connection: Connection, lang: string): string;
|
|
10
|
+
executeQuery(connection: Connection, query: string): Promise<any[]>;
|
|
10
11
|
}
|
|
@@ -178,5 +178,28 @@ finally:
|
|
|
178
178
|
}
|
|
179
179
|
return `# Language ${lang} not supported for MySQL connector yet.`;
|
|
180
180
|
}
|
|
181
|
+
async executeQuery(connection, query) {
|
|
182
|
+
console.log(`[MySQL] executeQuery called for ${connection.name}`);
|
|
183
|
+
let conn = null;
|
|
184
|
+
try {
|
|
185
|
+
conn = await promise_1.default.createConnection({
|
|
186
|
+
host: connection.host,
|
|
187
|
+
port: connection.port || 3306,
|
|
188
|
+
user: connection.user,
|
|
189
|
+
password: connection.password || '',
|
|
190
|
+
database: connection.database
|
|
191
|
+
});
|
|
192
|
+
const [rows] = await conn.execute(query);
|
|
193
|
+
return rows;
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
console.error(`[MySQL] executeQuery failed:`, error.message);
|
|
197
|
+
throw new Error(`Failed to execute query: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
if (conn)
|
|
201
|
+
await conn.end();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
181
204
|
}
|
|
182
205
|
exports.MySQLConnector = MySQLConnector;
|
|
@@ -8,4 +8,5 @@ export declare class PostgreSQLConnector implements Connector {
|
|
|
8
8
|
getData(connection: Connection, tableName: string, page: number, limit: number): Promise<TableData>;
|
|
9
9
|
getSchema(connection: Connection, tableName: string): Promise<import('../index').ColumnInfo[]>;
|
|
10
10
|
getSnippet(connection: Connection, lang: string): string;
|
|
11
|
+
executeQuery(connection: Connection, query: string): Promise<any[]>;
|
|
11
12
|
}
|
|
@@ -156,5 +156,21 @@ finally:
|
|
|
156
156
|
}
|
|
157
157
|
return `# Language ${lang} not supported for PostgreSQL connector yet.`;
|
|
158
158
|
}
|
|
159
|
+
async executeQuery(connection, query) {
|
|
160
|
+
console.log(`[PostgreSQL] executeQuery called for ${connection.name}`);
|
|
161
|
+
const client = this.createClient(connection);
|
|
162
|
+
try {
|
|
163
|
+
await client.connect();
|
|
164
|
+
const result = await client.query(query);
|
|
165
|
+
return result.rows;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error(`[PostgreSQL] executeQuery failed:`, error.message);
|
|
169
|
+
throw new Error(`Failed to execute query: ${error.message}`);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
await client.end();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
159
175
|
}
|
|
160
176
|
exports.PostgreSQLConnector = PostgreSQLConnector;
|
package/dist/routes/dashboard.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
const express_1 = require("express");
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const query_executor_1 = require("../services/query-executor");
|
|
39
40
|
const router = (0, express_1.Router)();
|
|
40
41
|
// Get reports/dashboards directory path relative to current working directory
|
|
41
42
|
function getDashboardsDir() {
|
|
@@ -82,7 +83,7 @@ router.get('/dashboards/:id', (req, res) => {
|
|
|
82
83
|
return res.status(404).json({ error: 'Dashboard not found' });
|
|
83
84
|
}
|
|
84
85
|
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
85
|
-
res.json(content);
|
|
86
|
+
res.json({ id, ...content });
|
|
86
87
|
}
|
|
87
88
|
catch (error) {
|
|
88
89
|
console.error('Error reading dashboard:', error);
|
|
@@ -90,24 +91,76 @@ router.get('/dashboards/:id', (req, res) => {
|
|
|
90
91
|
}
|
|
91
92
|
});
|
|
92
93
|
// Refresh a chart's data (placeholder for now - would re-run query)
|
|
93
|
-
router.post('/charts/:id/refresh', (req, res) => {
|
|
94
|
+
router.post('/charts/:id/refresh', async (req, res) => {
|
|
94
95
|
try {
|
|
95
96
|
const { id } = req.params;
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
const dashboardsDir = getDashboardsDir();
|
|
98
|
+
// 1. Find the chart in any dashboard
|
|
99
|
+
// We have to search all dashboards because we don't know which one calls it
|
|
100
|
+
// In a real DB we'd have a chart table, but here we scan JSONs
|
|
101
|
+
const files = fs.readdirSync(dashboardsDir).filter(file => file.endsWith('.json'));
|
|
102
|
+
let targetDashboard = null;
|
|
103
|
+
let targetDashboardFile = '';
|
|
104
|
+
let targetChart = null;
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
const filePath = path.join(dashboardsDir, file);
|
|
107
|
+
const dashboard = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
108
|
+
const chart = dashboard.charts.find(c => c.id === id);
|
|
109
|
+
if (chart) {
|
|
110
|
+
targetDashboard = dashboard;
|
|
111
|
+
targetDashboardFile = filePath;
|
|
112
|
+
targetChart = chart;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!targetDashboard || !targetChart || !targetDashboardFile) {
|
|
117
|
+
return res.status(404).json({ error: 'Chart not found' });
|
|
118
|
+
}
|
|
119
|
+
// 2. Execute Query
|
|
120
|
+
if (!targetChart.query || !targetChart.query.connection) {
|
|
121
|
+
return res.status(400).json({ error: 'Chart has no query configuration' });
|
|
122
|
+
}
|
|
123
|
+
console.log(`Refreshing chart ${id} using connection ${targetChart.query.connection}...`);
|
|
124
|
+
const newData = await query_executor_1.QueryExecutor.execute(targetChart.query);
|
|
125
|
+
// 3. Update Dashboard
|
|
126
|
+
targetChart.data = newData;
|
|
127
|
+
targetChart.lastRefreshed = new Date().toISOString();
|
|
128
|
+
// Save back to disk
|
|
129
|
+
fs.writeFileSync(targetDashboardFile, JSON.stringify(targetDashboard, null, 2));
|
|
130
|
+
// 4. Return new data
|
|
131
|
+
res.json(targetChart);
|
|
107
132
|
}
|
|
108
133
|
catch (error) {
|
|
109
134
|
console.error('Error refreshing chart:', error);
|
|
110
|
-
res.status(500).json({ error: 'Failed to refresh chart' });
|
|
135
|
+
res.status(500).json({ error: error instanceof Error ? error.message : 'Failed to refresh chart' });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// SSE Endpoint for file watching
|
|
139
|
+
router.get('/events', (req, res) => {
|
|
140
|
+
// Set headers for SSE
|
|
141
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
142
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
143
|
+
res.setHeader('Connection', 'keep-alive');
|
|
144
|
+
res.flushHeaders();
|
|
145
|
+
const dashboardsDir = getDashboardsDir();
|
|
146
|
+
if (!fs.existsSync(dashboardsDir)) {
|
|
147
|
+
return res.end();
|
|
111
148
|
}
|
|
149
|
+
console.log('Client connected to SSE stream');
|
|
150
|
+
// Watch for file changes
|
|
151
|
+
const watcher = fs.watch(dashboardsDir, (eventType, filename) => {
|
|
152
|
+
if (filename && filename.endsWith('.json')) {
|
|
153
|
+
console.log(`File changed: ${filename} (${eventType})`);
|
|
154
|
+
const dashboardId = path.basename(filename, '.json');
|
|
155
|
+
// Send event
|
|
156
|
+
res.write(`data: ${JSON.stringify({ type: 'dashboard-update', id: dashboardId })}\n\n`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// Cleanup on close
|
|
160
|
+
req.on('close', () => {
|
|
161
|
+
watcher.close();
|
|
162
|
+
console.log('Client disconnected from SSE stream');
|
|
163
|
+
res.end();
|
|
164
|
+
});
|
|
112
165
|
});
|
|
113
166
|
exports.default = router;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryExecutor = void 0;
|
|
4
|
+
const connections_1 = require("../connections");
|
|
5
|
+
const postgresql_1 = require("../connectors/postgresql");
|
|
6
|
+
const mysql_1 = require("../connectors/mysql");
|
|
7
|
+
// import { BigQueryConnector } from '../connectors/bigquery';
|
|
8
|
+
// import { SnowflakeConnector } from '../connectors/snowflake';
|
|
9
|
+
const shopify_1 = require("../connectors/shopify");
|
|
10
|
+
class QueryExecutor {
|
|
11
|
+
static async execute(query) {
|
|
12
|
+
if (!query.connection) {
|
|
13
|
+
throw new Error('No connection specified in query');
|
|
14
|
+
}
|
|
15
|
+
const connectionConfig = (0, connections_1.getConnection)(query.connection);
|
|
16
|
+
if (!connectionConfig) {
|
|
17
|
+
throw new Error(`Connection "${query.connection}" not found`);
|
|
18
|
+
}
|
|
19
|
+
let result;
|
|
20
|
+
// Execute query based on connection type
|
|
21
|
+
switch (connectionConfig.type) {
|
|
22
|
+
case 'postgresql': {
|
|
23
|
+
const connector = new postgresql_1.PostgreSQLConnector();
|
|
24
|
+
if (!query.sql)
|
|
25
|
+
throw new Error('SQL query required for Postgres');
|
|
26
|
+
result = await connector.executeQuery(connectionConfig, query.sql);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'mysql': {
|
|
30
|
+
const connector = new mysql_1.MySQLConnector();
|
|
31
|
+
if (!query.sql)
|
|
32
|
+
throw new Error('SQL query required for MySQL');
|
|
33
|
+
result = await connector.executeQuery(connectionConfig, query.sql);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
// case 'bigquery': {
|
|
37
|
+
// const connector = new BigQueryConnector();
|
|
38
|
+
// if (!query.sql) throw new Error('SQL query required for BigQuery');
|
|
39
|
+
// result = await connector.executeQuery(connectionConfig, query.sql);
|
|
40
|
+
// break;
|
|
41
|
+
// }
|
|
42
|
+
// case 'snowflake': {
|
|
43
|
+
// const connector = new SnowflakeConnector();
|
|
44
|
+
// if (!query.sql) throw new Error('SQL query required for Snowflake');
|
|
45
|
+
// result = await connector.executeQuery(connectionConfig, query.sql);
|
|
46
|
+
// break;
|
|
47
|
+
// }
|
|
48
|
+
case 'shopify': {
|
|
49
|
+
const connector = new shopify_1.ShopifyConnector();
|
|
50
|
+
// For Shopify, query.sql is treated as the table name/endpoint
|
|
51
|
+
const tableName = query.sql?.trim() || 'orders';
|
|
52
|
+
// Fetch first page, limit 1000
|
|
53
|
+
const tableData = await connector.getData(connectionConfig, tableName, 1, 1000);
|
|
54
|
+
result = tableData.rows;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unsupported connection type: ${connectionConfig.type}`);
|
|
59
|
+
}
|
|
60
|
+
// Transform result to ChartData format
|
|
61
|
+
return this.transformToChartData(result, query);
|
|
62
|
+
}
|
|
63
|
+
static transformToChartData(data, query) {
|
|
64
|
+
if (!data || data.length === 0) {
|
|
65
|
+
return { labels: [], datasets: [] };
|
|
66
|
+
}
|
|
67
|
+
// Auto-detect labels (first string/date column)
|
|
68
|
+
const keys = Object.keys(data[0]);
|
|
69
|
+
const labelKey = keys.find(k => typeof data[0][k] === 'string' || data[0][k] instanceof Date) || keys[0];
|
|
70
|
+
const labels = data.map(row => String(row[labelKey]));
|
|
71
|
+
// Create datasets for all numeric columns
|
|
72
|
+
const valueKeys = keys.filter(k => k !== labelKey && typeof data[0][k] === 'number');
|
|
73
|
+
const datasets = valueKeys.map((key, i) => ({
|
|
74
|
+
label: key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' '),
|
|
75
|
+
values: data.map(row => Number(row[key]))
|
|
76
|
+
}));
|
|
77
|
+
return { labels, datasets };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.QueryExecutor = QueryExecutor;
|