@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/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 connection "${connectionId}"`
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 loadConnections() {
116
- if (connectionsCache !== null) return connectionsCache;
290
+ async function loadConnections2() {
117
291
  const filePath = getConnectionsFilePath();
118
292
  try {
119
- const raw = readFileSync(filePath, "utf-8");
120
- connectionsCache = JSON.parse(raw);
293
+ const raw = await readFile(filePath, "utf-8");
294
+ return JSON.parse(raw);
121
295
  } catch {
122
- connectionsCache = {};
296
+ return {};
123
297
  }
124
- return connectionsCache;
125
298
  }
126
299
  async function getClient2(connectionId) {
127
- const connections = loadConnections();
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", "postgresql", "squadbase-db"`
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 readFile(`${dirPath}/${file}`, "utf-8");
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 isExternalConnector = connectorSlug === "snowflake" || connectorSlug === "bigquery";
490
+ const isLiteralConnector = connectorSlug === "snowflake" || connectorSlug === "bigquery" || connectorSlug === "athena" || connectorSlug === "redshift" || connectorSlug === "databricks";
305
491
  let queryText;
306
492
  let queryValues;
307
- if (isExternalConnector) {
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 の場合、TTL 期限切れ後も古いデータを即座に返しつつ、
16
- * バックグラウンドで新しいデータを非同期取得してキャッシュを更新する。
17
- * デフォルト: false
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
  }
@@ -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 connection "${connectionId}"`
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 loadConnections() {
116
- if (connectionsCache !== null) return connectionsCache;
292
+ async function loadConnections2() {
117
293
  const filePath = getConnectionsFilePath();
118
294
  try {
119
- const raw = readFileSync(filePath, "utf-8");
120
- connectionsCache = JSON.parse(raw);
295
+ const raw = await readFile(filePath, "utf-8");
296
+ return JSON.parse(raw);
121
297
  } catch {
122
- connectionsCache = {};
298
+ return {};
123
299
  }
124
- return connectionsCache;
125
300
  }
126
301
  async function getClient2(connectionId) {
127
- const connections = loadConnections();
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", "postgresql", "squadbase-db"`
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,