@malloy-publisher/server 0.0.227 → 0.0.228
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/README.docker.md +8 -8
- package/dist/app/assets/{EnvironmentPage-DvOJ7L_b.js → EnvironmentPage-EW2lbGvb.js} +1 -1
- package/dist/app/assets/{HomePage-CXguJsXS.js → HomePage-Bkwc9Woc.js} +1 -1
- package/dist/app/assets/{LightMode-ZsshUznu.js → LightMode-Bum_KBpN.js} +1 -1
- package/dist/app/assets/{MainPage-BIe0VwBa.js → MainPage-oiEy7TNM.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-BuZ6UJVx.js → MaterializationsPage-C_VJsTgU.js} +1 -1
- package/dist/app/assets/{ModelPage-DsPf-s8B.js → ModelPage-z8REqAmk.js} +1 -1
- package/dist/app/assets/{PackagePage-CEVNAKZa.js → PackagePage-C2Vtt1Ln.js} +1 -1
- package/dist/app/assets/{RouteError-Chn7lL96.js → RouteError-DmJLpLXm.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-DWC_FdNU.js → ThemeEditorPage-BywFjC7A.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CGrsFz8p.js → WorkbookPage-DCMizDMR.js} +1 -1
- package/dist/app/assets/{core-vVgoO8IR.es-BD_THWs_.js → core-CEDZMHV1.es-_yGzNgNH.js} +1 -1
- package/dist/app/assets/{index-D6YtyiJ0.js → index-CE9xhdra.js} +1 -1
- package/dist/app/assets/{index-BioohWQj.js → index-CdmFub34.js} +1 -1
- package/dist/app/assets/{index-gEWxu09x.js → index-DDMrjIT3.js} +1 -1
- package/dist/app/assets/{index-DNUZpnaa.js → index-EqslXZ44.js} +4 -4
- package/dist/app/index.html +1 -1
- package/dist/default-publisher.config.json +7 -7
- package/dist/runtime/publisher.js +5 -0
- package/dist/server.mjs +841 -963
- package/package.json +1 -1
- package/publisher.config.example.bigquery.json +7 -7
- package/publisher.config.example.duckdb.json +7 -7
- package/publisher.config.json +7 -11
- package/src/config.spec.ts +2 -2
- package/src/controller/package.controller.ts +62 -31
- package/src/default-publisher.config.json +7 -7
- package/src/mcp/handler_utils.spec.ts +108 -0
- package/src/mcp/handler_utils.ts +98 -4
- package/src/mcp/server.protocol.spec.ts +58 -0
- package/src/mcp/server.ts +29 -1
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/tools/compile_tool.spec.ts +207 -0
- package/src/mcp/tools/compile_tool.ts +177 -0
- package/src/mcp/tools/execute_query_tool.spec.ts +143 -0
- package/src/mcp/tools/execute_query_tool.ts +37 -4
- package/src/mcp/tools/get_context_tool.ts +1 -1
- package/src/mcp/tools/reload_package_tool.spec.ts +232 -0
- package/src/mcp/tools/reload_package_tool.ts +158 -0
- package/src/runtime/publisher.js +5 -0
- package/src/server.ts +7 -5
- package/src/service/environment.ts +70 -6
- package/src/service/model.spec.ts +92 -0
- package/src/service/model.ts +58 -7
- package/src/service/package.ts +31 -13
- package/src/service/package_reload_safety.spec.ts +193 -0
- package/tests/fixtures/query-givens/data/orders.csv +7 -0
- package/tests/fixtures/query-givens/model.malloy +34 -0
- package/tests/fixtures/query-givens/publisher.json +5 -0
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +22 -22
- package/tests/integration/query_givens/query_givens.integration.spec.ts +146 -0
- package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +121 -0
- package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +110 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP E2E coverage for `given:` runtime parameters on POST .../query:
|
|
5
|
+
* givens retarget query results, unknown/mistyped givens surface as 400 (not
|
|
6
|
+
* 500), givens compose with filterParams, a `null` given value is an
|
|
7
|
+
* explicit SQL-NULL override, and a numeric-string value is accepted for a
|
|
8
|
+
* `number`-typed given (Malloy's own coercion). Malloy is the single validator:
|
|
9
|
+
* an unknown name or bad value throws a `runtime-given-*` error that
|
|
10
|
+
* model.ts maps to a 400 (see getQueryResults).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { type RestE2EEnv, startRestE2E } from "../../harness/rest_e2e";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ENV_NAME = "query-givens-env";
|
|
20
|
+
const PKG = "query-givens";
|
|
21
|
+
const MODEL = "model.malloy";
|
|
22
|
+
|
|
23
|
+
type Row = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
describe("givens forwarding on /query (HTTP E2E)", () => {
|
|
26
|
+
let env: (RestE2EEnv & { stop(): Promise<void> }) | null = null;
|
|
27
|
+
let baseUrl: string;
|
|
28
|
+
|
|
29
|
+
beforeAll(async () => {
|
|
30
|
+
env = await startRestE2E();
|
|
31
|
+
baseUrl = env.baseUrl;
|
|
32
|
+
const fixtureDir = path.resolve(__dirname, "../../fixtures/query-givens");
|
|
33
|
+
const createRes = await fetch(`${baseUrl}/api/v0/environments`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: { "Content-Type": "application/json" },
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
name: ENV_NAME,
|
|
38
|
+
packages: [{ name: PKG, location: fixtureDir }],
|
|
39
|
+
connections: [],
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
if (!createRes.ok) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Failed to create test environment (${createRes.status}): ${await createRes.text()}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const deadline = Date.now() + 30_000;
|
|
48
|
+
while (Date.now() < deadline) {
|
|
49
|
+
const res = await fetch(
|
|
50
|
+
`${baseUrl}/api/v0/environments/${ENV_NAME}/packages/${PKG}`,
|
|
51
|
+
);
|
|
52
|
+
if (res.ok) break;
|
|
53
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterAll(async () => {
|
|
58
|
+
await fetch(`${baseUrl}/api/v0/environments/${ENV_NAME}`, {
|
|
59
|
+
method: "DELETE",
|
|
60
|
+
}).catch(() => {});
|
|
61
|
+
await env?.stop();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const query = (body: Record<string, unknown>) =>
|
|
65
|
+
fetch(
|
|
66
|
+
`${baseUrl}/api/v0/environments/${ENV_NAME}/packages/${PKG}/models/${MODEL}/query`,
|
|
67
|
+
{
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
sourceName: "orders",
|
|
72
|
+
queryName: "by_given_region",
|
|
73
|
+
compactJson: true,
|
|
74
|
+
...body,
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
async function rows(res: Response): Promise<Row[]> {
|
|
80
|
+
expect(res.status).toBe(200);
|
|
81
|
+
const body = (await res.json()) as { result: string };
|
|
82
|
+
return JSON.parse(body.result) as Row[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
it("retargets rows via a given override (EU vs default US)", async () => {
|
|
86
|
+
const res = await query({ givens: { target_region: "EU" } });
|
|
87
|
+
const r = await rows(res);
|
|
88
|
+
expect(r.length).toBe(1);
|
|
89
|
+
expect(Number(r[0].order_count)).toBe(3);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns 400 for an unknown given name, naming it in the message", async () => {
|
|
93
|
+
// Malloy rejects the unknown name (`runtime-given-unknown`, with a
|
|
94
|
+
// did-you-mean hint) at prepare time; model.ts maps that to a 400 and
|
|
95
|
+
// forwards Malloy's message, which names the given.
|
|
96
|
+
const res = await query({ givens: { NOtaGiven: 1 } });
|
|
97
|
+
expect(res.status).toBe(400);
|
|
98
|
+
const body = (await res.json()) as { message: string };
|
|
99
|
+
expect(body.message).toContain("NOtaGiven");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns 400 for a value-type mismatch on a number-typed given", async () => {
|
|
103
|
+
// "min_amount" is declared `:: number`; a non-numeric string can't be
|
|
104
|
+
// coerced, so Malloy rejects it at prepare time (`runtime-given-*`),
|
|
105
|
+
// surfaced as a 400 via the run try/catch in model.ts, not a 500.
|
|
106
|
+
const res = await query({ givens: { min_amount: "not-a-number" } });
|
|
107
|
+
expect(res.status).toBe(400);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("composes givens with filterParams", async () => {
|
|
111
|
+
// target_region=EU narrows to ids 4,5,6; filterParams status=active
|
|
112
|
+
// narrows further to ids 4,6 -> order_count=2.
|
|
113
|
+
const res = await query({
|
|
114
|
+
givens: { target_region: "EU" },
|
|
115
|
+
filterParams: { status: "active" },
|
|
116
|
+
});
|
|
117
|
+
const r = await rows(res);
|
|
118
|
+
expect(r.length).toBe(1);
|
|
119
|
+
expect(Number(r[0].order_count)).toBe(2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("omitting givens entirely uses the declared defaults (US baseline)", async () => {
|
|
123
|
+
const res = await query({});
|
|
124
|
+
const r = await rows(res);
|
|
125
|
+
expect(r.length).toBe(1);
|
|
126
|
+
expect(Number(r[0].order_count)).toBe(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("a null given value is an explicit SQL-NULL override", async () => {
|
|
130
|
+
// `region = NULL` is never true in SQL, so this should return zero
|
|
131
|
+
// matching rows rather than falling back to the 'US' default.
|
|
132
|
+
const res = await query({ givens: { target_region: null } });
|
|
133
|
+
const r = await rows(res);
|
|
134
|
+
expect(r.length).toBe(1);
|
|
135
|
+
expect(Number(r[0].order_count)).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("accepts a numeric string for a number-typed given", async () => {
|
|
139
|
+
// Guards against Malloy's numeric-string coercion for `number` givens
|
|
140
|
+
// regressing; "5" should be accepted just like the literal 5.
|
|
141
|
+
const res = await query({ givens: { min_amount: "5" } });
|
|
142
|
+
const r = await rows(res);
|
|
143
|
+
expect(r.length).toBe(1);
|
|
144
|
+
expect(Number(r[0].order_count)).toBe(3);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP E2E for the givens × `#(authorize)` interaction. Malloy is the single
|
|
5
|
+
* given validator, so on a GATED source a bad given (unknown NAME or wrong-typed
|
|
6
|
+
* VALUE) is caught inside the authorize probe, which fails closed — surfacing as
|
|
7
|
+
* a 403, not the 400 the ungated path returns. This suite pins that documented
|
|
8
|
+
* asymmetry so it stays intentional:
|
|
9
|
+
*
|
|
10
|
+
* - unknown given name -> 403 (probe can't bind it -> fails closed)
|
|
11
|
+
* - authorized + valid givens -> 200 (retargets rows)
|
|
12
|
+
* - authorize denies -> 403
|
|
13
|
+
* - valid name, BAD value -> 403 (probe can't evaluate it -> fails closed)
|
|
14
|
+
*
|
|
15
|
+
* On an UNGATED source the same unknown name / bad value are a clean 400 (Malloy
|
|
16
|
+
* `runtime-given-*` mapped by model.ts) — see query_givens.integration.spec.ts.
|
|
17
|
+
*
|
|
18
|
+
* See packages/server/src/service/authorize.ts (evaluateAuthorize, fail-closed).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
22
|
+
import path from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
import { type RestE2EEnv, startRestE2E } from "../../harness/rest_e2e";
|
|
25
|
+
|
|
26
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const ENV_NAME = "query-givens-authz-env";
|
|
28
|
+
const PKG = "query-givens";
|
|
29
|
+
const MODEL = "model.malloy";
|
|
30
|
+
|
|
31
|
+
type Row = Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
describe("givens × authorize on /query (HTTP E2E)", () => {
|
|
34
|
+
let env: (RestE2EEnv & { stop(): Promise<void> }) | null = null;
|
|
35
|
+
let baseUrl: string;
|
|
36
|
+
|
|
37
|
+
beforeAll(async () => {
|
|
38
|
+
env = await startRestE2E();
|
|
39
|
+
baseUrl = env.baseUrl;
|
|
40
|
+
const fixtureDir = path.resolve(__dirname, "../../fixtures/query-givens");
|
|
41
|
+
const createRes = await fetch(`${baseUrl}/api/v0/environments`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
name: ENV_NAME,
|
|
46
|
+
packages: [{ name: PKG, location: fixtureDir }],
|
|
47
|
+
connections: [],
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
if (!createRes.ok) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Failed to create test environment (${createRes.status}): ${await createRes.text()}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const deadline = Date.now() + 30_000;
|
|
56
|
+
while (Date.now() < deadline) {
|
|
57
|
+
const res = await fetch(
|
|
58
|
+
`${baseUrl}/api/v0/environments/${ENV_NAME}/packages/${PKG}`,
|
|
59
|
+
);
|
|
60
|
+
if (res.ok) break;
|
|
61
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterAll(async () => {
|
|
66
|
+
await fetch(`${baseUrl}/api/v0/environments/${ENV_NAME}`, {
|
|
67
|
+
method: "DELETE",
|
|
68
|
+
}).catch(() => {});
|
|
69
|
+
await env?.stop();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const queryGated = (body: Record<string, unknown>) =>
|
|
73
|
+
fetch(
|
|
74
|
+
`${baseUrl}/api/v0/environments/${ENV_NAME}/packages/${PKG}/models/${MODEL}/query`,
|
|
75
|
+
{
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
sourceName: "gated_orders",
|
|
80
|
+
queryName: "by_given_region",
|
|
81
|
+
compactJson: true,
|
|
82
|
+
...body,
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
it("unknown given name -> 403 on a gated source (authorize probe fails closed)", async () => {
|
|
88
|
+
// Even with role=admin, the unknown given makes the authorize probe throw
|
|
89
|
+
// (`runtime-given-unknown`), which the gate treats as not-granting. On an
|
|
90
|
+
// ungated source the same name is a clean 400 (see query_givens suite).
|
|
91
|
+
const res = await queryGated({
|
|
92
|
+
givens: { role: "admin", NOtaGiven: 1 },
|
|
93
|
+
});
|
|
94
|
+
expect(res.status).toBe(403);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("authorized caller with valid givens -> 200 and retargets rows", async () => {
|
|
98
|
+
const res = await queryGated({
|
|
99
|
+
givens: { role: "admin", target_region: "EU" },
|
|
100
|
+
});
|
|
101
|
+
expect(res.status).toBe(200);
|
|
102
|
+
const body = (await res.json()) as { result: string };
|
|
103
|
+
const r = JSON.parse(body.result) as Row[];
|
|
104
|
+
expect(Number(r[0].order_count)).toBe(3);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("authorize deny (non-admin role) -> 403", async () => {
|
|
108
|
+
const res = await queryGated({ givens: { role: "guest" } });
|
|
109
|
+
expect(res.status).toBe(403);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("valid given name with a bad value -> 403 on a gated source (fail-closed authorize)", async () => {
|
|
113
|
+
// The authorize probe binds the supplied givens; a value it can't evaluate
|
|
114
|
+
// makes the probe throw and the gate denies. On the UNGATED path the same
|
|
115
|
+
// bad value is a 400 (Malloy at prepare time) — this asymmetry is by design.
|
|
116
|
+
const res = await queryGated({
|
|
117
|
+
givens: { role: "admin", min_amount: "not-a-number" },
|
|
118
|
+
});
|
|
119
|
+
expect(res.status).toBe(403);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Proves the browser SDK (`packages/server/src/runtime/publisher.js`, served
|
|
5
|
+
* at GET /sdk/publisher.js) actually puts `givens` on the wire: fetch the
|
|
6
|
+
* served source, evaluate it in a sandboxed vm context standing in for
|
|
7
|
+
* `window` (self === top, so the in-iframe/live-reload branches are
|
|
8
|
+
* inert no-ops), and call `window.Publisher.query(...)` with a `givens`
|
|
9
|
+
* override against the real HTTP server. Approach used: sandbox-eval via
|
|
10
|
+
* Node's `vm` module — evaluating the actual runtime rather than just
|
|
11
|
+
* grepping its source proves the SDK's fetch body really carries `givens`
|
|
12
|
+
* end-to-end.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import vm from "vm";
|
|
19
|
+
import { type RestE2EEnv, startRestE2E } from "../../harness/rest_e2e";
|
|
20
|
+
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const ENV_NAME = "sdk-givens-env";
|
|
23
|
+
const PKG = "query-givens";
|
|
24
|
+
const MODEL = "model.malloy";
|
|
25
|
+
|
|
26
|
+
type Row = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
describe("Publisher SDK forwards givens (sandbox-eval HTTP E2E)", () => {
|
|
29
|
+
let env: (RestE2EEnv & { stop(): Promise<void> }) | null = null;
|
|
30
|
+
let baseUrl: string;
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
env = await startRestE2E();
|
|
34
|
+
baseUrl = env.baseUrl;
|
|
35
|
+
const fixtureDir = path.resolve(__dirname, "../../fixtures/query-givens");
|
|
36
|
+
const createRes = await fetch(`${baseUrl}/api/v0/environments`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
name: ENV_NAME,
|
|
41
|
+
packages: [{ name: PKG, location: fixtureDir }],
|
|
42
|
+
connections: [],
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
if (!createRes.ok) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Failed to create test environment (${createRes.status}): ${await createRes.text()}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const deadline = Date.now() + 30_000;
|
|
51
|
+
while (Date.now() < deadline) {
|
|
52
|
+
const res = await fetch(
|
|
53
|
+
`${baseUrl}/api/v0/environments/${ENV_NAME}/packages/${PKG}`,
|
|
54
|
+
);
|
|
55
|
+
if (res.ok) break;
|
|
56
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(async () => {
|
|
61
|
+
await fetch(`${baseUrl}/api/v0/environments/${ENV_NAME}`, {
|
|
62
|
+
method: "DELETE",
|
|
63
|
+
}).catch(() => {});
|
|
64
|
+
await env?.stop();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("Publisher.query() with a givens override reaches the server and retargets rows", async () => {
|
|
68
|
+
const sdkRes = await fetch(`${baseUrl}/sdk/publisher.js`);
|
|
69
|
+
expect(sdkRes.status).toBe(200);
|
|
70
|
+
const sdkSource = await sdkRes.text();
|
|
71
|
+
|
|
72
|
+
// Sandbox stands in for `window`. self === top so the runtime's
|
|
73
|
+
// in-iframe (postMessage resize) and live-reload (EventSource) branches
|
|
74
|
+
// are no-ops; document/EventSource are omitted since that code path
|
|
75
|
+
// never runs for a non-embedded page.
|
|
76
|
+
const sandbox: Record<string, unknown> = {
|
|
77
|
+
fetch,
|
|
78
|
+
location: {
|
|
79
|
+
pathname: `/environments/${ENV_NAME}/packages/${PKG}/`,
|
|
80
|
+
origin: baseUrl,
|
|
81
|
+
},
|
|
82
|
+
console,
|
|
83
|
+
};
|
|
84
|
+
sandbox.self = sandbox;
|
|
85
|
+
sandbox.top = sandbox;
|
|
86
|
+
sandbox.window = sandbox;
|
|
87
|
+
|
|
88
|
+
const context = vm.createContext(sandbox);
|
|
89
|
+
vm.runInContext(sdkSource, context, { filename: "publisher.js" });
|
|
90
|
+
|
|
91
|
+
const publisher = (sandbox.window as { Publisher: unknown })
|
|
92
|
+
.Publisher as {
|
|
93
|
+
query: (
|
|
94
|
+
modelPath: string,
|
|
95
|
+
malloyQuery: string | undefined,
|
|
96
|
+
opts: Record<string, unknown>,
|
|
97
|
+
) => Promise<Row[]>;
|
|
98
|
+
};
|
|
99
|
+
expect(publisher).toBeDefined();
|
|
100
|
+
|
|
101
|
+
const rows = await publisher.query(MODEL, undefined, {
|
|
102
|
+
sourceName: "orders",
|
|
103
|
+
queryName: "by_given_region",
|
|
104
|
+
givens: { target_region: "EU" },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(rows.length).toBe(1);
|
|
108
|
+
expect(Number(rows[0].order_count)).toBe(3);
|
|
109
|
+
});
|
|
110
|
+
});
|