@youtyan/code-viewer 0.2.11 → 0.4.0
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 +55 -18
- package/dist/code-viewer.js +1054 -183
- package/package.json +1 -1
- package/web/app.js +5238 -2295
- package/web/index.html +15 -22
- package/web/style.css +865 -68
package/dist/code-viewer.js
CHANGED
|
@@ -759,6 +759,16 @@ import {
|
|
|
759
759
|
statSync
|
|
760
760
|
} from "node:fs";
|
|
761
761
|
import { join as join2 } from "node:path";
|
|
762
|
+
function normalizeBlameRef(ref, base) {
|
|
763
|
+
const rawRef = ref || "worktree";
|
|
764
|
+
if (base === "worktree" && rawRef !== "worktree") {
|
|
765
|
+
return { base: "HEAD", ref: rawRef };
|
|
766
|
+
}
|
|
767
|
+
if (base === "HEAD" && rawRef === "worktree") {
|
|
768
|
+
return { base: "HEAD", ref: "HEAD" };
|
|
769
|
+
}
|
|
770
|
+
return { base, ref: rawRef };
|
|
771
|
+
}
|
|
762
772
|
function run(args, cwd) {
|
|
763
773
|
return runSync(args, cwd);
|
|
764
774
|
}
|
|
@@ -1063,6 +1073,13 @@ function commitHistory(cwd, options) {
|
|
|
1063
1073
|
const skip = Math.max(0, Math.floor(options.skip) || 0);
|
|
1064
1074
|
const limit = Math.max(1, Math.min(Math.floor(options.limit) || 1, MAX_HISTORY_LIMIT));
|
|
1065
1075
|
const { filterArgs, pathspec, shaTerm } = historyQueryArgs(options.query || "");
|
|
1076
|
+
const pathFilter = (options.path || "").trim();
|
|
1077
|
+
const pathArgs = [];
|
|
1078
|
+
if (pathFilter && !pathFilter.includes("\x00") && !pathFilter.startsWith("-")) {
|
|
1079
|
+
if (!pathFilter.endsWith("/"))
|
|
1080
|
+
pathArgs.push("--follow");
|
|
1081
|
+
pathArgs.push("--", pathFilter);
|
|
1082
|
+
}
|
|
1066
1083
|
const res = run([
|
|
1067
1084
|
"git",
|
|
1068
1085
|
"log",
|
|
@@ -1072,7 +1089,8 @@ function commitHistory(cwd, options) {
|
|
|
1072
1089
|
`--format=${HISTORY_FORMAT}`,
|
|
1073
1090
|
...filterArgs,
|
|
1074
1091
|
verified.stdout.trim(),
|
|
1075
|
-
...pathspec
|
|
1092
|
+
...pathspec,
|
|
1093
|
+
...pathArgs
|
|
1076
1094
|
], cwd);
|
|
1077
1095
|
if (res.code !== 0)
|
|
1078
1096
|
return { commits: [], hasMore: false, error: "git log failed" };
|
|
@@ -1173,6 +1191,130 @@ function numstatZ(args, cwd) {
|
|
|
1173
1191
|
function isToolInternalPath(path) {
|
|
1174
1192
|
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".code-viewer");
|
|
1175
1193
|
}
|
|
1194
|
+
function syntheticUncommittedBlameFromWorktree(cwd, path) {
|
|
1195
|
+
const filePath = join2(cwd, path);
|
|
1196
|
+
try {
|
|
1197
|
+
const stat = statSync(filePath);
|
|
1198
|
+
if (!stat.isFile())
|
|
1199
|
+
return { lines: [], commits: {}, error: "not a file" };
|
|
1200
|
+
const text = readFileSync(filePath, "utf8");
|
|
1201
|
+
const normalized = text.replace(/\r\n/g, `
|
|
1202
|
+
`).replace(/\r/g, `
|
|
1203
|
+
`);
|
|
1204
|
+
const lineCount = normalized.length ? normalized.endsWith(`
|
|
1205
|
+
`) ? normalized.length - 1 === 0 ? 1 : normalized.split(`
|
|
1206
|
+
`).length - 1 : normalized.split(`
|
|
1207
|
+
`).length : 1;
|
|
1208
|
+
const lines = [];
|
|
1209
|
+
for (let i = 1;i <= lineCount; i++) {
|
|
1210
|
+
lines.push({ lineNo: i, sha: BLAME_ZERO_SHA, isUncommitted: true });
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
lines,
|
|
1214
|
+
commits: {
|
|
1215
|
+
[BLAME_ZERO_SHA]: {
|
|
1216
|
+
sha: BLAME_ZERO_SHA,
|
|
1217
|
+
author: "Not Committed Yet",
|
|
1218
|
+
authorMail: "",
|
|
1219
|
+
authorTime: 0,
|
|
1220
|
+
summary: "Working tree",
|
|
1221
|
+
isUncommitted: true
|
|
1222
|
+
}
|
|
1223
|
+
},
|
|
1224
|
+
isUntracked: true,
|
|
1225
|
+
isSynthetic: true
|
|
1226
|
+
};
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") {
|
|
1229
|
+
return { lines: [], commits: {}, error: "file not found" };
|
|
1230
|
+
}
|
|
1231
|
+
return { lines: [], commits: {}, error: "file not readable" };
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
function blame(cwd, options) {
|
|
1235
|
+
const path = options.path;
|
|
1236
|
+
if (!path || path.includes("\x00") || path.startsWith("-")) {
|
|
1237
|
+
return { lines: [], commits: {}, error: "invalid path" };
|
|
1238
|
+
}
|
|
1239
|
+
const normalized = normalizeBlameRef(options.ref, options.base);
|
|
1240
|
+
const args = ["git", "blame", "--porcelain"];
|
|
1241
|
+
if (normalized.base === "HEAD") {
|
|
1242
|
+
if (normalized.ref.startsWith("-") || normalized.ref.includes("\x00"))
|
|
1243
|
+
return { lines: [], commits: {}, error: "invalid ref" };
|
|
1244
|
+
args.push(normalized.ref);
|
|
1245
|
+
}
|
|
1246
|
+
args.push("--", path);
|
|
1247
|
+
const res = run(args, cwd);
|
|
1248
|
+
if (res.code !== 0) {
|
|
1249
|
+
if (normalized.base === "worktree") {
|
|
1250
|
+
return syntheticUncommittedBlameFromWorktree(cwd, path);
|
|
1251
|
+
}
|
|
1252
|
+
return {
|
|
1253
|
+
lines: [],
|
|
1254
|
+
commits: {},
|
|
1255
|
+
error: res.stderr.trim() || "blame failed"
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const lines = [];
|
|
1259
|
+
const commits = {};
|
|
1260
|
+
const rawLines = res.stdout.split(`
|
|
1261
|
+
`);
|
|
1262
|
+
let i = 0;
|
|
1263
|
+
while (i < rawLines.length) {
|
|
1264
|
+
const headerLine = rawLines[i];
|
|
1265
|
+
if (!headerLine) {
|
|
1266
|
+
i++;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
const headerMatch = /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/.exec(headerLine);
|
|
1270
|
+
if (!headerMatch) {
|
|
1271
|
+
i++;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
const sha = headerMatch[1];
|
|
1275
|
+
const finalLine = Number(headerMatch[3]);
|
|
1276
|
+
i++;
|
|
1277
|
+
let commit = commits[sha];
|
|
1278
|
+
if (!commit) {
|
|
1279
|
+
commit = {
|
|
1280
|
+
sha,
|
|
1281
|
+
author: "",
|
|
1282
|
+
authorMail: "",
|
|
1283
|
+
authorTime: 0,
|
|
1284
|
+
summary: "",
|
|
1285
|
+
isUncommitted: sha === BLAME_ZERO_SHA
|
|
1286
|
+
};
|
|
1287
|
+
commits[sha] = commit;
|
|
1288
|
+
}
|
|
1289
|
+
while (i < rawLines.length && !rawLines[i].startsWith("\t")) {
|
|
1290
|
+
const metaLine = rawLines[i++];
|
|
1291
|
+
if (!metaLine)
|
|
1292
|
+
continue;
|
|
1293
|
+
const sp = metaLine.indexOf(" ");
|
|
1294
|
+
const key = sp >= 0 ? metaLine.slice(0, sp) : metaLine;
|
|
1295
|
+
const val = sp >= 0 ? metaLine.slice(sp + 1) : "";
|
|
1296
|
+
if (key === "author" && !commit.author)
|
|
1297
|
+
commit.author = val;
|
|
1298
|
+
else if (key === "author-mail" && !commit.authorMail)
|
|
1299
|
+
commit.authorMail = val.replace(/^</, "").replace(/>$/, "");
|
|
1300
|
+
else if (key === "author-time" && !commit.authorTime)
|
|
1301
|
+
commit.authorTime = Number(val) || 0;
|
|
1302
|
+
else if (key === "summary" && !commit.summary)
|
|
1303
|
+
commit.summary = val;
|
|
1304
|
+
}
|
|
1305
|
+
if (i < rawLines.length && rawLines[i].startsWith("\t"))
|
|
1306
|
+
i++;
|
|
1307
|
+
if (Number.isFinite(finalLine) && finalLine > 0) {
|
|
1308
|
+
lines.push({
|
|
1309
|
+
lineNo: finalLine,
|
|
1310
|
+
sha,
|
|
1311
|
+
isUncommitted: sha === BLAME_ZERO_SHA
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
lines.sort((a, b) => a.lineNo - b.lineNo);
|
|
1316
|
+
return { lines, commits };
|
|
1317
|
+
}
|
|
1176
1318
|
function untracked(cwd, path = "") {
|
|
1177
1319
|
const args = ["git", "ls-files", "--others", "--exclude-standard"];
|
|
1178
1320
|
if (path)
|
|
@@ -1509,7 +1651,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
|
|
|
1509
1651
|
lineTruncated
|
|
1510
1652
|
};
|
|
1511
1653
|
}
|
|
1512
|
-
var WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
|
|
1654
|
+
var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", DEFAULT_WORKTREE_OMIT_DIR_NAMES, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
|
|
1513
1655
|
var init_git = __esm(() => {
|
|
1514
1656
|
init_runtime();
|
|
1515
1657
|
DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
|
|
@@ -4016,12 +4158,41 @@ function sanitizeDbUiPrefs(raw) {
|
|
|
4016
4158
|
}
|
|
4017
4159
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
4018
4160
|
}
|
|
4161
|
+
function sanitizeDbUiExpandedTables(raw) {
|
|
4162
|
+
if (!isRecord(raw))
|
|
4163
|
+
return;
|
|
4164
|
+
const out = {};
|
|
4165
|
+
let scopeCount = 0;
|
|
4166
|
+
for (const [scopeRaw, tablesRaw] of Object.entries(raw)) {
|
|
4167
|
+
if (scopeCount >= MAX_DB_UI_EXPANDED_SCOPES)
|
|
4168
|
+
break;
|
|
4169
|
+
const scope = safeObjectKey(scopeRaw);
|
|
4170
|
+
if (!scope)
|
|
4171
|
+
continue;
|
|
4172
|
+
const tables = normalizeStringList(tablesRaw, {
|
|
4173
|
+
maxItems: MAX_DB_UI_TABLES,
|
|
4174
|
+
maxLen: MAX_KEY_LEN,
|
|
4175
|
+
sort: true
|
|
4176
|
+
});
|
|
4177
|
+
if (!tables || tables.length === 0)
|
|
4178
|
+
continue;
|
|
4179
|
+
out[scope] = tables;
|
|
4180
|
+
scopeCount++;
|
|
4181
|
+
}
|
|
4182
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
4183
|
+
}
|
|
4019
4184
|
function sanitizeDbUiState(raw) {
|
|
4020
4185
|
if (!isRecord(raw))
|
|
4021
4186
|
return emptyDbUiState();
|
|
4022
4187
|
const prefs = sanitizeDbUiPrefs(raw.prefs);
|
|
4188
|
+
const expandedTables = sanitizeDbUiExpandedTables(raw.expandedTables);
|
|
4023
4189
|
if (!isRecord(raw.columnWidths)) {
|
|
4024
|
-
|
|
4190
|
+
const out2 = emptyDbUiState();
|
|
4191
|
+
if (expandedTables)
|
|
4192
|
+
out2.expandedTables = expandedTables;
|
|
4193
|
+
if (prefs)
|
|
4194
|
+
out2.prefs = prefs;
|
|
4195
|
+
return out2;
|
|
4025
4196
|
}
|
|
4026
4197
|
const columnWidths = {};
|
|
4027
4198
|
let dbCount = 0;
|
|
@@ -4062,6 +4233,8 @@ function sanitizeDbUiState(raw) {
|
|
|
4062
4233
|
dbCount++;
|
|
4063
4234
|
}
|
|
4064
4235
|
const out = { version: 1, columnWidths };
|
|
4236
|
+
if (expandedTables)
|
|
4237
|
+
out.expandedTables = expandedTables;
|
|
4065
4238
|
if (prefs)
|
|
4066
4239
|
out.prefs = prefs;
|
|
4067
4240
|
return out;
|
|
@@ -4079,12 +4252,30 @@ function mergeDbUiPrefs(current, patch) {
|
|
|
4079
4252
|
}
|
|
4080
4253
|
return Object.keys(next).length > 0 ? next : undefined;
|
|
4081
4254
|
}
|
|
4255
|
+
function mergeDbUiExpandedTables(current, patch) {
|
|
4256
|
+
if (!isRecord(patch))
|
|
4257
|
+
return current;
|
|
4258
|
+
const next = { ...current ?? {} };
|
|
4259
|
+
for (const [scope, tablesRaw] of Object.entries(patch)) {
|
|
4260
|
+
if (tablesRaw === null) {
|
|
4261
|
+
delete next[scope];
|
|
4262
|
+
continue;
|
|
4263
|
+
}
|
|
4264
|
+
next[scope] = tablesRaw;
|
|
4265
|
+
}
|
|
4266
|
+
return sanitizeDbUiExpandedTables(next);
|
|
4267
|
+
}
|
|
4082
4268
|
function mergeDbUiState(current, patch) {
|
|
4083
4269
|
if (!isRecord(patch))
|
|
4084
4270
|
return current;
|
|
4085
4271
|
const mergedPrefs = "prefs" in patch ? mergeDbUiPrefs(current.prefs, patch.prefs) : current.prefs;
|
|
4272
|
+
const mergedExpandedTables = "expandedTables" in patch ? mergeDbUiExpandedTables(current.expandedTables, patch.expandedTables) : current.expandedTables;
|
|
4086
4273
|
if (!isRecord(patch.columnWidths)) {
|
|
4087
4274
|
const merged = { ...current, version: 1 };
|
|
4275
|
+
if (mergedExpandedTables)
|
|
4276
|
+
merged.expandedTables = mergedExpandedTables;
|
|
4277
|
+
else
|
|
4278
|
+
delete merged.expandedTables;
|
|
4088
4279
|
if (mergedPrefs)
|
|
4089
4280
|
merged.prefs = mergedPrefs;
|
|
4090
4281
|
else
|
|
@@ -4124,6 +4315,7 @@ function mergeDbUiState(current, patch) {
|
|
|
4124
4315
|
...current,
|
|
4125
4316
|
...patch,
|
|
4126
4317
|
columnWidths,
|
|
4318
|
+
expandedTables: mergedExpandedTables,
|
|
4127
4319
|
prefs: mergedPrefs,
|
|
4128
4320
|
version: 1
|
|
4129
4321
|
});
|
|
@@ -4155,7 +4347,7 @@ async function patchDbUiState(root, patch) {
|
|
|
4155
4347
|
return { state: next, result: next };
|
|
4156
4348
|
});
|
|
4157
4349
|
}
|
|
4158
|
-
var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
|
|
4350
|
+
var CODE_VIEWER_DIR2 = ".code-viewer", SETTINGS_FILE_NAME = "settings.json", VIEW_STATE_FILE_NAME = "view-state.json", DB_UI_FILE_NAME = "db-ui.json", MAX_SETTINGS_BYTES = 200000, MAX_VIEW_STATE_BYTES = 1e6, MAX_DB_UI_BYTES = 1e6, MAX_REF_LEN = 1024, MAX_KEY_LEN = 2048, MAX_VIEW_ITEMS = 20000, MAX_DB_UI_DBS = 200, MAX_DB_UI_TABLES = 500, MAX_DB_UI_COLUMNS = 1000, MAX_DB_UI_EXPANDED_SCOPES = 500, DB_UI_BOOL_PREF_KEYS, settingsStore, viewStateStore, dbUiStore;
|
|
4159
4351
|
var init_state_store = __esm(() => {
|
|
4160
4352
|
init_json_store();
|
|
4161
4353
|
DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
|
|
@@ -4576,52 +4768,37 @@ function serializeDbRow(row) {
|
|
|
4576
4768
|
function serializeDbRows(rows) {
|
|
4577
4769
|
return rows.map(serializeDbRow);
|
|
4578
4770
|
}
|
|
4771
|
+
function coerceDbValue(value, columnType) {
|
|
4772
|
+
if (value === null)
|
|
4773
|
+
return null;
|
|
4774
|
+
const t = (columnType || "").toLowerCase();
|
|
4775
|
+
if (/bool/.test(t)) {
|
|
4776
|
+
const v = value.trim().toLowerCase();
|
|
4777
|
+
if (v === "")
|
|
4778
|
+
return null;
|
|
4779
|
+
if (v === "true" || v === "t" || v === "1")
|
|
4780
|
+
return true;
|
|
4781
|
+
if (v === "false" || v === "f" || v === "0")
|
|
4782
|
+
return false;
|
|
4783
|
+
return value;
|
|
4784
|
+
}
|
|
4785
|
+
if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
|
|
4786
|
+
const trimmed = value.trim();
|
|
4787
|
+
if (trimmed === "")
|
|
4788
|
+
return null;
|
|
4789
|
+
const n = Number(trimmed);
|
|
4790
|
+
if (Number.isFinite(n) && String(n) === trimmed)
|
|
4791
|
+
return n;
|
|
4792
|
+
return value;
|
|
4793
|
+
}
|
|
4794
|
+
return value;
|
|
4795
|
+
}
|
|
4579
4796
|
var MIN_SAFE, MAX_SAFE;
|
|
4580
4797
|
var init_serialize = __esm(() => {
|
|
4581
4798
|
MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
|
4582
4799
|
MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
4583
4800
|
});
|
|
4584
4801
|
|
|
4585
|
-
// web-src/server/database/sources/sql-snapshot.ts
|
|
4586
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4587
|
-
function normalizeRawValue(v) {
|
|
4588
|
-
if (v === null)
|
|
4589
|
-
return "\\N";
|
|
4590
|
-
if (typeof v === "bigint")
|
|
4591
|
-
return v.toString();
|
|
4592
|
-
if (v instanceof Uint8Array) {
|
|
4593
|
-
return `\\x${Buffer.from(v).toString("hex")}`;
|
|
4594
|
-
}
|
|
4595
|
-
return String(v);
|
|
4596
|
-
}
|
|
4597
|
-
function rowToPayloadJson(columns, row) {
|
|
4598
|
-
const obj = {};
|
|
4599
|
-
for (let i = 0;i < columns.length; i++) {
|
|
4600
|
-
obj[columns[i]] = serializeDbValue(row[i]);
|
|
4601
|
-
}
|
|
4602
|
-
return JSON.stringify(obj);
|
|
4603
|
-
}
|
|
4604
|
-
function computeRowHash(columns, row) {
|
|
4605
|
-
const parts = columns.map((_, i) => normalizeRawValue(row[i]));
|
|
4606
|
-
return createHash2("sha256").update(parts.join("\t")).digest("hex");
|
|
4607
|
-
}
|
|
4608
|
-
function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
|
|
4609
|
-
if (pkColumns.length === 0) {
|
|
4610
|
-
return JSON.stringify({ __rowIndex: rowIndex });
|
|
4611
|
-
}
|
|
4612
|
-
const keyObj = {};
|
|
4613
|
-
for (const pk of pkColumns) {
|
|
4614
|
-
const idx = allColumns.indexOf(pk);
|
|
4615
|
-
if (idx >= 0)
|
|
4616
|
-
keyObj[pk] = serializeDbValue(row[idx]);
|
|
4617
|
-
}
|
|
4618
|
-
return JSON.stringify(keyObj);
|
|
4619
|
-
}
|
|
4620
|
-
var SQL_SNAPSHOT_BATCH_SIZE = 500;
|
|
4621
|
-
var init_sql_snapshot = __esm(() => {
|
|
4622
|
-
init_serialize();
|
|
4623
|
-
});
|
|
4624
|
-
|
|
4625
4802
|
// web-src/server/database/sql-utils.ts
|
|
4626
4803
|
function sanitizeIdentifier(name, kind = "sqlite") {
|
|
4627
4804
|
if (kind === "mysql")
|
|
@@ -4683,6 +4860,189 @@ function filterOrderByColumns(orderBy, columnNames) {
|
|
|
4683
4860
|
const filtered = orderBy.filter((order) => validColumns.has(order.column));
|
|
4684
4861
|
return filtered.length > 0 ? filtered : undefined;
|
|
4685
4862
|
}
|
|
4863
|
+
function buildOrderClause(orderBy, kind = "sqlite") {
|
|
4864
|
+
if (!orderBy?.length)
|
|
4865
|
+
return "";
|
|
4866
|
+
const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
|
|
4867
|
+
return ` ORDER BY ${parts.join(", ")}`;
|
|
4868
|
+
}
|
|
4869
|
+
function useParamsFor(kind) {
|
|
4870
|
+
return kind === "sqlite";
|
|
4871
|
+
}
|
|
4872
|
+
function placeValue(coerced, kind, useParams, params) {
|
|
4873
|
+
if (useParams) {
|
|
4874
|
+
params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
|
|
4875
|
+
return "?";
|
|
4876
|
+
}
|
|
4877
|
+
if (coerced === null)
|
|
4878
|
+
return "NULL";
|
|
4879
|
+
if (typeof coerced === "number")
|
|
4880
|
+
return String(coerced);
|
|
4881
|
+
if (typeof coerced === "boolean")
|
|
4882
|
+
return coerced ? "TRUE" : "FALSE";
|
|
4883
|
+
const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
|
|
4884
|
+
return escapeSqlString(text, kind);
|
|
4885
|
+
}
|
|
4886
|
+
function coerceCell(cell, columnType) {
|
|
4887
|
+
return coerceDbValue(cell.value, columnType);
|
|
4888
|
+
}
|
|
4889
|
+
function buildInsertSql(table, cells, columnTypes, kind) {
|
|
4890
|
+
if (cells.length === 0) {
|
|
4891
|
+
throw new Error("insert requires at least one column value");
|
|
4892
|
+
}
|
|
4893
|
+
const useParams = useParamsFor(kind);
|
|
4894
|
+
const params = [];
|
|
4895
|
+
const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
|
|
4896
|
+
const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
|
|
4897
|
+
const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
|
|
4898
|
+
return { sql, params };
|
|
4899
|
+
}
|
|
4900
|
+
function buildUpdateSql(table, set, pk, columnTypes, kind) {
|
|
4901
|
+
if (set.length === 0) {
|
|
4902
|
+
throw new Error("update requires at least one column to set");
|
|
4903
|
+
}
|
|
4904
|
+
if (pk.length === 0) {
|
|
4905
|
+
throw new Error("update requires a primary key condition");
|
|
4906
|
+
}
|
|
4907
|
+
const useParams = useParamsFor(kind);
|
|
4908
|
+
const params = [];
|
|
4909
|
+
const setSql = set.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(", ");
|
|
4910
|
+
const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
|
|
4911
|
+
const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
|
|
4912
|
+
return { sql, params };
|
|
4913
|
+
}
|
|
4914
|
+
function buildDeleteSql(table, pk, columnTypes, kind) {
|
|
4915
|
+
if (pk.length === 0) {
|
|
4916
|
+
throw new Error("delete requires a primary key condition");
|
|
4917
|
+
}
|
|
4918
|
+
const useParams = useParamsFor(kind);
|
|
4919
|
+
const params = [];
|
|
4920
|
+
const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
|
|
4921
|
+
const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
|
|
4922
|
+
return { sql, params };
|
|
4923
|
+
}
|
|
4924
|
+
var init_sql_utils = __esm(() => {
|
|
4925
|
+
init_serialize();
|
|
4926
|
+
});
|
|
4927
|
+
|
|
4928
|
+
// web-src/server/database/mutate.ts
|
|
4929
|
+
function assertCells(cells, label) {
|
|
4930
|
+
if (!Array.isArray(cells)) {
|
|
4931
|
+
throw new Error(`${label} must be an array`);
|
|
4932
|
+
}
|
|
4933
|
+
for (const cell of cells) {
|
|
4934
|
+
if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
|
|
4935
|
+
throw new Error(`${label} contains an invalid cell`);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
return cells;
|
|
4939
|
+
}
|
|
4940
|
+
function buildMutationStatements(table, mutations, columns, kind) {
|
|
4941
|
+
if (!Array.isArray(mutations) || mutations.length === 0) {
|
|
4942
|
+
throw new Error("no mutations provided");
|
|
4943
|
+
}
|
|
4944
|
+
if (mutations.length > MAX_MUTATIONS) {
|
|
4945
|
+
throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
|
|
4946
|
+
}
|
|
4947
|
+
const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
|
|
4948
|
+
const columnNames = new Set(columns.map((c) => c.name));
|
|
4949
|
+
const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
4950
|
+
const pkNames = new Set(pkColumns);
|
|
4951
|
+
const requireKnownColumns = (cells, label) => {
|
|
4952
|
+
for (const cell of cells) {
|
|
4953
|
+
if (!columnNames.has(cell.column)) {
|
|
4954
|
+
throw new Error(`unknown column: ${cell.column}`);
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
};
|
|
4958
|
+
const requirePrimaryKey = (pk) => {
|
|
4959
|
+
if (pkColumns.length === 0) {
|
|
4960
|
+
throw new Error("table has no primary key; row update/delete is not supported");
|
|
4961
|
+
}
|
|
4962
|
+
const provided = new Set(pk.map((c) => c.column));
|
|
4963
|
+
for (const name of pkColumns) {
|
|
4964
|
+
if (!provided.has(name)) {
|
|
4965
|
+
throw new Error(`missing primary key column: ${name}`);
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
for (const cell of pk) {
|
|
4969
|
+
if (!pkNames.has(cell.column)) {
|
|
4970
|
+
throw new Error(`not a primary key column: ${cell.column}`);
|
|
4971
|
+
}
|
|
4972
|
+
if (cell.value === null) {
|
|
4973
|
+
throw new Error(`primary key column cannot be null: ${cell.column}`);
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
};
|
|
4977
|
+
const statements = [];
|
|
4978
|
+
for (const mutation of mutations) {
|
|
4979
|
+
if (!mutation || typeof mutation !== "object") {
|
|
4980
|
+
throw new Error("invalid mutation");
|
|
4981
|
+
}
|
|
4982
|
+
if (mutation.kind === "insert") {
|
|
4983
|
+
const values = assertCells(mutation.values, "insert values");
|
|
4984
|
+
requireKnownColumns(values, "insert values");
|
|
4985
|
+
statements.push(buildInsertSql(table, values, columnTypes, kind));
|
|
4986
|
+
} else if (mutation.kind === "update") {
|
|
4987
|
+
const pk = assertCells(mutation.pk, "update pk");
|
|
4988
|
+
const values = assertCells(mutation.values, "update values");
|
|
4989
|
+
requirePrimaryKey(pk);
|
|
4990
|
+
requireKnownColumns(values, "update values");
|
|
4991
|
+
statements.push(buildUpdateSql(table, values, pk, columnTypes, kind));
|
|
4992
|
+
} else if (mutation.kind === "delete") {
|
|
4993
|
+
const pk = assertCells(mutation.pk, "delete pk");
|
|
4994
|
+
requirePrimaryKey(pk);
|
|
4995
|
+
statements.push(buildDeleteSql(table, pk, columnTypes, kind));
|
|
4996
|
+
} else {
|
|
4997
|
+
throw new Error(`unknown mutation kind: ${mutation.kind}`);
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
return statements;
|
|
5001
|
+
}
|
|
5002
|
+
var MAX_MUTATIONS = 1000;
|
|
5003
|
+
var init_mutate = __esm(() => {
|
|
5004
|
+
init_sql_utils();
|
|
5005
|
+
});
|
|
5006
|
+
|
|
5007
|
+
// web-src/server/database/sources/sql-snapshot.ts
|
|
5008
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5009
|
+
function normalizeRawValue(v) {
|
|
5010
|
+
if (v === null)
|
|
5011
|
+
return "\\N";
|
|
5012
|
+
if (typeof v === "bigint")
|
|
5013
|
+
return v.toString();
|
|
5014
|
+
if (v instanceof Uint8Array) {
|
|
5015
|
+
return `\\x${Buffer.from(v).toString("hex")}`;
|
|
5016
|
+
}
|
|
5017
|
+
return String(v);
|
|
5018
|
+
}
|
|
5019
|
+
function rowToPayloadJson(columns, row) {
|
|
5020
|
+
const obj = {};
|
|
5021
|
+
for (let i = 0;i < columns.length; i++) {
|
|
5022
|
+
obj[columns[i]] = serializeDbValue(row[i]);
|
|
5023
|
+
}
|
|
5024
|
+
return JSON.stringify(obj);
|
|
5025
|
+
}
|
|
5026
|
+
function computeRowHash(columns, row) {
|
|
5027
|
+
const parts = columns.map((_, i) => normalizeRawValue(row[i]));
|
|
5028
|
+
return createHash2("sha256").update(parts.join("\t")).digest("hex");
|
|
5029
|
+
}
|
|
5030
|
+
function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
|
|
5031
|
+
if (pkColumns.length === 0) {
|
|
5032
|
+
return JSON.stringify({ __rowIndex: rowIndex });
|
|
5033
|
+
}
|
|
5034
|
+
const keyObj = {};
|
|
5035
|
+
for (const pk of pkColumns) {
|
|
5036
|
+
const idx = allColumns.indexOf(pk);
|
|
5037
|
+
if (idx >= 0)
|
|
5038
|
+
keyObj[pk] = serializeDbValue(row[idx]);
|
|
5039
|
+
}
|
|
5040
|
+
return JSON.stringify(keyObj);
|
|
5041
|
+
}
|
|
5042
|
+
var SQL_SNAPSHOT_BATCH_SIZE = 500;
|
|
5043
|
+
var init_sql_snapshot = __esm(() => {
|
|
5044
|
+
init_serialize();
|
|
5045
|
+
});
|
|
4686
5046
|
|
|
4687
5047
|
// web-src/server/database/adapters/spawn-runner.ts
|
|
4688
5048
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -4932,6 +5292,24 @@ var init_docker_utils = __esm(() => {
|
|
|
4932
5292
|
};
|
|
4933
5293
|
});
|
|
4934
5294
|
|
|
5295
|
+
// web-src/server/database/adapters/sql-capture.ts
|
|
5296
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5297
|
+
function recordSql(sql) {
|
|
5298
|
+
const bucket = storage.getStore();
|
|
5299
|
+
if (!bucket)
|
|
5300
|
+
return;
|
|
5301
|
+
bucket.sqls.push(sql);
|
|
5302
|
+
}
|
|
5303
|
+
async function captureSql(fn) {
|
|
5304
|
+
const bucket = { sqls: [] };
|
|
5305
|
+
const result = await storage.run(bucket, fn);
|
|
5306
|
+
return { result, executedSql: bucket.sqls };
|
|
5307
|
+
}
|
|
5308
|
+
var storage;
|
|
5309
|
+
var init_sql_capture = __esm(() => {
|
|
5310
|
+
storage = new AsyncLocalStorage;
|
|
5311
|
+
});
|
|
5312
|
+
|
|
4935
5313
|
// web-src/server/database/adapters/docker.ts
|
|
4936
5314
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
4937
5315
|
function dockerDatabasesCacheKey(serviceName, kind, cwd) {
|
|
@@ -5087,6 +5465,7 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
|
|
|
5087
5465
|
});
|
|
5088
5466
|
}
|
|
5089
5467
|
async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
|
|
5468
|
+
recordSql(sql);
|
|
5090
5469
|
if (spawnSyncImpl2 !== spawnSync3)
|
|
5091
5470
|
return execInContainer(config, sql, timeoutMs);
|
|
5092
5471
|
const args = buildExecArgs(config, sql);
|
|
@@ -5174,12 +5553,6 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
|
|
|
5174
5553
|
const rows = lines.map((line) => splitTsvLine(line, false));
|
|
5175
5554
|
return { columns: [], rows };
|
|
5176
5555
|
}
|
|
5177
|
-
function buildOrderClause(orderBy, kind) {
|
|
5178
|
-
if (!orderBy?.length)
|
|
5179
|
-
return "";
|
|
5180
|
-
const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
|
|
5181
|
-
return ` ORDER BY ${parts.join(", ")}`;
|
|
5182
|
-
}
|
|
5183
5556
|
function isMysqlSpatialType(type) {
|
|
5184
5557
|
const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
|
|
5185
5558
|
return MYSQL_SPATIAL_TYPES.has(baseType);
|
|
@@ -5268,9 +5641,12 @@ function createDockerAdapter(config) {
|
|
|
5268
5641
|
const tableLiteral = table.replace(/'/g, "''");
|
|
5269
5642
|
if (config.kind === "postgresql") {
|
|
5270
5643
|
const schemaLiteral = postgresSchemaLiteral();
|
|
5271
|
-
return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END FROM information_schema.columns c LEFT JOIN (SELECT
|
|
5644
|
+
return `SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, CASE WHEN pk.column_name IS NULL THEN 'NO' ELSE 'YES' END, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum LEFT JOIN (SELECT att.attname AS column_name FROM pg_index ix JOIN pg_class clp ON clp.oid = ix.indrelid JOIN pg_namespace nn ON nn.oid = clp.relnamespace JOIN pg_attribute att ON att.attrelid = ix.indrelid AND att.attnum = ANY(ix.indkey) WHERE ix.indisprimary AND nn.nspname = ${schemaLiteral} AND clp.relname = '${tableLiteral}') pk ON pk.column_name = c.column_name WHERE c.table_schema = ${schemaLiteral} AND c.table_name = '${tableLiteral}' ORDER BY c.ordinal_position`;
|
|
5272
5645
|
}
|
|
5273
|
-
return `SELECT column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
|
|
5646
|
+
return `SELECT column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = '${tableLiteral}' ORDER BY ordinal_position`;
|
|
5647
|
+
}
|
|
5648
|
+
function columnCommentFromInfoValue(value) {
|
|
5649
|
+
return value ? value : null;
|
|
5274
5650
|
}
|
|
5275
5651
|
function columnsFromInfoRows(rows) {
|
|
5276
5652
|
if (config.kind === "postgresql") {
|
|
@@ -5279,7 +5655,8 @@ function createDockerAdapter(config) {
|
|
|
5279
5655
|
type: row[1],
|
|
5280
5656
|
nullable: row[2] === "YES",
|
|
5281
5657
|
primaryKey: row[4] === "YES",
|
|
5282
|
-
defaultValue: row[3] === "" ? null : row[3]
|
|
5658
|
+
defaultValue: row[3] === "" ? null : row[3],
|
|
5659
|
+
comment: columnCommentFromInfoValue(row[5])
|
|
5283
5660
|
}));
|
|
5284
5661
|
}
|
|
5285
5662
|
return rows.map((row) => ({
|
|
@@ -5287,7 +5664,8 @@ function createDockerAdapter(config) {
|
|
|
5287
5664
|
type: row[1],
|
|
5288
5665
|
nullable: row[2] === "YES",
|
|
5289
5666
|
primaryKey: row[4] === "PRI",
|
|
5290
|
-
defaultValue: row[3] === "NULL" ? null : row[3]
|
|
5667
|
+
defaultValue: row[3] === "NULL" ? null : row[3],
|
|
5668
|
+
comment: columnCommentFromInfoValue(row[5])
|
|
5291
5669
|
}));
|
|
5292
5670
|
}
|
|
5293
5671
|
async function fetchColumnsAsyncUncached(table, signal) {
|
|
@@ -5386,10 +5764,10 @@ function createDockerAdapter(config) {
|
|
|
5386
5764
|
let sql;
|
|
5387
5765
|
if (config.kind === "postgresql") {
|
|
5388
5766
|
const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
|
|
5389
|
-
sql = `SELECT table_name, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = ${postgresSchemaLiteral()} AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
|
|
5767
|
+
sql = `SELECT c.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default, COALESCE(d.description, '') FROM information_schema.columns c JOIN pg_namespace n ON n.nspname = c.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = c.table_name LEFT JOIN pg_attribute a ON a.attrelid = cls.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped LEFT JOIN pg_description d ON d.objoid = cls.oid AND d.objsubid = a.attnum WHERE c.table_schema = ${postgresSchemaLiteral()} AND c.table_name IN (${inList}) ORDER BY c.table_name, c.ordinal_position`;
|
|
5390
5768
|
} else {
|
|
5391
5769
|
const inList = uncached.map((t) => `'${t.replace(/'/g, "''")}'`).join(",");
|
|
5392
|
-
sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
|
|
5770
|
+
sql = `SELECT table_name, column_name, column_type, is_nullable, column_default, column_key, column_comment FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name IN (${inList}) ORDER BY table_name, ordinal_position`;
|
|
5393
5771
|
}
|
|
5394
5772
|
try {
|
|
5395
5773
|
const queryResult = await execAsync(sql, signal);
|
|
@@ -5425,7 +5803,8 @@ function createDockerAdapter(config) {
|
|
|
5425
5803
|
type: row[2],
|
|
5426
5804
|
nullable: row[3] === "YES",
|
|
5427
5805
|
primaryKey: pkCols.has(row[1]),
|
|
5428
|
-
defaultValue: row[4] === "" ? null : row[4]
|
|
5806
|
+
defaultValue: row[4] === "" ? null : row[4],
|
|
5807
|
+
comment: columnCommentFromInfoValue(row[5])
|
|
5429
5808
|
}));
|
|
5430
5809
|
} else {
|
|
5431
5810
|
cols = rows.map((row) => ({
|
|
@@ -5433,7 +5812,8 @@ function createDockerAdapter(config) {
|
|
|
5433
5812
|
type: row[2],
|
|
5434
5813
|
nullable: row[3] === "YES",
|
|
5435
5814
|
primaryKey: row[5] === "PRI",
|
|
5436
|
-
defaultValue: row[4] === "NULL" ? null : row[4]
|
|
5815
|
+
defaultValue: row[4] === "NULL" ? null : row[4],
|
|
5816
|
+
comment: columnCommentFromInfoValue(row[6])
|
|
5437
5817
|
}));
|
|
5438
5818
|
}
|
|
5439
5819
|
columnCache.set(tbl, cols);
|
|
@@ -5526,12 +5906,7 @@ function createDockerAdapter(config) {
|
|
|
5526
5906
|
async getFilteredTablePageWithMeta(table, options, signal) {
|
|
5527
5907
|
const id = tableIdentifier(table);
|
|
5528
5908
|
const columnsPromise = tableMetaCache.getColumns(table, () => fetchColumnsAsyncUncached(table, signal));
|
|
5529
|
-
|
|
5530
|
-
try {
|
|
5531
|
-
columns = await columnsPromise;
|
|
5532
|
-
} catch (err) {
|
|
5533
|
-
throw err;
|
|
5534
|
-
}
|
|
5909
|
+
const columns = await columnsPromise;
|
|
5535
5910
|
const columnNames = columns.map((column) => column.name);
|
|
5536
5911
|
const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames), config.kind);
|
|
5537
5912
|
const where = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), config.kind, filterExactColumns(options.exact, columnNames)).where;
|
|
@@ -5600,6 +5975,19 @@ function createDockerAdapter(config) {
|
|
|
5600
5975
|
rowCount: Math.min(result.rows.length, maxRows)
|
|
5601
5976
|
};
|
|
5602
5977
|
},
|
|
5978
|
+
async applyMutations(table, mutations, signal) {
|
|
5979
|
+
const columns = await this.getColumnsAsync(table, signal);
|
|
5980
|
+
if (columns.length === 0) {
|
|
5981
|
+
throw new Error(`unknown table: ${table}`);
|
|
5982
|
+
}
|
|
5983
|
+
const statements = buildMutationStatements(table, mutations, columns, config.kind);
|
|
5984
|
+
const body = statements.map((s) => s.sql).join(`;
|
|
5985
|
+
`);
|
|
5986
|
+
const wrapped = config.kind === "postgresql" ? `BEGIN; SET LOCAL search_path = ${sanitizeIdentifier(currentPostgresSchema(), config.kind)}; ${body}; COMMIT` : `START TRANSACTION; ${body}; COMMIT`;
|
|
5987
|
+
await execAsync(wrapped, signal);
|
|
5988
|
+
this.invalidateTableMetaCache?.(table);
|
|
5989
|
+
return { affected: statements.length };
|
|
5990
|
+
},
|
|
5603
5991
|
invalidateTableMetaCache(table) {
|
|
5604
5992
|
tableMetaCache.invalidate(table);
|
|
5605
5993
|
if (table) {
|
|
@@ -5781,9 +6169,12 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
|
|
|
5781
6169
|
}
|
|
5782
6170
|
var COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", MYSQL_SPATIAL_TYPES;
|
|
5783
6171
|
var init_docker = __esm(() => {
|
|
6172
|
+
init_mutate();
|
|
5784
6173
|
init_sql_snapshot();
|
|
6174
|
+
init_sql_utils();
|
|
5785
6175
|
init_docker_utils();
|
|
5786
6176
|
init_spawn_runner();
|
|
6177
|
+
init_sql_capture();
|
|
5787
6178
|
dockerDatabasesCache = new Map;
|
|
5788
6179
|
dockerSchemasCache = new Map;
|
|
5789
6180
|
spawnSyncImpl2 = spawnSync3;
|
|
@@ -5800,17 +6191,8 @@ var init_docker = __esm(() => {
|
|
|
5800
6191
|
]);
|
|
5801
6192
|
});
|
|
5802
6193
|
|
|
5803
|
-
// web-src/server/database/
|
|
5804
|
-
function
|
|
5805
|
-
const stmt = db.prepare(sql);
|
|
5806
|
-
if (typeof stmt.safeIntegers === "function") {
|
|
5807
|
-
try {
|
|
5808
|
-
stmt.safeIntegers(true);
|
|
5809
|
-
} catch {}
|
|
5810
|
-
}
|
|
5811
|
-
return stmt;
|
|
5812
|
-
}
|
|
5813
|
-
async function getSqliteClass() {
|
|
6194
|
+
// web-src/server/database/sqlite-driver.ts
|
|
6195
|
+
async function loadSqliteClass() {
|
|
5814
6196
|
if (cachedDbClass)
|
|
5815
6197
|
return cachedDbClass;
|
|
5816
6198
|
try {
|
|
@@ -5825,11 +6207,17 @@ async function getSqliteClass() {
|
|
|
5825
6207
|
} catch {}
|
|
5826
6208
|
throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
|
|
5827
6209
|
}
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
6210
|
+
var cachedDbClass = null;
|
|
6211
|
+
|
|
6212
|
+
// web-src/server/database/adapters/sqlite.ts
|
|
6213
|
+
function safePrepare(db, sql) {
|
|
6214
|
+
const stmt = db.prepare(sql);
|
|
6215
|
+
if (typeof stmt.safeIntegers === "function") {
|
|
6216
|
+
try {
|
|
6217
|
+
stmt.safeIntegers(true);
|
|
6218
|
+
} catch {}
|
|
6219
|
+
}
|
|
6220
|
+
return stmt;
|
|
5833
6221
|
}
|
|
5834
6222
|
function queryRowsToResult(rows, columns) {
|
|
5835
6223
|
const columnNames = rows.length > 0 ? Object.keys(rows[0]) : columns.map((c) => c.name);
|
|
@@ -5851,7 +6239,30 @@ function queryColumns(db, table) {
|
|
|
5851
6239
|
defaultValue: row.dflt_value
|
|
5852
6240
|
}));
|
|
5853
6241
|
}
|
|
5854
|
-
function
|
|
6242
|
+
function wrapDbWithSqlCapture(rawDb) {
|
|
6243
|
+
return new Proxy(rawDb, {
|
|
6244
|
+
get(target, prop, receiver) {
|
|
6245
|
+
if (prop === "prepare") {
|
|
6246
|
+
return (sql) => {
|
|
6247
|
+
recordSql(sql);
|
|
6248
|
+
return target.prepare(sql);
|
|
6249
|
+
};
|
|
6250
|
+
}
|
|
6251
|
+
return Reflect.get(target, prop, receiver);
|
|
6252
|
+
}
|
|
6253
|
+
});
|
|
6254
|
+
}
|
|
6255
|
+
function createSqliteAdapter(rawDb, openRawWriteDb) {
|
|
6256
|
+
const db = wrapDbWithSqlCapture(rawDb);
|
|
6257
|
+
let writeDb = null;
|
|
6258
|
+
const getWriteDb = () => {
|
|
6259
|
+
if (!openRawWriteDb) {
|
|
6260
|
+
throw new Error("writes are not supported for this connection");
|
|
6261
|
+
}
|
|
6262
|
+
if (!writeDb)
|
|
6263
|
+
writeDb = wrapDbWithSqlCapture(openRawWriteDb());
|
|
6264
|
+
return writeDb;
|
|
6265
|
+
};
|
|
5855
6266
|
const adapter = {
|
|
5856
6267
|
kind: "sqlite",
|
|
5857
6268
|
model: "sql",
|
|
@@ -5951,7 +6362,7 @@ function createSqliteAdapter(db) {
|
|
|
5951
6362
|
return this.getTableRowCounts(tables);
|
|
5952
6363
|
},
|
|
5953
6364
|
getTablePage(table, options) {
|
|
5954
|
-
const order =
|
|
6365
|
+
const order = buildOrderClause(options.orderBy);
|
|
5955
6366
|
const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
|
|
5956
6367
|
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
5957
6368
|
const cols = queryColumns(db, table);
|
|
@@ -5963,7 +6374,7 @@ function createSqliteAdapter(db) {
|
|
|
5963
6374
|
async getTablePageWithMeta(table, options) {
|
|
5964
6375
|
const columns = queryColumns(db, table);
|
|
5965
6376
|
const orderBy = filterOrderByColumns(options.orderBy, columns.map((column) => column.name));
|
|
5966
|
-
const order =
|
|
6377
|
+
const order = buildOrderClause(orderBy);
|
|
5967
6378
|
const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
|
|
5968
6379
|
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
5969
6380
|
const result = queryRowsToResult(rows, columns);
|
|
@@ -5979,7 +6390,7 @@ function createSqliteAdapter(db) {
|
|
|
5979
6390
|
const columns = queryColumns(db, table);
|
|
5980
6391
|
const columnNames = columns.map((column) => column.name);
|
|
5981
6392
|
const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
|
|
5982
|
-
const order =
|
|
6393
|
+
const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames));
|
|
5983
6394
|
const tableId = sanitizeIdentifier(table);
|
|
5984
6395
|
const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
|
|
5985
6396
|
const countRow = safePrepare(db, `SELECT COUNT(*) AS cnt FROM ${tableId}${whereClause}`).get(...filter.params);
|
|
@@ -6047,7 +6458,36 @@ function createSqliteAdapter(db) {
|
|
|
6047
6458
|
async getTriggersAsync(table) {
|
|
6048
6459
|
return this.getTriggers(table);
|
|
6049
6460
|
},
|
|
6461
|
+
async applyMutations(table, mutations) {
|
|
6462
|
+
const columns = queryColumns(db, table);
|
|
6463
|
+
if (columns.length === 0) {
|
|
6464
|
+
throw new Error(`unknown table: ${table}`);
|
|
6465
|
+
}
|
|
6466
|
+
const statements = buildMutationStatements(table, mutations, columns, "sqlite");
|
|
6467
|
+
const wdb = getWriteDb();
|
|
6468
|
+
let affected = 0;
|
|
6469
|
+
wdb.prepare("BEGIN").run();
|
|
6470
|
+
try {
|
|
6471
|
+
for (const stmt of statements) {
|
|
6472
|
+
const result = wdb.prepare(stmt.sql).run(...stmt.params);
|
|
6473
|
+
affected += result.changes ?? 0;
|
|
6474
|
+
}
|
|
6475
|
+
wdb.prepare("COMMIT").run();
|
|
6476
|
+
} catch (err) {
|
|
6477
|
+
try {
|
|
6478
|
+
wdb.prepare("ROLLBACK").run();
|
|
6479
|
+
} catch {}
|
|
6480
|
+
throw err;
|
|
6481
|
+
}
|
|
6482
|
+
return { affected };
|
|
6483
|
+
},
|
|
6050
6484
|
close() {
|
|
6485
|
+
if (writeDb) {
|
|
6486
|
+
try {
|
|
6487
|
+
writeDb.close();
|
|
6488
|
+
} catch {}
|
|
6489
|
+
writeDb = null;
|
|
6490
|
+
}
|
|
6051
6491
|
db.close();
|
|
6052
6492
|
},
|
|
6053
6493
|
async* iterateForSnapshot(table, signal) {
|
|
@@ -6085,14 +6525,18 @@ function createSqliteAdapter(db) {
|
|
|
6085
6525
|
};
|
|
6086
6526
|
return adapter;
|
|
6087
6527
|
}
|
|
6088
|
-
var
|
|
6528
|
+
var sqliteAdapterFactory;
|
|
6089
6529
|
var init_sqlite = __esm(() => {
|
|
6530
|
+
init_mutate();
|
|
6090
6531
|
init_sql_snapshot();
|
|
6532
|
+
init_sql_utils();
|
|
6533
|
+
init_sql_capture();
|
|
6091
6534
|
sqliteAdapterFactory = {
|
|
6092
6535
|
async open(path) {
|
|
6093
|
-
const DbClass = await
|
|
6536
|
+
const DbClass = await loadSqliteClass();
|
|
6094
6537
|
const db = new DbClass(path, { readonly: true, create: false });
|
|
6095
|
-
|
|
6538
|
+
const openWriteDb = () => new DbClass(path);
|
|
6539
|
+
return createSqliteAdapter(db, openWriteDb);
|
|
6096
6540
|
}
|
|
6097
6541
|
};
|
|
6098
6542
|
});
|
|
@@ -6861,6 +7305,7 @@ function getPrimaryKeyColumnsFromColumns(columns) {
|
|
|
6861
7305
|
}
|
|
6862
7306
|
var init_global_search = __esm(() => {
|
|
6863
7307
|
init_serialize();
|
|
7308
|
+
init_sql_utils();
|
|
6864
7309
|
});
|
|
6865
7310
|
|
|
6866
7311
|
// web-src/server/database/adapters/elasticsearch.ts
|
|
@@ -7060,6 +7505,38 @@ function createElasticsearchAdapter(config) {
|
|
|
7060
7505
|
primaryTerm: parsed._primary_term
|
|
7061
7506
|
};
|
|
7062
7507
|
}
|
|
7508
|
+
function assertIndex(index) {
|
|
7509
|
+
if (!index || index.includes("/") || index.includes("?")) {
|
|
7510
|
+
throw new Error(`invalid index name: ${index}`);
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
async function writeDocAsync(opts) {
|
|
7514
|
+
assertIndex(opts.index);
|
|
7515
|
+
const idGiven = typeof opts.id === "string" && opts.id !== "";
|
|
7516
|
+
let path;
|
|
7517
|
+
let method;
|
|
7518
|
+
if (idGiven && opts.create) {
|
|
7519
|
+
path = `/${encodeURIComponent(opts.index)}/_create/${encodeURIComponent(opts.id)}`;
|
|
7520
|
+
method = "PUT";
|
|
7521
|
+
} else if (idGiven) {
|
|
7522
|
+
path = `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`;
|
|
7523
|
+
method = "PUT";
|
|
7524
|
+
if (opts.seqNo !== undefined && opts.primaryTerm !== undefined) {
|
|
7525
|
+
path += `?if_seq_no=${opts.seqNo}&if_primary_term=${opts.primaryTerm}`;
|
|
7526
|
+
}
|
|
7527
|
+
} else {
|
|
7528
|
+
path = `/${encodeURIComponent(opts.index)}/_doc`;
|
|
7529
|
+
method = "POST";
|
|
7530
|
+
}
|
|
7531
|
+
const resp = await callJsonAsync(method, path, opts.source, "_doc write", opts.signal);
|
|
7532
|
+
return { id: resp._id ?? opts.id ?? "", result: resp.result ?? "" };
|
|
7533
|
+
}
|
|
7534
|
+
async function deleteDocAsync(opts) {
|
|
7535
|
+
assertIndex(opts.index);
|
|
7536
|
+
if (!opts.id)
|
|
7537
|
+
throw new Error("missing doc id");
|
|
7538
|
+
await callJsonAsync("DELETE", `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`, undefined, "_doc delete", opts.signal);
|
|
7539
|
+
}
|
|
7063
7540
|
async function* iterateForSnapshot(container, signal) {
|
|
7064
7541
|
const { index, query: query2 } = parseEsSnapshotContainer(container);
|
|
7065
7542
|
const PAGE = 1000;
|
|
@@ -7133,6 +7610,8 @@ function createElasticsearchAdapter(config) {
|
|
|
7133
7610
|
getMappingAsync,
|
|
7134
7611
|
searchDocsAsync,
|
|
7135
7612
|
getDocAsync,
|
|
7613
|
+
writeDocAsync,
|
|
7614
|
+
deleteDocAsync,
|
|
7136
7615
|
iterateForSnapshot,
|
|
7137
7616
|
listSnapshotContainers,
|
|
7138
7617
|
query,
|
|
@@ -7612,6 +8091,51 @@ async function handleMapping(req, cwd, url, omitDirNames) {
|
|
|
7612
8091
|
return handleError("elasticsearch", "read elasticsearch mapping", err);
|
|
7613
8092
|
}
|
|
7614
8093
|
}
|
|
8094
|
+
async function handleWrite(req, cwd, omitDirNames) {
|
|
8095
|
+
const parsed = await parseBoundedJsonBody(req, 4 * 1024 * 1024, "payload too large");
|
|
8096
|
+
if (parsed instanceof Response)
|
|
8097
|
+
return parsed;
|
|
8098
|
+
const body = parsed;
|
|
8099
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
8100
|
+
return textError("missing db", 400);
|
|
8101
|
+
}
|
|
8102
|
+
if (typeof body.index !== "string" || body.index === "") {
|
|
8103
|
+
return textError("missing index", 400);
|
|
8104
|
+
}
|
|
8105
|
+
const id = typeof body.id === "string" ? body.id : undefined;
|
|
8106
|
+
const r = await resolveEs(cwd, body.db, req.signal, omitDirNames);
|
|
8107
|
+
if (r instanceof Response)
|
|
8108
|
+
return r;
|
|
8109
|
+
try {
|
|
8110
|
+
if (body.op === "delete") {
|
|
8111
|
+
if (!id)
|
|
8112
|
+
return textError("missing id", 400);
|
|
8113
|
+
await r.explorer.deleteDocAsync({
|
|
8114
|
+
index: body.index,
|
|
8115
|
+
id,
|
|
8116
|
+
signal: req.signal
|
|
8117
|
+
});
|
|
8118
|
+
return json({ ok: true });
|
|
8119
|
+
}
|
|
8120
|
+
if (body.source === null || typeof body.source !== "object" || Array.isArray(body.source)) {
|
|
8121
|
+
return textError("source must be a JSON object", 400);
|
|
8122
|
+
}
|
|
8123
|
+
const seqNo = typeof body.seqNo === "number" ? body.seqNo : undefined;
|
|
8124
|
+
const primaryTerm = typeof body.primaryTerm === "number" ? body.primaryTerm : undefined;
|
|
8125
|
+
const result = await r.explorer.writeDocAsync({
|
|
8126
|
+
index: body.index,
|
|
8127
|
+
id,
|
|
8128
|
+
source: body.source,
|
|
8129
|
+
seqNo,
|
|
8130
|
+
primaryTerm,
|
|
8131
|
+
create: body.op === "create",
|
|
8132
|
+
signal: req.signal
|
|
8133
|
+
});
|
|
8134
|
+
return json({ ok: true, id: result.id, result: result.result });
|
|
8135
|
+
} catch (err) {
|
|
8136
|
+
return handleError("elasticsearch", "write elasticsearch doc", err);
|
|
8137
|
+
}
|
|
8138
|
+
}
|
|
7615
8139
|
async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
7616
8140
|
const wrap = createQueryStrippedLogger("elasticsearch", req, url);
|
|
7617
8141
|
return dispatchRoutes(req, url, {
|
|
@@ -7631,6 +8155,11 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
|
|
|
7631
8155
|
methods: ["GET"],
|
|
7632
8156
|
handler: () => handleDoc(req, cwd, url, omitDirNames)
|
|
7633
8157
|
},
|
|
8158
|
+
"/_db/elasticsearch/write": {
|
|
8159
|
+
methods: ["POST"],
|
|
8160
|
+
sideEffect: true,
|
|
8161
|
+
handler: () => handleWrite(req, cwd, omitDirNames)
|
|
8162
|
+
},
|
|
7634
8163
|
"/_db/elasticsearch/search": {
|
|
7635
8164
|
methods: ["GET", "POST"],
|
|
7636
8165
|
sideEffect: (m) => m === "POST",
|
|
@@ -7649,6 +8178,7 @@ var init_handle_elasticsearch = __esm(() => {
|
|
|
7649
8178
|
var exports_redis = {};
|
|
7650
8179
|
__export(exports_redis, {
|
|
7651
8180
|
openRedisExplorerAsync: () => openRedisExplorerAsync,
|
|
8181
|
+
createRedisAdapter: () => createRedisAdapter,
|
|
7652
8182
|
canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
|
|
7653
8183
|
});
|
|
7654
8184
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -8170,6 +8700,41 @@ function createRedisAdapter(config) {
|
|
|
8170
8700
|
async function listSnapshotContainers() {
|
|
8171
8701
|
return [];
|
|
8172
8702
|
}
|
|
8703
|
+
async function runWriteAsync(args, signal) {
|
|
8704
|
+
const res = await execRedisCliAsync(config, args, 1e4, signal);
|
|
8705
|
+
if (res.code !== 0) {
|
|
8706
|
+
throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
|
|
8707
|
+
}
|
|
8708
|
+
const out = res.stdout.trim();
|
|
8709
|
+
if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
|
|
8710
|
+
throw new Error(out);
|
|
8711
|
+
}
|
|
8712
|
+
}
|
|
8713
|
+
async function setStringAsync(opts) {
|
|
8714
|
+
await runWriteAsync(["-n", String(opts.db), "SET", opts.key, opts.value], opts.signal);
|
|
8715
|
+
}
|
|
8716
|
+
async function createStringAsync(opts) {
|
|
8717
|
+
const res = await execRedisCliAsync(config, ["-n", String(opts.db), "SET", opts.key, opts.value, "NX"], 1e4, opts.signal);
|
|
8718
|
+
if (res.code !== 0) {
|
|
8719
|
+
throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
|
|
8720
|
+
}
|
|
8721
|
+
const out = res.stdout.trim();
|
|
8722
|
+
if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
|
|
8723
|
+
throw new Error(out);
|
|
8724
|
+
}
|
|
8725
|
+
if (!/\bOK\b/i.test(out)) {
|
|
8726
|
+
throw new Error(`key already exists: ${opts.key}`);
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
async function setHashFieldAsync(opts) {
|
|
8730
|
+
await runWriteAsync(["-n", String(opts.db), "HSET", opts.key, opts.field, opts.value], opts.signal);
|
|
8731
|
+
}
|
|
8732
|
+
async function setListIndexAsync(opts) {
|
|
8733
|
+
await runWriteAsync(["-n", String(opts.db), "LSET", opts.key, String(opts.index), opts.value], opts.signal);
|
|
8734
|
+
}
|
|
8735
|
+
async function deleteKeyAsync(opts) {
|
|
8736
|
+
await runWriteAsync(["-n", String(opts.db), "DEL", opts.key], opts.signal);
|
|
8737
|
+
}
|
|
8173
8738
|
return {
|
|
8174
8739
|
kind: "redis",
|
|
8175
8740
|
model: "kv",
|
|
@@ -8177,6 +8742,11 @@ function createRedisAdapter(config) {
|
|
|
8177
8742
|
listDatabasesAsync,
|
|
8178
8743
|
listKeysAsync,
|
|
8179
8744
|
getValueAsync,
|
|
8745
|
+
setStringAsync,
|
|
8746
|
+
createStringAsync,
|
|
8747
|
+
setHashFieldAsync,
|
|
8748
|
+
setListIndexAsync,
|
|
8749
|
+
deleteKeyAsync,
|
|
8180
8750
|
iterateForSnapshot,
|
|
8181
8751
|
listSnapshotContainers,
|
|
8182
8752
|
close() {}
|
|
@@ -8253,6 +8823,78 @@ async function handleKeys(req, cwd, url, omitDirNames) {
|
|
|
8253
8823
|
return handleError("redis", "list redis keys", err);
|
|
8254
8824
|
}
|
|
8255
8825
|
}
|
|
8826
|
+
async function handleWrite2(req, cwd, omitDirNames) {
|
|
8827
|
+
const parsed = await parseBoundedJsonBody(req, 1024 * 1024, "payload too large");
|
|
8828
|
+
if (parsed instanceof Response)
|
|
8829
|
+
return parsed;
|
|
8830
|
+
const body = parsed;
|
|
8831
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
8832
|
+
return textError("missing db", 400);
|
|
8833
|
+
}
|
|
8834
|
+
const dbIndex = Number(body.dbIndex);
|
|
8835
|
+
if (!Number.isInteger(dbIndex) || dbIndex < 0 || dbIndex > 15) {
|
|
8836
|
+
return textError("dbIndex must be an integer in 0..15", 400);
|
|
8837
|
+
}
|
|
8838
|
+
if (typeof body.key !== "string" || body.key === "") {
|
|
8839
|
+
return textError("missing key", 400);
|
|
8840
|
+
}
|
|
8841
|
+
const op = body.op;
|
|
8842
|
+
const value = typeof body.value === "string" ? body.value : "";
|
|
8843
|
+
const r = await resolveRedis(cwd, body.db, req.signal, omitDirNames);
|
|
8844
|
+
if (r instanceof Response)
|
|
8845
|
+
return r;
|
|
8846
|
+
try {
|
|
8847
|
+
if (op === "setString") {
|
|
8848
|
+
await r.explorer.setStringAsync({
|
|
8849
|
+
db: dbIndex,
|
|
8850
|
+
key: body.key,
|
|
8851
|
+
value,
|
|
8852
|
+
signal: req.signal
|
|
8853
|
+
});
|
|
8854
|
+
} else if (op === "createString") {
|
|
8855
|
+
await r.explorer.createStringAsync({
|
|
8856
|
+
db: dbIndex,
|
|
8857
|
+
key: body.key,
|
|
8858
|
+
value,
|
|
8859
|
+
signal: req.signal
|
|
8860
|
+
});
|
|
8861
|
+
} else if (op === "setHashField") {
|
|
8862
|
+
if (typeof body.field !== "string" || body.field === "") {
|
|
8863
|
+
return textError("missing field", 400);
|
|
8864
|
+
}
|
|
8865
|
+
await r.explorer.setHashFieldAsync({
|
|
8866
|
+
db: dbIndex,
|
|
8867
|
+
key: body.key,
|
|
8868
|
+
field: body.field,
|
|
8869
|
+
value,
|
|
8870
|
+
signal: req.signal
|
|
8871
|
+
});
|
|
8872
|
+
} else if (op === "setListIndex") {
|
|
8873
|
+
const index = Number(body.index);
|
|
8874
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
8875
|
+
return textError("index must be a non-negative integer", 400);
|
|
8876
|
+
}
|
|
8877
|
+
await r.explorer.setListIndexAsync({
|
|
8878
|
+
db: dbIndex,
|
|
8879
|
+
key: body.key,
|
|
8880
|
+
index,
|
|
8881
|
+
value,
|
|
8882
|
+
signal: req.signal
|
|
8883
|
+
});
|
|
8884
|
+
} else if (op === "delete") {
|
|
8885
|
+
await r.explorer.deleteKeyAsync({
|
|
8886
|
+
db: dbIndex,
|
|
8887
|
+
key: body.key,
|
|
8888
|
+
signal: req.signal
|
|
8889
|
+
});
|
|
8890
|
+
} else {
|
|
8891
|
+
return textError(`unknown op: ${String(op)}`, 400);
|
|
8892
|
+
}
|
|
8893
|
+
return json({ ok: true });
|
|
8894
|
+
} catch (err) {
|
|
8895
|
+
return handleError("redis", "write redis value", err);
|
|
8896
|
+
}
|
|
8897
|
+
}
|
|
8256
8898
|
async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
8257
8899
|
const wrap = createQueryStrippedLogger("redis", req, url);
|
|
8258
8900
|
return dispatchRoutes(req, url, {
|
|
@@ -8267,6 +8909,11 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
|
|
|
8267
8909
|
"/_db/redis/value": {
|
|
8268
8910
|
methods: ["GET"],
|
|
8269
8911
|
handler: () => handleValue(req, cwd, url, omitDirNames)
|
|
8912
|
+
},
|
|
8913
|
+
"/_db/redis/write": {
|
|
8914
|
+
methods: ["POST"],
|
|
8915
|
+
sideEffect: true,
|
|
8916
|
+
handler: () => handleWrite2(req, cwd, omitDirNames)
|
|
8270
8917
|
}
|
|
8271
8918
|
}, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
|
|
8272
8919
|
}
|
|
@@ -8344,6 +8991,9 @@ function hmac(key, value) {
|
|
|
8344
8991
|
function sha256(value) {
|
|
8345
8992
|
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
8346
8993
|
}
|
|
8994
|
+
function sha256Bytes(value) {
|
|
8995
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
8996
|
+
}
|
|
8347
8997
|
function encodeRfc3986(value) {
|
|
8348
8998
|
return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
8349
8999
|
}
|
|
@@ -8713,9 +9363,10 @@ function createS3Adapter(config) {
|
|
|
8713
9363
|
const path = buildPath(opts.bucket, opts.key);
|
|
8714
9364
|
const query = canonicalQuery(opts.query);
|
|
8715
9365
|
const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
9366
|
+
const payloadHash = opts.body ? sha256Bytes(opts.body) : EMPTY_SHA256;
|
|
8716
9367
|
const headers = {
|
|
8717
9368
|
host: endpoint.host,
|
|
8718
|
-
"x-amz-content-sha256":
|
|
9369
|
+
"x-amz-content-sha256": payloadHash,
|
|
8719
9370
|
"x-amz-date": requestDate,
|
|
8720
9371
|
...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
|
|
8721
9372
|
...opts.headers || {}
|
|
@@ -8727,7 +9378,7 @@ function createS3Adapter(config) {
|
|
|
8727
9378
|
query,
|
|
8728
9379
|
canonicalHeaders(headers),
|
|
8729
9380
|
signedNames,
|
|
8730
|
-
|
|
9381
|
+
payloadHash
|
|
8731
9382
|
].join(`
|
|
8732
9383
|
`);
|
|
8733
9384
|
const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
|
|
@@ -8746,6 +9397,9 @@ function createS3Adapter(config) {
|
|
|
8746
9397
|
}
|
|
8747
9398
|
requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
|
|
8748
9399
|
if (config.dockerContainerName) {
|
|
9400
|
+
if (opts.body !== undefined) {
|
|
9401
|
+
throw new S3HttpError(503, "S3 object writes require a published host port (docker-exec transport cannot stream a request body)");
|
|
9402
|
+
}
|
|
8749
9403
|
if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
|
|
8750
9404
|
throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
|
|
8751
9405
|
}
|
|
@@ -8760,6 +9414,7 @@ function createS3Adapter(config) {
|
|
|
8760
9414
|
return fetch(url, {
|
|
8761
9415
|
method: opts.method,
|
|
8762
9416
|
headers: requestHeaders,
|
|
9417
|
+
...opts.body !== undefined ? { body: opts.body } : {},
|
|
8763
9418
|
signal: transportSignal
|
|
8764
9419
|
});
|
|
8765
9420
|
}, deadline);
|
|
@@ -8842,6 +9497,32 @@ function createS3Adapter(config) {
|
|
|
8842
9497
|
headers: rawObjectHeaders(opts.key, res)
|
|
8843
9498
|
});
|
|
8844
9499
|
}
|
|
9500
|
+
async function putObjectAsync(opts) {
|
|
9501
|
+
const deadline = createS3TransportDeadline(config);
|
|
9502
|
+
const res = await signedFetch({
|
|
9503
|
+
method: "PUT",
|
|
9504
|
+
bucket: opts.bucket,
|
|
9505
|
+
key: opts.key,
|
|
9506
|
+
body: opts.body,
|
|
9507
|
+
headers: opts.contentType ? { "content-type": opts.contentType } : undefined,
|
|
9508
|
+
signal: opts.signal
|
|
9509
|
+
}, deadline);
|
|
9510
|
+
if (!res.ok) {
|
|
9511
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
async function deleteObjectAsync(opts) {
|
|
9515
|
+
const deadline = createS3TransportDeadline(config);
|
|
9516
|
+
const res = await signedFetch({
|
|
9517
|
+
method: "DELETE",
|
|
9518
|
+
bucket: opts.bucket,
|
|
9519
|
+
key: opts.key,
|
|
9520
|
+
signal: opts.signal
|
|
9521
|
+
}, deadline);
|
|
9522
|
+
if (!res.ok) {
|
|
9523
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
9524
|
+
}
|
|
9525
|
+
}
|
|
8845
9526
|
return {
|
|
8846
9527
|
kind: "s3",
|
|
8847
9528
|
model: "object",
|
|
@@ -8850,7 +9531,9 @@ function createS3Adapter(config) {
|
|
|
8850
9531
|
listObjects,
|
|
8851
9532
|
headObject,
|
|
8852
9533
|
getObjectText,
|
|
8853
|
-
getObjectResponse
|
|
9534
|
+
getObjectResponse,
|
|
9535
|
+
putObjectAsync,
|
|
9536
|
+
deleteObjectAsync
|
|
8854
9537
|
};
|
|
8855
9538
|
}
|
|
8856
9539
|
async function s3ConfigFromDockerInfoAsync(info, signal) {
|
|
@@ -9253,6 +9936,60 @@ async function handleRaw(cwd, req, url, omitDirNames) {
|
|
|
9253
9936
|
return s3ErrorResponse(err, "stream s3 object");
|
|
9254
9937
|
}
|
|
9255
9938
|
}
|
|
9939
|
+
async function handleWrite3(req, cwd, omitDirNames) {
|
|
9940
|
+
const parsed = await parseBoundedJsonBody(req, 8 * 1024 * 1024, "payload too large");
|
|
9941
|
+
if (parsed instanceof Response)
|
|
9942
|
+
return parsed;
|
|
9943
|
+
const body = parsed;
|
|
9944
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
9945
|
+
return textError("missing db", 400);
|
|
9946
|
+
}
|
|
9947
|
+
const bucket = validateBucket(typeof body.bucket === "string" ? body.bucket : null);
|
|
9948
|
+
if (bucket instanceof Response)
|
|
9949
|
+
return bucket;
|
|
9950
|
+
const key = validateKey(typeof body.key === "string" ? body.key : null);
|
|
9951
|
+
if (key instanceof Response)
|
|
9952
|
+
return key;
|
|
9953
|
+
const r = await resolveS3(cwd, body.db, req.signal, omitDirNames);
|
|
9954
|
+
if (r instanceof Response)
|
|
9955
|
+
return r;
|
|
9956
|
+
try {
|
|
9957
|
+
if (body.op === "delete") {
|
|
9958
|
+
await r.explorer.deleteObjectAsync({
|
|
9959
|
+
bucket,
|
|
9960
|
+
key,
|
|
9961
|
+
signal: req.signal
|
|
9962
|
+
});
|
|
9963
|
+
return json({ ok: true });
|
|
9964
|
+
}
|
|
9965
|
+
if (body.op === "create") {
|
|
9966
|
+
let exists = false;
|
|
9967
|
+
try {
|
|
9968
|
+
await r.explorer.headObject({ bucket, key, signal: req.signal });
|
|
9969
|
+
exists = true;
|
|
9970
|
+
} catch (err) {
|
|
9971
|
+
if (isS3HttpError(err) && err.status === 404)
|
|
9972
|
+
exists = false;
|
|
9973
|
+
else
|
|
9974
|
+
throw err;
|
|
9975
|
+
}
|
|
9976
|
+
if (exists)
|
|
9977
|
+
return textError(`object already exists: ${key}`, 409);
|
|
9978
|
+
}
|
|
9979
|
+
const content = typeof body.content === "string" ? body.content : "";
|
|
9980
|
+
const contentType = typeof body.contentType === "string" && body.contentType ? body.contentType : "application/octet-stream";
|
|
9981
|
+
await r.explorer.putObjectAsync({
|
|
9982
|
+
bucket,
|
|
9983
|
+
key,
|
|
9984
|
+
body: new TextEncoder().encode(content),
|
|
9985
|
+
contentType,
|
|
9986
|
+
signal: req.signal
|
|
9987
|
+
});
|
|
9988
|
+
return json({ ok: true });
|
|
9989
|
+
} catch (err) {
|
|
9990
|
+
return handleError("s3", "write s3 object", err);
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9256
9993
|
async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
9257
9994
|
const wrap = createQueryStrippedLogger("s3", req, url);
|
|
9258
9995
|
return dispatchRoutes(req, url, {
|
|
@@ -9279,6 +10016,11 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
|
9279
10016
|
"/_db/s3/raw": {
|
|
9280
10017
|
methods: ["GET", "HEAD"],
|
|
9281
10018
|
handler: () => handleRaw(cwd, req, url, omitDirNames)
|
|
10019
|
+
},
|
|
10020
|
+
"/_db/s3/write": {
|
|
10021
|
+
methods: ["POST"],
|
|
10022
|
+
sideEffect: true,
|
|
10023
|
+
handler: () => handleWrite3(req, cwd, omitDirNames)
|
|
9282
10024
|
}
|
|
9283
10025
|
}, sideEffectAllowed, wrap, (err) => handleError("s3", "handle s3 request", err));
|
|
9284
10026
|
}
|
|
@@ -9445,21 +10187,6 @@ var init_query_history = __esm(() => {
|
|
|
9445
10187
|
import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
|
|
9446
10188
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9447
10189
|
import { join as join11 } from "node:path";
|
|
9448
|
-
async function getSqliteClass2() {
|
|
9449
|
-
if (cachedDbClass2)
|
|
9450
|
-
return cachedDbClass2;
|
|
9451
|
-
try {
|
|
9452
|
-
const mod = await import("bun:sqlite");
|
|
9453
|
-
cachedDbClass2 = mod.Database;
|
|
9454
|
-
return cachedDbClass2;
|
|
9455
|
-
} catch {}
|
|
9456
|
-
try {
|
|
9457
|
-
const mod = await Function('return import("better-sqlite3")')();
|
|
9458
|
-
cachedDbClass2 = mod.default || mod;
|
|
9459
|
-
return cachedDbClass2;
|
|
9460
|
-
} catch {}
|
|
9461
|
-
throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
|
|
9462
|
-
}
|
|
9463
10190
|
async function getStoreDb(cwd) {
|
|
9464
10191
|
const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
|
|
9465
10192
|
if (storeDb && storeDbPath === dbPath)
|
|
@@ -9470,7 +10197,7 @@ async function getStoreDb(cwd) {
|
|
|
9470
10197
|
} catch {}
|
|
9471
10198
|
}
|
|
9472
10199
|
mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
|
|
9473
|
-
const DbClass = await
|
|
10200
|
+
const DbClass = await loadSqliteClass();
|
|
9474
10201
|
storeDb = new DbClass(dbPath);
|
|
9475
10202
|
storeDbPath = dbPath;
|
|
9476
10203
|
storeDb.exec("PRAGMA journal_mode=WAL");
|
|
@@ -9729,7 +10456,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
9729
10456
|
});
|
|
9730
10457
|
return { rows, total };
|
|
9731
10458
|
}
|
|
9732
|
-
var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite",
|
|
10459
|
+
var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", SCHEMA_SQL = `
|
|
9733
10460
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
9734
10461
|
id TEXT PRIMARY KEY,
|
|
9735
10462
|
db_id TEXT NOT NULL,
|
|
@@ -9959,6 +10686,9 @@ function sanitize(input) {
|
|
|
9959
10686
|
const historyHeight = sanitizeCssSize(tab.historyHeight);
|
|
9960
10687
|
if (historyHeight !== undefined)
|
|
9961
10688
|
out.historyHeight = historyHeight;
|
|
10689
|
+
if (tab.activeHistoryTab === "log" || tab.activeHistoryTab === "history") {
|
|
10690
|
+
out.activeHistoryTab = tab.activeHistoryTab;
|
|
10691
|
+
}
|
|
9962
10692
|
const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
|
|
9963
10693
|
if (sidebarWidth !== undefined)
|
|
9964
10694
|
out.sidebarWidth = sidebarWidth;
|
|
@@ -10149,11 +10879,13 @@ async function handleSchemas(cwd, url, omitDirNames, signal) {
|
|
|
10149
10879
|
const body2 = { dbId: r.dbId, schemas: [] };
|
|
10150
10880
|
return json(body2);
|
|
10151
10881
|
}
|
|
10152
|
-
const
|
|
10882
|
+
const docker = r.docker;
|
|
10883
|
+
const { result: schemas, executedSql } = await captureSql(() => listDockerSchemasAsync(docker.serviceName, "postgresql", docker.env, docker.composeDir, docker.database, signal));
|
|
10153
10884
|
const body = {
|
|
10154
10885
|
dbId: r.dbId,
|
|
10155
10886
|
schemas: schemas.map((name) => ({ name })),
|
|
10156
|
-
selectedSchema: r.schema
|
|
10887
|
+
selectedSchema: r.schema,
|
|
10888
|
+
executedSql
|
|
10157
10889
|
};
|
|
10158
10890
|
return json(body);
|
|
10159
10891
|
}
|
|
@@ -10165,37 +10897,41 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
|
|
|
10165
10897
|
const linkedAbort = createLinkedAbortController(signal);
|
|
10166
10898
|
try {
|
|
10167
10899
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10168
|
-
const
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10900
|
+
const { result, executedSql } = await captureSql(async () => {
|
|
10901
|
+
const db = asAsync(adapter);
|
|
10902
|
+
const tables = await db.tables(linkedAbort.signal);
|
|
10903
|
+
const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
|
|
10904
|
+
const countMapPromise = db.tableRowCounts(tableNames, linkedAbort.signal);
|
|
10905
|
+
const indexesPromise = db.indexes(linkedAbort.signal);
|
|
10906
|
+
const foreignKeysPromise = db.foreignKeys(linkedAbort.signal);
|
|
10907
|
+
const columnsMapPromise = includeColumns ? db.columnsMulti(tableNames, linkedAbort.signal) : Promise.resolve(null);
|
|
10908
|
+
const schemaPromises = [
|
|
10909
|
+
countMapPromise,
|
|
10910
|
+
indexesPromise,
|
|
10911
|
+
foreignKeysPromise,
|
|
10912
|
+
columnsMapPromise
|
|
10913
|
+
];
|
|
10914
|
+
const [countMap, indexes, foreignKeys, colsMap] = await Promise.all(schemaPromises).catch(async (err) => {
|
|
10915
|
+
linkedAbort.abort();
|
|
10916
|
+
await Promise.allSettled(schemaPromises);
|
|
10917
|
+
throw err;
|
|
10918
|
+
});
|
|
10919
|
+
return { tables, countMap, indexes, foreignKeys, colsMap };
|
|
10185
10920
|
});
|
|
10186
|
-
const tablesWithCount = tables.map((t) => ({
|
|
10921
|
+
const tablesWithCount = result.tables.map((t) => ({
|
|
10187
10922
|
...t,
|
|
10188
|
-
rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
|
|
10923
|
+
rowCount: t.type === "table" ? result.countMap.get(t.name) ?? 0 : null
|
|
10189
10924
|
}));
|
|
10190
10925
|
const body = {
|
|
10191
10926
|
dbId: r.dbId,
|
|
10192
10927
|
...r.schema ? { schema: r.schema } : {},
|
|
10193
10928
|
tables: tablesWithCount,
|
|
10194
|
-
indexes,
|
|
10195
|
-
foreignKeys
|
|
10929
|
+
indexes: result.indexes,
|
|
10930
|
+
foreignKeys: result.foreignKeys,
|
|
10931
|
+
executedSql
|
|
10196
10932
|
};
|
|
10197
|
-
if (colsMap) {
|
|
10198
|
-
body.columnsMap = Object.fromEntries(colsMap);
|
|
10933
|
+
if (result.colsMap) {
|
|
10934
|
+
body.columnsMap = Object.fromEntries(result.colsMap);
|
|
10199
10935
|
}
|
|
10200
10936
|
return json(body);
|
|
10201
10937
|
} catch (err) {
|
|
@@ -10257,36 +10993,17 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
10257
10993
|
const exact = parseExactConditions(url);
|
|
10258
10994
|
try {
|
|
10259
10995
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
}, signal);
|
|
10268
|
-
const colNames2 = new Set(meta2.columns.map((c) => c.name));
|
|
10269
|
-
if (sortCol && !colNames2.has(sortCol)) {
|
|
10270
|
-
return textError(`invalid sort column: ${sortCol}`, 400);
|
|
10271
|
-
}
|
|
10272
|
-
const body2 = {
|
|
10273
|
-
dbId: r.dbId,
|
|
10274
|
-
...r.schema ? { schema: r.schema } : {},
|
|
10275
|
-
table,
|
|
10276
|
-
columns: meta2.columns,
|
|
10277
|
-
rows: serializeDbRows(meta2.rows),
|
|
10278
|
-
totalRows: meta2.totalRows,
|
|
10279
|
-
offset,
|
|
10280
|
-
limit,
|
|
10281
|
-
hasMore: offset + meta2.rowCount < meta2.totalRows
|
|
10282
|
-
};
|
|
10283
|
-
return json(body2);
|
|
10284
|
-
}
|
|
10285
|
-
const meta = await adapter.getTablePageWithMeta(table, {
|
|
10996
|
+
const { result: meta, executedSql } = await captureSql(() => filters.length > 0 || exact.length > 0 ? adapter.getFilteredTablePageWithMeta(table, {
|
|
10997
|
+
offset,
|
|
10998
|
+
limit,
|
|
10999
|
+
orderBy,
|
|
11000
|
+
grouped: groupFiltersByValue(filters),
|
|
11001
|
+
...exact.length > 0 ? { exact } : {}
|
|
11002
|
+
}, signal) : adapter.getTablePageWithMeta(table, {
|
|
10286
11003
|
offset,
|
|
10287
11004
|
limit,
|
|
10288
11005
|
orderBy
|
|
10289
|
-
}, signal);
|
|
11006
|
+
}, signal));
|
|
10290
11007
|
const colNames = new Set(meta.columns.map((c) => c.name));
|
|
10291
11008
|
if (sortCol && !colNames.has(sortCol)) {
|
|
10292
11009
|
return textError(`invalid sort column: ${sortCol}`, 400);
|
|
@@ -10300,7 +11017,8 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
10300
11017
|
totalRows: meta.totalRows,
|
|
10301
11018
|
offset,
|
|
10302
11019
|
limit,
|
|
10303
|
-
hasMore: offset + meta.rowCount < meta.totalRows
|
|
11020
|
+
hasMore: offset + meta.rowCount < meta.totalRows,
|
|
11021
|
+
executedSql
|
|
10304
11022
|
};
|
|
10305
11023
|
return json(body);
|
|
10306
11024
|
} catch (err) {
|
|
@@ -10377,8 +11095,7 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
10377
11095
|
const start = Date.now();
|
|
10378
11096
|
try {
|
|
10379
11097
|
const adapter = await getAdapter(r, cwd, req.signal);
|
|
10380
|
-
const
|
|
10381
|
-
const result = await db.readonlyQuery(body.sql, undefined, maxRows, req.signal);
|
|
11098
|
+
const { result, executedSql } = await captureSql(() => asAsync(adapter).readonlyQuery(body.sql ?? "", undefined, maxRows, req.signal));
|
|
10382
11099
|
const elapsed = Date.now() - start;
|
|
10383
11100
|
const serializedRows = serializeDbRows(result.rows);
|
|
10384
11101
|
const inferredColumns = result.columns.length === 0 && result.rows.length === 0 ? await inferEmptyQueryColumns(adapter, body.sql, r.schema, req.signal) : [];
|
|
@@ -10392,7 +11109,8 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
10392
11109
|
rows: serializedRows,
|
|
10393
11110
|
rowCount: result.rowCount,
|
|
10394
11111
|
truncated: result.rowCount >= maxRows,
|
|
10395
|
-
elapsedMs: elapsed
|
|
11112
|
+
elapsedMs: elapsed,
|
|
11113
|
+
executedSql
|
|
10396
11114
|
};
|
|
10397
11115
|
if (body.saveHistory) {
|
|
10398
11116
|
const entry = {
|
|
@@ -10611,13 +11329,13 @@ async function handleColumns(cwd, url, omitDirNames, signal) {
|
|
|
10611
11329
|
return textError("missing table parameter", 400);
|
|
10612
11330
|
try {
|
|
10613
11331
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10614
|
-
const
|
|
10615
|
-
const columns = await db.columns(table, signal);
|
|
11332
|
+
const { result: columns, executedSql } = await captureSql(() => asAsync(adapter).columns(table, signal));
|
|
10616
11333
|
return json({
|
|
10617
11334
|
dbId: r.dbId,
|
|
10618
11335
|
...r.schema ? { schema: r.schema } : {},
|
|
10619
11336
|
table,
|
|
10620
|
-
columns
|
|
11337
|
+
columns,
|
|
11338
|
+
executedSql
|
|
10621
11339
|
});
|
|
10622
11340
|
} catch (err) {
|
|
10623
11341
|
return handleError("database", "get columns", err);
|
|
@@ -10632,17 +11350,21 @@ async function handleDdl(cwd, url, omitDirNames, signal) {
|
|
|
10632
11350
|
return textError("missing table parameter", 400);
|
|
10633
11351
|
try {
|
|
10634
11352
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10635
|
-
const
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
11353
|
+
const { result, executedSql } = await captureSql(async () => {
|
|
11354
|
+
const db = asAsync(adapter);
|
|
11355
|
+
const [sql, triggers] = await Promise.all([
|
|
11356
|
+
db.createStatement(table, signal),
|
|
11357
|
+
db.triggers(table, signal)
|
|
11358
|
+
]);
|
|
11359
|
+
return { sql, triggers };
|
|
11360
|
+
});
|
|
10640
11361
|
return json({
|
|
10641
11362
|
dbId: r.dbId,
|
|
10642
11363
|
...r.schema ? { schema: r.schema } : {},
|
|
10643
11364
|
table,
|
|
10644
|
-
sql,
|
|
10645
|
-
triggers
|
|
11365
|
+
sql: result.sql,
|
|
11366
|
+
triggers: result.triggers,
|
|
11367
|
+
executedSql
|
|
10646
11368
|
});
|
|
10647
11369
|
} catch (err) {
|
|
10648
11370
|
return handleError("database", "get DDL", err);
|
|
@@ -11054,6 +11776,48 @@ async function handleClose(cwd, req, omitDirNames) {
|
|
|
11054
11776
|
}
|
|
11055
11777
|
return json({ ok: true });
|
|
11056
11778
|
}
|
|
11779
|
+
async function handleMutate(cwd, req, omitDirNames) {
|
|
11780
|
+
const parsed = await parseBoundedJsonBody(req, 1048576, "payload too large");
|
|
11781
|
+
if (parsed instanceof Response)
|
|
11782
|
+
return parsed;
|
|
11783
|
+
const body = parsed;
|
|
11784
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
11785
|
+
return textError("missing db", 400);
|
|
11786
|
+
}
|
|
11787
|
+
if (typeof body.table !== "string" || body.table === "") {
|
|
11788
|
+
return textError("missing table", 400);
|
|
11789
|
+
}
|
|
11790
|
+
if (!Array.isArray(body.mutations) || body.mutations.length === 0) {
|
|
11791
|
+
return textError("missing mutations", 400);
|
|
11792
|
+
}
|
|
11793
|
+
const schemaParam = typeof body.schema === "string" ? body.schema : undefined;
|
|
11794
|
+
const r = await resolveDb(cwd, body.db, omitDirNames, schemaParam, req.signal);
|
|
11795
|
+
if (r instanceof Response)
|
|
11796
|
+
return r;
|
|
11797
|
+
const adapter = await getAdapter(r, cwd, req.signal);
|
|
11798
|
+
if (!adapter.applyMutations) {
|
|
11799
|
+
return textError("writes are not supported for this datastore", 400);
|
|
11800
|
+
}
|
|
11801
|
+
try {
|
|
11802
|
+
const tableName = body.table;
|
|
11803
|
+
const { result, executedSql } = await captureSql(() => adapter.applyMutations?.(tableName, body.mutations, req.signal) ?? Promise.reject(new Error("applyMutations not supported")));
|
|
11804
|
+
adapter.invalidateTableMetaCache?.(tableName);
|
|
11805
|
+
const response = {
|
|
11806
|
+
dbId: r.dbId,
|
|
11807
|
+
...r.schema ? { schema: r.schema } : {},
|
|
11808
|
+
table: tableName,
|
|
11809
|
+
affected: result.affected,
|
|
11810
|
+
executedSql
|
|
11811
|
+
};
|
|
11812
|
+
return json(response);
|
|
11813
|
+
} catch (err) {
|
|
11814
|
+
if (isAbortLikeError(err, req.signal)) {
|
|
11815
|
+
return textError("mutation aborted", 503);
|
|
11816
|
+
}
|
|
11817
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11818
|
+
return textError(message, 400);
|
|
11819
|
+
}
|
|
11820
|
+
}
|
|
11057
11821
|
async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
|
|
11058
11822
|
ensureInit();
|
|
11059
11823
|
if (url.pathname.startsWith("/_db/redis/")) {
|
|
@@ -11114,6 +11878,11 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
|
|
|
11114
11878
|
sideEffect: true,
|
|
11115
11879
|
handler: () => handleQuery(cwd, req, sendSse, omitDirNames)
|
|
11116
11880
|
},
|
|
11881
|
+
"/_db/mutate": {
|
|
11882
|
+
methods: ["POST"],
|
|
11883
|
+
sideEffect: true,
|
|
11884
|
+
handler: () => handleMutate(cwd, req, omitDirNames)
|
|
11885
|
+
},
|
|
11117
11886
|
"/_db/close": {
|
|
11118
11887
|
methods: ["POST"],
|
|
11119
11888
|
sideEffect: true,
|
|
@@ -11196,6 +11965,7 @@ var init_handle = __esm(() => {
|
|
|
11196
11965
|
init_state_store();
|
|
11197
11966
|
init_docker();
|
|
11198
11967
|
init_docker_utils();
|
|
11968
|
+
init_sql_capture();
|
|
11199
11969
|
init_sqlite();
|
|
11200
11970
|
init_connection_pool();
|
|
11201
11971
|
init_discovery();
|
|
@@ -11607,7 +12377,7 @@ function fileToMeta(file, range, extraQs) {
|
|
|
11607
12377
|
untracked: file.untracked || false
|
|
11608
12378
|
};
|
|
11609
12379
|
}
|
|
11610
|
-
function computePayload(extras, range) {
|
|
12380
|
+
function computePayload(extras, range, pathFilter = "") {
|
|
11611
12381
|
if (isSameWorktreeRange(range)) {
|
|
11612
12382
|
return {
|
|
11613
12383
|
files: [],
|
|
@@ -11623,8 +12393,9 @@ function computePayload(extras, range) {
|
|
|
11623
12393
|
const files = fileMeta(fullArgs, cwd, false);
|
|
11624
12394
|
if (includeUntracked(range, refs2))
|
|
11625
12395
|
files.push(...untrackedMeta(cwd));
|
|
11626
|
-
files.
|
|
11627
|
-
|
|
12396
|
+
const filteredFiles = pathFilter ? files.filter((file) => file.path === pathFilter || file.old_path === pathFilter) : files;
|
|
12397
|
+
filteredFiles.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
12398
|
+
filteredFiles.forEach((file, i) => {
|
|
11628
12399
|
file.order = i + 1;
|
|
11629
12400
|
});
|
|
11630
12401
|
const extraQs = {};
|
|
@@ -11634,7 +12405,7 @@ function computePayload(extras, range) {
|
|
|
11634
12405
|
if (e === "--ignore-blank-lines")
|
|
11635
12406
|
extraQs.ignore_blank = "1";
|
|
11636
12407
|
}
|
|
11637
|
-
const meta =
|
|
12408
|
+
const meta = filteredFiles.map((file) => fileToMeta(file, range, extraQs));
|
|
11638
12409
|
const totals = meta.reduce((acc, file) => {
|
|
11639
12410
|
acc.additions += file.additions || 0;
|
|
11640
12411
|
acc.deletions += file.deletions || 0;
|
|
@@ -11661,9 +12432,12 @@ function handleDiffJson(url) {
|
|
|
11661
12432
|
from: url.searchParams.get("from") || "",
|
|
11662
12433
|
to: url.searchParams.get("to") || ""
|
|
11663
12434
|
};
|
|
11664
|
-
const
|
|
12435
|
+
const path = url.searchParams.get("path") || "";
|
|
12436
|
+
if (path && !safePath(path))
|
|
12437
|
+
return text("invalid path", 400);
|
|
12438
|
+
const key = `${range.from}|${range.to}|${url.searchParams.get("ignore_ws") || ""}|${url.searchParams.get("ignore_blank") || ""}|${path}`;
|
|
11665
12439
|
if (url.searchParams.get("nocache") === "1") {
|
|
11666
|
-
const payload2 = computePayload(extras, range);
|
|
12440
|
+
const payload2 = computePayload(extras, range, path);
|
|
11667
12441
|
const sig = JSON.stringify({ ...payload2, generation: undefined });
|
|
11668
12442
|
const cached2 = metaCache.get(key);
|
|
11669
12443
|
if (!cached2 || cached2.sig !== sig) {
|
|
@@ -11689,7 +12463,7 @@ function handleDiffJson(url) {
|
|
|
11689
12463
|
"Cache-Control": "no-store"
|
|
11690
12464
|
}
|
|
11691
12465
|
});
|
|
11692
|
-
const payload = computePayload(extras, range);
|
|
12466
|
+
const payload = computePayload(extras, range, path);
|
|
11693
12467
|
const body = JSON.stringify(payload);
|
|
11694
12468
|
setTimedCacheEntry(metaCache, key, {
|
|
11695
12469
|
body,
|
|
@@ -12106,15 +12880,109 @@ function handleLog(url) {
|
|
|
12106
12880
|
const ref = url.searchParams.get("ref") || "HEAD";
|
|
12107
12881
|
const skip = Number(url.searchParams.get("skip") || "0");
|
|
12108
12882
|
const limit = Number(url.searchParams.get("limit") || "50");
|
|
12883
|
+
const path = url.searchParams.get("path") || "";
|
|
12884
|
+
if (path && !safePath(path))
|
|
12885
|
+
return text("invalid path", 400);
|
|
12109
12886
|
const result = commitHistory(cwd, {
|
|
12110
12887
|
ref,
|
|
12111
12888
|
skip: Number.isFinite(skip) ? skip : 0,
|
|
12112
12889
|
limit: Number.isFinite(limit) ? limit : 50,
|
|
12113
|
-
query: url.searchParams.get("q") || ""
|
|
12890
|
+
query: url.searchParams.get("q") || "",
|
|
12891
|
+
...path ? { path } : {}
|
|
12114
12892
|
});
|
|
12115
12893
|
if (result.error)
|
|
12116
12894
|
return text(result.error, 400);
|
|
12117
|
-
|
|
12895
|
+
const wantsWorktreeHead = path && skip === 0 && (ref === "worktree" || url.searchParams.get("worktree") === "1");
|
|
12896
|
+
let commits = result.commits;
|
|
12897
|
+
let hasWorktree = false;
|
|
12898
|
+
if (wantsWorktreeHead) {
|
|
12899
|
+
const status = runSync([
|
|
12900
|
+
"git",
|
|
12901
|
+
"-c",
|
|
12902
|
+
"core.quotepath=false",
|
|
12903
|
+
"status",
|
|
12904
|
+
"--porcelain=v1",
|
|
12905
|
+
"-z",
|
|
12906
|
+
"--untracked-files=normal",
|
|
12907
|
+
"--",
|
|
12908
|
+
path
|
|
12909
|
+
], cwd);
|
|
12910
|
+
if (status.code === 0 && status.stdout.length > 0) {
|
|
12911
|
+
const parts = status.stdout.split("\x00").filter(Boolean);
|
|
12912
|
+
if (parts.length > 0) {
|
|
12913
|
+
hasWorktree = true;
|
|
12914
|
+
commits = [
|
|
12915
|
+
{
|
|
12916
|
+
sha: "worktree",
|
|
12917
|
+
subject: "未コミット変更 (Working tree)",
|
|
12918
|
+
author: "",
|
|
12919
|
+
when: "",
|
|
12920
|
+
parents: [],
|
|
12921
|
+
body: ""
|
|
12922
|
+
},
|
|
12923
|
+
...commits
|
|
12924
|
+
];
|
|
12925
|
+
}
|
|
12926
|
+
}
|
|
12927
|
+
}
|
|
12928
|
+
return json2({
|
|
12929
|
+
commits,
|
|
12930
|
+
hasMore: result.hasMore,
|
|
12931
|
+
generation,
|
|
12932
|
+
...hasWorktree ? { hasWorktree: true } : {}
|
|
12933
|
+
});
|
|
12934
|
+
}
|
|
12935
|
+
function blamePathKey(p) {
|
|
12936
|
+
try {
|
|
12937
|
+
const st = statSync3(join13(cwd, p));
|
|
12938
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
12939
|
+
} catch {
|
|
12940
|
+
return "missing";
|
|
12941
|
+
}
|
|
12942
|
+
}
|
|
12943
|
+
function rememberBlame(key, value) {
|
|
12944
|
+
if (blameCache.has(key))
|
|
12945
|
+
blameCache.delete(key);
|
|
12946
|
+
blameCache.set(key, value);
|
|
12947
|
+
while (blameCache.size > BLAME_CACHE_MAX) {
|
|
12948
|
+
const oldest = blameCache.keys().next();
|
|
12949
|
+
if (oldest.done)
|
|
12950
|
+
break;
|
|
12951
|
+
blameCache.delete(oldest.value);
|
|
12952
|
+
}
|
|
12953
|
+
}
|
|
12954
|
+
function handleFileBlame(url) {
|
|
12955
|
+
const path = url.searchParams.get("path") || "";
|
|
12956
|
+
if (!safePath(path))
|
|
12957
|
+
return text("invalid path", 400);
|
|
12958
|
+
const ref = url.searchParams.get("ref") || "worktree";
|
|
12959
|
+
if (!ref || ref.startsWith("-") || ref.includes("\x00"))
|
|
12960
|
+
return text("invalid ref", 400);
|
|
12961
|
+
const rawBase = url.searchParams.get("base");
|
|
12962
|
+
const requestedBase = rawBase === "HEAD" ? "HEAD" : rawBase === "worktree" ? "worktree" : ref === "worktree" ? "worktree" : "HEAD";
|
|
12963
|
+
const normalized = normalizeBlameRef(ref, requestedBase);
|
|
12964
|
+
const { base } = normalized;
|
|
12965
|
+
let cacheKey;
|
|
12966
|
+
if (base === "worktree") {
|
|
12967
|
+
cacheKey = `worktree|${path}|${blamePathKey(path)}`;
|
|
12968
|
+
} else {
|
|
12969
|
+
const resolved = runSync(["git", "rev-parse", "--verify", `${normalized.ref}^{commit}`], cwd);
|
|
12970
|
+
if (resolved.code !== 0)
|
|
12971
|
+
return text("unknown ref", 400);
|
|
12972
|
+
cacheKey = `HEAD|${path}|${resolved.stdout.trim()}`;
|
|
12973
|
+
}
|
|
12974
|
+
const cached = blameCache.get(cacheKey);
|
|
12975
|
+
if (cached) {
|
|
12976
|
+
if (blameCache.has(cacheKey)) {
|
|
12977
|
+
blameCache.delete(cacheKey);
|
|
12978
|
+
blameCache.set(cacheKey, cached);
|
|
12979
|
+
}
|
|
12980
|
+
return json2({ ...cached, base, ref, generation });
|
|
12981
|
+
}
|
|
12982
|
+
const result = blame(cwd, { path, ref: normalized.ref, base });
|
|
12983
|
+
if (!result.error)
|
|
12984
|
+
rememberBlame(cacheKey, result);
|
|
12985
|
+
return json2({ ...result, base, ref, generation });
|
|
12118
12986
|
}
|
|
12119
12987
|
function handleFileDiff(url) {
|
|
12120
12988
|
const path = url.searchParams.get("path") || "";
|
|
@@ -13123,7 +13991,7 @@ function restartWorktreeWatch() {
|
|
|
13123
13991
|
}
|
|
13124
13992
|
worktreeWatch = startScopedWorktreeWatch();
|
|
13125
13993
|
}
|
|
13126
|
-
var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
|
|
13994
|
+
var WEB_ROOT, VERSION, DEFAULT_ARGS, PREVIEW_HUNKS_DEFAULT = 3, PREVIEW_LINES_DEFAULT = 1200, WATCHED_ASSET_FILES, SIZE_SMALL = 2000, SIZE_MEDIUM = 8000, SIZE_LARGE = 20000, LINE_INDEX_MIN_START = 1e4, LINE_INDEX_MAX_FILE_BYTES, BLOB_LINE_CACHE_MAX_BYTES, MAX_UPLOAD_FILE_BYTES, MAX_UPLOAD_TOTAL_BYTES, MAX_UPLOAD_BODY_BYTES, MAX_UPLOAD_FILES = 50, SAFE_UPLOAD_EXTENSIONS, generation = 1, cwd, cliArgs, listenPort = 0, openAfterStart = false, scopeOmitDirNames, scopeOmitDirCliOverride = null, scopeExcludeNames, scopeWatchLimit, uploadEnabled = true, rgAvailableCache = null, enc, sseClients, sseKeepalives, fileCache, blameCache, BLAME_CACHE_MAX = 64, metaCache, fileListCache, lineIndexCache, blobLineIndexCache, blobBytesCache, blobLineCacheBytes = 0, isCodeViewerInternalPath, watchLimitReached = null, server, worktreeWatch = null, shuttingDown = false;
|
|
13127
13995
|
var init_preview = __esm(async () => {
|
|
13128
13996
|
init_routes();
|
|
13129
13997
|
init_annotations();
|
|
@@ -13191,6 +14059,7 @@ var init_preview = __esm(async () => {
|
|
|
13191
14059
|
sseClients = new Set;
|
|
13192
14060
|
sseKeepalives = new Map;
|
|
13193
14061
|
fileCache = new Map;
|
|
14062
|
+
blameCache = new Map;
|
|
13194
14063
|
metaCache = new Map;
|
|
13195
14064
|
fileListCache = new Map;
|
|
13196
14065
|
lineIndexCache = new Map;
|
|
@@ -13223,6 +14092,8 @@ var init_preview = __esm(async () => {
|
|
|
13223
14092
|
return handleRefCommits(url);
|
|
13224
14093
|
if (url.pathname === "/_log")
|
|
13225
14094
|
return handleLog(url);
|
|
14095
|
+
if (url.pathname === "/_file_blame")
|
|
14096
|
+
return handleFileBlame(url);
|
|
13226
14097
|
if (url.pathname === "/file_diff")
|
|
13227
14098
|
return handleFileDiff(url);
|
|
13228
14099
|
if (url.pathname === "/file_range")
|