@malloy-publisher/server 0.0.178 → 0.0.180-dev-v1

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 +1 -1
  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-JYvDXOkC.js → MainPage-BzB3yoqi.js} +2 -2
  5. package/dist/app/assets/{ModelPage-TEQrhaqq.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-DnSZEzkT.js → RouteError-DAShbVCG.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-DjQ8u5DD.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--80Q7qw1.js → index-Bb2jqquW.js} +1 -1
  15. package/dist/app/assets/{index-CZ4G_NMp.js → index-D68X76-7.js} +168 -166
  16. package/dist/app/assets/index.umd-DGBekgSu.js +1145 -0
  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 +10567 -10584
  22. package/dist/server.js +16973 -15367
  23. package/package.json +14 -12
  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/resources/model_resource.ts +12 -9
  32. package/src/mcp/resources/source_resource.ts +7 -6
  33. package/src/mcp/resources/view_resource.ts +0 -1
  34. package/src/mcp/tools/execute_query_tool.ts +9 -0
  35. package/src/server.ts +217 -5
  36. package/src/service/connection.ts +1 -4
  37. package/src/service/db_utils.spec.ts +4 -2
  38. package/src/service/db_utils.ts +6 -2
  39. package/src/service/filter.spec.ts +447 -0
  40. package/src/service/filter.ts +337 -0
  41. package/src/service/filter_integration.spec.ts +825 -0
  42. package/src/service/manifest_service.spec.ts +201 -0
  43. package/src/service/manifest_service.ts +106 -0
  44. package/src/service/materialization_service.spec.ts +648 -0
  45. package/src/service/materialization_service.ts +929 -0
  46. package/src/service/materialized_table_gc.spec.ts +383 -0
  47. package/src/service/materialized_table_gc.ts +279 -0
  48. package/src/service/model.ts +221 -47
  49. package/src/service/package.ts +50 -0
  50. package/src/service/project_store.ts +21 -2
  51. package/src/service/quoting.ts +41 -0
  52. package/src/service/resolve_project.ts +13 -0
  53. package/src/storage/DatabaseInterface.ts +103 -1
  54. package/src/storage/{StorageManager.spec.ts → StorageManager.mock.ts} +9 -0
  55. package/src/storage/StorageManager.ts +119 -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/dist/app/assets/HomePage-CwUkFsA8.js +0 -1
  71. package/dist/app/assets/PackagePage-CgE-izLw.js +0 -1
  72. package/dist/app/assets/ProjectPage-PiMPpFX8.js +0 -1
  73. package/dist/app/assets/index-BJUsHnGO.js +0 -467
  74. package/dist/app/assets/index.umd-Cf-wqh-R.js +0 -1145
package/src/server.ts CHANGED
@@ -20,7 +20,11 @@ import { ModelController } from "./controller/model.controller";
20
20
  import { PackageController } from "./controller/package.controller";
21
21
  import { QueryController } from "./controller/query.controller";
22
22
  import { WatchModeController } from "./controller/watch-mode.controller";
23
- import { internalErrorToHttpError, NotImplementedError } from "./errors";
23
+ import {
24
+ BadRequestError,
25
+ internalErrorToHttpError,
26
+ NotImplementedError,
27
+ } from "./errors";
24
28
  import {
25
29
  drainingGuard,
26
30
  registerHealthEndpoints,
@@ -28,7 +32,11 @@ import {
28
32
  } from "./health";
29
33
  import { logger, loggerMiddleware } from "./logger";
30
34
 
35
+ import { ManifestController } from "./controller/manifest.controller";
36
+ import { MaterializationController } from "./controller/materialization.controller";
31
37
  import { initializeMcpServer } from "./mcp/server";
38
+ import { ManifestService } from "./service/manifest_service";
39
+ import { MaterializationService } from "./service/materialization_service";
32
40
  import { ProjectStore } from "./service/project_store";
33
41
 
34
42
  /** Normalize an Express query param into a string[] or undefined. */
@@ -125,17 +133,29 @@ const SERVER_ROOT = path.resolve(process.cwd(), process.env.SERVER_ROOT || ".");
125
133
  const API_PREFIX = "/api/v0";
126
134
  const isDevelopment = process.env["NODE_ENV"] === "development";
127
135
 
128
- const app = express();
136
+ export const app = express();
129
137
  app.use(loggerMiddleware);
130
138
  app.use(httpMetricsMiddleware);
131
139
  const projectStore = new ProjectStore(SERVER_ROOT);
140
+ const manifestService = new ManifestService(projectStore);
132
141
  const watchModeController = new WatchModeController(projectStore);
133
142
  const connectionController = new ConnectionController(projectStore);
134
143
  const modelController = new ModelController(projectStore);
135
- const packageController = new PackageController(projectStore);
144
+ const packageController = new PackageController(projectStore, manifestService);
136
145
  const databaseController = new DatabaseController(projectStore);
137
146
  const queryController = new QueryController(projectStore);
138
147
  const compileController = new CompileController(projectStore);
148
+ const materializationService = new MaterializationService(
149
+ projectStore,
150
+ manifestService,
151
+ );
152
+ const materializationController = new MaterializationController(
153
+ materializationService,
154
+ );
155
+ const manifestController = new ManifestController(
156
+ projectStore,
157
+ manifestService,
158
+ );
139
159
 
140
160
  export const mcpApp = express();
141
161
 
@@ -262,7 +282,9 @@ app.use(
262
282
  credentials: true,
263
283
  }),
264
284
  );
265
- app.use(bodyParser.json());
285
+
286
+ // Set body-parser JSON limit to 1Mb (default: 100kb)
287
+ app.use(bodyParser.json({ limit: "1mb" }));
266
288
 
267
289
  // Register health check endpoints on main app:
268
290
  // - Required for production/Kubernetes monitoring (main server on PUBLISHER_PORT)
@@ -579,7 +601,7 @@ app.post(
579
601
  req.params.projectName,
580
602
  req.params.connectionName,
581
603
  req.body.sqlStatement as string,
582
- req.query.options as string,
604
+ req.body.options as string,
583
605
  ),
584
606
  );
585
607
  } catch (error) {
@@ -650,9 +672,11 @@ app.get(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
650
672
 
651
673
  app.post(`${API_PREFIX}/projects/:projectName/packages`, async (req, res) => {
652
674
  try {
675
+ const autoLoadManifest = req.query.autoLoadManifest === "true";
653
676
  const _package = await packageController.addPackage(
654
677
  req.params.projectName,
655
678
  req.body,
679
+ { autoLoadManifest },
656
680
  );
657
681
  res.status(200).json(_package?.getPackageMetadata());
658
682
  } catch (error) {
@@ -817,12 +841,29 @@ app.get(
817
841
  // Express stores wildcard matches in params['0']
818
842
  const notebookPath = (req.params as Record<string, string>)["0"];
819
843
 
844
+ // Parse optional filter_params (JSON query string) and bypass_filters
845
+ let filterParams: Record<string, string | string[]> | undefined;
846
+ if (typeof req.query.filter_params === "string") {
847
+ try {
848
+ filterParams = JSON.parse(req.query.filter_params);
849
+ } catch {
850
+ res.status(400).json({
851
+ error: "Invalid filter_params: must be valid JSON",
852
+ });
853
+ return;
854
+ }
855
+ }
856
+ const bypassFilters =
857
+ req.query.bypass_filters === "true" ? true : undefined;
858
+
820
859
  res.status(200).json(
821
860
  await modelController.executeNotebookCell(
822
861
  req.params.projectName,
823
862
  req.params.packageName,
824
863
  notebookPath,
825
864
  cellIndex,
865
+ filterParams,
866
+ bypassFilters,
826
867
  ),
827
868
  );
828
869
  } catch (error) {
@@ -879,6 +920,10 @@ app.post(
879
920
  req.body.queryName as string,
880
921
  req.body.query as string,
881
922
  req.body.compactJson === true,
923
+ (req.body.filterParams ?? req.body.sourceFilters) as
924
+ | Record<string, string | string[]>
925
+ | undefined,
926
+ req.body.bypassFilters === true ? true : undefined,
882
927
  ),
883
928
  );
884
929
  } catch (error) {
@@ -932,6 +977,173 @@ app.post(
932
977
  },
933
978
  );
934
979
 
980
+ // ==================== MATERIALIZATION ROUTES ====================
981
+
982
+ app.post(
983
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
984
+ async (req, res) => {
985
+ try {
986
+ const build = await materializationController.createMaterialization(
987
+ req.params.projectName,
988
+ req.params.packageName,
989
+ req.body || {},
990
+ );
991
+ res.status(201).json(build);
992
+ } catch (error) {
993
+ const { json, status } = internalErrorToHttpError(error as Error);
994
+ res.status(status).json(json);
995
+ }
996
+ },
997
+ );
998
+
999
+ app.get(
1000
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations`,
1001
+ async (req, res) => {
1002
+ try {
1003
+ const limit = req.query.limit
1004
+ ? parseInt(req.query.limit as string, 10)
1005
+ : undefined;
1006
+ const offset = req.query.offset
1007
+ ? parseInt(req.query.offset as string, 10)
1008
+ : undefined;
1009
+ const builds = await materializationController.listMaterializations(
1010
+ req.params.projectName,
1011
+ req.params.packageName,
1012
+ { limit, offset },
1013
+ );
1014
+ res.status(200).json(builds);
1015
+ } catch (error) {
1016
+ const { json, status } = internalErrorToHttpError(error as Error);
1017
+ res.status(status).json(json);
1018
+ }
1019
+ },
1020
+ );
1021
+
1022
+ app.get(
1023
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1024
+ async (req, res) => {
1025
+ try {
1026
+ const build = await materializationController.getMaterialization(
1027
+ req.params.projectName,
1028
+ req.params.packageName,
1029
+ req.params.materializationId,
1030
+ );
1031
+ res.status(200).json(build);
1032
+ } catch (error) {
1033
+ const { json, status } = internalErrorToHttpError(error as Error);
1034
+ res.status(status).json(json);
1035
+ }
1036
+ },
1037
+ );
1038
+
1039
+ app.post(
1040
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/teardown`,
1041
+ async (req, res) => {
1042
+ try {
1043
+ const result = await materializationController.teardownPackage(
1044
+ req.params.projectName,
1045
+ req.params.packageName,
1046
+ req.body || {},
1047
+ );
1048
+ res.status(200).json(result);
1049
+ } catch (error) {
1050
+ const { json, status } = internalErrorToHttpError(error as Error);
1051
+ res.status(status).json(json);
1052
+ }
1053
+ },
1054
+ );
1055
+
1056
+ app.post(
1057
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1058
+ async (req, res) => {
1059
+ try {
1060
+ const action = req.query.action;
1061
+ if (action === "start") {
1062
+ const build = await materializationController.startMaterialization(
1063
+ req.params.projectName,
1064
+ req.params.packageName,
1065
+ req.params.materializationId,
1066
+ );
1067
+ res.status(202).json(build);
1068
+ } else if (action === "stop") {
1069
+ const build = await materializationController.stopMaterialization(
1070
+ req.params.projectName,
1071
+ req.params.packageName,
1072
+ req.params.materializationId,
1073
+ );
1074
+ res.status(200).json(build);
1075
+ } else {
1076
+ throw new BadRequestError(
1077
+ `Unsupported action '${String(action ?? "")}'. Expected 'start' or 'stop'.`,
1078
+ );
1079
+ }
1080
+ } catch (error) {
1081
+ const { json, status } = internalErrorToHttpError(error as Error);
1082
+ res.status(status).json(json);
1083
+ }
1084
+ },
1085
+ );
1086
+
1087
+ app.delete(
1088
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1089
+ async (req, res) => {
1090
+ try {
1091
+ await materializationController.deleteMaterialization(
1092
+ req.params.projectName,
1093
+ req.params.packageName,
1094
+ req.params.materializationId,
1095
+ );
1096
+ res.status(204).send();
1097
+ } catch (error) {
1098
+ const { json, status } = internalErrorToHttpError(error as Error);
1099
+ res.status(status).json(json);
1100
+ }
1101
+ },
1102
+ );
1103
+
1104
+ // ==================== MANIFEST ROUTES ====================
1105
+
1106
+ app.get(
1107
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1108
+ async (req, res) => {
1109
+ try {
1110
+ const manifest = await manifestController.getManifest(
1111
+ req.params.projectName,
1112
+ req.params.packageName,
1113
+ );
1114
+ res.status(200).json(manifest);
1115
+ } catch (error) {
1116
+ logger.error("Get manifest error", { error });
1117
+ const { json, status } = internalErrorToHttpError(error as Error);
1118
+ res.status(status).json(json);
1119
+ }
1120
+ },
1121
+ );
1122
+
1123
+ app.post(
1124
+ `${API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1125
+ async (req, res) => {
1126
+ try {
1127
+ const action = req.query.action;
1128
+ if (action === "reload") {
1129
+ const manifest = await manifestController.reloadManifest(
1130
+ req.params.projectName,
1131
+ req.params.packageName,
1132
+ );
1133
+ res.status(200).json(manifest);
1134
+ } else {
1135
+ throw new BadRequestError(
1136
+ `Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
1137
+ );
1138
+ }
1139
+ } catch (error) {
1140
+ logger.error("Manifest action error", { error });
1141
+ const { json, status } = internalErrorToHttpError(error as Error);
1142
+ res.status(status).json(json);
1143
+ }
1144
+ },
1145
+ );
1146
+
935
1147
  // Modify the catch-all route to only serve index.html in production
936
1148
  if (!isDevelopment) {
937
1149
  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(
@@ -167,10 +167,11 @@ describe("listTablesForSchema", () => {
167
167
  },
168
168
  };
169
169
 
170
- it("queries information_schema.columns with correct schema", async () => {
170
+ it("queries information_schema.columns wrapped in row_to_json", async () => {
171
171
  const m = mockConnection(columnRows);
172
172
  const tables = await listTablesForSchema(conn, "public", m.conn);
173
173
 
174
+ expect(m.lastSQL).toContain("row_to_json");
174
175
  expect(m.lastSQL).toContain("information_schema.columns");
175
176
  expect(m.lastSQL).toContain("table_schema = 'public'");
176
177
  expect(tables).toHaveLength(2);
@@ -470,7 +471,7 @@ describe("getSchemasForConnection", () => {
470
471
  },
471
472
  };
472
473
 
473
- it("queries information_schema.schemata", async () => {
474
+ it("queries information_schema.schemata wrapped in row_to_json", async () => {
474
475
  const rows = [
475
476
  { schema_name: "public" },
476
477
  { schema_name: "information_schema" },
@@ -480,6 +481,7 @@ describe("getSchemasForConnection", () => {
480
481
  const m = mockConnection(rows);
481
482
  const schemas = await getSchemasForConnection(conn, m.conn);
482
483
 
484
+ expect(m.lastSQL).toContain("row_to_json");
483
485
  expect(m.lastSQL).toContain("information_schema.schemata");
484
486
  expect(schemas).toHaveLength(4);
485
487
  expect(schemas.find((s) => s.name === "public")?.isDefault).toBe(true);
@@ -175,8 +175,10 @@ async function getSchemasForPostgres(
175
175
  throw new Error("Postgres connection is required");
176
176
  }
177
177
  try {
178
+ // Wrap in row_to_json because the Malloy Postgres driver's runSQL
179
+ // de-JSONs each row via row.row (matching Malloy-generated queries).
178
180
  const result = await malloyConnection.runSQL(
179
- "SELECT schema_name FROM information_schema.schemata ORDER BY schema_name",
181
+ "SELECT row_to_json(t) as row FROM (SELECT schema_name FROM information_schema.schemata ORDER BY schema_name) t",
180
182
  );
181
183
  const rows = standardizeRunSQLResult(result);
182
184
  return rows.map((row: unknown) => {
@@ -946,8 +948,10 @@ async function listTablesForPostgres(
946
948
  throw new Error("Postgres connection is required");
947
949
  }
948
950
  try {
951
+ // Wrap in row_to_json because the Malloy Postgres driver's runSQL
952
+ // de-JSONs each row via row.row (matching Malloy-generated queries).
949
953
  const result = await malloyConnection.runSQL(
950
- `SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = '${schemaName}' ${sqlInFilter("table_name", tableNames)} ORDER BY table_name, ordinal_position`,
954
+ `SELECT row_to_json(t) as row FROM (SELECT table_name, column_name, data_type FROM information_schema.columns WHERE table_schema = '${schemaName}' ${sqlInFilter("table_name", tableNames)} ORDER BY table_name, ordinal_position) t`,
951
955
  );
952
956
  const rows = standardizeRunSQLResult(result);
953
957
  return groupColumnRowsIntoTables(rows, (t) => `${schemaName}.${t}`);