@malloy-publisher/server 0.0.222 → 0.0.224

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/dist/app/api-doc.yaml +17 -0
  2. package/dist/app/assets/{EnvironmentPage-D6G5n6mY.js → EnvironmentPage-DYTeXDll.js} +1 -1
  3. package/dist/app/assets/{HomePage-DgZluD8u.js → HomePage-pDK2BPJY.js} +1 -1
  4. package/dist/app/assets/{LightMode-w9v0pEbg.js → LightMode-C2bwGPY1.js} +1 -1
  5. package/dist/app/assets/{MainPage-Bi_Tukvr.js → MainPage-WtBulMH_.js} +2 -2
  6. package/dist/app/assets/{MaterializationsPage-BVERjnmG.js → MaterializationsPage-hMgOtflG.js} +1 -1
  7. package/dist/app/assets/{ModelPage-2OjS259-.js → ModelPage-B2N5kYII.js} +1 -1
  8. package/dist/app/assets/{PackagePage-C8L2On4z.js → PackagePage-CEN90nQG.js} +1 -1
  9. package/dist/app/assets/{RouteError-DJoNC_Vl.js → RouteError-BG2c5Zf0.js} +1 -1
  10. package/dist/app/assets/{ThemeEditorPage-B-hJ1zgd.js → ThemeEditorPage-DNfeUwEZ.js} +1 -1
  11. package/dist/app/assets/{WorkbookPage-CTbDgGeJ.js → WorkbookPage-NKI1BhFS.js} +1 -1
  12. package/dist/app/assets/{core-D9Hl0IDY.es-CmgzHn4c.js → core-C6anj5c0.es-DDLHqpzt.js} +1 -1
  13. package/dist/app/assets/{index-Cr-asgoV.js → index-C6gZ6sSY.js} +5 -5
  14. package/dist/app/assets/{index-BF8PkFm8.js → index-CzNqKMVl.js} +1 -1
  15. package/dist/app/assets/{index-BEjJCJjX.js → index-DMQtnaf4.js} +3 -3
  16. package/dist/app/assets/{index-DcCw_qxr.js → index-JXgvyZna.js} +1 -1
  17. package/dist/app/assets/{index-8E2uLeV9.js → index-OEjKNSYb.js} +2 -2
  18. package/dist/app/index.html +1 -1
  19. package/dist/server.mjs +267 -19
  20. package/package.json +12 -12
  21. package/src/mcp/skills/skills_bundle.json +1 -1
  22. package/src/service/environment.ts +3 -3
  23. package/src/service/freshness.spec.ts +183 -0
  24. package/src/service/freshness.ts +112 -0
  25. package/src/service/manifest_loader.spec.ts +33 -0
  26. package/src/service/manifest_loader.ts +17 -8
  27. package/src/service/materialization_cron_gate.spec.ts +23 -18
  28. package/src/service/materialization_service.ts +6 -1
  29. package/src/service/model.ts +54 -4
  30. package/src/service/package.ts +126 -33
  31. package/src/storage/DatabaseInterface.ts +23 -0
  32. package/tests/integration/materialization/freshness_gate.integration.spec.ts +292 -0
  33. package/tests/integration/materialization/manifest_binding.integration.spec.ts +135 -3
  34. package/dist/sshcrypto-8m50vnmb.node +0 -0
@@ -91,17 +91,107 @@ describe("Manifest binding via Package.manifestLocation (E2E)", () => {
91
91
  });
92
92
  }
93
93
 
94
+ const ROUTING_QUERY = "run: order_summary -> { aggregate: c is count() }";
95
+
94
96
  async function queryOrderSummaryStatus(): Promise<number> {
95
97
  const res = await fetch(`${baseUrl}${API}/models/${MODEL_PATH}/query`, {
96
98
  method: "POST",
97
99
  headers: { "Content-Type": "application/json" },
98
- body: JSON.stringify({
99
- query: "run: order_summary -> { aggregate: c is count() }",
100
- }),
100
+ body: JSON.stringify({ query: ROUTING_QUERY }),
101
101
  });
102
102
  return res.status;
103
103
  }
104
104
 
105
+ /** The SQL the served query actually compiled to (routing evidence). */
106
+ async function executedSql(): Promise<string> {
107
+ const res = await fetch(`${baseUrl}${API}/models/${MODEL_PATH}/query`, {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ query: ROUTING_QUERY }),
111
+ });
112
+ expect(res.status).toBe(200);
113
+ const body = (await res.json()) as { result: string };
114
+ return (JSON.parse(body.result) as { sql: string }).sql;
115
+ }
116
+
117
+ /** Read the package's build plan and return the persist source's real sourceEntityId. */
118
+ async function orderSummarySourceEntityId(): Promise<string> {
119
+ const res = await fetch(url(""));
120
+ expect(res.status).toBe(200);
121
+ const pkg = (await res.json()) as {
122
+ buildPlan?: { sources?: Record<string, { sourceEntityId?: string }> };
123
+ };
124
+ const sources = pkg.buildPlan?.sources ?? {};
125
+ const sourceEntityId = Object.values(sources)[0]?.sourceEntityId;
126
+ expect(typeof sourceEntityId).toBe("string");
127
+ return sourceEntityId as string;
128
+ }
129
+
130
+ /** Poll a materialization until it reaches a terminal state. */
131
+ async function pollUntilTerminal(
132
+ id: string,
133
+ timeoutMs = 90_000,
134
+ ): Promise<Record<string, unknown>> {
135
+ const terminal = ["MANIFEST_FILE_READY", "FAILED", "CANCELLED"];
136
+ const deadline = Date.now() + timeoutMs;
137
+ while (Date.now() < deadline) {
138
+ const res = await fetch(url(`/materializations/${id}`));
139
+ expect(res.status).toBe(200);
140
+ const data = (await res.json()) as Record<string, unknown>;
141
+ if (terminal.includes(data.status as string)) return data;
142
+ await new Promise((r) => setTimeout(r, 250));
143
+ }
144
+ throw new Error(`Materialization ${id} did not reach a terminal state`);
145
+ }
146
+
147
+ /**
148
+ * Build + physicalise the persist source (auto-run self-assigns
149
+ * physicalTableName = "order_summary" from `#@ persist name=...`) so a
150
+ * `SELECT * FROM order_summary` is actually queryable, then revert the
151
+ * package to live (unbound) — keeping the table — so a subsequent
152
+ * manifest-URI bind is what routes the served query.
153
+ */
154
+ async function buildTableThenRevertToLive(): Promise<void> {
155
+ const createRes = await fetch(url("/materializations"), {
156
+ method: "POST",
157
+ headers: { "Content-Type": "application/json" },
158
+ body: JSON.stringify({}),
159
+ });
160
+ expect(createRes.status).toBe(201);
161
+ const { id } = (await createRes.json()) as { id: string };
162
+ const built = await pollUntilTerminal(id);
163
+ expect(built.status).toBe("MANIFEST_FILE_READY");
164
+ // Revert to live: drop the auto-load binding but leave the built table.
165
+ await fetch(url("?reload=true"));
166
+ // Retire the run record so it doesn't hold the per-package active slot.
167
+ await fetch(url(`/materializations/${id}`), { method: "DELETE" });
168
+ }
169
+
170
+ /** Write a manifest file keyed by the persist source's real sourceEntityId. */
171
+ async function writeRoutingManifest(
172
+ sourceEntityId: string,
173
+ physicalTableName: string,
174
+ ): Promise<string> {
175
+ const file = path.join(tmpDir, `routing-manifest-${Date.now()}.json`);
176
+ await fsp.writeFile(
177
+ file,
178
+ JSON.stringify({
179
+ builtAt: new Date().toISOString(),
180
+ strict: false,
181
+ entries: {
182
+ [sourceEntityId]: {
183
+ sourceEntityId,
184
+ sourceName: "order_summary",
185
+ physicalTableName,
186
+ connectionName: "duckdb",
187
+ },
188
+ },
189
+ }),
190
+ "utf8",
191
+ );
192
+ return file;
193
+ }
194
+
105
195
  async function writeManifest(): Promise<string> {
106
196
  const file = path.join(tmpDir, `manifest-${Date.now()}.json`);
107
197
  await fsp.writeFile(
@@ -171,6 +261,48 @@ describe("Manifest binding via Package.manifestLocation (E2E)", () => {
171
261
  { timeout: 60_000 },
172
262
  );
173
263
 
264
+ it(
265
+ "routes served queries to the materialized table after a manifest-URI bind",
266
+ async () => {
267
+ // Start from live so the baseline recomputes from the base CSV.
268
+ await getPackage(true);
269
+ expect(await executedSql()).toContain("data/orders.csv");
270
+
271
+ // Physically build the persist source, then revert to live (keeping
272
+ // the table) so the manifest-URI bind below is the only thing that
273
+ // could route the served query.
274
+ await buildTableThenRevertToLive();
275
+ expect(await executedSql()).toContain("data/orders.csv");
276
+
277
+ // Bind the CP-shaped manifest via manifestLocation, keyed by the
278
+ // source's REAL sourceEntityId (the value Malloy recomputes at serve
279
+ // time to resolve the persist reference). Anything else silently misses.
280
+ const sourceEntityId = await orderSummarySourceEntityId();
281
+ const manifestFile = await writeRoutingManifest(
282
+ sourceEntityId,
283
+ "order_summary",
284
+ );
285
+ const patchRes = await patchPackage({
286
+ manifestLocation: manifestFile,
287
+ });
288
+ expect(patchRes.status).toBe(200);
289
+ const patched = await patchRes.json();
290
+ expect(patched.manifestBindingStatus).toBe("bound");
291
+ expect(patched.manifestEntryCount).toBe(1);
292
+
293
+ // The payoff: the served query now scans the materialized table and no
294
+ // longer recomputes from the base CSV.
295
+ const routed = await executedSql();
296
+ expect(routed).not.toContain("data/orders.csv");
297
+ expect(routed).toContain("order_summary");
298
+
299
+ // Cleanup: revert to live so the dangling binding doesn't leak.
300
+ await patchPackage({ manifestLocation: null });
301
+ await getPackage(true);
302
+ },
303
+ { timeout: 120_000 },
304
+ );
305
+
174
306
  it("degrades to serving live when the manifest is unreachable", async () => {
175
307
  const missing = path.join(tmpDir, "does-not-exist.json");
176
308
 
Binary file