@malloy-publisher/server 0.0.167 → 0.0.169
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/.eslintrc.json +9 -1
- package/dist/app/api-doc.yaml +74 -1
- package/dist/app/assets/HomePage-BWcnTdSg.js +1 -0
- package/dist/app/assets/{MainPage-C9Fr5IN8.js → MainPage-k-BDUTT_.js} +2 -2
- package/dist/app/assets/{ModelPage-BkU6HAHA.js → ModelPage-BbFrnQ1A.js} +1 -1
- package/dist/app/assets/PackagePage-CCwd7u2-.js +1 -0
- package/dist/app/assets/ProjectPage-CDHkneyO.js +1 -0
- package/dist/app/assets/RouteError-BZbAeXla.js +1 -0
- package/dist/app/assets/{WorkbookPage-D3rUQZj6.js → WorkbookPage-BcuqksYi.js} +1 -1
- package/dist/app/assets/{index-lhDwptrQ.js → index-BDS2El9V.js} +216 -216
- package/dist/app/assets/{index-BLxl0XLH.js → index-C-0P3N7Y.js} +150 -150
- package/dist/app/assets/index-EHh3Tsle.js +1237 -0
- package/dist/app/assets/{index.umd-BkXQ-YAe.js → index.umd-ClIgLTxW.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.js +2252 -964
- package/dist/server.js +2802 -1140
- package/package.json +10 -10
- package/src/controller/connection.controller.ts +22 -2
- package/src/controller/query.controller.ts +7 -0
- package/src/mcp/tools/execute_query_tool.ts +57 -29
- package/src/server.ts +5 -1
- package/src/service/connection.spec.ts +105 -0
- package/src/service/connection.ts +293 -17
- package/src/service/db_utils.ts +85 -4
- package/src/service/model.ts +11 -8
- package/src/service/project.ts +20 -3
- package/tests/harness/mcp_test_setup.ts +166 -26
- package/tests/unit/duckdb/attached_databases.test.ts +61 -3
- package/tests/unit/ducklake/ducklake.test.ts +950 -0
- package/dist/app/assets/HomePage-D76UaGFV.js +0 -1
- package/dist/app/assets/PackagePage-BhE9Wi7b.js +0 -1
- package/dist/app/assets/ProjectPage-BatZLVap.js +0 -1
- package/dist/app/assets/RouteError-Bo5zJ8Xa.js +0 -1
- package/dist/app/assets/index-hkABoiMV.js +0 -1259
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.169",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"malloy-publisher": "dist/server.js"
|
|
@@ -30,15 +30,15 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@aws-sdk/client-s3": "^3.958.0",
|
|
32
32
|
"@google-cloud/storage": "^7.16.0",
|
|
33
|
-
"@malloydata/db-bigquery": "^0.0.
|
|
34
|
-
"@malloydata/db-duckdb": "^0.0.
|
|
35
|
-
"@malloydata/db-mysql": "^0.0.
|
|
36
|
-
"@malloydata/db-postgres": "^0.0.
|
|
37
|
-
"@malloydata/db-snowflake": "^0.0.
|
|
38
|
-
"@malloydata/db-trino": "^0.0.
|
|
39
|
-
"@malloydata/malloy": "^0.0.
|
|
40
|
-
"@malloydata/malloy-sql": "^0.0.
|
|
41
|
-
"@malloydata/render": "^0.0.
|
|
33
|
+
"@malloydata/db-bigquery": "^0.0.354",
|
|
34
|
+
"@malloydata/db-duckdb": "^0.0.354",
|
|
35
|
+
"@malloydata/db-mysql": "^0.0.354",
|
|
36
|
+
"@malloydata/db-postgres": "^0.0.354",
|
|
37
|
+
"@malloydata/db-snowflake": "^0.0.354",
|
|
38
|
+
"@malloydata/db-trino": "^0.0.354",
|
|
39
|
+
"@malloydata/malloy": "^0.0.354",
|
|
40
|
+
"@malloydata/malloy-sql": "^0.0.354",
|
|
41
|
+
"@malloydata/render-validator": "^0.0.354",
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.13.2",
|
|
43
43
|
"@opentelemetry/api": "^1.9.0",
|
|
44
44
|
"@opentelemetry/auto-instrumentations-node": "^0.57.0",
|
|
@@ -155,17 +155,37 @@ export class ConnectionController {
|
|
|
155
155
|
public async getTable(
|
|
156
156
|
projectName: string,
|
|
157
157
|
connectionName: string,
|
|
158
|
-
|
|
158
|
+
schemaName: string,
|
|
159
159
|
tablePath: string,
|
|
160
160
|
): Promise<ApiTable> {
|
|
161
161
|
const malloyConnection = await this.getMalloyConnection(
|
|
162
162
|
projectName,
|
|
163
163
|
connectionName,
|
|
164
164
|
);
|
|
165
|
+
const connection = await this.getConnection(projectName, connectionName);
|
|
166
|
+
|
|
167
|
+
if (connection.type === "ducklake") {
|
|
168
|
+
if (tablePath.split(".").length === 1) {
|
|
169
|
+
// tablePath is just the table name, construct full path
|
|
170
|
+
tablePath = `${connectionName}.${schemaName}.${tablePath}`;
|
|
171
|
+
} else if (
|
|
172
|
+
tablePath.split(".").length === 2 &&
|
|
173
|
+
!tablePath.startsWith(connectionName)
|
|
174
|
+
) {
|
|
175
|
+
// tablePath is schemaName.tableName but missing connection prefix
|
|
176
|
+
tablePath = `${connectionName}.${tablePath}`;
|
|
177
|
+
}
|
|
178
|
+
// If tablePath already has 3+ parts or starts with connection name, use as-is
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const tableKey = tablePath.split(".").pop();
|
|
182
|
+
if (!tableKey) {
|
|
183
|
+
throw new Error(`Invalid tablePath: ${tablePath}`);
|
|
184
|
+
}
|
|
165
185
|
|
|
166
186
|
const tableSource = await getConnectionTableSource(
|
|
167
187
|
malloyConnection,
|
|
168
|
-
|
|
188
|
+
tableKey, // tableKey is the table name
|
|
169
189
|
tablePath,
|
|
170
190
|
);
|
|
171
191
|
|
|
@@ -41,11 +41,18 @@ export class QueryController {
|
|
|
41
41
|
queryName,
|
|
42
42
|
query,
|
|
43
43
|
);
|
|
44
|
+
// Lazy import since this creates a FakeDOM global which
|
|
45
|
+
// can confuse other imports.
|
|
46
|
+
const { validateRenderTags } = await import(
|
|
47
|
+
"@malloydata/render-validator"
|
|
48
|
+
);
|
|
49
|
+
const renderLogs = validateRenderTags(result);
|
|
44
50
|
return {
|
|
45
51
|
result: compactJson
|
|
46
52
|
? JSON.stringify(compactResult, bigIntReplacer)
|
|
47
53
|
: JSON.stringify(result),
|
|
48
54
|
resource: `${API_PREFIX}/projects/${projectName}/packages/${packageName}/models/${modelPath}/query`,
|
|
55
|
+
renderLogs: renderLogs.length > 0 ? renderLogs : undefined,
|
|
49
56
|
} as ApiQuery;
|
|
50
57
|
}
|
|
51
58
|
}
|
|
@@ -121,8 +121,11 @@ export function registerExecuteQueryTool(
|
|
|
121
121
|
undefined,
|
|
122
122
|
query,
|
|
123
123
|
);
|
|
124
|
+
const { validateRenderTags } = await import(
|
|
125
|
+
"@malloydata/render-validator"
|
|
126
|
+
);
|
|
127
|
+
const renderLogs = validateRenderTags(result);
|
|
124
128
|
|
|
125
|
-
// --- Format Success Response (Duplicated for now, could refactor) ---
|
|
126
129
|
const baseUriComponents = {
|
|
127
130
|
project: projectName,
|
|
128
131
|
package: packageName,
|
|
@@ -131,29 +134,43 @@ export function registerExecuteQueryTool(
|
|
|
131
134
|
};
|
|
132
135
|
const resultUri = buildMalloyUri(baseUriComponents, "result");
|
|
133
136
|
const resultString = JSON.stringify(result, null, 2);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
text: resultString,
|
|
143
|
-
},
|
|
137
|
+
|
|
138
|
+
const content = [
|
|
139
|
+
{
|
|
140
|
+
type: "resource" as const,
|
|
141
|
+
resource: {
|
|
142
|
+
type: "application/json",
|
|
143
|
+
uri: resultUri,
|
|
144
|
+
text: resultString,
|
|
144
145
|
},
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
if (renderLogs.length > 0) {
|
|
150
|
+
return {
|
|
151
|
+
isError: false,
|
|
152
|
+
content: [
|
|
153
|
+
...content,
|
|
154
|
+
{
|
|
155
|
+
type: "text" as const,
|
|
156
|
+
text: `Render tag warnings:\n${JSON.stringify(renderLogs, null, 2)}`,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { isError: false, content };
|
|
147
163
|
} else if (queryName) {
|
|
148
|
-
// Otherwise, use sourceName/queryName in 1st/2nd args
|
|
149
164
|
const { result } = await model.getQueryResults(
|
|
150
165
|
sourceName,
|
|
151
166
|
queryName,
|
|
152
167
|
undefined,
|
|
153
168
|
);
|
|
169
|
+
const { validateRenderTags } = await import(
|
|
170
|
+
"@malloydata/render-validator"
|
|
171
|
+
);
|
|
172
|
+
const renderLogs = validateRenderTags(result);
|
|
154
173
|
|
|
155
|
-
// --- Format Success Response ---
|
|
156
|
-
// Use the helper function to build valid URIs
|
|
157
174
|
const baseUriComponents = {
|
|
158
175
|
project: projectName,
|
|
159
176
|
package: packageName,
|
|
@@ -161,22 +178,33 @@ export function registerExecuteQueryTool(
|
|
|
161
178
|
resourceName: modelPath,
|
|
162
179
|
};
|
|
163
180
|
const resultUri = buildMalloyUri(baseUriComponents, "result");
|
|
164
|
-
|
|
165
181
|
const resultString = JSON.stringify(result, null, 2);
|
|
166
182
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{
|
|
171
|
-
type: "
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
uri: resultUri,
|
|
175
|
-
text: resultString,
|
|
176
|
-
},
|
|
183
|
+
const content = [
|
|
184
|
+
{
|
|
185
|
+
type: "resource" as const,
|
|
186
|
+
resource: {
|
|
187
|
+
type: "application/json",
|
|
188
|
+
uri: resultUri,
|
|
189
|
+
text: resultString,
|
|
177
190
|
},
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
},
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
if (renderLogs.length > 0) {
|
|
195
|
+
return {
|
|
196
|
+
isError: false,
|
|
197
|
+
content: [
|
|
198
|
+
...content,
|
|
199
|
+
{
|
|
200
|
+
type: "text" as const,
|
|
201
|
+
text: `Render tag warnings:\n${JSON.stringify(renderLogs, null, 2)}`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { isError: false, content };
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
// If execution reaches this point, something has gone wrong with
|
package/src/server.ts
CHANGED
|
@@ -131,6 +131,9 @@ const compileController = new CompileController(projectStore);
|
|
|
131
131
|
|
|
132
132
|
export const mcpApp = express();
|
|
133
133
|
|
|
134
|
+
// Register health endpoints on mcpApp (for E2E tests)
|
|
135
|
+
registerHealthEndpoints(mcpApp);
|
|
136
|
+
|
|
134
137
|
mcpApp.use(MCP_ENDPOINT, express.json());
|
|
135
138
|
mcpApp.use(MCP_ENDPOINT, cors());
|
|
136
139
|
|
|
@@ -253,7 +256,8 @@ app.use(
|
|
|
253
256
|
);
|
|
254
257
|
app.use(bodyParser.json());
|
|
255
258
|
|
|
256
|
-
// Register health check endpoints
|
|
259
|
+
// Register health check endpoints on main app:
|
|
260
|
+
// - Required for production/Kubernetes monitoring (main server on PUBLISHER_PORT)
|
|
257
261
|
registerHealthEndpoints(app);
|
|
258
262
|
|
|
259
263
|
// Register Prometheus metrics endpoint
|
|
@@ -975,6 +975,111 @@ describe("connection integration tests", () => {
|
|
|
975
975
|
});
|
|
976
976
|
|
|
977
977
|
describe("error handling", () => {
|
|
978
|
+
describe("DuckLake connection type", () => {
|
|
979
|
+
it(
|
|
980
|
+
"should create DuckLake connection",
|
|
981
|
+
async () => {
|
|
982
|
+
if (!hasPostgresCredentials() || !hasS3Credentials()) {
|
|
983
|
+
console.log(
|
|
984
|
+
"Skipping: PostgreSQL and S3 credentials not configured",
|
|
985
|
+
);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const { malloyConnections } = await createProjectConnections(
|
|
990
|
+
[
|
|
991
|
+
{
|
|
992
|
+
name: "ducklake_test",
|
|
993
|
+
type: "ducklake",
|
|
994
|
+
ducklakeConnection: {
|
|
995
|
+
catalog: {
|
|
996
|
+
postgresConnection: {
|
|
997
|
+
host: process.env.POSTGRES_TEST_HOST,
|
|
998
|
+
port: parseInt(
|
|
999
|
+
process.env.POSTGRES_TEST_PORT || "5432",
|
|
1000
|
+
),
|
|
1001
|
+
userName: process.env.POSTGRES_TEST_USER!,
|
|
1002
|
+
password:
|
|
1003
|
+
process.env.POSTGRES_TEST_PASSWORD!,
|
|
1004
|
+
databaseName:
|
|
1005
|
+
process.env.POSTGRES_TEST_DATABASE,
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
storage: {
|
|
1009
|
+
bucketUrl:
|
|
1010
|
+
process.env.S3_TEST_BUCKET_URL ||
|
|
1011
|
+
"s3://test-bucket",
|
|
1012
|
+
s3Connection: {
|
|
1013
|
+
accessKeyId:
|
|
1014
|
+
process.env.S3_TEST_ACCESS_KEY_ID!,
|
|
1015
|
+
secretAccessKey:
|
|
1016
|
+
process.env.S3_TEST_SECRET_ACCESS_KEY!,
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
testProjectPath,
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
const connection = malloyConnections.get(
|
|
1026
|
+
"ducklake_test",
|
|
1027
|
+
) as DuckDBConnection;
|
|
1028
|
+
createdConnections.push(connection);
|
|
1029
|
+
expect(connection).toBeDefined();
|
|
1030
|
+
|
|
1031
|
+
// Verify DuckLake database is attached
|
|
1032
|
+
const databases = await connection.runSQL("SHOW DATABASES");
|
|
1033
|
+
const dbNames = databases.rows.map(
|
|
1034
|
+
(row) => Object.values(row)[0],
|
|
1035
|
+
);
|
|
1036
|
+
expect(dbNames).toContain("ducklake_test");
|
|
1037
|
+
},
|
|
1038
|
+
{ timeout: 30000 },
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
it("should throw error if DuckLake catalog connection is missing", async () => {
|
|
1042
|
+
await expect(
|
|
1043
|
+
createProjectConnections(
|
|
1044
|
+
[
|
|
1045
|
+
{
|
|
1046
|
+
name: "ducklake_no_catalog",
|
|
1047
|
+
type: "ducklake",
|
|
1048
|
+
ducklakeConnection: {
|
|
1049
|
+
storage: {
|
|
1050
|
+
bucketUrl: "s3://test-bucket",
|
|
1051
|
+
s3Connection: {
|
|
1052
|
+
accessKeyId: "test",
|
|
1053
|
+
secretAccessKey: "test",
|
|
1054
|
+
},
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
} as ApiConnection,
|
|
1058
|
+
],
|
|
1059
|
+
testProjectPath,
|
|
1060
|
+
),
|
|
1061
|
+
).rejects.toThrow(
|
|
1062
|
+
/PostgreSQL connection configuration is required/,
|
|
1063
|
+
);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it("should throw error if DuckLake connection config is missing", async () => {
|
|
1067
|
+
await expect(
|
|
1068
|
+
createProjectConnections(
|
|
1069
|
+
[
|
|
1070
|
+
{
|
|
1071
|
+
name: "ducklake_missing_config",
|
|
1072
|
+
type: "ducklake",
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
testProjectPath,
|
|
1076
|
+
),
|
|
1077
|
+
).rejects.toThrow(
|
|
1078
|
+
/DuckLake connection configuration is missing/,
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
978
1083
|
it("should throw error if DuckDB connection name conflicts with attached database", async () => {
|
|
979
1084
|
await expect(
|
|
980
1085
|
createProjectConnections(
|