@malloy-publisher/server 0.0.181 → 0.0.183-dev
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/build.ts +7 -3
- package/dist/app/api-doc.yaml +505 -52
- package/dist/app/assets/HomePage-Dn3E4CuB.js +1 -0
- package/dist/app/assets/{MainPage-B53xidTF.js → MainPage-BzB3yoqi.js} +2 -2
- package/dist/app/assets/{ModelPage-UMuQe8qY.js → ModelPage-C9O_sAXT.js} +1 -1
- package/dist/app/assets/PackagePage-DcxKEjBX.js +1 -0
- package/dist/app/assets/ProjectPage-BDj307rF.js +1 -0
- package/dist/app/assets/{RouteError-Cv58zNpb.js → RouteError-DAShbVCG.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DZ1StqsX.js → WorkbookPage-Cs_XYEaB.js} +1 -1
- package/dist/app/assets/core-CjeTkq8O.es-BqRc6yhC.js +148 -0
- package/dist/app/assets/engine-oniguruma-C4vnmooL.es-jdkXmgTr.js +1 -0
- package/dist/app/assets/github-light-JYsPkUQd.es-DAi9KRSo.js +1 -0
- package/dist/app/assets/index-15BOvhp0.js +456 -0
- package/dist/app/assets/{index-DPThhVfX.js → index-Bb2jqquW.js} +1 -1
- package/dist/app/assets/{index-M3Zo817E.js → index-D68X76-7.js} +98 -98
- package/dist/app/assets/{index.umd-DnfBsVqO.js → index.umd-DGBekgSu.js} +1 -1
- package/dist/app/assets/json-71t8ZF9g.es-BQoSv7ci.js +1 -0
- package/dist/app/assets/sql-DCkt643-.es-COK4E0Yg.js +1 -0
- package/dist/app/assets/typescript-buWNZFwO.es-Dj6nwHGl.js +1 -0
- package/dist/app/index.html +1 -1
- package/dist/{instrumentation.js → instrumentation.mjs} +10567 -10584
- package/dist/{server.js → server.mjs} +16959 -15357
- package/package.json +19 -17
- package/src/controller/connection.controller.ts +27 -20
- package/src/controller/manifest.controller.ts +29 -0
- package/src/controller/materialization.controller.ts +125 -0
- package/src/controller/model.controller.ts +4 -3
- package/src/controller/package.controller.ts +53 -2
- package/src/controller/query.controller.ts +5 -0
- package/src/errors.ts +24 -0
- package/src/mcp/prompts/handlers.ts +1 -1
- package/src/mcp/resources/model_resource.ts +12 -9
- package/src/mcp/resources/source_resource.ts +7 -6
- package/src/mcp/resources/view_resource.ts +0 -1
- package/src/mcp/tools/execute_query_tool.ts +9 -0
- package/src/server.ts +223 -15
- package/src/service/connection.ts +1 -4
- package/src/service/filter.spec.ts +447 -0
- package/src/service/filter.ts +337 -0
- package/src/service/filter_integration.spec.ts +825 -0
- package/src/service/manifest_service.spec.ts +201 -0
- package/src/service/manifest_service.ts +106 -0
- package/src/service/materialization_service.spec.ts +648 -0
- package/src/service/materialization_service.ts +929 -0
- package/src/service/materialized_table_gc.spec.ts +383 -0
- package/src/service/materialized_table_gc.ts +279 -0
- package/src/service/model.ts +227 -49
- package/src/service/package.ts +50 -0
- package/src/service/project_store.ts +21 -2
- package/src/service/quoting.ts +41 -0
- package/src/service/resolve_project.ts +13 -0
- package/src/storage/DatabaseInterface.ts +103 -1
- package/src/storage/{StorageManager.spec.ts → StorageManager.mock.ts} +9 -0
- package/src/storage/StorageManager.ts +119 -1
- package/src/storage/duckdb/DuckDBConnection.ts +1 -1
- package/src/storage/duckdb/DuckDBManifestStore.ts +70 -0
- package/src/storage/duckdb/DuckDBRepository.ts +99 -9
- package/src/storage/duckdb/ManifestRepository.ts +119 -0
- package/src/storage/duckdb/MaterializationRepository.ts +249 -0
- package/src/storage/duckdb/manifest_store.spec.ts +133 -0
- package/src/storage/duckdb/schema.ts +59 -1
- package/src/storage/ducklake/DuckLakeManifestStore.ts +146 -0
- package/tests/fixtures/persist-test/data/orders.csv +5 -0
- package/tests/fixtures/persist-test/persist_test.malloy +11 -0
- package/tests/fixtures/persist-test/publisher.json +5 -0
- package/tests/fixtures/publisher.config.json +15 -0
- package/tests/harness/rest_e2e.ts +68 -0
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +470 -0
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +2 -2
- package/tsconfig.json +1 -1
- package/dist/app/assets/HomePage-B0C6gwGj.js +0 -1
- package/dist/app/assets/PackagePage-BEDvm_je.js +0 -1
- package/dist/app/assets/ProjectPage-DzN4P86H.js +0 -1
- package/dist/app/assets/index-D-xPyBUA.js +0 -467
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloy-publisher/server",
|
|
3
3
|
"description": "Malloy Publisher Server",
|
|
4
|
-
"version": "0.0.
|
|
5
|
-
"main": "dist/server.
|
|
4
|
+
"version": "0.0.183-dev",
|
|
5
|
+
"main": "dist/server.mjs",
|
|
6
6
|
"bin": {
|
|
7
|
-
"malloy-publisher": "dist/server.
|
|
7
|
+
"malloy-publisher": "dist/server.mjs"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -14,14 +14,16 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "bun test
|
|
17
|
+
"test": "bun run test:unit && bun run test:integration",
|
|
18
|
+
"test:unit": "bun test --timeout 100000 src",
|
|
19
|
+
"test:integration": "bun test --timeout 100000 tests",
|
|
18
20
|
"build": "bun generate-api-types && bun build:app && NODE_ENV=production bun run build.ts",
|
|
19
21
|
"build:server-only": "bun generate-api-types && NODE_ENV=production bun run build.ts",
|
|
20
|
-
"start": "NODE_ENV=production
|
|
21
|
-
"start:init": "NODE_ENV=production
|
|
22
|
+
"start": "NODE_ENV=production bun run ./dist/server.mjs",
|
|
23
|
+
"start:init": "NODE_ENV=production bun run ./dist/server.mjs --init",
|
|
22
24
|
"start:dev": "NODE_ENV=development bun --watch src/server.ts",
|
|
23
25
|
"start:dev:init": "NODE_ENV=development bun --watch src/server.ts --init",
|
|
24
|
-
"start:instrumented": "
|
|
26
|
+
"start:instrumented": "bun --preload ./dist/instrumentation.mjs ./dist/server.mjs",
|
|
25
27
|
"lint": "bunx eslint \"./src/**/*.ts\"",
|
|
26
28
|
"format": "bunx prettier --write '**/*.{ts,tsx}'",
|
|
27
29
|
"build:app": "cd ../app && NODE_ENV=production bunx vite build",
|
|
@@ -32,15 +34,15 @@
|
|
|
32
34
|
"@azure/identity": "^4.13.0",
|
|
33
35
|
"@azure/storage-blob": "^12.26.0",
|
|
34
36
|
"@google-cloud/storage": "^7.16.0",
|
|
35
|
-
"@malloydata/db-bigquery": "^0.0.
|
|
36
|
-
"@malloydata/db-duckdb": "^0.0.
|
|
37
|
-
"@malloydata/db-mysql": "^0.0.
|
|
38
|
-
"@malloydata/db-postgres": "^0.0.
|
|
39
|
-
"@malloydata/db-snowflake": "^0.0.
|
|
40
|
-
"@malloydata/db-trino": "^0.0.
|
|
41
|
-
"@malloydata/malloy": "^0.0.
|
|
42
|
-
"@malloydata/malloy-sql": "^0.0.
|
|
43
|
-
"@malloydata/render-validator": "^0.0.
|
|
37
|
+
"@malloydata/db-bigquery": "^0.0.377",
|
|
38
|
+
"@malloydata/db-duckdb": "^0.0.377",
|
|
39
|
+
"@malloydata/db-mysql": "^0.0.377",
|
|
40
|
+
"@malloydata/db-postgres": "^0.0.377",
|
|
41
|
+
"@malloydata/db-snowflake": "^0.0.377",
|
|
42
|
+
"@malloydata/db-trino": "^0.0.377",
|
|
43
|
+
"@malloydata/malloy": "^0.0.377",
|
|
44
|
+
"@malloydata/malloy-sql": "^0.0.377",
|
|
45
|
+
"@malloydata/render-validator": "^0.0.377",
|
|
44
46
|
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
45
47
|
"@opentelemetry/api": "^1.9.0",
|
|
46
48
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
@@ -56,7 +58,7 @@
|
|
|
56
58
|
"class-transformer": "^0.5.1",
|
|
57
59
|
"class-validator": "^0.14.1",
|
|
58
60
|
"cors": "^2.8.5",
|
|
59
|
-
"duckdb": "1.4.
|
|
61
|
+
"duckdb": "1.4.4",
|
|
60
62
|
"express": "^4.21.0",
|
|
61
63
|
"globals": "^15.9.0",
|
|
62
64
|
"handlebars": "^4.7.8",
|
|
@@ -141,7 +141,10 @@ export class ConnectionController {
|
|
|
141
141
|
if (!source) {
|
|
142
142
|
throw new ConnectionError(`Table ${tablePath} not found`);
|
|
143
143
|
}
|
|
144
|
-
|
|
144
|
+
// BigQueryConnection returns `error.message` as a string on failure instead of throwing.
|
|
145
|
+
if (typeof source === "string") {
|
|
146
|
+
throw new ConnectionError(source);
|
|
147
|
+
}
|
|
145
148
|
return {
|
|
146
149
|
source: JSON.stringify(source),
|
|
147
150
|
resource: tablePath,
|
|
@@ -173,11 +176,11 @@ export class ConnectionController {
|
|
|
173
176
|
if (!projectName || !connectionName) {
|
|
174
177
|
throw new BadRequestError("Connection payload is required");
|
|
175
178
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
return
|
|
179
|
+
// Prefer the in-memory API connection (which was materialized by the
|
|
180
|
+
// project on load and carries `attributes`). The DB row stores the
|
|
181
|
+
// raw config and FK columns, which aren't the ApiConnection shape.
|
|
182
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
183
|
+
return project.getApiConnection(connectionName);
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
public async listConnections(projectName: string): Promise<ApiConnection[]> {
|
|
@@ -231,22 +234,26 @@ export class ConnectionController {
|
|
|
231
234
|
projectName,
|
|
232
235
|
connectionName,
|
|
233
236
|
);
|
|
234
|
-
|
|
235
237
|
try {
|
|
238
|
+
const schema = await (
|
|
239
|
+
malloyConnection as Connection & {
|
|
240
|
+
fetchSelectSchema: (params: {
|
|
241
|
+
connection: string;
|
|
242
|
+
selectStr: string;
|
|
243
|
+
}) => Promise<unknown>;
|
|
244
|
+
}
|
|
245
|
+
).fetchSelectSchema({
|
|
246
|
+
connection: connectionName,
|
|
247
|
+
selectStr: sqlStatement,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// BigQueryConnection returns `error.message` as a string on failure instead of throwing.
|
|
251
|
+
if (typeof schema === "string") {
|
|
252
|
+
throw new ConnectionError(schema);
|
|
253
|
+
}
|
|
254
|
+
|
|
236
255
|
return {
|
|
237
|
-
source: JSON.stringify(
|
|
238
|
-
await (
|
|
239
|
-
malloyConnection as Connection & {
|
|
240
|
-
fetchSelectSchema: (params: {
|
|
241
|
-
connection: string;
|
|
242
|
-
selectStr: string;
|
|
243
|
-
}) => Promise<unknown>;
|
|
244
|
-
}
|
|
245
|
-
).fetchSelectSchema({
|
|
246
|
-
connection: connectionName,
|
|
247
|
-
selectStr: sqlStatement,
|
|
248
|
-
}),
|
|
249
|
-
),
|
|
256
|
+
source: JSON.stringify(schema),
|
|
250
257
|
};
|
|
251
258
|
} catch (error) {
|
|
252
259
|
throw new ConnectionError((error as Error).message);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ManifestService } from "../service/manifest_service";
|
|
2
|
+
import { ProjectStore } from "../service/project_store";
|
|
3
|
+
import { resolveProjectId } from "../service/resolve_project";
|
|
4
|
+
|
|
5
|
+
export class ManifestController {
|
|
6
|
+
constructor(
|
|
7
|
+
private projectStore: ProjectStore,
|
|
8
|
+
private manifestService: ManifestService,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
async getManifest(projectName: string, packageName: string) {
|
|
12
|
+
const repository = this.projectStore.storageManager.getRepository();
|
|
13
|
+
const projectId = await resolveProjectId(repository, projectName);
|
|
14
|
+
// Verify the package exists so we return 404 instead of an empty manifest.
|
|
15
|
+
const project = await this.projectStore.getProject(projectName, false);
|
|
16
|
+
await project.getPackage(packageName, false);
|
|
17
|
+
return this.manifestService.getManifest(projectId, packageName);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async reloadManifest(projectName: string, packageName: string) {
|
|
21
|
+
const repository = this.projectStore.storageManager.getRepository();
|
|
22
|
+
const projectId = await resolveProjectId(repository, projectName);
|
|
23
|
+
return this.manifestService.reloadManifest(
|
|
24
|
+
projectId,
|
|
25
|
+
packageName,
|
|
26
|
+
projectName,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { BadRequestError } from "../errors";
|
|
2
|
+
import { MaterializationService } from "../service/materialization_service";
|
|
3
|
+
|
|
4
|
+
export class MaterializationController {
|
|
5
|
+
constructor(private materializationService: MaterializationService) {}
|
|
6
|
+
|
|
7
|
+
async createMaterialization(
|
|
8
|
+
projectName: string,
|
|
9
|
+
packageName: string,
|
|
10
|
+
body: Record<string, unknown>,
|
|
11
|
+
) {
|
|
12
|
+
const options = this.validateCreateBody(body);
|
|
13
|
+
return this.materializationService.createMaterialization(
|
|
14
|
+
projectName,
|
|
15
|
+
packageName,
|
|
16
|
+
options,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private validateCreateBody(body: Record<string, unknown>): {
|
|
21
|
+
forceRefresh?: boolean;
|
|
22
|
+
autoLoadManifest?: boolean;
|
|
23
|
+
} {
|
|
24
|
+
const result: { forceRefresh?: boolean; autoLoadManifest?: boolean } = {};
|
|
25
|
+
if (body.forceRefresh !== undefined) {
|
|
26
|
+
if (typeof body.forceRefresh !== "boolean") {
|
|
27
|
+
throw new BadRequestError("forceRefresh must be a boolean");
|
|
28
|
+
}
|
|
29
|
+
result.forceRefresh = body.forceRefresh;
|
|
30
|
+
}
|
|
31
|
+
if (body.autoLoadManifest !== undefined) {
|
|
32
|
+
if (typeof body.autoLoadManifest !== "boolean") {
|
|
33
|
+
throw new BadRequestError("autoLoadManifest must be a boolean");
|
|
34
|
+
}
|
|
35
|
+
result.autoLoadManifest = body.autoLoadManifest;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async startMaterialization(
|
|
41
|
+
projectName: string,
|
|
42
|
+
packageName: string,
|
|
43
|
+
materializationId: string,
|
|
44
|
+
) {
|
|
45
|
+
return this.materializationService.startMaterialization(
|
|
46
|
+
projectName,
|
|
47
|
+
packageName,
|
|
48
|
+
materializationId,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async stopMaterialization(
|
|
53
|
+
projectName: string,
|
|
54
|
+
packageName: string,
|
|
55
|
+
materializationId: string,
|
|
56
|
+
) {
|
|
57
|
+
return this.materializationService.stopMaterialization(
|
|
58
|
+
projectName,
|
|
59
|
+
packageName,
|
|
60
|
+
materializationId,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async listMaterializations(
|
|
65
|
+
projectName: string,
|
|
66
|
+
packageName: string,
|
|
67
|
+
options?: { limit?: number; offset?: number },
|
|
68
|
+
) {
|
|
69
|
+
return this.materializationService.listMaterializations(
|
|
70
|
+
projectName,
|
|
71
|
+
packageName,
|
|
72
|
+
options,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getMaterialization(
|
|
77
|
+
projectName: string,
|
|
78
|
+
packageName: string,
|
|
79
|
+
materializationId: string,
|
|
80
|
+
) {
|
|
81
|
+
return this.materializationService.getMaterialization(
|
|
82
|
+
projectName,
|
|
83
|
+
packageName,
|
|
84
|
+
materializationId,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async deleteMaterialization(
|
|
89
|
+
projectName: string,
|
|
90
|
+
packageName: string,
|
|
91
|
+
materializationId: string,
|
|
92
|
+
) {
|
|
93
|
+
return this.materializationService.deleteMaterialization(
|
|
94
|
+
projectName,
|
|
95
|
+
packageName,
|
|
96
|
+
materializationId,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async teardownPackage(
|
|
101
|
+
projectName: string,
|
|
102
|
+
packageName: string,
|
|
103
|
+
body: Record<string, unknown>,
|
|
104
|
+
) {
|
|
105
|
+
const options = this.validateTeardownBody(body);
|
|
106
|
+
return this.materializationService.teardownPackage(
|
|
107
|
+
projectName,
|
|
108
|
+
packageName,
|
|
109
|
+
options,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private validateTeardownBody(body: Record<string, unknown>): {
|
|
114
|
+
dryRun?: boolean;
|
|
115
|
+
} {
|
|
116
|
+
const options: { dryRun?: boolean } = {};
|
|
117
|
+
if (body.dryRun !== undefined) {
|
|
118
|
+
if (typeof body.dryRun !== "boolean") {
|
|
119
|
+
throw new BadRequestError("dryRun must be a boolean");
|
|
120
|
+
}
|
|
121
|
+
options.dryRun = body.dryRun;
|
|
122
|
+
}
|
|
123
|
+
return options;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { components } from "../api";
|
|
2
2
|
import { ModelNotFoundError } from "../errors";
|
|
3
3
|
import { ProjectStore } from "../service/project_store";
|
|
4
|
+
import type { FilterParams } from "../service/filter";
|
|
4
5
|
|
|
5
6
|
type ApiNotebook = components["schemas"]["Notebook"];
|
|
6
7
|
type ApiModel = components["schemas"]["Model"];
|
|
7
8
|
type ApiCompiledModel = components["schemas"]["CompiledModel"];
|
|
8
9
|
type ApiRawNotebook = components["schemas"]["RawNotebook"];
|
|
9
|
-
export type ListModelsFilterEnum =
|
|
10
|
-
components["parameters"]["ListModelsFilterEnum"];
|
|
11
10
|
export class ModelController {
|
|
12
11
|
private projectStore: ProjectStore;
|
|
13
12
|
|
|
@@ -84,6 +83,8 @@ export class ModelController {
|
|
|
84
83
|
packageName: string,
|
|
85
84
|
notebookPath: string,
|
|
86
85
|
cellIndex: number,
|
|
86
|
+
filterParams?: FilterParams,
|
|
87
|
+
bypassFilters?: boolean,
|
|
87
88
|
): Promise<{
|
|
88
89
|
type: "code" | "markdown";
|
|
89
90
|
text: string;
|
|
@@ -101,6 +102,6 @@ export class ModelController {
|
|
|
101
102
|
throw new ModelNotFoundError(`${notebookPath} is a model`);
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
return model.executeNotebookCell(cellIndex);
|
|
105
|
+
return model.executeNotebookCell(cellIndex, filterParams, bypassFilters);
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -2,15 +2,19 @@ import * as path from "path";
|
|
|
2
2
|
import { components } from "../api";
|
|
3
3
|
import { PUBLISHER_DATA_DIR } from "../constants";
|
|
4
4
|
import { BadRequestError, FrozenConfigError } from "../errors";
|
|
5
|
+
import { logger } from "../logger";
|
|
6
|
+
import { ManifestService } from "../service/manifest_service";
|
|
5
7
|
import { ProjectStore } from "../service/project_store";
|
|
6
8
|
|
|
7
9
|
type ApiPackage = components["schemas"]["Package"];
|
|
8
10
|
|
|
9
11
|
export class PackageController {
|
|
10
12
|
private projectStore: ProjectStore;
|
|
13
|
+
private manifestService: ManifestService;
|
|
11
14
|
|
|
12
|
-
constructor(projectStore: ProjectStore) {
|
|
15
|
+
constructor(projectStore: ProjectStore, manifestService: ManifestService) {
|
|
13
16
|
this.projectStore = projectStore;
|
|
17
|
+
this.manifestService = manifestService;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
public async listPackages(projectName: string): Promise<ApiPackage[]> {
|
|
@@ -32,7 +36,11 @@ export class PackageController {
|
|
|
32
36
|
return _package.getPackageMetadata();
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
async addPackage(
|
|
39
|
+
async addPackage(
|
|
40
|
+
projectName: string,
|
|
41
|
+
body: ApiPackage,
|
|
42
|
+
options?: { autoLoadManifest?: boolean },
|
|
43
|
+
) {
|
|
36
44
|
if (this.projectStore.publisherConfigIsFrozen) {
|
|
37
45
|
throw new FrozenConfigError();
|
|
38
46
|
}
|
|
@@ -46,9 +54,52 @@ export class PackageController {
|
|
|
46
54
|
const result = await project.addPackage(body.name);
|
|
47
55
|
await this.projectStore.addPackageToDatabase(projectName, body.name);
|
|
48
56
|
|
|
57
|
+
if (options?.autoLoadManifest === true) {
|
|
58
|
+
await this.tryLoadExistingManifest(projectName, body.name);
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
return result;
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
/**
|
|
65
|
+
* If there are already manifest entries for this package (e.g. from a
|
|
66
|
+
* previous materialization run), reload all models with the manifest so
|
|
67
|
+
* persist references resolve to the materialized tables immediately.
|
|
68
|
+
*/
|
|
69
|
+
private async tryLoadExistingManifest(
|
|
70
|
+
projectName: string,
|
|
71
|
+
packageName: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
const repository = this.projectStore.storageManager.getRepository();
|
|
75
|
+
const dbProject = await repository.getProjectByName(projectName);
|
|
76
|
+
if (!dbProject) return;
|
|
77
|
+
|
|
78
|
+
const manifest = await this.manifestService.getManifest(
|
|
79
|
+
dbProject.id,
|
|
80
|
+
packageName,
|
|
81
|
+
);
|
|
82
|
+
if (Object.keys(manifest.entries).length === 0) return;
|
|
83
|
+
|
|
84
|
+
await this.manifestService.reloadManifest(
|
|
85
|
+
dbProject.id,
|
|
86
|
+
packageName,
|
|
87
|
+
projectName,
|
|
88
|
+
);
|
|
89
|
+
logger.info("Auto-loaded existing manifest for added package", {
|
|
90
|
+
projectName,
|
|
91
|
+
packageName,
|
|
92
|
+
entryCount: Object.keys(manifest.entries).length,
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger.warn("Failed to auto-load manifest for package", {
|
|
96
|
+
projectName,
|
|
97
|
+
packageName,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
52
103
|
public async deletePackage(projectName: string, packageName: string) {
|
|
53
104
|
if (this.projectStore.publisherConfigIsFrozen) {
|
|
54
105
|
throw new FrozenConfigError();
|
|
@@ -3,6 +3,7 @@ import { components } from "../api";
|
|
|
3
3
|
import { API_PREFIX } from "../constants";
|
|
4
4
|
import { ModelNotFoundError } from "../errors";
|
|
5
5
|
import { ProjectStore } from "../service/project_store";
|
|
6
|
+
import type { FilterParams } from "../service/filter";
|
|
6
7
|
|
|
7
8
|
type ApiQuery = components["schemas"]["QueryResult"];
|
|
8
9
|
|
|
@@ -29,6 +30,8 @@ export class QueryController {
|
|
|
29
30
|
queryName: string,
|
|
30
31
|
query: string,
|
|
31
32
|
compactJson: boolean = false,
|
|
33
|
+
filterParams?: FilterParams,
|
|
34
|
+
bypassFilters?: boolean,
|
|
32
35
|
): Promise<ApiQuery> {
|
|
33
36
|
const project = await this.projectStore.getProject(projectName, false);
|
|
34
37
|
const p = await project.getPackage(packageName, false);
|
|
@@ -41,6 +44,8 @@ export class QueryController {
|
|
|
41
44
|
sourceName,
|
|
42
45
|
queryName,
|
|
43
46
|
query,
|
|
47
|
+
filterParams,
|
|
48
|
+
bypassFilters,
|
|
44
49
|
);
|
|
45
50
|
const renderLogs = validateRenderTags(result);
|
|
46
51
|
return {
|
package/src/errors.ts
CHANGED
|
@@ -20,6 +20,12 @@ export function internalErrorToHttpError(error: Error) {
|
|
|
20
20
|
return httpError(424, error.message);
|
|
21
21
|
} else if (error instanceof ConnectionError) {
|
|
22
22
|
return httpError(502, error.message);
|
|
23
|
+
} else if (error instanceof MaterializationNotFoundError) {
|
|
24
|
+
return httpError(404, error.message);
|
|
25
|
+
} else if (error instanceof MaterializationConflictError) {
|
|
26
|
+
return httpError(409, error.message);
|
|
27
|
+
} else if (error instanceof InvalidStateTransitionError) {
|
|
28
|
+
return httpError(409, error.message);
|
|
23
29
|
} else {
|
|
24
30
|
return httpError(500, error.message);
|
|
25
31
|
}
|
|
@@ -90,3 +96,21 @@ export class FrozenConfigError extends Error {
|
|
|
90
96
|
super(message);
|
|
91
97
|
}
|
|
92
98
|
}
|
|
99
|
+
|
|
100
|
+
export class MaterializationNotFoundError extends Error {
|
|
101
|
+
constructor(message: string) {
|
|
102
|
+
super(message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class MaterializationConflictError extends Error {
|
|
107
|
+
constructor(message: string) {
|
|
108
|
+
super(message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class InvalidStateTransitionError extends Error {
|
|
113
|
+
constructor(message: string) {
|
|
114
|
+
super(message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -82,15 +82,18 @@ export function registerModelResource(
|
|
|
82
82
|
const compiledModelDefinition: components["schemas"]["CompiledModel"] =
|
|
83
83
|
await modelInstance.getModel();
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
// Strip implicit filters from agent-facing responses
|
|
86
|
+
if (compiledModelDefinition.sources) {
|
|
87
|
+
for (const source of compiledModelDefinition.sources) {
|
|
88
|
+
if (source.filters) {
|
|
89
|
+
source.filters = source.filters.filter(
|
|
90
|
+
(f) => !f.implicit,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return compiledModelDefinition;
|
|
94
97
|
} catch (error) {
|
|
95
98
|
let errorDetails;
|
|
96
99
|
// Provide specific context for error messages
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
McpServer,
|
|
3
3
|
ResourceTemplate,
|
|
4
4
|
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { URL } from "url";
|
|
6
|
-
import type { components } from "../../api";
|
|
5
|
+
import { URL } from "url";
|
|
6
|
+
import type { components } from "../../api";
|
|
7
7
|
import { ModelCompilationError } from "../../errors";
|
|
8
8
|
import { logger } from "../../logger";
|
|
9
9
|
import { ProjectStore } from "../../service/project_store";
|
|
@@ -89,22 +89,23 @@ export function registerSourceResource(
|
|
|
89
89
|
if (!sources) {
|
|
90
90
|
throw new Error("Could not retrieve sources from model.");
|
|
91
91
|
}
|
|
92
|
-
// Add type annotation for 's'
|
|
93
92
|
const source = sources.find(
|
|
94
|
-
// @ts-expect-error TODO: Fix missing Source type in API
|
|
95
93
|
(s: components["schemas"]["Source"]) =>
|
|
96
94
|
s.name === sourceName,
|
|
97
95
|
);
|
|
98
96
|
|
|
99
97
|
if (!source) {
|
|
100
|
-
// Specific "Source not found" error
|
|
101
98
|
const errorDetails = getNotFoundError(
|
|
102
99
|
`Source '${sourceName}' in model '${modelPath}' package '${packageName}' project '${projectName}'`,
|
|
103
100
|
);
|
|
104
101
|
throw new McpGetResourceError(errorDetails);
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
//
|
|
104
|
+
// Strip implicit filters from agent-facing responses
|
|
105
|
+
if (source.filters) {
|
|
106
|
+
source.filters = source.filters.filter((f) => !f.implicit);
|
|
107
|
+
}
|
|
108
|
+
|
|
108
109
|
return source;
|
|
109
110
|
} catch (error) {
|
|
110
111
|
// Catch errors from getModelForQuery or finding the source
|
|
@@ -83,7 +83,6 @@ export function registerViewResource(
|
|
|
83
83
|
throw new Error("Could not retrieve sources from model.");
|
|
84
84
|
}
|
|
85
85
|
const source = sources.find(
|
|
86
|
-
// @ts-expect-error TODO: Fix missing Source type in API
|
|
87
86
|
(s: components["schemas"]["Source"]) =>
|
|
88
87
|
s.name === sourceName,
|
|
89
88
|
);
|
|
@@ -24,6 +24,12 @@ const executeQueryShape = {
|
|
|
24
24
|
query: z.string().optional().describe("Ad-hoc Malloy query code"),
|
|
25
25
|
sourceName: z.string().optional().describe("Source name for a view"),
|
|
26
26
|
queryName: z.string().optional().describe("Named query or view"),
|
|
27
|
+
filterParams: z
|
|
28
|
+
.record(z.union([z.string(), z.array(z.string())]))
|
|
29
|
+
.optional()
|
|
30
|
+
.describe(
|
|
31
|
+
"Filter parameter values keyed by filter name. Used with sources that declare #(filter) annotations.",
|
|
32
|
+
),
|
|
27
33
|
};
|
|
28
34
|
|
|
29
35
|
// Type inference is handled automatically by the MCP server based on the executeQueryShape
|
|
@@ -49,6 +55,7 @@ export function registerExecuteQueryTool(
|
|
|
49
55
|
query,
|
|
50
56
|
sourceName,
|
|
51
57
|
queryName,
|
|
58
|
+
filterParams,
|
|
52
59
|
} = params;
|
|
53
60
|
|
|
54
61
|
logger.info("[MCP Tool executeQuery] Received params:", { params });
|
|
@@ -120,6 +127,7 @@ export function registerExecuteQueryTool(
|
|
|
120
127
|
undefined,
|
|
121
128
|
undefined,
|
|
122
129
|
query,
|
|
130
|
+
filterParams,
|
|
123
131
|
);
|
|
124
132
|
const { validateRenderTags } = await import(
|
|
125
133
|
"@malloydata/render-validator"
|
|
@@ -165,6 +173,7 @@ export function registerExecuteQueryTool(
|
|
|
165
173
|
sourceName,
|
|
166
174
|
queryName,
|
|
167
175
|
undefined,
|
|
176
|
+
filterParams,
|
|
168
177
|
);
|
|
169
178
|
const { validateRenderTags } = await import(
|
|
170
179
|
"@malloydata/render-validator"
|