db-player 0.0.0-stage → 0.4.1
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/README.md +56 -2
- package/bin/db-player.mjs +149 -0
- package/dist/client/assets/EmptyState-B_8ZzjRf.js +2 -0
- package/dist/client/assets/c._connectionId-BAPOAiu0.js +1 -0
- package/dist/client/assets/index-Cre7NCFa.js +136 -0
- package/dist/client/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/dist/client/assets/routes-_dAU0q9L.js +1 -0
- package/dist/client/assets/styles-D3kk8PAA.css +1669 -0
- package/dist/client/assets/t._tabId-DJ7LAi8J.js +1 -0
- package/dist/server/assets/EmptyState-D1iyQsG1.js +366 -0
- package/dist/server/assets/_tanstack-start-manifest_v-AE6zwk_Q.js +38 -0
- package/dist/server/assets/c._connectionId-BXPGBd-R.js +19 -0
- package/dist/server/assets/createServerRpc-F6SI7pt8.js +11 -0
- package/dist/server/assets/empty-plugin-adapters-BiGySXWX.js +4 -0
- package/dist/server/assets/fns-DNn8Qat1.js +321 -0
- package/dist/server/assets/layout-pJu_-O4s.js +36 -0
- package/dist/server/assets/router-C6umP2o0.js +4061 -0
- package/dist/server/assets/routes-Ce6MPKxr.js +24 -0
- package/dist/server/assets/start-Bai9AeXo.js +3 -0
- package/dist/server/assets/store-Bb9KDsJx.js +827 -0
- package/dist/server/assets/t._tabId-Cf1urQNG.js +4 -0
- package/dist/server/assets/theme-EhZN9QoB.js +32 -0
- package/dist/server/server.js +7788 -0
- package/package.json +73 -3
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createClient } from "@clickhouse/client";
|
|
3
|
+
import mysql from "mysql2/promise";
|
|
4
|
+
import mysqlCore from "mysql2";
|
|
5
|
+
import { chmodSync, closeSync, mkdirSync, openSync } from "node:fs";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { DatabaseSync } from "node:sqlite";
|
|
8
|
+
// src/lib/types.ts
|
|
9
|
+
var DB_KINDS = ["mysql", "clickhouse"];
|
|
10
|
+
var DB_KIND_INFO = {
|
|
11
|
+
mysql: {
|
|
12
|
+
label: "MySQL",
|
|
13
|
+
defaultPort: 3306
|
|
14
|
+
},
|
|
15
|
+
clickhouse: {
|
|
16
|
+
label: "ClickHouse",
|
|
17
|
+
defaultPort: 8123
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var FOLDER_COLORS = {
|
|
21
|
+
gray: "#8a8a8a",
|
|
22
|
+
red: "#ef4444",
|
|
23
|
+
orange: "#f97316",
|
|
24
|
+
yellow: "#eab308",
|
|
25
|
+
green: "#22c55e",
|
|
26
|
+
teal: "#14b8a6",
|
|
27
|
+
blue: "#3b82f6",
|
|
28
|
+
purple: "#a855f7"
|
|
29
|
+
};
|
|
30
|
+
var FOLDER_COLOR_NAMES = Object.keys(FOLDER_COLORS);
|
|
31
|
+
var folderInputSchema = z.object({
|
|
32
|
+
name: z.string().trim().min(1, "Name is required").max(60),
|
|
33
|
+
color: z.enum(FOLDER_COLOR_NAMES)
|
|
34
|
+
});
|
|
35
|
+
var connectionInputSchema = z.object({
|
|
36
|
+
folderId: z.string().nullable(),
|
|
37
|
+
name: z.string().trim().min(1, "Name is required").max(80),
|
|
38
|
+
kind: z.enum(DB_KINDS),
|
|
39
|
+
host: z.string().trim().min(1, "Host is required"),
|
|
40
|
+
port: z.number().int().min(1).max(65535),
|
|
41
|
+
user: z.string().trim().min(1, "User is required"),
|
|
42
|
+
password: z.string(),
|
|
43
|
+
database: z.string().trim().nullable(),
|
|
44
|
+
secure: z.boolean()
|
|
45
|
+
});
|
|
46
|
+
var tabIdSchema = z.custom((v) => typeof v === "string" && v.length > 0);
|
|
47
|
+
var newTabTargetSchema = z.discriminatedUnion("kind", [z.object({
|
|
48
|
+
kind: z.literal("sql"),
|
|
49
|
+
connectionId: z.string().min(1),
|
|
50
|
+
database: z.string().min(1).nullable()
|
|
51
|
+
}), z.object({
|
|
52
|
+
kind: z.literal("table"),
|
|
53
|
+
connectionId: z.string().min(1),
|
|
54
|
+
database: z.string().min(1),
|
|
55
|
+
table: z.string().min(1)
|
|
56
|
+
})]);
|
|
57
|
+
// src/server/drivers/shared.ts
|
|
58
|
+
function normalizeCell(value) {
|
|
59
|
+
if (value === null || value === void 0) return null;
|
|
60
|
+
if (Buffer.isBuffer(value)) return `0x${value.toString("hex")}`;
|
|
61
|
+
if (typeof value === "bigint") return value.toString();
|
|
62
|
+
if (value instanceof Date) return value.toISOString();
|
|
63
|
+
if (typeof value === "object") return JSON.parse(JSON.stringify(value));
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function fingerprint(parts) {
|
|
67
|
+
return JSON.stringify(parts);
|
|
68
|
+
}
|
|
69
|
+
var SQL_SCHEMA_ROW_CAP = 3e4;
|
|
70
|
+
/** Database and table names are user data, so they key prototype-free objects. */
|
|
71
|
+
function groupColumns(rows) {
|
|
72
|
+
const schema = Object.create(null);
|
|
73
|
+
for (const r of rows) {
|
|
74
|
+
const tables = schema[String(r.db)] ??= Object.create(null);
|
|
75
|
+
(tables[String(r.tbl)] ??= []).push({
|
|
76
|
+
name: String(r.col),
|
|
77
|
+
type: String(r.type)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return schema;
|
|
81
|
+
}
|
|
82
|
+
// src/server/drivers/clickhouse.ts
|
|
83
|
+
/** One client per database, because the HTTP interface picks the database per client. */
|
|
84
|
+
var clients = /* @__PURE__ */ new Map();
|
|
85
|
+
function getClient(conn, database = conn.database) {
|
|
86
|
+
const key = fingerprint([
|
|
87
|
+
conn.host,
|
|
88
|
+
conn.port,
|
|
89
|
+
conn.user,
|
|
90
|
+
conn.password,
|
|
91
|
+
conn.database,
|
|
92
|
+
conn.secure
|
|
93
|
+
]);
|
|
94
|
+
let entry = clients.get(conn.id);
|
|
95
|
+
if (entry?.key !== key) {
|
|
96
|
+
closeAll(entry);
|
|
97
|
+
entry = {
|
|
98
|
+
key,
|
|
99
|
+
byDatabase: /* @__PURE__ */ new Map()
|
|
100
|
+
};
|
|
101
|
+
clients.set(conn.id, entry);
|
|
102
|
+
}
|
|
103
|
+
const slot = database || "";
|
|
104
|
+
const cached = entry.byDatabase.get(slot);
|
|
105
|
+
if (cached) return cached;
|
|
106
|
+
const client = createClient({
|
|
107
|
+
url: `${conn.secure ? "https" : "http"}://${conn.host}:${conn.port}`,
|
|
108
|
+
username: conn.user,
|
|
109
|
+
password: conn.password,
|
|
110
|
+
database: database || void 0,
|
|
111
|
+
request_timeout: 3e4
|
|
112
|
+
});
|
|
113
|
+
entry.byDatabase.set(slot, client);
|
|
114
|
+
return client;
|
|
115
|
+
}
|
|
116
|
+
async function closeAll(entry) {
|
|
117
|
+
await Promise.all([...entry?.byDatabase.values() ?? []].map((client) => client.close().catch(() => {})));
|
|
118
|
+
}
|
|
119
|
+
var quote$1 = (name) => `\`${name.replaceAll("\\", "\\\\").replaceAll("`", "\\`")}\``;
|
|
120
|
+
var qualified$1 = (ref) => `${quote$1(ref.database)}.${quote$1(ref.table)}`;
|
|
121
|
+
var RETURNS_ROWS = /^[\s(]*(select|with|show|describe|desc|explain|exists|values)\b/i;
|
|
122
|
+
async function fetchRows(client, query, query_params, settings) {
|
|
123
|
+
return await (await client.query({
|
|
124
|
+
query,
|
|
125
|
+
query_params,
|
|
126
|
+
format: "JSONCompact",
|
|
127
|
+
clickhouse_settings: settings
|
|
128
|
+
})).json();
|
|
129
|
+
}
|
|
130
|
+
async function objects(client, query, params) {
|
|
131
|
+
const { meta, data } = await fetchRows(client, query, params);
|
|
132
|
+
return data.map((row) => Object.fromEntries(row.map((v, i) => [meta[i]?.name ?? String(i), v])));
|
|
133
|
+
}
|
|
134
|
+
/** Parameter types cannot carry LowCardinality. */
|
|
135
|
+
var paramType = (type) => type.replace(/LowCardinality\((.*)\)/, "$1");
|
|
136
|
+
var Params = class {
|
|
137
|
+
n = 0;
|
|
138
|
+
values = {};
|
|
139
|
+
bind(value, type) {
|
|
140
|
+
const name = `p${this.n++}`;
|
|
141
|
+
this.values[name] = typeof value === "object" ? JSON.stringify(value) : value;
|
|
142
|
+
return `{${name}:${paramType(type)}}`;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
async function columnTypes(client, ref) {
|
|
146
|
+
const rows = await objects(client, "SELECT name, type FROM system.columns WHERE database = {db:String} AND table = {t:String}", {
|
|
147
|
+
db: ref.database,
|
|
148
|
+
t: ref.table
|
|
149
|
+
});
|
|
150
|
+
return new Map(rows.map((r) => [String(r.name), String(r.type)]));
|
|
151
|
+
}
|
|
152
|
+
function condition(key, types, params) {
|
|
153
|
+
return Object.entries(key).map(([col, value]) => value === null ? `isNull(${quote$1(col)})` : `${quote$1(col)} = ${params.bind(value, types.get(col) ?? "String")}`).join(" AND ");
|
|
154
|
+
}
|
|
155
|
+
var MUTATION_SETTINGS = { mutations_sync: "2" };
|
|
156
|
+
/** Level 2 refuses writes and DDL but still lets the client pass its own settings. */
|
|
157
|
+
var READ_ONLY_SETTINGS = { readonly: "2" };
|
|
158
|
+
var clickhouseDriver = {
|
|
159
|
+
async test(conn) {
|
|
160
|
+
const rows = await objects(getClient(conn), "SELECT version() AS v");
|
|
161
|
+
return { version: String(rows[0]?.v) };
|
|
162
|
+
},
|
|
163
|
+
async listDatabases(conn) {
|
|
164
|
+
return (await objects(getClient(conn), "SELECT name FROM system.databases ORDER BY name")).map((r) => String(r.name));
|
|
165
|
+
},
|
|
166
|
+
async listTables(conn, database) {
|
|
167
|
+
return (await objects(getClient(conn), "SELECT name, engine FROM system.tables WHERE database = {db:String} ORDER BY name", { db: database })).map((r) => ({
|
|
168
|
+
name: String(r.name),
|
|
169
|
+
isView: String(r.engine).endsWith("View")
|
|
170
|
+
}));
|
|
171
|
+
},
|
|
172
|
+
async describeTable(conn, ref) {
|
|
173
|
+
const client = getClient(conn);
|
|
174
|
+
const [cols, tables] = await Promise.all([objects(client, `SELECT name, type, default_expression, is_in_primary_key, is_in_sorting_key
|
|
175
|
+
FROM system.columns WHERE database = {db:String} AND table = {t:String} ORDER BY position`, {
|
|
176
|
+
db: ref.database,
|
|
177
|
+
t: ref.table
|
|
178
|
+
}), objects(client, "SELECT engine FROM system.tables WHERE database = {db:String} AND name = {t:String}", {
|
|
179
|
+
db: ref.database,
|
|
180
|
+
t: ref.table
|
|
181
|
+
})]);
|
|
182
|
+
const columns = cols.map((r) => ({
|
|
183
|
+
name: String(r.name),
|
|
184
|
+
type: String(r.type),
|
|
185
|
+
nullable: /Nullable\(/.test(String(r.type)),
|
|
186
|
+
isPrimaryKey: Number(r.is_in_primary_key) === 1 || Number(r.is_in_sorting_key) === 1,
|
|
187
|
+
default: r.default_expression ? String(r.default_expression) : null
|
|
188
|
+
}));
|
|
189
|
+
const keyColumns = columns.filter((c) => c.isPrimaryKey).map((c) => c.name);
|
|
190
|
+
const engine = String(tables[0]?.engine ?? "");
|
|
191
|
+
return {
|
|
192
|
+
columns,
|
|
193
|
+
keyColumns,
|
|
194
|
+
editBlockedReason: !engine.endsWith("MergeTree") ? `Engine ${engine || "unknown"} does not support row mutations. Editing is disabled.` : keyColumns.length === 0 ? "This table has no sorting key (ORDER BY tuple()), so rows cannot be addressed safely. Editing is disabled." : null
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
async sqlSchema(conn) {
|
|
198
|
+
const defaultDatabase = conn.database || "default";
|
|
199
|
+
return {
|
|
200
|
+
defaultDatabase,
|
|
201
|
+
schema: groupColumns(await objects(getClient(conn), `SELECT database AS db, table AS tbl, name AS col, type
|
|
202
|
+
FROM system.columns
|
|
203
|
+
WHERE database NOT IN ('system', 'INFORMATION_SCHEMA', 'information_schema')
|
|
204
|
+
OR database = {def:String}
|
|
205
|
+
ORDER BY database = {def:String} DESC, database, table, position
|
|
206
|
+
LIMIT ${SQL_SCHEMA_ROW_CAP}`, { def: defaultDatabase }))
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
async query(conn, sql, { database, limit, readOnly }) {
|
|
210
|
+
const client = getClient(conn, database ?? conn.database);
|
|
211
|
+
const text = sql.trim().replace(/;+\s*$/, "");
|
|
212
|
+
const started = performance.now();
|
|
213
|
+
if (!RETURNS_ROWS.test(text)) {
|
|
214
|
+
await client.command({
|
|
215
|
+
query: text,
|
|
216
|
+
clickhouse_settings: readOnly ? READ_ONLY_SETTINGS : MUTATION_SETTINGS
|
|
217
|
+
});
|
|
218
|
+
return {
|
|
219
|
+
columns: [],
|
|
220
|
+
rows: [],
|
|
221
|
+
rowCount: 0,
|
|
222
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
223
|
+
truncated: false
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const { meta, data } = await fetchRows(client, text, void 0, {
|
|
227
|
+
...readOnly ? READ_ONLY_SETTINGS : {},
|
|
228
|
+
max_result_rows: String(limit + 1),
|
|
229
|
+
result_overflow_mode: "break"
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
columns: meta,
|
|
233
|
+
rows: data.slice(0, limit),
|
|
234
|
+
rowCount: Math.min(data.length, limit),
|
|
235
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
236
|
+
truncated: data.length > limit
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
async selectRows(conn, ref, opts) {
|
|
240
|
+
const client = getClient(conn);
|
|
241
|
+
const started = performance.now();
|
|
242
|
+
const where = opts.where?.trim() ? ` WHERE ${opts.where}` : "";
|
|
243
|
+
const order = opts.orderBy ? ` ORDER BY ${quote$1(opts.orderBy.column)} ${opts.orderBy.direction === "desc" ? "DESC" : "ASC"}` : "";
|
|
244
|
+
const { meta, data } = await fetchRows(client, `SELECT * FROM ${qualified$1(ref)}${where}${order} LIMIT {limit:UInt32} OFFSET {offset:UInt64}`, {
|
|
245
|
+
limit: opts.limit,
|
|
246
|
+
offset: opts.offset
|
|
247
|
+
});
|
|
248
|
+
const counted = await objects(client, `SELECT count() AS n FROM ${qualified$1(ref)}${where}`);
|
|
249
|
+
return {
|
|
250
|
+
result: {
|
|
251
|
+
columns: meta,
|
|
252
|
+
rows: data,
|
|
253
|
+
rowCount: data.length,
|
|
254
|
+
elapsedMs: Math.round(performance.now() - started),
|
|
255
|
+
truncated: false
|
|
256
|
+
},
|
|
257
|
+
total: Number(counted[0]?.n ?? 0)
|
|
258
|
+
};
|
|
259
|
+
},
|
|
260
|
+
async updateRow(conn, ref, key, changes) {
|
|
261
|
+
const cols = Object.keys(changes);
|
|
262
|
+
if (cols.length === 0) return;
|
|
263
|
+
const client = getClient(conn);
|
|
264
|
+
const types = await columnTypes(client, ref);
|
|
265
|
+
const params = new Params();
|
|
266
|
+
const set = cols.map((c) => {
|
|
267
|
+
const v = changes[c];
|
|
268
|
+
return `${quote$1(c)} = ${v === null ? "NULL" : params.bind(v, types.get(c) ?? "String")}`;
|
|
269
|
+
}).join(", ");
|
|
270
|
+
const where = condition(key, types, params);
|
|
271
|
+
await client.command({
|
|
272
|
+
query: `ALTER TABLE ${qualified$1(ref)} UPDATE ${set} WHERE ${where}`,
|
|
273
|
+
query_params: params.values,
|
|
274
|
+
clickhouse_settings: MUTATION_SETTINGS
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
async insertRow(conn, ref, values) {
|
|
278
|
+
await getClient(conn).insert({
|
|
279
|
+
table: qualified$1(ref),
|
|
280
|
+
values: [values],
|
|
281
|
+
format: "JSONEachRow"
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
async deleteRows(conn, ref, keys) {
|
|
285
|
+
if (keys.length === 0) return;
|
|
286
|
+
const client = getClient(conn);
|
|
287
|
+
const types = await columnTypes(client, ref);
|
|
288
|
+
const params = new Params();
|
|
289
|
+
const where = keys.map((k) => `(${condition(k, types, params)})`).join(" OR ");
|
|
290
|
+
await client.command({
|
|
291
|
+
query: `ALTER TABLE ${qualified$1(ref)} DELETE WHERE ${where}`,
|
|
292
|
+
query_params: params.values,
|
|
293
|
+
clickhouse_settings: MUTATION_SETTINGS
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
async dispose(id) {
|
|
297
|
+
const cached = clients.get(id);
|
|
298
|
+
clients.delete(id);
|
|
299
|
+
await closeAll(cached);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
// src/server/drivers/mysql.ts
|
|
303
|
+
var pools = /* @__PURE__ */ new Map();
|
|
304
|
+
function getPool(conn) {
|
|
305
|
+
const key = fingerprint([
|
|
306
|
+
conn.host,
|
|
307
|
+
conn.port,
|
|
308
|
+
conn.user,
|
|
309
|
+
conn.password,
|
|
310
|
+
conn.database,
|
|
311
|
+
conn.secure
|
|
312
|
+
]);
|
|
313
|
+
const cached = pools.get(conn.id);
|
|
314
|
+
if (cached?.key === key) return cached.pool;
|
|
315
|
+
cached?.pool.end().catch(() => {});
|
|
316
|
+
const pool = mysql.createPool({
|
|
317
|
+
host: conn.host,
|
|
318
|
+
port: conn.port,
|
|
319
|
+
user: conn.user,
|
|
320
|
+
password: conn.password,
|
|
321
|
+
database: conn.database || void 0,
|
|
322
|
+
ssl: conn.secure ? {} : void 0,
|
|
323
|
+
connectionLimit: 4,
|
|
324
|
+
connectTimeout: 8e3,
|
|
325
|
+
dateStrings: true,
|
|
326
|
+
supportBigNumbers: true,
|
|
327
|
+
bigNumberStrings: true
|
|
328
|
+
});
|
|
329
|
+
pools.set(conn.id, {
|
|
330
|
+
key,
|
|
331
|
+
pool
|
|
332
|
+
});
|
|
333
|
+
return pool;
|
|
334
|
+
}
|
|
335
|
+
var NO_MATCH = "No row matched the key. The row may have changed or been deleted.";
|
|
336
|
+
var quote = (name) => `\`${name.replaceAll("`", "``")}\``;
|
|
337
|
+
var qualified = (ref) => `${quote(ref.database)}.${quote(ref.table)}`;
|
|
338
|
+
var typeNames = new Map(Object.entries(mysqlCore.Types).map(([name, id]) => [id, name]));
|
|
339
|
+
function fieldType(field) {
|
|
340
|
+
const id = field.columnType;
|
|
341
|
+
return id !== void 0 && typeNames.get(id) || "UNKNOWN";
|
|
342
|
+
}
|
|
343
|
+
function whereKey(key) {
|
|
344
|
+
const params = [];
|
|
345
|
+
return {
|
|
346
|
+
sql: Object.entries(key).map(([col, value]) => {
|
|
347
|
+
if (value === null) return `${quote(col)} IS NULL`;
|
|
348
|
+
params.push(value);
|
|
349
|
+
return `${quote(col)} = ?`;
|
|
350
|
+
}).join(" AND "),
|
|
351
|
+
params
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async function inTransaction$1(pool, fn) {
|
|
355
|
+
const c = await pool.getConnection();
|
|
356
|
+
try {
|
|
357
|
+
await c.beginTransaction();
|
|
358
|
+
const out = await fn(c);
|
|
359
|
+
await c.commit();
|
|
360
|
+
return out;
|
|
361
|
+
} catch (error) {
|
|
362
|
+
await c.rollback().catch(() => {});
|
|
363
|
+
throw error;
|
|
364
|
+
} finally {
|
|
365
|
+
c.release();
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async function run(pool, sql, params, limit, session) {
|
|
369
|
+
const started = performance.now();
|
|
370
|
+
const conn = await pool.getConnection();
|
|
371
|
+
let overflowed = false;
|
|
372
|
+
try {
|
|
373
|
+
if (session?.use) await conn.query("USE ??", [session.use]);
|
|
374
|
+
if (session?.readOnly) {
|
|
375
|
+
await conn.query("SET SESSION TRANSACTION READ ONLY");
|
|
376
|
+
await conn.query("START TRANSACTION READ ONLY");
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
...await new Promise((resolve, reject) => {
|
|
380
|
+
const rows = [];
|
|
381
|
+
let fields;
|
|
382
|
+
let affectedRows;
|
|
383
|
+
const finish = (truncated) => resolve({
|
|
384
|
+
columns: fields?.map((f) => ({
|
|
385
|
+
name: f.name,
|
|
386
|
+
type: fieldType(f)
|
|
387
|
+
})) ?? [],
|
|
388
|
+
rows,
|
|
389
|
+
rowCount: rows.length,
|
|
390
|
+
truncated,
|
|
391
|
+
...affectedRows === void 0 ? {} : { affectedRows }
|
|
392
|
+
});
|
|
393
|
+
const query = conn.connection.query({
|
|
394
|
+
sql,
|
|
395
|
+
values: params,
|
|
396
|
+
rowsAsArray: true
|
|
397
|
+
});
|
|
398
|
+
query.on("fields", (f) => {
|
|
399
|
+
fields = f;
|
|
400
|
+
});
|
|
401
|
+
query.on("result", (row) => {
|
|
402
|
+
if (!Array.isArray(row)) {
|
|
403
|
+
affectedRows = row.affectedRows;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (rows.length >= limit) {
|
|
407
|
+
overflowed = true;
|
|
408
|
+
conn.destroy();
|
|
409
|
+
finish(true);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
rows.push(row.map(normalizeCell));
|
|
413
|
+
});
|
|
414
|
+
query.on("error", reject);
|
|
415
|
+
query.on("end", () => finish(false));
|
|
416
|
+
}),
|
|
417
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
418
|
+
};
|
|
419
|
+
} finally {
|
|
420
|
+
if (!overflowed) {
|
|
421
|
+
if (session?.discard) conn.destroy();
|
|
422
|
+
else conn.release();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function rowsOf(pool, sql, params) {
|
|
427
|
+
const [rows] = await pool.query({
|
|
428
|
+
sql,
|
|
429
|
+
values: params
|
|
430
|
+
});
|
|
431
|
+
return rows;
|
|
432
|
+
}
|
|
433
|
+
// src/server/drivers/index.ts
|
|
434
|
+
var drivers = {
|
|
435
|
+
mysql: {
|
|
436
|
+
async test(conn) {
|
|
437
|
+
const rows = await rowsOf(getPool(conn), "SELECT VERSION() AS v", []);
|
|
438
|
+
return { version: String(rows[0]?.v) };
|
|
439
|
+
},
|
|
440
|
+
async listDatabases(conn) {
|
|
441
|
+
return (await rowsOf(getPool(conn), "SELECT schema_name AS name FROM information_schema.schemata ORDER BY schema_name", [])).map((r) => String(r.name));
|
|
442
|
+
},
|
|
443
|
+
async listTables(conn, database) {
|
|
444
|
+
return (await rowsOf(getPool(conn), "SELECT table_name AS name, table_type AS type FROM information_schema.tables WHERE table_schema = ? ORDER BY table_name", [database])).map((r) => ({
|
|
445
|
+
name: String(r.name),
|
|
446
|
+
isView: r.type === "VIEW"
|
|
447
|
+
}));
|
|
448
|
+
},
|
|
449
|
+
async describeTable(conn, ref) {
|
|
450
|
+
const pool = getPool(conn);
|
|
451
|
+
const columns = (await rowsOf(pool, `SELECT column_name AS name, column_type AS type, is_nullable AS nullable,
|
|
452
|
+
column_key AS col_key, column_default AS dflt
|
|
453
|
+
FROM information_schema.columns
|
|
454
|
+
WHERE table_schema = ? AND table_name = ?
|
|
455
|
+
ORDER BY ordinal_position`, [ref.database, ref.table])).map((r) => ({
|
|
456
|
+
name: String(r.name),
|
|
457
|
+
type: String(r.type),
|
|
458
|
+
nullable: r.nullable === "YES",
|
|
459
|
+
isPrimaryKey: r.col_key === "PRI",
|
|
460
|
+
default: r.dflt === null || r.dflt === void 0 ? null : String(r.dflt)
|
|
461
|
+
}));
|
|
462
|
+
const tables = await rowsOf(pool, "SELECT table_type AS type FROM information_schema.tables WHERE table_schema = ? AND table_name = ?", [ref.database, ref.table]);
|
|
463
|
+
const keyColumns = columns.filter((c) => c.isPrimaryKey).map((c) => c.name);
|
|
464
|
+
return {
|
|
465
|
+
columns,
|
|
466
|
+
keyColumns,
|
|
467
|
+
editBlockedReason: tables[0]?.type === "VIEW" ? "Views are read-only." : keyColumns.length === 0 ? "This table has no primary key, so rows cannot be addressed safely. Editing is disabled." : null
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
async sqlSchema(conn) {
|
|
471
|
+
const rows = await rowsOf(getPool(conn), `SELECT table_schema AS db, table_name AS tbl, column_name AS col, column_type AS type
|
|
472
|
+
FROM information_schema.columns
|
|
473
|
+
WHERE table_schema NOT IN ('mysql', 'sys', 'performance_schema', 'information_schema')
|
|
474
|
+
OR table_schema = ?
|
|
475
|
+
ORDER BY table_schema = ? DESC, table_schema, table_name, ordinal_position
|
|
476
|
+
LIMIT ${SQL_SCHEMA_ROW_CAP}`, [conn.database ?? "", conn.database ?? ""]);
|
|
477
|
+
return {
|
|
478
|
+
defaultDatabase: conn.database || null,
|
|
479
|
+
schema: groupColumns(rows)
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
query(conn, sql, { database, limit, readOnly }) {
|
|
483
|
+
const use = database ?? (conn.database || void 0);
|
|
484
|
+
return run(getPool(conn), sql, void 0, limit, {
|
|
485
|
+
use,
|
|
486
|
+
discard: true,
|
|
487
|
+
readOnly
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
async selectRows(conn, ref, opts) {
|
|
491
|
+
const pool = getPool(conn);
|
|
492
|
+
const where = opts.where?.trim() ? ` WHERE ${opts.where}` : "";
|
|
493
|
+
const order = opts.orderBy ? ` ORDER BY ${quote(opts.orderBy.column)} ${opts.orderBy.direction === "desc" ? "DESC" : "ASC"}` : "";
|
|
494
|
+
const result = await run(pool, `SELECT * FROM ${qualified(ref)}${where}${order} LIMIT ? OFFSET ?`, [opts.limit, opts.offset], opts.limit);
|
|
495
|
+
const counted = await rowsOf(pool, `SELECT COUNT(*) AS n FROM ${qualified(ref)}${where}`, []);
|
|
496
|
+
return {
|
|
497
|
+
result,
|
|
498
|
+
total: Number(counted[0]?.n ?? 0)
|
|
499
|
+
};
|
|
500
|
+
},
|
|
501
|
+
async updateRow(conn, ref, key, changes) {
|
|
502
|
+
const cols = Object.keys(changes);
|
|
503
|
+
if (cols.length === 0) return;
|
|
504
|
+
const w = whereKey(key);
|
|
505
|
+
const sql = `UPDATE ${qualified(ref)} SET ${cols.map((c) => `${quote(c)} = ?`).join(", ")} WHERE ${w.sql} LIMIT 1`;
|
|
506
|
+
const [header] = await getPool(conn).query(sql, [...cols.map((c) => changes[c]), ...w.params]);
|
|
507
|
+
if (header.affectedRows === 0) throw new Error(NO_MATCH);
|
|
508
|
+
},
|
|
509
|
+
async insertRow(conn, ref, values) {
|
|
510
|
+
const cols = Object.keys(values);
|
|
511
|
+
const sql = cols.length === 0 ? `INSERT INTO ${qualified(ref)} () VALUES ()` : `INSERT INTO ${qualified(ref)} (${cols.map(quote).join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`;
|
|
512
|
+
await getPool(conn).query(sql, cols.map((c) => values[c]));
|
|
513
|
+
},
|
|
514
|
+
async deleteRows(conn, ref, keys) {
|
|
515
|
+
await inTransaction$1(getPool(conn), async (c) => {
|
|
516
|
+
for (const key of keys) {
|
|
517
|
+
const w = whereKey(key);
|
|
518
|
+
const [header] = await c.query(`DELETE FROM ${qualified(ref)} WHERE ${w.sql} LIMIT 1`, w.params);
|
|
519
|
+
if (header.affectedRows === 0) throw new Error(NO_MATCH);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
},
|
|
523
|
+
async dispose(id) {
|
|
524
|
+
const cached = pools.get(id);
|
|
525
|
+
pools.delete(id);
|
|
526
|
+
await cached?.pool.end().catch(() => {});
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
clickhouse: clickhouseDriver
|
|
530
|
+
};
|
|
531
|
+
// src/server/store.ts
|
|
532
|
+
var db;
|
|
533
|
+
function open() {
|
|
534
|
+
if (db) return db;
|
|
535
|
+
const file = resolve(process.env.DB_PLAYER_DATA ?? "data/db-player.sqlite");
|
|
536
|
+
const dir = dirname(file);
|
|
537
|
+
mkdirSync(dir, {
|
|
538
|
+
recursive: true,
|
|
539
|
+
mode: 448
|
|
540
|
+
});
|
|
541
|
+
chmodSync(dir, 448);
|
|
542
|
+
closeSync(openSync(file, "a", 384));
|
|
543
|
+
chmodSync(file, 384);
|
|
544
|
+
db = new DatabaseSync(file);
|
|
545
|
+
db.exec(`
|
|
546
|
+
PRAGMA foreign_keys = ON;
|
|
547
|
+
CREATE TABLE IF NOT EXISTS folders (
|
|
548
|
+
id TEXT PRIMARY KEY,
|
|
549
|
+
name TEXT NOT NULL,
|
|
550
|
+
color TEXT NOT NULL,
|
|
551
|
+
position INTEGER NOT NULL
|
|
552
|
+
);
|
|
553
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
554
|
+
id TEXT PRIMARY KEY,
|
|
555
|
+
folder_id TEXT REFERENCES folders(id) ON DELETE SET NULL,
|
|
556
|
+
name TEXT NOT NULL,
|
|
557
|
+
kind TEXT NOT NULL,
|
|
558
|
+
host TEXT NOT NULL,
|
|
559
|
+
port INTEGER NOT NULL,
|
|
560
|
+
user TEXT NOT NULL,
|
|
561
|
+
password TEXT NOT NULL,
|
|
562
|
+
database TEXT,
|
|
563
|
+
secure INTEGER NOT NULL DEFAULT 0
|
|
564
|
+
);
|
|
565
|
+
CREATE TABLE IF NOT EXISTS tabs (
|
|
566
|
+
id TEXT PRIMARY KEY,
|
|
567
|
+
kind TEXT NOT NULL,
|
|
568
|
+
connection_id TEXT NOT NULL,
|
|
569
|
+
database TEXT,
|
|
570
|
+
table_name TEXT,
|
|
571
|
+
serial INTEGER,
|
|
572
|
+
sql TEXT NOT NULL DEFAULT '',
|
|
573
|
+
position INTEGER NOT NULL
|
|
574
|
+
);
|
|
575
|
+
CREATE TABLE IF NOT EXISTS workspace (
|
|
576
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
577
|
+
active_tab_id TEXT
|
|
578
|
+
);
|
|
579
|
+
INSERT OR IGNORE INTO workspace (id, active_tab_id) VALUES (1, NULL);
|
|
580
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
581
|
+
key TEXT PRIMARY KEY,
|
|
582
|
+
value TEXT NOT NULL
|
|
583
|
+
);
|
|
584
|
+
`);
|
|
585
|
+
migrateWorkspaceRevision(db);
|
|
586
|
+
return db;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* `workspace.revision` counts every change to the tabs or the active tab, so a client can tell which
|
|
590
|
+
* of two snapshots is newer. Triggers bump it, so no writer can forget and a new one is covered.
|
|
591
|
+
*/
|
|
592
|
+
function migrateWorkspaceRevision(d) {
|
|
593
|
+
if (!d.prepare("PRAGMA table_info(workspace)").all().some((c) => c.name === "revision")) d.exec("ALTER TABLE workspace ADD COLUMN revision INTEGER NOT NULL DEFAULT 0");
|
|
594
|
+
const bump = "UPDATE workspace SET revision = revision + 1 WHERE id = 1;";
|
|
595
|
+
d.exec(`
|
|
596
|
+
CREATE TRIGGER IF NOT EXISTS tabs_revision_insert AFTER INSERT ON tabs BEGIN ${bump} END;
|
|
597
|
+
CREATE TRIGGER IF NOT EXISTS tabs_revision_update AFTER UPDATE ON tabs BEGIN ${bump} END;
|
|
598
|
+
CREATE TRIGGER IF NOT EXISTS tabs_revision_delete AFTER DELETE ON tabs BEGIN ${bump} END;
|
|
599
|
+
CREATE TRIGGER IF NOT EXISTS workspace_revision_active
|
|
600
|
+
AFTER UPDATE OF active_tab_id ON workspace
|
|
601
|
+
BEGIN ${bump} END;
|
|
602
|
+
`);
|
|
603
|
+
}
|
|
604
|
+
var toFolder = (r) => ({
|
|
605
|
+
id: r.id,
|
|
606
|
+
name: r.name,
|
|
607
|
+
color: r.color,
|
|
608
|
+
position: r.position
|
|
609
|
+
});
|
|
610
|
+
var toConnection = (r) => ({
|
|
611
|
+
id: r.id,
|
|
612
|
+
folderId: r.folder_id,
|
|
613
|
+
name: r.name,
|
|
614
|
+
kind: r.kind,
|
|
615
|
+
host: r.host,
|
|
616
|
+
port: r.port,
|
|
617
|
+
user: r.user,
|
|
618
|
+
password: r.password,
|
|
619
|
+
database: r.database,
|
|
620
|
+
secure: r.secure === 1
|
|
621
|
+
});
|
|
622
|
+
function toSummary({ password, ...rest }) {
|
|
623
|
+
return {
|
|
624
|
+
...rest,
|
|
625
|
+
hasPassword: password.length > 0
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function listFolders() {
|
|
629
|
+
return open().prepare("SELECT * FROM folders ORDER BY position, name").all().map(toFolder);
|
|
630
|
+
}
|
|
631
|
+
function createFolder(input) {
|
|
632
|
+
const d = open();
|
|
633
|
+
const next = d.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS p FROM folders").get();
|
|
634
|
+
const id = crypto.randomUUID();
|
|
635
|
+
d.prepare("INSERT INTO folders (id, name, color, position) VALUES (?, ?, ?, ?)").run(id, input.name, input.color, next.p);
|
|
636
|
+
return {
|
|
637
|
+
id,
|
|
638
|
+
name: input.name,
|
|
639
|
+
color: input.color,
|
|
640
|
+
position: next.p
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function updateFolder(id, input) {
|
|
644
|
+
open().prepare("UPDATE folders SET name = ?, color = ? WHERE id = ?").run(input.name, input.color, id);
|
|
645
|
+
}
|
|
646
|
+
function deleteFolder(id) {
|
|
647
|
+
open().prepare("DELETE FROM folders WHERE id = ?").run(id);
|
|
648
|
+
}
|
|
649
|
+
function listConnections() {
|
|
650
|
+
return open().prepare("SELECT * FROM connections ORDER BY name").all().map(toConnection);
|
|
651
|
+
}
|
|
652
|
+
function getConnection(id) {
|
|
653
|
+
const row = open().prepare("SELECT * FROM connections WHERE id = ?").get(id);
|
|
654
|
+
if (!row) throw new Error("Connection not found");
|
|
655
|
+
return toConnection(row);
|
|
656
|
+
}
|
|
657
|
+
function createConnection(input) {
|
|
658
|
+
const id = crypto.randomUUID();
|
|
659
|
+
open().prepare(`INSERT INTO connections (id, folder_id, name, kind, host, port, user, password, database, secure)
|
|
660
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, input.folderId, input.name, input.kind, input.host, input.port, input.user, input.password, input.database || null, input.secure ? 1 : 0);
|
|
661
|
+
return getConnection(id);
|
|
662
|
+
}
|
|
663
|
+
function updateConnection(id, input) {
|
|
664
|
+
const current = getConnection(id);
|
|
665
|
+
const password = input.password === "" ? current.password : input.password;
|
|
666
|
+
open().prepare(`UPDATE connections SET folder_id = ?, name = ?, kind = ?, host = ?, port = ?, user = ?,
|
|
667
|
+
password = ?, database = ?, secure = ? WHERE id = ?`).run(input.folderId, input.name, input.kind, input.host, input.port, input.user, password, input.database || null, input.secure ? 1 : 0, id);
|
|
668
|
+
return getConnection(id);
|
|
669
|
+
}
|
|
670
|
+
function moveConnection(id, folderId) {
|
|
671
|
+
open().prepare("UPDATE connections SET folder_id = ? WHERE id = ?").run(folderId, id);
|
|
672
|
+
}
|
|
673
|
+
function deleteConnection(id) {
|
|
674
|
+
return inTransaction(() => {
|
|
675
|
+
const doomed = listTabs().filter((t) => t.target.connectionId === id).map((t) => t.id);
|
|
676
|
+
if (doomed.length > 0) removeTabs(doomed);
|
|
677
|
+
open().prepare("DELETE FROM connections WHERE id = ?").run(id);
|
|
678
|
+
return getWorkspace();
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
function inTransaction(fn) {
|
|
682
|
+
const d = open();
|
|
683
|
+
d.exec("BEGIN IMMEDIATE");
|
|
684
|
+
try {
|
|
685
|
+
const out = fn();
|
|
686
|
+
d.exec("COMMIT");
|
|
687
|
+
return out;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
d.exec("ROLLBACK");
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function toTab(r) {
|
|
694
|
+
const target = r.kind === "table" ? {
|
|
695
|
+
kind: "table",
|
|
696
|
+
connectionId: r.connection_id,
|
|
697
|
+
database: r.database ?? "",
|
|
698
|
+
table: r.table_name ?? ""
|
|
699
|
+
} : {
|
|
700
|
+
kind: "sql",
|
|
701
|
+
connectionId: r.connection_id,
|
|
702
|
+
database: r.database,
|
|
703
|
+
serial: r.serial ?? 1
|
|
704
|
+
};
|
|
705
|
+
return {
|
|
706
|
+
id: r.id,
|
|
707
|
+
target,
|
|
708
|
+
sql: r.sql,
|
|
709
|
+
position: r.position
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function listTabs() {
|
|
713
|
+
return open().prepare("SELECT * FROM tabs ORDER BY position").all().map(toTab);
|
|
714
|
+
}
|
|
715
|
+
function readActive() {
|
|
716
|
+
return open().prepare("SELECT active_tab_id AS id FROM workspace WHERE id = 1").get().id;
|
|
717
|
+
}
|
|
718
|
+
function readRevision() {
|
|
719
|
+
return open().prepare("SELECT revision FROM workspace WHERE id = 1").get().revision;
|
|
720
|
+
}
|
|
721
|
+
function writeActive(id) {
|
|
722
|
+
open().prepare("UPDATE workspace SET active_tab_id = ? WHERE id = 1").run(id);
|
|
723
|
+
}
|
|
724
|
+
function writeOrder(ids) {
|
|
725
|
+
const stmt = open().prepare("UPDATE tabs SET position = ? WHERE id = ?");
|
|
726
|
+
ids.forEach((id, i) => stmt.run(i, id));
|
|
727
|
+
}
|
|
728
|
+
function getWorkspace() {
|
|
729
|
+
const tabs = listTabs();
|
|
730
|
+
const active = readActive();
|
|
731
|
+
return {
|
|
732
|
+
tabs,
|
|
733
|
+
activeTabId: tabs.some((t) => t.id === active) ? active : tabs[0]?.id ?? null,
|
|
734
|
+
revision: readRevision()
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
var sameTable = (a, b) => a.kind === "table" && b.kind === "table" && a.connectionId === b.connectionId && a.database === b.database && a.table === b.table;
|
|
738
|
+
/** Table tabs are identified by their table, so opening one twice focuses it. SQL tabs are always new. */
|
|
739
|
+
function openTab(target, opts) {
|
|
740
|
+
return inTransaction(() => {
|
|
741
|
+
const tabs = listTabs();
|
|
742
|
+
const existing = tabs.find((t) => sameTable(t.target, target));
|
|
743
|
+
if (existing) {
|
|
744
|
+
writeActive(existing.id);
|
|
745
|
+
return {
|
|
746
|
+
workspace: getWorkspace(),
|
|
747
|
+
tabId: existing.id
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
const id = crypto.randomUUID();
|
|
751
|
+
let serial = null;
|
|
752
|
+
if (target.kind === "sql") {
|
|
753
|
+
const taken = new Set(tabs.flatMap((t) => t.target.kind === "sql" && t.target.connectionId === target.connectionId ? [t.target.serial] : []));
|
|
754
|
+
serial = 1;
|
|
755
|
+
while (taken.has(serial)) serial++;
|
|
756
|
+
}
|
|
757
|
+
const ids = tabs.map((t) => t.id);
|
|
758
|
+
const anchor = opts.after ?? readActive();
|
|
759
|
+
const anchorAt = anchor ? ids.indexOf(anchor) : -1;
|
|
760
|
+
ids.splice(anchorAt === -1 ? ids.length : anchorAt + 1, 0, id);
|
|
761
|
+
open().prepare(`INSERT INTO tabs (id, kind, connection_id, database, table_name, serial, sql, position)
|
|
762
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, target.kind, target.connectionId, target.database, target.kind === "table" ? target.table : null, serial, target.kind === "sql" ? opts.sql : "", ids.indexOf(id));
|
|
763
|
+
writeOrder(ids);
|
|
764
|
+
writeActive(id);
|
|
765
|
+
return {
|
|
766
|
+
workspace: getWorkspace(),
|
|
767
|
+
tabId: id
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Removes the tabs. When the active one goes, `prefer` takes over if it survives, else the right
|
|
773
|
+
* neighbour of the active tab, else the left.
|
|
774
|
+
*/
|
|
775
|
+
function removeTabs(doomed, prefer) {
|
|
776
|
+
const ordered = listTabs().map((t) => t.id);
|
|
777
|
+
const gone = new Set(doomed);
|
|
778
|
+
const active = getWorkspace().activeTabId;
|
|
779
|
+
const stmt = open().prepare("DELETE FROM tabs WHERE id = ?");
|
|
780
|
+
for (const id of gone) stmt.run(id);
|
|
781
|
+
const survivors = ordered.filter((id) => !gone.has(id));
|
|
782
|
+
writeOrder(survivors);
|
|
783
|
+
if (active && !gone.has(active)) return;
|
|
784
|
+
if (prefer && survivors.includes(prefer)) return writeActive(prefer);
|
|
785
|
+
const at = active ? ordered.indexOf(active) : -1;
|
|
786
|
+
const right = ordered.slice(at + 1).find((id) => !gone.has(id));
|
|
787
|
+
const left = ordered.slice(0, Math.max(at, 0)).reverse().find((id) => !gone.has(id));
|
|
788
|
+
writeActive(right ?? left ?? null);
|
|
789
|
+
}
|
|
790
|
+
function closeTabs(ids, prefer) {
|
|
791
|
+
return inTransaction(() => {
|
|
792
|
+
removeTabs(ids, prefer);
|
|
793
|
+
return getWorkspace();
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
function reorderTabs(ids) {
|
|
797
|
+
return inTransaction(() => {
|
|
798
|
+
const current = listTabs().map((t) => t.id);
|
|
799
|
+
if (ids.length !== current.length || !current.every((id) => ids.includes(id))) throw new Error("Tab order does not match the open tabs.");
|
|
800
|
+
writeOrder(ids);
|
|
801
|
+
return getWorkspace();
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
function setActiveTab(id) {
|
|
805
|
+
if (!listTabs().some((t) => t.id === id)) return;
|
|
806
|
+
writeActive(id);
|
|
807
|
+
}
|
|
808
|
+
function updateTabDraft(id, sql) {
|
|
809
|
+
open().prepare("UPDATE tabs SET sql = ? WHERE id = ? AND kind = 'sql'").run(sql, id);
|
|
810
|
+
}
|
|
811
|
+
function setTabDatabase(id, database) {
|
|
812
|
+
open().prepare("UPDATE tabs SET database = ? WHERE id = ? AND kind = 'sql'").run(database, id);
|
|
813
|
+
}
|
|
814
|
+
var MCP_TOKEN_KEY = "mcp_token";
|
|
815
|
+
var newToken = () => Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32))).toString("base64url");
|
|
816
|
+
/** Generated on first use. `INSERT OR IGNORE` keeps concurrent first callers on one token. */
|
|
817
|
+
function getMcpToken() {
|
|
818
|
+
const d = open();
|
|
819
|
+
d.prepare("INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)").run(MCP_TOKEN_KEY, newToken());
|
|
820
|
+
return d.prepare("SELECT value FROM settings WHERE key = ?").get(MCP_TOKEN_KEY).value;
|
|
821
|
+
}
|
|
822
|
+
function regenerateMcpToken() {
|
|
823
|
+
const token = newToken();
|
|
824
|
+
open().prepare("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)").run(MCP_TOKEN_KEY, token);
|
|
825
|
+
return token;
|
|
826
|
+
}
|
|
827
|
+
export { DB_KIND_INFO as C, folderInputSchema as D, connectionInputSchema as E, newTabTargetSchema as O, DB_KINDS as S, FOLDER_COLOR_NAMES as T, toSummary as _, deleteFolder as a, updateTabDraft as b, getWorkspace as c, moveConnection as d, openTab as f, setTabDatabase as g, setActiveTab as h, deleteConnection as i, tabIdSchema as k, listConnections as l, reorderTabs as m, createConnection as n, getConnection as o, regenerateMcpToken as p, createFolder as r, getMcpToken as s, closeTabs as t, listFolders as u, updateConnection as v, FOLDER_COLORS as w, drivers as x, updateFolder as y };
|