@malloy-publisher/server 0.0.382-dev → 0.2.1

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 (175) hide show
  1. package/README.docker.md +145 -20
  2. package/README.md +65 -12
  3. package/dist/app/api-doc.html +5 -0
  4. package/dist/app/api-doc.yaml +3929 -534
  5. package/dist/app/assets/EnvironmentPage-CHq5BF4l.js +1 -0
  6. package/dist/app/assets/HomePage-CIp-pJCa.js +1 -0
  7. package/dist/app/assets/LightMode-mHHPjEeA.js +1 -0
  8. package/dist/app/assets/MainPage-CylxO5Yu.js +2 -0
  9. package/dist/app/assets/MaterializationsPage-a0vjqxib.js +1 -0
  10. package/dist/app/assets/ModelPage-Cz-MrSZQ.js +1 -0
  11. package/dist/app/assets/PackagePage-Cdg5D7AN.js +1 -0
  12. package/dist/app/assets/RouteError-B5SIyn6G.js +1 -0
  13. package/dist/app/assets/ThemeEditorPage-B62rRc-y.js +1 -0
  14. package/dist/app/assets/WorkbookPage-45cG1Gx6.js +1 -0
  15. package/dist/app/assets/{core-CjeTkq8O.es-BqRc6yhC.js → core-DyU47reh.es-DY9b3WjS.js} +1 -1
  16. package/dist/app/assets/github-dark-DenFmJkN.es-DHJKELXO.js +1 -0
  17. package/dist/app/assets/index-5eLCcNmP.css +1 -0
  18. package/dist/app/assets/index-C5ClsnS7.js +1774 -0
  19. package/dist/app/assets/index-DF9tVqwN.js +1 -0
  20. package/dist/app/assets/index-DSoCUF-f.js +2725 -0
  21. package/dist/app/assets/index-DgVRz2wm.js +2 -0
  22. package/dist/app/assets/index-P-pF8nU5.js +632 -0
  23. package/dist/app/assets/index-w_0OQJgZ.js +23 -0
  24. package/dist/app/index.html +8 -4
  25. package/dist/cpufeatures-1yrn0vtw.node +0 -0
  26. package/dist/default-publisher.config.json +23 -0
  27. package/dist/{instrumentation.js → instrumentation.mjs} +10631 -10573
  28. package/dist/package_load_worker.mjs +15219 -0
  29. package/dist/runtime/publisher.js +331 -0
  30. package/dist/{server.js → server.mjs} +141276 -74705
  31. package/dist/sshcrypto-vd2k5hq9.node +0 -0
  32. package/package.json +41 -28
  33. package/publisher.config.example.bigquery.json +33 -0
  34. package/publisher.config.example.duckdb.json +23 -0
  35. package/.eslintrc.json +0 -44
  36. package/.prettierignore +0 -1
  37. package/.prettierrc +0 -4
  38. package/build.ts +0 -38
  39. package/dist/app/assets/HomePage-Dn3E4CuB.js +0 -1
  40. package/dist/app/assets/MainPage-BzB3yoqi.js +0 -2
  41. package/dist/app/assets/ModelPage-C9O_sAXT.js +0 -1
  42. package/dist/app/assets/PackagePage-DcxKEjBX.js +0 -1
  43. package/dist/app/assets/ProjectPage-BDj307rF.js +0 -1
  44. package/dist/app/assets/RouteError-DAShbVCG.js +0 -1
  45. package/dist/app/assets/WorkbookPage-Cs_XYEaB.js +0 -1
  46. package/dist/app/assets/index-15BOvhp0.js +0 -456
  47. package/dist/app/assets/index-Bb2jqquW.js +0 -676
  48. package/dist/app/assets/index-CMlGQMcl.css +0 -1
  49. package/dist/app/assets/index-D68X76-7.js +0 -1276
  50. package/dist/app/assets/index.umd-DGBekgSu.js +0 -1145
  51. package/dxt/malloy_bridge.py +0 -354
  52. package/dxt/manifest.json +0 -22
  53. package/eslint.config.mjs +0 -76
  54. package/k6-tests/README.md +0 -144
  55. package/k6-tests/bun.lock +0 -797
  56. package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
  57. package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
  58. package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
  59. package/k6-tests/load-test/load-test-crud.ts +0 -142
  60. package/k6-tests/load-test/load-test.ts +0 -784
  61. package/k6-tests/package.json +0 -27
  62. package/k6-tests/scripts/run-with-otel.sh +0 -50
  63. package/k6-tests/smoke-test/smoke-test.ts +0 -158
  64. package/k6-tests/tsconfig.json +0 -29
  65. package/k6-tests/utils/client_factory.ts +0 -128
  66. package/k6-tests/utils/common.ts +0 -489
  67. package/k6-tests/utils/logger.ts +0 -62
  68. package/k6-tests/utils/types.d.ts +0 -35
  69. package/malloy_mcp.dxt +0 -0
  70. package/publisher.config.json +0 -33
  71. package/src/config.spec.ts +0 -850
  72. package/src/config.ts +0 -305
  73. package/src/constants.ts +0 -10
  74. package/src/controller/compile.controller.ts +0 -35
  75. package/src/controller/connection.controller.ts +0 -515
  76. package/src/controller/database.controller.ts +0 -21
  77. package/src/controller/manifest.controller.ts +0 -29
  78. package/src/controller/materialization.controller.ts +0 -125
  79. package/src/controller/model.controller.ts +0 -107
  80. package/src/controller/package.controller.ts +0 -182
  81. package/src/controller/query.controller.ts +0 -60
  82. package/src/controller/watch-mode.controller.ts +0 -98
  83. package/src/data_styles.ts +0 -71
  84. package/src/dto/connection.dto.spec.ts +0 -131
  85. package/src/dto/connection.dto.ts +0 -174
  86. package/src/dto/index.ts +0 -2
  87. package/src/dto/package.dto.spec.ts +0 -42
  88. package/src/dto/package.dto.ts +0 -12
  89. package/src/dto/validate.spec.ts +0 -76
  90. package/src/dto/validate.ts +0 -31
  91. package/src/errors.ts +0 -116
  92. package/src/health.ts +0 -189
  93. package/src/instrumentation.ts +0 -148
  94. package/src/logger.ts +0 -154
  95. package/src/mcp/error_messages.ts +0 -199
  96. package/src/mcp/handler_utils.ts +0 -239
  97. package/src/mcp/mcp_constants.ts +0 -22
  98. package/src/mcp/prompts/handlers.ts +0 -84
  99. package/src/mcp/prompts/index.ts +0 -11
  100. package/src/mcp/prompts/prompt_definitions.ts +0 -160
  101. package/src/mcp/prompts/prompt_service.ts +0 -67
  102. package/src/mcp/prompts/utils.ts +0 -62
  103. package/src/mcp/resource_metadata.ts +0 -47
  104. package/src/mcp/resources/model_resource.ts +0 -153
  105. package/src/mcp/resources/notebook_resource.ts +0 -137
  106. package/src/mcp/resources/package_resource.ts +0 -370
  107. package/src/mcp/resources/project_resource.ts +0 -184
  108. package/src/mcp/resources/query_resource.ts +0 -117
  109. package/src/mcp/resources/source_resource.ts +0 -141
  110. package/src/mcp/resources/view_resource.ts +0 -136
  111. package/src/mcp/server.ts +0 -62
  112. package/src/mcp/tools/discovery_tools.ts +0 -258
  113. package/src/mcp/tools/execute_query_tool.ts +0 -265
  114. package/src/server.ts +0 -1195
  115. package/src/service/connection.spec.ts +0 -1331
  116. package/src/service/connection.ts +0 -1511
  117. package/src/service/connection_service.spec.ts +0 -641
  118. package/src/service/connection_service.ts +0 -181
  119. package/src/service/db_utils.spec.ts +0 -714
  120. package/src/service/db_utils.ts +0 -1227
  121. package/src/service/filter.spec.ts +0 -447
  122. package/src/service/filter.ts +0 -337
  123. package/src/service/filter_integration.spec.ts +0 -825
  124. package/src/service/gcs_s3_utils.ts +0 -427
  125. package/src/service/manifest_service.spec.ts +0 -201
  126. package/src/service/manifest_service.ts +0 -106
  127. package/src/service/materialization_service.spec.ts +0 -648
  128. package/src/service/materialization_service.ts +0 -929
  129. package/src/service/materialized_table_gc.spec.ts +0 -383
  130. package/src/service/materialized_table_gc.ts +0 -279
  131. package/src/service/model.spec.ts +0 -257
  132. package/src/service/model.ts +0 -1069
  133. package/src/service/package.spec.ts +0 -236
  134. package/src/service/package.ts +0 -463
  135. package/src/service/project.ts +0 -681
  136. package/src/service/project_compile.spec.ts +0 -197
  137. package/src/service/project_store.spec.ts +0 -859
  138. package/src/service/project_store.ts +0 -1531
  139. package/src/service/quoting.ts +0 -41
  140. package/src/service/resolve_project.ts +0 -13
  141. package/src/storage/BaseRepository.ts +0 -31
  142. package/src/storage/DatabaseInterface.ts +0 -180
  143. package/src/storage/StorageManager.mock.ts +0 -59
  144. package/src/storage/StorageManager.ts +0 -210
  145. package/src/storage/duckdb/ConnectionRepository.ts +0 -141
  146. package/src/storage/duckdb/DuckDBConnection.ts +0 -177
  147. package/src/storage/duckdb/DuckDBManifestStore.ts +0 -70
  148. package/src/storage/duckdb/DuckDBRepository.ts +0 -224
  149. package/src/storage/duckdb/ManifestRepository.ts +0 -119
  150. package/src/storage/duckdb/MaterializationRepository.ts +0 -249
  151. package/src/storage/duckdb/PackageRepository.ts +0 -135
  152. package/src/storage/duckdb/ProjectRepository.ts +0 -148
  153. package/src/storage/duckdb/manifest_store.spec.ts +0 -133
  154. package/src/storage/duckdb/schema.ts +0 -147
  155. package/src/storage/ducklake/DuckLakeManifestStore.ts +0 -146
  156. package/src/utils.ts +0 -13
  157. package/tests/fixtures/persist-test/data/orders.csv +0 -5
  158. package/tests/fixtures/persist-test/persist_test.malloy +0 -11
  159. package/tests/fixtures/persist-test/publisher.json +0 -5
  160. package/tests/fixtures/publisher.config.json +0 -15
  161. package/tests/harness/e2e.ts +0 -96
  162. package/tests/harness/mcp_test_setup.ts +0 -344
  163. package/tests/harness/mocks.ts +0 -37
  164. package/tests/harness/rest_e2e.ts +0 -68
  165. package/tests/harness/uris.ts +0 -31
  166. package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -470
  167. package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -434
  168. package/tests/integration/mcp/mcp_resource.integration.spec.ts +0 -663
  169. package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -122
  170. package/tests/integration/mcp/setup.spec.ts +0 -5
  171. package/tests/unit/duckdb/attached_databases.test.ts +0 -1132
  172. package/tests/unit/ducklake/ducklake.test.ts +0 -950
  173. package/tests/unit/mcp/prompt_definitions.test.ts +0 -102
  174. package/tests/unit/mcp/prompt_happy.test.ts +0 -51
  175. package/tsconfig.json +0 -32
@@ -1,470 +0,0 @@
1
- /// <reference types="bun-types" />
2
-
3
- import { afterAll, beforeAll, describe, expect, it } from "bun:test";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
- import { RestE2EEnv, startRestE2E } from "../../harness/rest_e2e";
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- const PROJECT_NAME = "test-project";
12
- const PACKAGE_NAME = "persist-test";
13
- const API = `/api/v0/projects/${PROJECT_NAME}/packages/${PACKAGE_NAME}`;
14
-
15
- describe("Materialization & Manifest REST API (E2E)", () => {
16
- let env: (RestE2EEnv & { stop(): Promise<void> }) | null = null;
17
- let baseUrl: string;
18
-
19
- beforeAll(async () => {
20
- env = await startRestE2E();
21
- baseUrl = env.baseUrl;
22
-
23
- // Create the test project via the REST API using an absolute
24
- // path to the fixture so it works regardless of SERVER_ROOT.
25
- const fixtureDir = path.resolve(__dirname, "../../fixtures/persist-test");
26
- const createRes = await fetch(`${baseUrl}/api/v0/projects`, {
27
- method: "POST",
28
- headers: { "Content-Type": "application/json" },
29
- body: JSON.stringify({
30
- name: PROJECT_NAME,
31
- packages: [{ name: PACKAGE_NAME, location: fixtureDir }],
32
- connections: [],
33
- }),
34
- });
35
- if (!createRes.ok) {
36
- const body = await createRes.text();
37
- throw new Error(
38
- `Failed to create test project (${createRes.status}): ${body}`,
39
- );
40
- }
41
-
42
- // Wait for the package to finish loading.
43
- const deadline = Date.now() + 30_000;
44
- let pkgReady = false;
45
- while (!pkgReady && Date.now() < deadline) {
46
- try {
47
- const res = await fetch(
48
- `${baseUrl}/api/v0/projects/${PROJECT_NAME}/packages/${PACKAGE_NAME}`,
49
- );
50
- if (res.ok) {
51
- pkgReady = true;
52
- break;
53
- }
54
- } catch {
55
- // not ready yet
56
- }
57
- await new Promise((r) => setTimeout(r, 500));
58
- }
59
- if (!pkgReady) {
60
- throw new Error("Test package did not become available in time");
61
- }
62
- });
63
-
64
- afterAll(async () => {
65
- // Tear down the test project, then the HTTP server.
66
- if (baseUrl) {
67
- try {
68
- await fetch(`${baseUrl}/api/v0/projects/${PROJECT_NAME}`, {
69
- method: "DELETE",
70
- });
71
- } catch {
72
- // best-effort cleanup
73
- }
74
- }
75
- await env?.stop();
76
- env = null;
77
- });
78
-
79
- // ── helpers ──────────────────────────────────────────────────────
80
-
81
- function url(p: string): string {
82
- return `${baseUrl}${API}${p}`;
83
- }
84
-
85
- async function createMaterialization(
86
- body: Record<string, unknown> = {},
87
- ): Promise<Response> {
88
- return fetch(url("/materializations"), {
89
- method: "POST",
90
- headers: { "Content-Type": "application/json" },
91
- body: JSON.stringify(body),
92
- });
93
- }
94
-
95
- async function pollUntilTerminal(
96
- id: string,
97
- timeoutMs = 30_000,
98
- ): Promise<Record<string, unknown>> {
99
- const deadline = Date.now() + timeoutMs;
100
- while (Date.now() < deadline) {
101
- const res = await fetch(url(`/materializations/${id}`));
102
- expect(res.status).toBe(200);
103
- const data = (await res.json()) as Record<string, unknown>;
104
- const status = data.status as string;
105
- if (["SUCCESS", "FAILED", "CANCELLED"].includes(status)) {
106
- return data;
107
- }
108
- await new Promise((r) => setTimeout(r, 500));
109
- }
110
- throw new Error(`Materialization ${id} did not reach terminal state`);
111
- }
112
-
113
- /**
114
- * Clean up a materialization so it doesn't interfere with other tests.
115
- * Stops it if active, then deletes if terminal.
116
- */
117
- async function cleanup(id: string): Promise<void> {
118
- const res = await fetch(url(`/materializations/${id}`));
119
- if (res.status !== 200) return;
120
- const data = (await res.json()) as Record<string, unknown>;
121
- const status = data.status as string;
122
-
123
- if (status === "PENDING" || status === "RUNNING") {
124
- await fetch(url(`/materializations/${id}?action=stop`), {
125
- method: "POST",
126
- });
127
- await pollUntilTerminal(id);
128
- }
129
- await fetch(url(`/materializations/${id}`), { method: "DELETE" });
130
- }
131
-
132
- // ── Group A: Full lifecycle with persist sources ──────────────────
133
-
134
- describe("full lifecycle (happy path)", () => {
135
- let materializationId: string;
136
-
137
- afterAll(async () => {
138
- if (materializationId) await cleanup(materializationId);
139
- });
140
-
141
- it(
142
- "should create, start, build, verify manifest, and delete",
143
- async () => {
144
- // 1. Create
145
- const createRes = await createMaterialization({
146
- autoLoadManifest: true,
147
- });
148
- expect(createRes.status).toBe(201);
149
- const created = (await createRes.json()) as Record<string, unknown>;
150
- expect(created.status).toBe("PENDING");
151
- expect(created.id).toBeDefined();
152
- materializationId = created.id as string;
153
-
154
- // 2. List
155
- const listRes = await fetch(url("/materializations"));
156
- expect(listRes.status).toBe(200);
157
- const list = (await listRes.json()) as Record<string, unknown>[];
158
- expect(list.some((m) => m.id === materializationId)).toBe(true);
159
-
160
- // 3. Get by ID
161
- const getRes = await fetch(
162
- url(`/materializations/${materializationId}`),
163
- );
164
- expect(getRes.status).toBe(200);
165
- const got = (await getRes.json()) as Record<string, unknown>;
166
- expect(got.status).toBe("PENDING");
167
-
168
- // 4. Start
169
- const startRes = await fetch(
170
- url(`/materializations/${materializationId}?action=start`),
171
- { method: "POST" },
172
- );
173
- expect(startRes.status).toBe(202);
174
- const started = (await startRes.json()) as Record<string, unknown>;
175
- expect(started.status).toBe("RUNNING");
176
-
177
- // 5. Poll until terminal
178
- const terminal = await pollUntilTerminal(materializationId);
179
- expect(terminal.status).toBe("SUCCESS");
180
- const metadata = terminal.metadata as Record<string, unknown>;
181
- expect(metadata.sourcesBuilt).toBeGreaterThan(0);
182
-
183
- // 6. Get manifest
184
- const manifestRes = await fetch(url("/manifest"));
185
- expect(manifestRes.status).toBe(200);
186
- const manifest = (await manifestRes.json()) as Record<
187
- string,
188
- unknown
189
- >;
190
- expect(manifest.entries).toBeDefined();
191
- const entries = manifest.entries as Record<string, unknown>;
192
- expect(Object.keys(entries).length).toBeGreaterThan(0);
193
- const firstEntry = Object.values(entries)[0] as Record<
194
- string,
195
- unknown
196
- >;
197
- expect(firstEntry.tableName).toBe("order_summary");
198
-
199
- // 7. Reload manifest
200
- const reloadRes = await fetch(url("/manifest?action=reload"), {
201
- method: "POST",
202
- });
203
- expect(reloadRes.status).toBe(200);
204
- const reloadedManifest = (await reloadRes.json()) as Record<
205
- string,
206
- unknown
207
- >;
208
- expect(reloadedManifest.entries).toBeDefined();
209
-
210
- // 8. Delete
211
- const deleteRes = await fetch(
212
- url(`/materializations/${materializationId}`),
213
- { method: "DELETE" },
214
- );
215
- expect(deleteRes.status).toBe(204);
216
- materializationId = ""; // prevent afterAll cleanup
217
- },
218
- { timeout: 60_000 },
219
- );
220
- });
221
-
222
- // ── Group B: Error cases and state machine validation ────────────
223
-
224
- describe("error cases", () => {
225
- it("should stop a PENDING materialization (PENDING -> CANCELLED)", async () => {
226
- const createRes = await createMaterialization();
227
- expect(createRes.status).toBe(201);
228
- const created = (await createRes.json()) as Record<string, unknown>;
229
- const id = created.id as string;
230
-
231
- const stopRes = await fetch(
232
- url(`/materializations/${id}?action=stop`),
233
- {
234
- method: "POST",
235
- },
236
- );
237
- expect(stopRes.status).toBe(200);
238
- const stopped = (await stopRes.json()) as Record<string, unknown>;
239
- expect(stopped.status).toBe("CANCELLED");
240
-
241
- await cleanup(id);
242
- });
243
-
244
- it("should reject a second concurrent materialization with 409", async () => {
245
- const first = await createMaterialization();
246
- expect(first.status).toBe(201);
247
- const firstData = (await first.json()) as Record<string, unknown>;
248
- const firstId = firstData.id as string;
249
-
250
- const second = await createMaterialization();
251
- expect(second.status).toBe(409);
252
-
253
- await cleanup(firstId);
254
- });
255
-
256
- it("should reject starting a CANCELLED materialization with 409", async () => {
257
- const createRes = await createMaterialization();
258
- expect(createRes.status).toBe(201);
259
- const created = (await createRes.json()) as Record<string, unknown>;
260
- const id = created.id as string;
261
-
262
- await fetch(url(`/materializations/${id}?action=stop`), {
263
- method: "POST",
264
- });
265
-
266
- const startRes = await fetch(
267
- url(`/materializations/${id}?action=start`),
268
- {
269
- method: "POST",
270
- },
271
- );
272
- expect(startRes.status).toBe(409);
273
-
274
- await cleanup(id);
275
- });
276
-
277
- it("should reject deleting a PENDING materialization with 409", async () => {
278
- const createRes = await createMaterialization();
279
- expect(createRes.status).toBe(201);
280
- const created = (await createRes.json()) as Record<string, unknown>;
281
- const id = created.id as string;
282
-
283
- const deleteRes = await fetch(url(`/materializations/${id}`), {
284
- method: "DELETE",
285
- });
286
- expect(deleteRes.status).toBe(409);
287
-
288
- await cleanup(id);
289
- });
290
-
291
- it("should return 404 for a non-existent materialization", async () => {
292
- const res = await fetch(
293
- url("/materializations/non-existent-id-12345"),
294
- );
295
- expect(res.status).toBe(404);
296
- });
297
- });
298
-
299
- // ── Group C: Package Teardown ────────────────────────────────────
300
-
301
- describe("package teardown", () => {
302
- it(
303
- "dryRun reports stale entries without dropping tables or deleting rows",
304
- async () => {
305
- // Run a full build so there are manifest entries to tear down.
306
- const createRes = await createMaterialization({
307
- autoLoadManifest: true,
308
- });
309
- expect(createRes.status).toBe(201);
310
- const created = (await createRes.json()) as Record<string, unknown>;
311
- const id = created.id as string;
312
- await fetch(url(`/materializations/${id}?action=start`), {
313
- method: "POST",
314
- });
315
- const terminal = await pollUntilTerminal(id);
316
- expect(terminal.status).toBe("SUCCESS");
317
-
318
- // Must delete the materialization record before teardown
319
- // (teardown refuses to run while an active materialization exists).
320
- await fetch(url(`/materializations/${id}`), { method: "DELETE" });
321
-
322
- // dryRun teardown — should report entries but not actually drop them.
323
- const teardownRes = await fetch(url("/materializations/teardown"), {
324
- method: "POST",
325
- headers: { "Content-Type": "application/json" },
326
- body: JSON.stringify({ dryRun: true }),
327
- });
328
- expect(teardownRes.status).toBe(200);
329
- const teardownResult = (await teardownRes.json()) as Record<
330
- string,
331
- unknown
332
- >;
333
- const dropped = teardownResult.dropped as Record<string, unknown>[];
334
- expect(dropped).toBeDefined();
335
- expect(teardownResult.errors).toBeDefined();
336
-
337
- // Manifest should still be intact after a dry run.
338
- const manifestRes = await fetch(url("/manifest"));
339
- expect(manifestRes.status).toBe(200);
340
- const manifest = (await manifestRes.json()) as Record<
341
- string,
342
- unknown
343
- >;
344
- const entries = manifest.entries as Record<string, unknown>;
345
- expect(Object.keys(entries).length).toBeGreaterThan(0);
346
- },
347
- { timeout: 60_000 },
348
- );
349
-
350
- it(
351
- "live teardown drops stale manifest entries and cleans up tables",
352
- async () => {
353
- // Build so there are manifest entries.
354
- const createRes = await createMaterialization({
355
- autoLoadManifest: true,
356
- });
357
- expect(createRes.status).toBe(201);
358
- const created = (await createRes.json()) as Record<string, unknown>;
359
- const id = created.id as string;
360
- await fetch(url(`/materializations/${id}?action=start`), {
361
- method: "POST",
362
- });
363
- const terminal = await pollUntilTerminal(id);
364
- expect(terminal.status).toBe("SUCCESS");
365
-
366
- await fetch(url(`/materializations/${id}`), { method: "DELETE" });
367
-
368
- // Live teardown — should drop everything since all entries are
369
- // stale (no active build claims them).
370
- const teardownRes = await fetch(url("/materializations/teardown"), {
371
- method: "POST",
372
- headers: { "Content-Type": "application/json" },
373
- body: JSON.stringify({}),
374
- });
375
- expect(teardownRes.status).toBe(200);
376
- const teardownResult = (await teardownRes.json()) as Record<
377
- string,
378
- unknown
379
- >;
380
- const dropped = teardownResult.dropped as Record<string, unknown>[];
381
- expect(dropped.length).toBeGreaterThan(0);
382
- expect((teardownResult.errors as unknown[]).length).toBe(0);
383
-
384
- // Manifest should be empty after live teardown.
385
- const manifestRes = await fetch(url("/manifest"));
386
- expect(manifestRes.status).toBe(200);
387
- const manifest = (await manifestRes.json()) as Record<
388
- string,
389
- unknown
390
- >;
391
- const entries = manifest.entries as Record<string, unknown>;
392
- expect(Object.keys(entries).length).toBe(0);
393
- },
394
- { timeout: 60_000 },
395
- );
396
-
397
- it("teardown rejects while an active materialization exists", async () => {
398
- const createRes = await createMaterialization();
399
- expect(createRes.status).toBe(201);
400
- const created = (await createRes.json()) as Record<string, unknown>;
401
- const id = created.id as string;
402
-
403
- const teardownRes = await fetch(url("/materializations/teardown"), {
404
- method: "POST",
405
- headers: { "Content-Type": "application/json" },
406
- body: JSON.stringify({}),
407
- });
408
- expect(teardownRes.status).toBe(409);
409
-
410
- await cleanup(id);
411
- });
412
-
413
- it(
414
- "forceRefresh rebuilds and post-build GC step executes",
415
- async () => {
416
- // First build — populates manifest.
417
- const first = await createMaterialization({
418
- autoLoadManifest: true,
419
- });
420
- expect(first.status).toBe(201);
421
- const firstData = (await first.json()) as Record<string, unknown>;
422
- const firstId = firstData.id as string;
423
- await fetch(url(`/materializations/${firstId}?action=start`), {
424
- method: "POST",
425
- });
426
- const firstTerminal = await pollUntilTerminal(firstId);
427
- expect(firstTerminal.status).toBe("SUCCESS");
428
- await fetch(url(`/materializations/${firstId}`), {
429
- method: "DELETE",
430
- });
431
-
432
- // Second build with forceRefresh — the buildId won't change
433
- // (hash of SQL + connection is identical), but forceRefresh
434
- // forces a rebuild rather than skipping.
435
- const second = await createMaterialization({
436
- forceRefresh: true,
437
- autoLoadManifest: true,
438
- });
439
- expect(second.status).toBe(201);
440
- const secondData = (await second.json()) as Record<string, unknown>;
441
- const secondId = secondData.id as string;
442
- await fetch(url(`/materializations/${secondId}?action=start`), {
443
- method: "POST",
444
- });
445
- const secondTerminal = await pollUntilTerminal(secondId);
446
- expect(secondTerminal.status).toBe("SUCCESS");
447
-
448
- const metadata = secondTerminal.metadata as Record<string, unknown>;
449
- // forceRefresh should actually rebuild, not skip.
450
- expect(metadata.sourcesBuilt).toBeGreaterThan(0);
451
- expect(metadata.sourcesSkipped).toBe(0);
452
- // Post-build GC step ran (arrays present even if empty).
453
- expect(metadata.gcDropped).toBeDefined();
454
- expect(metadata.gcErrors).toBeDefined();
455
-
456
- // Manifest should still have entries after rebuild.
457
- const manifestRes = await fetch(url("/manifest"));
458
- const manifest = (await manifestRes.json()) as Record<
459
- string,
460
- unknown
461
- >;
462
- const entries = manifest.entries as Record<string, unknown>;
463
- expect(Object.keys(entries).length).toBeGreaterThan(0);
464
-
465
- await cleanup(secondId);
466
- },
467
- { timeout: 90_000 },
468
- );
469
- });
470
- });