Files
dbx/.github/ui-preview/seed.mjs
T

245 lines
12 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fills the UI preview's sandbox (run.sh) with data to show: a fresh dbx-web
// has no connections, so a change to the sidebar, the grid, query history or
// the ER diagram has nothing to draw. Made up, every bit of it, and never
// real.
//
// node seed.mjs dbs <dir> [--as <path on the server>] — two SQLite
// databases with fixed, deterministic content (python3 writes them, so
// the files are identical byte-for-byte on every run);
// node seed.mjs api <base-url> <dir> [--as <path on the server>]
// — saves the connections (SQLite: the file path is the host), connects,
// runs real queries through the gateway so history and result caches
// are genuine, then adds a saved-SQL library and a few more history
// entries the UI can list.
//
// The connections point at files as the dbx-web process sees them: pass
// --as when the databases are generated on another machine than the one
// dbx-web runs on (local testing over an SSH tunnel).
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const arg = (n) => process.argv[n];
const opt = (name, d) => {
const i = process.argv.indexOf(name);
return i > 0 && i + 1 < process.argv.length ? process.argv[i + 1] : d;
};
const DBS = [
{
file: "dbx-shop.sqlite",
name: "商店 Shop (SQLite)",
note: "订单、商品与客户示例库",
tables: "customers · products · orders · audit_log(含视图 v_daily_revenue、外键与触发器)",
warm: [
"SELECT * FROM orders ORDER BY created_at DESC LIMIT 50",
"SELECT c.city, COUNT(*) AS orders, SUM(o.amount) AS revenue\nFROM orders o JOIN customers c ON c.id = o.customer_id\nGROUP BY c.city ORDER BY revenue DESC",
"SELECT category, COUNT(*) AS n, ROUND(AVG(price), 2) AS avg_price\nFROM products WHERE listed = 1 GROUP BY category ORDER BY n DESC",
],
},
{
file: "dbx-analytics.sqlite",
name: "分析 Analytics (SQLite)",
note: "事件流与每日指标",
tables: "events(5000 行,看分页)· daily_stats(90 天)",
warm: [
"SELECT event_type, COUNT(*) AS n, ROUND(AVG(duration_ms)) AS avg_ms\nFROM events GROUP BY event_type ORDER BY n DESC",
"SELECT date(ts) AS day, COUNT(DISTINCT user_id) AS dau\nFROM events GROUP BY day ORDER BY day DESC LIMIT 14",
"SELECT * FROM daily_stats ORDER BY stat_date DESC LIMIT 30",
],
},
];
// Deterministic content: the same LCG magpie's seed uses, so two previews of
// one PR differ only by the PR.
function python(dir) {
const py = `
import os, sqlite3, random, datetime
rng = random.Random(20261008)
def d(days, hours=0):
return (datetime.datetime(2026, 10, 8, 12, 0, 0) - datetime.timedelta(days=days, hours=hours)).strftime("%Y-%m-%d %H:%M:%S")
shop = sqlite3.connect(os.path.join(${JSON.stringify(dir)}, "dbx-shop.sqlite"))
shop.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT, city TEXT, level TEXT, created_at TEXT);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, category TEXT, price REAL, stock INTEGER, listed INTEGER DEFAULT 1);
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER REFERENCES customers(id), product_id INTEGER REFERENCES products(id),
quantity INTEGER, amount REAL, status TEXT, created_at TEXT);
CREATE TABLE audit_log (id INTEGER PRIMARY KEY, ts TEXT, what TEXT);
CREATE INDEX idx_orders_created ON orders(created_at);
CREATE INDEX idx_orders_status ON orders(status);
CREATE VIEW v_daily_revenue AS SELECT date(created_at) AS day, COUNT(*) AS orders, ROUND(SUM(amount), 2) AS revenue FROM orders GROUP BY date(created_at);
CREATE TRIGGER trg_orders_audit AFTER INSERT ON orders BEGIN INSERT INTO audit_log(ts, what) VALUES (datetime('now'), 'order ' || NEW.id); END;
""")
cities = ["上海", "北京", "深圳", "杭州", "成都", "广州", "武汉", "南京"]
surnames = ["王", "李", "张", "刘", "陈", "杨", "赵", "周", "吴", "徐"]
givens = ["晓东", "思远", "雨桐", "子墨", "一鸣", "嘉怡", "浩然", "若彤", "俊杰", "梦琪"]
levels = ["bronze", "silver", "gold"]
for i in range(1, 201):
shop.execute("INSERT INTO customers VALUES (?,?,?,?,?,?)",
(i, rng.choice(surnames) + rng.choice(givens), f"user{i}@example.com", rng.choice(cities), rng.choice(levels), d(rng.randint(30, 400), rng.randint(0, 23))))
cats = ["键盘", "显示器", "鼠标", "硬盘", "内存", "耳机", "摄像头", "扩展坞"]
for i in range(1, 121):
shop.execute("INSERT INTO products VALUES (?,?,?,?,?,?)",
(i, f"{rng.choice(cats)} {chr(65 + i % 26)}{i:03d}", rng.choice(cats), round(rng.uniform(49, 4999), 2), rng.randint(0, 500), 1 if rng.random() > 0.1 else 0))
statuses = ["paid", "shipped", "done", "refund"]
for i in range(1, 501):
q = rng.randint(1, 3)
price = rng.uniform(49, 4999)
shop.execute("INSERT INTO orders VALUES (?,?,?,?,?,?,?)",
(i, rng.randint(1, 200), rng.randint(1, 120), q, round(q * price, 2), rng.choice(statuses) if rng.random() > 0.15 else "pending", d(rng.randint(0, 90), rng.randint(0, 23))))
shop.commit()
an = sqlite3.connect(os.path.join(${JSON.stringify(dir)}, "dbx-analytics.sqlite"))
an.executescript("""
CREATE TABLE events (id INTEGER PRIMARY KEY, ts TEXT, user_id INTEGER, event_type TEXT, page TEXT, duration_ms INTEGER);
CREATE TABLE daily_stats (stat_date TEXT PRIMARY KEY, dau INTEGER, revenue REAL, new_users INTEGER);
CREATE INDEX idx_events_ts ON events(ts);
CREATE INDEX idx_events_type ON events(event_type);
""")
types = ["page_view", "click", "scroll", "search", "submit", "error"]
pages = ["/", "/grid", "/editor", "/history", "/settings", "/diagram"]
day = datetime.date(2026, 10, 8)
for i in range(1, 5001):
back = rng.randint(0, 29)
ts = datetime.datetime(2026, 10, 8, rng.randint(0, 23), rng.randint(0, 59), rng.randint(0, 59)) - datetime.timedelta(days=back)
an.execute("INSERT INTO events VALUES (?,?,?,?,?,?)", (i, ts.strftime("%Y-%m-%d %H:%M:%S"), rng.randint(1, 400), rng.choice(types) if rng.random() > 0.02 else "error", rng.choice(pages), rng.randint(50, 180000) if rng.random() > 0.1 else None))
for k in range(90):
date = (day - datetime.timedelta(days=k)).strftime("%Y-%m-%d")
an.execute("INSERT INTO daily_stats VALUES (?,?,?,?)", (date, rng.randint(180, 420), round(rng.uniform(800, 5200), 2), rng.randint(3, 26)))
an.commit()
print("ok")
`;
execFileSync("python3", ["-c", py], { stdio: ["ignore", "inherit", "inherit"] });
}
async function api(base, p, init = {}) {
const res = await fetch(base.replace(/\/$/, "") + p, {
...init,
headers: { "Content-Type": "application/json", ...(init.headers || {}) },
});
const text = await res.text();
if (!res.ok) throw new Error(`${init.method || "GET"} ${p}: ${res.status} ${text.slice(0, 300)}`);
try {
return JSON.parse(text);
} catch {
return text;
}
}
async function seedApi() {
const base = arg(3);
const dir = path.resolve(arg(4));
const as = path.resolve(opt("--as", dir));
const now = Date.now();
const iso = (msAgo) => new Date(now - msAgo).toISOString();
const conns = DBS.map((db, i) => ({
id: `seed-${db.file.replace(".sqlite", "")}`,
name: db.name,
note: db.note,
db_type: "sqlite",
driver_label: "SQLite",
host: path.join(as, db.file),
port: 0,
username: "",
password: "",
database: "main",
color: i === 0 ? "#2f6fed" : "#8f4bd6",
}));
await api(base, "/api/connection/save", { method: "POST", body: JSON.stringify({ configs: conns, removed_ids: [] }) });
console.log("[ui-preview] saved", conns.length, "connections");
for (const c of conns) {
await api(base, "/api/connection/connect", { method: "POST", body: JSON.stringify({ config: c, client_attempt: 1 }) });
const info = await api(base, `/api/connection/info?connection_id=${encodeURIComponent(c.id)}`).catch((e) => (console.log("[ui-preview] connection info:", e.message), null));
const database = info?.current_database || info?.databases?.[0] || "main";
const db = DBS.find((x) => c.host.endsWith(x.file));
for (const sql of db.warm) {
const r = await api(base, "/api/query/execute", {
method: "POST",
body: JSON.stringify({ connectionId: c.id, database, sql, maxRows: 200 }),
}).catch((e) => (console.log("[ui-preview] warm query failed:", e.message), null));
if (r) console.log("[ui-preview] ran", sql.split("\n")[0].slice(0, 60));
}
}
// A saved-SQL library, so the SQL library panel has folders and files.
const folder = await api(base, "/api/saved-sql/folders", {
method: "POST",
body: JSON.stringify({ id: "seed-folder-daily", name: "常用查询", createdAt: iso(36e5 * 72), updatedAt: iso(36e5 * 5) }),
}).catch(() => null);
const saved = [
{ id: "seed-sql-1", name: "各城市营收 Top", sql: "SELECT c.city, COUNT(*) AS orders, ROUND(SUM(o.amount), 2) AS revenue\nFROM orders o JOIN customers c ON c.id = o.customer_id\nGROUP BY c.city ORDER BY revenue DESC" },
{ id: "seed-sql-2", name: "近 14 天 DAU", sql: "SELECT date(ts) AS day, COUNT(DISTINCT user_id) AS dau\nFROM events GROUP BY day ORDER BY day DESC LIMIT 14" },
{ id: "seed-sql-3", name: "低库存商品", sql: "SELECT name, category, stock FROM products WHERE stock < 20 AND listed = 1 ORDER BY stock" },
];
for (const [i, s] of saved.entries()) {
await api(base, "/api/saved-sql", {
method: "POST",
body: JSON.stringify({
...s,
connectionId: conns[i % conns.length].id,
folderId: folder?.id || "seed-folder-daily",
database: "main",
orderIndex: i,
openCount: 3 + i,
createdAt: iso(36e5 * (70 - i)),
updatedAt: iso(36e5 * (24 - i * 6)),
}),
}).catch((e) => console.log("[ui-preview] saved-sql:", e.message));
}
console.log("[ui-preview] saved-sql library seeded");
// A few more history entries besides the real ones above: spread over the
// last three days, one failure included, so the history list pages.
const extra = [
[conns[0], "SELECT COUNT(*) FROM orders WHERE status = 'pending'", 12, true],
[conns[0], "SELECT * FROM v_daily_revenue ORDER BY day DESC LIMIT 7", 18, true],
[conns[0], "UPDATE products SET stock = stock - 1 WHERE id = 42", 6, false],
[conns[1], "SELECT stat_date, dau, revenue FROM daily_stats ORDER BY stat_date DESC LIMIT 30", 15, true],
[conns[1], "SELECT page, COUNT(*) FROM events WHERE event_type = 'error' GROUP BY page", 22, true],
[conns[1], "DELETE FROM events WHERE id < 0", 4, false],
];
for (const [c, sql, ms, ok] of extra) {
await api(base, "/api/history/save", {
method: "POST",
body: JSON.stringify({
entry: {
id: `seed-h-${Math.random().toString(36).slice(2, 10)}`,
connection_id: c.id,
connection_name: c.name,
database: "main",
sql,
executed_at: iso(36e5 * (2 + Math.random() * 60)),
execution_time_ms: ms,
success: ok,
error: ok ? null : "demo entry: would have modified rows (not really run)",
activity_kind: "query",
source: "editor",
},
}),
}).catch((e) => console.log("[ui-preview] history:", e.message));
}
console.log("[ui-preview] history seeded");
}
const cmd = process.argv[2];
if (cmd === "dbs") {
const dir = path.resolve(arg(3));
fs.mkdirSync(dir, { recursive: true });
python(dir);
// a small importable file, for import/mapping dialogs (record.mjs's
// "upload" step resolves names inside this directory)
fs.writeFileSync(path.join(dir, "sample.csv"), "name,category,price,stock\n" + Array.from({ length: 8 }, (_, i) => `键盘 K${i + 100},键盘,${(199 + i * 23).toFixed(2)},${40 - i * 3}`).join("\n") + "\n");
console.log("[ui-preview] databases written to", dir);
} else if (cmd === "api") {
seedApi().catch((e) => {
console.error(e);
process.exit(1);
});
} else {
console.error("usage: seed.mjs dbs <dir> | seed.mjs api <base-url> <dir> [--as <server path>]");
process.exit(1);
}