@malloy-publisher/server 0.0.203 → 0.0.205
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.
- package/build.ts +10 -1
- package/dist/app/api-doc.yaml +146 -0
- package/dist/app/assets/{EnvironmentPage-BVQ7glKP.js → EnvironmentPage-CAge6UHD.js} +1 -1
- package/dist/app/assets/HomePage-DhTe8qpa.js +1 -0
- package/dist/app/assets/{MainPage-bYOWcgDP.js → MainPage-CeTxxGex.js} +2 -2
- package/dist/app/assets/MaterializationsPage-CpDHB70t.js +1 -0
- package/dist/app/assets/ModelPage-D9sSMb75.js +1 -0
- package/dist/app/assets/PackagePage-LRqQWrFY.js +1 -0
- package/dist/app/assets/{RouteError-_J-EBz7W.js → RouteError-xT6kuCNw.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Bjs9Nm-_.js → WorkbookPage-DsIh9svZ.js} +1 -1
- package/dist/app/assets/{core-BPLlx5VM.es-C2ARtwWI.js → core-C2sQrwVu.es-Bjem0hym.js} +1 -1
- package/dist/app/assets/{index-CqUWJELr.js → index-BdOZDcce.js} +2 -2
- package/dist/app/assets/index-DHHAcY5o.js +1812 -0
- package/dist/app/assets/index-RX3QOTde.js +455 -0
- package/dist/app/assets/index.umd-D2WH3D-f.js +2469 -0
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +392 -67
- package/dist/runtime/publisher.js +318 -0
- package/dist/server.mjs +982 -346
- package/package.json +15 -14
- package/scripts/bake-duckdb-extensions.js +104 -0
- package/src/controller/watch-mode.controller.ts +176 -46
- package/src/ducklake_version.spec.ts +43 -0
- package/src/ducklake_version.ts +26 -0
- package/src/errors.spec.ts +21 -0
- package/src/errors.ts +18 -1
- package/src/mcp/error_messages.spec.ts +35 -0
- package/src/mcp/error_messages.ts +14 -1
- package/src/mcp/handler_utils.ts +12 -0
- package/src/package_load/package_load_pool.ts +0 -5
- package/src/package_load/package_load_worker.ts +41 -99
- package/src/package_load/protocol.ts +1 -7
- package/src/runtime/publisher.js +318 -0
- package/src/server.ts +479 -2
- package/src/service/annotations.spec.ts +118 -0
- package/src/service/annotations.ts +91 -0
- package/src/service/authorize.spec.ts +132 -0
- package/src/service/authorize.ts +241 -0
- package/src/service/authorize_integration.spec.ts +932 -0
- package/src/service/compile_authorize.spec.ts +85 -0
- package/src/service/connection.ts +1 -1
- package/src/service/environment.ts +67 -9
- package/src/service/environment_store.ts +142 -11
- package/src/service/filter.spec.ts +14 -3
- package/src/service/filter.ts +5 -1
- package/src/service/filter_bypass.spec.ts +418 -0
- package/src/service/given.ts +37 -12
- package/src/service/givens_integration.spec.ts +34 -7
- package/src/service/materialization_service.ts +25 -20
- package/src/service/materialized_table_gc.spec.ts +6 -5
- package/src/service/materialized_table_gc.ts +2 -50
- package/src/service/model.spec.ts +203 -8
- package/src/service/model.ts +349 -155
- package/src/service/package.ts +17 -6
- package/src/service/package_worker_path.spec.ts +113 -0
- package/src/service/quoting.ts +0 -20
- package/src/service/restricted_mode.spec.ts +299 -0
- package/src/service/source_extraction.ts +226 -0
- package/src/storage/StorageManager.ts +73 -0
- package/src/storage/duckdb/DuckDBConnection.ts +70 -124
- package/tests/fixtures/authorize-compile/model.malloy +9 -0
- package/tests/fixtures/authorize-compile/publisher.json +4 -0
- package/tests/fixtures/html-pages-nopublic/model.malloy +1 -0
- package/tests/fixtures/html-pages-nopublic/publisher.json +5 -0
- package/tests/fixtures/html-pages-test/data.csv +3 -0
- package/tests/fixtures/html-pages-test/public/assets/app.css +3 -0
- package/tests/fixtures/html-pages-test/public/data.json +1 -0
- package/tests/fixtures/html-pages-test/public/index.html +9 -0
- package/tests/fixtures/html-pages-test/public/sub/page2.html +9 -0
- package/tests/fixtures/html-pages-test/publisher.json +5 -0
- package/tests/fixtures/html-pages-test/report.malloy +1 -0
- package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +92 -0
- package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +138 -0
- package/tests/integration/html_pages/html_pages.integration.spec.ts +378 -0
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +421 -0
- package/tests/unit/duckdb/attached_databases.test.ts +111 -0
- package/tests/unit/duckdb/duckdb_connection.test.ts +181 -0
- package/tests/unit/duckdb/repositories.test.ts +208 -0
- package/dist/app/assets/HomePage-D9drXoZX.js +0 -1
- package/dist/app/assets/ModelPage-DT0gjNy1.js +0 -1
- package/dist/app/assets/PackagePage-N1ZBNJul.js +0 -1
- package/dist/app/assets/index-BeNwIeYQ.js +0 -454
- package/dist/app/assets/index-Dx7qi2LO.js +0 -1803
- package/dist/app/assets/index.umd-BXm2lnUO.js +0 -1145
|
@@ -174,7 +174,7 @@ describe("dropManifestEntries", () => {
|
|
|
174
174
|
);
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
it("
|
|
177
|
+
it("drops regardless of connection dialect — DDL uses table names verbatim", async () => {
|
|
178
178
|
const ctx2 = makeCtx("martian-sql");
|
|
179
179
|
const entry = makeEntry();
|
|
180
180
|
|
|
@@ -184,10 +184,11 @@ describe("dropManifestEntries", () => {
|
|
|
184
184
|
environmentId: "proj-1",
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
expect(result.
|
|
190
|
-
expect(
|
|
187
|
+
// GC no longer needs a per-dialect quoter: table names are used
|
|
188
|
+
// verbatim, so an unrecognized dialect is no longer a special case.
|
|
189
|
+
expect(result.dropped).toHaveLength(1);
|
|
190
|
+
expect(result.errors).toHaveLength(0);
|
|
191
|
+
expect(ctx2.conn.runSQL.callCount).toBe(2);
|
|
191
192
|
});
|
|
192
193
|
|
|
193
194
|
it("dryRun lists what would drop without issuing SQL or deleting rows", async () => {
|
|
@@ -1,37 +1,8 @@
|
|
|
1
1
|
import type { Connection } from "@malloydata/malloy";
|
|
2
|
-
import {
|
|
3
|
-
DatabricksDialect,
|
|
4
|
-
DuckDBDialect,
|
|
5
|
-
MySQLDialect,
|
|
6
|
-
PostgresDialect,
|
|
7
|
-
SnowflakeDialect,
|
|
8
|
-
StandardSQLDialect,
|
|
9
|
-
TrinoDialect,
|
|
10
|
-
} from "@malloydata/malloy";
|
|
11
2
|
import { logger } from "../logger";
|
|
12
3
|
import { ManifestEntry } from "../storage/DatabaseInterface";
|
|
13
4
|
import { ManifestService } from "./manifest_service";
|
|
14
5
|
import { stagingSuffix } from "./materialization_service";
|
|
15
|
-
import { type Quoter, quoteTablePath } from "./quoting";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Registry of built-in dialects keyed by `Connection.dialectName`. Malloy's
|
|
19
|
-
* internal `getDialect` helper isn't part of the package's public exports,
|
|
20
|
-
* so we assemble our own registry from the exported dialect classes.
|
|
21
|
-
*
|
|
22
|
-
* Note: `presto` (extends `TrinoDialect`) is not re-exported publicly and
|
|
23
|
-
* is niche enough to omit; if/when it ships as a publisher connection type,
|
|
24
|
-
* add it here.
|
|
25
|
-
*/
|
|
26
|
-
const DIALECTS: Readonly<Record<string, Quoter>> = Object.freeze({
|
|
27
|
-
duckdb: new DuckDBDialect(),
|
|
28
|
-
standardsql: new StandardSQLDialect(),
|
|
29
|
-
trino: new TrinoDialect(),
|
|
30
|
-
postgres: new PostgresDialect(),
|
|
31
|
-
snowflake: new SnowflakeDialect(),
|
|
32
|
-
mysql: new MySQLDialect(),
|
|
33
|
-
databricks: new DatabricksDialect(),
|
|
34
|
-
});
|
|
35
6
|
|
|
36
7
|
/** Build a stable key for a `(connectionName, tableName)` tuple. */
|
|
37
8
|
export function liveTableKey(
|
|
@@ -154,19 +125,6 @@ async function processOneEntry(
|
|
|
154
125
|
};
|
|
155
126
|
}
|
|
156
127
|
|
|
157
|
-
// ── Unknown dialect ───────────────────────────────────────────
|
|
158
|
-
const dialect = DIALECTS[connection.dialectName];
|
|
159
|
-
if (!dialect) {
|
|
160
|
-
return {
|
|
161
|
-
error: {
|
|
162
|
-
buildId: entry.buildId,
|
|
163
|
-
tableName: entry.tableName,
|
|
164
|
-
connectionName: entry.connectionName,
|
|
165
|
-
error: `No dialect registered for '${connection.dialectName}'`,
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
128
|
// ── Dry run ───────────────────────────────────────────────────
|
|
171
129
|
if (ctx.dryRun) {
|
|
172
130
|
return {
|
|
@@ -181,8 +139,6 @@ async function processOneEntry(
|
|
|
181
139
|
}
|
|
182
140
|
|
|
183
141
|
// ── Live run: delete manifest row first ───────────────────────
|
|
184
|
-
const quoted = (p: string) => quoteTablePath(p, dialect);
|
|
185
|
-
|
|
186
142
|
try {
|
|
187
143
|
await ctx.manifestService.deleteEntry(ctx.environmentId, entry.id);
|
|
188
144
|
} catch (err) {
|
|
@@ -214,9 +170,7 @@ async function processOneEntry(
|
|
|
214
170
|
);
|
|
215
171
|
} else {
|
|
216
172
|
try {
|
|
217
|
-
await connection.runSQL(
|
|
218
|
-
`DROP TABLE IF EXISTS ${quoted(entry.tableName)}`,
|
|
219
|
-
);
|
|
173
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${entry.tableName}`);
|
|
220
174
|
} catch (err) {
|
|
221
175
|
logger.warn(
|
|
222
176
|
"GC: deleted manifest row but failed to drop materialized table (orphaned)",
|
|
@@ -231,9 +185,7 @@ async function processOneEntry(
|
|
|
231
185
|
|
|
232
186
|
// ── Best-effort drop staging companion ────────────────────────
|
|
233
187
|
try {
|
|
234
|
-
await connection.runSQL(
|
|
235
|
-
`DROP TABLE IF EXISTS ${quoted(stagingTableName)}`,
|
|
236
|
-
);
|
|
188
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
237
189
|
} catch (err) {
|
|
238
190
|
logger.warn("GC: failed to drop staging table (best-effort)", {
|
|
239
191
|
stagingTableName,
|
|
@@ -191,6 +191,84 @@ describe("service/model", () => {
|
|
|
191
191
|
|
|
192
192
|
sinon.restore();
|
|
193
193
|
});
|
|
194
|
+
|
|
195
|
+
it("embeds model-level givens in each newSources SourceInfo", async () => {
|
|
196
|
+
const sourceInfo = {
|
|
197
|
+
name: "carriers",
|
|
198
|
+
schema: { fields: [] },
|
|
199
|
+
};
|
|
200
|
+
const givens = [
|
|
201
|
+
{
|
|
202
|
+
name: "region",
|
|
203
|
+
type: "string",
|
|
204
|
+
annotations: ["#(doc) Region"],
|
|
205
|
+
},
|
|
206
|
+
];
|
|
207
|
+
const model = new Model(
|
|
208
|
+
packageName,
|
|
209
|
+
"test.malloynb",
|
|
210
|
+
{},
|
|
211
|
+
"notebook",
|
|
212
|
+
undefined, // modelMaterializer
|
|
213
|
+
undefined, // modelDef
|
|
214
|
+
undefined, // sources
|
|
215
|
+
undefined, // queries
|
|
216
|
+
undefined, // sourceInfos
|
|
217
|
+
[
|
|
218
|
+
{
|
|
219
|
+
type: "code",
|
|
220
|
+
text: "import 'carriers.malloy'",
|
|
221
|
+
newSources: [sourceInfo],
|
|
222
|
+
},
|
|
223
|
+
], // runnableNotebookCells
|
|
224
|
+
undefined, // compilationError
|
|
225
|
+
undefined, // filterMap
|
|
226
|
+
givens, // givens
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const notebook = await model.getNotebook();
|
|
230
|
+
expect(notebook.notebookCells).toHaveLength(1);
|
|
231
|
+
const parsed = JSON.parse(
|
|
232
|
+
notebook.notebookCells![0].newSources![0],
|
|
233
|
+
);
|
|
234
|
+
expect(parsed.name).toBe("carriers");
|
|
235
|
+
// SourceInfo fields are preserved untouched.
|
|
236
|
+
expect(parsed.schema).toEqual({ fields: [] });
|
|
237
|
+
// Givens ride along verbatim — no second getModel round-trip needed.
|
|
238
|
+
expect(parsed.givens).toEqual(givens);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("omits givens from newSources when the model declares none", async () => {
|
|
242
|
+
const sourceInfo = { name: "carriers", schema: { fields: [] } };
|
|
243
|
+
const model = new Model(
|
|
244
|
+
packageName,
|
|
245
|
+
"test.malloynb",
|
|
246
|
+
{},
|
|
247
|
+
"notebook",
|
|
248
|
+
undefined,
|
|
249
|
+
undefined,
|
|
250
|
+
undefined,
|
|
251
|
+
undefined,
|
|
252
|
+
undefined,
|
|
253
|
+
[
|
|
254
|
+
{
|
|
255
|
+
type: "code",
|
|
256
|
+
text: "import 'carriers.malloy'",
|
|
257
|
+
newSources: [sourceInfo],
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
undefined,
|
|
261
|
+
undefined,
|
|
262
|
+
undefined, // no givens
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const notebook = await model.getNotebook();
|
|
266
|
+
const parsed = JSON.parse(
|
|
267
|
+
notebook.notebookCells![0].newSources![0],
|
|
268
|
+
);
|
|
269
|
+
expect(parsed.name).toBe("carriers");
|
|
270
|
+
expect(parsed).not.toHaveProperty("givens");
|
|
271
|
+
});
|
|
194
272
|
});
|
|
195
273
|
|
|
196
274
|
describe("getQueryResults", () => {
|
|
@@ -239,6 +317,78 @@ describe("service/model", () => {
|
|
|
239
317
|
sinon.restore();
|
|
240
318
|
});
|
|
241
319
|
|
|
320
|
+
// Both caller-driven compile paths — the free-form `query` text and the
|
|
321
|
+
// `run: source->view` string built from `sourceName`/`queryName` — must
|
|
322
|
+
// go through restricted mode. The trusted `loadQuery` is reserved for
|
|
323
|
+
// author-curated content (notebook cells) and must never be reached from
|
|
324
|
+
// `getQueryResults`. These tests pin the dispatch so a regression that
|
|
325
|
+
// re-routes either path back to `loadQuery` is caught.
|
|
326
|
+
describe("compile dispatch", () => {
|
|
327
|
+
function buildDispatchModel(): {
|
|
328
|
+
model: Model;
|
|
329
|
+
loadQuery: sinon.SinonStub;
|
|
330
|
+
loadRestrictedQuery: sinon.SinonStub;
|
|
331
|
+
} {
|
|
332
|
+
// getPreparedResult rejects so execution stops right after the
|
|
333
|
+
// loader call; we only assert which loader was invoked.
|
|
334
|
+
const runnableStub = {
|
|
335
|
+
getPreparedResult: sinon
|
|
336
|
+
.stub()
|
|
337
|
+
.rejects(new MalloyError("stub-stop", [])),
|
|
338
|
+
run: sinon.stub().rejects(new MalloyError("stub-stop", [])),
|
|
339
|
+
};
|
|
340
|
+
const loadQuery = sinon.stub().returns(runnableStub);
|
|
341
|
+
const loadRestrictedQuery = sinon.stub().returns(runnableStub);
|
|
342
|
+
const modelMaterializer = { loadQuery, loadRestrictedQuery };
|
|
343
|
+
const model = new Model(
|
|
344
|
+
packageName,
|
|
345
|
+
mockModelPath,
|
|
346
|
+
{},
|
|
347
|
+
"model",
|
|
348
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
349
|
+
modelMaterializer as any,
|
|
350
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
351
|
+
{ contents: {}, exports: [], queryList: [] } as any,
|
|
352
|
+
undefined,
|
|
353
|
+
undefined,
|
|
354
|
+
undefined,
|
|
355
|
+
undefined,
|
|
356
|
+
undefined,
|
|
357
|
+
);
|
|
358
|
+
return { model, loadQuery, loadRestrictedQuery };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
afterEach(() => sinon.restore());
|
|
362
|
+
|
|
363
|
+
it("compiles ad-hoc query text in restricted mode, never trusted loadQuery", async () => {
|
|
364
|
+
const { model, loadQuery, loadRestrictedQuery } =
|
|
365
|
+
buildDispatchModel();
|
|
366
|
+
|
|
367
|
+
await expect(
|
|
368
|
+
model.getQueryResults(
|
|
369
|
+
undefined,
|
|
370
|
+
undefined,
|
|
371
|
+
"run: orders -> { aggregate: c is count() }",
|
|
372
|
+
),
|
|
373
|
+
).rejects.toThrow(MalloyError);
|
|
374
|
+
|
|
375
|
+
expect(loadRestrictedQuery.calledOnce).toBe(true);
|
|
376
|
+
expect(loadQuery.called).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("compiles the named source/view path in restricted mode, never trusted loadQuery", async () => {
|
|
380
|
+
const { model, loadQuery, loadRestrictedQuery } =
|
|
381
|
+
buildDispatchModel();
|
|
382
|
+
|
|
383
|
+
await expect(
|
|
384
|
+
model.getQueryResults("orders", "summary"),
|
|
385
|
+
).rejects.toThrow(MalloyError);
|
|
386
|
+
|
|
387
|
+
expect(loadRestrictedQuery.calledOnce).toBe(true);
|
|
388
|
+
expect(loadQuery.called).toBe(false);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
242
392
|
it("forwards givens to runnable.getPreparedResult and .run", async () => {
|
|
243
393
|
const givensArg = { region: "EU" };
|
|
244
394
|
const preparedResultStub = sinon
|
|
@@ -247,11 +397,13 @@ describe("service/model", () => {
|
|
|
247
397
|
const runStub = sinon
|
|
248
398
|
.stub()
|
|
249
399
|
.rejects(new MalloyError("stub-stop", []));
|
|
400
|
+
const runnableStub = {
|
|
401
|
+
getPreparedResult: preparedResultStub,
|
|
402
|
+
run: runStub,
|
|
403
|
+
};
|
|
250
404
|
const modelMaterializer = {
|
|
251
|
-
loadQuery: sinon.stub().returns(
|
|
252
|
-
|
|
253
|
-
run: runStub,
|
|
254
|
-
}),
|
|
405
|
+
loadQuery: sinon.stub().returns(runnableStub),
|
|
406
|
+
loadRestrictedQuery: sinon.stub().returns(runnableStub),
|
|
255
407
|
};
|
|
256
408
|
|
|
257
409
|
const model = new Model(
|
|
@@ -347,11 +499,13 @@ describe("service/model", () => {
|
|
|
347
499
|
typeof API.util.wrapResult
|
|
348
500
|
>,
|
|
349
501
|
);
|
|
502
|
+
const runnableStub = {
|
|
503
|
+
getPreparedResult: preparedResultStub,
|
|
504
|
+
run: runStub,
|
|
505
|
+
};
|
|
350
506
|
const modelMaterializer = {
|
|
351
|
-
loadQuery: sinon.stub().returns(
|
|
352
|
-
|
|
353
|
-
run: runStub,
|
|
354
|
-
}),
|
|
507
|
+
loadQuery: sinon.stub().returns(runnableStub),
|
|
508
|
+
loadRestrictedQuery: sinon.stub().returns(runnableStub),
|
|
355
509
|
};
|
|
356
510
|
const model = new Model(
|
|
357
511
|
packageName,
|
|
@@ -529,6 +683,47 @@ describe("service/model", () => {
|
|
|
529
683
|
|
|
530
684
|
sinon.restore();
|
|
531
685
|
});
|
|
686
|
+
|
|
687
|
+
it("embeds model-level givens in executed cell newSources", async () => {
|
|
688
|
+
const sourceInfo = { name: "carriers", schema: { fields: [] } };
|
|
689
|
+
const givens = [
|
|
690
|
+
{
|
|
691
|
+
name: "region",
|
|
692
|
+
type: "string",
|
|
693
|
+
annotations: ["#(doc) Region"],
|
|
694
|
+
},
|
|
695
|
+
];
|
|
696
|
+
// A source-only code cell (no runnable) still emits newSources.
|
|
697
|
+
const runnableCells = [
|
|
698
|
+
{
|
|
699
|
+
type: "code" as const,
|
|
700
|
+
text: "import 'carriers.malloy'",
|
|
701
|
+
newSources: [sourceInfo],
|
|
702
|
+
},
|
|
703
|
+
];
|
|
704
|
+
|
|
705
|
+
const model = new Model(
|
|
706
|
+
packageName,
|
|
707
|
+
"test.malloynb",
|
|
708
|
+
{},
|
|
709
|
+
"notebook",
|
|
710
|
+
undefined, // modelMaterializer
|
|
711
|
+
undefined, // modelDef
|
|
712
|
+
undefined, // sources
|
|
713
|
+
undefined, // queries
|
|
714
|
+
undefined, // sourceInfos
|
|
715
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
716
|
+
runnableCells as any, // runnableNotebookCells
|
|
717
|
+
undefined, // compilationError
|
|
718
|
+
undefined, // filterMap
|
|
719
|
+
givens, // givens
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
const result = await model.executeNotebookCell(0);
|
|
723
|
+
const parsed = JSON.parse(result.newSources![0]);
|
|
724
|
+
expect(parsed.name).toBe("carriers");
|
|
725
|
+
expect(parsed.givens).toEqual(givens);
|
|
726
|
+
});
|
|
532
727
|
});
|
|
533
728
|
});
|
|
534
729
|
|