@malloy-publisher/server 0.0.205 → 0.0.206

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 (69) hide show
  1. package/dist/app/api-doc.yaml +363 -395
  2. package/dist/app/assets/{EnvironmentPage-CAge6UHD.js → EnvironmentPage-BYwBeC2F.js} +1 -1
  3. package/dist/app/assets/{HomePage-DhTe8qpa.js → HomePage-ivu4vdpj.js} +1 -1
  4. package/dist/app/assets/{MainPage-CeTxxGex.js → MainPage-B2DnHEDU.js} +2 -2
  5. package/dist/app/assets/{MaterializationsPage-CpDHB70t.js → MaterializationsPage-BZEuwF9P.js} +1 -1
  6. package/dist/app/assets/{ModelPage-D9sSMb75.js → ModelPage-Dpu3bfPg.js} +1 -1
  7. package/dist/app/assets/{PackagePage-LRqQWrFY.js → PackagePage-B8PwcRHt.js} +1 -1
  8. package/dist/app/assets/{RouteError-xT6kuCNw.js → RouteError-BhbywAeC.js} +1 -1
  9. package/dist/app/assets/{WorkbookPage-DsIh9svZ.js → WorkbookPage-C-JXsJG0.js} +1 -1
  10. package/dist/app/assets/{core-C2sQrwVu.es-Bjem0hym.js → core-pPlPr7jK.es-CNEOlxKB.js} +1 -1
  11. package/dist/app/assets/{index-BdOZDcce.js → index-BHEm8Egc.js} +1 -1
  12. package/dist/app/assets/{index-DHHAcY5o.js → index-BsvDrV14.js} +1 -1
  13. package/dist/app/assets/{index-RX3QOTde.js → index-ChR1fKR2.js} +127 -127
  14. package/dist/app/assets/{index.umd-D2WH3D-f.js → index.umd-BVLPYNuj.js} +1 -1
  15. package/dist/app/index.html +1 -1
  16. package/dist/instrumentation.mjs +18 -8
  17. package/dist/package_load_worker.mjs +19 -2
  18. package/dist/server.mjs +1151 -1264
  19. package/package.json +1 -1
  20. package/src/constants.ts +12 -0
  21. package/src/controller/materialization.controller.ts +79 -35
  22. package/src/controller/package.controller.spec.ts +179 -0
  23. package/src/controller/package.controller.ts +60 -73
  24. package/src/dto/package.dto.ts +16 -1
  25. package/src/errors.spec.ts +12 -0
  26. package/src/errors.ts +18 -0
  27. package/src/health.spec.ts +34 -1
  28. package/src/instrumentation.ts +33 -17
  29. package/src/materialization_metrics.ts +66 -0
  30. package/src/package_load/package_load_pool.ts +7 -1
  31. package/src/package_load/package_load_worker.ts +44 -4
  32. package/src/package_load/protocol.ts +7 -1
  33. package/src/server-old.ts +7 -149
  34. package/src/server.ts +9 -188
  35. package/src/service/authorize_integration.spec.ts +67 -0
  36. package/src/service/environment.ts +212 -12
  37. package/src/service/environment_store.spec.ts +0 -81
  38. package/src/service/environment_store.ts +0 -23
  39. package/src/service/explore_visibility.spec.ts +434 -0
  40. package/src/service/exports_probe.spec.ts +107 -0
  41. package/src/service/manifest_loader.spec.ts +99 -0
  42. package/src/service/manifest_loader.ts +95 -0
  43. package/src/service/materialization_service.spec.ts +324 -512
  44. package/src/service/materialization_service.ts +816 -656
  45. package/src/service/model.ts +400 -13
  46. package/src/service/package.spec.ts +14 -2
  47. package/src/service/package.ts +254 -14
  48. package/src/service/package_rollback.spec.ts +190 -0
  49. package/src/service/package_worker_path.spec.ts +223 -0
  50. package/src/service/query_boundary.spec.ts +470 -0
  51. package/src/storage/DatabaseInterface.ts +35 -57
  52. package/src/storage/StorageManager.mock.ts +0 -9
  53. package/src/storage/StorageManager.ts +7 -290
  54. package/src/storage/duckdb/DuckDBRepository.ts +2 -35
  55. package/src/storage/duckdb/MaterializationRepository.ts +52 -27
  56. package/src/storage/duckdb/schema.ts +4 -20
  57. package/tests/integration/materialization/manifest_binding.integration.spec.ts +175 -0
  58. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +277 -266
  59. package/tests/unit/duckdb/legacy_schema_migration.test.ts +7 -4
  60. package/src/controller/manifest.controller.ts +0 -38
  61. package/src/service/manifest_service.spec.ts +0 -206
  62. package/src/service/manifest_service.ts +0 -117
  63. package/src/service/materialized_table_gc.spec.ts +0 -384
  64. package/src/service/materialized_table_gc.ts +0 -231
  65. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  66. package/src/storage/duckdb/ManifestRepository.ts +0 -120
  67. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  68. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -155
  69. package/tests/unit/storage/StorageManager.test.ts +0 -166
@@ -1,7 +1,16 @@
1
- import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ spyOn,
9
+ } from "bun:test";
2
10
  import { Server } from "http";
3
11
  import { performGracefulShutdownAfterDrain } from "./health";
4
12
  import { logger } from "./logger";
13
+ import { __setPackageLoadPoolForTests } from "./package_load/package_load_pool";
5
14
 
6
15
  // Regression test for the graceful-shutdown ordering bug that caused
7
16
  // [winston] Attempt to write logs with no transports: {"message":"Waiting 50 seconds..."}
@@ -16,6 +25,18 @@ describe("performGracefulShutdownAfterDrain: shutdown ordering", () => {
16
25
  const originalExit = process.exit;
17
26
  let callOrder: string[];
18
27
 
28
+ afterAll(async () => {
29
+ // performGracefulShutdownAfterDrain drains the package-load worker pool
30
+ // via getPackageLoadPool().shutdown() — which lazily CREATES the global
31
+ // singleton and leaves it installed in its shut-down state. In prod the
32
+ // process exits right after, so that's fine; in this shared test process
33
+ // it poisons every later spec that touches the worker path
34
+ // (Package.create → "PackageLoadPool is shutting down"), with failures
35
+ // that come and go with bun's platform-dependent file order. Reset the
36
+ // singleton so the next user lazily creates a fresh pool.
37
+ await __setPackageLoadPoolForTests(null);
38
+ });
39
+
19
40
  beforeEach(() => {
20
41
  callOrder = [];
21
42
 
@@ -41,6 +62,18 @@ describe("performGracefulShutdownAfterDrain: shutdown ordering", () => {
41
62
  process.exit = originalExit;
42
63
  });
43
64
 
65
+ // performGracefulShutdownAfterDrain lazily creates and shuts down the
66
+ // module-level PackageLoadPool singleton. If we leave that shut-down
67
+ // pool installed, sibling specs that run later in the same Bun process
68
+ // (e.g. package.spec.ts, package_race.spec.ts) inherit a dead pool and
69
+ // fail with "PackageLoadPool is shutting down". Reset the singleton so
70
+ // the next getPackageLoadPool() spins up a fresh one. The leaked-pool
71
+ // ordering is filesystem-readdir dependent, which is why it only
72
+ // surfaced on Linux CI.
73
+ afterAll(async () => {
74
+ await __setPackageLoadPoolForTests(null);
75
+ });
76
+
44
77
  const fakeServer = (): Server => ({ listening: false }) as unknown as Server;
45
78
 
46
79
  it("logs the 'Waiting ...' message before closing the logger", async () => {
@@ -121,8 +121,20 @@ const httpRequestCount = meter.createCounter("http_server_requests_total", {
121
121
  // /health/liveness probe (a pure synchronous 200 handler) can fail under K8s,
122
122
  // so we surface p50/p99/max so an operator can correlate liveness restarts
123
123
  // with sustained event-loop pressure (large Malloy compiles, GC, etc.).
124
- const eventLoopHistogram = monitorEventLoopDelay({ resolution: 20 });
125
- eventLoopHistogram.enable();
124
+ //
125
+ // `monitorEventLoopDelay` is a Node-only API; some runtimes (e.g. Bun on
126
+ // certain platforms, used by the integration test suite) don't implement it.
127
+ // Degrade gracefully there rather than crashing module load: the metrics are
128
+ // simply not registered.
129
+ const eventLoopHistogram = (() => {
130
+ try {
131
+ const histogram = monitorEventLoopDelay({ resolution: 20 });
132
+ histogram.enable();
133
+ return histogram;
134
+ } catch {
135
+ return null;
136
+ }
137
+ })();
126
138
 
127
139
  const eventLoopLagP50 = meter.createObservableGauge(
128
140
  "publisher_event_loop_lag_p50_ms",
@@ -150,21 +162,25 @@ const eventLoopLagMax = meter.createObservableGauge(
150
162
  );
151
163
 
152
164
  // Sample all three in one batch so the histogram reset can't race the reads.
153
- meter.addBatchObservableCallback(
154
- (observableResult) => {
155
- observableResult.observe(
156
- eventLoopLagP50,
157
- eventLoopHistogram.percentile(50) / 1e6,
158
- );
159
- observableResult.observe(
160
- eventLoopLagP99,
161
- eventLoopHistogram.percentile(99) / 1e6,
162
- );
163
- observableResult.observe(eventLoopLagMax, eventLoopHistogram.max / 1e6);
164
- eventLoopHistogram.reset();
165
- },
166
- [eventLoopLagP50, eventLoopLagP99, eventLoopLagMax],
167
- );
165
+ // Skipped entirely when the runtime lacks event-loop-delay monitoring.
166
+ if (eventLoopHistogram) {
167
+ const histogram = eventLoopHistogram;
168
+ meter.addBatchObservableCallback(
169
+ (observableResult) => {
170
+ observableResult.observe(
171
+ eventLoopLagP50,
172
+ histogram.percentile(50) / 1e6,
173
+ );
174
+ observableResult.observe(
175
+ eventLoopLagP99,
176
+ histogram.percentile(99) / 1e6,
177
+ );
178
+ observableResult.observe(eventLoopLagMax, histogram.max / 1e6);
179
+ histogram.reset();
180
+ },
181
+ [eventLoopLagP50, eventLoopLagP99, eventLoopLagMax],
182
+ );
183
+ }
168
184
 
169
185
  const IGNORED_PATHS = new Set([
170
186
  "/health",
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Centralized telemetry for the two-round materialization protocol.
3
+ *
4
+ * Operators need to answer "are builds completing, how long do the two
5
+ * rounds take, and where are they failing?" without grepping logs. The
6
+ * counter carries `round` (`round1`|`round2`) and `outcome`
7
+ * (`success`|`failed`|`cancelled`); the histogram carries `round` so a
8
+ * dashboard can render Round 1 (compile+plan) vs Round 2 (build) latency
9
+ * side by side.
10
+ *
11
+ * Lazy init for the same reason as {@link ./query_cap_metrics}: instruments
12
+ * created before `setGlobalMeterProvider` bind to a NoOp meter
13
+ * (https://github.com/open-telemetry/opentelemetry-js/issues/3505).
14
+ */
15
+
16
+ import { metrics, type Counter, type Histogram } from "@opentelemetry/api";
17
+
18
+ export type MaterializationRound = "round1" | "round2" | "auto";
19
+ export type MaterializationOutcome = "success" | "failed" | "cancelled";
20
+
21
+ let roundCounter: Counter | null = null;
22
+ let roundDuration: Histogram | null = null;
23
+
24
+ function ensureTelemetry(): { counter: Counter; duration: Histogram } {
25
+ if (roundCounter && roundDuration) {
26
+ return { counter: roundCounter, duration: roundDuration };
27
+ }
28
+ const meter = metrics.getMeter("publisher");
29
+ if (!roundCounter) {
30
+ roundCounter = meter.createCounter(
31
+ "publisher_materialization_rounds_total",
32
+ {
33
+ description:
34
+ "Materialization rounds completed. Labels: round ('round1'|'round2'|'auto'), outcome ('success'|'failed'|'cancelled').",
35
+ },
36
+ );
37
+ }
38
+ if (!roundDuration) {
39
+ roundDuration = meter.createHistogram(
40
+ "publisher_materialization_round_duration_ms",
41
+ {
42
+ description:
43
+ "Wall-clock duration of a materialization round. Label: round ('round1'|'round2'|'auto').",
44
+ unit: "ms",
45
+ },
46
+ );
47
+ }
48
+ return { counter: roundCounter, duration: roundDuration };
49
+ }
50
+
51
+ /** Record the outcome and duration of a materialization round. */
52
+ export function recordMaterializationRound(
53
+ round: MaterializationRound,
54
+ outcome: MaterializationOutcome,
55
+ durationMs: number,
56
+ ): void {
57
+ const { counter, duration } = ensureTelemetry();
58
+ counter.add(1, { round, outcome });
59
+ duration.record(durationMs, { round });
60
+ }
61
+
62
+ /** Visible for tests. Drops cached instruments so a fresh MeterProvider can capture emissions. */
63
+ export function resetMaterializationTelemetryForTesting(): void {
64
+ roundCounter = null;
65
+ roundDuration = null;
66
+ }
@@ -228,7 +228,13 @@ export interface LoadPackageJob {
228
228
  * don't allocate a Map for models the caller never looks at.
229
229
  */
230
230
  export interface LoadPackageOutcome {
231
- packageMetadata: { name?: string; description?: string };
231
+ packageMetadata: {
232
+ name?: string;
233
+ description?: string;
234
+ explores?: string[];
235
+ queryableSources?: "declared" | "all";
236
+ manifestLocation?: string | null;
237
+ };
232
238
  models: Array<
233
239
  Omit<SerializedModel, "modelDef" | "sourceInfos"> & {
234
240
  modelDef?: ModelDef;
@@ -75,6 +75,7 @@ import { fileURLToPath, pathToFileURL } from "url";
75
75
 
76
76
  import {
77
77
  MODEL_FILE_SUFFIX,
78
+ normalizeModelPath,
78
79
  NOTEBOOK_FILE_SUFFIX,
79
80
  PACKAGE_MANIFEST_NAME,
80
81
  } from "../constants";
@@ -375,16 +376,38 @@ function buildWorkerMalloyConfig(job: LoadPackageRequest): MalloyConfig {
375
376
  // stays decoupled from the main-thread service module graph)
376
377
  // ──────────────────────────────────────────────────────────────────────
377
378
 
378
- async function readPackageMetadata(
379
- packagePath: string,
380
- ): Promise<{ name?: string; description?: string }> {
379
+ async function readPackageMetadata(packagePath: string): Promise<{
380
+ name?: string;
381
+ description?: string;
382
+ explores?: string[];
383
+ queryableSources?: "declared" | "all";
384
+ manifestLocation?: string | null;
385
+ }> {
381
386
  const manifestPath = path.join(packagePath, PACKAGE_MANIFEST_NAME);
382
387
  const contents = await fs.promises.readFile(manifestPath, "utf8");
383
388
  const parsed = JSON.parse(contents) as {
384
389
  name?: string;
385
390
  description?: string;
391
+ explores?: string[];
392
+ queryableSources?: unknown;
393
+ manifestLocation?: unknown;
394
+ };
395
+ return {
396
+ name: parsed.name,
397
+ description: parsed.description,
398
+ explores: Array.isArray(parsed.explores)
399
+ ? parsed.explores.map(normalizeModelPath)
400
+ : undefined,
401
+ // Default + invalid fall back to "declared" (fail-safe: queryable ==
402
+ // discoverable). Only an explicit "all" opts out of the query boundary.
403
+ queryableSources: parsed.queryableSources === "all" ? "all" : "declared",
404
+ // URI (gs://, s3://, file://, or local path) of the control-plane-computed
405
+ // build manifest. The main thread fetches + binds it after load.
406
+ manifestLocation:
407
+ typeof parsed.manifestLocation === "string"
408
+ ? parsed.manifestLocation
409
+ : null,
386
410
  };
387
- return { name: parsed.name, description: parsed.description };
388
411
  }
389
412
 
390
413
  async function listPackageFiles(packagePath: string): Promise<string[]> {
@@ -400,6 +423,18 @@ function filterModelPaths(allRelative: string[]): string[] {
400
423
  );
401
424
  }
402
425
 
426
+ // `normalizeModelPath` (shared, from ../constants) runs here at parse time so
427
+ // the keys stored in Package.models are already normalized, and the API
428
+ // publish/update path normalizes its `explores` input through the same helper —
429
+ // so on-disk and API-written manifests share one representation.
430
+
431
+ // explores validation is intentionally NOT done here. The worker is the
432
+ // shared load path (startup, reload, AND publish), but the policy differs by
433
+ // context — strict-reject at publish, warn-and-fail-safe at load — so it lives
434
+ // on the main thread (see Package.getInvalidExplores / loadViaWorker and the
435
+ // publish path in package.controller). An unmatched entry simply lists nothing
436
+ // (listModels filters by set membership), which is the fail-safe outcome.
437
+
403
438
  // ──────────────────────────────────────────────────────────────────────
404
439
  // Model compile (mirrors service/model.ts but produces SerializedModel)
405
440
  // ──────────────────────────────────────────────────────────────────────
@@ -558,6 +593,10 @@ async function compileMalloyModel(
558
593
  modelDef,
559
594
  modelInfo: modelDefToModelInfo(modelDef),
560
595
  sourceInfos,
596
+ // `sources`/`queries` ship complete (authorize + filter enforcement and
597
+ // join resolution read the full set); the Model's discovery accessors
598
+ // filter them down to the export closure (`modelDef.exports`) to match
599
+ // `modelInfo`/`sourceInfos`.
561
600
  sources,
562
601
  queries,
563
602
  filterMap: Array.from(filterMap.entries()),
@@ -800,6 +839,7 @@ async function loadPackage(
800
839
 
801
840
  const allFiles = await listPackageFiles(job.packagePath);
802
841
  const modelPaths = filterModelPaths(allFiles);
842
+
803
843
  const models = await Promise.all(
804
844
  modelPaths.map((modelPath) =>
805
845
  compileOneModel(job, malloyConfig, modelPath),
@@ -173,7 +173,13 @@ export interface SerializedNotebookCell {
173
173
  export interface LoadPackageResult {
174
174
  type: "load-package-result";
175
175
  requestId: string;
176
- packageMetadata: { name?: string; description?: string };
176
+ packageMetadata: {
177
+ name?: string;
178
+ description?: string;
179
+ explores?: string[];
180
+ queryableSources?: "declared" | "all";
181
+ manifestLocation?: string | null;
182
+ };
177
183
  models: SerializedModel[];
178
184
  /** Wall-clock ms inside the worker for the full package load. */
179
185
  loadDurationMs: number;
package/src/server-old.ts CHANGED
@@ -30,7 +30,6 @@ import { ParsedQs } from "qs";
30
30
  import { CompileController } from "./controller/compile.controller";
31
31
  import { ConnectionController } from "./controller/connection.controller";
32
32
  import { DatabaseController } from "./controller/database.controller";
33
- import { ManifestController } from "./controller/manifest.controller";
34
33
  import { MaterializationController } from "./controller/materialization.controller";
35
34
  import { ModelController } from "./controller/model.controller";
36
35
  import { PackageController } from "./controller/package.controller";
@@ -57,7 +56,6 @@ export interface LegacyControllerSet {
57
56
  queryController: QueryController;
58
57
  compileController: CompileController;
59
58
  materializationController: MaterializationController;
60
- manifestController: ManifestController;
61
59
  }
62
60
 
63
61
  // ─── response/body field mappers ───────────────────────────────────────────
@@ -97,7 +95,6 @@ export function registerLegacyRoutes(
97
95
  queryController,
98
96
  compileController,
99
97
  materializationController,
100
- manifestController,
101
98
  } = controllers;
102
99
 
103
100
  // body-parser is already registered on the main app for `${API_PREFIX}/*`
@@ -379,26 +376,7 @@ export function registerLegacyRoutes(
379
376
  },
380
377
  );
381
378
 
382
- // sqlSource (deprecated GET + supported POST), per-project + per-package
383
- app.get(
384
- `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlSource`,
385
- async (req, res) => {
386
- try {
387
- res.status(200).json(
388
- await connectionController.getConnectionSqlSource(
389
- req.params.projectName,
390
- req.params.connectionName,
391
- req.query.sqlStatement as string,
392
- ),
393
- );
394
- } catch (error) {
395
- logger.error(error);
396
- const { json, status } = internalErrorToHttpError(error as Error);
397
- res.status(status).json(json);
398
- }
399
- },
400
- );
401
-
379
+ // sqlSource (POST), per-project + per-package
402
380
  app.post(
403
381
  `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlSource`,
404
382
  async (req, res) => {
@@ -418,26 +396,6 @@ export function registerLegacyRoutes(
418
396
  },
419
397
  );
420
398
 
421
- app.get(
422
- `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
423
- async (req, res) => {
424
- try {
425
- res.status(200).json(
426
- await connectionController.getConnectionSqlSource(
427
- req.params.projectName,
428
- req.params.connectionName,
429
- req.query.sqlStatement as string,
430
- req.params.packageName,
431
- ),
432
- );
433
- } catch (error) {
434
- logger.error(error);
435
- const { json, status } = internalErrorToHttpError(error as Error);
436
- res.status(status).json(json);
437
- }
438
- },
439
- );
440
-
441
399
  app.post(
442
400
  `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/sqlSource`,
443
401
  async (req, res) => {
@@ -567,48 +525,7 @@ export function registerLegacyRoutes(
567
525
  },
568
526
  );
569
527
 
570
- // temporaryTable (deprecated GET) + sqlTemporaryTable (supported POST)
571
- app.get(
572
- `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/temporaryTable`,
573
- queryConcurrency(),
574
- async (req, res) => {
575
- try {
576
- res.status(200).json(
577
- await connectionController.getConnectionTemporaryTable(
578
- req.params.projectName,
579
- req.params.connectionName,
580
- req.query.sqlStatement as string,
581
- ),
582
- );
583
- } catch (error) {
584
- logger.error(error);
585
- const { json, status } = internalErrorToHttpError(error as Error);
586
- res.status(status).json(json);
587
- }
588
- },
589
- );
590
-
591
- app.get(
592
- `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/connections/:connectionName/temporaryTable`,
593
- queryConcurrency(),
594
- async (req, res) => {
595
- try {
596
- res.status(200).json(
597
- await connectionController.getConnectionTemporaryTable(
598
- req.params.projectName,
599
- req.params.connectionName,
600
- req.query.sqlStatement as string,
601
- req.params.packageName,
602
- ),
603
- );
604
- } catch (error) {
605
- logger.error(error);
606
- const { json, status } = internalErrorToHttpError(error as Error);
607
- res.status(status).json(json);
608
- }
609
- },
610
- );
611
-
528
+ // sqlTemporaryTable (POST), per-project + per-package
612
529
  app.post(
613
530
  `${LEGACY_API_PREFIX}/projects/:projectName/connections/:connectionName/sqlTemporaryTable`,
614
531
  queryConcurrency(),
@@ -674,11 +591,9 @@ export function registerLegacyRoutes(
674
591
  `${LEGACY_API_PREFIX}/projects/:projectName/packages`,
675
592
  async (req, res) => {
676
593
  try {
677
- const autoLoadManifest = req.query.autoLoadManifest === "true";
678
594
  const _package = await packageController.addPackage(
679
595
  req.params.projectName,
680
596
  req.body,
681
- { autoLoadManifest },
682
597
  );
683
598
  res.status(200).json(_package?.getPackageMetadata());
684
599
  } catch (error) {
@@ -1024,34 +939,18 @@ export function registerLegacyRoutes(
1024
939
  },
1025
940
  );
1026
941
 
1027
- app.post(
1028
- `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/teardown`,
1029
- async (req, res) => {
1030
- try {
1031
- const result = await materializationController.teardownPackage(
1032
- req.params.projectName,
1033
- req.params.packageName,
1034
- req.body || {},
1035
- );
1036
- res.status(200).json(result);
1037
- } catch (error) {
1038
- const { json, status } = internalErrorToHttpError(error as Error);
1039
- res.status(status).json(json);
1040
- }
1041
- },
1042
- );
1043
-
1044
942
  app.post(
1045
943
  `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/materializations/:materializationId`,
1046
944
  async (req, res) => {
1047
945
  try {
1048
946
  const action = req.query.action;
1049
- if (action === "start") {
947
+ if (action === "build") {
1050
948
  const build =
1051
- await materializationController.startMaterialization(
949
+ await materializationController.buildMaterialization(
1052
950
  req.params.projectName,
1053
951
  req.params.packageName,
1054
952
  req.params.materializationId,
953
+ req.body || {},
1055
954
  );
1056
955
  res.status(202).json(remapMaterializationResponse(build));
1057
956
  } else if (action === "stop") {
@@ -1064,7 +963,7 @@ export function registerLegacyRoutes(
1064
963
  res.status(200).json(remapMaterializationResponse(build));
1065
964
  } else {
1066
965
  throw new BadRequestError(
1067
- `Unsupported action '${String(action ?? "")}'. Expected 'start' or 'stop'.`,
966
+ `Unsupported action '${String(action ?? "")}'. Expected 'build' or 'stop'.`,
1068
967
  );
1069
968
  }
1070
969
  } catch (error) {
@@ -1082,6 +981,7 @@ export function registerLegacyRoutes(
1082
981
  req.params.projectName,
1083
982
  req.params.packageName,
1084
983
  req.params.materializationId,
984
+ { dropTables: req.query.dropTables === "true" },
1085
985
  );
1086
986
  res.status(204).send();
1087
987
  } catch (error) {
@@ -1091,48 +991,6 @@ export function registerLegacyRoutes(
1091
991
  },
1092
992
  );
1093
993
 
1094
- // ── manifest ────────────────────────────────────────────────────────────
1095
- app.get(
1096
- `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1097
- async (req, res) => {
1098
- try {
1099
- const manifest = await manifestController.getManifest(
1100
- req.params.projectName,
1101
- req.params.packageName,
1102
- );
1103
- res.status(200).json(manifest);
1104
- } catch (error) {
1105
- logger.error("Get manifest error", { error });
1106
- const { json, status } = internalErrorToHttpError(error as Error);
1107
- res.status(status).json(json);
1108
- }
1109
- },
1110
- );
1111
-
1112
- app.post(
1113
- `${LEGACY_API_PREFIX}/projects/:projectName/packages/:packageName/manifest`,
1114
- async (req, res) => {
1115
- try {
1116
- const action = req.query.action;
1117
- if (action === "reload") {
1118
- const manifest = await manifestController.reloadManifest(
1119
- req.params.projectName,
1120
- req.params.packageName,
1121
- );
1122
- res.status(200).json(manifest);
1123
- } else {
1124
- throw new BadRequestError(
1125
- `Unsupported action '${String(action ?? "")}'. Expected 'reload'.`,
1126
- );
1127
- }
1128
- } catch (error) {
1129
- logger.error("Manifest action error", { error });
1130
- const { json, status } = internalErrorToHttpError(error as Error);
1131
- res.status(status).json(json);
1132
- }
1133
- },
1134
- );
1135
-
1136
994
  logger.info(
1137
995
  "Legacy /projects/* routes registered for backwards compatibility",
1138
996
  );