@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.
Files changed (34) hide show
  1. package/.eslintrc.json +9 -1
  2. package/dist/app/api-doc.yaml +74 -1
  3. package/dist/app/assets/HomePage-BWcnTdSg.js +1 -0
  4. package/dist/app/assets/{MainPage-C9Fr5IN8.js → MainPage-k-BDUTT_.js} +2 -2
  5. package/dist/app/assets/{ModelPage-BkU6HAHA.js → ModelPage-BbFrnQ1A.js} +1 -1
  6. package/dist/app/assets/PackagePage-CCwd7u2-.js +1 -0
  7. package/dist/app/assets/ProjectPage-CDHkneyO.js +1 -0
  8. package/dist/app/assets/RouteError-BZbAeXla.js +1 -0
  9. package/dist/app/assets/{WorkbookPage-D3rUQZj6.js → WorkbookPage-BcuqksYi.js} +1 -1
  10. package/dist/app/assets/{index-lhDwptrQ.js → index-BDS2El9V.js} +216 -216
  11. package/dist/app/assets/{index-BLxl0XLH.js → index-C-0P3N7Y.js} +150 -150
  12. package/dist/app/assets/index-EHh3Tsle.js +1237 -0
  13. package/dist/app/assets/{index.umd-BkXQ-YAe.js → index.umd-ClIgLTxW.js} +1 -1
  14. package/dist/app/index.html +1 -1
  15. package/dist/instrumentation.js +2252 -964
  16. package/dist/server.js +2802 -1140
  17. package/package.json +10 -10
  18. package/src/controller/connection.controller.ts +22 -2
  19. package/src/controller/query.controller.ts +7 -0
  20. package/src/mcp/tools/execute_query_tool.ts +57 -29
  21. package/src/server.ts +5 -1
  22. package/src/service/connection.spec.ts +105 -0
  23. package/src/service/connection.ts +293 -17
  24. package/src/service/db_utils.ts +85 -4
  25. package/src/service/model.ts +11 -8
  26. package/src/service/project.ts +20 -3
  27. package/tests/harness/mcp_test_setup.ts +166 -26
  28. package/tests/unit/duckdb/attached_databases.test.ts +61 -3
  29. package/tests/unit/ducklake/ducklake.test.ts +950 -0
  30. package/dist/app/assets/HomePage-D76UaGFV.js +0 -1
  31. package/dist/app/assets/PackagePage-BhE9Wi7b.js +0 -1
  32. package/dist/app/assets/ProjectPage-BatZLVap.js +0 -1
  33. package/dist/app/assets/RouteError-Bo5zJ8Xa.js +0 -1
  34. 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.167",
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.333",
34
- "@malloydata/db-duckdb": "^0.0.333",
35
- "@malloydata/db-mysql": "^0.0.333",
36
- "@malloydata/db-postgres": "^0.0.333",
37
- "@malloydata/db-snowflake": "^0.0.333",
38
- "@malloydata/db-trino": "^0.0.333",
39
- "@malloydata/malloy": "^0.0.333",
40
- "@malloydata/malloy-sql": "^0.0.333",
41
- "@malloydata/render": "^0.0.333",
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
- _schemaName: string,
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
- tablePath.split(".").pop()!, // tableKey is the table name
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
- return {
135
- isError: false,
136
- content: [
137
- {
138
- type: "resource",
139
- resource: {
140
- type: "application/json",
141
- uri: resultUri,
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
- return {
168
- isError: false,
169
- content: [
170
- {
171
- type: "resource",
172
- resource: {
173
- type: "application/json",
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(