@squadbase/vite-server 0.0.1-build-7 → 0.0.1-build-9
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/cli/index.js +211 -21
- package/dist/index.d.ts +126 -1
- package/dist/index.js +482 -19
- package/dist/main.js +212 -18
- package/dist/types/data-source.d.ts +5 -5
- package/dist/vite-plugin.js +215 -16
- package/package.json +8 -2
package/dist/main.js
CHANGED
|
@@ -4,12 +4,13 @@ import { cors } from "hono/cors";
|
|
|
4
4
|
import path4 from "path";
|
|
5
5
|
|
|
6
6
|
// src/registry.ts
|
|
7
|
-
import { readdir, readFile, mkdir } from "fs/promises";
|
|
7
|
+
import { readdir, readFile as readFile2, mkdir } from "fs/promises";
|
|
8
8
|
import { watch as fsWatch2 } from "fs";
|
|
9
9
|
import path2 from "path";
|
|
10
10
|
|
|
11
11
|
// src/connector-client/registry.ts
|
|
12
12
|
import { readFileSync, watch as fsWatch } from "fs";
|
|
13
|
+
import { readFile } from "fs/promises";
|
|
13
14
|
import path from "path";
|
|
14
15
|
|
|
15
16
|
// src/connector-client/postgresql.ts
|
|
@@ -37,6 +38,11 @@ function resolveEnvVar(entry, key, connectionId) {
|
|
|
37
38
|
}
|
|
38
39
|
return value;
|
|
39
40
|
}
|
|
41
|
+
function resolveEnvVarOptional(entry, key) {
|
|
42
|
+
const envVarName = entry.envVars[key];
|
|
43
|
+
if (!envVarName) return void 0;
|
|
44
|
+
return process.env[envVarName] || void 0;
|
|
45
|
+
}
|
|
40
46
|
|
|
41
47
|
// src/connector-client/bigquery.ts
|
|
42
48
|
function createBigQueryClient(entry, connectionId) {
|
|
@@ -48,7 +54,7 @@ function createBigQueryClient(entry, connectionId) {
|
|
|
48
54
|
gcpCredentials = JSON.parse(serviceAccountJson);
|
|
49
55
|
} catch {
|
|
50
56
|
throw new Error(
|
|
51
|
-
`BigQuery service account JSON (decoded from base64) is not valid JSON for
|
|
57
|
+
`BigQuery service account JSON (decoded from base64) is not valid JSON for connectionId "${connectionId}"`
|
|
52
58
|
);
|
|
53
59
|
}
|
|
54
60
|
return {
|
|
@@ -105,26 +111,193 @@ function createSnowflakeClient(entry, connectionId) {
|
|
|
105
111
|
};
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
// src/connector-client/mysql.ts
|
|
115
|
+
function createMySQLClient(entry, connectionId) {
|
|
116
|
+
const connectionUrl = resolveEnvVar(entry, "connection-url", connectionId);
|
|
117
|
+
let poolPromise = null;
|
|
118
|
+
function getPool() {
|
|
119
|
+
if (!poolPromise) {
|
|
120
|
+
poolPromise = import("mysql2/promise").then(
|
|
121
|
+
(mysql) => mysql.default.createPool(connectionUrl)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return poolPromise;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
async query(sql, params) {
|
|
128
|
+
const pool = await getPool();
|
|
129
|
+
const [rows] = await pool.execute(sql, params);
|
|
130
|
+
return { rows };
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/connector-client/aws-athena.ts
|
|
136
|
+
function createAthenaClient(entry, connectionId) {
|
|
137
|
+
const region = resolveEnvVar(entry, "aws-region", connectionId);
|
|
138
|
+
const accessKeyId = resolveEnvVar(entry, "aws-access-key-id", connectionId);
|
|
139
|
+
const secretAccessKey = resolveEnvVar(entry, "aws-secret-access-key", connectionId);
|
|
140
|
+
const workgroup = resolveEnvVarOptional(entry, "workgroup") ?? "primary";
|
|
141
|
+
const outputLocation = resolveEnvVarOptional(entry, "output-location");
|
|
142
|
+
return {
|
|
143
|
+
async query(sql) {
|
|
144
|
+
const {
|
|
145
|
+
AthenaClient,
|
|
146
|
+
StartQueryExecutionCommand,
|
|
147
|
+
GetQueryExecutionCommand,
|
|
148
|
+
GetQueryResultsCommand
|
|
149
|
+
} = await import("@aws-sdk/client-athena");
|
|
150
|
+
const client = new AthenaClient({
|
|
151
|
+
region,
|
|
152
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
153
|
+
});
|
|
154
|
+
const startParams = {
|
|
155
|
+
QueryString: sql,
|
|
156
|
+
WorkGroup: workgroup
|
|
157
|
+
};
|
|
158
|
+
if (outputLocation) {
|
|
159
|
+
startParams.ResultConfiguration = { OutputLocation: outputLocation };
|
|
160
|
+
}
|
|
161
|
+
const { QueryExecutionId } = await client.send(
|
|
162
|
+
new StartQueryExecutionCommand(startParams)
|
|
163
|
+
);
|
|
164
|
+
if (!QueryExecutionId) throw new Error("Athena: failed to start query execution");
|
|
165
|
+
while (true) {
|
|
166
|
+
const { QueryExecution } = await client.send(
|
|
167
|
+
new GetQueryExecutionCommand({ QueryExecutionId })
|
|
168
|
+
);
|
|
169
|
+
const state = QueryExecution?.Status?.State;
|
|
170
|
+
if (state === "SUCCEEDED") break;
|
|
171
|
+
if (state === "FAILED") {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Athena query failed: ${QueryExecution?.Status?.StateChangeReason ?? "unknown"}`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (state === "CANCELLED") throw new Error("Athena query was cancelled");
|
|
177
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
178
|
+
}
|
|
179
|
+
const { ResultSet } = await client.send(
|
|
180
|
+
new GetQueryResultsCommand({ QueryExecutionId })
|
|
181
|
+
);
|
|
182
|
+
const resultRows = ResultSet?.Rows ?? [];
|
|
183
|
+
if (resultRows.length === 0) return { rows: [] };
|
|
184
|
+
const headers = resultRows[0].Data?.map((d) => d.VarCharValue ?? "") ?? [];
|
|
185
|
+
const rows = resultRows.slice(1).map((row) => {
|
|
186
|
+
const obj = {};
|
|
187
|
+
row.Data?.forEach((d, i) => {
|
|
188
|
+
obj[headers[i]] = d.VarCharValue ?? null;
|
|
189
|
+
});
|
|
190
|
+
return obj;
|
|
191
|
+
});
|
|
192
|
+
return { rows };
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/connector-client/redshift.ts
|
|
198
|
+
function createRedshiftClient(entry, connectionId) {
|
|
199
|
+
const region = resolveEnvVar(entry, "aws-region", connectionId);
|
|
200
|
+
const accessKeyId = resolveEnvVar(entry, "aws-access-key-id", connectionId);
|
|
201
|
+
const secretAccessKey = resolveEnvVar(entry, "aws-secret-access-key", connectionId);
|
|
202
|
+
const database = resolveEnvVar(entry, "database", connectionId);
|
|
203
|
+
const clusterIdentifier = resolveEnvVarOptional(entry, "cluster-identifier");
|
|
204
|
+
const workgroupName = resolveEnvVarOptional(entry, "workgroup-name");
|
|
205
|
+
const secretArn = resolveEnvVarOptional(entry, "secret-arn");
|
|
206
|
+
const dbUser = resolveEnvVarOptional(entry, "db-user");
|
|
207
|
+
return {
|
|
208
|
+
async query(sql) {
|
|
209
|
+
const {
|
|
210
|
+
RedshiftDataClient,
|
|
211
|
+
ExecuteStatementCommand,
|
|
212
|
+
DescribeStatementCommand,
|
|
213
|
+
GetStatementResultCommand
|
|
214
|
+
} = await import("@aws-sdk/client-redshift-data");
|
|
215
|
+
const client = new RedshiftDataClient({
|
|
216
|
+
region,
|
|
217
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
218
|
+
});
|
|
219
|
+
const executeParams = {
|
|
220
|
+
Sql: sql,
|
|
221
|
+
Database: database
|
|
222
|
+
};
|
|
223
|
+
if (clusterIdentifier) executeParams.ClusterIdentifier = clusterIdentifier;
|
|
224
|
+
if (workgroupName) executeParams.WorkgroupName = workgroupName;
|
|
225
|
+
if (secretArn) executeParams.SecretArn = secretArn;
|
|
226
|
+
if (dbUser) executeParams.DbUser = dbUser;
|
|
227
|
+
const { Id } = await client.send(
|
|
228
|
+
new ExecuteStatementCommand(executeParams)
|
|
229
|
+
);
|
|
230
|
+
if (!Id) throw new Error("Redshift: failed to start statement execution");
|
|
231
|
+
while (true) {
|
|
232
|
+
const desc = await client.send(new DescribeStatementCommand({ Id }));
|
|
233
|
+
const status = desc.Status;
|
|
234
|
+
if (status === "FINISHED") break;
|
|
235
|
+
if (status === "FAILED") {
|
|
236
|
+
throw new Error(`Redshift query failed: ${desc.Error ?? "unknown"}`);
|
|
237
|
+
}
|
|
238
|
+
if (status === "ABORTED") throw new Error("Redshift query was aborted");
|
|
239
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
240
|
+
}
|
|
241
|
+
const result = await client.send(new GetStatementResultCommand({ Id }));
|
|
242
|
+
const columns = result.ColumnMetadata?.map((c) => c.name ?? "") ?? [];
|
|
243
|
+
const rows = (result.Records ?? []).map((record) => {
|
|
244
|
+
const obj = {};
|
|
245
|
+
record.forEach((field, i) => {
|
|
246
|
+
const col = columns[i];
|
|
247
|
+
const value = field.stringValue ?? field.longValue ?? field.doubleValue ?? field.booleanValue ?? (field.isNull ? null : field.blobValue ?? null);
|
|
248
|
+
obj[col] = value;
|
|
249
|
+
});
|
|
250
|
+
return obj;
|
|
251
|
+
});
|
|
252
|
+
return { rows };
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/connector-client/databricks.ts
|
|
258
|
+
function createDatabricksClient(entry, connectionId) {
|
|
259
|
+
const host = resolveEnvVar(entry, "host", connectionId);
|
|
260
|
+
const httpPath = resolveEnvVar(entry, "http-path", connectionId);
|
|
261
|
+
const token = resolveEnvVar(entry, "token", connectionId);
|
|
262
|
+
return {
|
|
263
|
+
async query(sql) {
|
|
264
|
+
const { DBSQLClient } = await import("@databricks/sql");
|
|
265
|
+
const client = new DBSQLClient();
|
|
266
|
+
await client.connect({ host, path: httpPath, token });
|
|
267
|
+
try {
|
|
268
|
+
const session = await client.openSession();
|
|
269
|
+
try {
|
|
270
|
+
const operation = await session.executeStatement(sql);
|
|
271
|
+
const result = await operation.fetchAll();
|
|
272
|
+
await operation.close();
|
|
273
|
+
return { rows: result };
|
|
274
|
+
} finally {
|
|
275
|
+
await session.close();
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
await client.close();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
108
284
|
// src/connector-client/registry.ts
|
|
109
285
|
function createConnectorRegistry() {
|
|
110
|
-
let connectionsCache = null;
|
|
111
286
|
const clientCache = /* @__PURE__ */ new Map();
|
|
112
287
|
function getConnectionsFilePath() {
|
|
113
288
|
return process.env.CONNECTIONS_PATH ?? path.join(process.cwd(), ".squadbase/connections.json");
|
|
114
289
|
}
|
|
115
|
-
function
|
|
116
|
-
if (connectionsCache !== null) return connectionsCache;
|
|
290
|
+
async function loadConnections2() {
|
|
117
291
|
const filePath = getConnectionsFilePath();
|
|
118
292
|
try {
|
|
119
|
-
const raw =
|
|
120
|
-
|
|
293
|
+
const raw = await readFile(filePath, "utf-8");
|
|
294
|
+
return JSON.parse(raw);
|
|
121
295
|
} catch {
|
|
122
|
-
|
|
296
|
+
return {};
|
|
123
297
|
}
|
|
124
|
-
return connectionsCache;
|
|
125
298
|
}
|
|
126
299
|
async function getClient2(connectionId) {
|
|
127
|
-
const connections =
|
|
300
|
+
const connections = await loadConnections2();
|
|
128
301
|
const entry = connections[connectionId];
|
|
129
302
|
if (!entry) {
|
|
130
303
|
throw new Error(`connection '${connectionId}' not found in .squadbase/connections.json`);
|
|
@@ -138,6 +311,20 @@ function createConnectorRegistry() {
|
|
|
138
311
|
if (connectorSlug === "bigquery") {
|
|
139
312
|
return { client: createBigQueryClient(entry, connectionId), connectorSlug };
|
|
140
313
|
}
|
|
314
|
+
if (connectorSlug === "athena") {
|
|
315
|
+
return { client: createAthenaClient(entry, connectionId), connectorSlug };
|
|
316
|
+
}
|
|
317
|
+
if (connectorSlug === "redshift") {
|
|
318
|
+
return { client: createRedshiftClient(entry, connectionId), connectorSlug };
|
|
319
|
+
}
|
|
320
|
+
if (connectorSlug === "databricks") {
|
|
321
|
+
return { client: createDatabricksClient(entry, connectionId), connectorSlug };
|
|
322
|
+
}
|
|
323
|
+
if (connectorSlug === "mysql") {
|
|
324
|
+
const client = createMySQLClient(entry, connectionId);
|
|
325
|
+
clientCache.set(connectionId, client);
|
|
326
|
+
return { client, connectorSlug };
|
|
327
|
+
}
|
|
141
328
|
if (connectorSlug === "postgresql" || connectorSlug === "squadbase-db") {
|
|
142
329
|
const urlEnvName = entry.envVars["connection-url"];
|
|
143
330
|
if (!urlEnvName) {
|
|
@@ -154,7 +341,7 @@ function createConnectorRegistry() {
|
|
|
154
341
|
return { client, connectorSlug };
|
|
155
342
|
}
|
|
156
343
|
throw new Error(
|
|
157
|
-
`connector type '${connectorSlug}' is not supported. Supported: "snowflake", "bigquery", "
|
|
344
|
+
`connector type '${connectorSlug}' is not supported as a SQL connector. Supported SQL types: "postgresql", "squadbase-db", "mysql", "snowflake", "bigquery", "athena", "redshift", "databricks". Non-SQL types (airtable, google-analytics, kintone, wix-store, dbt) should be used via TypeScript handlers.`
|
|
158
345
|
);
|
|
159
346
|
}
|
|
160
347
|
function reloadEnvFile2(envPath) {
|
|
@@ -178,19 +365,18 @@ function createConnectorRegistry() {
|
|
|
178
365
|
const envPath = path.join(process.cwd(), ".env");
|
|
179
366
|
try {
|
|
180
367
|
fsWatch(filePath, { persistent: false }, () => {
|
|
181
|
-
console.log("[connector-client] connections.json changed, clearing cache");
|
|
182
|
-
connectionsCache = null;
|
|
368
|
+
console.log("[connector-client] connections.json changed, clearing client cache");
|
|
183
369
|
clientCache.clear();
|
|
184
370
|
setImmediate(() => reloadEnvFile2(envPath));
|
|
185
371
|
});
|
|
186
372
|
} catch {
|
|
187
373
|
}
|
|
188
374
|
}
|
|
189
|
-
return { getClient: getClient2, reloadEnvFile: reloadEnvFile2, watchConnectionsFile: watchConnectionsFile2 };
|
|
375
|
+
return { getClient: getClient2, loadConnections: loadConnections2, reloadEnvFile: reloadEnvFile2, watchConnectionsFile: watchConnectionsFile2 };
|
|
190
376
|
}
|
|
191
377
|
|
|
192
378
|
// src/connector-client/index.ts
|
|
193
|
-
var { getClient, reloadEnvFile, watchConnectionsFile } = createConnectorRegistry();
|
|
379
|
+
var { getClient, loadConnections, reloadEnvFile, watchConnectionsFile } = createConnectorRegistry();
|
|
194
380
|
|
|
195
381
|
// src/registry.ts
|
|
196
382
|
var dataSources = /* @__PURE__ */ new Map();
|
|
@@ -256,7 +442,7 @@ async function initialize() {
|
|
|
256
442
|
const results = await Promise.allSettled(
|
|
257
443
|
jsonFiles.map(async (file) => {
|
|
258
444
|
const slug = file.replace(/\.json$/, "");
|
|
259
|
-
const raw = await
|
|
445
|
+
const raw = await readFile2(`${dirPath}/${file}`, "utf-8");
|
|
260
446
|
const def = JSON.parse(raw);
|
|
261
447
|
if (!def.description) {
|
|
262
448
|
console.warn(`[registry] Skipping ${file}: missing description`);
|
|
@@ -301,10 +487,10 @@ async function initialize() {
|
|
|
301
487
|
_query: sqlDef.query,
|
|
302
488
|
handler: async (runtimeParams) => {
|
|
303
489
|
const { client, connectorSlug } = await getClient(sqlDef.connectionId);
|
|
304
|
-
const
|
|
490
|
+
const isLiteralConnector = connectorSlug === "snowflake" || connectorSlug === "bigquery" || connectorSlug === "athena" || connectorSlug === "redshift" || connectorSlug === "databricks";
|
|
305
491
|
let queryText;
|
|
306
492
|
let queryValues;
|
|
307
|
-
if (
|
|
493
|
+
if (isLiteralConnector) {
|
|
308
494
|
const defaults = new Map(
|
|
309
495
|
(sqlDef.parameters ?? []).map((p) => [p.name, p.default ?? null])
|
|
310
496
|
);
|
|
@@ -322,6 +508,14 @@ async function initialize() {
|
|
|
322
508
|
}
|
|
323
509
|
);
|
|
324
510
|
queryValues = [];
|
|
511
|
+
} else if (connectorSlug === "mysql") {
|
|
512
|
+
const built = buildQuery(
|
|
513
|
+
sqlDef.query,
|
|
514
|
+
sqlDef.parameters ?? [],
|
|
515
|
+
runtimeParams
|
|
516
|
+
);
|
|
517
|
+
queryText = built.text.replace(/\$(\d+)/g, "?");
|
|
518
|
+
queryValues = built.values;
|
|
325
519
|
} else {
|
|
326
520
|
const built = buildQuery(
|
|
327
521
|
sqlDef.query,
|
|
@@ -7,14 +7,14 @@ interface ParameterMeta {
|
|
|
7
7
|
}
|
|
8
8
|
interface DataSourceCacheConfig {
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* 0
|
|
10
|
+
* Cache TTL in seconds.
|
|
11
|
+
* 0 or unset means no caching (default behavior for backward compatibility).
|
|
12
12
|
*/
|
|
13
13
|
ttl: number;
|
|
14
14
|
/**
|
|
15
|
-
* true
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* When true, stale data is returned immediately after TTL expiry
|
|
16
|
+
* while fresh data is fetched asynchronously in the background to update the cache.
|
|
17
|
+
* Default: false
|
|
18
18
|
*/
|
|
19
19
|
staleWhileRevalidate?: boolean;
|
|
20
20
|
}
|
package/dist/vite-plugin.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import buildPlugin from "@hono/vite-build/node";
|
|
3
3
|
import devServer from "@hono/vite-dev-server";
|
|
4
4
|
import nodeAdapter from "@hono/vite-dev-server/node";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import path3 from "path";
|
|
5
7
|
|
|
6
8
|
// src/registry.ts
|
|
7
|
-
import { readdir, readFile, mkdir } from "fs/promises";
|
|
9
|
+
import { readdir, readFile as readFile2, mkdir } from "fs/promises";
|
|
8
10
|
import { watch as fsWatch2 } from "fs";
|
|
9
11
|
import path2 from "path";
|
|
10
12
|
|
|
11
13
|
// src/connector-client/registry.ts
|
|
12
14
|
import { readFileSync, watch as fsWatch } from "fs";
|
|
15
|
+
import { readFile } from "fs/promises";
|
|
13
16
|
import path from "path";
|
|
14
17
|
|
|
15
18
|
// src/connector-client/postgresql.ts
|
|
@@ -37,6 +40,11 @@ function resolveEnvVar(entry, key, connectionId) {
|
|
|
37
40
|
}
|
|
38
41
|
return value;
|
|
39
42
|
}
|
|
43
|
+
function resolveEnvVarOptional(entry, key) {
|
|
44
|
+
const envVarName = entry.envVars[key];
|
|
45
|
+
if (!envVarName) return void 0;
|
|
46
|
+
return process.env[envVarName] || void 0;
|
|
47
|
+
}
|
|
40
48
|
|
|
41
49
|
// src/connector-client/bigquery.ts
|
|
42
50
|
function createBigQueryClient(entry, connectionId) {
|
|
@@ -48,7 +56,7 @@ function createBigQueryClient(entry, connectionId) {
|
|
|
48
56
|
gcpCredentials = JSON.parse(serviceAccountJson);
|
|
49
57
|
} catch {
|
|
50
58
|
throw new Error(
|
|
51
|
-
`BigQuery service account JSON (decoded from base64) is not valid JSON for
|
|
59
|
+
`BigQuery service account JSON (decoded from base64) is not valid JSON for connectionId "${connectionId}"`
|
|
52
60
|
);
|
|
53
61
|
}
|
|
54
62
|
return {
|
|
@@ -105,26 +113,193 @@ function createSnowflakeClient(entry, connectionId) {
|
|
|
105
113
|
};
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
// src/connector-client/mysql.ts
|
|
117
|
+
function createMySQLClient(entry, connectionId) {
|
|
118
|
+
const connectionUrl = resolveEnvVar(entry, "connection-url", connectionId);
|
|
119
|
+
let poolPromise = null;
|
|
120
|
+
function getPool() {
|
|
121
|
+
if (!poolPromise) {
|
|
122
|
+
poolPromise = import("mysql2/promise").then(
|
|
123
|
+
(mysql) => mysql.default.createPool(connectionUrl)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return poolPromise;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
async query(sql, params) {
|
|
130
|
+
const pool = await getPool();
|
|
131
|
+
const [rows] = await pool.execute(sql, params);
|
|
132
|
+
return { rows };
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/connector-client/aws-athena.ts
|
|
138
|
+
function createAthenaClient(entry, connectionId) {
|
|
139
|
+
const region = resolveEnvVar(entry, "aws-region", connectionId);
|
|
140
|
+
const accessKeyId = resolveEnvVar(entry, "aws-access-key-id", connectionId);
|
|
141
|
+
const secretAccessKey = resolveEnvVar(entry, "aws-secret-access-key", connectionId);
|
|
142
|
+
const workgroup = resolveEnvVarOptional(entry, "workgroup") ?? "primary";
|
|
143
|
+
const outputLocation = resolveEnvVarOptional(entry, "output-location");
|
|
144
|
+
return {
|
|
145
|
+
async query(sql) {
|
|
146
|
+
const {
|
|
147
|
+
AthenaClient,
|
|
148
|
+
StartQueryExecutionCommand,
|
|
149
|
+
GetQueryExecutionCommand,
|
|
150
|
+
GetQueryResultsCommand
|
|
151
|
+
} = await import("@aws-sdk/client-athena");
|
|
152
|
+
const client = new AthenaClient({
|
|
153
|
+
region,
|
|
154
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
155
|
+
});
|
|
156
|
+
const startParams = {
|
|
157
|
+
QueryString: sql,
|
|
158
|
+
WorkGroup: workgroup
|
|
159
|
+
};
|
|
160
|
+
if (outputLocation) {
|
|
161
|
+
startParams.ResultConfiguration = { OutputLocation: outputLocation };
|
|
162
|
+
}
|
|
163
|
+
const { QueryExecutionId } = await client.send(
|
|
164
|
+
new StartQueryExecutionCommand(startParams)
|
|
165
|
+
);
|
|
166
|
+
if (!QueryExecutionId) throw new Error("Athena: failed to start query execution");
|
|
167
|
+
while (true) {
|
|
168
|
+
const { QueryExecution } = await client.send(
|
|
169
|
+
new GetQueryExecutionCommand({ QueryExecutionId })
|
|
170
|
+
);
|
|
171
|
+
const state = QueryExecution?.Status?.State;
|
|
172
|
+
if (state === "SUCCEEDED") break;
|
|
173
|
+
if (state === "FAILED") {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Athena query failed: ${QueryExecution?.Status?.StateChangeReason ?? "unknown"}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (state === "CANCELLED") throw new Error("Athena query was cancelled");
|
|
179
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
180
|
+
}
|
|
181
|
+
const { ResultSet } = await client.send(
|
|
182
|
+
new GetQueryResultsCommand({ QueryExecutionId })
|
|
183
|
+
);
|
|
184
|
+
const resultRows = ResultSet?.Rows ?? [];
|
|
185
|
+
if (resultRows.length === 0) return { rows: [] };
|
|
186
|
+
const headers = resultRows[0].Data?.map((d) => d.VarCharValue ?? "") ?? [];
|
|
187
|
+
const rows = resultRows.slice(1).map((row) => {
|
|
188
|
+
const obj = {};
|
|
189
|
+
row.Data?.forEach((d, i) => {
|
|
190
|
+
obj[headers[i]] = d.VarCharValue ?? null;
|
|
191
|
+
});
|
|
192
|
+
return obj;
|
|
193
|
+
});
|
|
194
|
+
return { rows };
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/connector-client/redshift.ts
|
|
200
|
+
function createRedshiftClient(entry, connectionId) {
|
|
201
|
+
const region = resolveEnvVar(entry, "aws-region", connectionId);
|
|
202
|
+
const accessKeyId = resolveEnvVar(entry, "aws-access-key-id", connectionId);
|
|
203
|
+
const secretAccessKey = resolveEnvVar(entry, "aws-secret-access-key", connectionId);
|
|
204
|
+
const database = resolveEnvVar(entry, "database", connectionId);
|
|
205
|
+
const clusterIdentifier = resolveEnvVarOptional(entry, "cluster-identifier");
|
|
206
|
+
const workgroupName = resolveEnvVarOptional(entry, "workgroup-name");
|
|
207
|
+
const secretArn = resolveEnvVarOptional(entry, "secret-arn");
|
|
208
|
+
const dbUser = resolveEnvVarOptional(entry, "db-user");
|
|
209
|
+
return {
|
|
210
|
+
async query(sql) {
|
|
211
|
+
const {
|
|
212
|
+
RedshiftDataClient,
|
|
213
|
+
ExecuteStatementCommand,
|
|
214
|
+
DescribeStatementCommand,
|
|
215
|
+
GetStatementResultCommand
|
|
216
|
+
} = await import("@aws-sdk/client-redshift-data");
|
|
217
|
+
const client = new RedshiftDataClient({
|
|
218
|
+
region,
|
|
219
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
220
|
+
});
|
|
221
|
+
const executeParams = {
|
|
222
|
+
Sql: sql,
|
|
223
|
+
Database: database
|
|
224
|
+
};
|
|
225
|
+
if (clusterIdentifier) executeParams.ClusterIdentifier = clusterIdentifier;
|
|
226
|
+
if (workgroupName) executeParams.WorkgroupName = workgroupName;
|
|
227
|
+
if (secretArn) executeParams.SecretArn = secretArn;
|
|
228
|
+
if (dbUser) executeParams.DbUser = dbUser;
|
|
229
|
+
const { Id } = await client.send(
|
|
230
|
+
new ExecuteStatementCommand(executeParams)
|
|
231
|
+
);
|
|
232
|
+
if (!Id) throw new Error("Redshift: failed to start statement execution");
|
|
233
|
+
while (true) {
|
|
234
|
+
const desc = await client.send(new DescribeStatementCommand({ Id }));
|
|
235
|
+
const status = desc.Status;
|
|
236
|
+
if (status === "FINISHED") break;
|
|
237
|
+
if (status === "FAILED") {
|
|
238
|
+
throw new Error(`Redshift query failed: ${desc.Error ?? "unknown"}`);
|
|
239
|
+
}
|
|
240
|
+
if (status === "ABORTED") throw new Error("Redshift query was aborted");
|
|
241
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
242
|
+
}
|
|
243
|
+
const result = await client.send(new GetStatementResultCommand({ Id }));
|
|
244
|
+
const columns = result.ColumnMetadata?.map((c) => c.name ?? "") ?? [];
|
|
245
|
+
const rows = (result.Records ?? []).map((record) => {
|
|
246
|
+
const obj = {};
|
|
247
|
+
record.forEach((field, i) => {
|
|
248
|
+
const col = columns[i];
|
|
249
|
+
const value = field.stringValue ?? field.longValue ?? field.doubleValue ?? field.booleanValue ?? (field.isNull ? null : field.blobValue ?? null);
|
|
250
|
+
obj[col] = value;
|
|
251
|
+
});
|
|
252
|
+
return obj;
|
|
253
|
+
});
|
|
254
|
+
return { rows };
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/connector-client/databricks.ts
|
|
260
|
+
function createDatabricksClient(entry, connectionId) {
|
|
261
|
+
const host = resolveEnvVar(entry, "host", connectionId);
|
|
262
|
+
const httpPath = resolveEnvVar(entry, "http-path", connectionId);
|
|
263
|
+
const token = resolveEnvVar(entry, "token", connectionId);
|
|
264
|
+
return {
|
|
265
|
+
async query(sql) {
|
|
266
|
+
const { DBSQLClient } = await import("@databricks/sql");
|
|
267
|
+
const client = new DBSQLClient();
|
|
268
|
+
await client.connect({ host, path: httpPath, token });
|
|
269
|
+
try {
|
|
270
|
+
const session = await client.openSession();
|
|
271
|
+
try {
|
|
272
|
+
const operation = await session.executeStatement(sql);
|
|
273
|
+
const result = await operation.fetchAll();
|
|
274
|
+
await operation.close();
|
|
275
|
+
return { rows: result };
|
|
276
|
+
} finally {
|
|
277
|
+
await session.close();
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
await client.close();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
108
286
|
// src/connector-client/registry.ts
|
|
109
287
|
function createConnectorRegistry() {
|
|
110
|
-
let connectionsCache = null;
|
|
111
288
|
const clientCache = /* @__PURE__ */ new Map();
|
|
112
289
|
function getConnectionsFilePath() {
|
|
113
290
|
return process.env.CONNECTIONS_PATH ?? path.join(process.cwd(), ".squadbase/connections.json");
|
|
114
291
|
}
|
|
115
|
-
function
|
|
116
|
-
if (connectionsCache !== null) return connectionsCache;
|
|
292
|
+
async function loadConnections2() {
|
|
117
293
|
const filePath = getConnectionsFilePath();
|
|
118
294
|
try {
|
|
119
|
-
const raw =
|
|
120
|
-
|
|
295
|
+
const raw = await readFile(filePath, "utf-8");
|
|
296
|
+
return JSON.parse(raw);
|
|
121
297
|
} catch {
|
|
122
|
-
|
|
298
|
+
return {};
|
|
123
299
|
}
|
|
124
|
-
return connectionsCache;
|
|
125
300
|
}
|
|
126
301
|
async function getClient2(connectionId) {
|
|
127
|
-
const connections =
|
|
302
|
+
const connections = await loadConnections2();
|
|
128
303
|
const entry = connections[connectionId];
|
|
129
304
|
if (!entry) {
|
|
130
305
|
throw new Error(`connection '${connectionId}' not found in .squadbase/connections.json`);
|
|
@@ -138,6 +313,20 @@ function createConnectorRegistry() {
|
|
|
138
313
|
if (connectorSlug === "bigquery") {
|
|
139
314
|
return { client: createBigQueryClient(entry, connectionId), connectorSlug };
|
|
140
315
|
}
|
|
316
|
+
if (connectorSlug === "athena") {
|
|
317
|
+
return { client: createAthenaClient(entry, connectionId), connectorSlug };
|
|
318
|
+
}
|
|
319
|
+
if (connectorSlug === "redshift") {
|
|
320
|
+
return { client: createRedshiftClient(entry, connectionId), connectorSlug };
|
|
321
|
+
}
|
|
322
|
+
if (connectorSlug === "databricks") {
|
|
323
|
+
return { client: createDatabricksClient(entry, connectionId), connectorSlug };
|
|
324
|
+
}
|
|
325
|
+
if (connectorSlug === "mysql") {
|
|
326
|
+
const client = createMySQLClient(entry, connectionId);
|
|
327
|
+
clientCache.set(connectionId, client);
|
|
328
|
+
return { client, connectorSlug };
|
|
329
|
+
}
|
|
141
330
|
if (connectorSlug === "postgresql" || connectorSlug === "squadbase-db") {
|
|
142
331
|
const urlEnvName = entry.envVars["connection-url"];
|
|
143
332
|
if (!urlEnvName) {
|
|
@@ -154,7 +343,7 @@ function createConnectorRegistry() {
|
|
|
154
343
|
return { client, connectorSlug };
|
|
155
344
|
}
|
|
156
345
|
throw new Error(
|
|
157
|
-
`connector type '${connectorSlug}' is not supported. Supported: "snowflake", "bigquery", "
|
|
346
|
+
`connector type '${connectorSlug}' is not supported as a SQL connector. Supported SQL types: "postgresql", "squadbase-db", "mysql", "snowflake", "bigquery", "athena", "redshift", "databricks". Non-SQL types (airtable, google-analytics, kintone, wix-store, dbt) should be used via TypeScript handlers.`
|
|
158
347
|
);
|
|
159
348
|
}
|
|
160
349
|
function reloadEnvFile2(envPath) {
|
|
@@ -178,19 +367,18 @@ function createConnectorRegistry() {
|
|
|
178
367
|
const envPath = path.join(process.cwd(), ".env");
|
|
179
368
|
try {
|
|
180
369
|
fsWatch(filePath, { persistent: false }, () => {
|
|
181
|
-
console.log("[connector-client] connections.json changed, clearing cache");
|
|
182
|
-
connectionsCache = null;
|
|
370
|
+
console.log("[connector-client] connections.json changed, clearing client cache");
|
|
183
371
|
clientCache.clear();
|
|
184
372
|
setImmediate(() => reloadEnvFile2(envPath));
|
|
185
373
|
});
|
|
186
374
|
} catch {
|
|
187
375
|
}
|
|
188
376
|
}
|
|
189
|
-
return { getClient: getClient2, reloadEnvFile: reloadEnvFile2, watchConnectionsFile: watchConnectionsFile2 };
|
|
377
|
+
return { getClient: getClient2, loadConnections: loadConnections2, reloadEnvFile: reloadEnvFile2, watchConnectionsFile: watchConnectionsFile2 };
|
|
190
378
|
}
|
|
191
379
|
|
|
192
380
|
// src/connector-client/index.ts
|
|
193
|
-
var { getClient, reloadEnvFile, watchConnectionsFile } = createConnectorRegistry();
|
|
381
|
+
var { getClient, loadConnections, reloadEnvFile, watchConnectionsFile } = createConnectorRegistry();
|
|
194
382
|
|
|
195
383
|
// src/registry.ts
|
|
196
384
|
var viteServer = null;
|
|
@@ -211,6 +399,17 @@ var DEFAULT_EXCLUDE = [
|
|
|
211
399
|
/^\/node_modules\/.*/,
|
|
212
400
|
/^(?!\/api)/
|
|
213
401
|
];
|
|
402
|
+
function resolveEntry(entry) {
|
|
403
|
+
if (entry.startsWith(".") || entry.startsWith("/")) return entry;
|
|
404
|
+
try {
|
|
405
|
+
const resolvedUrl = import.meta.resolve(entry);
|
|
406
|
+
const absolutePath = fileURLToPath(resolvedUrl);
|
|
407
|
+
const relativePath = path3.relative(process.cwd(), absolutePath).replace(/\\/g, "/");
|
|
408
|
+
return "./" + relativePath;
|
|
409
|
+
} catch {
|
|
410
|
+
return entry;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
214
413
|
function squadbasePlugin(options = {}) {
|
|
215
414
|
const {
|
|
216
415
|
buildEntry = "@squadbase/vite-server/main",
|
|
@@ -221,7 +420,7 @@ function squadbasePlugin(options = {}) {
|
|
|
221
420
|
} = options;
|
|
222
421
|
const isServerBuild = (_, { command, mode }) => command === "build" && mode !== "client";
|
|
223
422
|
const rawBuildPlugin = buildPlugin({
|
|
224
|
-
entry: buildEntry,
|
|
423
|
+
entry: resolveEntry(buildEntry),
|
|
225
424
|
outputDir: "./dist/server",
|
|
226
425
|
output: "index.js",
|
|
227
426
|
port,
|