@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.
Files changed (74) hide show
  1. package/build.ts +7 -3
  2. package/dist/app/api-doc.yaml +505 -52
  3. package/dist/app/assets/HomePage-Dn3E4CuB.js +1 -0
  4. package/dist/app/assets/{MainPage-B53xidTF.js → MainPage-BzB3yoqi.js} +2 -2
  5. package/dist/app/assets/{ModelPage-UMuQe8qY.js → ModelPage-C9O_sAXT.js} +1 -1
  6. package/dist/app/assets/PackagePage-DcxKEjBX.js +1 -0
  7. package/dist/app/assets/ProjectPage-BDj307rF.js +1 -0
  8. package/dist/app/assets/{RouteError-Cv58zNpb.js → RouteError-DAShbVCG.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-DZ1StqsX.js → WorkbookPage-Cs_XYEaB.js} +1 -1
  10. package/dist/app/assets/core-CjeTkq8O.es-BqRc6yhC.js +148 -0
  11. package/dist/app/assets/engine-oniguruma-C4vnmooL.es-jdkXmgTr.js +1 -0
  12. package/dist/app/assets/github-light-JYsPkUQd.es-DAi9KRSo.js +1 -0
  13. package/dist/app/assets/index-15BOvhp0.js +456 -0
  14. package/dist/app/assets/{index-DPThhVfX.js → index-Bb2jqquW.js} +1 -1
  15. package/dist/app/assets/{index-M3Zo817E.js → index-D68X76-7.js} +98 -98
  16. package/dist/app/assets/{index.umd-DnfBsVqO.js → index.umd-DGBekgSu.js} +1 -1
  17. package/dist/app/assets/json-71t8ZF9g.es-BQoSv7ci.js +1 -0
  18. package/dist/app/assets/sql-DCkt643-.es-COK4E0Yg.js +1 -0
  19. package/dist/app/assets/typescript-buWNZFwO.es-Dj6nwHGl.js +1 -0
  20. package/dist/app/index.html +1 -1
  21. package/dist/{instrumentation.js → instrumentation.mjs} +10567 -10584
  22. package/dist/{server.js → server.mjs} +16959 -15357
  23. package/package.json +19 -17
  24. package/src/controller/connection.controller.ts +27 -20
  25. package/src/controller/manifest.controller.ts +29 -0
  26. package/src/controller/materialization.controller.ts +125 -0
  27. package/src/controller/model.controller.ts +4 -3
  28. package/src/controller/package.controller.ts +53 -2
  29. package/src/controller/query.controller.ts +5 -0
  30. package/src/errors.ts +24 -0
  31. package/src/mcp/prompts/handlers.ts +1 -1
  32. package/src/mcp/resources/model_resource.ts +12 -9
  33. package/src/mcp/resources/source_resource.ts +7 -6
  34. package/src/mcp/resources/view_resource.ts +0 -1
  35. package/src/mcp/tools/execute_query_tool.ts +9 -0
  36. package/src/server.ts +223 -15
  37. package/src/service/connection.ts +1 -4
  38. package/src/service/filter.spec.ts +447 -0
  39. package/src/service/filter.ts +337 -0
  40. package/src/service/filter_integration.spec.ts +825 -0
  41. package/src/service/manifest_service.spec.ts +201 -0
  42. package/src/service/manifest_service.ts +106 -0
  43. package/src/service/materialization_service.spec.ts +648 -0
  44. package/src/service/materialization_service.ts +929 -0
  45. package/src/service/materialized_table_gc.spec.ts +383 -0
  46. package/src/service/materialized_table_gc.ts +279 -0
  47. package/src/service/model.ts +227 -49
  48. package/src/service/package.ts +50 -0
  49. package/src/service/project_store.ts +21 -2
  50. package/src/service/quoting.ts +41 -0
  51. package/src/service/resolve_project.ts +13 -0
  52. package/src/storage/DatabaseInterface.ts +103 -1
  53. package/src/storage/{StorageManager.spec.ts → StorageManager.mock.ts} +9 -0
  54. package/src/storage/StorageManager.ts +119 -1
  55. package/src/storage/duckdb/DuckDBConnection.ts +1 -1
  56. package/src/storage/duckdb/DuckDBManifestStore.ts +70 -0
  57. package/src/storage/duckdb/DuckDBRepository.ts +99 -9
  58. package/src/storage/duckdb/ManifestRepository.ts +119 -0
  59. package/src/storage/duckdb/MaterializationRepository.ts +249 -0
  60. package/src/storage/duckdb/manifest_store.spec.ts +133 -0
  61. package/src/storage/duckdb/schema.ts +59 -1
  62. package/src/storage/ducklake/DuckLakeManifestStore.ts +146 -0
  63. package/tests/fixtures/persist-test/data/orders.csv +5 -0
  64. package/tests/fixtures/persist-test/persist_test.malloy +11 -0
  65. package/tests/fixtures/persist-test/publisher.json +5 -0
  66. package/tests/fixtures/publisher.config.json +15 -0
  67. package/tests/harness/rest_e2e.ts +68 -0
  68. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +470 -0
  69. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +2 -2
  70. package/tsconfig.json +1 -1
  71. package/dist/app/assets/HomePage-B0C6gwGj.js +0 -1
  72. package/dist/app/assets/PackagePage-BEDvm_je.js +0 -1
  73. package/dist/app/assets/ProjectPage-DzN4P86H.js +0 -1
  74. package/dist/app/assets/index-D-xPyBUA.js +0 -467
package/src/server.ts CHANGED
@@ -6,13 +6,14 @@ import {
6
6
  } from "./instrumentation";
7
7
 
8
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
- import * as bodyParser from "body-parser";
9
+ import bodyParser from "body-parser";
10
10
  import cors from "cors";
11
11
  import express from "express";
12
12
  import * as http from "http";
13
13
  import { createProxyMiddleware } from "http-proxy-middleware";
14
14
  import { AddressInfo } from "net";
15
15
  import * as path from "path";
16
+ import { fileURLToPath } from "url";
16
17
  import { CompileController } from "./controller/compile.controller";
17
18
  import { ConnectionController } from "./controller/connection.controller";
18
19
  import { DatabaseController } from "./controller/database.controller";
@@ -20,7 +21,11 @@ import { ModelController } from "./controller/model.controller";
20
21
  import { PackageController } from "./controller/package.controller";
21
22
  import { QueryController } from "./controller/query.controller";
22
23
  import { WatchModeController } from "./controller/watch-mode.controller";
23
- import { internalErrorToHttpError, NotImplementedError } from "./errors";
24
+ import {
25
+ BadRequestError,
26
+ internalErrorToHttpError,
27
+ NotImplementedError,
28
+ } from "./errors";
24
29
  import {
25
30
  drainingGuard,
26
31
  registerHealthEndpoints,
@@ -28,7 +33,11 @@ import {
28
33
  } from "./health";
29
34
  import { logger, loggerMiddleware } from "./logger";
30
35
 
36
+ import { ManifestController } from "./controller/manifest.controller";
37
+ import { MaterializationController } from "./controller/materialization.controller";
31
38
  import { initializeMcpServer } from "./mcp/server";
39
+ import { ManifestService } from "./service/manifest_service";
40
+ import { MaterializationService } from "./service/materialization_service";
32
41
  import { ProjectStore } from "./service/project_store";
33
42
 
34
43
  /** Normalize an Express query param into a string[] or undefined. */
@@ -112,30 +121,37 @@ const SHUTDOWN_DRAIN_DURATION_SECONDS = Number(
112
121
  const SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS = Number(
113
122
  process.env.SHUTDOWN_GRACEFUL_CLOSE_TIMEOUT_SECONDS || 0,
114
123
  );
115
- // Find the app directory - handle NPX vs local execution
116
- let ROOT: string;
117
- if (require.main) {
118
- // Use the main module's directory (works for NPX and direct execution)
119
- ROOT = path.join(path.dirname(require.main.filename), "app");
120
- } else {
121
- // Fallback to current script directory
122
- ROOT = path.join(path.dirname(process.argv[1] || __filename), "app");
123
- }
124
+ // Find the app directory relative to this bundled server file.
125
+ // Works under both ESM (import.meta.url) and when invoked via NPX.
126
+ const __filename_esm = fileURLToPath(import.meta.url);
127
+ const ROOT = path.join(path.dirname(__filename_esm), "app");
124
128
  const SERVER_ROOT = path.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
125
129
  const API_PREFIX = "/api/v0";
126
130
  const isDevelopment = process.env["NODE_ENV"] === "development";
127
131
 
128
- const app = express();
132
+ export const app = express();
129
133
  app.use(loggerMiddleware);
130
134
  app.use(httpMetricsMiddleware);
131
135
  const projectStore = new ProjectStore(SERVER_ROOT);
136
+ const manifestService = new ManifestService(projectStore);
132
137
  const watchModeController = new WatchModeController(projectStore);
133
138
  const connectionController = new ConnectionController(projectStore);
134
139
  const modelController = new ModelController(projectStore);
135
- const packageController = new PackageController(projectStore);
140
+ const packageController = new PackageController(projectStore, manifestService);
136
141
  const databaseController = new DatabaseController(projectStore);
137
142
  const queryController = new QueryController(projectStore);
138
143
  const compileController = new CompileController(projectStore);
144
+ const materializationService = new MaterializationService(
145
+ projectStore,
146
+ manifestService,
147
+ );
148
+ const materializationController = new MaterializationController(
149
+ materializationService,
150
+ );
151
+ const manifestController = new ManifestController(
152
+ projectStore,
153
+ manifestService,
154
+ );
139
155
 
140
156
  export const mcpApp = express();
141
157
 
@@ -262,7 +278,9 @@ app.use(
262
278
  credentials: true,
263
279
  }),
264
280
  );
265
- app.use(bodyParser.json());
281
+
282
+ // Set body-parser JSON limit to 1Mb (default: 100kb)
283
+ app.use(bodyParser.json({ limit: "1mb" }));
266
284
 
267
285
  // Register health check endpoints on main app:
268
286
  // - Required for production/Kubernetes monitoring (main server on PUBLISHER_PORT)
@@ -579,7 +597,7 @@ app.post(
579
597
  req.params.projectName,
580
598
  req.params.connectionName,
581
599
  req.body.sqlStatement as string,
582
- req.query.options as string,
600
+ req.body.options as string,
583
601
  ),
584
602
  );
585
603
  } catch (error) {
@@ -650,9 +668,11 @@ app.get(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
650
668
 
651
669
  app.post(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
652
670
  try {
671
+ const autoLoadManifest = req.query.autoLoadManifest === "true";
653
672
  const _package = await packageController.addPackage(
654
673
  req.params.projectName,
655
674
  req.body,
675
+ { autoLoadManifest },
656
676
  );
657
677
  res.status(200).json(_package?.getPackageMetadata());
658
678
  } catch (error) {
@@ -817,12 +837,29 @@ app.get(
817
837
  // Express stores wildcard matches in params['0']
818
838
  const notebookPath = (req.params as Record<string, string>)["0"];
819
839
 
840
+ // Parse optional filter_params (JSON query string) and bypass_filters
841
+ let filterParams: Record<string, string | string[]> | undefined;
842
+ if (typeof req.query.filter_params === "string") {
843
+ try {
844
+ filterParams = JSON.parse(req.query.filter_params);
845
+ } catch {
846
+ res.status(400).json({
847
+ error: "Invalid filter_params: must be valid JSON",
848
+ });
849
+ return;
850
+ }
851
+ }
852
+ const bypassFilters =
853
+ req.query.bypass_filters === "true" ? true : undefined;
854
+
820
855
  res.status(200).json(
821
856
  await modelController.executeNotebookCell(
822
857
  req.params.projectName,
823
858
  req.params.packageName,
824
859
  notebookPath,
825
860
  cellIndex,
861
+ filterParams,
862
+ bypassFilters,
826
863
  ),
827
864
  );
828
865
  } catch (error) {
@@ -879,6 +916,10 @@ app.post(
879
916
  req.body.queryName as string,
880
917
  req.body.query as string,
881
918
  req.body.compactJson === true,
919
+ (req.body.filterParams ?? req.body.sourceFilters) as
920
+ | Record<string, string | string[]>
921
+ | undefined,
922
+ req.body.bypassFilters === true ? true : undefined,
882
923
  ),
883
924
  );
884
925
  } catch (error) {
@@ -932,6 +973,173 @@ app.post(
932
973
  },
933
974
  );
934
975
 
976
+ // ==================== MATERIALIZATION ROUTES ====================
977
+
978
+ app.post(
979
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
980
+ async (req, res) => {
981
+ try {
982
+ const build = await materializationController.createMaterialization(
983
+ req.params.projectName,
984
+ req.params.packageName,
985
+ req.body || {},
986
+ );
987
+ res.status(201).json(build);
988
+ } catch (error) {
989
+ const { json, status } = internalErrorToHttpError(error as Error);
990
+ res.status(status).json(json);
991
+ }
992
+ },
993
+ );
994
+
995
+ app.get(
996
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
997
+ async (req, res) => {
998
+ try {
999
+ const limit = req.query.limit
1000
+ ? parseInt(req.query.limit as string, 10)
1001
+ : undefined;
1002
+ const offset = req.query.offset
1003
+ ? parseInt(req.query.offset as string, 10)
1004
+ : undefined;
1005
+ const builds = await materializationController.listMaterializations(
1006
+ req.params.projectName,
1007
+ req.params.packageName,
1008
+ { limit, offset },
1009
+ );
1010
+ res.status(200).json(builds);
1011
+ } catch (error) {
1012
+ const { json, status } = internalErrorToHttpError(error as Error);
1013
+ res.status(status).json(json);
1014
+ }
1015
+ },
1016
+ );
1017
+
1018
+ app.get(
1019
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1020
+ async (req, res) => {
1021
+ try {
1022
+ const build = await materializationController.getMaterialization(
1023
+ req.params.projectName,
1024
+ req.params.packageName,
1025
+ req.params.materializationId,
1026
+ );
1027
+ res.status(200).json(build);
1028
+ } catch (error) {
1029
+ const { json, status } = internalErrorToHttpError(error as Error);
1030
+ res.status(status).json(json);
1031
+ }
1032
+ },
1033
+ );
1034
+
1035
+ app.post(
1036
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/teardown`,
1037
+ async (req, res) => {
1038
+ try {
1039
+ const result = await materializationController.teardownPackage(
1040
+ req.params.projectName,
1041
+ req.params.packageName,
1042
+ req.body || {},
1043
+ );
1044
+ res.status(200).json(result);
1045
+ } catch (error) {
1046
+ const { json, status } = internalErrorToHttpError(error as Error);
1047
+ res.status(status).json(json);
1048
+ }
1049
+ },
1050
+ );
1051
+
1052
+ app.post(
1053
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1054
+ async (req, res) => {
1055
+ try {
1056
+ const action = req.query.action;
1057
+ if (action === "start") {
1058
+ const build = await materializationController.startMaterialization(
1059
+ req.params.projectName,
1060
+ req.params.packageName,
1061
+ req.params.materializationId,
1062
+ );
1063
+ res.status(202).json(build);
1064
+ } else if (action === "stop") {
1065
+ const build = await materializationController.stopMaterialization(
1066
+ req.params.projectName,
1067
+ req.params.packageName,
1068
+ req.params.materializationId,
1069
+ );
1070
+ res.status(200).json(build);
1071
+ } else {
1072
+ throw new BadRequestError(
1073
+ `Unsupported action '${String(action ?? "")}'. Expected 'start' or 'stop'.`,
1074
+ );
1075
+ }
1076
+ } catch (error) {
1077
+ const { json, status } = internalErrorToHttpError(error as Error);
1078
+ res.status(status).json(json);
1079
+ }
1080
+ },
1081
+ );
1082
+
1083
+ app.delete(
1084
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1085
+ async (req, res) => {
1086
+ try {
1087
+ await materializationController.deleteMaterialization(
1088
+ req.params.projectName,
1089
+ req.params.packageName,
1090
+ req.params.materializationId,
1091
+ );
1092
+ res.status(204).send();
1093
+ } catch (error) {
1094
+ const { json, status } = internalErrorToHttpError(error as Error);
1095
+ res.status(status).json(json);
1096
+ }
1097
+ },
1098
+ );
1099
+
1100
+ // ==================== MANIFEST ROUTES ====================
1101
+
1102
+ app.get(
1103
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1104
+ async (req, res) => {
1105
+ try {
1106
+ const manifest = await manifestController.getManifest(
1107
+ req.params.projectName,
1108
+ req.params.packageName,
1109
+ );
1110
+ res.status(200).json(manifest);
1111
+ } catch (error) {
1112
+ logger.error("Get manifest error", { error });
1113
+ const { json, status } = internalErrorToHttpError(error as Error);
1114
+ res.status(status).json(json);
1115
+ }
1116
+ },
1117
+ );
1118
+
1119
+ app.post(
1120
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1121
+ async (req, res) => {
1122
+ try {
1123
+ const action = req.query.action;
1124
+ if (action === "reload") {
1125
+ const manifest = await manifestController.reloadManifest(
1126
+ req.params.projectName,
1127
+ req.params.packageName,
1128
+ );
1129
+ res.status(200).json(manifest);
1130
+ } else {
1131
+ throw new BadRequestError(
1132
+ `Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
1133
+ );
1134
+ }
1135
+ } catch (error) {
1136
+ logger.error("Manifest action error", { error });
1137
+ const { json, status } = internalErrorToHttpError(error as Error);
1138
+ res.status(status).json(json);
1139
+ }
1140
+ },
1141
+ );
1142
+
935
1143
  // Modify the catch-all route to only serve index.html in production
936
1144
  if (!isDevelopment) {
937
1145
  app.get("*", (_req, res) => res.sendFile(path.resolve(ROOT, "index.html")));
@@ -1093,10 +1093,7 @@ export async function createProjectConnections(
1093
1093
  },
1094
1094
  poolOptions: {
1095
1095
  min: 1,
1096
- max: 5,
1097
- testOnBorrow: false,
1098
- testOnReturn: false,
1099
- testWhileIdle: true,
1096
+ max: 20,
1100
1097
  },
1101
1098
  };
1102
1099
  const snowflakeConnection = new SnowflakeConnection(