@youtyan/code-viewer 0.3.0 → 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 +37 -17
- package/dist/code-viewer.js +988 -408
- package/package.json +1 -1
- package/web/app.js +2323 -528
- package/web/index.html +13 -0
- package/web/style.css +521 -37
package/dist/code-viewer.js
CHANGED
|
@@ -3888,10 +3888,260 @@ var init_search = __esm(() => {
|
|
|
3888
3888
|
DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
|
|
3889
3889
|
});
|
|
3890
3890
|
|
|
3891
|
+
// web-src/server/worktree-watcher.ts
|
|
3892
|
+
import {
|
|
3893
|
+
lstatSync as lstatSync3,
|
|
3894
|
+
readdirSync as nodeReaddirSync,
|
|
3895
|
+
watch as nodeWatch
|
|
3896
|
+
} from "node:fs";
|
|
3897
|
+
import { join as join7, relative } from "node:path";
|
|
3898
|
+
function normalizeRelativePath(path) {
|
|
3899
|
+
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
3900
|
+
}
|
|
3901
|
+
function isInsideRoot(root, path) {
|
|
3902
|
+
const rel = relative(root, path).replace(/\\/g, "/");
|
|
3903
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
3904
|
+
}
|
|
3905
|
+
function startWorktreeUpdateWatch(options) {
|
|
3906
|
+
const watch = options.watch || nodeWatch;
|
|
3907
|
+
const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
|
|
3908
|
+
const isDirectory = options.isDirectory || ((path) => {
|
|
3909
|
+
try {
|
|
3910
|
+
return lstatSync3(path).isDirectory();
|
|
3911
|
+
} catch {
|
|
3912
|
+
return false;
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
const directorySignature = options.directorySignature || ((path) => {
|
|
3916
|
+
try {
|
|
3917
|
+
const stats = lstatSync3(path);
|
|
3918
|
+
if (!stats.isDirectory())
|
|
3919
|
+
return null;
|
|
3920
|
+
return `${stats.dev}:${stats.ino}`;
|
|
3921
|
+
} catch {
|
|
3922
|
+
return null;
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3925
|
+
const setTimer = options.setTimeoutFn || setTimeout;
|
|
3926
|
+
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
3927
|
+
const debounceMs = options.debounceMs ?? 250;
|
|
3928
|
+
const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
|
|
3929
|
+
const watchers = new Map;
|
|
3930
|
+
const signatures = new Map;
|
|
3931
|
+
const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
|
|
3932
|
+
const initialScanQueue = [];
|
|
3933
|
+
let initialScanTimer = null;
|
|
3934
|
+
const pendingPathInspections = new Map;
|
|
3935
|
+
let pathInspectionTimer = null;
|
|
3936
|
+
let timer = null;
|
|
3937
|
+
const pendingChangedPaths = new Set;
|
|
3938
|
+
let watchLimitReported = false;
|
|
3939
|
+
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
3940
|
+
const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
|
|
3941
|
+
const ignoredDirectory = (dir) => {
|
|
3942
|
+
const rel = directoryRelativePath(dir);
|
|
3943
|
+
return Boolean(rel && ignored(rel));
|
|
3944
|
+
};
|
|
3945
|
+
const scheduleUpdate = (changedPath) => {
|
|
3946
|
+
if (changedPath)
|
|
3947
|
+
pendingChangedPaths.add(changedPath);
|
|
3948
|
+
if (timer)
|
|
3949
|
+
clearTimer(timer);
|
|
3950
|
+
timer = setTimer(() => {
|
|
3951
|
+
timer = null;
|
|
3952
|
+
const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
|
|
3953
|
+
pendingChangedPaths.clear();
|
|
3954
|
+
options.onUpdate(paths);
|
|
3955
|
+
}, debounceMs);
|
|
3956
|
+
};
|
|
3957
|
+
const reportWatchLimit = () => {
|
|
3958
|
+
if (watchLimitReported)
|
|
3959
|
+
return;
|
|
3960
|
+
watchLimitReported = true;
|
|
3961
|
+
options.onWatchLimit?.(maxWatchedDirectories);
|
|
3962
|
+
options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
|
|
3963
|
+
};
|
|
3964
|
+
const closeSubtree = (dir) => {
|
|
3965
|
+
for (const [watchedDir, watcher] of [...watchers]) {
|
|
3966
|
+
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
3967
|
+
continue;
|
|
3968
|
+
try {
|
|
3969
|
+
watcher.close?.();
|
|
3970
|
+
} catch {}
|
|
3971
|
+
watchers.delete(watchedDir);
|
|
3972
|
+
signatures.delete(watchedDir);
|
|
3973
|
+
}
|
|
3974
|
+
};
|
|
3975
|
+
const closeAll = () => {
|
|
3976
|
+
if (initialScanTimer) {
|
|
3977
|
+
clearTimer(initialScanTimer);
|
|
3978
|
+
initialScanTimer = null;
|
|
3979
|
+
}
|
|
3980
|
+
if (pathInspectionTimer) {
|
|
3981
|
+
clearTimer(pathInspectionTimer);
|
|
3982
|
+
pathInspectionTimer = null;
|
|
3983
|
+
}
|
|
3984
|
+
initialScanQueue.length = 0;
|
|
3985
|
+
pendingPathInspections.clear();
|
|
3986
|
+
for (const watcher of [...watchers.values()]) {
|
|
3987
|
+
try {
|
|
3988
|
+
watcher.close?.();
|
|
3989
|
+
} catch {}
|
|
3990
|
+
}
|
|
3991
|
+
watchers.clear();
|
|
3992
|
+
signatures.clear();
|
|
3993
|
+
};
|
|
3994
|
+
const readChildDirectories = (dir) => {
|
|
3995
|
+
let entries;
|
|
3996
|
+
try {
|
|
3997
|
+
entries = readDirs(dir);
|
|
3998
|
+
} catch (error) {
|
|
3999
|
+
options.onError?.(error);
|
|
4000
|
+
return [];
|
|
4001
|
+
}
|
|
4002
|
+
const children = [];
|
|
4003
|
+
for (const entry of entries) {
|
|
4004
|
+
if (!entry.isDirectory())
|
|
4005
|
+
continue;
|
|
4006
|
+
const child = join7(dir, entry.name);
|
|
4007
|
+
if (ignoredDirectory(child))
|
|
4008
|
+
continue;
|
|
4009
|
+
children.push(child);
|
|
4010
|
+
}
|
|
4011
|
+
return children;
|
|
4012
|
+
};
|
|
4013
|
+
const processInitialScanQueue = () => {
|
|
4014
|
+
initialScanTimer = null;
|
|
4015
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
4016
|
+
reportWatchLimit();
|
|
4017
|
+
initialScanQueue.length = 0;
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
const next = initialScanQueue.shift();
|
|
4021
|
+
if (next)
|
|
4022
|
+
watchDirectory(next, true);
|
|
4023
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
4024
|
+
reportWatchLimit();
|
|
4025
|
+
initialScanQueue.length = 0;
|
|
4026
|
+
}
|
|
4027
|
+
if (initialScanQueue.length)
|
|
4028
|
+
initialScanTimer = setTimer(processInitialScanQueue, 50);
|
|
4029
|
+
};
|
|
4030
|
+
const queueInitialChildren = (dir) => {
|
|
4031
|
+
const remaining = maxWatchedDirectories - watchers.size;
|
|
4032
|
+
if (remaining <= 0) {
|
|
4033
|
+
reportWatchLimit();
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4036
|
+
const children = readChildDirectories(dir);
|
|
4037
|
+
if (children.length > remaining)
|
|
4038
|
+
reportWatchLimit();
|
|
4039
|
+
initialScanQueue.push(...children.slice(0, remaining));
|
|
4040
|
+
if (!initialScanTimer)
|
|
4041
|
+
initialScanTimer = setTimer(processInitialScanQueue, 5000);
|
|
4042
|
+
};
|
|
4043
|
+
const processChangedPath = (changed, fullChangedPath) => {
|
|
4044
|
+
const known = watchers.has(fullChangedPath);
|
|
4045
|
+
if (isDirectory(fullChangedPath)) {
|
|
4046
|
+
if (known) {
|
|
4047
|
+
const signature = directorySignature(fullChangedPath);
|
|
4048
|
+
if (signature && signature !== signatures.get(fullChangedPath)) {
|
|
4049
|
+
closeSubtree(fullChangedPath);
|
|
4050
|
+
watchDirectory(fullChangedPath, initialScanAsync);
|
|
4051
|
+
}
|
|
4052
|
+
scheduleUpdate(changed);
|
|
4053
|
+
return;
|
|
4054
|
+
}
|
|
4055
|
+
watchDirectory(fullChangedPath, initialScanAsync);
|
|
4056
|
+
} else if (known) {
|
|
4057
|
+
closeSubtree(fullChangedPath);
|
|
4058
|
+
}
|
|
4059
|
+
scheduleUpdate(changed);
|
|
4060
|
+
};
|
|
4061
|
+
const processPathInspections = () => {
|
|
4062
|
+
pathInspectionTimer = null;
|
|
4063
|
+
const entries = [...pendingPathInspections];
|
|
4064
|
+
pendingPathInspections.clear();
|
|
4065
|
+
for (const [changed, fullChangedPath] of entries) {
|
|
4066
|
+
processChangedPath(changed, fullChangedPath);
|
|
4067
|
+
}
|
|
4068
|
+
};
|
|
4069
|
+
const queuePathInspection = (changed, fullChangedPath) => {
|
|
4070
|
+
pendingPathInspections.set(changed, fullChangedPath);
|
|
4071
|
+
if (!pathInspectionTimer)
|
|
4072
|
+
pathInspectionTimer = setTimer(processPathInspections, 25);
|
|
4073
|
+
};
|
|
4074
|
+
const watchDirectory = (dir, initialScan = false) => {
|
|
4075
|
+
if (watchers.has(dir))
|
|
4076
|
+
return;
|
|
4077
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
4078
|
+
reportWatchLimit();
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
const rel = directoryRelativePath(dir);
|
|
4082
|
+
if (rel && ignored(rel))
|
|
4083
|
+
return;
|
|
4084
|
+
try {
|
|
4085
|
+
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
4086
|
+
if (!filename) {
|
|
4087
|
+
scheduleUpdate();
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
const changed = normalizeRelativePath(join7(rel, filename.toString()));
|
|
4091
|
+
if (ignored(changed))
|
|
4092
|
+
return;
|
|
4093
|
+
const fullChangedPath = join7(options.root, changed);
|
|
4094
|
+
if (!isInsideRoot(options.root, fullChangedPath))
|
|
4095
|
+
return;
|
|
4096
|
+
if (initialScanAsync) {
|
|
4097
|
+
queuePathInspection(changed, fullChangedPath);
|
|
4098
|
+
return;
|
|
4099
|
+
}
|
|
4100
|
+
processChangedPath(changed, fullChangedPath);
|
|
4101
|
+
}) || {};
|
|
4102
|
+
watchers.set(dir, watcher);
|
|
4103
|
+
const signature = directorySignature(dir);
|
|
4104
|
+
if (signature)
|
|
4105
|
+
signatures.set(dir, signature);
|
|
4106
|
+
watcher.on?.("error", () => {
|
|
4107
|
+
if (watchers.get(dir) === watcher) {
|
|
4108
|
+
watchers.delete(dir);
|
|
4109
|
+
signatures.delete(dir);
|
|
4110
|
+
}
|
|
4111
|
+
});
|
|
4112
|
+
watcher.on?.("close", () => {
|
|
4113
|
+
if (watchers.get(dir) === watcher) {
|
|
4114
|
+
watchers.delete(dir);
|
|
4115
|
+
signatures.delete(dir);
|
|
4116
|
+
}
|
|
4117
|
+
});
|
|
4118
|
+
} catch (error) {
|
|
4119
|
+
options.onError?.(error);
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
if (initialScanAsync && initialScan) {
|
|
4123
|
+
queueInitialChildren(dir);
|
|
4124
|
+
return;
|
|
4125
|
+
}
|
|
4126
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
4127
|
+
reportWatchLimit();
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
for (const child of readChildDirectories(dir))
|
|
4131
|
+
watchDirectory(child);
|
|
4132
|
+
};
|
|
4133
|
+
watchDirectory(options.root, true);
|
|
4134
|
+
return { started: watchers.size > 0, close: closeAll };
|
|
4135
|
+
}
|
|
4136
|
+
var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
|
|
4137
|
+
var init_worktree_watcher = __esm(() => {
|
|
4138
|
+
init_search();
|
|
4139
|
+
});
|
|
4140
|
+
|
|
3891
4141
|
// web-src/server/state-store.ts
|
|
3892
|
-
import { join as
|
|
4142
|
+
import { join as join8 } from "node:path";
|
|
3893
4143
|
function codeViewerPath(root, fileName) {
|
|
3894
|
-
return
|
|
4144
|
+
return join8(root, CODE_VIEWER_DIR2, fileName);
|
|
3895
4145
|
}
|
|
3896
4146
|
function isRecord(value) {
|
|
3897
4147
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4029,6 +4279,9 @@ function sanitizeSettings(raw) {
|
|
|
4029
4279
|
});
|
|
4030
4280
|
if (scopeExcludeNames)
|
|
4031
4281
|
out.scopeExcludeNames = scopeExcludeNames;
|
|
4282
|
+
const scopeWatchLimit = optionalNumber(raw.scopeWatchLimit, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT);
|
|
4283
|
+
if (scopeWatchLimit !== undefined)
|
|
4284
|
+
out.scopeWatchLimit = scopeWatchLimit;
|
|
4032
4285
|
const uploadEnabled = optionalBoolean(raw.uploadEnabled);
|
|
4033
4286
|
if (uploadEnabled !== undefined)
|
|
4034
4287
|
out.uploadEnabled = uploadEnabled;
|
|
@@ -4350,6 +4603,7 @@ async function patchDbUiState(root, patch) {
|
|
|
4350
4603
|
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;
|
|
4351
4604
|
var init_state_store = __esm(() => {
|
|
4352
4605
|
init_json_store();
|
|
4606
|
+
init_worktree_watcher();
|
|
4353
4607
|
DB_UI_BOOL_PREF_KEYS = ["s3TooltipEnabled", "inferFkRails"];
|
|
4354
4608
|
settingsStore = createJsonFileStore({
|
|
4355
4609
|
filePath: (root) => codeViewerPath(root, SETTINGS_FILE_NAME),
|
|
@@ -4377,256 +4631,6 @@ var init_state_store = __esm(() => {
|
|
|
4377
4631
|
});
|
|
4378
4632
|
});
|
|
4379
4633
|
|
|
4380
|
-
// web-src/server/worktree-watcher.ts
|
|
4381
|
-
import {
|
|
4382
|
-
lstatSync as lstatSync3,
|
|
4383
|
-
readdirSync as nodeReaddirSync,
|
|
4384
|
-
watch as nodeWatch
|
|
4385
|
-
} from "node:fs";
|
|
4386
|
-
import { join as join8, relative } from "node:path";
|
|
4387
|
-
function normalizeRelativePath(path) {
|
|
4388
|
-
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
4389
|
-
}
|
|
4390
|
-
function isInsideRoot(root, path) {
|
|
4391
|
-
const rel = relative(root, path).replace(/\\/g, "/");
|
|
4392
|
-
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/");
|
|
4393
|
-
}
|
|
4394
|
-
function startWorktreeUpdateWatch(options) {
|
|
4395
|
-
const watch = options.watch || nodeWatch;
|
|
4396
|
-
const readDirs = options.readdirSync || ((path) => nodeReaddirSync(path, { withFileTypes: true }));
|
|
4397
|
-
const isDirectory = options.isDirectory || ((path) => {
|
|
4398
|
-
try {
|
|
4399
|
-
return lstatSync3(path).isDirectory();
|
|
4400
|
-
} catch {
|
|
4401
|
-
return false;
|
|
4402
|
-
}
|
|
4403
|
-
});
|
|
4404
|
-
const directorySignature = options.directorySignature || ((path) => {
|
|
4405
|
-
try {
|
|
4406
|
-
const stats = lstatSync3(path);
|
|
4407
|
-
if (!stats.isDirectory())
|
|
4408
|
-
return null;
|
|
4409
|
-
return `${stats.dev}:${stats.ino}`;
|
|
4410
|
-
} catch {
|
|
4411
|
-
return null;
|
|
4412
|
-
}
|
|
4413
|
-
});
|
|
4414
|
-
const setTimer = options.setTimeoutFn || setTimeout;
|
|
4415
|
-
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
4416
|
-
const debounceMs = options.debounceMs ?? 250;
|
|
4417
|
-
const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
|
|
4418
|
-
const watchers = new Map;
|
|
4419
|
-
const signatures = new Map;
|
|
4420
|
-
const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
|
|
4421
|
-
const initialScanQueue = [];
|
|
4422
|
-
let initialScanTimer = null;
|
|
4423
|
-
const pendingPathInspections = new Map;
|
|
4424
|
-
let pathInspectionTimer = null;
|
|
4425
|
-
let timer = null;
|
|
4426
|
-
const pendingChangedPaths = new Set;
|
|
4427
|
-
let watchLimitReported = false;
|
|
4428
|
-
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
4429
|
-
const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
|
|
4430
|
-
const ignoredDirectory = (dir) => {
|
|
4431
|
-
const rel = directoryRelativePath(dir);
|
|
4432
|
-
return Boolean(rel && ignored(rel));
|
|
4433
|
-
};
|
|
4434
|
-
const scheduleUpdate = (changedPath) => {
|
|
4435
|
-
if (changedPath)
|
|
4436
|
-
pendingChangedPaths.add(changedPath);
|
|
4437
|
-
if (timer)
|
|
4438
|
-
clearTimer(timer);
|
|
4439
|
-
timer = setTimer(() => {
|
|
4440
|
-
timer = null;
|
|
4441
|
-
const paths = pendingChangedPaths.size ? [...pendingChangedPaths] : undefined;
|
|
4442
|
-
pendingChangedPaths.clear();
|
|
4443
|
-
options.onUpdate(paths);
|
|
4444
|
-
}, debounceMs);
|
|
4445
|
-
};
|
|
4446
|
-
const reportWatchLimit = () => {
|
|
4447
|
-
if (watchLimitReported)
|
|
4448
|
-
return;
|
|
4449
|
-
watchLimitReported = true;
|
|
4450
|
-
options.onWatchLimit?.(maxWatchedDirectories);
|
|
4451
|
-
options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
|
|
4452
|
-
};
|
|
4453
|
-
const closeSubtree = (dir) => {
|
|
4454
|
-
for (const [watchedDir, watcher] of [...watchers]) {
|
|
4455
|
-
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
4456
|
-
continue;
|
|
4457
|
-
try {
|
|
4458
|
-
watcher.close?.();
|
|
4459
|
-
} catch {}
|
|
4460
|
-
watchers.delete(watchedDir);
|
|
4461
|
-
signatures.delete(watchedDir);
|
|
4462
|
-
}
|
|
4463
|
-
};
|
|
4464
|
-
const closeAll = () => {
|
|
4465
|
-
if (initialScanTimer) {
|
|
4466
|
-
clearTimer(initialScanTimer);
|
|
4467
|
-
initialScanTimer = null;
|
|
4468
|
-
}
|
|
4469
|
-
if (pathInspectionTimer) {
|
|
4470
|
-
clearTimer(pathInspectionTimer);
|
|
4471
|
-
pathInspectionTimer = null;
|
|
4472
|
-
}
|
|
4473
|
-
initialScanQueue.length = 0;
|
|
4474
|
-
pendingPathInspections.clear();
|
|
4475
|
-
for (const watcher of [...watchers.values()]) {
|
|
4476
|
-
try {
|
|
4477
|
-
watcher.close?.();
|
|
4478
|
-
} catch {}
|
|
4479
|
-
}
|
|
4480
|
-
watchers.clear();
|
|
4481
|
-
signatures.clear();
|
|
4482
|
-
};
|
|
4483
|
-
const readChildDirectories = (dir) => {
|
|
4484
|
-
let entries;
|
|
4485
|
-
try {
|
|
4486
|
-
entries = readDirs(dir);
|
|
4487
|
-
} catch (error) {
|
|
4488
|
-
options.onError?.(error);
|
|
4489
|
-
return [];
|
|
4490
|
-
}
|
|
4491
|
-
const children = [];
|
|
4492
|
-
for (const entry of entries) {
|
|
4493
|
-
if (!entry.isDirectory())
|
|
4494
|
-
continue;
|
|
4495
|
-
const child = join8(dir, entry.name);
|
|
4496
|
-
if (ignoredDirectory(child))
|
|
4497
|
-
continue;
|
|
4498
|
-
children.push(child);
|
|
4499
|
-
}
|
|
4500
|
-
return children;
|
|
4501
|
-
};
|
|
4502
|
-
const processInitialScanQueue = () => {
|
|
4503
|
-
initialScanTimer = null;
|
|
4504
|
-
if (watchers.size >= maxWatchedDirectories) {
|
|
4505
|
-
reportWatchLimit();
|
|
4506
|
-
initialScanQueue.length = 0;
|
|
4507
|
-
return;
|
|
4508
|
-
}
|
|
4509
|
-
const next = initialScanQueue.shift();
|
|
4510
|
-
if (next)
|
|
4511
|
-
watchDirectory(next, true);
|
|
4512
|
-
if (watchers.size >= maxWatchedDirectories) {
|
|
4513
|
-
reportWatchLimit();
|
|
4514
|
-
initialScanQueue.length = 0;
|
|
4515
|
-
}
|
|
4516
|
-
if (initialScanQueue.length)
|
|
4517
|
-
initialScanTimer = setTimer(processInitialScanQueue, 50);
|
|
4518
|
-
};
|
|
4519
|
-
const queueInitialChildren = (dir) => {
|
|
4520
|
-
const remaining = maxWatchedDirectories - watchers.size;
|
|
4521
|
-
if (remaining <= 0) {
|
|
4522
|
-
reportWatchLimit();
|
|
4523
|
-
return;
|
|
4524
|
-
}
|
|
4525
|
-
const children = readChildDirectories(dir);
|
|
4526
|
-
if (children.length > remaining)
|
|
4527
|
-
reportWatchLimit();
|
|
4528
|
-
initialScanQueue.push(...children.slice(0, remaining));
|
|
4529
|
-
if (!initialScanTimer)
|
|
4530
|
-
initialScanTimer = setTimer(processInitialScanQueue, 5000);
|
|
4531
|
-
};
|
|
4532
|
-
const processChangedPath = (changed, fullChangedPath) => {
|
|
4533
|
-
const known = watchers.has(fullChangedPath);
|
|
4534
|
-
if (isDirectory(fullChangedPath)) {
|
|
4535
|
-
if (known) {
|
|
4536
|
-
const signature = directorySignature(fullChangedPath);
|
|
4537
|
-
if (signature && signature !== signatures.get(fullChangedPath)) {
|
|
4538
|
-
closeSubtree(fullChangedPath);
|
|
4539
|
-
watchDirectory(fullChangedPath, initialScanAsync);
|
|
4540
|
-
}
|
|
4541
|
-
scheduleUpdate(changed);
|
|
4542
|
-
return;
|
|
4543
|
-
}
|
|
4544
|
-
watchDirectory(fullChangedPath, initialScanAsync);
|
|
4545
|
-
} else if (known) {
|
|
4546
|
-
closeSubtree(fullChangedPath);
|
|
4547
|
-
}
|
|
4548
|
-
scheduleUpdate(changed);
|
|
4549
|
-
};
|
|
4550
|
-
const processPathInspections = () => {
|
|
4551
|
-
pathInspectionTimer = null;
|
|
4552
|
-
const entries = [...pendingPathInspections];
|
|
4553
|
-
pendingPathInspections.clear();
|
|
4554
|
-
for (const [changed, fullChangedPath] of entries) {
|
|
4555
|
-
processChangedPath(changed, fullChangedPath);
|
|
4556
|
-
}
|
|
4557
|
-
};
|
|
4558
|
-
const queuePathInspection = (changed, fullChangedPath) => {
|
|
4559
|
-
pendingPathInspections.set(changed, fullChangedPath);
|
|
4560
|
-
if (!pathInspectionTimer)
|
|
4561
|
-
pathInspectionTimer = setTimer(processPathInspections, 25);
|
|
4562
|
-
};
|
|
4563
|
-
const watchDirectory = (dir, initialScan = false) => {
|
|
4564
|
-
if (watchers.has(dir))
|
|
4565
|
-
return;
|
|
4566
|
-
if (watchers.size >= maxWatchedDirectories) {
|
|
4567
|
-
reportWatchLimit();
|
|
4568
|
-
return;
|
|
4569
|
-
}
|
|
4570
|
-
const rel = directoryRelativePath(dir);
|
|
4571
|
-
if (rel && ignored(rel))
|
|
4572
|
-
return;
|
|
4573
|
-
try {
|
|
4574
|
-
const watcher = watch(dir, { persistent: false }, (_event, filename) => {
|
|
4575
|
-
if (!filename) {
|
|
4576
|
-
scheduleUpdate();
|
|
4577
|
-
return;
|
|
4578
|
-
}
|
|
4579
|
-
const changed = normalizeRelativePath(join8(rel, filename.toString()));
|
|
4580
|
-
if (ignored(changed))
|
|
4581
|
-
return;
|
|
4582
|
-
const fullChangedPath = join8(options.root, changed);
|
|
4583
|
-
if (!isInsideRoot(options.root, fullChangedPath))
|
|
4584
|
-
return;
|
|
4585
|
-
if (initialScanAsync) {
|
|
4586
|
-
queuePathInspection(changed, fullChangedPath);
|
|
4587
|
-
return;
|
|
4588
|
-
}
|
|
4589
|
-
processChangedPath(changed, fullChangedPath);
|
|
4590
|
-
}) || {};
|
|
4591
|
-
watchers.set(dir, watcher);
|
|
4592
|
-
const signature = directorySignature(dir);
|
|
4593
|
-
if (signature)
|
|
4594
|
-
signatures.set(dir, signature);
|
|
4595
|
-
watcher.on?.("error", () => {
|
|
4596
|
-
if (watchers.get(dir) === watcher) {
|
|
4597
|
-
watchers.delete(dir);
|
|
4598
|
-
signatures.delete(dir);
|
|
4599
|
-
}
|
|
4600
|
-
});
|
|
4601
|
-
watcher.on?.("close", () => {
|
|
4602
|
-
if (watchers.get(dir) === watcher) {
|
|
4603
|
-
watchers.delete(dir);
|
|
4604
|
-
signatures.delete(dir);
|
|
4605
|
-
}
|
|
4606
|
-
});
|
|
4607
|
-
} catch (error) {
|
|
4608
|
-
options.onError?.(error);
|
|
4609
|
-
return;
|
|
4610
|
-
}
|
|
4611
|
-
if (initialScanAsync && initialScan) {
|
|
4612
|
-
queueInitialChildren(dir);
|
|
4613
|
-
return;
|
|
4614
|
-
}
|
|
4615
|
-
if (watchers.size >= maxWatchedDirectories) {
|
|
4616
|
-
reportWatchLimit();
|
|
4617
|
-
return;
|
|
4618
|
-
}
|
|
4619
|
-
for (const child of readChildDirectories(dir))
|
|
4620
|
-
watchDirectory(child);
|
|
4621
|
-
};
|
|
4622
|
-
watchDirectory(options.root, true);
|
|
4623
|
-
return { started: watchers.size > 0, close: closeAll };
|
|
4624
|
-
}
|
|
4625
|
-
var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 1024, MIN_WORKTREE_WATCH_DIRECTORY_LIMIT = 1, MAX_WORKTREE_WATCH_DIRECTORY_LIMIT = 65536;
|
|
4626
|
-
var init_worktree_watcher = __esm(() => {
|
|
4627
|
-
init_search();
|
|
4628
|
-
});
|
|
4629
|
-
|
|
4630
4634
|
// web-src/core/control-chars.ts
|
|
4631
4635
|
function hasControlCharacter(value) {
|
|
4632
4636
|
for (const ch of value) {
|
|
@@ -4768,52 +4772,37 @@ function serializeDbRow(row) {
|
|
|
4768
4772
|
function serializeDbRows(rows) {
|
|
4769
4773
|
return rows.map(serializeDbRow);
|
|
4770
4774
|
}
|
|
4775
|
+
function coerceDbValue(value, columnType) {
|
|
4776
|
+
if (value === null)
|
|
4777
|
+
return null;
|
|
4778
|
+
const t = (columnType || "").toLowerCase();
|
|
4779
|
+
if (/bool/.test(t)) {
|
|
4780
|
+
const v = value.trim().toLowerCase();
|
|
4781
|
+
if (v === "")
|
|
4782
|
+
return null;
|
|
4783
|
+
if (v === "true" || v === "t" || v === "1")
|
|
4784
|
+
return true;
|
|
4785
|
+
if (v === "false" || v === "f" || v === "0")
|
|
4786
|
+
return false;
|
|
4787
|
+
return value;
|
|
4788
|
+
}
|
|
4789
|
+
if (/int|serial|real|floa|doub|numeric|decimal|number/.test(t)) {
|
|
4790
|
+
const trimmed = value.trim();
|
|
4791
|
+
if (trimmed === "")
|
|
4792
|
+
return null;
|
|
4793
|
+
const n = Number(trimmed);
|
|
4794
|
+
if (Number.isFinite(n) && String(n) === trimmed)
|
|
4795
|
+
return n;
|
|
4796
|
+
return value;
|
|
4797
|
+
}
|
|
4798
|
+
return value;
|
|
4799
|
+
}
|
|
4771
4800
|
var MIN_SAFE, MAX_SAFE;
|
|
4772
4801
|
var init_serialize = __esm(() => {
|
|
4773
4802
|
MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
|
4774
4803
|
MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
4775
4804
|
});
|
|
4776
4805
|
|
|
4777
|
-
// web-src/server/database/sources/sql-snapshot.ts
|
|
4778
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
4779
|
-
function normalizeRawValue(v) {
|
|
4780
|
-
if (v === null)
|
|
4781
|
-
return "\\N";
|
|
4782
|
-
if (typeof v === "bigint")
|
|
4783
|
-
return v.toString();
|
|
4784
|
-
if (v instanceof Uint8Array) {
|
|
4785
|
-
return `\\x${Buffer.from(v).toString("hex")}`;
|
|
4786
|
-
}
|
|
4787
|
-
return String(v);
|
|
4788
|
-
}
|
|
4789
|
-
function rowToPayloadJson(columns, row) {
|
|
4790
|
-
const obj = {};
|
|
4791
|
-
for (let i = 0;i < columns.length; i++) {
|
|
4792
|
-
obj[columns[i]] = serializeDbValue(row[i]);
|
|
4793
|
-
}
|
|
4794
|
-
return JSON.stringify(obj);
|
|
4795
|
-
}
|
|
4796
|
-
function computeRowHash(columns, row) {
|
|
4797
|
-
const parts = columns.map((_, i) => normalizeRawValue(row[i]));
|
|
4798
|
-
return createHash2("sha256").update(parts.join("\t")).digest("hex");
|
|
4799
|
-
}
|
|
4800
|
-
function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
|
|
4801
|
-
if (pkColumns.length === 0) {
|
|
4802
|
-
return JSON.stringify({ __rowIndex: rowIndex });
|
|
4803
|
-
}
|
|
4804
|
-
const keyObj = {};
|
|
4805
|
-
for (const pk of pkColumns) {
|
|
4806
|
-
const idx = allColumns.indexOf(pk);
|
|
4807
|
-
if (idx >= 0)
|
|
4808
|
-
keyObj[pk] = serializeDbValue(row[idx]);
|
|
4809
|
-
}
|
|
4810
|
-
return JSON.stringify(keyObj);
|
|
4811
|
-
}
|
|
4812
|
-
var SQL_SNAPSHOT_BATCH_SIZE = 500;
|
|
4813
|
-
var init_sql_snapshot = __esm(() => {
|
|
4814
|
-
init_serialize();
|
|
4815
|
-
});
|
|
4816
|
-
|
|
4817
4806
|
// web-src/server/database/sql-utils.ts
|
|
4818
4807
|
function sanitizeIdentifier(name, kind = "sqlite") {
|
|
4819
4808
|
if (kind === "mysql")
|
|
@@ -4875,6 +4864,189 @@ function filterOrderByColumns(orderBy, columnNames) {
|
|
|
4875
4864
|
const filtered = orderBy.filter((order) => validColumns.has(order.column));
|
|
4876
4865
|
return filtered.length > 0 ? filtered : undefined;
|
|
4877
4866
|
}
|
|
4867
|
+
function buildOrderClause(orderBy, kind = "sqlite") {
|
|
4868
|
+
if (!orderBy?.length)
|
|
4869
|
+
return "";
|
|
4870
|
+
const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
|
|
4871
|
+
return ` ORDER BY ${parts.join(", ")}`;
|
|
4872
|
+
}
|
|
4873
|
+
function useParamsFor(kind) {
|
|
4874
|
+
return kind === "sqlite";
|
|
4875
|
+
}
|
|
4876
|
+
function placeValue(coerced, kind, useParams, params) {
|
|
4877
|
+
if (useParams) {
|
|
4878
|
+
params.push(typeof coerced === "boolean" ? coerced ? 1 : 0 : coerced);
|
|
4879
|
+
return "?";
|
|
4880
|
+
}
|
|
4881
|
+
if (coerced === null)
|
|
4882
|
+
return "NULL";
|
|
4883
|
+
if (typeof coerced === "number")
|
|
4884
|
+
return String(coerced);
|
|
4885
|
+
if (typeof coerced === "boolean")
|
|
4886
|
+
return coerced ? "TRUE" : "FALSE";
|
|
4887
|
+
const text = coerced instanceof Uint8Array ? new TextDecoder().decode(coerced) : String(coerced);
|
|
4888
|
+
return escapeSqlString(text, kind);
|
|
4889
|
+
}
|
|
4890
|
+
function coerceCell(cell, columnType) {
|
|
4891
|
+
return coerceDbValue(cell.value, columnType);
|
|
4892
|
+
}
|
|
4893
|
+
function buildInsertSql(table, cells, columnTypes, kind) {
|
|
4894
|
+
if (cells.length === 0) {
|
|
4895
|
+
throw new Error("insert requires at least one column value");
|
|
4896
|
+
}
|
|
4897
|
+
const useParams = useParamsFor(kind);
|
|
4898
|
+
const params = [];
|
|
4899
|
+
const cols = cells.map((c) => sanitizeIdentifier(c.column, kind));
|
|
4900
|
+
const placeholders = cells.map((c) => placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params));
|
|
4901
|
+
const sql = `INSERT INTO ${sanitizeIdentifier(table, kind)} (${cols.join(", ")}) VALUES (${placeholders.join(", ")})`;
|
|
4902
|
+
return { sql, params };
|
|
4903
|
+
}
|
|
4904
|
+
function buildUpdateSql(table, set, pk, columnTypes, kind) {
|
|
4905
|
+
if (set.length === 0) {
|
|
4906
|
+
throw new Error("update requires at least one column to set");
|
|
4907
|
+
}
|
|
4908
|
+
if (pk.length === 0) {
|
|
4909
|
+
throw new Error("update requires a primary key condition");
|
|
4910
|
+
}
|
|
4911
|
+
const useParams = useParamsFor(kind);
|
|
4912
|
+
const params = [];
|
|
4913
|
+
const setSql = set.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(", ");
|
|
4914
|
+
const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
|
|
4915
|
+
const sql = `UPDATE ${sanitizeIdentifier(table, kind)} SET ${setSql} WHERE ${whereSql}`;
|
|
4916
|
+
return { sql, params };
|
|
4917
|
+
}
|
|
4918
|
+
function buildDeleteSql(table, pk, columnTypes, kind) {
|
|
4919
|
+
if (pk.length === 0) {
|
|
4920
|
+
throw new Error("delete requires a primary key condition");
|
|
4921
|
+
}
|
|
4922
|
+
const useParams = useParamsFor(kind);
|
|
4923
|
+
const params = [];
|
|
4924
|
+
const whereSql = pk.map((c) => `${sanitizeIdentifier(c.column, kind)} = ${placeValue(coerceCell(c, columnTypes.get(c.column) ?? "TEXT"), kind, useParams, params)}`).join(" AND ");
|
|
4925
|
+
const sql = `DELETE FROM ${sanitizeIdentifier(table, kind)} WHERE ${whereSql}`;
|
|
4926
|
+
return { sql, params };
|
|
4927
|
+
}
|
|
4928
|
+
var init_sql_utils = __esm(() => {
|
|
4929
|
+
init_serialize();
|
|
4930
|
+
});
|
|
4931
|
+
|
|
4932
|
+
// web-src/server/database/mutate.ts
|
|
4933
|
+
function assertCells(cells, label) {
|
|
4934
|
+
if (!Array.isArray(cells)) {
|
|
4935
|
+
throw new Error(`${label} must be an array`);
|
|
4936
|
+
}
|
|
4937
|
+
for (const cell of cells) {
|
|
4938
|
+
if (!cell || typeof cell !== "object" || typeof cell.column !== "string" || cell.value !== null && typeof cell.value !== "string") {
|
|
4939
|
+
throw new Error(`${label} contains an invalid cell`);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
return cells;
|
|
4943
|
+
}
|
|
4944
|
+
function buildMutationStatements(table, mutations, columns, kind) {
|
|
4945
|
+
if (!Array.isArray(mutations) || mutations.length === 0) {
|
|
4946
|
+
throw new Error("no mutations provided");
|
|
4947
|
+
}
|
|
4948
|
+
if (mutations.length > MAX_MUTATIONS) {
|
|
4949
|
+
throw new Error(`too many mutations (max ${MAX_MUTATIONS})`);
|
|
4950
|
+
}
|
|
4951
|
+
const columnTypes = new Map(columns.map((c) => [c.name, c.type]));
|
|
4952
|
+
const columnNames = new Set(columns.map((c) => c.name));
|
|
4953
|
+
const pkColumns = columns.filter((c) => c.primaryKey).map((c) => c.name);
|
|
4954
|
+
const pkNames = new Set(pkColumns);
|
|
4955
|
+
const requireKnownColumns = (cells, label) => {
|
|
4956
|
+
for (const cell of cells) {
|
|
4957
|
+
if (!columnNames.has(cell.column)) {
|
|
4958
|
+
throw new Error(`unknown column: ${cell.column}`);
|
|
4959
|
+
}
|
|
4960
|
+
}
|
|
4961
|
+
};
|
|
4962
|
+
const requirePrimaryKey = (pk) => {
|
|
4963
|
+
if (pkColumns.length === 0) {
|
|
4964
|
+
throw new Error("table has no primary key; row update/delete is not supported");
|
|
4965
|
+
}
|
|
4966
|
+
const provided = new Set(pk.map((c) => c.column));
|
|
4967
|
+
for (const name of pkColumns) {
|
|
4968
|
+
if (!provided.has(name)) {
|
|
4969
|
+
throw new Error(`missing primary key column: ${name}`);
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
for (const cell of pk) {
|
|
4973
|
+
if (!pkNames.has(cell.column)) {
|
|
4974
|
+
throw new Error(`not a primary key column: ${cell.column}`);
|
|
4975
|
+
}
|
|
4976
|
+
if (cell.value === null) {
|
|
4977
|
+
throw new Error(`primary key column cannot be null: ${cell.column}`);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
};
|
|
4981
|
+
const statements = [];
|
|
4982
|
+
for (const mutation of mutations) {
|
|
4983
|
+
if (!mutation || typeof mutation !== "object") {
|
|
4984
|
+
throw new Error("invalid mutation");
|
|
4985
|
+
}
|
|
4986
|
+
if (mutation.kind === "insert") {
|
|
4987
|
+
const values = assertCells(mutation.values, "insert values");
|
|
4988
|
+
requireKnownColumns(values, "insert values");
|
|
4989
|
+
statements.push(buildInsertSql(table, values, columnTypes, kind));
|
|
4990
|
+
} else if (mutation.kind === "update") {
|
|
4991
|
+
const pk = assertCells(mutation.pk, "update pk");
|
|
4992
|
+
const values = assertCells(mutation.values, "update values");
|
|
4993
|
+
requirePrimaryKey(pk);
|
|
4994
|
+
requireKnownColumns(values, "update values");
|
|
4995
|
+
statements.push(buildUpdateSql(table, values, pk, columnTypes, kind));
|
|
4996
|
+
} else if (mutation.kind === "delete") {
|
|
4997
|
+
const pk = assertCells(mutation.pk, "delete pk");
|
|
4998
|
+
requirePrimaryKey(pk);
|
|
4999
|
+
statements.push(buildDeleteSql(table, pk, columnTypes, kind));
|
|
5000
|
+
} else {
|
|
5001
|
+
throw new Error(`unknown mutation kind: ${mutation.kind}`);
|
|
5002
|
+
}
|
|
5003
|
+
}
|
|
5004
|
+
return statements;
|
|
5005
|
+
}
|
|
5006
|
+
var MAX_MUTATIONS = 1000;
|
|
5007
|
+
var init_mutate = __esm(() => {
|
|
5008
|
+
init_sql_utils();
|
|
5009
|
+
});
|
|
5010
|
+
|
|
5011
|
+
// web-src/server/database/sources/sql-snapshot.ts
|
|
5012
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5013
|
+
function normalizeRawValue(v) {
|
|
5014
|
+
if (v === null)
|
|
5015
|
+
return "\\N";
|
|
5016
|
+
if (typeof v === "bigint")
|
|
5017
|
+
return v.toString();
|
|
5018
|
+
if (v instanceof Uint8Array) {
|
|
5019
|
+
return `\\x${Buffer.from(v).toString("hex")}`;
|
|
5020
|
+
}
|
|
5021
|
+
return String(v);
|
|
5022
|
+
}
|
|
5023
|
+
function rowToPayloadJson(columns, row) {
|
|
5024
|
+
const obj = {};
|
|
5025
|
+
for (let i = 0;i < columns.length; i++) {
|
|
5026
|
+
obj[columns[i]] = serializeDbValue(row[i]);
|
|
5027
|
+
}
|
|
5028
|
+
return JSON.stringify(obj);
|
|
5029
|
+
}
|
|
5030
|
+
function computeRowHash(columns, row) {
|
|
5031
|
+
const parts = columns.map((_, i) => normalizeRawValue(row[i]));
|
|
5032
|
+
return createHash2("sha256").update(parts.join("\t")).digest("hex");
|
|
5033
|
+
}
|
|
5034
|
+
function buildRowKeyJson(pkColumns, allColumns, row, rowIndex) {
|
|
5035
|
+
if (pkColumns.length === 0) {
|
|
5036
|
+
return JSON.stringify({ __rowIndex: rowIndex });
|
|
5037
|
+
}
|
|
5038
|
+
const keyObj = {};
|
|
5039
|
+
for (const pk of pkColumns) {
|
|
5040
|
+
const idx = allColumns.indexOf(pk);
|
|
5041
|
+
if (idx >= 0)
|
|
5042
|
+
keyObj[pk] = serializeDbValue(row[idx]);
|
|
5043
|
+
}
|
|
5044
|
+
return JSON.stringify(keyObj);
|
|
5045
|
+
}
|
|
5046
|
+
var SQL_SNAPSHOT_BATCH_SIZE = 500;
|
|
5047
|
+
var init_sql_snapshot = __esm(() => {
|
|
5048
|
+
init_serialize();
|
|
5049
|
+
});
|
|
4878
5050
|
|
|
4879
5051
|
// web-src/server/database/adapters/spawn-runner.ts
|
|
4880
5052
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -5124,6 +5296,24 @@ var init_docker_utils = __esm(() => {
|
|
|
5124
5296
|
};
|
|
5125
5297
|
});
|
|
5126
5298
|
|
|
5299
|
+
// web-src/server/database/adapters/sql-capture.ts
|
|
5300
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5301
|
+
function recordSql(sql) {
|
|
5302
|
+
const bucket = storage.getStore();
|
|
5303
|
+
if (!bucket)
|
|
5304
|
+
return;
|
|
5305
|
+
bucket.sqls.push(sql);
|
|
5306
|
+
}
|
|
5307
|
+
async function captureSql(fn) {
|
|
5308
|
+
const bucket = { sqls: [] };
|
|
5309
|
+
const result = await storage.run(bucket, fn);
|
|
5310
|
+
return { result, executedSql: bucket.sqls };
|
|
5311
|
+
}
|
|
5312
|
+
var storage;
|
|
5313
|
+
var init_sql_capture = __esm(() => {
|
|
5314
|
+
storage = new AsyncLocalStorage;
|
|
5315
|
+
});
|
|
5316
|
+
|
|
5127
5317
|
// web-src/server/database/adapters/docker.ts
|
|
5128
5318
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5129
5319
|
function dockerDatabasesCacheKey(serviceName, kind, cwd) {
|
|
@@ -5279,6 +5469,7 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
|
|
|
5279
5469
|
});
|
|
5280
5470
|
}
|
|
5281
5471
|
async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
|
|
5472
|
+
recordSql(sql);
|
|
5282
5473
|
if (spawnSyncImpl2 !== spawnSync3)
|
|
5283
5474
|
return execInContainer(config, sql, timeoutMs);
|
|
5284
5475
|
const args = buildExecArgs(config, sql);
|
|
@@ -5366,12 +5557,6 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
|
|
|
5366
5557
|
const rows = lines.map((line) => splitTsvLine(line, false));
|
|
5367
5558
|
return { columns: [], rows };
|
|
5368
5559
|
}
|
|
5369
|
-
function buildOrderClause(orderBy, kind) {
|
|
5370
|
-
if (!orderBy?.length)
|
|
5371
|
-
return "";
|
|
5372
|
-
const parts = orderBy.map((o) => `${sanitizeIdentifier(o.column, kind)} ${o.direction === "desc" ? "DESC" : "ASC"}`);
|
|
5373
|
-
return ` ORDER BY ${parts.join(", ")}`;
|
|
5374
|
-
}
|
|
5375
5560
|
function isMysqlSpatialType(type) {
|
|
5376
5561
|
const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
|
|
5377
5562
|
return MYSQL_SPATIAL_TYPES.has(baseType);
|
|
@@ -5460,7 +5645,7 @@ function createDockerAdapter(config) {
|
|
|
5460
5645
|
const tableLiteral = table.replace(/'/g, "''");
|
|
5461
5646
|
if (config.kind === "postgresql") {
|
|
5462
5647
|
const schemaLiteral = postgresSchemaLiteral();
|
|
5463
|
-
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
|
|
5648
|
+
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`;
|
|
5464
5649
|
}
|
|
5465
5650
|
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`;
|
|
5466
5651
|
}
|
|
@@ -5794,6 +5979,19 @@ function createDockerAdapter(config) {
|
|
|
5794
5979
|
rowCount: Math.min(result.rows.length, maxRows)
|
|
5795
5980
|
};
|
|
5796
5981
|
},
|
|
5982
|
+
async applyMutations(table, mutations, signal) {
|
|
5983
|
+
const columns = await this.getColumnsAsync(table, signal);
|
|
5984
|
+
if (columns.length === 0) {
|
|
5985
|
+
throw new Error(`unknown table: ${table}`);
|
|
5986
|
+
}
|
|
5987
|
+
const statements = buildMutationStatements(table, mutations, columns, config.kind);
|
|
5988
|
+
const body = statements.map((s) => s.sql).join(`;
|
|
5989
|
+
`);
|
|
5990
|
+
const wrapped = config.kind === "postgresql" ? `BEGIN; SET LOCAL search_path = ${sanitizeIdentifier(currentPostgresSchema(), config.kind)}; ${body}; COMMIT` : `START TRANSACTION; ${body}; COMMIT`;
|
|
5991
|
+
await execAsync(wrapped, signal);
|
|
5992
|
+
this.invalidateTableMetaCache?.(table);
|
|
5993
|
+
return { affected: statements.length };
|
|
5994
|
+
},
|
|
5797
5995
|
invalidateTableMetaCache(table) {
|
|
5798
5996
|
tableMetaCache.invalidate(table);
|
|
5799
5997
|
if (table) {
|
|
@@ -5975,9 +6173,12 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
|
|
|
5975
6173
|
}
|
|
5976
6174
|
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;
|
|
5977
6175
|
var init_docker = __esm(() => {
|
|
6176
|
+
init_mutate();
|
|
5978
6177
|
init_sql_snapshot();
|
|
6178
|
+
init_sql_utils();
|
|
5979
6179
|
init_docker_utils();
|
|
5980
6180
|
init_spawn_runner();
|
|
6181
|
+
init_sql_capture();
|
|
5981
6182
|
dockerDatabasesCache = new Map;
|
|
5982
6183
|
dockerSchemasCache = new Map;
|
|
5983
6184
|
spawnSyncImpl2 = spawnSync3;
|
|
@@ -5994,17 +6195,8 @@ var init_docker = __esm(() => {
|
|
|
5994
6195
|
]);
|
|
5995
6196
|
});
|
|
5996
6197
|
|
|
5997
|
-
// web-src/server/database/
|
|
5998
|
-
function
|
|
5999
|
-
const stmt = db.prepare(sql);
|
|
6000
|
-
if (typeof stmt.safeIntegers === "function") {
|
|
6001
|
-
try {
|
|
6002
|
-
stmt.safeIntegers(true);
|
|
6003
|
-
} catch {}
|
|
6004
|
-
}
|
|
6005
|
-
return stmt;
|
|
6006
|
-
}
|
|
6007
|
-
async function getSqliteClass() {
|
|
6198
|
+
// web-src/server/database/sqlite-driver.ts
|
|
6199
|
+
async function loadSqliteClass() {
|
|
6008
6200
|
if (cachedDbClass)
|
|
6009
6201
|
return cachedDbClass;
|
|
6010
6202
|
try {
|
|
@@ -6019,11 +6211,17 @@ async function getSqliteClass() {
|
|
|
6019
6211
|
} catch {}
|
|
6020
6212
|
throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
|
|
6021
6213
|
}
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6214
|
+
var cachedDbClass = null;
|
|
6215
|
+
|
|
6216
|
+
// web-src/server/database/adapters/sqlite.ts
|
|
6217
|
+
function safePrepare(db, sql) {
|
|
6218
|
+
const stmt = db.prepare(sql);
|
|
6219
|
+
if (typeof stmt.safeIntegers === "function") {
|
|
6220
|
+
try {
|
|
6221
|
+
stmt.safeIntegers(true);
|
|
6222
|
+
} catch {}
|
|
6223
|
+
}
|
|
6224
|
+
return stmt;
|
|
6027
6225
|
}
|
|
6028
6226
|
function queryRowsToResult(rows, columns) {
|
|
6029
6227
|
const columnNames = rows.length > 0 ? Object.keys(rows[0]) : columns.map((c) => c.name);
|
|
@@ -6045,7 +6243,30 @@ function queryColumns(db, table) {
|
|
|
6045
6243
|
defaultValue: row.dflt_value
|
|
6046
6244
|
}));
|
|
6047
6245
|
}
|
|
6048
|
-
function
|
|
6246
|
+
function wrapDbWithSqlCapture(rawDb) {
|
|
6247
|
+
return new Proxy(rawDb, {
|
|
6248
|
+
get(target, prop, receiver) {
|
|
6249
|
+
if (prop === "prepare") {
|
|
6250
|
+
return (sql) => {
|
|
6251
|
+
recordSql(sql);
|
|
6252
|
+
return target.prepare(sql);
|
|
6253
|
+
};
|
|
6254
|
+
}
|
|
6255
|
+
return Reflect.get(target, prop, receiver);
|
|
6256
|
+
}
|
|
6257
|
+
});
|
|
6258
|
+
}
|
|
6259
|
+
function createSqliteAdapter(rawDb, openRawWriteDb) {
|
|
6260
|
+
const db = wrapDbWithSqlCapture(rawDb);
|
|
6261
|
+
let writeDb = null;
|
|
6262
|
+
const getWriteDb = () => {
|
|
6263
|
+
if (!openRawWriteDb) {
|
|
6264
|
+
throw new Error("writes are not supported for this connection");
|
|
6265
|
+
}
|
|
6266
|
+
if (!writeDb)
|
|
6267
|
+
writeDb = wrapDbWithSqlCapture(openRawWriteDb());
|
|
6268
|
+
return writeDb;
|
|
6269
|
+
};
|
|
6049
6270
|
const adapter = {
|
|
6050
6271
|
kind: "sqlite",
|
|
6051
6272
|
model: "sql",
|
|
@@ -6145,7 +6366,7 @@ function createSqliteAdapter(db) {
|
|
|
6145
6366
|
return this.getTableRowCounts(tables);
|
|
6146
6367
|
},
|
|
6147
6368
|
getTablePage(table, options) {
|
|
6148
|
-
const order =
|
|
6369
|
+
const order = buildOrderClause(options.orderBy);
|
|
6149
6370
|
const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
|
|
6150
6371
|
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
6151
6372
|
const cols = queryColumns(db, table);
|
|
@@ -6157,7 +6378,7 @@ function createSqliteAdapter(db) {
|
|
|
6157
6378
|
async getTablePageWithMeta(table, options) {
|
|
6158
6379
|
const columns = queryColumns(db, table);
|
|
6159
6380
|
const orderBy = filterOrderByColumns(options.orderBy, columns.map((column) => column.name));
|
|
6160
|
-
const order =
|
|
6381
|
+
const order = buildOrderClause(orderBy);
|
|
6161
6382
|
const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
|
|
6162
6383
|
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
6163
6384
|
const result = queryRowsToResult(rows, columns);
|
|
@@ -6173,7 +6394,7 @@ function createSqliteAdapter(db) {
|
|
|
6173
6394
|
const columns = queryColumns(db, table);
|
|
6174
6395
|
const columnNames = columns.map((column) => column.name);
|
|
6175
6396
|
const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
|
|
6176
|
-
const order =
|
|
6397
|
+
const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames));
|
|
6177
6398
|
const tableId = sanitizeIdentifier(table);
|
|
6178
6399
|
const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
|
|
6179
6400
|
const countRow = safePrepare(db, `SELECT COUNT(*) AS cnt FROM ${tableId}${whereClause}`).get(...filter.params);
|
|
@@ -6241,7 +6462,36 @@ function createSqliteAdapter(db) {
|
|
|
6241
6462
|
async getTriggersAsync(table) {
|
|
6242
6463
|
return this.getTriggers(table);
|
|
6243
6464
|
},
|
|
6465
|
+
async applyMutations(table, mutations) {
|
|
6466
|
+
const columns = queryColumns(db, table);
|
|
6467
|
+
if (columns.length === 0) {
|
|
6468
|
+
throw new Error(`unknown table: ${table}`);
|
|
6469
|
+
}
|
|
6470
|
+
const statements = buildMutationStatements(table, mutations, columns, "sqlite");
|
|
6471
|
+
const wdb = getWriteDb();
|
|
6472
|
+
let affected = 0;
|
|
6473
|
+
wdb.prepare("BEGIN").run();
|
|
6474
|
+
try {
|
|
6475
|
+
for (const stmt of statements) {
|
|
6476
|
+
const result = wdb.prepare(stmt.sql).run(...stmt.params);
|
|
6477
|
+
affected += result.changes ?? 0;
|
|
6478
|
+
}
|
|
6479
|
+
wdb.prepare("COMMIT").run();
|
|
6480
|
+
} catch (err) {
|
|
6481
|
+
try {
|
|
6482
|
+
wdb.prepare("ROLLBACK").run();
|
|
6483
|
+
} catch {}
|
|
6484
|
+
throw err;
|
|
6485
|
+
}
|
|
6486
|
+
return { affected };
|
|
6487
|
+
},
|
|
6244
6488
|
close() {
|
|
6489
|
+
if (writeDb) {
|
|
6490
|
+
try {
|
|
6491
|
+
writeDb.close();
|
|
6492
|
+
} catch {}
|
|
6493
|
+
writeDb = null;
|
|
6494
|
+
}
|
|
6245
6495
|
db.close();
|
|
6246
6496
|
},
|
|
6247
6497
|
async* iterateForSnapshot(table, signal) {
|
|
@@ -6279,14 +6529,18 @@ function createSqliteAdapter(db) {
|
|
|
6279
6529
|
};
|
|
6280
6530
|
return adapter;
|
|
6281
6531
|
}
|
|
6282
|
-
var
|
|
6532
|
+
var sqliteAdapterFactory;
|
|
6283
6533
|
var init_sqlite = __esm(() => {
|
|
6534
|
+
init_mutate();
|
|
6284
6535
|
init_sql_snapshot();
|
|
6536
|
+
init_sql_utils();
|
|
6537
|
+
init_sql_capture();
|
|
6285
6538
|
sqliteAdapterFactory = {
|
|
6286
6539
|
async open(path) {
|
|
6287
|
-
const DbClass = await
|
|
6540
|
+
const DbClass = await loadSqliteClass();
|
|
6288
6541
|
const db = new DbClass(path, { readonly: true, create: false });
|
|
6289
|
-
|
|
6542
|
+
const openWriteDb = () => new DbClass(path);
|
|
6543
|
+
return createSqliteAdapter(db, openWriteDb);
|
|
6290
6544
|
}
|
|
6291
6545
|
};
|
|
6292
6546
|
});
|
|
@@ -7055,6 +7309,7 @@ function getPrimaryKeyColumnsFromColumns(columns) {
|
|
|
7055
7309
|
}
|
|
7056
7310
|
var init_global_search = __esm(() => {
|
|
7057
7311
|
init_serialize();
|
|
7312
|
+
init_sql_utils();
|
|
7058
7313
|
});
|
|
7059
7314
|
|
|
7060
7315
|
// web-src/server/database/adapters/elasticsearch.ts
|
|
@@ -7254,6 +7509,38 @@ function createElasticsearchAdapter(config) {
|
|
|
7254
7509
|
primaryTerm: parsed._primary_term
|
|
7255
7510
|
};
|
|
7256
7511
|
}
|
|
7512
|
+
function assertIndex(index) {
|
|
7513
|
+
if (!index || index.includes("/") || index.includes("?")) {
|
|
7514
|
+
throw new Error(`invalid index name: ${index}`);
|
|
7515
|
+
}
|
|
7516
|
+
}
|
|
7517
|
+
async function writeDocAsync(opts) {
|
|
7518
|
+
assertIndex(opts.index);
|
|
7519
|
+
const idGiven = typeof opts.id === "string" && opts.id !== "";
|
|
7520
|
+
let path;
|
|
7521
|
+
let method;
|
|
7522
|
+
if (idGiven && opts.create) {
|
|
7523
|
+
path = `/${encodeURIComponent(opts.index)}/_create/${encodeURIComponent(opts.id)}`;
|
|
7524
|
+
method = "PUT";
|
|
7525
|
+
} else if (idGiven) {
|
|
7526
|
+
path = `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`;
|
|
7527
|
+
method = "PUT";
|
|
7528
|
+
if (opts.seqNo !== undefined && opts.primaryTerm !== undefined) {
|
|
7529
|
+
path += `?if_seq_no=${opts.seqNo}&if_primary_term=${opts.primaryTerm}`;
|
|
7530
|
+
}
|
|
7531
|
+
} else {
|
|
7532
|
+
path = `/${encodeURIComponent(opts.index)}/_doc`;
|
|
7533
|
+
method = "POST";
|
|
7534
|
+
}
|
|
7535
|
+
const resp = await callJsonAsync(method, path, opts.source, "_doc write", opts.signal);
|
|
7536
|
+
return { id: resp._id ?? opts.id ?? "", result: resp.result ?? "" };
|
|
7537
|
+
}
|
|
7538
|
+
async function deleteDocAsync(opts) {
|
|
7539
|
+
assertIndex(opts.index);
|
|
7540
|
+
if (!opts.id)
|
|
7541
|
+
throw new Error("missing doc id");
|
|
7542
|
+
await callJsonAsync("DELETE", `/${encodeURIComponent(opts.index)}/_doc/${encodeURIComponent(opts.id)}`, undefined, "_doc delete", opts.signal);
|
|
7543
|
+
}
|
|
7257
7544
|
async function* iterateForSnapshot(container, signal) {
|
|
7258
7545
|
const { index, query: query2 } = parseEsSnapshotContainer(container);
|
|
7259
7546
|
const PAGE = 1000;
|
|
@@ -7327,6 +7614,8 @@ function createElasticsearchAdapter(config) {
|
|
|
7327
7614
|
getMappingAsync,
|
|
7328
7615
|
searchDocsAsync,
|
|
7329
7616
|
getDocAsync,
|
|
7617
|
+
writeDocAsync,
|
|
7618
|
+
deleteDocAsync,
|
|
7330
7619
|
iterateForSnapshot,
|
|
7331
7620
|
listSnapshotContainers,
|
|
7332
7621
|
query,
|
|
@@ -7806,6 +8095,51 @@ async function handleMapping(req, cwd, url, omitDirNames) {
|
|
|
7806
8095
|
return handleError("elasticsearch", "read elasticsearch mapping", err);
|
|
7807
8096
|
}
|
|
7808
8097
|
}
|
|
8098
|
+
async function handleWrite(req, cwd, omitDirNames) {
|
|
8099
|
+
const parsed = await parseBoundedJsonBody(req, 4 * 1024 * 1024, "payload too large");
|
|
8100
|
+
if (parsed instanceof Response)
|
|
8101
|
+
return parsed;
|
|
8102
|
+
const body = parsed;
|
|
8103
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
8104
|
+
return textError("missing db", 400);
|
|
8105
|
+
}
|
|
8106
|
+
if (typeof body.index !== "string" || body.index === "") {
|
|
8107
|
+
return textError("missing index", 400);
|
|
8108
|
+
}
|
|
8109
|
+
const id = typeof body.id === "string" ? body.id : undefined;
|
|
8110
|
+
const r = await resolveEs(cwd, body.db, req.signal, omitDirNames);
|
|
8111
|
+
if (r instanceof Response)
|
|
8112
|
+
return r;
|
|
8113
|
+
try {
|
|
8114
|
+
if (body.op === "delete") {
|
|
8115
|
+
if (!id)
|
|
8116
|
+
return textError("missing id", 400);
|
|
8117
|
+
await r.explorer.deleteDocAsync({
|
|
8118
|
+
index: body.index,
|
|
8119
|
+
id,
|
|
8120
|
+
signal: req.signal
|
|
8121
|
+
});
|
|
8122
|
+
return json({ ok: true });
|
|
8123
|
+
}
|
|
8124
|
+
if (body.source === null || typeof body.source !== "object" || Array.isArray(body.source)) {
|
|
8125
|
+
return textError("source must be a JSON object", 400);
|
|
8126
|
+
}
|
|
8127
|
+
const seqNo = typeof body.seqNo === "number" ? body.seqNo : undefined;
|
|
8128
|
+
const primaryTerm = typeof body.primaryTerm === "number" ? body.primaryTerm : undefined;
|
|
8129
|
+
const result = await r.explorer.writeDocAsync({
|
|
8130
|
+
index: body.index,
|
|
8131
|
+
id,
|
|
8132
|
+
source: body.source,
|
|
8133
|
+
seqNo,
|
|
8134
|
+
primaryTerm,
|
|
8135
|
+
create: body.op === "create",
|
|
8136
|
+
signal: req.signal
|
|
8137
|
+
});
|
|
8138
|
+
return json({ ok: true, id: result.id, result: result.result });
|
|
8139
|
+
} catch (err) {
|
|
8140
|
+
return handleError("elasticsearch", "write elasticsearch doc", err);
|
|
8141
|
+
}
|
|
8142
|
+
}
|
|
7809
8143
|
async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
7810
8144
|
const wrap = createQueryStrippedLogger("elasticsearch", req, url);
|
|
7811
8145
|
return dispatchRoutes(req, url, {
|
|
@@ -7825,6 +8159,11 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
|
|
|
7825
8159
|
methods: ["GET"],
|
|
7826
8160
|
handler: () => handleDoc(req, cwd, url, omitDirNames)
|
|
7827
8161
|
},
|
|
8162
|
+
"/_db/elasticsearch/write": {
|
|
8163
|
+
methods: ["POST"],
|
|
8164
|
+
sideEffect: true,
|
|
8165
|
+
handler: () => handleWrite(req, cwd, omitDirNames)
|
|
8166
|
+
},
|
|
7828
8167
|
"/_db/elasticsearch/search": {
|
|
7829
8168
|
methods: ["GET", "POST"],
|
|
7830
8169
|
sideEffect: (m) => m === "POST",
|
|
@@ -7843,6 +8182,7 @@ var init_handle_elasticsearch = __esm(() => {
|
|
|
7843
8182
|
var exports_redis = {};
|
|
7844
8183
|
__export(exports_redis, {
|
|
7845
8184
|
openRedisExplorerAsync: () => openRedisExplorerAsync,
|
|
8185
|
+
createRedisAdapter: () => createRedisAdapter,
|
|
7846
8186
|
canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
|
|
7847
8187
|
});
|
|
7848
8188
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -8364,6 +8704,41 @@ function createRedisAdapter(config) {
|
|
|
8364
8704
|
async function listSnapshotContainers() {
|
|
8365
8705
|
return [];
|
|
8366
8706
|
}
|
|
8707
|
+
async function runWriteAsync(args, signal) {
|
|
8708
|
+
const res = await execRedisCliAsync(config, args, 1e4, signal);
|
|
8709
|
+
if (res.code !== 0) {
|
|
8710
|
+
throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
|
|
8711
|
+
}
|
|
8712
|
+
const out = res.stdout.trim();
|
|
8713
|
+
if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
|
|
8714
|
+
throw new Error(out);
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
async function setStringAsync(opts) {
|
|
8718
|
+
await runWriteAsync(["-n", String(opts.db), "SET", opts.key, opts.value], opts.signal);
|
|
8719
|
+
}
|
|
8720
|
+
async function createStringAsync(opts) {
|
|
8721
|
+
const res = await execRedisCliAsync(config, ["-n", String(opts.db), "SET", opts.key, opts.value, "NX"], 1e4, opts.signal);
|
|
8722
|
+
if (res.code !== 0) {
|
|
8723
|
+
throw new Error(res.stderr.trim() || res.stdout.trim() || "redis command failed");
|
|
8724
|
+
}
|
|
8725
|
+
const out = res.stdout.trim();
|
|
8726
|
+
if (/^\(error\)/i.test(out) || /^ERR\b/i.test(out)) {
|
|
8727
|
+
throw new Error(out);
|
|
8728
|
+
}
|
|
8729
|
+
if (!/\bOK\b/i.test(out)) {
|
|
8730
|
+
throw new Error(`key already exists: ${opts.key}`);
|
|
8731
|
+
}
|
|
8732
|
+
}
|
|
8733
|
+
async function setHashFieldAsync(opts) {
|
|
8734
|
+
await runWriteAsync(["-n", String(opts.db), "HSET", opts.key, opts.field, opts.value], opts.signal);
|
|
8735
|
+
}
|
|
8736
|
+
async function setListIndexAsync(opts) {
|
|
8737
|
+
await runWriteAsync(["-n", String(opts.db), "LSET", opts.key, String(opts.index), opts.value], opts.signal);
|
|
8738
|
+
}
|
|
8739
|
+
async function deleteKeyAsync(opts) {
|
|
8740
|
+
await runWriteAsync(["-n", String(opts.db), "DEL", opts.key], opts.signal);
|
|
8741
|
+
}
|
|
8367
8742
|
return {
|
|
8368
8743
|
kind: "redis",
|
|
8369
8744
|
model: "kv",
|
|
@@ -8371,6 +8746,11 @@ function createRedisAdapter(config) {
|
|
|
8371
8746
|
listDatabasesAsync,
|
|
8372
8747
|
listKeysAsync,
|
|
8373
8748
|
getValueAsync,
|
|
8749
|
+
setStringAsync,
|
|
8750
|
+
createStringAsync,
|
|
8751
|
+
setHashFieldAsync,
|
|
8752
|
+
setListIndexAsync,
|
|
8753
|
+
deleteKeyAsync,
|
|
8374
8754
|
iterateForSnapshot,
|
|
8375
8755
|
listSnapshotContainers,
|
|
8376
8756
|
close() {}
|
|
@@ -8447,6 +8827,78 @@ async function handleKeys(req, cwd, url, omitDirNames) {
|
|
|
8447
8827
|
return handleError("redis", "list redis keys", err);
|
|
8448
8828
|
}
|
|
8449
8829
|
}
|
|
8830
|
+
async function handleWrite2(req, cwd, omitDirNames) {
|
|
8831
|
+
const parsed = await parseBoundedJsonBody(req, 1024 * 1024, "payload too large");
|
|
8832
|
+
if (parsed instanceof Response)
|
|
8833
|
+
return parsed;
|
|
8834
|
+
const body = parsed;
|
|
8835
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
8836
|
+
return textError("missing db", 400);
|
|
8837
|
+
}
|
|
8838
|
+
const dbIndex = Number(body.dbIndex);
|
|
8839
|
+
if (!Number.isInteger(dbIndex) || dbIndex < 0 || dbIndex > 15) {
|
|
8840
|
+
return textError("dbIndex must be an integer in 0..15", 400);
|
|
8841
|
+
}
|
|
8842
|
+
if (typeof body.key !== "string" || body.key === "") {
|
|
8843
|
+
return textError("missing key", 400);
|
|
8844
|
+
}
|
|
8845
|
+
const op = body.op;
|
|
8846
|
+
const value = typeof body.value === "string" ? body.value : "";
|
|
8847
|
+
const r = await resolveRedis(cwd, body.db, req.signal, omitDirNames);
|
|
8848
|
+
if (r instanceof Response)
|
|
8849
|
+
return r;
|
|
8850
|
+
try {
|
|
8851
|
+
if (op === "setString") {
|
|
8852
|
+
await r.explorer.setStringAsync({
|
|
8853
|
+
db: dbIndex,
|
|
8854
|
+
key: body.key,
|
|
8855
|
+
value,
|
|
8856
|
+
signal: req.signal
|
|
8857
|
+
});
|
|
8858
|
+
} else if (op === "createString") {
|
|
8859
|
+
await r.explorer.createStringAsync({
|
|
8860
|
+
db: dbIndex,
|
|
8861
|
+
key: body.key,
|
|
8862
|
+
value,
|
|
8863
|
+
signal: req.signal
|
|
8864
|
+
});
|
|
8865
|
+
} else if (op === "setHashField") {
|
|
8866
|
+
if (typeof body.field !== "string" || body.field === "") {
|
|
8867
|
+
return textError("missing field", 400);
|
|
8868
|
+
}
|
|
8869
|
+
await r.explorer.setHashFieldAsync({
|
|
8870
|
+
db: dbIndex,
|
|
8871
|
+
key: body.key,
|
|
8872
|
+
field: body.field,
|
|
8873
|
+
value,
|
|
8874
|
+
signal: req.signal
|
|
8875
|
+
});
|
|
8876
|
+
} else if (op === "setListIndex") {
|
|
8877
|
+
const index = Number(body.index);
|
|
8878
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
8879
|
+
return textError("index must be a non-negative integer", 400);
|
|
8880
|
+
}
|
|
8881
|
+
await r.explorer.setListIndexAsync({
|
|
8882
|
+
db: dbIndex,
|
|
8883
|
+
key: body.key,
|
|
8884
|
+
index,
|
|
8885
|
+
value,
|
|
8886
|
+
signal: req.signal
|
|
8887
|
+
});
|
|
8888
|
+
} else if (op === "delete") {
|
|
8889
|
+
await r.explorer.deleteKeyAsync({
|
|
8890
|
+
db: dbIndex,
|
|
8891
|
+
key: body.key,
|
|
8892
|
+
signal: req.signal
|
|
8893
|
+
});
|
|
8894
|
+
} else {
|
|
8895
|
+
return textError(`unknown op: ${String(op)}`, 400);
|
|
8896
|
+
}
|
|
8897
|
+
return json({ ok: true });
|
|
8898
|
+
} catch (err) {
|
|
8899
|
+
return handleError("redis", "write redis value", err);
|
|
8900
|
+
}
|
|
8901
|
+
}
|
|
8450
8902
|
async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
8451
8903
|
const wrap = createQueryStrippedLogger("redis", req, url);
|
|
8452
8904
|
return dispatchRoutes(req, url, {
|
|
@@ -8461,6 +8913,11 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
|
|
|
8461
8913
|
"/_db/redis/value": {
|
|
8462
8914
|
methods: ["GET"],
|
|
8463
8915
|
handler: () => handleValue(req, cwd, url, omitDirNames)
|
|
8916
|
+
},
|
|
8917
|
+
"/_db/redis/write": {
|
|
8918
|
+
methods: ["POST"],
|
|
8919
|
+
sideEffect: true,
|
|
8920
|
+
handler: () => handleWrite2(req, cwd, omitDirNames)
|
|
8464
8921
|
}
|
|
8465
8922
|
}, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
|
|
8466
8923
|
}
|
|
@@ -8538,6 +8995,9 @@ function hmac(key, value) {
|
|
|
8538
8995
|
function sha256(value) {
|
|
8539
8996
|
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
8540
8997
|
}
|
|
8998
|
+
function sha256Bytes(value) {
|
|
8999
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
9000
|
+
}
|
|
8541
9001
|
function encodeRfc3986(value) {
|
|
8542
9002
|
return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
8543
9003
|
}
|
|
@@ -8907,9 +9367,10 @@ function createS3Adapter(config) {
|
|
|
8907
9367
|
const path = buildPath(opts.bucket, opts.key);
|
|
8908
9368
|
const query = canonicalQuery(opts.query);
|
|
8909
9369
|
const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
9370
|
+
const payloadHash = opts.body ? sha256Bytes(opts.body) : EMPTY_SHA256;
|
|
8910
9371
|
const headers = {
|
|
8911
9372
|
host: endpoint.host,
|
|
8912
|
-
"x-amz-content-sha256":
|
|
9373
|
+
"x-amz-content-sha256": payloadHash,
|
|
8913
9374
|
"x-amz-date": requestDate,
|
|
8914
9375
|
...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
|
|
8915
9376
|
...opts.headers || {}
|
|
@@ -8921,7 +9382,7 @@ function createS3Adapter(config) {
|
|
|
8921
9382
|
query,
|
|
8922
9383
|
canonicalHeaders(headers),
|
|
8923
9384
|
signedNames,
|
|
8924
|
-
|
|
9385
|
+
payloadHash
|
|
8925
9386
|
].join(`
|
|
8926
9387
|
`);
|
|
8927
9388
|
const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
|
|
@@ -8940,6 +9401,9 @@ function createS3Adapter(config) {
|
|
|
8940
9401
|
}
|
|
8941
9402
|
requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
|
|
8942
9403
|
if (config.dockerContainerName) {
|
|
9404
|
+
if (opts.body !== undefined) {
|
|
9405
|
+
throw new S3HttpError(503, "S3 object writes require a published host port (docker-exec transport cannot stream a request body)");
|
|
9406
|
+
}
|
|
8943
9407
|
if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
|
|
8944
9408
|
throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
|
|
8945
9409
|
}
|
|
@@ -8954,6 +9418,7 @@ function createS3Adapter(config) {
|
|
|
8954
9418
|
return fetch(url, {
|
|
8955
9419
|
method: opts.method,
|
|
8956
9420
|
headers: requestHeaders,
|
|
9421
|
+
...opts.body !== undefined ? { body: opts.body } : {},
|
|
8957
9422
|
signal: transportSignal
|
|
8958
9423
|
});
|
|
8959
9424
|
}, deadline);
|
|
@@ -9036,6 +9501,32 @@ function createS3Adapter(config) {
|
|
|
9036
9501
|
headers: rawObjectHeaders(opts.key, res)
|
|
9037
9502
|
});
|
|
9038
9503
|
}
|
|
9504
|
+
async function putObjectAsync(opts) {
|
|
9505
|
+
const deadline = createS3TransportDeadline(config);
|
|
9506
|
+
const res = await signedFetch({
|
|
9507
|
+
method: "PUT",
|
|
9508
|
+
bucket: opts.bucket,
|
|
9509
|
+
key: opts.key,
|
|
9510
|
+
body: opts.body,
|
|
9511
|
+
headers: opts.contentType ? { "content-type": opts.contentType } : undefined,
|
|
9512
|
+
signal: opts.signal
|
|
9513
|
+
}, deadline);
|
|
9514
|
+
if (!res.ok) {
|
|
9515
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
9516
|
+
}
|
|
9517
|
+
}
|
|
9518
|
+
async function deleteObjectAsync(opts) {
|
|
9519
|
+
const deadline = createS3TransportDeadline(config);
|
|
9520
|
+
const res = await signedFetch({
|
|
9521
|
+
method: "DELETE",
|
|
9522
|
+
bucket: opts.bucket,
|
|
9523
|
+
key: opts.key,
|
|
9524
|
+
signal: opts.signal
|
|
9525
|
+
}, deadline);
|
|
9526
|
+
if (!res.ok) {
|
|
9527
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
9528
|
+
}
|
|
9529
|
+
}
|
|
9039
9530
|
return {
|
|
9040
9531
|
kind: "s3",
|
|
9041
9532
|
model: "object",
|
|
@@ -9044,7 +9535,9 @@ function createS3Adapter(config) {
|
|
|
9044
9535
|
listObjects,
|
|
9045
9536
|
headObject,
|
|
9046
9537
|
getObjectText,
|
|
9047
|
-
getObjectResponse
|
|
9538
|
+
getObjectResponse,
|
|
9539
|
+
putObjectAsync,
|
|
9540
|
+
deleteObjectAsync
|
|
9048
9541
|
};
|
|
9049
9542
|
}
|
|
9050
9543
|
async function s3ConfigFromDockerInfoAsync(info, signal) {
|
|
@@ -9447,6 +9940,60 @@ async function handleRaw(cwd, req, url, omitDirNames) {
|
|
|
9447
9940
|
return s3ErrorResponse(err, "stream s3 object");
|
|
9448
9941
|
}
|
|
9449
9942
|
}
|
|
9943
|
+
async function handleWrite3(req, cwd, omitDirNames) {
|
|
9944
|
+
const parsed = await parseBoundedJsonBody(req, 8 * 1024 * 1024, "payload too large");
|
|
9945
|
+
if (parsed instanceof Response)
|
|
9946
|
+
return parsed;
|
|
9947
|
+
const body = parsed;
|
|
9948
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
9949
|
+
return textError("missing db", 400);
|
|
9950
|
+
}
|
|
9951
|
+
const bucket = validateBucket(typeof body.bucket === "string" ? body.bucket : null);
|
|
9952
|
+
if (bucket instanceof Response)
|
|
9953
|
+
return bucket;
|
|
9954
|
+
const key = validateKey(typeof body.key === "string" ? body.key : null);
|
|
9955
|
+
if (key instanceof Response)
|
|
9956
|
+
return key;
|
|
9957
|
+
const r = await resolveS3(cwd, body.db, req.signal, omitDirNames);
|
|
9958
|
+
if (r instanceof Response)
|
|
9959
|
+
return r;
|
|
9960
|
+
try {
|
|
9961
|
+
if (body.op === "delete") {
|
|
9962
|
+
await r.explorer.deleteObjectAsync({
|
|
9963
|
+
bucket,
|
|
9964
|
+
key,
|
|
9965
|
+
signal: req.signal
|
|
9966
|
+
});
|
|
9967
|
+
return json({ ok: true });
|
|
9968
|
+
}
|
|
9969
|
+
if (body.op === "create") {
|
|
9970
|
+
let exists = false;
|
|
9971
|
+
try {
|
|
9972
|
+
await r.explorer.headObject({ bucket, key, signal: req.signal });
|
|
9973
|
+
exists = true;
|
|
9974
|
+
} catch (err) {
|
|
9975
|
+
if (isS3HttpError(err) && err.status === 404)
|
|
9976
|
+
exists = false;
|
|
9977
|
+
else
|
|
9978
|
+
throw err;
|
|
9979
|
+
}
|
|
9980
|
+
if (exists)
|
|
9981
|
+
return textError(`object already exists: ${key}`, 409);
|
|
9982
|
+
}
|
|
9983
|
+
const content = typeof body.content === "string" ? body.content : "";
|
|
9984
|
+
const contentType = typeof body.contentType === "string" && body.contentType ? body.contentType : "application/octet-stream";
|
|
9985
|
+
await r.explorer.putObjectAsync({
|
|
9986
|
+
bucket,
|
|
9987
|
+
key,
|
|
9988
|
+
body: new TextEncoder().encode(content),
|
|
9989
|
+
contentType,
|
|
9990
|
+
signal: req.signal
|
|
9991
|
+
});
|
|
9992
|
+
return json({ ok: true });
|
|
9993
|
+
} catch (err) {
|
|
9994
|
+
return handleError("s3", "write s3 object", err);
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9450
9997
|
async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
9451
9998
|
const wrap = createQueryStrippedLogger("s3", req, url);
|
|
9452
9999
|
return dispatchRoutes(req, url, {
|
|
@@ -9473,6 +10020,11 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
|
9473
10020
|
"/_db/s3/raw": {
|
|
9474
10021
|
methods: ["GET", "HEAD"],
|
|
9475
10022
|
handler: () => handleRaw(cwd, req, url, omitDirNames)
|
|
10023
|
+
},
|
|
10024
|
+
"/_db/s3/write": {
|
|
10025
|
+
methods: ["POST"],
|
|
10026
|
+
sideEffect: true,
|
|
10027
|
+
handler: () => handleWrite3(req, cwd, omitDirNames)
|
|
9476
10028
|
}
|
|
9477
10029
|
}, sideEffectAllowed, wrap, (err) => handleError("s3", "handle s3 request", err));
|
|
9478
10030
|
}
|
|
@@ -9639,21 +10191,6 @@ var init_query_history = __esm(() => {
|
|
|
9639
10191
|
import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
|
|
9640
10192
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9641
10193
|
import { join as join11 } from "node:path";
|
|
9642
|
-
async function getSqliteClass2() {
|
|
9643
|
-
if (cachedDbClass2)
|
|
9644
|
-
return cachedDbClass2;
|
|
9645
|
-
try {
|
|
9646
|
-
const mod = await import("bun:sqlite");
|
|
9647
|
-
cachedDbClass2 = mod.Database;
|
|
9648
|
-
return cachedDbClass2;
|
|
9649
|
-
} catch {}
|
|
9650
|
-
try {
|
|
9651
|
-
const mod = await Function('return import("better-sqlite3")')();
|
|
9652
|
-
cachedDbClass2 = mod.default || mod;
|
|
9653
|
-
return cachedDbClass2;
|
|
9654
|
-
} catch {}
|
|
9655
|
-
throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
|
|
9656
|
-
}
|
|
9657
10194
|
async function getStoreDb(cwd) {
|
|
9658
10195
|
const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
|
|
9659
10196
|
if (storeDb && storeDbPath === dbPath)
|
|
@@ -9664,7 +10201,7 @@ async function getStoreDb(cwd) {
|
|
|
9664
10201
|
} catch {}
|
|
9665
10202
|
}
|
|
9666
10203
|
mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
|
|
9667
|
-
const DbClass = await
|
|
10204
|
+
const DbClass = await loadSqliteClass();
|
|
9668
10205
|
storeDb = new DbClass(dbPath);
|
|
9669
10206
|
storeDbPath = dbPath;
|
|
9670
10207
|
storeDb.exec("PRAGMA journal_mode=WAL");
|
|
@@ -9923,7 +10460,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
9923
10460
|
});
|
|
9924
10461
|
return { rows, total };
|
|
9925
10462
|
}
|
|
9926
|
-
var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite",
|
|
10463
|
+
var CODE_VIEWER_DIR4 = ".code-viewer", SNAPSHOT_DB_NAME = "db-snapshots.sqlite", SCHEMA_SQL = `
|
|
9927
10464
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
9928
10465
|
id TEXT PRIMARY KEY,
|
|
9929
10466
|
db_id TEXT NOT NULL,
|
|
@@ -10153,6 +10690,9 @@ function sanitize(input) {
|
|
|
10153
10690
|
const historyHeight = sanitizeCssSize(tab.historyHeight);
|
|
10154
10691
|
if (historyHeight !== undefined)
|
|
10155
10692
|
out.historyHeight = historyHeight;
|
|
10693
|
+
if (tab.activeHistoryTab === "log" || tab.activeHistoryTab === "history") {
|
|
10694
|
+
out.activeHistoryTab = tab.activeHistoryTab;
|
|
10695
|
+
}
|
|
10156
10696
|
const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
|
|
10157
10697
|
if (sidebarWidth !== undefined)
|
|
10158
10698
|
out.sidebarWidth = sidebarWidth;
|
|
@@ -10343,11 +10883,13 @@ async function handleSchemas(cwd, url, omitDirNames, signal) {
|
|
|
10343
10883
|
const body2 = { dbId: r.dbId, schemas: [] };
|
|
10344
10884
|
return json(body2);
|
|
10345
10885
|
}
|
|
10346
|
-
const
|
|
10886
|
+
const docker = r.docker;
|
|
10887
|
+
const { result: schemas, executedSql } = await captureSql(() => listDockerSchemasAsync(docker.serviceName, "postgresql", docker.env, docker.composeDir, docker.database, signal));
|
|
10347
10888
|
const body = {
|
|
10348
10889
|
dbId: r.dbId,
|
|
10349
10890
|
schemas: schemas.map((name) => ({ name })),
|
|
10350
|
-
selectedSchema: r.schema
|
|
10891
|
+
selectedSchema: r.schema,
|
|
10892
|
+
executedSql
|
|
10351
10893
|
};
|
|
10352
10894
|
return json(body);
|
|
10353
10895
|
}
|
|
@@ -10359,37 +10901,41 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
|
|
|
10359
10901
|
const linkedAbort = createLinkedAbortController(signal);
|
|
10360
10902
|
try {
|
|
10361
10903
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10362
|
-
const
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10904
|
+
const { result, executedSql } = await captureSql(async () => {
|
|
10905
|
+
const db = asAsync(adapter);
|
|
10906
|
+
const tables = await db.tables(linkedAbort.signal);
|
|
10907
|
+
const tableNames = tables.filter((t) => t.type === "table").map((t) => t.name);
|
|
10908
|
+
const countMapPromise = db.tableRowCounts(tableNames, linkedAbort.signal);
|
|
10909
|
+
const indexesPromise = db.indexes(linkedAbort.signal);
|
|
10910
|
+
const foreignKeysPromise = db.foreignKeys(linkedAbort.signal);
|
|
10911
|
+
const columnsMapPromise = includeColumns ? db.columnsMulti(tableNames, linkedAbort.signal) : Promise.resolve(null);
|
|
10912
|
+
const schemaPromises = [
|
|
10913
|
+
countMapPromise,
|
|
10914
|
+
indexesPromise,
|
|
10915
|
+
foreignKeysPromise,
|
|
10916
|
+
columnsMapPromise
|
|
10917
|
+
];
|
|
10918
|
+
const [countMap, indexes, foreignKeys, colsMap] = await Promise.all(schemaPromises).catch(async (err) => {
|
|
10919
|
+
linkedAbort.abort();
|
|
10920
|
+
await Promise.allSettled(schemaPromises);
|
|
10921
|
+
throw err;
|
|
10922
|
+
});
|
|
10923
|
+
return { tables, countMap, indexes, foreignKeys, colsMap };
|
|
10379
10924
|
});
|
|
10380
|
-
const tablesWithCount = tables.map((t) => ({
|
|
10925
|
+
const tablesWithCount = result.tables.map((t) => ({
|
|
10381
10926
|
...t,
|
|
10382
|
-
rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
|
|
10927
|
+
rowCount: t.type === "table" ? result.countMap.get(t.name) ?? 0 : null
|
|
10383
10928
|
}));
|
|
10384
10929
|
const body = {
|
|
10385
10930
|
dbId: r.dbId,
|
|
10386
10931
|
...r.schema ? { schema: r.schema } : {},
|
|
10387
10932
|
tables: tablesWithCount,
|
|
10388
|
-
indexes,
|
|
10389
|
-
foreignKeys
|
|
10933
|
+
indexes: result.indexes,
|
|
10934
|
+
foreignKeys: result.foreignKeys,
|
|
10935
|
+
executedSql
|
|
10390
10936
|
};
|
|
10391
|
-
if (colsMap) {
|
|
10392
|
-
body.columnsMap = Object.fromEntries(colsMap);
|
|
10937
|
+
if (result.colsMap) {
|
|
10938
|
+
body.columnsMap = Object.fromEntries(result.colsMap);
|
|
10393
10939
|
}
|
|
10394
10940
|
return json(body);
|
|
10395
10941
|
} catch (err) {
|
|
@@ -10451,36 +10997,17 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
10451
10997
|
const exact = parseExactConditions(url);
|
|
10452
10998
|
try {
|
|
10453
10999
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
}, signal);
|
|
10462
|
-
const colNames2 = new Set(meta2.columns.map((c) => c.name));
|
|
10463
|
-
if (sortCol && !colNames2.has(sortCol)) {
|
|
10464
|
-
return textError(`invalid sort column: ${sortCol}`, 400);
|
|
10465
|
-
}
|
|
10466
|
-
const body2 = {
|
|
10467
|
-
dbId: r.dbId,
|
|
10468
|
-
...r.schema ? { schema: r.schema } : {},
|
|
10469
|
-
table,
|
|
10470
|
-
columns: meta2.columns,
|
|
10471
|
-
rows: serializeDbRows(meta2.rows),
|
|
10472
|
-
totalRows: meta2.totalRows,
|
|
10473
|
-
offset,
|
|
10474
|
-
limit,
|
|
10475
|
-
hasMore: offset + meta2.rowCount < meta2.totalRows
|
|
10476
|
-
};
|
|
10477
|
-
return json(body2);
|
|
10478
|
-
}
|
|
10479
|
-
const meta = await adapter.getTablePageWithMeta(table, {
|
|
11000
|
+
const { result: meta, executedSql } = await captureSql(() => filters.length > 0 || exact.length > 0 ? adapter.getFilteredTablePageWithMeta(table, {
|
|
11001
|
+
offset,
|
|
11002
|
+
limit,
|
|
11003
|
+
orderBy,
|
|
11004
|
+
grouped: groupFiltersByValue(filters),
|
|
11005
|
+
...exact.length > 0 ? { exact } : {}
|
|
11006
|
+
}, signal) : adapter.getTablePageWithMeta(table, {
|
|
10480
11007
|
offset,
|
|
10481
11008
|
limit,
|
|
10482
11009
|
orderBy
|
|
10483
|
-
}, signal);
|
|
11010
|
+
}, signal));
|
|
10484
11011
|
const colNames = new Set(meta.columns.map((c) => c.name));
|
|
10485
11012
|
if (sortCol && !colNames.has(sortCol)) {
|
|
10486
11013
|
return textError(`invalid sort column: ${sortCol}`, 400);
|
|
@@ -10494,7 +11021,8 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
10494
11021
|
totalRows: meta.totalRows,
|
|
10495
11022
|
offset,
|
|
10496
11023
|
limit,
|
|
10497
|
-
hasMore: offset + meta.rowCount < meta.totalRows
|
|
11024
|
+
hasMore: offset + meta.rowCount < meta.totalRows,
|
|
11025
|
+
executedSql
|
|
10498
11026
|
};
|
|
10499
11027
|
return json(body);
|
|
10500
11028
|
} catch (err) {
|
|
@@ -10571,8 +11099,7 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
10571
11099
|
const start = Date.now();
|
|
10572
11100
|
try {
|
|
10573
11101
|
const adapter = await getAdapter(r, cwd, req.signal);
|
|
10574
|
-
const
|
|
10575
|
-
const result = await db.readonlyQuery(body.sql, undefined, maxRows, req.signal);
|
|
11102
|
+
const { result, executedSql } = await captureSql(() => asAsync(adapter).readonlyQuery(body.sql ?? "", undefined, maxRows, req.signal));
|
|
10576
11103
|
const elapsed = Date.now() - start;
|
|
10577
11104
|
const serializedRows = serializeDbRows(result.rows);
|
|
10578
11105
|
const inferredColumns = result.columns.length === 0 && result.rows.length === 0 ? await inferEmptyQueryColumns(adapter, body.sql, r.schema, req.signal) : [];
|
|
@@ -10586,7 +11113,8 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
10586
11113
|
rows: serializedRows,
|
|
10587
11114
|
rowCount: result.rowCount,
|
|
10588
11115
|
truncated: result.rowCount >= maxRows,
|
|
10589
|
-
elapsedMs: elapsed
|
|
11116
|
+
elapsedMs: elapsed,
|
|
11117
|
+
executedSql
|
|
10590
11118
|
};
|
|
10591
11119
|
if (body.saveHistory) {
|
|
10592
11120
|
const entry = {
|
|
@@ -10805,13 +11333,13 @@ async function handleColumns(cwd, url, omitDirNames, signal) {
|
|
|
10805
11333
|
return textError("missing table parameter", 400);
|
|
10806
11334
|
try {
|
|
10807
11335
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10808
|
-
const
|
|
10809
|
-
const columns = await db.columns(table, signal);
|
|
11336
|
+
const { result: columns, executedSql } = await captureSql(() => asAsync(adapter).columns(table, signal));
|
|
10810
11337
|
return json({
|
|
10811
11338
|
dbId: r.dbId,
|
|
10812
11339
|
...r.schema ? { schema: r.schema } : {},
|
|
10813
11340
|
table,
|
|
10814
|
-
columns
|
|
11341
|
+
columns,
|
|
11342
|
+
executedSql
|
|
10815
11343
|
});
|
|
10816
11344
|
} catch (err) {
|
|
10817
11345
|
return handleError("database", "get columns", err);
|
|
@@ -10826,17 +11354,21 @@ async function handleDdl(cwd, url, omitDirNames, signal) {
|
|
|
10826
11354
|
return textError("missing table parameter", 400);
|
|
10827
11355
|
try {
|
|
10828
11356
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10829
|
-
const
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
11357
|
+
const { result, executedSql } = await captureSql(async () => {
|
|
11358
|
+
const db = asAsync(adapter);
|
|
11359
|
+
const [sql, triggers] = await Promise.all([
|
|
11360
|
+
db.createStatement(table, signal),
|
|
11361
|
+
db.triggers(table, signal)
|
|
11362
|
+
]);
|
|
11363
|
+
return { sql, triggers };
|
|
11364
|
+
});
|
|
10834
11365
|
return json({
|
|
10835
11366
|
dbId: r.dbId,
|
|
10836
11367
|
...r.schema ? { schema: r.schema } : {},
|
|
10837
11368
|
table,
|
|
10838
|
-
sql,
|
|
10839
|
-
triggers
|
|
11369
|
+
sql: result.sql,
|
|
11370
|
+
triggers: result.triggers,
|
|
11371
|
+
executedSql
|
|
10840
11372
|
});
|
|
10841
11373
|
} catch (err) {
|
|
10842
11374
|
return handleError("database", "get DDL", err);
|
|
@@ -11248,6 +11780,48 @@ async function handleClose(cwd, req, omitDirNames) {
|
|
|
11248
11780
|
}
|
|
11249
11781
|
return json({ ok: true });
|
|
11250
11782
|
}
|
|
11783
|
+
async function handleMutate(cwd, req, omitDirNames) {
|
|
11784
|
+
const parsed = await parseBoundedJsonBody(req, 1048576, "payload too large");
|
|
11785
|
+
if (parsed instanceof Response)
|
|
11786
|
+
return parsed;
|
|
11787
|
+
const body = parsed;
|
|
11788
|
+
if (typeof body.db !== "string" || body.db === "") {
|
|
11789
|
+
return textError("missing db", 400);
|
|
11790
|
+
}
|
|
11791
|
+
if (typeof body.table !== "string" || body.table === "") {
|
|
11792
|
+
return textError("missing table", 400);
|
|
11793
|
+
}
|
|
11794
|
+
if (!Array.isArray(body.mutations) || body.mutations.length === 0) {
|
|
11795
|
+
return textError("missing mutations", 400);
|
|
11796
|
+
}
|
|
11797
|
+
const schemaParam = typeof body.schema === "string" ? body.schema : undefined;
|
|
11798
|
+
const r = await resolveDb(cwd, body.db, omitDirNames, schemaParam, req.signal);
|
|
11799
|
+
if (r instanceof Response)
|
|
11800
|
+
return r;
|
|
11801
|
+
const adapter = await getAdapter(r, cwd, req.signal);
|
|
11802
|
+
if (!adapter.applyMutations) {
|
|
11803
|
+
return textError("writes are not supported for this datastore", 400);
|
|
11804
|
+
}
|
|
11805
|
+
try {
|
|
11806
|
+
const tableName = body.table;
|
|
11807
|
+
const { result, executedSql } = await captureSql(() => adapter.applyMutations?.(tableName, body.mutations, req.signal) ?? Promise.reject(new Error("applyMutations not supported")));
|
|
11808
|
+
adapter.invalidateTableMetaCache?.(tableName);
|
|
11809
|
+
const response = {
|
|
11810
|
+
dbId: r.dbId,
|
|
11811
|
+
...r.schema ? { schema: r.schema } : {},
|
|
11812
|
+
table: tableName,
|
|
11813
|
+
affected: result.affected,
|
|
11814
|
+
executedSql
|
|
11815
|
+
};
|
|
11816
|
+
return json(response);
|
|
11817
|
+
} catch (err) {
|
|
11818
|
+
if (isAbortLikeError(err, req.signal)) {
|
|
11819
|
+
return textError("mutation aborted", 503);
|
|
11820
|
+
}
|
|
11821
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
11822
|
+
return textError(message, 400);
|
|
11823
|
+
}
|
|
11824
|
+
}
|
|
11251
11825
|
async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
|
|
11252
11826
|
ensureInit();
|
|
11253
11827
|
if (url.pathname.startsWith("/_db/redis/")) {
|
|
@@ -11308,6 +11882,11 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
|
|
|
11308
11882
|
sideEffect: true,
|
|
11309
11883
|
handler: () => handleQuery(cwd, req, sendSse, omitDirNames)
|
|
11310
11884
|
},
|
|
11885
|
+
"/_db/mutate": {
|
|
11886
|
+
methods: ["POST"],
|
|
11887
|
+
sideEffect: true,
|
|
11888
|
+
handler: () => handleMutate(cwd, req, omitDirNames)
|
|
11889
|
+
},
|
|
11311
11890
|
"/_db/close": {
|
|
11312
11891
|
methods: ["POST"],
|
|
11313
11892
|
sideEffect: true,
|
|
@@ -11390,6 +11969,7 @@ var init_handle = __esm(() => {
|
|
|
11390
11969
|
init_state_store();
|
|
11391
11970
|
init_docker();
|
|
11392
11971
|
init_docker_utils();
|
|
11972
|
+
init_sql_capture();
|
|
11393
11973
|
init_sqlite();
|
|
11394
11974
|
init_connection_pool();
|
|
11395
11975
|
init_discovery();
|