@askdb/studio 0.2.0-beta.23 → 0.2.0-beta.25
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 +18 -0
- package/dist/client/assets/index-Bj6qGfT6.css +2 -0
- package/dist/client/assets/index-DYBXDl3K.js +69 -0
- package/dist/client/index.html +2 -2
- package/dist/execute-registry.d.ts +34 -0
- package/dist/execute-registry.d.ts.map +1 -0
- package/dist/execute-registry.js +240 -0
- package/dist/execute-registry.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +177 -47
- package/dist/server.js.map +1 -1
- package/dist/shared/api.d.ts +25 -0
- package/dist/shared/api.d.ts.map +1 -1
- package/package.json +48 -21
- package/dist/client/assets/index-DY6BxO-7.css +0 -2
- package/dist/client/assets/index-_lnkmWaK.js +0 -68
package/dist/client/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<title>AskDB Studio</title>
|
|
7
7
|
<link rel="icon" type="image/png" media="(prefers-color-scheme: light)" href="/assets/brand/light-icon.png" />
|
|
8
8
|
<link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="/assets/brand/dark-icon.png" />
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DYBXDl3K.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bj6qGfT6.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio execute-driver registry.
|
|
3
|
+
*
|
|
4
|
+
* Each entry describes a live execute provider — how to dynamically import the
|
|
5
|
+
* driver package, what connection shape it expects, and how to normalize its
|
|
6
|
+
* results into the shared `ExecuteResponse` DTO.
|
|
7
|
+
*
|
|
8
|
+
* Drivers are optional peers. The registry never imports a driver at module
|
|
9
|
+
* load time; every import lives inside an `executeXxx()` function so a missing
|
|
10
|
+
* package only fails when the user actually tries to execute a query.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExecuteResponse } from "./shared/api.js";
|
|
13
|
+
export type StudioExecuteProvider = "postgres" | "mysql" | "sqlite" | "sqlserver";
|
|
14
|
+
export type StudioExecuteInput = {
|
|
15
|
+
connectionString?: string;
|
|
16
|
+
file?: string;
|
|
17
|
+
sql: string;
|
|
18
|
+
params: unknown[];
|
|
19
|
+
};
|
|
20
|
+
export type StudioDriverDefinition = {
|
|
21
|
+
provider: StudioExecuteProvider;
|
|
22
|
+
label: string;
|
|
23
|
+
packageName: "pg" | "mysql2" | "better-sqlite3" | "mssql";
|
|
24
|
+
installCommand: string;
|
|
25
|
+
execute(input: StudioExecuteInput): Promise<ExecuteResponse>;
|
|
26
|
+
};
|
|
27
|
+
export declare const EXECUTE_DRIVER_REGISTRY: Record<StudioExecuteProvider, StudioDriverDefinition>;
|
|
28
|
+
/**
|
|
29
|
+
* Check whether a driver package is importable at runtime.
|
|
30
|
+
* Returns `true` when the import succeeds (even if the value is empty),
|
|
31
|
+
* `false` when it throws (module not found).
|
|
32
|
+
*/
|
|
33
|
+
export declare function isDriverInstalled(packageName: string): Promise<boolean>;
|
|
34
|
+
//# sourceMappingURL=execute-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-registry.d.ts","sourceRoot":"","sources":["../src/execute-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAMvD,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,WAAW,CAAC;AAElF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,OAAO,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,IAAI,GAAG,QAAQ,GAAG,gBAAgB,GAAG,OAAO,CAAC;IAC1D,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CAC9D,CAAC;AA6PF,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,qBAAqB,EAAE,sBAAsB,CA6BzF,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO7E"}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Studio execute-driver registry.
|
|
3
|
+
*
|
|
4
|
+
* Each entry describes a live execute provider — how to dynamically import the
|
|
5
|
+
* driver package, what connection shape it expects, and how to normalize its
|
|
6
|
+
* results into the shared `ExecuteResponse` DTO.
|
|
7
|
+
*
|
|
8
|
+
* Drivers are optional peers. The registry never imports a driver at module
|
|
9
|
+
* load time; every import lives inside an `executeXxx()` function so a missing
|
|
10
|
+
* package only fails when the user actually tries to execute a query.
|
|
11
|
+
*/
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Normalization helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const MAX_ROWS = 500;
|
|
16
|
+
function buildOkResponse(columns, rows, rowCount, durationMs) {
|
|
17
|
+
const truncated = rows.length > MAX_ROWS;
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
columns,
|
|
21
|
+
rows: rows.slice(0, MAX_ROWS),
|
|
22
|
+
rowCount,
|
|
23
|
+
durationMs,
|
|
24
|
+
truncated,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function executePostgres(input) {
|
|
28
|
+
const { connectionString, sql, params } = input;
|
|
29
|
+
if (!connectionString) {
|
|
30
|
+
return { ok: false, error: "No connection URL configured for Postgres execute. Set ASKDB_STUDIO_DATABASE_URL or introspection.providerConfig.postgres.databaseUrl." };
|
|
31
|
+
}
|
|
32
|
+
let pgMod;
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import("pg");
|
|
35
|
+
pgMod = (mod.default ?? mod);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { ok: false, error: "The `pg` package is not installed. Install it with `pnpm add pg`." };
|
|
39
|
+
}
|
|
40
|
+
const client = new pgMod.Client({ connectionString });
|
|
41
|
+
try {
|
|
42
|
+
await client.connect();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
46
|
+
}
|
|
47
|
+
const startMs = Date.now();
|
|
48
|
+
try {
|
|
49
|
+
await client.query("BEGIN READ ONLY");
|
|
50
|
+
const result = await client.query({ text: sql, values: params });
|
|
51
|
+
await client.query("COMMIT");
|
|
52
|
+
const durationMs = Date.now() - startMs;
|
|
53
|
+
const columns = result.fields.map((f) => f.name);
|
|
54
|
+
const rows = result.rows.map((r) => columns.map((c) => r[c]));
|
|
55
|
+
return buildOkResponse(columns, rows, result.rowCount ?? rows.length, durationMs);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
await client.query("ROLLBACK").catch(() => { });
|
|
59
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await client.end().catch(() => { });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function executeMySQL(input) {
|
|
66
|
+
const { connectionString, sql, params } = input;
|
|
67
|
+
if (!connectionString) {
|
|
68
|
+
return { ok: false, error: "No connection URL configured for MySQL execute. Set ASKDB_STUDIO_DATABASE_URL or introspection.providerConfig.mysql.databaseUrl." };
|
|
69
|
+
}
|
|
70
|
+
let mysql2Mod;
|
|
71
|
+
try {
|
|
72
|
+
const mod = await import("mysql2/promise");
|
|
73
|
+
mysql2Mod = (mod.default ?? mod);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { ok: false, error: "The `mysql2` package is not installed. Install it with `pnpm add mysql2`." };
|
|
77
|
+
}
|
|
78
|
+
let conn;
|
|
79
|
+
try {
|
|
80
|
+
conn = await mysql2Mod.createConnection({ uri: connectionString });
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
84
|
+
}
|
|
85
|
+
const startMs = Date.now();
|
|
86
|
+
try {
|
|
87
|
+
await conn.query("SET SESSION TRANSACTION READ ONLY");
|
|
88
|
+
await conn.query("START TRANSACTION");
|
|
89
|
+
const [rowsRaw, fields] = await conn.execute(sql, params);
|
|
90
|
+
await conn.query("ROLLBACK");
|
|
91
|
+
const durationMs = Date.now() - startMs;
|
|
92
|
+
const columns = fields.map((f) => f.name);
|
|
93
|
+
const rows = rowsRaw.map((r) => columns.map((c) => r[c]));
|
|
94
|
+
return buildOkResponse(columns, rows, rows.length, durationMs);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
await conn.query("ROLLBACK").catch(() => { });
|
|
98
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await conn.end().catch(() => { });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function executeSQLite(input) {
|
|
105
|
+
const { file, sql, params } = input;
|
|
106
|
+
if (!file) {
|
|
107
|
+
return { ok: false, error: "No SQLite file path configured for execute. Set ASKDB_STUDIO_SQLITE_FILE or introspection.providerConfig.sqlite.file." };
|
|
108
|
+
}
|
|
109
|
+
let Sqlite3;
|
|
110
|
+
try {
|
|
111
|
+
const mod = await import("better-sqlite3");
|
|
112
|
+
Sqlite3 = mod.default;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { ok: false, error: "The `better-sqlite3` package is not installed. Install it with `pnpm add better-sqlite3`." };
|
|
116
|
+
}
|
|
117
|
+
const startMs = Date.now();
|
|
118
|
+
const db = new Sqlite3(file, { readonly: true, fileMustExist: true });
|
|
119
|
+
try {
|
|
120
|
+
const stmt = db.prepare(sql);
|
|
121
|
+
const columns = stmt.columns().map((c) => c.name);
|
|
122
|
+
const rowsRaw = stmt.all(...params);
|
|
123
|
+
const durationMs = Date.now() - startMs;
|
|
124
|
+
const rows = rowsRaw.map((r) => columns.map((c) => r[c]));
|
|
125
|
+
return buildOkResponse(columns, rows, rows.length, durationMs);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
db.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Rewrite `$1`, `$2`, ... or bare `?` positional params to MSSQL `@p0`, `@p1`, ...
|
|
136
|
+
* and return an object of named bindings.
|
|
137
|
+
*/
|
|
138
|
+
function rewriteSqlServerParams(sql, params) {
|
|
139
|
+
if (params.length === 0)
|
|
140
|
+
return { sql, bindings: {} };
|
|
141
|
+
const bindings = {};
|
|
142
|
+
params.forEach((value, idx) => {
|
|
143
|
+
bindings[`p${idx}`] = value;
|
|
144
|
+
});
|
|
145
|
+
// Replace $N placeholders (1-indexed) first, then bare ? placeholders.
|
|
146
|
+
let idx = 0;
|
|
147
|
+
const rewritten = sql
|
|
148
|
+
.replace(/\$\d+/g, () => `@p${idx++}`)
|
|
149
|
+
.replace(/\?/g, () => `@p${idx++}`);
|
|
150
|
+
return { sql: rewritten, bindings };
|
|
151
|
+
}
|
|
152
|
+
async function executeSQLServer(input) {
|
|
153
|
+
const { connectionString, sql, params } = input;
|
|
154
|
+
if (!connectionString) {
|
|
155
|
+
return { ok: false, error: "No connection URL configured for SQL Server execute. Set ASKDB_STUDIO_DATABASE_URL or introspection.providerConfig.sqlserver.databaseUrl." };
|
|
156
|
+
}
|
|
157
|
+
let mssqlMod;
|
|
158
|
+
try {
|
|
159
|
+
const mod = await import("mssql");
|
|
160
|
+
mssqlMod = (mod.default ?? mod);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { ok: false, error: "The `mssql` package is not installed. Install it with `pnpm add mssql`." };
|
|
164
|
+
}
|
|
165
|
+
let pool;
|
|
166
|
+
try {
|
|
167
|
+
pool = await new mssqlMod.ConnectionPool(connectionString).connect();
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
171
|
+
}
|
|
172
|
+
const startMs = Date.now();
|
|
173
|
+
try {
|
|
174
|
+
const { sql: rewrittenSql, bindings } = rewriteSqlServerParams(sql, params);
|
|
175
|
+
const req = pool.request();
|
|
176
|
+
for (const [name, value] of Object.entries(bindings)) {
|
|
177
|
+
req.input(name, value);
|
|
178
|
+
}
|
|
179
|
+
const result = await req.query(rewrittenSql);
|
|
180
|
+
const durationMs = Date.now() - startMs;
|
|
181
|
+
const rowsRaw = result.recordset ?? [];
|
|
182
|
+
const columns = rowsRaw.length > 0 ? Object.keys(rowsRaw[0]) : [];
|
|
183
|
+
const rows = rowsRaw.map((r) => columns.map((c) => r[c]));
|
|
184
|
+
return buildOkResponse(columns, rows, rows.length, durationMs);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await pool.close();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Registry
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
export const EXECUTE_DRIVER_REGISTRY = {
|
|
197
|
+
postgres: {
|
|
198
|
+
provider: "postgres",
|
|
199
|
+
label: "Postgres",
|
|
200
|
+
packageName: "pg",
|
|
201
|
+
installCommand: "pnpm add pg",
|
|
202
|
+
execute: executePostgres,
|
|
203
|
+
},
|
|
204
|
+
mysql: {
|
|
205
|
+
provider: "mysql",
|
|
206
|
+
label: "MySQL",
|
|
207
|
+
packageName: "mysql2",
|
|
208
|
+
installCommand: "pnpm add mysql2",
|
|
209
|
+
execute: executeMySQL,
|
|
210
|
+
},
|
|
211
|
+
sqlite: {
|
|
212
|
+
provider: "sqlite",
|
|
213
|
+
label: "SQLite",
|
|
214
|
+
packageName: "better-sqlite3",
|
|
215
|
+
installCommand: "pnpm add better-sqlite3",
|
|
216
|
+
execute: executeSQLite,
|
|
217
|
+
},
|
|
218
|
+
sqlserver: {
|
|
219
|
+
provider: "sqlserver",
|
|
220
|
+
label: "SQL Server",
|
|
221
|
+
packageName: "mssql",
|
|
222
|
+
installCommand: "pnpm add mssql",
|
|
223
|
+
execute: executeSQLServer,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Check whether a driver package is importable at runtime.
|
|
228
|
+
* Returns `true` when the import succeeds (even if the value is empty),
|
|
229
|
+
* `false` when it throws (module not found).
|
|
230
|
+
*/
|
|
231
|
+
export async function isDriverInstalled(packageName) {
|
|
232
|
+
try {
|
|
233
|
+
await import(packageName);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=execute-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-registry.js","sourceRoot":"","sources":["../src/execute-registry.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAA6C,MAAM,WAAW,CAAC;AA+BpF,OAAO,EAOL,mBAAmB,EAOpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAaL,KAAK,SAAS,EACf,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAYV,kBAAkB,EAGnB,MAAM,iBAAiB,CAAC;AAQzB,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AAyD3D,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,OAAO,mBAAmB,GAAG,SAAS,GAC9C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAiHvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CAyC3E"}
|
package/dist/server.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
|
-
import { extname, relative, join, resolve } from "node:path";
|
|
4
|
+
import { extname, relative, join, resolve, dirname } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { generateText as defaultGenerateText } from "ai";
|
|
7
8
|
import { getAskDbRuntimeConfig } from "@askdb/config";
|
|
8
|
-
import {
|
|
9
|
+
import { createAiRegistry, } from "@askdb/ai";
|
|
10
|
+
import { anthropicProvider } from "@askdb/ai-anthropic";
|
|
9
11
|
import { azureProvider } from "@askdb/ai-azure";
|
|
10
12
|
import { googleProvider } from "@askdb/ai-google";
|
|
11
13
|
import { openaiProvider } from "@askdb/ai-openai";
|
|
12
14
|
import { ask, isBuiltInDialectId, loadSchema, suggestEnrichment, tenantScopeSchema, writeTenantPolicyMarkdown, tenantPolicyFrontmatterSchema, } from "@askdb/core";
|
|
13
15
|
import { buildSchemaIndex, chunkContentHash, chunkSchema, createAiSdkEmbedder, createFileStore, createMemoryStore, createPgvectorStore, createRetriever, loadChunkerSourcesFromDir, } from "@askdb/rag";
|
|
14
16
|
import { buildDefaultTableBody, buildFrontmatter, buildSuggestionContext, buildSuggestionTarget, buildTableDraft, loadWorkspace, replaceH2Section, replaceTableDescription, saveConcepts, saveTable, } from "@askdb/enrich";
|
|
15
|
-
|
|
17
|
+
import { EXECUTE_DRIVER_REGISTRY, isDriverInstalled } from "./execute-registry.js";
|
|
18
|
+
const ai = createAiRegistry([openaiProvider, azureProvider, googleProvider, anthropicProvider]);
|
|
16
19
|
const CLIENT_DIR = fileURLToPath(new URL("./client/", import.meta.url));
|
|
17
20
|
const STUDIO_RAG_MOCK_DIMENSIONS = 64;
|
|
18
21
|
const STUDIO_RAG_MOCK_EMBEDDER_ID = `studio:mock-lexical-${STUDIO_RAG_MOCK_DIMENSIONS}`;
|
|
@@ -104,6 +107,18 @@ export function createStudioServer(options) {
|
|
|
104
107
|
const result = deletePlaygroundHistoryEntry(state.schemaDir, id);
|
|
105
108
|
return writeJson(res, 200, result);
|
|
106
109
|
}
|
|
110
|
+
if (req.method === "GET" && url.pathname === "/api/execute/status") {
|
|
111
|
+
const result = await getExecuteStatus();
|
|
112
|
+
return writeJson(res, 200, result);
|
|
113
|
+
}
|
|
114
|
+
if (req.method === "POST" && url.pathname === "/api/execute/install-driver") {
|
|
115
|
+
if (!isLoopbackRequest(req)) {
|
|
116
|
+
return writeJson(res, 403, { error: { message: "Driver install is only available from loopback clients." } });
|
|
117
|
+
}
|
|
118
|
+
const body = await readJson(req);
|
|
119
|
+
const result = await installExecuteDriver(body, state.schemaDir);
|
|
120
|
+
return writeJson(res, 200, result);
|
|
121
|
+
}
|
|
107
122
|
if (req.method === "POST" && url.pathname === "/api/execute") {
|
|
108
123
|
const body = await readJson(req);
|
|
109
124
|
const result = await executeQuery(body);
|
|
@@ -126,7 +141,7 @@ export function serializeWorkspace(workspace) {
|
|
|
126
141
|
const rt = getAskDbRuntimeConfig();
|
|
127
142
|
const aiConfig = (() => {
|
|
128
143
|
try {
|
|
129
|
-
return resolveAiConfig(rt.ai.aiEnv);
|
|
144
|
+
return ai.resolveAiConfig(rt.ai.aiEnv);
|
|
130
145
|
}
|
|
131
146
|
catch {
|
|
132
147
|
// A misconfigured AI env (e.g. azure without resourceName) shouldn't crash the workspace
|
|
@@ -202,7 +217,7 @@ async function suggestForSource(workspace, source) {
|
|
|
202
217
|
const rt = getAskDbRuntimeConfig();
|
|
203
218
|
const model = await ai.createLanguageModelFromEnv(rt.ai.aiEnv);
|
|
204
219
|
if (!model) {
|
|
205
|
-
throw new StudioHttpError(400,
|
|
220
|
+
throw new StudioHttpError(400, ai.keyMissingMessage("AI enrichment suggestions"));
|
|
206
221
|
}
|
|
207
222
|
const candidates = await suggestEnrichment(buildSuggestionTarget(workspace, source), buildSuggestionContext(workspace, source.tableId), model);
|
|
208
223
|
return candidates.map((candidate) => ({ text: candidate.text }));
|
|
@@ -211,7 +226,7 @@ async function suggestTenantPolicyDraft(state) {
|
|
|
211
226
|
const rt = getAskDbRuntimeConfig();
|
|
212
227
|
const model = await ai.createLanguageModelFromEnv(rt.ai.aiEnv);
|
|
213
228
|
if (!model) {
|
|
214
|
-
throw new StudioHttpError(400,
|
|
229
|
+
throw new StudioHttpError(400, ai.keyMissingMessage("AI tenant policy suggestion"));
|
|
215
230
|
}
|
|
216
231
|
// Build a compact DDL-like representation of the schema for the prompt
|
|
217
232
|
const workspace = state.workspace;
|
|
@@ -299,9 +314,9 @@ If the schema has no clear multi-tenant pattern, still make your best guess at w
|
|
|
299
314
|
async function askSampleQuestion(state, options) {
|
|
300
315
|
const rt = getAskDbRuntimeConfig();
|
|
301
316
|
const mockSql = rt.dev.mockSql;
|
|
302
|
-
const aiConfig = mockSql ? undefined : resolveAiConfig(rt.ai.aiEnv);
|
|
317
|
+
const aiConfig = mockSql ? undefined : ai.resolveAiConfig(rt.ai.aiEnv);
|
|
303
318
|
if (!mockSql && !aiConfig) {
|
|
304
|
-
throw new StudioHttpError(400, `${
|
|
319
|
+
throw new StudioHttpError(400, `${ai.keyMissingMessage("Sample NL-to-SQL generation")} Set ASKDB_MOCK_SQL to bypass the live model.`);
|
|
305
320
|
}
|
|
306
321
|
const schema = loadSchema(state.schemaDir);
|
|
307
322
|
const retrievedChunks = [];
|
|
@@ -619,7 +634,7 @@ function resolveStudioRagEmbedderConfig() {
|
|
|
619
634
|
throw new StudioHttpError(400, `Unsupported Studio RAG embedder: ${kind}`);
|
|
620
635
|
}
|
|
621
636
|
const env = buildStudioRagEmbeddingEnv(kind, base);
|
|
622
|
-
const aiConfig = resolveEmbeddingConfig(env, {
|
|
637
|
+
const aiConfig = ai.resolveEmbeddingConfig(env, {
|
|
623
638
|
modelEnvVar: "ASKDB_RAG_EMBEDDER_MODEL",
|
|
624
639
|
modelDefault: DEFAULT_EMBEDDING_MODEL,
|
|
625
640
|
});
|
|
@@ -864,56 +879,171 @@ function parsePlaygroundHistoryEntry(body) {
|
|
|
864
879
|
return body;
|
|
865
880
|
}
|
|
866
881
|
// ---------------------------------------------------------------------------
|
|
867
|
-
// Execute endpoint
|
|
882
|
+
// Execute endpoint helpers
|
|
868
883
|
// ---------------------------------------------------------------------------
|
|
869
|
-
async function
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
884
|
+
async function getExecuteStatus() {
|
|
885
|
+
const rt = getAskDbRuntimeConfig();
|
|
886
|
+
const { provider, databaseUrl, file } = rt.studio.execute;
|
|
887
|
+
const def = EXECUTE_DRIVER_REGISTRY[provider];
|
|
888
|
+
const installed = await isDriverInstalled(def.packageName);
|
|
889
|
+
const connectionKind = provider === "sqlite" ? "file" : "url";
|
|
890
|
+
const configured = connectionKind === "file" ? Boolean(file) : Boolean(databaseUrl);
|
|
891
|
+
return {
|
|
892
|
+
provider,
|
|
893
|
+
label: def.label,
|
|
894
|
+
configured,
|
|
895
|
+
connectionKind,
|
|
896
|
+
packageName: def.packageName,
|
|
897
|
+
installed,
|
|
898
|
+
installCommand: def.installCommand,
|
|
899
|
+
canInstallFromStudio: true,
|
|
900
|
+
manualInstallReason: null,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
async function installExecuteDriver(body, schemaDir) {
|
|
904
|
+
const rt = getAskDbRuntimeConfig();
|
|
905
|
+
const configuredProvider = rt.studio.execute.provider;
|
|
906
|
+
// Accept the configured provider or an explicit allowlisted provider.
|
|
907
|
+
let provider = configuredProvider;
|
|
908
|
+
if (isRecord(body) && typeof body.provider === "string") {
|
|
909
|
+
const requested = body.provider;
|
|
910
|
+
if (!(requested in EXECUTE_DRIVER_REGISTRY)) {
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
provider: configuredProvider,
|
|
914
|
+
packageName: EXECUTE_DRIVER_REGISTRY[configuredProvider].packageName,
|
|
915
|
+
command: [],
|
|
916
|
+
error: `Unknown provider: ${body.provider}`,
|
|
917
|
+
installed: false,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
provider = requested;
|
|
873
921
|
}
|
|
874
|
-
|
|
875
|
-
|
|
922
|
+
const def = EXECUTE_DRIVER_REGISTRY[provider];
|
|
923
|
+
const { packageManager, command, args, manualReason } = detectPackageManager(schemaDir, def.packageName);
|
|
924
|
+
if (manualReason) {
|
|
925
|
+
return {
|
|
926
|
+
ok: false,
|
|
927
|
+
provider,
|
|
928
|
+
packageName: def.packageName,
|
|
929
|
+
command: [],
|
|
930
|
+
error: manualReason,
|
|
931
|
+
installed: false,
|
|
932
|
+
};
|
|
876
933
|
}
|
|
877
|
-
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
934
|
+
// Run the install.
|
|
935
|
+
const { stdout, stderr, code } = await spawnCommand(command, args, schemaDir);
|
|
936
|
+
const installed = await isDriverInstalled(def.packageName);
|
|
937
|
+
return {
|
|
938
|
+
ok: code === 0,
|
|
939
|
+
provider,
|
|
940
|
+
packageName: def.packageName,
|
|
941
|
+
command: [command, ...args],
|
|
942
|
+
stdout,
|
|
943
|
+
stderr,
|
|
944
|
+
error: code !== 0 ? `${packageManager} exited with code ${code}` : undefined,
|
|
945
|
+
installed,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function detectPackageManager(cwd, packageName) {
|
|
949
|
+
// Walk up from schemaDir to find the project root's lockfile.
|
|
950
|
+
const projectRoot = findProjectRoot(cwd);
|
|
951
|
+
if (!projectRoot) {
|
|
952
|
+
return {
|
|
953
|
+
packageManager: "",
|
|
954
|
+
command: "",
|
|
955
|
+
args: [],
|
|
956
|
+
manualReason: `Could not locate a package.json above ${cwd}. Run the install manually.`,
|
|
957
|
+
};
|
|
883
958
|
}
|
|
884
|
-
|
|
885
|
-
return {
|
|
959
|
+
if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
|
|
960
|
+
return {
|
|
961
|
+
packageManager: "pnpm",
|
|
962
|
+
command: "pnpm",
|
|
963
|
+
args: ["add", packageName],
|
|
964
|
+
manualReason: null,
|
|
965
|
+
};
|
|
886
966
|
}
|
|
887
|
-
|
|
888
|
-
await client.connect();
|
|
889
|
-
const startMs = Date.now();
|
|
890
|
-
try {
|
|
891
|
-
await client.query("BEGIN READ ONLY");
|
|
892
|
-
const result = await client.query({ text: sql, values: params });
|
|
893
|
-
await client.query("COMMIT");
|
|
894
|
-
const durationMs = Date.now() - startMs;
|
|
895
|
-
const columns = result.fields.map((f) => f.name);
|
|
896
|
-
const rows = result.rows.map((r) => columns.map((c) => r[c]));
|
|
897
|
-
const truncated = rows.length > 500;
|
|
967
|
+
if (existsSync(join(projectRoot, "package-lock.json"))) {
|
|
898
968
|
return {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
durationMs,
|
|
904
|
-
truncated,
|
|
969
|
+
packageManager: "npm",
|
|
970
|
+
command: "npm",
|
|
971
|
+
args: ["install", packageName],
|
|
972
|
+
manualReason: null,
|
|
905
973
|
};
|
|
906
974
|
}
|
|
907
|
-
|
|
908
|
-
await client.query("ROLLBACK").catch(() => { });
|
|
975
|
+
if (existsSync(join(projectRoot, "yarn.lock"))) {
|
|
909
976
|
return {
|
|
910
|
-
|
|
911
|
-
|
|
977
|
+
packageManager: "yarn",
|
|
978
|
+
command: "yarn",
|
|
979
|
+
args: ["add", packageName],
|
|
980
|
+
manualReason: null,
|
|
912
981
|
};
|
|
913
982
|
}
|
|
914
|
-
|
|
915
|
-
|
|
983
|
+
if (existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bun.lock"))) {
|
|
984
|
+
return {
|
|
985
|
+
packageManager: "bun",
|
|
986
|
+
command: "bun",
|
|
987
|
+
args: ["add", packageName],
|
|
988
|
+
manualReason: null,
|
|
989
|
+
};
|
|
916
990
|
}
|
|
991
|
+
return {
|
|
992
|
+
packageManager: "",
|
|
993
|
+
command: "",
|
|
994
|
+
args: [],
|
|
995
|
+
manualReason: `No recognized lockfile found in ${projectRoot}. Run: pnpm add ${packageName}`,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function findProjectRoot(startDir) {
|
|
999
|
+
let current = resolve(startDir);
|
|
1000
|
+
for (let i = 0; i < 20; i++) {
|
|
1001
|
+
if (existsSync(join(current, "package.json")))
|
|
1002
|
+
return current;
|
|
1003
|
+
const parent = dirname(current);
|
|
1004
|
+
if (parent === current)
|
|
1005
|
+
return null;
|
|
1006
|
+
current = parent;
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
function spawnCommand(command, args, cwd) {
|
|
1011
|
+
return new Promise((resolve) => {
|
|
1012
|
+
const proc = spawn(command, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
1013
|
+
const stdoutChunks = [];
|
|
1014
|
+
const stderrChunks = [];
|
|
1015
|
+
proc.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
1016
|
+
proc.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
1017
|
+
proc.on("close", (code) => {
|
|
1018
|
+
resolve({
|
|
1019
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8").slice(0, 4096),
|
|
1020
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8").slice(0, 4096),
|
|
1021
|
+
code: code ?? 1,
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
proc.on("error", (err) => {
|
|
1025
|
+
resolve({ stdout: "", stderr: err.message, code: 1 });
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
async function executeQuery(body) {
|
|
1030
|
+
const rt = getAskDbRuntimeConfig();
|
|
1031
|
+
const { provider, databaseUrl, file } = rt.studio.execute;
|
|
1032
|
+
if (!isRecord(body) || typeof body.sql !== "string" || body.sql.trim() === "") {
|
|
1033
|
+
return { ok: false, error: "`sql` is required." };
|
|
1034
|
+
}
|
|
1035
|
+
const sql = body.sql;
|
|
1036
|
+
const params = Array.isArray(body.params) ? body.params : [];
|
|
1037
|
+
const def = EXECUTE_DRIVER_REGISTRY[provider];
|
|
1038
|
+
return def.execute({ connectionString: databaseUrl, file, sql, params });
|
|
1039
|
+
}
|
|
1040
|
+
function isLoopbackRequest(req) {
|
|
1041
|
+
const socket = req.socket;
|
|
1042
|
+
const remoteAddress = socket?.remoteAddress ?? "";
|
|
1043
|
+
return (remoteAddress === "127.0.0.1" ||
|
|
1044
|
+
remoteAddress === "::1" ||
|
|
1045
|
+
remoteAddress === "::ffff:127.0.0.1" ||
|
|
1046
|
+
remoteAddress === "localhost");
|
|
917
1047
|
}
|
|
918
1048
|
function readOptionalJson(path) {
|
|
919
1049
|
if (!existsSync(path))
|