@squadbase/vite-server 0.0.1-build-3 → 0.0.1-build-5
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/index.d.ts +1 -120
- package/dist/index.js +60 -530
- package/dist/main.js +59 -260
- package/dist/types/data-source.d.ts +14 -9
- package/dist/vite-plugin.js +34 -251
- package/package.json +2 -9
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -848
package/dist/cli/index.js
DELETED
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
-
var __esm = (fn, res) => function __init() {
|
|
5
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
-
};
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// src/cli/interactive.ts
|
|
13
|
-
var interactive_exports = {};
|
|
14
|
-
__export(interactive_exports, {
|
|
15
|
-
confirmRunAll: () => confirmRunAll,
|
|
16
|
-
inputParameters: () => inputParameters,
|
|
17
|
-
selectDataSource: () => selectDataSource
|
|
18
|
-
});
|
|
19
|
-
async function getPrompts() {
|
|
20
|
-
try {
|
|
21
|
-
const mod = await import("@clack/prompts");
|
|
22
|
-
return mod;
|
|
23
|
-
} catch {
|
|
24
|
-
throw new Error(
|
|
25
|
-
"@clack/prompts is not installed. Run: npm install @clack/prompts --save-dev"
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
async function selectDataSource(slugs) {
|
|
30
|
-
const { select, isCancel } = await getPrompts();
|
|
31
|
-
const result = await select({
|
|
32
|
-
message: "Select a data source to test:",
|
|
33
|
-
options: slugs.map((s) => ({ value: s, label: s }))
|
|
34
|
-
});
|
|
35
|
-
if (isCancel(result)) return null;
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
async function inputParameters(params) {
|
|
39
|
-
if (params.length === 0) return {};
|
|
40
|
-
const { text, isCancel } = await getPrompts();
|
|
41
|
-
const result = {};
|
|
42
|
-
for (const p of params) {
|
|
43
|
-
const defaultHint = p.default !== void 0 ? ` (default: ${p.default})` : "";
|
|
44
|
-
const requiredHint = p.required ? " *required*" : "";
|
|
45
|
-
const value = await text({
|
|
46
|
-
message: `${p.name}${requiredHint} [${p.type}]${defaultHint}: ${p.description}`,
|
|
47
|
-
placeholder: p.default !== void 0 ? String(p.default) : "",
|
|
48
|
-
validate: (v) => {
|
|
49
|
-
if (p.required && !v && p.default === void 0) {
|
|
50
|
-
return `${p.name} is required`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
if (isCancel(value)) {
|
|
55
|
-
process.exit(0);
|
|
56
|
-
}
|
|
57
|
-
const strValue = value;
|
|
58
|
-
if (!strValue && p.default !== void 0) {
|
|
59
|
-
result[p.name] = p.default;
|
|
60
|
-
} else if (strValue) {
|
|
61
|
-
if (p.type === "number") result[p.name] = Number(strValue);
|
|
62
|
-
else if (p.type === "boolean") result[p.name] = strValue === "true";
|
|
63
|
-
else result[p.name] = strValue;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return result;
|
|
67
|
-
}
|
|
68
|
-
async function confirmRunAll() {
|
|
69
|
-
const { confirm, isCancel } = await getPrompts();
|
|
70
|
-
const result = await confirm({
|
|
71
|
-
message: "Run all data sources?"
|
|
72
|
-
});
|
|
73
|
-
if (isCancel(result)) return false;
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
var init_interactive = __esm({
|
|
77
|
-
"src/cli/interactive.ts"() {
|
|
78
|
-
"use strict";
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// src/cli/index.ts
|
|
83
|
-
import { parseArgs } from "util";
|
|
84
|
-
import path4 from "path";
|
|
85
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
86
|
-
|
|
87
|
-
// src/connector-client/registry.ts
|
|
88
|
-
import { readFileSync, watch as fsWatch } from "fs";
|
|
89
|
-
import path from "path";
|
|
90
|
-
|
|
91
|
-
// src/connector-client/postgresql.ts
|
|
92
|
-
import pg from "pg";
|
|
93
|
-
var { Pool } = pg;
|
|
94
|
-
function createPostgreSQLClient(connectionString) {
|
|
95
|
-
const pool = new Pool({ connectionString, ssl: { rejectUnauthorized: false } });
|
|
96
|
-
return {
|
|
97
|
-
async query(sql, params) {
|
|
98
|
-
const result = await pool.query(sql, params);
|
|
99
|
-
return { rows: result.rows };
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// src/connector-client/env.ts
|
|
105
|
-
function resolveEnvVar(entry, key, slug) {
|
|
106
|
-
const envVarName = entry.envVars[key];
|
|
107
|
-
if (!envVarName) {
|
|
108
|
-
throw new Error(`Connector "${slug}" is missing envVars mapping for key "${key}"`);
|
|
109
|
-
}
|
|
110
|
-
const value = process.env[envVarName];
|
|
111
|
-
if (!value) {
|
|
112
|
-
throw new Error(`Environment variable "${envVarName}" (for "${slug}.${key}") is not set`);
|
|
113
|
-
}
|
|
114
|
-
return value;
|
|
115
|
-
}
|
|
116
|
-
function resolveEnvVarOptional(entry, key) {
|
|
117
|
-
const envVarName = entry.envVars[key];
|
|
118
|
-
if (!envVarName) return void 0;
|
|
119
|
-
return process.env[envVarName] || void 0;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// src/connector-client/bigquery.ts
|
|
123
|
-
function createBigQueryClient(entry, slug) {
|
|
124
|
-
const projectId = resolveEnvVar(entry, "project-id", slug);
|
|
125
|
-
const serviceAccountJsonBase64 = resolveEnvVar(entry, "service-account-json-base64", slug);
|
|
126
|
-
const serviceAccountJson = Buffer.from(serviceAccountJsonBase64, "base64").toString("utf-8");
|
|
127
|
-
let gcpCredentials;
|
|
128
|
-
try {
|
|
129
|
-
gcpCredentials = JSON.parse(serviceAccountJson);
|
|
130
|
-
} catch {
|
|
131
|
-
throw new Error(
|
|
132
|
-
`BigQuery service account JSON (decoded from base64) is not valid JSON for slug "${slug}"`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
async query(sql) {
|
|
137
|
-
const { BigQuery } = await import("@google-cloud/bigquery");
|
|
138
|
-
const bq = new BigQuery({ projectId, credentials: gcpCredentials });
|
|
139
|
-
const [job] = await bq.createQueryJob({ query: sql });
|
|
140
|
-
const [allRows] = await job.getQueryResults({ timeoutMs: 3e4 });
|
|
141
|
-
return { rows: allRows };
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/connector-client/snowflake.ts
|
|
147
|
-
function createSnowflakeClient(entry, slug) {
|
|
148
|
-
const accountIdentifier = resolveEnvVar(entry, "account", slug);
|
|
149
|
-
const user = resolveEnvVar(entry, "user", slug);
|
|
150
|
-
const role = resolveEnvVar(entry, "role", slug);
|
|
151
|
-
const warehouse = resolveEnvVar(entry, "warehouse", slug);
|
|
152
|
-
const privateKeyBase64 = resolveEnvVar(entry, "private-key-base64", slug);
|
|
153
|
-
const privateKey = Buffer.from(privateKeyBase64, "base64").toString("utf-8");
|
|
154
|
-
return {
|
|
155
|
-
async query(sql) {
|
|
156
|
-
const snowflake = (await import("snowflake-sdk")).default;
|
|
157
|
-
snowflake.configure({ logLevel: "ERROR" });
|
|
158
|
-
const connection = snowflake.createConnection({
|
|
159
|
-
account: accountIdentifier,
|
|
160
|
-
username: user,
|
|
161
|
-
role,
|
|
162
|
-
warehouse,
|
|
163
|
-
authenticator: "SNOWFLAKE_JWT",
|
|
164
|
-
privateKey
|
|
165
|
-
});
|
|
166
|
-
await new Promise((resolve, reject) => {
|
|
167
|
-
connection.connect((err) => {
|
|
168
|
-
if (err) reject(new Error(`Snowflake connect failed: ${err.message}`));
|
|
169
|
-
else resolve();
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
const rows = await new Promise((resolve, reject) => {
|
|
173
|
-
connection.execute({
|
|
174
|
-
sqlText: sql,
|
|
175
|
-
complete: (err, _stmt, rows2) => {
|
|
176
|
-
if (err) reject(new Error(`Snowflake query failed: ${err.message}`));
|
|
177
|
-
else resolve(rows2 ?? []);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
connection.destroy((err) => {
|
|
182
|
-
if (err) console.warn(`[connector-client] Snowflake destroy error: ${err.message}`);
|
|
183
|
-
});
|
|
184
|
-
return { rows };
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// src/connector-client/mysql.ts
|
|
190
|
-
function createMySQLClient(entry, slug) {
|
|
191
|
-
const connectionUrl = resolveEnvVar(entry, "connection-url", slug);
|
|
192
|
-
let poolPromise = null;
|
|
193
|
-
function getPool() {
|
|
194
|
-
if (!poolPromise) {
|
|
195
|
-
poolPromise = import("mysql2/promise").then(
|
|
196
|
-
(mysql) => mysql.default.createPool(connectionUrl)
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
return poolPromise;
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
async query(sql, params) {
|
|
203
|
-
const pool = await getPool();
|
|
204
|
-
const [rows] = await pool.execute(sql, params);
|
|
205
|
-
return { rows };
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// src/connector-client/aws-athena.ts
|
|
211
|
-
function createAthenaClient(entry, slug) {
|
|
212
|
-
const region = resolveEnvVar(entry, "aws-region", slug);
|
|
213
|
-
const accessKeyId = resolveEnvVar(entry, "aws-access-key-id", slug);
|
|
214
|
-
const secretAccessKey = resolveEnvVar(entry, "aws-secret-access-key", slug);
|
|
215
|
-
const workgroup = resolveEnvVarOptional(entry, "workgroup") ?? "primary";
|
|
216
|
-
const outputLocation = resolveEnvVarOptional(entry, "output-location");
|
|
217
|
-
return {
|
|
218
|
-
async query(sql) {
|
|
219
|
-
const {
|
|
220
|
-
AthenaClient,
|
|
221
|
-
StartQueryExecutionCommand,
|
|
222
|
-
GetQueryExecutionCommand,
|
|
223
|
-
GetQueryResultsCommand
|
|
224
|
-
} = await import("@aws-sdk/client-athena");
|
|
225
|
-
const client = new AthenaClient({
|
|
226
|
-
region,
|
|
227
|
-
credentials: { accessKeyId, secretAccessKey }
|
|
228
|
-
});
|
|
229
|
-
const startParams = {
|
|
230
|
-
QueryString: sql,
|
|
231
|
-
WorkGroup: workgroup
|
|
232
|
-
};
|
|
233
|
-
if (outputLocation) {
|
|
234
|
-
startParams.ResultConfiguration = { OutputLocation: outputLocation };
|
|
235
|
-
}
|
|
236
|
-
const { QueryExecutionId } = await client.send(
|
|
237
|
-
new StartQueryExecutionCommand(startParams)
|
|
238
|
-
);
|
|
239
|
-
if (!QueryExecutionId) throw new Error("Athena: failed to start query execution");
|
|
240
|
-
while (true) {
|
|
241
|
-
const { QueryExecution } = await client.send(
|
|
242
|
-
new GetQueryExecutionCommand({ QueryExecutionId })
|
|
243
|
-
);
|
|
244
|
-
const state = QueryExecution?.Status?.State;
|
|
245
|
-
if (state === "SUCCEEDED") break;
|
|
246
|
-
if (state === "FAILED") {
|
|
247
|
-
throw new Error(
|
|
248
|
-
`Athena query failed: ${QueryExecution?.Status?.StateChangeReason ?? "unknown"}`
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
if (state === "CANCELLED") throw new Error("Athena query was cancelled");
|
|
252
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
253
|
-
}
|
|
254
|
-
const { ResultSet } = await client.send(
|
|
255
|
-
new GetQueryResultsCommand({ QueryExecutionId })
|
|
256
|
-
);
|
|
257
|
-
const resultRows = ResultSet?.Rows ?? [];
|
|
258
|
-
if (resultRows.length === 0) return { rows: [] };
|
|
259
|
-
const headers = resultRows[0].Data?.map((d) => d.VarCharValue ?? "") ?? [];
|
|
260
|
-
const rows = resultRows.slice(1).map((row) => {
|
|
261
|
-
const obj = {};
|
|
262
|
-
row.Data?.forEach((d, i) => {
|
|
263
|
-
obj[headers[i]] = d.VarCharValue ?? null;
|
|
264
|
-
});
|
|
265
|
-
return obj;
|
|
266
|
-
});
|
|
267
|
-
return { rows };
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// src/connector-client/redshift.ts
|
|
273
|
-
function createRedshiftClient(entry, slug) {
|
|
274
|
-
const region = resolveEnvVar(entry, "aws-region", slug);
|
|
275
|
-
const accessKeyId = resolveEnvVar(entry, "aws-access-key-id", slug);
|
|
276
|
-
const secretAccessKey = resolveEnvVar(entry, "aws-secret-access-key", slug);
|
|
277
|
-
const database = resolveEnvVar(entry, "database", slug);
|
|
278
|
-
const clusterIdentifier = resolveEnvVarOptional(entry, "cluster-identifier");
|
|
279
|
-
const workgroupName = resolveEnvVarOptional(entry, "workgroup-name");
|
|
280
|
-
const secretArn = resolveEnvVarOptional(entry, "secret-arn");
|
|
281
|
-
const dbUser = resolveEnvVarOptional(entry, "db-user");
|
|
282
|
-
return {
|
|
283
|
-
async query(sql) {
|
|
284
|
-
const {
|
|
285
|
-
RedshiftDataClient,
|
|
286
|
-
ExecuteStatementCommand,
|
|
287
|
-
DescribeStatementCommand,
|
|
288
|
-
GetStatementResultCommand
|
|
289
|
-
} = await import("@aws-sdk/client-redshift-data");
|
|
290
|
-
const client = new RedshiftDataClient({
|
|
291
|
-
region,
|
|
292
|
-
credentials: { accessKeyId, secretAccessKey }
|
|
293
|
-
});
|
|
294
|
-
const executeParams = {
|
|
295
|
-
Sql: sql,
|
|
296
|
-
Database: database
|
|
297
|
-
};
|
|
298
|
-
if (clusterIdentifier) executeParams.ClusterIdentifier = clusterIdentifier;
|
|
299
|
-
if (workgroupName) executeParams.WorkgroupName = workgroupName;
|
|
300
|
-
if (secretArn) executeParams.SecretArn = secretArn;
|
|
301
|
-
if (dbUser) executeParams.DbUser = dbUser;
|
|
302
|
-
const { Id } = await client.send(
|
|
303
|
-
new ExecuteStatementCommand(executeParams)
|
|
304
|
-
);
|
|
305
|
-
if (!Id) throw new Error("Redshift: failed to start statement execution");
|
|
306
|
-
while (true) {
|
|
307
|
-
const desc = await client.send(new DescribeStatementCommand({ Id }));
|
|
308
|
-
const status = desc.Status;
|
|
309
|
-
if (status === "FINISHED") break;
|
|
310
|
-
if (status === "FAILED") {
|
|
311
|
-
throw new Error(`Redshift query failed: ${desc.Error ?? "unknown"}`);
|
|
312
|
-
}
|
|
313
|
-
if (status === "ABORTED") throw new Error("Redshift query was aborted");
|
|
314
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
315
|
-
}
|
|
316
|
-
const result = await client.send(new GetStatementResultCommand({ Id }));
|
|
317
|
-
const columns = result.ColumnMetadata?.map((c) => c.name ?? "") ?? [];
|
|
318
|
-
const rows = (result.Records ?? []).map((record) => {
|
|
319
|
-
const obj = {};
|
|
320
|
-
record.forEach((field, i) => {
|
|
321
|
-
const col = columns[i];
|
|
322
|
-
const value = field.stringValue ?? field.longValue ?? field.doubleValue ?? field.booleanValue ?? (field.isNull ? null : field.blobValue ?? null);
|
|
323
|
-
obj[col] = value;
|
|
324
|
-
});
|
|
325
|
-
return obj;
|
|
326
|
-
});
|
|
327
|
-
return { rows };
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// src/connector-client/databricks.ts
|
|
333
|
-
function createDatabricksClient(entry, slug) {
|
|
334
|
-
const host = resolveEnvVar(entry, "host", slug);
|
|
335
|
-
const httpPath = resolveEnvVar(entry, "http-path", slug);
|
|
336
|
-
const token = resolveEnvVar(entry, "token", slug);
|
|
337
|
-
return {
|
|
338
|
-
async query(sql) {
|
|
339
|
-
const { DBSQLClient } = await import("@databricks/sql");
|
|
340
|
-
const client = new DBSQLClient();
|
|
341
|
-
await client.connect({ host, path: httpPath, token });
|
|
342
|
-
try {
|
|
343
|
-
const session = await client.openSession();
|
|
344
|
-
try {
|
|
345
|
-
const operation = await session.executeStatement(sql);
|
|
346
|
-
const result = await operation.fetchAll();
|
|
347
|
-
await operation.close();
|
|
348
|
-
return { rows: result };
|
|
349
|
-
} finally {
|
|
350
|
-
await session.close();
|
|
351
|
-
}
|
|
352
|
-
} finally {
|
|
353
|
-
await client.close();
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// src/connector-client/registry.ts
|
|
360
|
-
function createConnectorRegistry() {
|
|
361
|
-
let connectionsCache = null;
|
|
362
|
-
const clientCache = /* @__PURE__ */ new Map();
|
|
363
|
-
function getConnectionsFilePath() {
|
|
364
|
-
return process.env.CONNECTIONS_PATH ?? path.join(process.cwd(), "../../.squadbase/connections.json");
|
|
365
|
-
}
|
|
366
|
-
function loadConnections2() {
|
|
367
|
-
if (connectionsCache !== null) return connectionsCache;
|
|
368
|
-
const filePath = getConnectionsFilePath();
|
|
369
|
-
try {
|
|
370
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
371
|
-
connectionsCache = JSON.parse(raw);
|
|
372
|
-
} catch {
|
|
373
|
-
connectionsCache = {};
|
|
374
|
-
}
|
|
375
|
-
return connectionsCache;
|
|
376
|
-
}
|
|
377
|
-
async function getClient2(connectorSlug, connectorType) {
|
|
378
|
-
if (!connectorSlug) {
|
|
379
|
-
const cacheKey = "__squadbase-db__";
|
|
380
|
-
const cached2 = clientCache.get(cacheKey);
|
|
381
|
-
if (cached2) return cached2;
|
|
382
|
-
const url = process.env.SQUADBASE_POSTGRESQL_URL;
|
|
383
|
-
if (!url) throw new Error("SQUADBASE_POSTGRESQL_URL environment variable is not set");
|
|
384
|
-
const client = createPostgreSQLClient(url);
|
|
385
|
-
clientCache.set(cacheKey, client);
|
|
386
|
-
return client;
|
|
387
|
-
}
|
|
388
|
-
const cached = clientCache.get(connectorSlug);
|
|
389
|
-
if (cached) return cached;
|
|
390
|
-
const connections = loadConnections2();
|
|
391
|
-
const entry = connections[connectorSlug];
|
|
392
|
-
if (!entry) {
|
|
393
|
-
throw new Error(`connector slug '${connectorSlug}' not found in .squadbase/connections.json`);
|
|
394
|
-
}
|
|
395
|
-
const resolvedType = connectorType ?? entry.connectorType;
|
|
396
|
-
if (!resolvedType) {
|
|
397
|
-
throw new Error(
|
|
398
|
-
`connector type could not be determined for slug '${connectorSlug}'. Specify connectorType in the data-source JSON or in .squadbase/connections.json.`
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
if (resolvedType === "snowflake") {
|
|
402
|
-
return createSnowflakeClient(entry, connectorSlug);
|
|
403
|
-
}
|
|
404
|
-
if (resolvedType === "bigquery") {
|
|
405
|
-
return createBigQueryClient(entry, connectorSlug);
|
|
406
|
-
}
|
|
407
|
-
if (resolvedType === "athena") {
|
|
408
|
-
return createAthenaClient(entry, connectorSlug);
|
|
409
|
-
}
|
|
410
|
-
if (resolvedType === "redshift") {
|
|
411
|
-
return createRedshiftClient(entry, connectorSlug);
|
|
412
|
-
}
|
|
413
|
-
if (resolvedType === "databricks") {
|
|
414
|
-
return createDatabricksClient(entry, connectorSlug);
|
|
415
|
-
}
|
|
416
|
-
if (resolvedType === "mysql") {
|
|
417
|
-
const client = createMySQLClient(entry, connectorSlug);
|
|
418
|
-
clientCache.set(connectorSlug, client);
|
|
419
|
-
return client;
|
|
420
|
-
}
|
|
421
|
-
if (resolvedType === "postgresql" || resolvedType === "squadbase-db") {
|
|
422
|
-
const urlEnvName = entry.envVars["connection-url"];
|
|
423
|
-
if (!urlEnvName) {
|
|
424
|
-
throw new Error(`'connection-url' is not defined in envVars for connector '${connectorSlug}'`);
|
|
425
|
-
}
|
|
426
|
-
const connectionUrl = process.env[urlEnvName];
|
|
427
|
-
if (!connectionUrl) {
|
|
428
|
-
throw new Error(
|
|
429
|
-
`environment variable '${urlEnvName}' (mapped from connector '${connectorSlug}') is not set`
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
const client = createPostgreSQLClient(connectionUrl);
|
|
433
|
-
clientCache.set(connectorSlug, client);
|
|
434
|
-
return client;
|
|
435
|
-
}
|
|
436
|
-
throw new Error(
|
|
437
|
-
`connector type '${resolvedType}' 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.`
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
function reloadEnvFile2(envPath) {
|
|
441
|
-
try {
|
|
442
|
-
const raw = readFileSync(envPath, "utf-8");
|
|
443
|
-
for (const line of raw.split("\n")) {
|
|
444
|
-
const trimmed = line.trim();
|
|
445
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
446
|
-
const eqIdx = trimmed.indexOf("=");
|
|
447
|
-
if (eqIdx === -1) continue;
|
|
448
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
449
|
-
const value = trimmed.slice(eqIdx + 1).trim();
|
|
450
|
-
if (key) process.env[key] = value;
|
|
451
|
-
}
|
|
452
|
-
console.log("[connector-client] .env reloaded");
|
|
453
|
-
} catch {
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
function watchConnectionsFile2() {
|
|
457
|
-
const filePath = getConnectionsFilePath();
|
|
458
|
-
const envPath = path.join(process.cwd(), "..", "..", ".env");
|
|
459
|
-
try {
|
|
460
|
-
fsWatch(filePath, { persistent: false }, () => {
|
|
461
|
-
console.log("[connector-client] connections.json changed, clearing cache");
|
|
462
|
-
connectionsCache = null;
|
|
463
|
-
clientCache.clear();
|
|
464
|
-
setImmediate(() => reloadEnvFile2(envPath));
|
|
465
|
-
});
|
|
466
|
-
} catch {
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
return { getClient: getClient2, loadConnections: loadConnections2, reloadEnvFile: reloadEnvFile2, watchConnectionsFile: watchConnectionsFile2 };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// src/connector-client/index.ts
|
|
473
|
-
var { getClient, loadConnections, reloadEnvFile, watchConnectionsFile } = createConnectorRegistry();
|
|
474
|
-
|
|
475
|
-
// src/cli/env-loader.ts
|
|
476
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
477
|
-
function loadEnvFile(envPath) {
|
|
478
|
-
reloadEnvFile(envPath);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// src/cli/runner.ts
|
|
482
|
-
import { pathToFileURL } from "url";
|
|
483
|
-
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
484
|
-
import path3 from "path";
|
|
485
|
-
|
|
486
|
-
// src/registry.ts
|
|
487
|
-
import { readdir, readFile, mkdir } from "fs/promises";
|
|
488
|
-
import { watch as fsWatch2 } from "fs";
|
|
489
|
-
import path2 from "path";
|
|
490
|
-
function buildQuery(queryTemplate, parameterMeta, runtimeParams) {
|
|
491
|
-
const defaults = new Map(
|
|
492
|
-
parameterMeta.map((p) => [p.name, p.default ?? null])
|
|
493
|
-
);
|
|
494
|
-
const placeholderToIndex = /* @__PURE__ */ new Map();
|
|
495
|
-
const values = [];
|
|
496
|
-
const text = queryTemplate.replace(
|
|
497
|
-
/\{\{(\w+)\}\}/g,
|
|
498
|
-
(_match, name) => {
|
|
499
|
-
if (!placeholderToIndex.has(name)) {
|
|
500
|
-
const value = Object.prototype.hasOwnProperty.call(runtimeParams, name) ? runtimeParams[name] : defaults.get(name) ?? null;
|
|
501
|
-
values.push(value);
|
|
502
|
-
placeholderToIndex.set(name, values.length);
|
|
503
|
-
}
|
|
504
|
-
return `$${placeholderToIndex.get(name)}`;
|
|
505
|
-
}
|
|
506
|
-
);
|
|
507
|
-
return { text, values };
|
|
508
|
-
}
|
|
509
|
-
var defaultDataSourceDir = path2.join(process.cwd(), "data-source");
|
|
510
|
-
|
|
511
|
-
// src/cli/runner.ts
|
|
512
|
-
function createStubContext(params) {
|
|
513
|
-
const stub = {
|
|
514
|
-
req: {
|
|
515
|
-
json: () => Promise.resolve(params),
|
|
516
|
-
query: (name) => {
|
|
517
|
-
if (name === void 0) {
|
|
518
|
-
return Object.fromEntries(
|
|
519
|
-
Object.entries(params).map(([k, v2]) => [k, String(v2)])
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
const v = params[name];
|
|
523
|
-
return v !== void 0 ? String(v) : "";
|
|
524
|
-
},
|
|
525
|
-
param: (_name) => void 0,
|
|
526
|
-
header: (_name) => void 0,
|
|
527
|
-
raw: new Request("http://localhost/cli")
|
|
528
|
-
},
|
|
529
|
-
json: (data) => data,
|
|
530
|
-
text: (data) => data,
|
|
531
|
-
body: (data) => data,
|
|
532
|
-
env: {},
|
|
533
|
-
var: {},
|
|
534
|
-
get: (_key) => void 0,
|
|
535
|
-
set: () => {
|
|
536
|
-
}
|
|
537
|
-
};
|
|
538
|
-
return stub;
|
|
539
|
-
}
|
|
540
|
-
async function runSqlDataSource(slug, def, params, limit) {
|
|
541
|
-
const start = Date.now();
|
|
542
|
-
try {
|
|
543
|
-
const client = await getClient(def.connectorSlug, def.connectorType);
|
|
544
|
-
const isExternal = def.connectorType === "snowflake" || def.connectorType === "bigquery";
|
|
545
|
-
let queryText;
|
|
546
|
-
let queryValues;
|
|
547
|
-
if (isExternal) {
|
|
548
|
-
const defaults = new Map(
|
|
549
|
-
(def.parameters ?? []).map((p) => [p.name, p.default ?? null])
|
|
550
|
-
);
|
|
551
|
-
queryText = def.query.replace(/\{\{(\w+)\}\}/g, (_match, name) => {
|
|
552
|
-
const value = Object.prototype.hasOwnProperty.call(params, name) ? params[name] : defaults.get(name) ?? "";
|
|
553
|
-
if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
|
|
554
|
-
if (value === null || value === void 0) return "NULL";
|
|
555
|
-
return String(value);
|
|
556
|
-
});
|
|
557
|
-
queryValues = [];
|
|
558
|
-
} else {
|
|
559
|
-
const built = buildQuery(def.query, def.parameters ?? [], params);
|
|
560
|
-
queryText = built.text;
|
|
561
|
-
queryValues = built.values;
|
|
562
|
-
}
|
|
563
|
-
const result = await client.query(queryText, queryValues);
|
|
564
|
-
const rows = result.rows.slice(0, limit);
|
|
565
|
-
return {
|
|
566
|
-
slug,
|
|
567
|
-
rows,
|
|
568
|
-
rowCount: result.rows.length,
|
|
569
|
-
durationMs: Date.now() - start,
|
|
570
|
-
query: queryText,
|
|
571
|
-
queryValues
|
|
572
|
-
};
|
|
573
|
-
} catch (error) {
|
|
574
|
-
return {
|
|
575
|
-
slug,
|
|
576
|
-
rows: [],
|
|
577
|
-
rowCount: 0,
|
|
578
|
-
durationMs: Date.now() - start,
|
|
579
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
async function runTypescriptDataSource(slug, handlerPath, params) {
|
|
584
|
-
const start = Date.now();
|
|
585
|
-
try {
|
|
586
|
-
const mod = await import(pathToFileURL(handlerPath).href);
|
|
587
|
-
const handler = mod.default;
|
|
588
|
-
if (typeof handler !== "function") {
|
|
589
|
-
throw new Error(`Handler must export a default function: ${handlerPath}`);
|
|
590
|
-
}
|
|
591
|
-
const ctx = createStubContext(params);
|
|
592
|
-
const raw = await handler(ctx);
|
|
593
|
-
let rows;
|
|
594
|
-
if (Array.isArray(raw)) {
|
|
595
|
-
rows = raw;
|
|
596
|
-
} else if (raw !== null && typeof raw === "object") {
|
|
597
|
-
rows = [raw];
|
|
598
|
-
} else {
|
|
599
|
-
rows = [{ result: raw }];
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
slug,
|
|
603
|
-
rows,
|
|
604
|
-
rowCount: rows.length,
|
|
605
|
-
durationMs: Date.now() - start
|
|
606
|
-
};
|
|
607
|
-
} catch (error) {
|
|
608
|
-
return {
|
|
609
|
-
slug,
|
|
610
|
-
rows: [],
|
|
611
|
-
rowCount: 0,
|
|
612
|
-
durationMs: Date.now() - start,
|
|
613
|
-
error: error instanceof Error ? error : new Error(String(error))
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
async function runDataSource(slug, dirPath, params, limit) {
|
|
618
|
-
const jsonPath = path3.join(dirPath, `${slug}.json`);
|
|
619
|
-
let def;
|
|
620
|
-
try {
|
|
621
|
-
const raw = await readFile2(jsonPath, "utf-8");
|
|
622
|
-
def = JSON.parse(raw);
|
|
623
|
-
} catch {
|
|
624
|
-
return {
|
|
625
|
-
slug,
|
|
626
|
-
rows: [],
|
|
627
|
-
rowCount: 0,
|
|
628
|
-
durationMs: 0,
|
|
629
|
-
error: new Error(`Data source not found: ${jsonPath}`)
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
if (def.type === "typescript") {
|
|
633
|
-
const absolutePath = path3.resolve(dirPath, def.handlerPath);
|
|
634
|
-
return runTypescriptDataSource(slug, absolutePath, params);
|
|
635
|
-
}
|
|
636
|
-
return runSqlDataSource(slug, def, params, limit);
|
|
637
|
-
}
|
|
638
|
-
async function listSlugs(dirPath) {
|
|
639
|
-
try {
|
|
640
|
-
const files = await readdir2(dirPath);
|
|
641
|
-
return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
|
|
642
|
-
} catch {
|
|
643
|
-
return [];
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
async function runAll(dirPath, params, limit) {
|
|
647
|
-
const slugs = await listSlugs(dirPath);
|
|
648
|
-
return Promise.all(slugs.map((slug) => runDataSource(slug, dirPath, params, limit)));
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// src/cli/display.ts
|
|
652
|
-
var RESET = "\x1B[0m";
|
|
653
|
-
var BOLD = "\x1B[1m";
|
|
654
|
-
var RED = "\x1B[31m";
|
|
655
|
-
var GREEN = "\x1B[32m";
|
|
656
|
-
var YELLOW = "\x1B[33m";
|
|
657
|
-
var CYAN = "\x1B[36m";
|
|
658
|
-
var DIM = "\x1B[2m";
|
|
659
|
-
function truncate(value, maxLen) {
|
|
660
|
-
return value.length > maxLen ? value.slice(0, maxLen - 1) + "\u2026" : value;
|
|
661
|
-
}
|
|
662
|
-
function formatValue(value) {
|
|
663
|
-
if (value === null || value === void 0) return DIM + "null" + RESET;
|
|
664
|
-
if (typeof value === "object") return JSON.stringify(value);
|
|
665
|
-
return String(value);
|
|
666
|
-
}
|
|
667
|
-
function displayTable(rows, limit) {
|
|
668
|
-
if (rows.length === 0) {
|
|
669
|
-
console.log(DIM + " (no rows)" + RESET);
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
const display = rows.slice(0, limit);
|
|
673
|
-
const columns = Object.keys(display[0] ?? {});
|
|
674
|
-
const colWidths = columns.map((col) => {
|
|
675
|
-
const maxData = Math.max(...display.map((r) => formatValue(r[col]).replace(/\x1b\[[0-9;]*m/g, "").length));
|
|
676
|
-
return Math.min(40, Math.max(col.length, maxData));
|
|
677
|
-
});
|
|
678
|
-
const header = columns.map((c, i) => BOLD + CYAN + c.padEnd(colWidths[i] ?? 0) + RESET).join(" ");
|
|
679
|
-
const divider = colWidths.map((w) => "-".repeat(w)).join("--");
|
|
680
|
-
console.log(" " + header);
|
|
681
|
-
console.log(" " + DIM + divider + RESET);
|
|
682
|
-
for (const row of display) {
|
|
683
|
-
const line = columns.map((c, i) => {
|
|
684
|
-
const raw = formatValue(row[c]);
|
|
685
|
-
const plain = raw.replace(/\x1b\[[0-9;]*m/g, "");
|
|
686
|
-
const padded = truncate(plain, colWidths[i] ?? 40).padEnd(colWidths[i] ?? 0);
|
|
687
|
-
return raw.startsWith(DIM) ? DIM + padded + RESET : padded;
|
|
688
|
-
}).join(" ");
|
|
689
|
-
console.log(" " + line);
|
|
690
|
-
}
|
|
691
|
-
if (rows.length > limit) {
|
|
692
|
-
console.log(DIM + ` \u2026 and ${rows.length - limit} more rows (use --limit to show more)` + RESET);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
function displaySummary(result) {
|
|
696
|
-
const status = result.error ? RED + "\u2717 FAIL" + RESET : GREEN + "\u2713 OK" + RESET;
|
|
697
|
-
const duration = DIM + `(${result.durationMs}ms)` + RESET;
|
|
698
|
-
console.log(`
|
|
699
|
-
${BOLD}[${result.slug}]${RESET} ${status} ${duration}`);
|
|
700
|
-
if (result.error) {
|
|
701
|
-
console.log(RED + ` Error: ${result.error.message}` + RESET);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
console.log(DIM + ` ${result.rowCount} row(s)` + RESET);
|
|
705
|
-
}
|
|
706
|
-
function displayDebug(result) {
|
|
707
|
-
if (result.query) {
|
|
708
|
-
console.log(YELLOW + " Query:" + RESET);
|
|
709
|
-
console.log(DIM + ` ${result.query.replace(/\n/g, "\n ")}` + RESET);
|
|
710
|
-
}
|
|
711
|
-
if (result.queryValues && result.queryValues.length > 0) {
|
|
712
|
-
console.log(YELLOW + " Params:" + RESET, result.queryValues);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
function displayJson(results) {
|
|
716
|
-
const output = results.map((r) => ({
|
|
717
|
-
slug: r.slug,
|
|
718
|
-
rowCount: r.rowCount,
|
|
719
|
-
durationMs: r.durationMs,
|
|
720
|
-
rows: r.rows,
|
|
721
|
-
error: r.error?.message
|
|
722
|
-
}));
|
|
723
|
-
console.log(JSON.stringify(output, null, 2));
|
|
724
|
-
}
|
|
725
|
-
function displayError(error) {
|
|
726
|
-
console.error(RED + "Error: " + error.message + RESET);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// src/cli/index.ts
|
|
730
|
-
var HELP = `
|
|
731
|
-
Usage: squadbase-ds-test [options]
|
|
732
|
-
|
|
733
|
-
Options:
|
|
734
|
-
--slug <slug> Run a specific data source
|
|
735
|
-
--all Run all data sources
|
|
736
|
-
--params k=v,... Comma-separated key=value parameters
|
|
737
|
-
--env <path> Path to .env file (default: ../../.env)
|
|
738
|
-
--dir <path> Data source directory (default: ./data-source)
|
|
739
|
-
--format table|json Output format (default: table)
|
|
740
|
-
--limit <n> Max rows to display (default: 50)
|
|
741
|
-
--debug Show SQL query and parameter values
|
|
742
|
-
--help Show this help
|
|
743
|
-
|
|
744
|
-
Examples:
|
|
745
|
-
npx tsx src/cli/index.ts --slug sales-summary
|
|
746
|
-
npx tsx src/cli/index.ts --slug sales-summary --params year=2024,limit=10
|
|
747
|
-
npx tsx src/cli/index.ts --all --format json
|
|
748
|
-
npx tsx src/cli/index.ts # interactive mode
|
|
749
|
-
`;
|
|
750
|
-
async function main() {
|
|
751
|
-
const { values } = parseArgs({
|
|
752
|
-
options: {
|
|
753
|
-
slug: { type: "string" },
|
|
754
|
-
all: { type: "boolean", default: false },
|
|
755
|
-
params: { type: "string" },
|
|
756
|
-
env: { type: "string" },
|
|
757
|
-
dir: { type: "string" },
|
|
758
|
-
format: { type: "string", default: "table" },
|
|
759
|
-
limit: { type: "string", default: "50" },
|
|
760
|
-
debug: { type: "boolean", default: false },
|
|
761
|
-
help: { type: "boolean", default: false }
|
|
762
|
-
},
|
|
763
|
-
allowPositionals: false
|
|
764
|
-
});
|
|
765
|
-
if (values.help) {
|
|
766
|
-
console.log(HELP);
|
|
767
|
-
process.exit(0);
|
|
768
|
-
}
|
|
769
|
-
const cwd = process.cwd();
|
|
770
|
-
const dirPath = values.dir ? path4.resolve(cwd, values.dir) : path4.join(cwd, "data-source");
|
|
771
|
-
const envPath = values.env ? path4.resolve(cwd, values.env) : path4.join(cwd, "../../.env");
|
|
772
|
-
const limit = parseInt(values.limit ?? "50", 10);
|
|
773
|
-
const format = values.format ?? "table";
|
|
774
|
-
loadEnvFile(envPath);
|
|
775
|
-
const params = {};
|
|
776
|
-
if (values.params) {
|
|
777
|
-
for (const pair of values.params.split(",")) {
|
|
778
|
-
const eqIdx = pair.indexOf("=");
|
|
779
|
-
if (eqIdx === -1) continue;
|
|
780
|
-
const key = pair.slice(0, eqIdx).trim();
|
|
781
|
-
const val = pair.slice(eqIdx + 1).trim();
|
|
782
|
-
params[key] = val;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
if (values.slug) {
|
|
786
|
-
const result = await runDataSource(values.slug, dirPath, params, limit);
|
|
787
|
-
if (format === "json") {
|
|
788
|
-
displayJson([result]);
|
|
789
|
-
} else {
|
|
790
|
-
displaySummary(result);
|
|
791
|
-
if (values.debug) displayDebug(result);
|
|
792
|
-
if (!result.error) displayTable(result.rows, limit);
|
|
793
|
-
}
|
|
794
|
-
if (result.error) process.exit(1);
|
|
795
|
-
} else if (values.all) {
|
|
796
|
-
const results = await runAll(dirPath, params, limit);
|
|
797
|
-
if (format === "json") {
|
|
798
|
-
displayJson(results);
|
|
799
|
-
} else {
|
|
800
|
-
for (const r of results) {
|
|
801
|
-
displaySummary(r);
|
|
802
|
-
if (values.debug) displayDebug(r);
|
|
803
|
-
if (!r.error) displayTable(r.rows, limit);
|
|
804
|
-
}
|
|
805
|
-
const failed = results.filter((r) => r.error).length;
|
|
806
|
-
console.log(`
|
|
807
|
-
Total: ${results.length}, Failed: ${failed}`);
|
|
808
|
-
}
|
|
809
|
-
const anyFailed = results.some((r) => r.error);
|
|
810
|
-
if (anyFailed) process.exit(1);
|
|
811
|
-
} else {
|
|
812
|
-
const slugs = await listSlugs(dirPath);
|
|
813
|
-
if (slugs.length === 0) {
|
|
814
|
-
displayError(new Error(`No data sources found in ${dirPath}`));
|
|
815
|
-
process.exit(1);
|
|
816
|
-
}
|
|
817
|
-
try {
|
|
818
|
-
const { selectDataSource: selectDataSource2, inputParameters: inputParameters2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
|
|
819
|
-
const slug = await selectDataSource2(slugs);
|
|
820
|
-
if (!slug) {
|
|
821
|
-
console.log("Cancelled.");
|
|
822
|
-
process.exit(0);
|
|
823
|
-
}
|
|
824
|
-
const jsonPath = path4.join(dirPath, `${slug}.json`);
|
|
825
|
-
let paramMeta = [];
|
|
826
|
-
try {
|
|
827
|
-
const raw = await readFile3(jsonPath, "utf-8");
|
|
828
|
-
const def = JSON.parse(raw);
|
|
829
|
-
paramMeta = def.parameters ?? [];
|
|
830
|
-
} catch {
|
|
831
|
-
}
|
|
832
|
-
const interactiveParams = await inputParameters2(paramMeta);
|
|
833
|
-
const merged = { ...interactiveParams, ...params };
|
|
834
|
-
const result = await runDataSource(slug, dirPath, merged, limit);
|
|
835
|
-
displaySummary(result);
|
|
836
|
-
if (values.debug) displayDebug(result);
|
|
837
|
-
if (!result.error) displayTable(result.rows, limit);
|
|
838
|
-
if (result.error) process.exit(1);
|
|
839
|
-
} catch (err) {
|
|
840
|
-
displayError(err instanceof Error ? err : new Error(String(err)));
|
|
841
|
-
process.exit(1);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
main().catch((err) => {
|
|
846
|
-
displayError(err instanceof Error ? err : new Error(String(err)));
|
|
847
|
-
process.exit(1);
|
|
848
|
-
});
|