@graphenedata/cli 0.0.4 → 0.0.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/cli.ts +7 -43
- package/dist/cli/cli.js +529 -293
- package/dist/docs/graphene.md +924 -63
- package/dist/ui/component-utilities/echarts.js +3 -1
- package/dist/ui/component-utilities/themeStores.ts +35 -7
- package/dist/ui/components/AreaChart.svelte +2 -1
- package/dist/ui/components/BarChart.svelte +2 -1
- package/dist/ui/components/BigValue.svelte +1 -1
- package/dist/ui/components/Chart.svelte +10 -1
- package/dist/ui/components/ECharts.svelte +2 -0
- package/dist/ui/components/LineChart.svelte +2 -1
- package/dist/ui/components/PieChart.svelte +1 -1
- package/dist/ui/components/QueryLoad.svelte +5 -6
- package/dist/ui/components/TableRow.svelte +1 -1
- package/dist/ui/components/_Table.svelte +2 -0
- package/dist/ui/internal/queryEngine.ts +16 -13
- package/dist/ui/internal/telemetry.ts +5 -3
- package/dist/ui/web.js +26 -11
- package/package.json +2 -1
- package/dist/docs/data_apps/components/charts/annotations.md +0 -673
- package/dist/docs/data_apps/components/charts/area-chart.md +0 -202
- package/dist/docs/data_apps/components/charts/bar-chart.md +0 -317
- package/dist/docs/data_apps/components/charts/box-plot.md +0 -190
- package/dist/docs/data_apps/components/charts/bubble-chart.md +0 -151
- package/dist/docs/data_apps/components/charts/calendar-heatmap.md +0 -112
- package/dist/docs/data_apps/components/charts/custom-echarts.md +0 -308
- package/dist/docs/data_apps/components/charts/echarts-options.md +0 -217
- package/dist/docs/data_apps/components/charts/funnel-chart.md +0 -106
- package/dist/docs/data_apps/components/charts/heatmap.md +0 -180
- package/dist/docs/data_apps/components/charts/histogram.md +0 -107
- package/dist/docs/data_apps/components/charts/line-chart.md +0 -265
- package/dist/docs/data_apps/components/charts/mixed-type-charts.md +0 -240
- package/dist/docs/data_apps/components/charts/sankey-diagram.md +0 -301
- package/dist/docs/data_apps/components/charts/scatter-plot.md +0 -134
- package/dist/docs/data_apps/components/charts/sparkline.md +0 -68
- package/dist/docs/data_apps/components/data/big-value.md +0 -153
- package/dist/docs/data_apps/components/data/delta.md +0 -89
- package/dist/docs/data_apps/components/data/table.md +0 -470
- package/dist/docs/data_apps/components/data/value.md +0 -97
- package/dist/docs/data_apps/components/inputs/button-group.md +0 -154
- package/dist/docs/data_apps/components/inputs/checkbox.md +0 -52
- package/dist/docs/data_apps/components/inputs/date-input.md +0 -131
- package/dist/docs/data_apps/components/inputs/date-range.md +0 -124
- package/dist/docs/data_apps/components/inputs/dimension-grid.md +0 -67
- package/dist/docs/data_apps/components/inputs/dropdown.md +0 -199
- package/dist/docs/data_apps/components/inputs/index.md +0 -3
- package/dist/docs/data_apps/components/inputs/slider.md +0 -126
- package/dist/docs/data_apps/components/inputs/text-input.md +0 -86
- package/dist/docs/data_apps/components/maps/area-map.md +0 -397
- package/dist/docs/data_apps/components/maps/base-map.md +0 -269
- package/dist/docs/data_apps/components/maps/bubble-map.md +0 -361
- package/dist/docs/data_apps/components/maps/point-map.md +0 -326
- package/dist/docs/data_apps/components/maps/us-map.md +0 -167
- package/dist/docs/data_apps/components/ui/accordion.md +0 -116
- package/dist/docs/data_apps/components/ui/alert.md +0 -37
- package/dist/docs/data_apps/components/ui/big-link.md +0 -19
- package/dist/docs/data_apps/components/ui/details.md +0 -58
- package/dist/docs/data_apps/components/ui/download-data.md +0 -41
- package/dist/docs/data_apps/components/ui/embed.md +0 -47
- package/dist/docs/data_apps/components/ui/grid.md +0 -45
- package/dist/docs/data_apps/components/ui/image.md +0 -61
- package/dist/docs/data_apps/components/ui/info.md +0 -47
- package/dist/docs/data_apps/components/ui/last-refreshed.md +0 -28
- package/dist/docs/data_apps/components/ui/link-button.md +0 -20
- package/dist/docs/data_apps/components/ui/link.md +0 -40
- package/dist/docs/data_apps/components/ui/modal.md +0 -57
- package/dist/docs/data_apps/components/ui/note.md +0 -32
- package/dist/docs/data_apps/components/ui/print-format-components.md +0 -85
- package/dist/docs/data_apps/components/ui/tabs.md +0 -122
package/dist/cli/cli.js
CHANGED
|
@@ -58,6 +58,15 @@ function walkExpression(root, fn, parent = null) {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
async function pollFor(fn, timeoutMs, interval) {
|
|
62
|
+
let end = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < end) {
|
|
64
|
+
let res = fn();
|
|
65
|
+
if (res) return res;
|
|
66
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
61
70
|
var init_util = __esm({
|
|
62
71
|
"../lang/util.ts"() {
|
|
63
72
|
}
|
|
@@ -147,17 +156,15 @@ function loadConfig(dir) {
|
|
|
147
156
|
} catch {
|
|
148
157
|
console.warn("No package.json found in current directory");
|
|
149
158
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
root: packageJsonObject.root || process.cwd()
|
|
155
|
-
});
|
|
159
|
+
let dialect = "duckdb";
|
|
160
|
+
if (packageJsonObject.bigquery) dialect = "bigquery";
|
|
161
|
+
else if (packageJsonObject.snowflake) dialect = "snowflake";
|
|
162
|
+
setConfig({ ...packageJsonObject, dialect, root: packageJsonObject.root || process.cwd() });
|
|
156
163
|
}
|
|
157
164
|
var config;
|
|
158
165
|
var init_config = __esm({
|
|
159
166
|
"../lang/config.ts"() {
|
|
160
|
-
config = { dialect: "duckdb" };
|
|
167
|
+
config = { dialect: "duckdb", root: "" };
|
|
161
168
|
}
|
|
162
169
|
});
|
|
163
170
|
|
|
@@ -521,10 +528,10 @@ function analyzeQueryTable(table2) {
|
|
|
521
528
|
if (table2.query) return;
|
|
522
529
|
let node = TABLE_NODE_MAP.get(table2);
|
|
523
530
|
let query = analyzeQuery(node.getChild("QueryStatement"));
|
|
524
|
-
if (!query)
|
|
531
|
+
if (!query) return;
|
|
525
532
|
table2.fields = query.fields.map((f) => ({ type: f.type, name: f.name, metadata: f.metadata }));
|
|
526
533
|
table2.query = query.malloyQuery;
|
|
527
|
-
if (typeof table2.query.structRef == "string") {
|
|
534
|
+
if (table2.query && typeof table2.query.structRef == "string") {
|
|
528
535
|
table2.query.structRef = lookupTable(table2.query.structRef, node);
|
|
529
536
|
}
|
|
530
537
|
}
|
|
@@ -534,9 +541,10 @@ function analyzeQuery(queryNode) {
|
|
|
534
541
|
let isAgg = false;
|
|
535
542
|
let subQuerySources = [];
|
|
536
543
|
if (!txt(queryNode)) return;
|
|
544
|
+
if (txt(queryNode).trim().toLowerCase() == "select 1") return { fields: [{ name: "col_0", type: "number", metadata: {}, e: { node: "numberLiteral", literal: "1", type: "number" } }], subQuerySources, rawSql: "select 1" };
|
|
537
545
|
let froms = queryNode.getChild("FromClause")?.getChildren("TablePrimary") || [];
|
|
538
546
|
if (froms.find((f) => f.name == "JoinClause")) diag(froms[0], "Query joins not yet supported");
|
|
539
|
-
if (froms.length == 0) diag(queryNode, "No tables in FROM clause");
|
|
547
|
+
if (froms.length == 0) return diag(queryNode, "No tables in FROM clause");
|
|
540
548
|
if (froms.length > 1) diag(froms[0], "Multiple tables/joins in FROM clause not yet supported");
|
|
541
549
|
if (froms[0].name == "Subquery") {
|
|
542
550
|
structRef = txt(froms[0].getChild("Alias")) || "subquery";
|
|
@@ -555,9 +563,9 @@ function analyzeQuery(queryNode) {
|
|
|
555
563
|
isAgg ||= !!isSelectDistinct;
|
|
556
564
|
selects.forEach((s) => {
|
|
557
565
|
if (s.getChild("Wildcard")) {
|
|
558
|
-
let
|
|
559
|
-
let pathStrings =
|
|
560
|
-
let target = followJoins(
|
|
566
|
+
let path9 = s.getChild("Wildcard").getChildren("Identifier");
|
|
567
|
+
let pathStrings = path9.map((p) => txt(p));
|
|
568
|
+
let target = followJoins(path9, scope.table);
|
|
561
569
|
if (!target) return;
|
|
562
570
|
target.fields.forEach((f) => {
|
|
563
571
|
if (isJoin(f) || f.isAgg) return;
|
|
@@ -655,8 +663,8 @@ function analyzeExpression(expr, scope) {
|
|
|
655
663
|
if (scope.outputFields.includes(field) && field.isAgg) {
|
|
656
664
|
return { node: "outputField", name: field.name, ...typeInfo, isAgg: field.isAgg };
|
|
657
665
|
}
|
|
658
|
-
let
|
|
659
|
-
return { node: "field", path:
|
|
666
|
+
let path9 = expr.getChildren("Identifier").map((i) => txt(i));
|
|
667
|
+
return { node: "field", path: path9, ...typeInfo, isAgg: field.isAgg };
|
|
660
668
|
}
|
|
661
669
|
case "ExtractExpression": {
|
|
662
670
|
let e = analyzeExpression(expr.getChild("Expression"), scope);
|
|
@@ -862,6 +870,10 @@ function convertDataType(dataType) {
|
|
|
862
870
|
return "number";
|
|
863
871
|
case "INT64":
|
|
864
872
|
return "number";
|
|
873
|
+
case "NUMBER":
|
|
874
|
+
return "number";
|
|
875
|
+
case "VARIANT":
|
|
876
|
+
return "string";
|
|
865
877
|
case "TEXT":
|
|
866
878
|
return "string";
|
|
867
879
|
case "STRING":
|
|
@@ -884,6 +896,8 @@ function convertDataType(dataType) {
|
|
|
884
896
|
return "timestamp";
|
|
885
897
|
case "TIMESTAMP":
|
|
886
898
|
return "timestamp";
|
|
899
|
+
case "TIMESTAMP_NTZ":
|
|
900
|
+
return "timestamp";
|
|
887
901
|
case "DECIMAL":
|
|
888
902
|
return "number";
|
|
889
903
|
case "DOUBLE":
|
|
@@ -1215,11 +1229,11 @@ async function loadWorkspace(dir, includeMd) {
|
|
|
1215
1229
|
updateFile(contents, file);
|
|
1216
1230
|
}
|
|
1217
1231
|
}
|
|
1218
|
-
function updateFile(contents,
|
|
1219
|
-
FILE_MAP[
|
|
1220
|
-
FILE_MAP[
|
|
1221
|
-
FILE_MAP[
|
|
1222
|
-
return FILE_MAP[
|
|
1232
|
+
function updateFile(contents, path9) {
|
|
1233
|
+
FILE_MAP[path9] ||= { path: path9, contents, tree: null, tables: [], queries: [] };
|
|
1234
|
+
FILE_MAP[path9].contents = contents;
|
|
1235
|
+
FILE_MAP[path9].tree = null;
|
|
1236
|
+
return FILE_MAP[path9];
|
|
1223
1237
|
}
|
|
1224
1238
|
function analyze(contents, type) {
|
|
1225
1239
|
clearDiagnostics();
|
|
@@ -1243,11 +1257,13 @@ function analyze(contents, type) {
|
|
|
1243
1257
|
}
|
|
1244
1258
|
}
|
|
1245
1259
|
function toSql(query, params = {}) {
|
|
1260
|
+
if (query.rawSql) return query.rawSql;
|
|
1246
1261
|
let contents = {};
|
|
1247
1262
|
let gsqlTables = Object.values(FILE_MAP).filter((f) => f.path !== "input").flatMap((f) => f.tables);
|
|
1248
1263
|
gsqlTables.forEach((t) => contents[t.name] = t);
|
|
1249
1264
|
let inputTables = [...FILE_MAP["input"]?.tables || [], ...query.subQuerySources];
|
|
1250
1265
|
inputTables.forEach((t) => contents[t.name] = { ...t, query: t.query && fillInParams(t.query, params) });
|
|
1266
|
+
if (!query.malloyQuery) throw new Error("Cannot compile query without Malloy query");
|
|
1251
1267
|
let malloyQuery = fillInParams(query.malloyQuery, params);
|
|
1252
1268
|
let qm = new QueryModel({
|
|
1253
1269
|
name: "generated_model",
|
|
@@ -1281,6 +1297,172 @@ var init_core = __esm({
|
|
|
1281
1297
|
}
|
|
1282
1298
|
});
|
|
1283
1299
|
|
|
1300
|
+
// printer.ts
|
|
1301
|
+
import { styleText as nodeStyleText } from "node:util";
|
|
1302
|
+
import Table from "cli-table3";
|
|
1303
|
+
import chalk from "chalk";
|
|
1304
|
+
function offsetToLineCol(src, offset2) {
|
|
1305
|
+
let lines = src.split(/\r?\n/);
|
|
1306
|
+
let acc = 0;
|
|
1307
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1308
|
+
let lineText = lines[i];
|
|
1309
|
+
let nextAcc = acc + lineText.length + 1;
|
|
1310
|
+
if (offset2 < nextAcc || i === lines.length - 1) {
|
|
1311
|
+
let col = Math.max(0, offset2 - acc);
|
|
1312
|
+
return { line: i + 1, col, lineStart: acc, lineText };
|
|
1313
|
+
}
|
|
1314
|
+
acc = nextAcc;
|
|
1315
|
+
}
|
|
1316
|
+
return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
|
|
1317
|
+
}
|
|
1318
|
+
function printDiagnostics(diags, log) {
|
|
1319
|
+
log ||= console.log;
|
|
1320
|
+
let parts = [];
|
|
1321
|
+
for (let d of diags) {
|
|
1322
|
+
let src = getFile2(d.file)?.contents || "";
|
|
1323
|
+
let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
|
|
1324
|
+
let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
|
|
1325
|
+
let caretLen = Math.max(1, endCol - col);
|
|
1326
|
+
let sev = d.severity === "error" ? "red" : "yellow";
|
|
1327
|
+
let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
|
|
1328
|
+
let gutter = " | ";
|
|
1329
|
+
let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
|
|
1330
|
+
parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
|
|
1331
|
+
}
|
|
1332
|
+
if (parts.length) log(parts.join("\n"));
|
|
1333
|
+
}
|
|
1334
|
+
function printTable(rows) {
|
|
1335
|
+
if (!rows || rows.length === 0) {
|
|
1336
|
+
console.log(chalk.yellow("No results returned"));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
let headers = Object.keys(rows[0]);
|
|
1340
|
+
let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
|
|
1341
|
+
let MAX_DISPLAY_ROWS = 200;
|
|
1342
|
+
let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
|
|
1343
|
+
displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
|
|
1344
|
+
console.log(table2.toString());
|
|
1345
|
+
if (rows.length > MAX_DISPLAY_ROWS) {
|
|
1346
|
+
console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
var styleText;
|
|
1350
|
+
var init_printer = __esm({
|
|
1351
|
+
"printer.ts"() {
|
|
1352
|
+
init_core();
|
|
1353
|
+
styleText = (style, text) => {
|
|
1354
|
+
try {
|
|
1355
|
+
return nodeStyleText ? nodeStyleText(style, text) : text;
|
|
1356
|
+
} catch {
|
|
1357
|
+
return text;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
// background.ts
|
|
1364
|
+
import { spawn } from "child_process";
|
|
1365
|
+
import { fileURLToPath } from "url";
|
|
1366
|
+
import fs2 from "fs-extra";
|
|
1367
|
+
import path3 from "path";
|
|
1368
|
+
async function runServeInBackground() {
|
|
1369
|
+
let root = process.cwd();
|
|
1370
|
+
let grapheneCache = getGrapheneCache(root);
|
|
1371
|
+
let logFile = path3.join(grapheneCache, "serve.log");
|
|
1372
|
+
await fs2.ensureDir(grapheneCache);
|
|
1373
|
+
await stopGrapheneIfRunning(root);
|
|
1374
|
+
let log = fs2.openSync(logFile, "w");
|
|
1375
|
+
let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
|
|
1376
|
+
let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
|
|
1377
|
+
let child = spawn(process.execPath, childArgs, {
|
|
1378
|
+
cwd: root,
|
|
1379
|
+
detached: true,
|
|
1380
|
+
env: { ...process.env },
|
|
1381
|
+
stdio: ["ignore", log, log]
|
|
1382
|
+
});
|
|
1383
|
+
if (!child.pid) throw new Error("Failed to start server process");
|
|
1384
|
+
await new Promise((resolve, reject) => {
|
|
1385
|
+
let buffer = "";
|
|
1386
|
+
fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
|
|
1387
|
+
if (curr.size > prev.size) {
|
|
1388
|
+
let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
|
|
1389
|
+
stream.on("data", (d) => {
|
|
1390
|
+
process.stdout.write(d);
|
|
1391
|
+
buffer = (buffer + d.toString()).slice(-200);
|
|
1392
|
+
if (buffer.includes("Server running at http://localhost:")) resolve();
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
child.once("exit", () => {
|
|
1397
|
+
process.stdout.write(fs2.readFileSync(logFile));
|
|
1398
|
+
reject(new Error("Exited before server started"));
|
|
1399
|
+
});
|
|
1400
|
+
child.once("error", (e) => reject(e));
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function getGrapheneCache(root) {
|
|
1404
|
+
return path3.join(root, "node_modules", ".graphene");
|
|
1405
|
+
}
|
|
1406
|
+
function getPidFilePath(root) {
|
|
1407
|
+
return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
|
|
1408
|
+
}
|
|
1409
|
+
function targetPids(pid) {
|
|
1410
|
+
if (process.platform === "win32") return [pid];
|
|
1411
|
+
return [pid, -pid];
|
|
1412
|
+
}
|
|
1413
|
+
function sendSignal(pid, signal) {
|
|
1414
|
+
for (let target of targetPids(pid)) {
|
|
1415
|
+
try {
|
|
1416
|
+
process.kill(target, signal);
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
let code = err.code;
|
|
1419
|
+
if (code === "ESRCH" || code === "EINVAL") continue;
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
async function stopGrapheneIfRunning(root) {
|
|
1426
|
+
if (!await isServerRunning()) return;
|
|
1427
|
+
let pidFile = getPidFilePath(root);
|
|
1428
|
+
let pid = await readPid(pidFile);
|
|
1429
|
+
if (!pid) return;
|
|
1430
|
+
console.log(`Stopping server (${pid})`);
|
|
1431
|
+
sendSignal(pid, "SIGTERM");
|
|
1432
|
+
let end = Date.now() + 5e3;
|
|
1433
|
+
while (Date.now() < end && isServerRunning()) {
|
|
1434
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1435
|
+
}
|
|
1436
|
+
if (!isServerRunning()) return;
|
|
1437
|
+
sendSignal(pid, "SIGKILL");
|
|
1438
|
+
await fs2.remove(pidFile);
|
|
1439
|
+
}
|
|
1440
|
+
async function readPid(pidFile) {
|
|
1441
|
+
if (!await fs2.pathExists(pidFile)) return void 0;
|
|
1442
|
+
let contents = (await fs2.readFile(pidFile, "utf8")).trim();
|
|
1443
|
+
if (!contents) return void 0;
|
|
1444
|
+
let pid = Number.parseInt(contents, 10);
|
|
1445
|
+
if (Number.isNaN(pid)) return void 0;
|
|
1446
|
+
return pid;
|
|
1447
|
+
}
|
|
1448
|
+
async function isServerRunning() {
|
|
1449
|
+
let pidFile = getPidFilePath(config.root);
|
|
1450
|
+
let pid = await readPid(pidFile);
|
|
1451
|
+
if (!pid) return false;
|
|
1452
|
+
try {
|
|
1453
|
+
process.kill(pid, 0);
|
|
1454
|
+
return true;
|
|
1455
|
+
} catch {
|
|
1456
|
+
fs2.removeSync(pidFile);
|
|
1457
|
+
return false;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
var init_background = __esm({
|
|
1461
|
+
"background.ts"() {
|
|
1462
|
+
init_config();
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1284
1466
|
// connections/bigQuery.ts
|
|
1285
1467
|
var bigQuery_exports = {};
|
|
1286
1468
|
__export(bigQuery_exports, {
|
|
@@ -1294,16 +1476,14 @@ var init_bigQuery = __esm({
|
|
|
1294
1476
|
BigQueryConnection = class {
|
|
1295
1477
|
client;
|
|
1296
1478
|
constructor(options = {}) {
|
|
1479
|
+
options.projectId ||= config.bigquery?.projectId;
|
|
1297
1480
|
if (process.env.GOOGLE_CREDENTIALS_CONTENT) {
|
|
1298
1481
|
let parsed = JSON.parse(process.env.GOOGLE_CREDENTIALS_CONTENT);
|
|
1299
1482
|
options.projectId = parsed.project_id;
|
|
1300
1483
|
options.credentials = parsed;
|
|
1301
1484
|
}
|
|
1302
|
-
options.projectId
|
|
1303
|
-
|
|
1304
|
-
options.userAgent ||= "Graphene";
|
|
1305
|
-
if (!options.projectId) throw new Error("googleProjectId must be set in config or provided in service account credentials");
|
|
1306
|
-
this.client = new BigQuery(options);
|
|
1485
|
+
if (!options.projectId) throw new Error("projectId must be set in config or provided in service account credentials");
|
|
1486
|
+
this.client = new BigQuery({ ...options, userAgent: "Graphene" });
|
|
1307
1487
|
}
|
|
1308
1488
|
async runQuery(sql) {
|
|
1309
1489
|
let [job] = await this.client.createQueryJob({ query: sql, useLegacySql: false });
|
|
@@ -1335,19 +1515,24 @@ var init_duckdb = __esm({
|
|
|
1335
1515
|
"connections/duckdb.ts"() {
|
|
1336
1516
|
init_config();
|
|
1337
1517
|
DuckDBConnection = class {
|
|
1518
|
+
options;
|
|
1338
1519
|
ready;
|
|
1339
1520
|
connection = null;
|
|
1340
|
-
constructor() {
|
|
1521
|
+
constructor(options) {
|
|
1522
|
+
this.options = options || {};
|
|
1341
1523
|
this.ready = this.initialize();
|
|
1342
1524
|
}
|
|
1343
1525
|
async initialize() {
|
|
1344
|
-
let
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1526
|
+
let dbPath = this.options.path;
|
|
1527
|
+
if (!dbPath) {
|
|
1528
|
+
let files = await fs3.readdir(config.root);
|
|
1529
|
+
dbPath = files.find((f) => f.endsWith(".duckdb"));
|
|
1530
|
+
if (!dbPath) throw new Error("No .duckdb file found in current directory");
|
|
1531
|
+
dbPath = path4.resolve(config.root, dbPath);
|
|
1532
|
+
}
|
|
1348
1533
|
let db = await DuckDBInstance.create(":memory:");
|
|
1349
1534
|
this.connection = await db.connect();
|
|
1350
|
-
let escapedPath =
|
|
1535
|
+
let escapedPath = dbPath.replace(/'/g, "''");
|
|
1351
1536
|
await this.connection.run(`attach '${escapedPath}' as graphene_cli (READ_ONLY);`);
|
|
1352
1537
|
await this.connection.run("use graphene_cli;");
|
|
1353
1538
|
}
|
|
@@ -1372,6 +1557,76 @@ var init_duckdb = __esm({
|
|
|
1372
1557
|
}
|
|
1373
1558
|
});
|
|
1374
1559
|
|
|
1560
|
+
// connections/snowflake.ts
|
|
1561
|
+
var snowflake_exports = {};
|
|
1562
|
+
__export(snowflake_exports, {
|
|
1563
|
+
SnowflakeConnection: () => SnowflakeConnection
|
|
1564
|
+
});
|
|
1565
|
+
import { createPrivateKey } from "node:crypto";
|
|
1566
|
+
import snowflake from "snowflake-sdk";
|
|
1567
|
+
var SnowflakeConnection;
|
|
1568
|
+
var init_snowflake = __esm({
|
|
1569
|
+
"connections/snowflake.ts"() {
|
|
1570
|
+
init_config();
|
|
1571
|
+
SnowflakeConnection = class {
|
|
1572
|
+
ready;
|
|
1573
|
+
connection;
|
|
1574
|
+
constructor(opts) {
|
|
1575
|
+
this.ready = this.initialize(opts || {});
|
|
1576
|
+
}
|
|
1577
|
+
async initialize(opts) {
|
|
1578
|
+
let privateKeyPath = process.env.SNOWFLAKE_PRI_KEY_PATH || config.snowflake?.privateKeyPath;
|
|
1579
|
+
let privateKeyPass = process.env.SNOWFLAKE_PRI_PASSPHRASE;
|
|
1580
|
+
let authOptions = {};
|
|
1581
|
+
if (privateKeyPath) {
|
|
1582
|
+
authOptions = { privateKeyPath, privateKeyPass };
|
|
1583
|
+
} else if (opts.privateKey) {
|
|
1584
|
+
let privateKey = createPrivateKey({ key: opts.privateKey, format: "pem", passphrase: privateKeyPass });
|
|
1585
|
+
authOptions = { privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) };
|
|
1586
|
+
}
|
|
1587
|
+
snowflake.configure({ logLevel: process.env.SNOWFLAKE_LOG_LEVEL || "WARN", logFilePath: "/dev/null" });
|
|
1588
|
+
this.connection = snowflake.createConnection({
|
|
1589
|
+
...opts,
|
|
1590
|
+
...config.snowflake || {},
|
|
1591
|
+
...authOptions,
|
|
1592
|
+
authenticator: "SNOWFLAKE_JWT",
|
|
1593
|
+
application: "Graphene"
|
|
1594
|
+
});
|
|
1595
|
+
await new Promise((resolve, reject) => {
|
|
1596
|
+
this.connection.connect((err, conn) => err ? reject(err) : resolve(conn));
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
async runQuery(sql) {
|
|
1600
|
+
await this.ready;
|
|
1601
|
+
return await new Promise((resolve, reject) => {
|
|
1602
|
+
let rows = [];
|
|
1603
|
+
this.connection.execute({
|
|
1604
|
+
sqlText: sql,
|
|
1605
|
+
streamResult: true,
|
|
1606
|
+
complete: (error, statement) => {
|
|
1607
|
+
if (error) {
|
|
1608
|
+
reject(new Error(`Snowflake query failed: ${error.message || error}`));
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
let stream = statement.streamRows();
|
|
1612
|
+
stream.on("error", (err) => reject(err));
|
|
1613
|
+
stream.on("readable", function(row) {
|
|
1614
|
+
while ((row = this.read()) !== null) {
|
|
1615
|
+
rows.push(row);
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
stream.on("end", () => {
|
|
1619
|
+
let totalRows = Number(statement.getNumRows());
|
|
1620
|
+
resolve({ rows, totalRows });
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1375
1630
|
// connections/index.ts
|
|
1376
1631
|
async function getConnection() {
|
|
1377
1632
|
if (config.dialect === "bigquery") {
|
|
@@ -1379,7 +1634,10 @@ async function getConnection() {
|
|
|
1379
1634
|
return new mod.BigQueryConnection();
|
|
1380
1635
|
} else if (config.dialect === "duckdb") {
|
|
1381
1636
|
let mod = await Promise.resolve().then(() => (init_duckdb(), duckdb_exports));
|
|
1382
|
-
return new mod.DuckDBConnection();
|
|
1637
|
+
return new mod.DuckDBConnection({});
|
|
1638
|
+
} else if (config.dialect === "snowflake") {
|
|
1639
|
+
let mod = await Promise.resolve().then(() => (init_snowflake(), snowflake_exports));
|
|
1640
|
+
return new mod.SnowflakeConnection({});
|
|
1383
1641
|
} else {
|
|
1384
1642
|
throw new Error(`Unsupported dialect: ${config.dialect}`);
|
|
1385
1643
|
}
|
|
@@ -1390,9 +1648,204 @@ var init_connections = __esm({
|
|
|
1390
1648
|
}
|
|
1391
1649
|
});
|
|
1392
1650
|
|
|
1393
|
-
//
|
|
1394
|
-
|
|
1651
|
+
// mockFiles.ts
|
|
1652
|
+
var mockFileMap;
|
|
1653
|
+
var init_mockFiles = __esm({
|
|
1654
|
+
"mockFiles.ts"() {
|
|
1655
|
+
mockFileMap = {};
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
// check.ts
|
|
1660
|
+
import fs4 from "fs-extra";
|
|
1661
|
+
import os from "os";
|
|
1395
1662
|
import path5 from "path";
|
|
1663
|
+
import { spawn as spawn2 } from "child_process";
|
|
1664
|
+
import { WebSocketServer } from "ws";
|
|
1665
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1666
|
+
import { styleText as styleText2 } from "node:util";
|
|
1667
|
+
async function check(options) {
|
|
1668
|
+
let log = options.log || console.log;
|
|
1669
|
+
let mdFile = options.mdArg && normalizeMdFile(options.mdArg);
|
|
1670
|
+
if (options.mdArg && !mdFile) {
|
|
1671
|
+
log(`Couldn't find ${options.mdArg}`);
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
await loadWorkspace(config.root, !mdFile);
|
|
1675
|
+
if (mdFile) {
|
|
1676
|
+
if (process.env.NODE_ENV == "test" && mockFileMap[mdFile]) {
|
|
1677
|
+
updateFile(mockFileMap[mdFile], mdFile);
|
|
1678
|
+
} else {
|
|
1679
|
+
let content = readFileSync2(path5.resolve(config.root, mdFile), "utf-8");
|
|
1680
|
+
updateFile(content, mdFile);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
analyze();
|
|
1684
|
+
if (getDiagnostics().length > 0) {
|
|
1685
|
+
printDiagnostics(getDiagnostics(), log);
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
if (!mdFile) {
|
|
1689
|
+
log("No errors found \u{1F48E}");
|
|
1690
|
+
return true;
|
|
1691
|
+
}
|
|
1692
|
+
let host = `http://localhost:${config.port || Number(process.env.GRAPHENE_PORT) || 4e3}`;
|
|
1693
|
+
let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "").replace(/\\/g, "/");
|
|
1694
|
+
if (pageUrl === "/index") pageUrl = "/";
|
|
1695
|
+
if (process.env.NODE_ENV !== "test" && !await isServerRunning()) {
|
|
1696
|
+
log("Starting Graphene server...");
|
|
1697
|
+
await runServeInBackground();
|
|
1698
|
+
}
|
|
1699
|
+
let resp = await sendCheckRequest({ host, pageUrl, chart: options.chart });
|
|
1700
|
+
if (resp.checkError == "no_server") {
|
|
1701
|
+
log("Failed to start Graphene server");
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
if (resp.checkError == "no_tab" && process.env.NODE_ENV !== "test") {
|
|
1705
|
+
log(`Opening page ${host}${pageUrl}`);
|
|
1706
|
+
spawn2("open", [host + pageUrl]);
|
|
1707
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1708
|
+
resp = await sendCheckRequest({ host, pageUrl, chart: options.chart });
|
|
1709
|
+
}
|
|
1710
|
+
if (resp.checkError == "no_tab") {
|
|
1711
|
+
log("Failed to open a new tab");
|
|
1712
|
+
return false;
|
|
1713
|
+
}
|
|
1714
|
+
if (resp.checkError) {
|
|
1715
|
+
log("Failed to run check: " + resp.checkError);
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
let errors = Array.from(resp.errors || []);
|
|
1719
|
+
if (errors.length) {
|
|
1720
|
+
log(styleText2("red", "Runtime errors") + ` in ${mdFile}:`);
|
|
1721
|
+
} else {
|
|
1722
|
+
log("No errors found \u{1F48E}");
|
|
1723
|
+
}
|
|
1724
|
+
errors.forEach((e) => {
|
|
1725
|
+
if (e.file && e.line) printDiagnostics([e], log);
|
|
1726
|
+
else if (e.id) log(`${e.id}: ${e.message}`);
|
|
1727
|
+
else log(e.message);
|
|
1728
|
+
});
|
|
1729
|
+
if (resp?.stillLoading) {
|
|
1730
|
+
log("Warning: Queries were still loading when the screenshot was taken");
|
|
1731
|
+
}
|
|
1732
|
+
if (resp?.screenshot) {
|
|
1733
|
+
let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
|
|
1734
|
+
let screenshotPath = path5.join(os.tmpdir(), filename);
|
|
1735
|
+
let base64Data = resp.screenshot.replace(/^data:image\/png;base64,/, "");
|
|
1736
|
+
await fs4.writeFile(screenshotPath, base64Data, "base64");
|
|
1737
|
+
log("Screenshot saved to", screenshotPath);
|
|
1738
|
+
}
|
|
1739
|
+
return !!errors.length;
|
|
1740
|
+
}
|
|
1741
|
+
async function sendCheckRequest({ host, pageUrl, chart }) {
|
|
1742
|
+
let abort = new AbortController();
|
|
1743
|
+
let timeout = setTimeout(() => abort.abort(), 3e4);
|
|
1744
|
+
try {
|
|
1745
|
+
let response = await fetch(`${host}/_api/check`, {
|
|
1746
|
+
method: "POST",
|
|
1747
|
+
headers: { "Content-Type": "application/json" },
|
|
1748
|
+
body: JSON.stringify({ pageUrl: host + pageUrl, chart }),
|
|
1749
|
+
signal: abort.signal
|
|
1750
|
+
});
|
|
1751
|
+
clearTimeout(timeout);
|
|
1752
|
+
let body = response.headers.get("content-type") == "application/json" ? await response.json() : { error: await response.text() };
|
|
1753
|
+
if (!response.ok) {
|
|
1754
|
+
if (body.error) return { checkError: body.error };
|
|
1755
|
+
console.error(`Unexpected response: ${JSON.stringify(body)}`);
|
|
1756
|
+
return { checkError: "Unexpected response from Graphene server" };
|
|
1757
|
+
}
|
|
1758
|
+
return body;
|
|
1759
|
+
} catch (err) {
|
|
1760
|
+
clearTimeout(timeout);
|
|
1761
|
+
if (err.name === "AbortError") return { checkError: "timeout" };
|
|
1762
|
+
return { checkError: "no_server" };
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function normalizeMdFile(mdFile) {
|
|
1766
|
+
let clean = mdFile.trim();
|
|
1767
|
+
if (!clean) return null;
|
|
1768
|
+
if (!clean.endsWith(".md")) clean = clean + ".md";
|
|
1769
|
+
if (process.env.NODE_ENV == "test" && mockFileMap[clean]) {
|
|
1770
|
+
return clean;
|
|
1771
|
+
}
|
|
1772
|
+
let absolute = [
|
|
1773
|
+
path5.resolve(process.cwd(), clean),
|
|
1774
|
+
path5.resolve(config.root, clean)
|
|
1775
|
+
].find((p) => fs4.existsSync(p)) || null;
|
|
1776
|
+
if (!absolute) return null;
|
|
1777
|
+
let relative = path5.relative(config.root, absolute);
|
|
1778
|
+
return relative;
|
|
1779
|
+
}
|
|
1780
|
+
async function proxyCheckRequest(req, res) {
|
|
1781
|
+
let chunks = [];
|
|
1782
|
+
for await (let chunk of req) chunks.push(chunk);
|
|
1783
|
+
let { pageUrl, chart } = JSON.parse(Buffer.concat(chunks).toString());
|
|
1784
|
+
let id = Math.random().toString(36).slice(2);
|
|
1785
|
+
res.setHeader("Content-Type", "application/json");
|
|
1786
|
+
let normalizedPageUrl = pageUrl.replace(/\/$/, "");
|
|
1787
|
+
let conn = await pollFor(() => browserConnections.find((conn2) => conn2.url === normalizedPageUrl), 5e3, 100);
|
|
1788
|
+
if (!conn) {
|
|
1789
|
+
res.statusCode = 400;
|
|
1790
|
+
res.end(JSON.stringify({ error: "no_tab" }));
|
|
1791
|
+
return;
|
|
1792
|
+
} else {
|
|
1793
|
+
conn.socket.send(JSON.stringify({ type: "check", chart, requestId: id }));
|
|
1794
|
+
pendingRequests[id] = { response: res };
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
function checkVitePlugin() {
|
|
1798
|
+
return {
|
|
1799
|
+
name: "graphene-check-plugin",
|
|
1800
|
+
configureServer(server) {
|
|
1801
|
+
let wss = new WebSocketServer({ noServer: true });
|
|
1802
|
+
server.httpServer?.on("upgrade", (req, socket, head) => {
|
|
1803
|
+
if (!req.url || !req.url.includes("/_api/ws") && !req.url.includes("graphene-ws")) return;
|
|
1804
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1805
|
+
wss.emit("connection", ws, req);
|
|
1806
|
+
});
|
|
1807
|
+
});
|
|
1808
|
+
wss.on("connection", (socket) => {
|
|
1809
|
+
socket.on("message", (data) => {
|
|
1810
|
+
let message = JSON.parse(data.toString());
|
|
1811
|
+
if (message.type === "register") {
|
|
1812
|
+
let normalizedUrl = message.url.replace(/\/$/, "");
|
|
1813
|
+
browserConnections.push({ url: normalizedUrl, socket });
|
|
1814
|
+
}
|
|
1815
|
+
if (message.type === "checkResponse") {
|
|
1816
|
+
pendingRequests[message.requestId].response.end(JSON.stringify(message));
|
|
1817
|
+
delete pendingRequests[message.requestId];
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
socket.on("close", () => {
|
|
1821
|
+
browserConnections = browserConnections.filter((conn) => conn.socket !== socket);
|
|
1822
|
+
});
|
|
1823
|
+
});
|
|
1824
|
+
server.httpServer?.on("close", () => wss.close());
|
|
1825
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1826
|
+
let [pathName] = (req.url || "").split("?");
|
|
1827
|
+
if (pathName === "/_api/check") await proxyCheckRequest(req, res);
|
|
1828
|
+
else next();
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
var browserConnections, pendingRequests;
|
|
1834
|
+
var init_check = __esm({
|
|
1835
|
+
"check.ts"() {
|
|
1836
|
+
init_core();
|
|
1837
|
+
init_printer();
|
|
1838
|
+
init_mockFiles();
|
|
1839
|
+
init_background();
|
|
1840
|
+
init_util();
|
|
1841
|
+
browserConnections = [];
|
|
1842
|
+
pendingRequests = {};
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
// mdCompile.ts
|
|
1847
|
+
import fs5 from "fs";
|
|
1848
|
+
import path6 from "path";
|
|
1396
1849
|
import { visit } from "unist-util-visit";
|
|
1397
1850
|
import sanitizeHtml from "sanitize-html";
|
|
1398
1851
|
function extractQueries() {
|
|
@@ -1469,8 +1922,8 @@ ${content}`;
|
|
|
1469
1922
|
}
|
|
1470
1923
|
function componentNames() {
|
|
1471
1924
|
if (cachedComponentNames) return cachedComponentNames;
|
|
1472
|
-
let files =
|
|
1473
|
-
cachedComponentNames = files.map((f) =>
|
|
1925
|
+
let files = fs5.readdirSync(path6.join(import.meta.dirname, "../ui/components"));
|
|
1926
|
+
cachedComponentNames = files.map((f) => path6.basename(f, ".svelte")).filter((f) => !f.startsWith("_"));
|
|
1474
1927
|
return cachedComponentNames || [];
|
|
1475
1928
|
}
|
|
1476
1929
|
var cachedComponentNames;
|
|
@@ -1483,23 +1936,20 @@ var init_mdCompile = __esm({
|
|
|
1483
1936
|
// serve2.ts
|
|
1484
1937
|
var serve2_exports = {};
|
|
1485
1938
|
__export(serve2_exports, {
|
|
1486
|
-
mockFileMap: () => mockFileMap,
|
|
1487
1939
|
serve2: () => serve2
|
|
1488
1940
|
});
|
|
1489
1941
|
import { createServer, optimizeDeps } from "vite";
|
|
1490
1942
|
import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|
1491
|
-
import
|
|
1943
|
+
import fs6 from "fs-extra";
|
|
1492
1944
|
import crypto from "crypto";
|
|
1493
1945
|
import { mdsvex } from "mdsvex";
|
|
1494
|
-
import
|
|
1946
|
+
import path7 from "path";
|
|
1495
1947
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1496
|
-
import { WebSocketServer } from "ws";
|
|
1497
|
-
import { spawn as spawn2 } from "child_process";
|
|
1498
1948
|
async function serve2() {
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
await
|
|
1502
|
-
await
|
|
1949
|
+
uiRoot = path7.join(fileURLToPath2(import.meta.url), "../../ui");
|
|
1950
|
+
let port = Number(process.env.GRAPHENE_PORT) || 4e3;
|
|
1951
|
+
await fs6.ensureDir(path7.resolve(config.root, "node_modules/.graphene"));
|
|
1952
|
+
await fs6.writeFile(path7.resolve(config.root, `node_modules/.graphene/${process.env.NODE_ENV == "test" ? "test" : "serve"}.pid`), String(process.pid));
|
|
1503
1953
|
let server = await createServer({
|
|
1504
1954
|
root: config.root,
|
|
1505
1955
|
plugins: [
|
|
@@ -1515,24 +1965,32 @@ async function serve2() {
|
|
|
1515
1965
|
injectComponentImports()
|
|
1516
1966
|
]
|
|
1517
1967
|
}),
|
|
1968
|
+
checkVitePlugin(),
|
|
1518
1969
|
handleRequestPlugin,
|
|
1519
1970
|
updateWorkspacePlugin,
|
|
1520
1971
|
mockFilesForTests()
|
|
1521
1972
|
],
|
|
1973
|
+
publicDir: path7.resolve(uiRoot),
|
|
1522
1974
|
server: {
|
|
1523
|
-
port
|
|
1975
|
+
port,
|
|
1524
1976
|
fs: { strict: false },
|
|
1525
1977
|
strictPort: true
|
|
1526
1978
|
},
|
|
1527
1979
|
resolve: {
|
|
1528
1980
|
alias: {
|
|
1529
|
-
graphene:
|
|
1981
|
+
graphene: path7.resolve(uiRoot, "web.js")
|
|
1530
1982
|
}
|
|
1531
1983
|
}
|
|
1984
|
+
// optimizeDeps: { // this seems prudent in tests, but currently breaks because ssf needs to be optimized, even in tests
|
|
1985
|
+
// noDiscovery: process.env.NODE_ENV == 'test',
|
|
1986
|
+
// include: process.env.NODE_ENV == 'test' ? [] : undefined,
|
|
1987
|
+
// },
|
|
1532
1988
|
});
|
|
1533
1989
|
await optimizeDeps(server.config);
|
|
1534
1990
|
await server.listen();
|
|
1535
|
-
|
|
1991
|
+
if (process.env.NODE_ENV !== "test") {
|
|
1992
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
1993
|
+
}
|
|
1536
1994
|
return server;
|
|
1537
1995
|
}
|
|
1538
1996
|
async function handleQuery(req, res) {
|
|
@@ -1562,31 +2020,6 @@ async function handleQuery(req, res) {
|
|
|
1562
2020
|
let fields = queries[0].fields.map((f) => ({ name: f.name, type: f.type }));
|
|
1563
2021
|
res.end(JSON.stringify({ rows: queryResults.rows, hash, fields, sql }));
|
|
1564
2022
|
}
|
|
1565
|
-
async function handleView(req, res) {
|
|
1566
|
-
let chunks = [];
|
|
1567
|
-
for await (let chunk of req) chunks.push(chunk);
|
|
1568
|
-
let { mdFile, chart } = JSON.parse(Buffer.concat(chunks).toString());
|
|
1569
|
-
let id = Math.random().toString(36).slice(2);
|
|
1570
|
-
res.setHeader("Content-Type", "application/json");
|
|
1571
|
-
viewRequests[id] = { response: res };
|
|
1572
|
-
let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "");
|
|
1573
|
-
if (pageUrl === "/index") pageUrl = "/";
|
|
1574
|
-
pageUrl = `http://localhost:${config.port || 4e3}${pageUrl}`;
|
|
1575
|
-
let conn = browserConnections.find((conn2) => conn2.url === pageUrl);
|
|
1576
|
-
if (!conn) {
|
|
1577
|
-
spawn2("open", [pageUrl]);
|
|
1578
|
-
let end = Date.now() + 5e3;
|
|
1579
|
-
while (Date.now() < end && !conn) {
|
|
1580
|
-
conn = browserConnections.find((conn2) => conn2.url === pageUrl);
|
|
1581
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1582
|
-
}
|
|
1583
|
-
if (!conn) {
|
|
1584
|
-
res.statusCode = 500;
|
|
1585
|
-
return res.end(JSON.stringify({ error: "No browser tab available and failed to open one" }));
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
conn.socket.send(JSON.stringify({ type: "view", chart, requestId: id }));
|
|
1589
|
-
}
|
|
1590
2023
|
async function handlePage(server, res, filePath, mount) {
|
|
1591
2024
|
res.setHeader("Content-Type", "text/html");
|
|
1592
2025
|
let mdMount = mount ? `
|
|
@@ -1599,7 +2032,7 @@ async function handlePage(server, res, filePath, mount) {
|
|
|
1599
2032
|
<meta charset="UTF-8" />
|
|
1600
2033
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1601
2034
|
<title>Graphene</title>
|
|
1602
|
-
<link rel="icon" href="
|
|
2035
|
+
<link rel="icon" href="/assets/favicon.ico" />
|
|
1603
2036
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1604
2037
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
1605
2038
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
|
@@ -1609,10 +2042,7 @@ async function handlePage(server, res, filePath, mount) {
|
|
|
1609
2042
|
<div id="content"></div>
|
|
1610
2043
|
</main>
|
|
1611
2044
|
<script type="module">
|
|
1612
|
-
// do this first so we can track errors caused by importing the md file
|
|
1613
|
-
import 'graphene'
|
|
1614
|
-
</script>
|
|
1615
|
-
<script type="module">
|
|
2045
|
+
import 'graphene' // do this first so we can track errors caused by importing the md file
|
|
1616
2046
|
${mdMount}
|
|
1617
2047
|
</script>
|
|
1618
2048
|
</body>
|
|
@@ -1625,63 +2055,44 @@ function mockFilesForTests() {
|
|
|
1625
2055
|
name: "mock-files-for-tests",
|
|
1626
2056
|
enforce: "pre",
|
|
1627
2057
|
resolveId(id) {
|
|
1628
|
-
if (mockFileMap[id.replace(
|
|
2058
|
+
if (mockFileMap[id.replace(config.root + "/", "")]) return id + "?mock";
|
|
1629
2059
|
},
|
|
1630
2060
|
load(id) {
|
|
1631
2061
|
if (!id.endsWith("?mock")) return null;
|
|
1632
|
-
return mockFileMap[id.replace(
|
|
2062
|
+
return mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
|
|
1633
2063
|
}
|
|
1634
2064
|
};
|
|
1635
2065
|
}
|
|
1636
|
-
var
|
|
2066
|
+
var uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin;
|
|
1637
2067
|
var init_serve2 = __esm({
|
|
1638
2068
|
"serve2.ts"() {
|
|
1639
2069
|
init_core();
|
|
1640
2070
|
init_connections();
|
|
1641
2071
|
init_mdCompile();
|
|
2072
|
+
init_check();
|
|
2073
|
+
init_mockFiles();
|
|
1642
2074
|
updateWorkspacePlugin = {
|
|
1643
2075
|
name: "updateWorkspace",
|
|
1644
2076
|
configureServer: (s) => {
|
|
1645
2077
|
s.watcher.add("**/*.gsql");
|
|
1646
2078
|
s.watcher.on("change", () => {
|
|
1647
2079
|
clearWorkspace();
|
|
1648
|
-
workspaceLoadPromise = loadWorkspace(
|
|
2080
|
+
workspaceLoadPromise = loadWorkspace(config.root, false);
|
|
1649
2081
|
});
|
|
1650
|
-
workspaceLoadPromise = loadWorkspace(
|
|
2082
|
+
workspaceLoadPromise = loadWorkspace(config.root, false);
|
|
1651
2083
|
}
|
|
1652
2084
|
};
|
|
1653
2085
|
handleRequestPlugin = {
|
|
1654
2086
|
name: "handleRequest",
|
|
1655
2087
|
configureServer: (s) => {
|
|
1656
|
-
let wss = new WebSocketServer({ noServer: true });
|
|
1657
|
-
s.httpServer.on("upgrade", (req, socket, head) => {
|
|
1658
|
-
if (!req.url?.endsWith("/graphene-ws")) return;
|
|
1659
|
-
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1660
|
-
wss.emit("connection", ws, req);
|
|
1661
|
-
});
|
|
1662
|
-
});
|
|
1663
|
-
wss.on("connection", (socket) => {
|
|
1664
|
-
socket.on("message", (data) => {
|
|
1665
|
-
let message = JSON.parse(data.toString());
|
|
1666
|
-
if (message.type === "register") {
|
|
1667
|
-
browserConnections.push({ url: message.url, socket });
|
|
1668
|
-
}
|
|
1669
|
-
if (message.type === "viewResponse") {
|
|
1670
|
-
viewRequests[message.requestId].response.end(JSON.stringify(message));
|
|
1671
|
-
delete viewRequests[message.requestId];
|
|
1672
|
-
}
|
|
1673
|
-
});
|
|
1674
|
-
socket.on("close", () => browserConnections = browserConnections.filter((conn) => conn.socket !== socket));
|
|
1675
|
-
});
|
|
1676
2088
|
s.middlewares.use(async function handleRequest(req, res, next) {
|
|
1677
2089
|
try {
|
|
1678
2090
|
let [pathName] = (req.url || "").split("?");
|
|
1679
2091
|
if (pathName == "/_api/query") return await handleQuery(req, res);
|
|
1680
|
-
if (pathName == "/graphene/view") return await handleView(req, res);
|
|
1681
2092
|
if (pathName == "/__ct") return await handlePage(s, res, "__ct", false);
|
|
1682
2093
|
if (!pathName || pathName == "/") pathName = "index";
|
|
1683
|
-
let mdPath =
|
|
1684
|
-
if (await
|
|
2094
|
+
let mdPath = path7.join(config.root, pathName + ".md");
|
|
2095
|
+
if (await fs6.exists(mdPath)) {
|
|
1685
2096
|
await handlePage(s, res, mdPath, true);
|
|
1686
2097
|
} else {
|
|
1687
2098
|
next();
|
|
@@ -1694,166 +2105,19 @@ var init_serve2 = __esm({
|
|
|
1694
2105
|
});
|
|
1695
2106
|
}
|
|
1696
2107
|
};
|
|
1697
|
-
browserConnections = [];
|
|
1698
|
-
viewRequests = {};
|
|
1699
|
-
mockFileMap = {};
|
|
1700
2108
|
}
|
|
1701
2109
|
});
|
|
1702
2110
|
|
|
1703
2111
|
// cli.ts
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
// printer.ts
|
|
1707
|
-
init_core();
|
|
1708
|
-
import { styleText as nodeStyleText } from "node:util";
|
|
1709
|
-
import Table from "cli-table3";
|
|
1710
|
-
import chalk from "chalk";
|
|
1711
|
-
var styleText = (style, text) => {
|
|
1712
|
-
try {
|
|
1713
|
-
return nodeStyleText ? nodeStyleText(style, text) : text;
|
|
1714
|
-
} catch {
|
|
1715
|
-
return text;
|
|
1716
|
-
}
|
|
1717
|
-
};
|
|
1718
|
-
function offsetToLineCol(src, offset2) {
|
|
1719
|
-
let lines = src.split(/\r?\n/);
|
|
1720
|
-
let acc = 0;
|
|
1721
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1722
|
-
let lineText = lines[i];
|
|
1723
|
-
let nextAcc = acc + lineText.length + 1;
|
|
1724
|
-
if (offset2 < nextAcc || i === lines.length - 1) {
|
|
1725
|
-
let col = Math.max(0, offset2 - acc);
|
|
1726
|
-
return { line: i + 1, col, lineStart: acc, lineText };
|
|
1727
|
-
}
|
|
1728
|
-
acc = nextAcc;
|
|
1729
|
-
}
|
|
1730
|
-
return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
|
|
1731
|
-
}
|
|
1732
|
-
function printDiagnostics(diags) {
|
|
1733
|
-
let parts = [];
|
|
1734
|
-
for (let d of diags) {
|
|
1735
|
-
let src = getFile2(d.file)?.contents || "";
|
|
1736
|
-
let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
|
|
1737
|
-
let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
|
|
1738
|
-
let caretLen = Math.max(1, endCol - col);
|
|
1739
|
-
let sev = d.severity === "error" ? "red" : "yellow";
|
|
1740
|
-
let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
|
|
1741
|
-
let gutter = " | ";
|
|
1742
|
-
let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
|
|
1743
|
-
parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
|
|
1744
|
-
}
|
|
1745
|
-
if (parts.length) console.error(parts.join("\n"));
|
|
1746
|
-
}
|
|
1747
|
-
function printTable(rows) {
|
|
1748
|
-
if (!rows || rows.length === 0) {
|
|
1749
|
-
console.log(chalk.yellow("No results returned"));
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
let headers = Object.keys(rows[0]);
|
|
1753
|
-
let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
|
|
1754
|
-
let MAX_DISPLAY_ROWS = 200;
|
|
1755
|
-
let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
|
|
1756
|
-
displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
|
|
1757
|
-
console.log(table2.toString());
|
|
1758
|
-
if (rows.length > MAX_DISPLAY_ROWS) {
|
|
1759
|
-
console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
// cli.ts
|
|
2112
|
+
init_printer();
|
|
1764
2113
|
init_core();
|
|
1765
2114
|
init_config();
|
|
1766
|
-
|
|
1767
|
-
import path7 from "path";
|
|
1768
|
-
import os from "os";
|
|
1769
|
-
|
|
1770
|
-
// background.ts
|
|
1771
|
-
import { spawn } from "child_process";
|
|
1772
|
-
import { fileURLToPath } from "url";
|
|
1773
|
-
import fs2 from "fs-extra";
|
|
1774
|
-
import path3 from "path";
|
|
1775
|
-
async function runServeInBackground() {
|
|
1776
|
-
let root = process.cwd();
|
|
1777
|
-
let grapheneCache = getGrapheneCache(root);
|
|
1778
|
-
let logFile = path3.join(grapheneCache, "serve.log");
|
|
1779
|
-
await fs2.ensureDir(grapheneCache);
|
|
1780
|
-
await stopGrapheneIfRunning(root);
|
|
1781
|
-
let log = fs2.openSync(logFile, "w");
|
|
1782
|
-
let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
|
|
1783
|
-
let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
|
|
1784
|
-
let child = spawn(process.execPath, childArgs, {
|
|
1785
|
-
cwd: root,
|
|
1786
|
-
detached: true,
|
|
1787
|
-
env: { ...process.env },
|
|
1788
|
-
stdio: ["ignore", log, log]
|
|
1789
|
-
});
|
|
1790
|
-
if (!child.pid) throw new Error("Failed to start server process");
|
|
1791
|
-
await new Promise((resolve, reject) => {
|
|
1792
|
-
let buffer = "";
|
|
1793
|
-
fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
|
|
1794
|
-
if (curr.size > prev.size) {
|
|
1795
|
-
let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
|
|
1796
|
-
stream.on("data", (d) => {
|
|
1797
|
-
process.stdout.write(d);
|
|
1798
|
-
buffer = (buffer + d.toString()).slice(-200);
|
|
1799
|
-
if (buffer.includes("Server running at http://localhost:")) resolve();
|
|
1800
|
-
});
|
|
1801
|
-
}
|
|
1802
|
-
});
|
|
1803
|
-
child.once("exit", () => {
|
|
1804
|
-
process.stdout.write(fs2.readFileSync(logFile));
|
|
1805
|
-
reject(new Error("Exited before server started"));
|
|
1806
|
-
});
|
|
1807
|
-
child.once("error", (e) => reject(e));
|
|
1808
|
-
});
|
|
1809
|
-
}
|
|
1810
|
-
function getGrapheneCache(root) {
|
|
1811
|
-
return path3.join(root, "node_modules", ".graphene");
|
|
1812
|
-
}
|
|
1813
|
-
function getPidFilePath(root) {
|
|
1814
|
-
return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
|
|
1815
|
-
}
|
|
1816
|
-
async function stopGrapheneIfRunning(root) {
|
|
1817
|
-
let pidFile = getPidFilePath(root);
|
|
1818
|
-
let pid = await readPid(pidFile);
|
|
1819
|
-
if (!pid) return true;
|
|
1820
|
-
if (!isProcessRunning(pid)) {
|
|
1821
|
-
await fs2.remove(pidFile);
|
|
1822
|
-
return true;
|
|
1823
|
-
}
|
|
1824
|
-
try {
|
|
1825
|
-
console.log(`Stopping server (${pid})`);
|
|
1826
|
-
process.kill(pid, "SIGTERM");
|
|
1827
|
-
} catch (err) {
|
|
1828
|
-
if (err.code === "ESRCH") return true;
|
|
1829
|
-
return false;
|
|
1830
|
-
}
|
|
1831
|
-
let end = Date.now() + 5e3;
|
|
1832
|
-
while (Date.now() < end && isProcessRunning(pid)) {
|
|
1833
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1834
|
-
}
|
|
1835
|
-
await fs2.remove(pidFile);
|
|
1836
|
-
return !isProcessRunning(pid);
|
|
1837
|
-
}
|
|
1838
|
-
async function readPid(pidFile) {
|
|
1839
|
-
if (!await fs2.pathExists(pidFile)) return void 0;
|
|
1840
|
-
let contents = (await fs2.readFile(pidFile, "utf8")).trim();
|
|
1841
|
-
if (!contents) return void 0;
|
|
1842
|
-
let pid = Number.parseInt(contents, 10);
|
|
1843
|
-
if (Number.isNaN(pid)) return void 0;
|
|
1844
|
-
return pid;
|
|
1845
|
-
}
|
|
1846
|
-
function isProcessRunning(pid) {
|
|
1847
|
-
try {
|
|
1848
|
-
process.kill(pid, 0);
|
|
1849
|
-
return true;
|
|
1850
|
-
} catch {
|
|
1851
|
-
return false;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
// cli.ts
|
|
2115
|
+
init_background();
|
|
1856
2116
|
init_connections();
|
|
2117
|
+
init_check();
|
|
2118
|
+
import { Command } from "commander";
|
|
2119
|
+
import fs7 from "fs-extra";
|
|
2120
|
+
import path8 from "path";
|
|
1857
2121
|
var program = new Command();
|
|
1858
2122
|
program.name("graphene").description("Graphene CLI").version("1.0.0");
|
|
1859
2123
|
program.hook("preAction", async () => {
|
|
@@ -1891,37 +2155,9 @@ program.command("serve").description("Run the local server").option("--fg", "Run
|
|
|
1891
2155
|
program.command("stop").description("Stop the local server").action(async () => {
|
|
1892
2156
|
await stopGrapheneIfRunning(process.cwd());
|
|
1893
2157
|
});
|
|
1894
|
-
program.command("check").description("Check the project for errors").action(async () => {
|
|
1895
|
-
await
|
|
1896
|
-
|
|
1897
|
-
if (getDiagnostics().length) {
|
|
1898
|
-
printDiagnostics(getDiagnostics());
|
|
1899
|
-
process.exit(1);
|
|
1900
|
-
}
|
|
1901
|
-
console.log("No errors found \u{1F48E}");
|
|
1902
|
-
});
|
|
1903
|
-
program.command("view").description("Capture a screenshot of a rendered markdown file").argument("<mdFile>", "Markdown file to view (e.g., index.md)").option("-c, --chart <chartName>", "Name of specific chart to capture").action(async (mdFile, options) => {
|
|
1904
|
-
let response = await fetch("http://localhost:4000/graphene/view", {
|
|
1905
|
-
method: "POST",
|
|
1906
|
-
headers: { "Content-Type": "application/json" },
|
|
1907
|
-
body: JSON.stringify({ mdFile, chart: options.chart })
|
|
1908
|
-
});
|
|
1909
|
-
if (!response.ok) throw new Error(`View request failed: ${await response.text()}`);
|
|
1910
|
-
let result = await response.json();
|
|
1911
|
-
if (result.errors && result.errors.length > 0) {
|
|
1912
|
-
console.error("Errors found:");
|
|
1913
|
-
result.errors.forEach((error) => console.error(JSON.stringify(error)));
|
|
1914
|
-
}
|
|
1915
|
-
if (result.stillLoading) {
|
|
1916
|
-
console.error("Warning: Queries were still loading when the screenshot was taken");
|
|
1917
|
-
}
|
|
1918
|
-
if (result.screenshot) {
|
|
1919
|
-
let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
|
|
1920
|
-
let screenshotPath = path7.join(os.tmpdir(), filename);
|
|
1921
|
-
let base64Data = result.screenshot.replace(/^data:image\/png;base64,/, "");
|
|
1922
|
-
await fs6.writeFile(screenshotPath, base64Data, "base64");
|
|
1923
|
-
console.log("Screenshot saved to", screenshotPath);
|
|
1924
|
-
}
|
|
2158
|
+
program.command("check").description("Check the project for errors, optionally capturing a page screenshot").argument("[mdFile]", "Markdown file to check (e.g., index.md)").option("-c, --chart <chartTitle>", "Title of a specific chart to capture").action(async (mdArg, options) => {
|
|
2159
|
+
let res = await check({ mdArg, chart: options.chart });
|
|
2160
|
+
process.exit(res ? 0 : 1);
|
|
1925
2161
|
});
|
|
1926
2162
|
program.parse(process.argv);
|
|
1927
2163
|
async function readInput(arg) {
|
|
@@ -1934,9 +2170,9 @@ async function readInput(arg) {
|
|
|
1934
2170
|
process.stdin.resume();
|
|
1935
2171
|
});
|
|
1936
2172
|
}
|
|
1937
|
-
let absolutePath =
|
|
1938
|
-
if (
|
|
1939
|
-
return await
|
|
2173
|
+
let absolutePath = path8.resolve(arg);
|
|
2174
|
+
if (fs7.existsSync(absolutePath)) {
|
|
2175
|
+
return await fs7.promises.readFile(absolutePath, "utf-8");
|
|
1940
2176
|
}
|
|
1941
2177
|
return arg;
|
|
1942
2178
|
}
|