@malloy-publisher/server 0.0.226 → 0.0.228
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.docker.md +8 -8
- package/README.md +2 -11
- package/dist/app/assets/{EnvironmentPage-DvOJ7L_b.js → EnvironmentPage-EW2lbGvb.js} +1 -1
- package/dist/app/assets/{HomePage-CXguJsXS.js → HomePage-Bkwc9Woc.js} +1 -1
- package/dist/app/assets/{LightMode-ZsshUznu.js → LightMode-Bum_KBpN.js} +1 -1
- package/dist/app/assets/{MainPage-BIe0VwBa.js → MainPage-oiEy7TNM.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-BuZ6UJVx.js → MaterializationsPage-C_VJsTgU.js} +1 -1
- package/dist/app/assets/{ModelPage-DsPf-s8B.js → ModelPage-z8REqAmk.js} +1 -1
- package/dist/app/assets/{PackagePage-CEVNAKZa.js → PackagePage-C2Vtt1Ln.js} +1 -1
- package/dist/app/assets/{RouteError-Chn7lL96.js → RouteError-DmJLpLXm.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DWC_FdNU.js → ThemeEditorPage-BywFjC7A.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CGrsFz8p.js → WorkbookPage-DCMizDMR.js} +1 -1
- package/dist/app/assets/{core-vVgoO8IR.es-BD_THWs_.js → core-CEDZMHV1.es-_yGzNgNH.js} +1 -1
- package/dist/app/assets/{index-D6YtyiJ0.js → index-CE9xhdra.js} +1 -1
- package/dist/app/assets/{index-BioohWQj.js → index-CdmFub34.js} +1 -1
- package/dist/app/assets/{index-gEWxu09x.js → index-DDMrjIT3.js} +1 -1
- package/dist/app/assets/{index-DNUZpnaa.js → index-EqslXZ44.js} +4 -4
- package/dist/app/index.html +1 -1
- package/dist/default-publisher.config.json +7 -7
- package/dist/package_load_worker.mjs +86 -24
- package/dist/runtime/publisher.js +5 -0
- package/dist/server.mjs +1415 -7876
- package/package.json +1 -4
- package/publisher.config.example.bigquery.json +7 -7
- package/publisher.config.example.duckdb.json +7 -7
- package/publisher.config.json +7 -11
- package/src/config.spec.ts +2 -2
- package/src/controller/package.controller.ts +62 -31
- package/src/default-publisher.config.json +7 -7
- package/src/health.ts +3 -8
- package/src/mcp/error_messages.ts +8 -37
- package/src/mcp/handler_utils.spec.ts +108 -0
- package/src/mcp/handler_utils.ts +99 -124
- package/src/mcp/mcp_constants.ts +0 -16
- package/src/mcp/server.protocol.spec.ts +138 -0
- package/src/mcp/server.ts +57 -37
- package/src/mcp/skills/build_skills_bundle.ts +1 -1
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/tools/compile_tool.spec.ts +207 -0
- package/src/mcp/tools/compile_tool.ts +177 -0
- package/src/mcp/tools/docs_search_tool.ts +1 -1
- package/src/mcp/tools/execute_query_tool.spec.ts +143 -0
- package/src/mcp/tools/execute_query_tool.ts +39 -6
- package/src/mcp/tools/get_context_eval.ts +3 -3
- package/src/mcp/tools/get_context_tool.spec.ts +196 -1
- package/src/mcp/tools/get_context_tool.ts +165 -67
- package/src/mcp/tools/reload_package_tool.spec.ts +232 -0
- package/src/mcp/tools/reload_package_tool.ts +158 -0
- package/src/package_load/package_load_pool.ts +3 -0
- package/src/package_load/package_load_worker.ts +68 -24
- package/src/package_load/protocol.ts +16 -0
- package/src/package_load/rpc_wait_accountant.spec.ts +109 -0
- package/src/package_load/rpc_wait_accountant.ts +76 -0
- package/src/package_load_metrics.spec.ts +114 -0
- package/src/package_load_metrics.ts +127 -0
- package/src/pg_helpers.spec.ts +2 -206
- package/src/pg_helpers.ts +4 -120
- package/src/runtime/publisher.js +5 -0
- package/src/server.ts +7 -21
- package/src/service/environment.ts +71 -7
- package/src/service/model.spec.ts +92 -0
- package/src/service/model.ts +58 -7
- package/src/service/package.ts +113 -55
- package/src/service/package_reload_safety.spec.ts +193 -0
- package/src/test_helpers/metrics_harness.ts +40 -0
- package/tests/fixtures/query-givens/data/orders.csv +7 -0
- package/tests/fixtures/query-givens/model.malloy +34 -0
- package/tests/fixtures/query-givens/publisher.json +5 -0
- package/tests/harness/mcp_test_setup.ts +1 -1
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +22 -22
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +7 -31
- package/tests/integration/query_givens/query_givens.integration.spec.ts +146 -0
- package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +121 -0
- package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +110 -0
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -6
- package/dxt/malloy_bridge.py +0 -354
- package/dxt/manifest.json +0 -22
- package/src/dto/connection.dto.spec.ts +0 -186
- package/src/dto/connection.dto.ts +0 -308
- package/src/dto/index.ts +0 -2
- package/src/dto/package.dto.spec.ts +0 -42
- package/src/dto/package.dto.ts +0 -27
- package/src/dto/validate.spec.ts +0 -76
- package/src/dto/validate.ts +0 -31
- package/src/ducklake_version.spec.ts +0 -43
- package/src/ducklake_version.ts +0 -26
- package/src/mcp/agent_server.protocol.spec.ts +0 -78
- package/src/mcp/agent_server.spec.ts +0 -18
- package/src/mcp/agent_server.ts +0 -144
- package/src/mcp/prompts/handlers.ts +0 -84
- package/src/mcp/prompts/index.ts +0 -11
- package/src/mcp/prompts/prompt_definitions.ts +0 -160
- package/src/mcp/prompts/prompt_service.ts +0 -67
- package/src/mcp/prompts/utils.ts +0 -62
- package/src/mcp/resource_metadata.ts +0 -47
- package/src/mcp/resources/environment_resource.ts +0 -187
- package/src/mcp/resources/model_resource.ts +0 -155
- package/src/mcp/resources/notebook_resource.ts +0 -137
- package/src/mcp/resources/package_resource.ts +0 -373
- package/src/mcp/resources/query_resource.ts +0 -122
- package/src/mcp/resources/source_resource.ts +0 -141
- package/src/mcp/resources/view_resource.ts +0 -136
- package/src/mcp/tools/discovery_tools.ts +0 -280
- package/src/storage/BaseRepository.ts +0 -31
- package/src/storage/StorageManager.mock.ts +0 -50
- package/tests/harness/e2e.ts +0 -96
- package/tests/harness/mocks.ts +0 -39
- package/tests/harness/uris.ts +0 -31
- package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -655
- package/tests/integration/mcp/setup.spec.ts +0 -5
- package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
- package/tests/unit/mcp/prompt_happy.test.ts +0 -51
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.228",
|
|
5
5
|
"main": "dist/server.mjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.mjs"
|
|
@@ -58,8 +58,6 @@
|
|
|
58
58
|
"aws-sdk": "^2.1692.0",
|
|
59
59
|
"body-parser": "^1.20.2",
|
|
60
60
|
"chokidar": "^4.0.3",
|
|
61
|
-
"class-transformer": "^0.5.1",
|
|
62
|
-
"class-validator": "^0.14.1",
|
|
63
61
|
"cors": "^2.8.5",
|
|
64
62
|
"express": "^4.21.0",
|
|
65
63
|
"extract-zip": "^2.0.1",
|
|
@@ -74,7 +72,6 @@
|
|
|
74
72
|
"uuid": "^11.0.3"
|
|
75
73
|
},
|
|
76
74
|
"devDependencies": {
|
|
77
|
-
"@anthropic-ai/dxt": "^0.1.0",
|
|
78
75
|
"@eslint/compat": "^1.2.7",
|
|
79
76
|
"@eslint/eslintrc": "^3.3.1",
|
|
80
77
|
"@eslint/js": "^9.23.0",
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
"frozenConfig": false,
|
|
3
3
|
"environments": [
|
|
4
4
|
{
|
|
5
|
-
"name": "
|
|
5
|
+
"name": "examples",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"location": "https://github.com/
|
|
8
|
+
"name": "storefront",
|
|
9
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/storefront"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
"name": "
|
|
13
|
-
"location": "https://github.com/
|
|
12
|
+
"name": "governed-analytics",
|
|
13
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/governed-analytics"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"name": "
|
|
17
|
-
"location": "https://github.com/
|
|
16
|
+
"name": "html-data-app",
|
|
17
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/html-data-app"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"name": "bigquery-hackernews",
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
"frozenConfig": false,
|
|
3
3
|
"environments": [
|
|
4
4
|
{
|
|
5
|
-
"name": "
|
|
5
|
+
"name": "examples",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"location": "https://github.com/
|
|
8
|
+
"name": "storefront",
|
|
9
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/storefront"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
"name": "
|
|
13
|
-
"location": "https://github.com/
|
|
12
|
+
"name": "governed-analytics",
|
|
13
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/governed-analytics"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"name": "
|
|
17
|
-
"location": "https://github.com/
|
|
16
|
+
"name": "html-data-app",
|
|
17
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/html-data-app"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"connections": []
|
package/publisher.config.json
CHANGED
|
@@ -2,23 +2,19 @@
|
|
|
2
2
|
"frozenConfig": false,
|
|
3
3
|
"environments": [
|
|
4
4
|
{
|
|
5
|
-
"name": "
|
|
5
|
+
"name": "examples",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"location": "
|
|
8
|
+
"name": "storefront",
|
|
9
|
+
"location": "../../examples/storefront"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
"name": "
|
|
13
|
-
"location": "
|
|
12
|
+
"name": "governed-analytics",
|
|
13
|
+
"location": "../../examples/governed-analytics"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"name": "
|
|
17
|
-
"location": "
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"name": "faa-givens-demo",
|
|
21
|
-
"location": "https://github.com/credibledata/malloy-samples/tree/main/faa-givens-demo"
|
|
16
|
+
"name": "html-data-app",
|
|
17
|
+
"location": "../../examples/html-data-app"
|
|
22
18
|
}
|
|
23
19
|
],
|
|
24
20
|
"connections": []
|
package/src/config.spec.ts
CHANGED
|
@@ -1040,8 +1040,8 @@ describe("Config path resolution (--config and bundled default)", () => {
|
|
|
1040
1040
|
|
|
1041
1041
|
expect(result.environments.length).toBeGreaterThan(0);
|
|
1042
1042
|
const env = result.environments[0];
|
|
1043
|
-
expect(env.name).toBe("
|
|
1044
|
-
expect(env.packages.some((p) => p.name === "
|
|
1043
|
+
expect(env.name).toBe("examples");
|
|
1044
|
+
expect(env.packages.some((p) => p.name === "storefront")).toBe(true);
|
|
1045
1045
|
expect(env.packages.some((p) => p.name === "bigquery-hackernews")).toBe(
|
|
1046
1046
|
false,
|
|
1047
1047
|
);
|
|
@@ -5,6 +5,13 @@ import { EnvironmentStore } from "../service/environment_store";
|
|
|
5
5
|
|
|
6
6
|
type ApiPackage = components["schemas"]["Package"];
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Which path a reload took. `in-place` recompiles the tree already on disk and
|
|
10
|
+
* leaves it alone; `reinstalled` re-fetches from the package's install location,
|
|
11
|
+
* which overwrites on-disk edits.
|
|
12
|
+
*/
|
|
13
|
+
export type PackageReloadMode = "in-place" | "reinstalled";
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Everything that is strict-at-publish, joined into one 400 message (or
|
|
10
17
|
* undefined when the package is publishable): invalid explores entries plus
|
|
@@ -50,45 +57,69 @@ export class PackageController {
|
|
|
50
57
|
packageName: string,
|
|
51
58
|
reload: boolean,
|
|
52
59
|
): Promise<ApiPackage> {
|
|
60
|
+
if (reload) {
|
|
61
|
+
return (await this.reloadPackage(environmentName, packageName))
|
|
62
|
+
.metadata;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
const environment = await this.environmentStore.getEnvironment(
|
|
54
66
|
environmentName,
|
|
55
67
|
false,
|
|
56
68
|
);
|
|
57
|
-
|
|
58
|
-
if (reload) {
|
|
59
|
-
// Resolve the package's source location from the currently-cached
|
|
60
|
-
// metadata WITHOUT triggering a stale-state reload. If a `location`
|
|
61
|
-
// is set, route the reload through `installPackage` so that
|
|
62
|
-
// download-then-load happens atomically; otherwise fall back to an
|
|
63
|
-
// in-place reload of the existing on-disk content.
|
|
64
|
-
let location: string | undefined;
|
|
65
|
-
try {
|
|
66
|
-
const cached = await environment.getPackage(packageName, false);
|
|
67
|
-
location = cached.getPackageMetadata().location;
|
|
68
|
-
} catch {
|
|
69
|
-
// Not previously loaded — nothing to reinstall from.
|
|
70
|
-
}
|
|
71
|
-
if (location) {
|
|
72
|
-
const reinstalled = await environment.installPackage(
|
|
73
|
-
packageName,
|
|
74
|
-
(stagingPath) =>
|
|
75
|
-
this.downloadInto(
|
|
76
|
-
environmentName,
|
|
77
|
-
packageName,
|
|
78
|
-
location,
|
|
79
|
-
stagingPath,
|
|
80
|
-
),
|
|
81
|
-
);
|
|
82
|
-
return reinstalled.getPackageMetadata();
|
|
83
|
-
}
|
|
84
|
-
const _package = await environment.getPackage(packageName, true);
|
|
85
|
-
return _package.getPackageMetadata();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
69
|
const _package = await environment.getPackage(packageName, false);
|
|
89
70
|
return _package.getPackageMetadata();
|
|
90
71
|
}
|
|
91
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Reload a package and report WHICH path ran. The two are not equivalent to
|
|
75
|
+
* anyone holding on-disk edits: an in-place reload recompiles the tree that
|
|
76
|
+
* is already there, while a reinstall re-fetches from the package's install
|
|
77
|
+
* location and overwrites it. A caller that surfaces the reload to a user
|
|
78
|
+
* needs to be able to say which happened, so it is returned rather than
|
|
79
|
+
* inferred. `getPackage` keeps returning bare metadata, so the REST contract
|
|
80
|
+
* is unchanged.
|
|
81
|
+
*/
|
|
82
|
+
public async reloadPackage(
|
|
83
|
+
environmentName: string,
|
|
84
|
+
packageName: string,
|
|
85
|
+
): Promise<{ metadata: ApiPackage; mode: PackageReloadMode }> {
|
|
86
|
+
const environment = await this.environmentStore.getEnvironment(
|
|
87
|
+
environmentName,
|
|
88
|
+
false,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Resolve the package's source location from the currently-cached
|
|
92
|
+
// metadata WITHOUT triggering a stale-state reload. If a `location`
|
|
93
|
+
// is set, route the reload through `installPackage` so that
|
|
94
|
+
// download-then-load happens atomically; otherwise fall back to an
|
|
95
|
+
// in-place reload of the existing on-disk content.
|
|
96
|
+
let location: string | undefined;
|
|
97
|
+
try {
|
|
98
|
+
const cached = await environment.getPackage(packageName, false);
|
|
99
|
+
location = cached.getPackageMetadata().location;
|
|
100
|
+
} catch {
|
|
101
|
+
// Not previously loaded, so there is nothing to reinstall from.
|
|
102
|
+
}
|
|
103
|
+
if (location) {
|
|
104
|
+
const reinstalled = await environment.installPackage(
|
|
105
|
+
packageName,
|
|
106
|
+
(stagingPath) =>
|
|
107
|
+
this.downloadInto(
|
|
108
|
+
environmentName,
|
|
109
|
+
packageName,
|
|
110
|
+
location,
|
|
111
|
+
stagingPath,
|
|
112
|
+
),
|
|
113
|
+
);
|
|
114
|
+
return {
|
|
115
|
+
metadata: reinstalled.getPackageMetadata(),
|
|
116
|
+
mode: "reinstalled",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const _package = await environment.getPackage(packageName, true);
|
|
120
|
+
return { metadata: _package.getPackageMetadata(), mode: "in-place" };
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
async addPackage(environmentName: string, body: ApiPackage) {
|
|
93
124
|
if (this.environmentStore.publisherConfigIsFrozen) {
|
|
94
125
|
throw new FrozenConfigError();
|
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
"frozenConfig": false,
|
|
3
3
|
"environments": [
|
|
4
4
|
{
|
|
5
|
-
"name": "
|
|
5
|
+
"name": "examples",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"location": "https://github.com/
|
|
8
|
+
"name": "storefront",
|
|
9
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/storefront"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
"name": "
|
|
13
|
-
"location": "https://github.com/
|
|
12
|
+
"name": "governed-analytics",
|
|
13
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/governed-analytics"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
"name": "
|
|
17
|
-
"location": "https://github.com/
|
|
16
|
+
"name": "html-data-app",
|
|
17
|
+
"location": "https://github.com/malloydata/publisher/tree/main/examples/html-data-app"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
20
|
"connections": []
|
package/src/health.ts
CHANGED
|
@@ -74,10 +74,10 @@ export function registerSignalHandlers(
|
|
|
74
74
|
mcpServer: Server,
|
|
75
75
|
shutdownDrainDurationSeconds: number = 0,
|
|
76
76
|
shutdownGracefulCloseTimeoutSeconds: number = 0,
|
|
77
|
-
agentMcpServer?: Server,
|
|
78
77
|
): void {
|
|
79
|
-
//
|
|
80
|
-
// K8s will SIGKILL after terminationGracePeriodSeconds
|
|
78
|
+
// On SIGTERM, drain in-flight requests before closing the servers (see the
|
|
79
|
+
// shutdown sequence above). K8s will SIGKILL after terminationGracePeriodSeconds
|
|
80
|
+
// if this handler does not exit first.
|
|
81
81
|
process.once("SIGTERM", async () => {
|
|
82
82
|
logger.info("========== SIGTERM RECEIVED ==========");
|
|
83
83
|
markNotReady();
|
|
@@ -97,7 +97,6 @@ export function registerSignalHandlers(
|
|
|
97
97
|
server,
|
|
98
98
|
mcpServer,
|
|
99
99
|
shutdownGracefulCloseTimeoutSeconds,
|
|
100
|
-
agentMcpServer,
|
|
101
100
|
);
|
|
102
101
|
});
|
|
103
102
|
}
|
|
@@ -116,7 +115,6 @@ export async function performGracefulShutdownAfterDrain(
|
|
|
116
115
|
server: Server,
|
|
117
116
|
mcpServer: Server,
|
|
118
117
|
shutdownGracefulCloseTimeoutSeconds: number,
|
|
119
|
-
agentMcpServer?: Server,
|
|
120
118
|
): Promise<void> {
|
|
121
119
|
const closeServer = (server: Server, name: string) =>
|
|
122
120
|
new Promise<void>((resolve) => {
|
|
@@ -137,9 +135,6 @@ export async function performGracefulShutdownAfterDrain(
|
|
|
137
135
|
await Promise.all([
|
|
138
136
|
closeServer(server, "Main server"),
|
|
139
137
|
closeServer(mcpServer, "MCP server"),
|
|
140
|
-
...(agentMcpServer
|
|
141
|
-
? [closeServer(agentMcpServer, "Agent MCP server")]
|
|
142
|
-
: []),
|
|
143
138
|
]);
|
|
144
139
|
|
|
145
140
|
try {
|
|
@@ -3,31 +3,6 @@ export interface ErrorDetails {
|
|
|
3
3
|
suggestions: string[];
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
export interface InvalidParamDetail {
|
|
7
|
-
name: string;
|
|
8
|
-
reason: string;
|
|
9
|
-
value?: unknown;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function getInvalidParamsError(
|
|
13
|
-
params: InvalidParamDetail[],
|
|
14
|
-
): ErrorDetails {
|
|
15
|
-
const reasons = params
|
|
16
|
-
.map(
|
|
17
|
-
(p) =>
|
|
18
|
-
`${p.name}: ${p.reason}${p.value !== undefined ? ` (value: ${JSON.stringify(p.value)})` : ""}`,
|
|
19
|
-
)
|
|
20
|
-
.join("; ");
|
|
21
|
-
return {
|
|
22
|
-
message: `Invalid parameter(s) provided. ${reasons}`,
|
|
23
|
-
suggestions: [
|
|
24
|
-
"Check the parameter names and values against the prompt's argument definition.",
|
|
25
|
-
"Ensure all required parameters are provided and have the correct data types.",
|
|
26
|
-
"For URI parameters, ensure they are correctly formatted.",
|
|
27
|
-
],
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
6
|
/**
|
|
32
7
|
* Generates error details for a resource not found scenario.
|
|
33
8
|
* @param resourceUriOrContext The URI that was not found, or a context string (e.g., "Package 'X'", "Model 'Y' in package 'X'").
|
|
@@ -36,13 +11,9 @@ export function getInvalidParamsError(
|
|
|
36
11
|
export function getNotFoundError(resourceUriOrContext: string): ErrorDetails {
|
|
37
12
|
const baseMessage = `Resource not found: ${resourceUriOrContext}`;
|
|
38
13
|
const suggestions: string[] = [
|
|
39
|
-
`Verify the identifier
|
|
40
|
-
`If using a URI, ensure it follows the correct format (e.g., malloy://environment/...) and includes the right path segments (e.g., /models/, /sources/, /queries/, /views/).`,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
suggestions.push(
|
|
14
|
+
`Verify the identifier (${resourceUriOrContext}) is spelled correctly and exists. Check capitalization and path separators.`,
|
|
44
15
|
"Check if the resource exists and is correctly named in your Malloy environment structure or the specific model file.",
|
|
45
|
-
|
|
16
|
+
];
|
|
46
17
|
|
|
47
18
|
return {
|
|
48
19
|
message: baseMessage,
|
|
@@ -52,7 +23,7 @@ export function getNotFoundError(resourceUriOrContext: string): ErrorDetails {
|
|
|
52
23
|
|
|
53
24
|
/**
|
|
54
25
|
* Generates generic error details for internal server errors.
|
|
55
|
-
* @param operation The operation that failed (e.g., '
|
|
26
|
+
* @param operation The operation that failed (e.g., 'executeQuery').
|
|
56
27
|
* @param error Optional: The underlying error object or message.
|
|
57
28
|
* @returns ErrorDetails object.
|
|
58
29
|
*/
|
|
@@ -73,7 +44,7 @@ export function getInternalError(
|
|
|
73
44
|
|
|
74
45
|
/**
|
|
75
46
|
* Generates detailed error information for Malloy compilation or query execution errors.
|
|
76
|
-
* @param operation The operation that failed (e.g., '
|
|
47
|
+
* @param operation The operation that failed (e.g., 'executeQuery (load model)', 'executeQuery').
|
|
77
48
|
* @param modelIdentifier A path or URI identifying the model/notebook involved.
|
|
78
49
|
* @param error The underlying Malloy error object or other error.
|
|
79
50
|
* @returns ErrorDetails object.
|
|
@@ -144,21 +115,21 @@ export function getMalloyErrorDetails(
|
|
|
144
115
|
refined = true;
|
|
145
116
|
const [, viewName, sourceName] = viewNotFoundMatch;
|
|
146
117
|
suggestions.unshift(
|
|
147
|
-
`Suggestion: View '${viewName}' was not found in source '${sourceName}'. Check the view name spelling or
|
|
118
|
+
`Suggestion: View '${viewName}' was not found in source '${sourceName}'. Check the view name spelling or call malloy_getContext with sourceName '${sourceName}' to see the list of available views. Views are defined within sources like 'source: ${sourceName} is ... extend { view: ${viewName} is { ... } }'.`,
|
|
148
119
|
);
|
|
149
120
|
} else if (sourceNotFoundMatch) {
|
|
150
121
|
refined = true;
|
|
151
122
|
const [, sourceName] = sourceNotFoundMatch;
|
|
152
123
|
suggestions.unshift(
|
|
153
124
|
`Suggestion: Source '${sourceName}' was not found or could not be accessed. Verify its definition (e.g., \`source: ${sourceName} is table('...')\` or \`duckdb.sql("...")\`) and ensure any associated connections are valid.`,
|
|
154
|
-
`Suggestion: Check the spelling of '${sourceName}'. You can list sources in the
|
|
125
|
+
`Suggestion: Check the spelling of '${sourceName}'. You can list the sources in the package using malloy_getContext.`,
|
|
155
126
|
);
|
|
156
127
|
} else if (queryNotFoundMatch) {
|
|
157
128
|
refined = true;
|
|
158
129
|
const [, queryName] = queryNotFoundMatch;
|
|
159
130
|
suggestions.unshift(
|
|
160
131
|
`Suggestion: Named query '${queryName}' was not found. Verify its definition (e.g., \`query: ${queryName} is source_name -> { ... }\`) within the model '${modelIdentifier}'.`,
|
|
161
|
-
`Suggestion: Check the spelling of '${queryName}'. Ensure it's a named query (defined with \`query:\`), not a view. You can list named queries using
|
|
132
|
+
`Suggestion: Check the spelling of '${queryName}'. Ensure it's a named query (defined with \`query:\`), not a view. You can list named queries using malloy_getContext.`,
|
|
162
133
|
);
|
|
163
134
|
} else if (fieldNotFoundMatch) {
|
|
164
135
|
refined = true;
|
|
@@ -166,7 +137,7 @@ export function getMalloyErrorDetails(
|
|
|
166
137
|
fieldNotFoundMatch;
|
|
167
138
|
suggestions.unshift(
|
|
168
139
|
`Suggestion: Field '${fieldName}' was not found in ${fieldContextType} '${fieldContextName}'. Check the spelling of the field name within the definition of '${fieldContextName}'.`,
|
|
169
|
-
`Suggestion: Ensure the field is defined directly or inherited correctly in the '${fieldContextName}' ${fieldContextType}. You can inspect the ${fieldContextType}
|
|
140
|
+
`Suggestion: Ensure the field is defined directly or inherited correctly in the '${fieldContextName}' ${fieldContextType}. You can inspect the ${fieldContextType}'s fields using malloy_getContext.`,
|
|
170
141
|
);
|
|
171
142
|
} else if (referenceErrorMatch) {
|
|
172
143
|
refined = true;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { MalloyError } from "@malloydata/malloy";
|
|
3
|
+
import { classifyToolError } from "./handler_utils";
|
|
4
|
+
import {
|
|
5
|
+
AccessDeniedError,
|
|
6
|
+
BadRequestError,
|
|
7
|
+
ModelCompilationError,
|
|
8
|
+
PackageNotFoundError,
|
|
9
|
+
ServiceUnavailableError,
|
|
10
|
+
} from "../errors";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* classifyToolError decides what an agent is told to do about a failure, so
|
|
14
|
+
* every branch is pinned by the advice it produces, not just by "an error came
|
|
15
|
+
* back". Asserting only that an error exists is what let every class funnel
|
|
16
|
+
* through the Malloy helper unnoticed.
|
|
17
|
+
*
|
|
18
|
+
* The routing is by error CLASS on purpose. getMalloyErrorDetails refines on
|
|
19
|
+
* message patterns the shipped engine does not emit, so "did it refine?" would
|
|
20
|
+
* send real compile errors to the internal branch.
|
|
21
|
+
*/
|
|
22
|
+
describe("classifyToolError", () => {
|
|
23
|
+
const advice = (e: unknown) =>
|
|
24
|
+
JSON.stringify(classifyToolError("op", "env/pkg", e).suggestions);
|
|
25
|
+
|
|
26
|
+
it("homes a missing package as not-found, not as Malloy", () => {
|
|
27
|
+
const details = classifyToolError(
|
|
28
|
+
"op",
|
|
29
|
+
"env/pkg",
|
|
30
|
+
new PackageNotFoundError("Package 'nope' not found"),
|
|
31
|
+
);
|
|
32
|
+
expect(details.message).toContain("Resource not found");
|
|
33
|
+
expect(JSON.stringify(details.suggestions)).not.toContain("Malloy file");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("homes back-pressure as retryable, not as Malloy", () => {
|
|
37
|
+
const details = classifyToolError(
|
|
38
|
+
"op",
|
|
39
|
+
"env/pkg",
|
|
40
|
+
new ServiceUnavailableError("Memory limit reached"),
|
|
41
|
+
);
|
|
42
|
+
expect(details.message).toContain("Memory limit reached");
|
|
43
|
+
expect(JSON.stringify(details.suggestions)).toContain("Retry");
|
|
44
|
+
expect(JSON.stringify(details.suggestions)).not.toContain("Malloy file");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("keeps Malloy advice for a raw engine error", () => {
|
|
48
|
+
// The regression guard for executeQuery. A bad query throws MalloyError,
|
|
49
|
+
// NOT ModelCompilationError: the engine's error reaches the tool's catch
|
|
50
|
+
// unwrapped. Route it by the internal branch and a syntax error tells the
|
|
51
|
+
// agent an "unexpected internal error occurred", which is worse than the
|
|
52
|
+
// bug this classifier was written to fix.
|
|
53
|
+
const details = classifyToolError(
|
|
54
|
+
"executeQuery",
|
|
55
|
+
"env/pkg/m.malloy",
|
|
56
|
+
new MalloyError("unexpected '@'", []),
|
|
57
|
+
);
|
|
58
|
+
expect(details.message).not.toContain("unexpected internal error");
|
|
59
|
+
expect(advice(new MalloyError("unexpected '@'", []))).toContain("Malloy");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("keeps Malloy advice for a wrapped compile error", () => {
|
|
63
|
+
// What a failed reload throws. #890's promise is that a failed reload
|
|
64
|
+
// returns the compile errors, so this must not read as an internal fault.
|
|
65
|
+
const details = classifyToolError(
|
|
66
|
+
"reloadPackage",
|
|
67
|
+
"env/pkg",
|
|
68
|
+
new ModelCompilationError({
|
|
69
|
+
message: "Error(s) compiling model: unexpected '@'",
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
expect(details.message).not.toContain("unexpected internal error");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("keeps Malloy advice for an authorize denial and a malformed request", () => {
|
|
76
|
+
expect(
|
|
77
|
+
classifyToolError(
|
|
78
|
+
"op",
|
|
79
|
+
"env/pkg",
|
|
80
|
+
new AccessDeniedError('Access denied for source "orders"'),
|
|
81
|
+
).message,
|
|
82
|
+
).not.toContain("unexpected internal error");
|
|
83
|
+
expect(
|
|
84
|
+
classifyToolError(
|
|
85
|
+
"op",
|
|
86
|
+
"env/pkg",
|
|
87
|
+
new BadRequestError("Invalid query request."),
|
|
88
|
+
).message,
|
|
89
|
+
).not.toContain("unexpected internal error");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("reports anything else as internal rather than blaming the Malloy", () => {
|
|
93
|
+
// A bug in our own code, a filesystem failure, or a worker crash. Telling
|
|
94
|
+
// the agent to check its syntax sends it to edit a model that is fine.
|
|
95
|
+
for (const error of [
|
|
96
|
+
new TypeError("cannot read properties of undefined"),
|
|
97
|
+
Object.assign(new Error("EACCES: permission denied"), {
|
|
98
|
+
code: "EACCES",
|
|
99
|
+
}),
|
|
100
|
+
]) {
|
|
101
|
+
const details = classifyToolError("op", "env/pkg", error);
|
|
102
|
+
expect(details.message).toContain("unexpected internal error");
|
|
103
|
+
expect(JSON.stringify(details.suggestions)).not.toContain(
|
|
104
|
+
"Malloy file",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|