@askdb/studio 0.2.0-beta.28 → 0.2.0-beta.29
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/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +7 -1
- package/dist/bin.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-BgdvYJyR.js +108 -0
- package/dist/client/index.html +1 -1
- package/dist/introspection.d.ts +35 -0
- package/dist/introspection.d.ts.map +1 -0
- package/dist/introspection.js +138 -0
- package/dist/introspection.js.map +1 -0
- package/dist/server.d.ts +8 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +219 -19
- package/dist/server.js.map +1 -1
- package/dist/setup.d.ts +58 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +373 -0
- package/dist/setup.js.map +1 -0
- package/dist/shared/api.d.ts +57 -0
- package/dist/shared/api.d.ts.map +1 -1
- package/package.json +9 -6
- package/dist/client/assets/index-DyBQErXG.js +0 -69
package/dist/client/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BgdvYJyR.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-Bj6qGfT6.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ConnectorProvider } from "@askdb/connectors";
|
|
2
|
+
export type StudioIntrospectionPlan = {
|
|
3
|
+
ok: true;
|
|
4
|
+
engine: ConnectorProvider;
|
|
5
|
+
sourceLabel: string;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
engine: ConnectorProvider | null;
|
|
9
|
+
error: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Resolve what a server-side introspection run would do, from the runtime
|
|
13
|
+
* config alone. Mirrors the CLI's flag-free resolution in
|
|
14
|
+
* `apps/cli/src/introspect.ts` (config provider + per-engine connection).
|
|
15
|
+
* Never includes credentials in `sourceLabel` — it is shown in the UI.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveStudioIntrospectionPlan(): StudioIntrospectionPlan;
|
|
18
|
+
export type StudioIntrospectionRun = {
|
|
19
|
+
engine: ConnectorProvider;
|
|
20
|
+
schemaId: string;
|
|
21
|
+
tables: number;
|
|
22
|
+
warnings: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Run introspection server-side into `outDir`, preserving any existing
|
|
26
|
+
* enrichment markdown (the writer keeps `tables/*.md`, `concepts.md`, and
|
|
27
|
+
* `tenant-policy.md` when `existingArtifactDir` is supplied — the same
|
|
28
|
+
* guarantee `askdb introspect` gives).
|
|
29
|
+
*/
|
|
30
|
+
export declare function runStudioIntrospection(options: {
|
|
31
|
+
outDir: string;
|
|
32
|
+
schemaId?: string;
|
|
33
|
+
hasExistingArtifact: boolean;
|
|
34
|
+
}): Promise<StudioIntrospectionRun>;
|
|
35
|
+
//# sourceMappingURL=introspection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.d.ts","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"AAOA,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,mBAAmB,CAAC;AAe3B,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,iBAAiB,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAC5D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;GAKG;AACH,wBAAgB,8BAA8B,IAAI,uBAAuB,CASxE;AA4ED,MAAM,MAAM,sBAAsB,GAAG;IACnC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;CAC9B,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAkClC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { getAskDbRuntimeConfig } from "@askdb/config";
|
|
3
|
+
import { introspect, } from "@askdb/introspect";
|
|
4
|
+
import { createConnectorRegistry, } from "@askdb/connectors";
|
|
5
|
+
import { postgresConnectorProvider } from "@askdb/postgres";
|
|
6
|
+
import { mysqlConnectorProvider } from "@askdb/mysql";
|
|
7
|
+
import { sqliteConnectorProvider } from "@askdb/sqlite";
|
|
8
|
+
import { sqlServerConnectorProvider } from "@askdb/sqlserver";
|
|
9
|
+
import { prismaConnectorProvider } from "@askdb/prisma";
|
|
10
|
+
const connectorRegistry = createConnectorRegistry([
|
|
11
|
+
postgresConnectorProvider,
|
|
12
|
+
mysqlConnectorProvider,
|
|
13
|
+
sqliteConnectorProvider,
|
|
14
|
+
sqlServerConnectorProvider,
|
|
15
|
+
prismaConnectorProvider,
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Resolve what a server-side introspection run would do, from the runtime
|
|
19
|
+
* config alone. Mirrors the CLI's flag-free resolution in
|
|
20
|
+
* `apps/cli/src/introspect.ts` (config provider + per-engine connection).
|
|
21
|
+
* Never includes credentials in `sourceLabel` — it is shown in the UI.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveStudioIntrospectionPlan() {
|
|
24
|
+
const rt = getAskDbRuntimeConfig();
|
|
25
|
+
const engine = rt.introspection.provider;
|
|
26
|
+
if (!connectorRegistry.hasProvider(engine)) {
|
|
27
|
+
return { ok: false, engine: null, error: `Unsupported introspection provider: ${engine}` };
|
|
28
|
+
}
|
|
29
|
+
const connection = resolveConnection(engine);
|
|
30
|
+
if (!connection.ok)
|
|
31
|
+
return { ok: false, engine, error: connection.error };
|
|
32
|
+
return { ok: true, engine, sourceLabel: connection.sourceLabel };
|
|
33
|
+
}
|
|
34
|
+
function resolveConnection(engine) {
|
|
35
|
+
const rt = getAskDbRuntimeConfig();
|
|
36
|
+
switch (engine) {
|
|
37
|
+
case "postgres": {
|
|
38
|
+
const url = rt.introspection.postgresDatabaseUrl;
|
|
39
|
+
if (!url) {
|
|
40
|
+
return {
|
|
41
|
+
ok: false,
|
|
42
|
+
error: "No Postgres connection configured. Set introspection.providerConfig.postgres.databaseUrl in askdb.config.ts (bound to an env var in .env).",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, url, sourceLabel: redactUrl(url) };
|
|
46
|
+
}
|
|
47
|
+
case "mysql": {
|
|
48
|
+
const url = rt.introspection.mysqlDatabaseUrl;
|
|
49
|
+
if (!url) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: "No MySQL connection configured. Set introspection.providerConfig.mysql.databaseUrl in askdb.config.ts (bound to an env var in .env).",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, url, sourceLabel: redactUrl(url) };
|
|
56
|
+
}
|
|
57
|
+
case "sqlserver": {
|
|
58
|
+
const url = rt.introspection.sqlserverDatabaseUrl;
|
|
59
|
+
if (!url) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: "No SQL Server connection configured. Set introspection.providerConfig.sqlserver.databaseUrl in askdb.config.ts (bound to an env var in .env).",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, url, sourceLabel: redactUrl(url) };
|
|
66
|
+
}
|
|
67
|
+
case "sqlite": {
|
|
68
|
+
const file = rt.introspection.sqliteFile;
|
|
69
|
+
if (!file) {
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: "No SQLite file configured. Set introspection.providerConfig.sqlite.file in askdb.config.ts.",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, url: file, sourceLabel: file };
|
|
76
|
+
}
|
|
77
|
+
case "prisma": {
|
|
78
|
+
// When unset, @askdb/prisma auto-discovers prisma/schema.prisma in the project root.
|
|
79
|
+
const schemaPath = rt.introspection.prismaSchemaPath;
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
schemaPath,
|
|
83
|
+
sourceLabel: schemaPath ?? "auto-discovered prisma/schema.prisma",
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Strip credentials from a connection string for display (never shown raw in the UI). */
|
|
89
|
+
function redactUrl(raw) {
|
|
90
|
+
try {
|
|
91
|
+
const url = new URL(raw);
|
|
92
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ADO.NET-style or otherwise unparseable — show only that it is configured.
|
|
96
|
+
return "configured connection";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Run introspection server-side into `outDir`, preserving any existing
|
|
101
|
+
* enrichment markdown (the writer keeps `tables/*.md`, `concepts.md`, and
|
|
102
|
+
* `tenant-policy.md` when `existingArtifactDir` is supplied — the same
|
|
103
|
+
* guarantee `askdb introspect` gives).
|
|
104
|
+
*/
|
|
105
|
+
export async function runStudioIntrospection(options) {
|
|
106
|
+
const plan = resolveStudioIntrospectionPlan();
|
|
107
|
+
if (!plan.ok)
|
|
108
|
+
throw new Error(plan.error);
|
|
109
|
+
const connection = resolveConnection(plan.engine);
|
|
110
|
+
if (!connection.ok)
|
|
111
|
+
throw new Error(connection.error);
|
|
112
|
+
const schemaId = options.schemaId ?? inferSchemaId(options.outDir);
|
|
113
|
+
const connectorConfig = {
|
|
114
|
+
provider: plan.engine,
|
|
115
|
+
url: connection.url,
|
|
116
|
+
schemaPath: connection.schemaPath,
|
|
117
|
+
schemaId,
|
|
118
|
+
};
|
|
119
|
+
const runConfig = connectorRegistry.createConnector(connectorConfig);
|
|
120
|
+
const connector = runConfig.connector;
|
|
121
|
+
const result = await introspect(runConfig.input, {
|
|
122
|
+
outDir: options.outDir,
|
|
123
|
+
schemaId,
|
|
124
|
+
existingArtifactDir: options.hasExistingArtifact ? options.outDir : undefined,
|
|
125
|
+
}, { connector });
|
|
126
|
+
return {
|
|
127
|
+
engine: plan.engine,
|
|
128
|
+
schemaId,
|
|
129
|
+
tables: result.schema.schemas.reduce((sum, namespace) => sum + namespace.tables.length, 0),
|
|
130
|
+
warnings: result.warnings.map((warning) => typeof warning === "string" ? warning : JSON.stringify(warning)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function inferSchemaId(path) {
|
|
134
|
+
const name = basename(path);
|
|
135
|
+
const inferred = name.endsWith(".schema") ? name.slice(0, -".schema".length) : name;
|
|
136
|
+
return inferred || "introspected";
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=introspection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.js","sourceRoot":"","sources":["../src/introspection.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createPgvectorStore } from "@askdb/rag";
|
|
3
3
|
import { type Workspace } from "@askdb/enrich";
|
|
4
|
-
import type { StudioWorkspaceDto } from "./shared/api.js";
|
|
4
|
+
import type { SetupReason, StudioWorkspaceDto } from "./shared/api.js";
|
|
5
5
|
export type StudioOptions = {
|
|
6
6
|
schema: string;
|
|
7
7
|
host?: string;
|
|
8
8
|
port?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Start in setup mode instead of failing: `"no-config"` when no
|
|
11
|
+
* `askdb.config.*` was found, `"no-artifact"` when the config exists but the
|
|
12
|
+
* schema artifact hasn't been introspected yet. The browser wizard walks the
|
|
13
|
+
* user through the missing steps and flips the server to ready.
|
|
14
|
+
*/
|
|
15
|
+
setupReason?: SetupReason | null;
|
|
9
16
|
};
|
|
10
17
|
export type StudioServer = ReturnType<typeof createServer>;
|
|
11
18
|
export declare function setStudioPgvectorStoreFactoryForTests(factory: typeof createPgvectorStore | undefined): void;
|
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;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;
|
|
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,WAAW,EAKX,kBAAkB,EAGnB,MAAM,iBAAiB,CAAC;AAUzB,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AA0D3D,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,OAAO,mBAAmB,GAAG,SAAS,GAC9C,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,YAAY,CAwJvE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,kBAAkB,CA8C3E"}
|
package/dist/server.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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, dirname } from "node:path";
|
|
4
|
+
import { extname, relative, join, resolve, dirname, sep } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { generateText as defaultGenerateText } from "ai";
|
|
8
|
-
import { getAskDbRuntimeConfig } from "@askdb/config";
|
|
8
|
+
import { bootstrapAskDbEnv, getAskDbRuntimeConfig } from "@askdb/config";
|
|
9
9
|
import { createAiRegistry, } from "@askdb/ai";
|
|
10
10
|
import { anthropicProvider } from "@askdb/ai-anthropic";
|
|
11
11
|
import { azureProvider } from "@askdb/ai-azure";
|
|
@@ -15,6 +15,8 @@ import { ask, isBuiltInDialectId, loadSchema, suggestEnrichment, tenantScopeSche
|
|
|
15
15
|
import { buildSchemaIndex, chunkContentHash, chunkSchema, createAiSdkEmbedder, createFileStore, createMemoryStore, createPgvectorStore, createRetriever, loadChunkerSourcesFromDir, } from "@askdb/rag";
|
|
16
16
|
import { buildDefaultTableBody, buildFrontmatter, buildSuggestionContext, buildSuggestionTarget, buildTableDraft, loadWorkspace, replaceH2Section, replaceTableDescription, saveConcepts, saveTable, } from "@askdb/enrich";
|
|
17
17
|
import { EXECUTE_DRIVER_REGISTRY, isDriverInstalled } from "./execute-registry.js";
|
|
18
|
+
import { resolveStudioIntrospectionPlan, runStudioIntrospection } from "./introspection.js";
|
|
19
|
+
import { probeSetupState, writeSetupConfig, SetupError } from "./setup.js";
|
|
18
20
|
const ai = createAiRegistry([openaiProvider, azureProvider, googleProvider, anthropicProvider]);
|
|
19
21
|
const CLIENT_DIR = fileURLToPath(new URL("./client/", import.meta.url));
|
|
20
22
|
const STUDIO_RAG_MOCK_DIMENSIONS = 64;
|
|
@@ -26,12 +28,14 @@ export function setStudioPgvectorStoreFactoryForTests(factory) {
|
|
|
26
28
|
}
|
|
27
29
|
export function createStudioServer(options) {
|
|
28
30
|
const schemaDir = resolve(options.schema);
|
|
29
|
-
|
|
31
|
+
const setupReason = options.setupReason ?? null;
|
|
32
|
+
if (!setupReason && !existsSync(schemaDir)) {
|
|
30
33
|
throw new Error(`schema directory not found: ${schemaDir}`);
|
|
31
34
|
}
|
|
32
35
|
const state = {
|
|
33
36
|
schemaDir,
|
|
34
|
-
workspace: loadWorkspace(schemaDir),
|
|
37
|
+
workspace: setupReason ? null : loadWorkspace(schemaDir),
|
|
38
|
+
setupReason,
|
|
35
39
|
};
|
|
36
40
|
return createServer(async (req, res) => {
|
|
37
41
|
try {
|
|
@@ -42,27 +46,63 @@ export function createStudioServer(options) {
|
|
|
42
46
|
if (req.method === "GET" && url.pathname.startsWith("/assets/")) {
|
|
43
47
|
return serveClientFile(res, decodeURIComponent(url.pathname.slice(1)));
|
|
44
48
|
}
|
|
49
|
+
if (req.method === "GET" && url.pathname === "/api/setup/status") {
|
|
50
|
+
return writeJson(res, 200, buildSetupStatus(state));
|
|
51
|
+
}
|
|
52
|
+
if (req.method === "POST" && url.pathname === "/api/setup/config") {
|
|
53
|
+
if (!isLoopbackRequest(req)) {
|
|
54
|
+
return writeJson(res, 403, { error: { message: "Setup is only available from loopback clients." } });
|
|
55
|
+
}
|
|
56
|
+
const body = await readJson(req);
|
|
57
|
+
const result = handleSetupConfig(state, body);
|
|
58
|
+
return writeJson(res, 200, result);
|
|
59
|
+
}
|
|
60
|
+
if (req.method === "POST" && url.pathname === "/api/setup/introspect") {
|
|
61
|
+
if (!isLoopbackRequest(req)) {
|
|
62
|
+
return writeJson(res, 403, { error: { message: "Setup is only available from loopback clients." } });
|
|
63
|
+
}
|
|
64
|
+
const result = await handleSetupIntrospect(state);
|
|
65
|
+
return writeJson(res, 200, result);
|
|
66
|
+
}
|
|
67
|
+
// Everything below needs a loaded workspace — route setup-mode clients to the wizard.
|
|
68
|
+
if (state.setupReason && url.pathname.startsWith("/api/")) {
|
|
69
|
+
return writeJson(res, 409, {
|
|
70
|
+
error: { message: "Studio needs setup. Open Studio in the browser to finish the guided setup." },
|
|
71
|
+
setup: buildSetupStatus(state),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
45
74
|
if (req.method === "GET" && url.pathname === "/api/workspace") {
|
|
46
|
-
return writeJson(res, 200, serializeWorkspace(state
|
|
75
|
+
return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
|
|
76
|
+
}
|
|
77
|
+
if (req.method === "GET" && url.pathname === "/api/introspect/status") {
|
|
78
|
+
requireWorkspace(state);
|
|
79
|
+
return writeJson(res, 200, buildIntrospectionPlanDto());
|
|
80
|
+
}
|
|
81
|
+
if (req.method === "POST" && url.pathname === "/api/introspect") {
|
|
82
|
+
if (!isLoopbackRequest(req)) {
|
|
83
|
+
return writeJson(res, 403, { error: { message: "Resync is only available from loopback clients." } });
|
|
84
|
+
}
|
|
85
|
+
const result = await handleResync(state);
|
|
86
|
+
return writeJson(res, 200, result);
|
|
47
87
|
}
|
|
48
88
|
if (req.method === "POST" && url.pathname.startsWith("/api/tables/")) {
|
|
49
89
|
const tableId = decodeURIComponent(url.pathname.slice("/api/tables/".length));
|
|
50
90
|
const body = await readJson(req);
|
|
51
91
|
const draft = parseTableDraftBody(body);
|
|
52
92
|
saveDraft(state, tableId, draft);
|
|
53
|
-
return writeJson(res, 200, serializeWorkspace(state
|
|
93
|
+
return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
|
|
54
94
|
}
|
|
55
95
|
if (req.method === "POST" && url.pathname === "/api/concepts") {
|
|
56
96
|
const body = await readJson(req);
|
|
57
97
|
const concepts = parseConceptsBody(body);
|
|
58
98
|
saveConceptsDraft(state, concepts);
|
|
59
|
-
return writeJson(res, 200, serializeWorkspace(state
|
|
99
|
+
return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
|
|
60
100
|
}
|
|
61
101
|
if (req.method === "POST" && url.pathname === "/api/tenant-policy") {
|
|
62
102
|
const body = await readJson(req);
|
|
63
103
|
const { frontmatter, bodyText } = parseTenantPolicyBody(body);
|
|
64
104
|
saveTenantPolicy(state, frontmatter, bodyText);
|
|
65
|
-
return writeJson(res, 200, serializeWorkspace(state
|
|
105
|
+
return writeJson(res, 200, serializeWorkspace(requireWorkspace(state)));
|
|
66
106
|
}
|
|
67
107
|
if (req.method === "POST" && url.pathname === "/api/suggest-tenant-policy") {
|
|
68
108
|
const draft = await suggestTenantPolicyDraft(state);
|
|
@@ -71,7 +111,7 @@ export function createStudioServer(options) {
|
|
|
71
111
|
if (req.method === "POST" && url.pathname === "/api/suggest") {
|
|
72
112
|
const body = await readJson(req);
|
|
73
113
|
const source = parseSuggestSource(body);
|
|
74
|
-
const candidates = await suggestForSource(state
|
|
114
|
+
const candidates = await suggestForSource(requireWorkspace(state), source);
|
|
75
115
|
return writeJson(res, 200, { candidates });
|
|
76
116
|
}
|
|
77
117
|
if (req.method === "GET" && url.pathname === "/api/rag/status") {
|
|
@@ -130,7 +170,7 @@ export function createStudioServer(options) {
|
|
|
130
170
|
writeJson(res, 404, { error: { message: "Not found" } });
|
|
131
171
|
}
|
|
132
172
|
catch (error) {
|
|
133
|
-
const status = error instanceof StudioHttpError ? error.status : 500;
|
|
173
|
+
const status = error instanceof StudioHttpError || error instanceof SetupError ? error.status : 500;
|
|
134
174
|
writeJson(res, status, {
|
|
135
175
|
error: { message: error instanceof Error ? error.message : String(error) },
|
|
136
176
|
});
|
|
@@ -149,9 +189,15 @@ export function serializeWorkspace(workspace) {
|
|
|
149
189
|
return undefined;
|
|
150
190
|
}
|
|
151
191
|
})();
|
|
192
|
+
const schema = loadSchema(workspace.schemaDir);
|
|
193
|
+
const schemaProvider = "provider" in schema && typeof schema.provider === "string" ? schema.provider : undefined;
|
|
194
|
+
const dialect = rt.nlToSql.dialect ??
|
|
195
|
+
(schemaProvider && isBuiltInDialectId(schemaProvider) ? schemaProvider : "postgres");
|
|
152
196
|
return {
|
|
153
197
|
schemaDir: workspace.schemaDir,
|
|
198
|
+
schemaPathRelative: toRelativeSchemaPath(workspace.schemaDir),
|
|
154
199
|
schemaId: workspace.physical.schemaId,
|
|
200
|
+
dialect,
|
|
155
201
|
warnings: workspace.warnings,
|
|
156
202
|
aiConfigured: Boolean(aiConfig),
|
|
157
203
|
model: aiConfig?.model ?? "gpt-4o-mini",
|
|
@@ -169,14 +215,167 @@ export function serializeWorkspace(workspace) {
|
|
|
169
215
|
};
|
|
170
216
|
}),
|
|
171
217
|
concepts: workspace.concepts?.frontmatter.concepts ?? [],
|
|
172
|
-
tenantPolicy:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
218
|
+
tenantPolicy: schema.tenantPolicy ?? null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function toRelativeSchemaPath(schemaDir) {
|
|
222
|
+
const rel = relative(process.cwd(), schemaDir);
|
|
223
|
+
if (rel === "")
|
|
224
|
+
return ".";
|
|
225
|
+
if (rel.startsWith("..") || rel.includes(`..${sep}`))
|
|
226
|
+
return schemaDir;
|
|
227
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Setup wizard + server-side introspection (resync)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
function requireWorkspace(state) {
|
|
233
|
+
if (!state.workspace) {
|
|
234
|
+
throw new StudioHttpError(409, "Studio is in setup mode — finish the guided setup first.");
|
|
235
|
+
}
|
|
236
|
+
return state.workspace;
|
|
237
|
+
}
|
|
238
|
+
function buildSetupStatus(state) {
|
|
239
|
+
const projectDir = process.cwd();
|
|
240
|
+
if (!state.setupReason) {
|
|
241
|
+
return {
|
|
242
|
+
needed: false,
|
|
243
|
+
reason: null,
|
|
244
|
+
projectDir,
|
|
245
|
+
configPath: null,
|
|
246
|
+
outputDir: state.schemaDir,
|
|
247
|
+
configLoadError: null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const probe = probeSetupState(projectDir);
|
|
251
|
+
return {
|
|
252
|
+
needed: true,
|
|
253
|
+
reason: probe.hasConfig ? "no-artifact" : "no-config",
|
|
254
|
+
projectDir,
|
|
255
|
+
configPath: probe.configPath,
|
|
256
|
+
outputDir: probe.outputDir,
|
|
257
|
+
configLoadError: probe.loadError,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function handleSetupConfig(state, body) {
|
|
261
|
+
if (!state.setupReason) {
|
|
262
|
+
throw new StudioHttpError(409, "Studio is already set up — edit askdb.config.ts directly.");
|
|
263
|
+
}
|
|
264
|
+
const input = parseSetupConfigBody(body);
|
|
265
|
+
const result = writeSetupConfig(process.cwd(), input);
|
|
266
|
+
// Config now exists; the artifact usually doesn't yet.
|
|
267
|
+
const probe = probeSetupState(process.cwd());
|
|
268
|
+
if (probe.hasArtifact && probe.outputDir) {
|
|
269
|
+
state.schemaDir = probe.outputDir;
|
|
270
|
+
state.workspace = loadWorkspace(probe.outputDir);
|
|
271
|
+
state.setupReason = null;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
state.setupReason = "no-artifact";
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
configPath: result.configPath,
|
|
278
|
+
envExamplePath: result.envExamplePath,
|
|
279
|
+
envVars: result.envVars,
|
|
280
|
+
installed: result.installed,
|
|
281
|
+
manualInstallCommand: result.manualInstallCommand,
|
|
282
|
+
packageJsonCreated: result.packageJsonCreated,
|
|
283
|
+
status: buildSetupStatus(state),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
async function handleSetupIntrospect(state) {
|
|
287
|
+
if (!state.setupReason) {
|
|
288
|
+
throw new StudioHttpError(409, "Studio is already set up — use Resync schema instead.");
|
|
289
|
+
}
|
|
290
|
+
// probeSetupState re-bootstraps, picking up any .env created since server start.
|
|
291
|
+
const probe = probeSetupState(process.cwd());
|
|
292
|
+
if (!probe.hasConfig) {
|
|
293
|
+
throw new StudioHttpError(409, "Write askdb.config.ts first (setup step 1).");
|
|
294
|
+
}
|
|
295
|
+
if (probe.loadError || !probe.outputDir) {
|
|
296
|
+
throw new StudioHttpError(409, `askdb.config.ts exists but could not be loaded: ${probe.loadError ?? "unknown error"}. ` +
|
|
297
|
+
"Install the project dependencies shown in setup step 1, then retry.");
|
|
298
|
+
}
|
|
299
|
+
const outDir = probe.outputDir;
|
|
300
|
+
const run = await runStudioIntrospection({
|
|
301
|
+
outDir,
|
|
302
|
+
hasExistingArtifact: existsSync(join(outDir, "schema.json")),
|
|
303
|
+
});
|
|
304
|
+
state.schemaDir = outDir;
|
|
305
|
+
state.workspace = loadWorkspace(outDir);
|
|
306
|
+
state.setupReason = null;
|
|
307
|
+
return {
|
|
308
|
+
ok: true,
|
|
309
|
+
engine: run.engine,
|
|
310
|
+
schemaId: run.schemaId,
|
|
311
|
+
tables: run.tables,
|
|
312
|
+
warnings: run.warnings,
|
|
313
|
+
workspace: serializeWorkspace(state.workspace),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async function handleResync(state) {
|
|
317
|
+
const workspace = requireWorkspace(state);
|
|
318
|
+
// Refresh the runtime snapshot so config edits made while Studio runs are honored.
|
|
319
|
+
try {
|
|
320
|
+
bootstrapAskDbEnv({ cwd: process.cwd() });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
// Keep the snapshot the server started with when the config disappeared mid-session.
|
|
324
|
+
}
|
|
325
|
+
const run = await runStudioIntrospection({
|
|
326
|
+
outDir: state.schemaDir,
|
|
327
|
+
schemaId: workspace.physical.schemaId,
|
|
328
|
+
hasExistingArtifact: true,
|
|
329
|
+
});
|
|
330
|
+
state.workspace = loadWorkspace(state.schemaDir);
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
engine: run.engine,
|
|
334
|
+
schemaId: run.schemaId,
|
|
335
|
+
tables: run.tables,
|
|
336
|
+
warnings: run.warnings,
|
|
337
|
+
workspace: serializeWorkspace(state.workspace),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function buildIntrospectionPlanDto() {
|
|
341
|
+
const plan = resolveStudioIntrospectionPlan();
|
|
342
|
+
return plan.ok
|
|
343
|
+
? { ok: true, engine: plan.engine, sourceLabel: plan.sourceLabel }
|
|
344
|
+
: { ok: false, engine: plan.engine, error: plan.error };
|
|
345
|
+
}
|
|
346
|
+
function parseSetupConfigBody(body) {
|
|
347
|
+
if (!isRecord(body)) {
|
|
348
|
+
throw new StudioHttpError(400, "Request body must be a JSON object.");
|
|
349
|
+
}
|
|
350
|
+
const databases = ["postgres", "mysql", "sqlite", "sqlserver", "prisma"];
|
|
351
|
+
const aiProviders = ["openai", "anthropic", "google", "azure"];
|
|
352
|
+
if (typeof body.database !== "string" || !databases.includes(body.database)) {
|
|
353
|
+
throw new StudioHttpError(400, `\`database\` must be one of: ${databases.join(", ")}.`);
|
|
354
|
+
}
|
|
355
|
+
if (typeof body.aiProvider !== "string" ||
|
|
356
|
+
!aiProviders.includes(body.aiProvider)) {
|
|
357
|
+
throw new StudioHttpError(400, `\`aiProvider\` must be one of: ${aiProviders.join(", ")}.`);
|
|
358
|
+
}
|
|
359
|
+
const optionalString = (key) => {
|
|
360
|
+
const value = body[key];
|
|
361
|
+
if (value === undefined || value === null || value === "")
|
|
362
|
+
return undefined;
|
|
363
|
+
if (typeof value !== "string")
|
|
364
|
+
throw new StudioHttpError(400, `\`${key}\` must be a string.`);
|
|
365
|
+
return value;
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
database: body.database,
|
|
369
|
+
aiProvider: body.aiProvider,
|
|
370
|
+
connectionEnv: optionalString("connectionEnv"),
|
|
371
|
+
sqliteFile: optionalString("sqliteFile"),
|
|
372
|
+
prismaSchema: optionalString("prismaSchema"),
|
|
373
|
+
aiKeyEnv: optionalString("aiKeyEnv"),
|
|
374
|
+
schemaOut: optionalString("schemaOut"),
|
|
176
375
|
};
|
|
177
376
|
}
|
|
178
377
|
function saveConceptsDraft(state, concepts) {
|
|
179
|
-
saveConcepts(state
|
|
378
|
+
saveConcepts(requireWorkspace(state), { concepts });
|
|
180
379
|
state.workspace = loadWorkspace(state.schemaDir);
|
|
181
380
|
}
|
|
182
381
|
function saveTenantPolicy(state, frontmatter, body) {
|
|
@@ -197,10 +396,11 @@ function parseTenantPolicyBody(body) {
|
|
|
197
396
|
return { frontmatter: parsed.data, bodyText };
|
|
198
397
|
}
|
|
199
398
|
function saveDraft(state, tableId, draft) {
|
|
200
|
-
const
|
|
399
|
+
const workspace = requireWorkspace(state);
|
|
400
|
+
const table = workspace.tables.find((candidate) => candidate.physical.id === tableId);
|
|
201
401
|
if (!table)
|
|
202
402
|
throw new StudioHttpError(404, `No such table: ${tableId}`);
|
|
203
|
-
const frontmatter = buildFrontmatter(table.physical,
|
|
403
|
+
const frontmatter = buildFrontmatter(table.physical, workspace.physical.schemaId, draft);
|
|
204
404
|
let body = table.parsed
|
|
205
405
|
? replaceTableDescription(table.parsed.body, draft.description)
|
|
206
406
|
: buildDefaultTableBody(table.physical.name, draft.description);
|
|
@@ -210,7 +410,7 @@ function saveDraft(state, tableId, draft) {
|
|
|
210
410
|
if (draft.exampleQuestions !== undefined) {
|
|
211
411
|
body = replaceH2Section(body, "Example questions", draft.exampleQuestions);
|
|
212
412
|
}
|
|
213
|
-
saveTable(
|
|
413
|
+
saveTable(workspace, tableId, frontmatter, body);
|
|
214
414
|
state.workspace = loadWorkspace(state.schemaDir);
|
|
215
415
|
}
|
|
216
416
|
async function suggestForSource(workspace, source) {
|
|
@@ -229,7 +429,7 @@ async function suggestTenantPolicyDraft(state) {
|
|
|
229
429
|
throw new StudioHttpError(400, ai.keyMissingMessage("AI tenant policy suggestion"));
|
|
230
430
|
}
|
|
231
431
|
// Build a compact DDL-like representation of the schema for the prompt
|
|
232
|
-
const workspace = state
|
|
432
|
+
const workspace = requireWorkspace(state);
|
|
233
433
|
const schemaId = workspace.physical.schemaId;
|
|
234
434
|
const schemaDdl = workspace.tables
|
|
235
435
|
.map((t) => {
|