@youtyan/code-viewer 0.3.0 → 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 +37 -17
- package/dist/code-viewer.js +732 -156
- 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
|
@@ -4768,52 +4768,37 @@ function serializeDbRow(row) {
|
|
|
4768
4768
|
function serializeDbRows(rows) {
|
|
4769
4769
|
return rows.map(serializeDbRow);
|
|
4770
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
|
+
}
|
|
4771
4796
|
var MIN_SAFE, MAX_SAFE;
|
|
4772
4797
|
var init_serialize = __esm(() => {
|
|
4773
4798
|
MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
|
|
4774
4799
|
MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
|
|
4775
4800
|
});
|
|
4776
4801
|
|
|
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
4802
|
// web-src/server/database/sql-utils.ts
|
|
4818
4803
|
function sanitizeIdentifier(name, kind = "sqlite") {
|
|
4819
4804
|
if (kind === "mysql")
|
|
@@ -4875,6 +4860,189 @@ function filterOrderByColumns(orderBy, columnNames) {
|
|
|
4875
4860
|
const filtered = orderBy.filter((order) => validColumns.has(order.column));
|
|
4876
4861
|
return filtered.length > 0 ? filtered : undefined;
|
|
4877
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
|
+
});
|
|
4878
5046
|
|
|
4879
5047
|
// web-src/server/database/adapters/spawn-runner.ts
|
|
4880
5048
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -5124,6 +5292,24 @@ var init_docker_utils = __esm(() => {
|
|
|
5124
5292
|
};
|
|
5125
5293
|
});
|
|
5126
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
|
+
|
|
5127
5313
|
// web-src/server/database/adapters/docker.ts
|
|
5128
5314
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
5129
5315
|
function dockerDatabasesCacheKey(serviceName, kind, cwd) {
|
|
@@ -5279,6 +5465,7 @@ function execWithNodeSpawn(args, timeoutMs, signal) {
|
|
|
5279
5465
|
});
|
|
5280
5466
|
}
|
|
5281
5467
|
async function execInContainerAsync(config, sql, timeoutMs = 1e4, signal) {
|
|
5468
|
+
recordSql(sql);
|
|
5282
5469
|
if (spawnSyncImpl2 !== spawnSync3)
|
|
5283
5470
|
return execInContainer(config, sql, timeoutMs);
|
|
5284
5471
|
const args = buildExecArgs(config, sql);
|
|
@@ -5366,12 +5553,6 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
|
|
|
5366
5553
|
const rows = lines.map((line) => splitTsvLine(line, false));
|
|
5367
5554
|
return { columns: [], rows };
|
|
5368
5555
|
}
|
|
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
5556
|
function isMysqlSpatialType(type) {
|
|
5376
5557
|
const baseType = type.trim().toLowerCase().split(/[\s(]/, 1)[0];
|
|
5377
5558
|
return MYSQL_SPATIAL_TYPES.has(baseType);
|
|
@@ -5460,7 +5641,7 @@ function createDockerAdapter(config) {
|
|
|
5460
5641
|
const tableLiteral = table.replace(/'/g, "''");
|
|
5461
5642
|
if (config.kind === "postgresql") {
|
|
5462
5643
|
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
|
|
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`;
|
|
5464
5645
|
}
|
|
5465
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`;
|
|
5466
5647
|
}
|
|
@@ -5794,6 +5975,19 @@ function createDockerAdapter(config) {
|
|
|
5794
5975
|
rowCount: Math.min(result.rows.length, maxRows)
|
|
5795
5976
|
};
|
|
5796
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
|
+
},
|
|
5797
5991
|
invalidateTableMetaCache(table) {
|
|
5798
5992
|
tableMetaCache.invalidate(table);
|
|
5799
5993
|
if (table) {
|
|
@@ -5975,9 +6169,12 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
|
|
|
5975
6169
|
}
|
|
5976
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;
|
|
5977
6171
|
var init_docker = __esm(() => {
|
|
6172
|
+
init_mutate();
|
|
5978
6173
|
init_sql_snapshot();
|
|
6174
|
+
init_sql_utils();
|
|
5979
6175
|
init_docker_utils();
|
|
5980
6176
|
init_spawn_runner();
|
|
6177
|
+
init_sql_capture();
|
|
5981
6178
|
dockerDatabasesCache = new Map;
|
|
5982
6179
|
dockerSchemasCache = new Map;
|
|
5983
6180
|
spawnSyncImpl2 = spawnSync3;
|
|
@@ -5994,17 +6191,8 @@ var init_docker = __esm(() => {
|
|
|
5994
6191
|
]);
|
|
5995
6192
|
});
|
|
5996
6193
|
|
|
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() {
|
|
6194
|
+
// web-src/server/database/sqlite-driver.ts
|
|
6195
|
+
async function loadSqliteClass() {
|
|
6008
6196
|
if (cachedDbClass)
|
|
6009
6197
|
return cachedDbClass;
|
|
6010
6198
|
try {
|
|
@@ -6019,11 +6207,17 @@ async function getSqliteClass() {
|
|
|
6019
6207
|
} catch {}
|
|
6020
6208
|
throw new Error("No SQLite driver available. Install better-sqlite3 or use the bun runtime.");
|
|
6021
6209
|
}
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
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;
|
|
6027
6221
|
}
|
|
6028
6222
|
function queryRowsToResult(rows, columns) {
|
|
6029
6223
|
const columnNames = rows.length > 0 ? Object.keys(rows[0]) : columns.map((c) => c.name);
|
|
@@ -6045,7 +6239,30 @@ function queryColumns(db, table) {
|
|
|
6045
6239
|
defaultValue: row.dflt_value
|
|
6046
6240
|
}));
|
|
6047
6241
|
}
|
|
6048
|
-
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
|
+
};
|
|
6049
6266
|
const adapter = {
|
|
6050
6267
|
kind: "sqlite",
|
|
6051
6268
|
model: "sql",
|
|
@@ -6145,7 +6362,7 @@ function createSqliteAdapter(db) {
|
|
|
6145
6362
|
return this.getTableRowCounts(tables);
|
|
6146
6363
|
},
|
|
6147
6364
|
getTablePage(table, options) {
|
|
6148
|
-
const order =
|
|
6365
|
+
const order = buildOrderClause(options.orderBy);
|
|
6149
6366
|
const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
|
|
6150
6367
|
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
6151
6368
|
const cols = queryColumns(db, table);
|
|
@@ -6157,7 +6374,7 @@ function createSqliteAdapter(db) {
|
|
|
6157
6374
|
async getTablePageWithMeta(table, options) {
|
|
6158
6375
|
const columns = queryColumns(db, table);
|
|
6159
6376
|
const orderBy = filterOrderByColumns(options.orderBy, columns.map((column) => column.name));
|
|
6160
|
-
const order =
|
|
6377
|
+
const order = buildOrderClause(orderBy);
|
|
6161
6378
|
const sql = `SELECT * FROM ${sanitizeIdentifier(table)}${order} LIMIT ? OFFSET ?`;
|
|
6162
6379
|
const rows = safePrepare(db, sql).all(options.limit, options.offset);
|
|
6163
6380
|
const result = queryRowsToResult(rows, columns);
|
|
@@ -6173,7 +6390,7 @@ function createSqliteAdapter(db) {
|
|
|
6173
6390
|
const columns = queryColumns(db, table);
|
|
6174
6391
|
const columnNames = columns.map((column) => column.name);
|
|
6175
6392
|
const filter = buildFilterWhere(filterGroupedColumns(options.grouped, columnNames), "sqlite", filterExactColumns(options.exact, columnNames));
|
|
6176
|
-
const order =
|
|
6393
|
+
const order = buildOrderClause(filterOrderByColumns(options.orderBy, columnNames));
|
|
6177
6394
|
const tableId = sanitizeIdentifier(table);
|
|
6178
6395
|
const whereClause = filter.where ? ` WHERE ${filter.where}` : "";
|
|
6179
6396
|
const countRow = safePrepare(db, `SELECT COUNT(*) AS cnt FROM ${tableId}${whereClause}`).get(...filter.params);
|
|
@@ -6241,7 +6458,36 @@ function createSqliteAdapter(db) {
|
|
|
6241
6458
|
async getTriggersAsync(table) {
|
|
6242
6459
|
return this.getTriggers(table);
|
|
6243
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
|
+
},
|
|
6244
6484
|
close() {
|
|
6485
|
+
if (writeDb) {
|
|
6486
|
+
try {
|
|
6487
|
+
writeDb.close();
|
|
6488
|
+
} catch {}
|
|
6489
|
+
writeDb = null;
|
|
6490
|
+
}
|
|
6245
6491
|
db.close();
|
|
6246
6492
|
},
|
|
6247
6493
|
async* iterateForSnapshot(table, signal) {
|
|
@@ -6279,14 +6525,18 @@ function createSqliteAdapter(db) {
|
|
|
6279
6525
|
};
|
|
6280
6526
|
return adapter;
|
|
6281
6527
|
}
|
|
6282
|
-
var
|
|
6528
|
+
var sqliteAdapterFactory;
|
|
6283
6529
|
var init_sqlite = __esm(() => {
|
|
6530
|
+
init_mutate();
|
|
6284
6531
|
init_sql_snapshot();
|
|
6532
|
+
init_sql_utils();
|
|
6533
|
+
init_sql_capture();
|
|
6285
6534
|
sqliteAdapterFactory = {
|
|
6286
6535
|
async open(path) {
|
|
6287
|
-
const DbClass = await
|
|
6536
|
+
const DbClass = await loadSqliteClass();
|
|
6288
6537
|
const db = new DbClass(path, { readonly: true, create: false });
|
|
6289
|
-
|
|
6538
|
+
const openWriteDb = () => new DbClass(path);
|
|
6539
|
+
return createSqliteAdapter(db, openWriteDb);
|
|
6290
6540
|
}
|
|
6291
6541
|
};
|
|
6292
6542
|
});
|
|
@@ -7055,6 +7305,7 @@ function getPrimaryKeyColumnsFromColumns(columns) {
|
|
|
7055
7305
|
}
|
|
7056
7306
|
var init_global_search = __esm(() => {
|
|
7057
7307
|
init_serialize();
|
|
7308
|
+
init_sql_utils();
|
|
7058
7309
|
});
|
|
7059
7310
|
|
|
7060
7311
|
// web-src/server/database/adapters/elasticsearch.ts
|
|
@@ -7254,6 +7505,38 @@ function createElasticsearchAdapter(config) {
|
|
|
7254
7505
|
primaryTerm: parsed._primary_term
|
|
7255
7506
|
};
|
|
7256
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
|
+
}
|
|
7257
7540
|
async function* iterateForSnapshot(container, signal) {
|
|
7258
7541
|
const { index, query: query2 } = parseEsSnapshotContainer(container);
|
|
7259
7542
|
const PAGE = 1000;
|
|
@@ -7327,6 +7610,8 @@ function createElasticsearchAdapter(config) {
|
|
|
7327
7610
|
getMappingAsync,
|
|
7328
7611
|
searchDocsAsync,
|
|
7329
7612
|
getDocAsync,
|
|
7613
|
+
writeDocAsync,
|
|
7614
|
+
deleteDocAsync,
|
|
7330
7615
|
iterateForSnapshot,
|
|
7331
7616
|
listSnapshotContainers,
|
|
7332
7617
|
query,
|
|
@@ -7806,6 +8091,51 @@ async function handleMapping(req, cwd, url, omitDirNames) {
|
|
|
7806
8091
|
return handleError("elasticsearch", "read elasticsearch mapping", err);
|
|
7807
8092
|
}
|
|
7808
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
|
+
}
|
|
7809
8139
|
async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
7810
8140
|
const wrap = createQueryStrippedLogger("elasticsearch", req, url);
|
|
7811
8141
|
return dispatchRoutes(req, url, {
|
|
@@ -7825,6 +8155,11 @@ async function handleElasticsearchRoute(req, url, cwd, sideEffectAllowed, omitDi
|
|
|
7825
8155
|
methods: ["GET"],
|
|
7826
8156
|
handler: () => handleDoc(req, cwd, url, omitDirNames)
|
|
7827
8157
|
},
|
|
8158
|
+
"/_db/elasticsearch/write": {
|
|
8159
|
+
methods: ["POST"],
|
|
8160
|
+
sideEffect: true,
|
|
8161
|
+
handler: () => handleWrite(req, cwd, omitDirNames)
|
|
8162
|
+
},
|
|
7828
8163
|
"/_db/elasticsearch/search": {
|
|
7829
8164
|
methods: ["GET", "POST"],
|
|
7830
8165
|
sideEffect: (m) => m === "POST",
|
|
@@ -7843,6 +8178,7 @@ var init_handle_elasticsearch = __esm(() => {
|
|
|
7843
8178
|
var exports_redis = {};
|
|
7844
8179
|
__export(exports_redis, {
|
|
7845
8180
|
openRedisExplorerAsync: () => openRedisExplorerAsync,
|
|
8181
|
+
createRedisAdapter: () => createRedisAdapter,
|
|
7846
8182
|
canonicalizeRedisSnapshotContainer: () => canonicalizeRedisSnapshotContainer
|
|
7847
8183
|
});
|
|
7848
8184
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -8364,6 +8700,41 @@ function createRedisAdapter(config) {
|
|
|
8364
8700
|
async function listSnapshotContainers() {
|
|
8365
8701
|
return [];
|
|
8366
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
|
+
}
|
|
8367
8738
|
return {
|
|
8368
8739
|
kind: "redis",
|
|
8369
8740
|
model: "kv",
|
|
@@ -8371,6 +8742,11 @@ function createRedisAdapter(config) {
|
|
|
8371
8742
|
listDatabasesAsync,
|
|
8372
8743
|
listKeysAsync,
|
|
8373
8744
|
getValueAsync,
|
|
8745
|
+
setStringAsync,
|
|
8746
|
+
createStringAsync,
|
|
8747
|
+
setHashFieldAsync,
|
|
8748
|
+
setListIndexAsync,
|
|
8749
|
+
deleteKeyAsync,
|
|
8374
8750
|
iterateForSnapshot,
|
|
8375
8751
|
listSnapshotContainers,
|
|
8376
8752
|
close() {}
|
|
@@ -8447,6 +8823,78 @@ async function handleKeys(req, cwd, url, omitDirNames) {
|
|
|
8447
8823
|
return handleError("redis", "list redis keys", err);
|
|
8448
8824
|
}
|
|
8449
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
|
+
}
|
|
8450
8898
|
async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
8451
8899
|
const wrap = createQueryStrippedLogger("redis", req, url);
|
|
8452
8900
|
return dispatchRoutes(req, url, {
|
|
@@ -8461,6 +8909,11 @@ async function handleRedisRoute(req, url, cwd, sideEffectAllowed, omitDirNames)
|
|
|
8461
8909
|
"/_db/redis/value": {
|
|
8462
8910
|
methods: ["GET"],
|
|
8463
8911
|
handler: () => handleValue(req, cwd, url, omitDirNames)
|
|
8912
|
+
},
|
|
8913
|
+
"/_db/redis/write": {
|
|
8914
|
+
methods: ["POST"],
|
|
8915
|
+
sideEffect: true,
|
|
8916
|
+
handler: () => handleWrite2(req, cwd, omitDirNames)
|
|
8464
8917
|
}
|
|
8465
8918
|
}, sideEffectAllowed, wrap, (err) => handleError("redis", "handle redis request", err));
|
|
8466
8919
|
}
|
|
@@ -8538,6 +8991,9 @@ function hmac(key, value) {
|
|
|
8538
8991
|
function sha256(value) {
|
|
8539
8992
|
return createHash4("sha256").update(value, "utf8").digest("hex");
|
|
8540
8993
|
}
|
|
8994
|
+
function sha256Bytes(value) {
|
|
8995
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
8996
|
+
}
|
|
8541
8997
|
function encodeRfc3986(value) {
|
|
8542
8998
|
return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
8543
8999
|
}
|
|
@@ -8907,9 +9363,10 @@ function createS3Adapter(config) {
|
|
|
8907
9363
|
const path = buildPath(opts.bucket, opts.key);
|
|
8908
9364
|
const query = canonicalQuery(opts.query);
|
|
8909
9365
|
const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
9366
|
+
const payloadHash = opts.body ? sha256Bytes(opts.body) : EMPTY_SHA256;
|
|
8910
9367
|
const headers = {
|
|
8911
9368
|
host: endpoint.host,
|
|
8912
|
-
"x-amz-content-sha256":
|
|
9369
|
+
"x-amz-content-sha256": payloadHash,
|
|
8913
9370
|
"x-amz-date": requestDate,
|
|
8914
9371
|
...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
|
|
8915
9372
|
...opts.headers || {}
|
|
@@ -8921,7 +9378,7 @@ function createS3Adapter(config) {
|
|
|
8921
9378
|
query,
|
|
8922
9379
|
canonicalHeaders(headers),
|
|
8923
9380
|
signedNames,
|
|
8924
|
-
|
|
9381
|
+
payloadHash
|
|
8925
9382
|
].join(`
|
|
8926
9383
|
`);
|
|
8927
9384
|
const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
|
|
@@ -8940,6 +9397,9 @@ function createS3Adapter(config) {
|
|
|
8940
9397
|
}
|
|
8941
9398
|
requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
|
|
8942
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
|
+
}
|
|
8943
9403
|
if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
|
|
8944
9404
|
throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
|
|
8945
9405
|
}
|
|
@@ -8954,6 +9414,7 @@ function createS3Adapter(config) {
|
|
|
8954
9414
|
return fetch(url, {
|
|
8955
9415
|
method: opts.method,
|
|
8956
9416
|
headers: requestHeaders,
|
|
9417
|
+
...opts.body !== undefined ? { body: opts.body } : {},
|
|
8957
9418
|
signal: transportSignal
|
|
8958
9419
|
});
|
|
8959
9420
|
}, deadline);
|
|
@@ -9036,6 +9497,32 @@ function createS3Adapter(config) {
|
|
|
9036
9497
|
headers: rawObjectHeaders(opts.key, res)
|
|
9037
9498
|
});
|
|
9038
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
|
+
}
|
|
9039
9526
|
return {
|
|
9040
9527
|
kind: "s3",
|
|
9041
9528
|
model: "object",
|
|
@@ -9044,7 +9531,9 @@ function createS3Adapter(config) {
|
|
|
9044
9531
|
listObjects,
|
|
9045
9532
|
headObject,
|
|
9046
9533
|
getObjectText,
|
|
9047
|
-
getObjectResponse
|
|
9534
|
+
getObjectResponse,
|
|
9535
|
+
putObjectAsync,
|
|
9536
|
+
deleteObjectAsync
|
|
9048
9537
|
};
|
|
9049
9538
|
}
|
|
9050
9539
|
async function s3ConfigFromDockerInfoAsync(info, signal) {
|
|
@@ -9447,6 +9936,60 @@ async function handleRaw(cwd, req, url, omitDirNames) {
|
|
|
9447
9936
|
return s3ErrorResponse(err, "stream s3 object");
|
|
9448
9937
|
}
|
|
9449
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
|
+
}
|
|
9450
9993
|
async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
9451
9994
|
const wrap = createQueryStrippedLogger("s3", req, url);
|
|
9452
9995
|
return dispatchRoutes(req, url, {
|
|
@@ -9473,6 +10016,11 @@ async function handleS3Route(req, url, cwd, sideEffectAllowed, omitDirNames) {
|
|
|
9473
10016
|
"/_db/s3/raw": {
|
|
9474
10017
|
methods: ["GET", "HEAD"],
|
|
9475
10018
|
handler: () => handleRaw(cwd, req, url, omitDirNames)
|
|
10019
|
+
},
|
|
10020
|
+
"/_db/s3/write": {
|
|
10021
|
+
methods: ["POST"],
|
|
10022
|
+
sideEffect: true,
|
|
10023
|
+
handler: () => handleWrite3(req, cwd, omitDirNames)
|
|
9476
10024
|
}
|
|
9477
10025
|
}, sideEffectAllowed, wrap, (err) => handleError("s3", "handle s3 request", err));
|
|
9478
10026
|
}
|
|
@@ -9639,21 +10187,6 @@ var init_query_history = __esm(() => {
|
|
|
9639
10187
|
import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
|
|
9640
10188
|
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
9641
10189
|
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
10190
|
async function getStoreDb(cwd) {
|
|
9658
10191
|
const dbPath = join11(cwd, CODE_VIEWER_DIR4, SNAPSHOT_DB_NAME);
|
|
9659
10192
|
if (storeDb && storeDbPath === dbPath)
|
|
@@ -9664,7 +10197,7 @@ async function getStoreDb(cwd) {
|
|
|
9664
10197
|
} catch {}
|
|
9665
10198
|
}
|
|
9666
10199
|
mkdirSync3(join11(cwd, CODE_VIEWER_DIR4), { recursive: true });
|
|
9667
|
-
const DbClass = await
|
|
10200
|
+
const DbClass = await loadSqliteClass();
|
|
9668
10201
|
storeDb = new DbClass(dbPath);
|
|
9669
10202
|
storeDbPath = dbPath;
|
|
9670
10203
|
storeDb.exec("PRAGMA journal_mode=WAL");
|
|
@@ -9923,7 +10456,7 @@ async function computeDiffRows(cwd, beforeId, afterId, table, offset = 0, limit
|
|
|
9923
10456
|
});
|
|
9924
10457
|
return { rows, total };
|
|
9925
10458
|
}
|
|
9926
|
-
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 = `
|
|
9927
10460
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
9928
10461
|
id TEXT PRIMARY KEY,
|
|
9929
10462
|
db_id TEXT NOT NULL,
|
|
@@ -10153,6 +10686,9 @@ function sanitize(input) {
|
|
|
10153
10686
|
const historyHeight = sanitizeCssSize(tab.historyHeight);
|
|
10154
10687
|
if (historyHeight !== undefined)
|
|
10155
10688
|
out.historyHeight = historyHeight;
|
|
10689
|
+
if (tab.activeHistoryTab === "log" || tab.activeHistoryTab === "history") {
|
|
10690
|
+
out.activeHistoryTab = tab.activeHistoryTab;
|
|
10691
|
+
}
|
|
10156
10692
|
const sidebarWidth = sanitizeCssSize(tab.sidebarWidth);
|
|
10157
10693
|
if (sidebarWidth !== undefined)
|
|
10158
10694
|
out.sidebarWidth = sidebarWidth;
|
|
@@ -10343,11 +10879,13 @@ async function handleSchemas(cwd, url, omitDirNames, signal) {
|
|
|
10343
10879
|
const body2 = { dbId: r.dbId, schemas: [] };
|
|
10344
10880
|
return json(body2);
|
|
10345
10881
|
}
|
|
10346
|
-
const
|
|
10882
|
+
const docker = r.docker;
|
|
10883
|
+
const { result: schemas, executedSql } = await captureSql(() => listDockerSchemasAsync(docker.serviceName, "postgresql", docker.env, docker.composeDir, docker.database, signal));
|
|
10347
10884
|
const body = {
|
|
10348
10885
|
dbId: r.dbId,
|
|
10349
10886
|
schemas: schemas.map((name) => ({ name })),
|
|
10350
|
-
selectedSchema: r.schema
|
|
10887
|
+
selectedSchema: r.schema,
|
|
10888
|
+
executedSql
|
|
10351
10889
|
};
|
|
10352
10890
|
return json(body);
|
|
10353
10891
|
}
|
|
@@ -10359,37 +10897,41 @@ async function handleSchema(cwd, url, omitDirNames, signal) {
|
|
|
10359
10897
|
const linkedAbort = createLinkedAbortController(signal);
|
|
10360
10898
|
try {
|
|
10361
10899
|
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
|
-
|
|
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 };
|
|
10379
10920
|
});
|
|
10380
|
-
const tablesWithCount = tables.map((t) => ({
|
|
10921
|
+
const tablesWithCount = result.tables.map((t) => ({
|
|
10381
10922
|
...t,
|
|
10382
|
-
rowCount: t.type === "table" ? countMap.get(t.name) ?? 0 : null
|
|
10923
|
+
rowCount: t.type === "table" ? result.countMap.get(t.name) ?? 0 : null
|
|
10383
10924
|
}));
|
|
10384
10925
|
const body = {
|
|
10385
10926
|
dbId: r.dbId,
|
|
10386
10927
|
...r.schema ? { schema: r.schema } : {},
|
|
10387
10928
|
tables: tablesWithCount,
|
|
10388
|
-
indexes,
|
|
10389
|
-
foreignKeys
|
|
10929
|
+
indexes: result.indexes,
|
|
10930
|
+
foreignKeys: result.foreignKeys,
|
|
10931
|
+
executedSql
|
|
10390
10932
|
};
|
|
10391
|
-
if (colsMap) {
|
|
10392
|
-
body.columnsMap = Object.fromEntries(colsMap);
|
|
10933
|
+
if (result.colsMap) {
|
|
10934
|
+
body.columnsMap = Object.fromEntries(result.colsMap);
|
|
10393
10935
|
}
|
|
10394
10936
|
return json(body);
|
|
10395
10937
|
} catch (err) {
|
|
@@ -10451,36 +10993,17 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
10451
10993
|
const exact = parseExactConditions(url);
|
|
10452
10994
|
try {
|
|
10453
10995
|
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, {
|
|
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, {
|
|
10480
11003
|
offset,
|
|
10481
11004
|
limit,
|
|
10482
11005
|
orderBy
|
|
10483
|
-
}, signal);
|
|
11006
|
+
}, signal));
|
|
10484
11007
|
const colNames = new Set(meta.columns.map((c) => c.name));
|
|
10485
11008
|
if (sortCol && !colNames.has(sortCol)) {
|
|
10486
11009
|
return textError(`invalid sort column: ${sortCol}`, 400);
|
|
@@ -10494,7 +11017,8 @@ async function handleTable(cwd, url, omitDirNames, signal) {
|
|
|
10494
11017
|
totalRows: meta.totalRows,
|
|
10495
11018
|
offset,
|
|
10496
11019
|
limit,
|
|
10497
|
-
hasMore: offset + meta.rowCount < meta.totalRows
|
|
11020
|
+
hasMore: offset + meta.rowCount < meta.totalRows,
|
|
11021
|
+
executedSql
|
|
10498
11022
|
};
|
|
10499
11023
|
return json(body);
|
|
10500
11024
|
} catch (err) {
|
|
@@ -10571,8 +11095,7 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
10571
11095
|
const start = Date.now();
|
|
10572
11096
|
try {
|
|
10573
11097
|
const adapter = await getAdapter(r, cwd, req.signal);
|
|
10574
|
-
const
|
|
10575
|
-
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));
|
|
10576
11099
|
const elapsed = Date.now() - start;
|
|
10577
11100
|
const serializedRows = serializeDbRows(result.rows);
|
|
10578
11101
|
const inferredColumns = result.columns.length === 0 && result.rows.length === 0 ? await inferEmptyQueryColumns(adapter, body.sql, r.schema, req.signal) : [];
|
|
@@ -10586,7 +11109,8 @@ async function handleQuery(cwd, req, sendSse, omitDirNames) {
|
|
|
10586
11109
|
rows: serializedRows,
|
|
10587
11110
|
rowCount: result.rowCount,
|
|
10588
11111
|
truncated: result.rowCount >= maxRows,
|
|
10589
|
-
elapsedMs: elapsed
|
|
11112
|
+
elapsedMs: elapsed,
|
|
11113
|
+
executedSql
|
|
10590
11114
|
};
|
|
10591
11115
|
if (body.saveHistory) {
|
|
10592
11116
|
const entry = {
|
|
@@ -10805,13 +11329,13 @@ async function handleColumns(cwd, url, omitDirNames, signal) {
|
|
|
10805
11329
|
return textError("missing table parameter", 400);
|
|
10806
11330
|
try {
|
|
10807
11331
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10808
|
-
const
|
|
10809
|
-
const columns = await db.columns(table, signal);
|
|
11332
|
+
const { result: columns, executedSql } = await captureSql(() => asAsync(adapter).columns(table, signal));
|
|
10810
11333
|
return json({
|
|
10811
11334
|
dbId: r.dbId,
|
|
10812
11335
|
...r.schema ? { schema: r.schema } : {},
|
|
10813
11336
|
table,
|
|
10814
|
-
columns
|
|
11337
|
+
columns,
|
|
11338
|
+
executedSql
|
|
10815
11339
|
});
|
|
10816
11340
|
} catch (err) {
|
|
10817
11341
|
return handleError("database", "get columns", err);
|
|
@@ -10826,17 +11350,21 @@ async function handleDdl(cwd, url, omitDirNames, signal) {
|
|
|
10826
11350
|
return textError("missing table parameter", 400);
|
|
10827
11351
|
try {
|
|
10828
11352
|
const adapter = await getAdapter(r, cwd, signal);
|
|
10829
|
-
const
|
|
10830
|
-
|
|
10831
|
-
|
|
10832
|
-
|
|
10833
|
-
|
|
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
|
+
});
|
|
10834
11361
|
return json({
|
|
10835
11362
|
dbId: r.dbId,
|
|
10836
11363
|
...r.schema ? { schema: r.schema } : {},
|
|
10837
11364
|
table,
|
|
10838
|
-
sql,
|
|
10839
|
-
triggers
|
|
11365
|
+
sql: result.sql,
|
|
11366
|
+
triggers: result.triggers,
|
|
11367
|
+
executedSql
|
|
10840
11368
|
});
|
|
10841
11369
|
} catch (err) {
|
|
10842
11370
|
return handleError("database", "get DDL", err);
|
|
@@ -11248,6 +11776,48 @@ async function handleClose(cwd, req, omitDirNames) {
|
|
|
11248
11776
|
}
|
|
11249
11777
|
return json({ ok: true });
|
|
11250
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
|
+
}
|
|
11251
11821
|
async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowed, sendSse) {
|
|
11252
11822
|
ensureInit();
|
|
11253
11823
|
if (url.pathname.startsWith("/_db/redis/")) {
|
|
@@ -11308,6 +11878,11 @@ async function handleDatabaseRoute(req, url, cwd, omitDirNames, sideEffectAllowe
|
|
|
11308
11878
|
sideEffect: true,
|
|
11309
11879
|
handler: () => handleQuery(cwd, req, sendSse, omitDirNames)
|
|
11310
11880
|
},
|
|
11881
|
+
"/_db/mutate": {
|
|
11882
|
+
methods: ["POST"],
|
|
11883
|
+
sideEffect: true,
|
|
11884
|
+
handler: () => handleMutate(cwd, req, omitDirNames)
|
|
11885
|
+
},
|
|
11311
11886
|
"/_db/close": {
|
|
11312
11887
|
methods: ["POST"],
|
|
11313
11888
|
sideEffect: true,
|
|
@@ -11390,6 +11965,7 @@ var init_handle = __esm(() => {
|
|
|
11390
11965
|
init_state_store();
|
|
11391
11966
|
init_docker();
|
|
11392
11967
|
init_docker_utils();
|
|
11968
|
+
init_sql_capture();
|
|
11393
11969
|
init_sqlite();
|
|
11394
11970
|
init_connection_pool();
|
|
11395
11971
|
init_discovery();
|