@malloydata/malloyyo 0.2.29 → 0.2.31
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.md +15 -0
- package/dist/index.js +108 -9
- package/dist/shared/env-refs.ts +45 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -74,6 +74,21 @@ malloyyo publish main --dry-run # show what would be sent
|
|
|
74
74
|
malloyyo status main # what's live: version, commit, compile state
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
The target dataset must already exist; publishing to a missing one fails rather than
|
|
78
|
+
inventing it (a config typo would otherwise spawn junk datasets). To provision it from the
|
|
79
|
+
CLI instead of the UI, opt in explicitly:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
malloyyo publish main --create-dataset # create the dataset if it isn't there yet
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The dataset is created **only after the model compiles**, so a rejected publish still
|
|
86
|
+
creates nothing, and it is created **private** — visibility is a deliberate act in the UI,
|
|
87
|
+
and publishing never changes it. On an existing dataset the flag does nothing: you just get
|
|
88
|
+
the next version. The dataset name comes from the target's `dataset` in the config, and must
|
|
89
|
+
already be a valid name (lowercase letters, digits, underscores) — the CLI won't silently
|
|
90
|
+
create it under a slugified variant that later publishes wouldn't find.
|
|
91
|
+
|
|
77
92
|
`publish` exits non-zero on a server-side compile failure, so it's safe to gate CI on.
|
|
78
93
|
|
|
79
94
|
**Token precedence:** `--token` flag → the `malloyyo_token` env var from config (for CI) →
|
package/dist/index.js
CHANGED
|
@@ -2608,6 +2608,36 @@ function printLintReport(report) {
|
|
|
2608
2608
|
}
|
|
2609
2609
|
}
|
|
2610
2610
|
|
|
2611
|
+
// src/shared/env-refs.ts
|
|
2612
|
+
function missingEnvRefs(configJson, env = process.env) {
|
|
2613
|
+
if (!configJson) return [];
|
|
2614
|
+
let parsed;
|
|
2615
|
+
try {
|
|
2616
|
+
parsed = JSON.parse(configJson);
|
|
2617
|
+
} catch {
|
|
2618
|
+
return [];
|
|
2619
|
+
}
|
|
2620
|
+
const missing = /* @__PURE__ */ new Set();
|
|
2621
|
+
const walk = (node) => {
|
|
2622
|
+
if (Array.isArray(node)) return void node.forEach(walk);
|
|
2623
|
+
if (typeof node !== "object" || node === null) return;
|
|
2624
|
+
const rec = node;
|
|
2625
|
+
if (typeof rec.env === "string" && !env[rec.env]) missing.add(rec.env);
|
|
2626
|
+
for (const v of Object.values(rec)) walk(v);
|
|
2627
|
+
};
|
|
2628
|
+
walk(parsed);
|
|
2629
|
+
return [...missing];
|
|
2630
|
+
}
|
|
2631
|
+
function missingEnvHint(missing, where) {
|
|
2632
|
+
if (missing.length === 0) return "";
|
|
2633
|
+
const vars = missing.map((v) => `$${v}`).join(", ");
|
|
2634
|
+
const isAre = missing.length > 1 ? "are" : "is";
|
|
2635
|
+
return `
|
|
2636
|
+
malloy-config.json references ${vars}, which ${isAre} NOT set on ${where}.
|
|
2637
|
+
Malloy reads an unset reference as an empty value, which usually shows up as
|
|
2638
|
+
the connection error above.`;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2611
2641
|
// src/oauth.ts
|
|
2612
2642
|
import http from "node:http";
|
|
2613
2643
|
import crypto from "node:crypto";
|
|
@@ -2790,6 +2820,11 @@ async function refresh(baseUrl, creds) {
|
|
|
2790
2820
|
saveCreds(baseUrl, updated);
|
|
2791
2821
|
return updated;
|
|
2792
2822
|
}
|
|
2823
|
+
function tokenSource(target, opts) {
|
|
2824
|
+
if (opts.tokenFlag) return "flag";
|
|
2825
|
+
if (target.tokenEnv && process.env[target.tokenEnv]) return "env";
|
|
2826
|
+
return "login";
|
|
2827
|
+
}
|
|
2793
2828
|
async function getAccessToken(target, opts) {
|
|
2794
2829
|
if (opts.tokenFlag) return opts.tokenFlag;
|
|
2795
2830
|
if (target.tokenEnv && process.env[target.tokenEnv]) return process.env[target.tokenEnv];
|
|
@@ -4202,15 +4237,66 @@ async function launchCmd(mode, opts) {
|
|
|
4202
4237
|
}
|
|
4203
4238
|
|
|
4204
4239
|
// package.json
|
|
4205
|
-
var version = "0.2.
|
|
4240
|
+
var version = "0.2.31";
|
|
4206
4241
|
|
|
4207
4242
|
// src/index.ts
|
|
4208
4243
|
function shortSha(sha) {
|
|
4209
4244
|
return sha ? sha.slice(0, 7) : "";
|
|
4210
4245
|
}
|
|
4246
|
+
function authHint(status2, t, source) {
|
|
4247
|
+
const login2 = `malloyyo login ${t.name}`;
|
|
4248
|
+
if (status2 === 403) {
|
|
4249
|
+
return `
|
|
4250
|
+
The token is valid, but that account isn't an admin on ${t.url} \u2014
|
|
4251
|
+
publishing is admin-only. Ask an admin there to grant access.`;
|
|
4252
|
+
}
|
|
4253
|
+
switch (source) {
|
|
4254
|
+
case "flag":
|
|
4255
|
+
return `
|
|
4256
|
+
That token came from --token. Drop the flag and run: ${login2}`;
|
|
4257
|
+
case "env":
|
|
4258
|
+
return `
|
|
4259
|
+
That token came from $${t.tokenEnv}. Re-issue it, or unset it and run: ${login2}`;
|
|
4260
|
+
default:
|
|
4261
|
+
return `
|
|
4262
|
+
Your saved login for ${t.url} is expired or revoked.
|
|
4263
|
+
Run: ${login2}`;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
function failureHint(out, t) {
|
|
4267
|
+
switch (out.kind) {
|
|
4268
|
+
case "missing-import":
|
|
4269
|
+
return `
|
|
4270
|
+
A file the model imports wasn't in the upload. Publish from the directory
|
|
4271
|
+
that holds index.malloy, and check the import path's spelling/case.`;
|
|
4272
|
+
case "connection":
|
|
4273
|
+
if (out.missingEnv?.length) {
|
|
4274
|
+
const vars = out.missingEnv.map((v) => `$${v}`).join(", ");
|
|
4275
|
+
return `
|
|
4276
|
+
malloy-config.json references ${vars}, which ${out.missingEnv.length > 1 ? "are" : "is"} NOT set on ${t.url}.
|
|
4277
|
+
Secrets don't travel with the model \u2014 set them in that deployment's environment
|
|
4278
|
+
(Vercel: Settings \u2192 Environment Variables), then publish again.`;
|
|
4279
|
+
}
|
|
4280
|
+
return `
|
|
4281
|
+
The server couldn't open the connection the model uses. Check the
|
|
4282
|
+
\`connections\` block in malloy-config.json, and that ${t.url} can reach it.`;
|
|
4283
|
+
case "persist":
|
|
4284
|
+
return `
|
|
4285
|
+
The model itself is fine \u2014 this failed writing to the server's database.
|
|
4286
|
+
Retry; if it repeats, the message above is the database's own.`;
|
|
4287
|
+
default:
|
|
4288
|
+
return "";
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
function requestFailed(what, res, out, t, source) {
|
|
4292
|
+
const detail = out.error ?? `${res.status} ${res.statusText}`;
|
|
4293
|
+
const hint = res.status === 401 || res.status === 403 ? authHint(res.status, t, source) : failureHint(out, t);
|
|
4294
|
+
return new Error(`${what} failed: ${detail}${hint}`);
|
|
4295
|
+
}
|
|
4211
4296
|
async function publish(target, dir, opts) {
|
|
4212
4297
|
const root = resolve2(dir);
|
|
4213
4298
|
const t = resolveTarget(root, target);
|
|
4299
|
+
const source = tokenSource(t, { tokenFlag: opts.token });
|
|
4214
4300
|
const bearer = await getAccessToken(t, { tokenFlag: opts.token });
|
|
4215
4301
|
const { files, config } = gatherDirectory(root);
|
|
4216
4302
|
if (files.length === 0) {
|
|
@@ -4223,27 +4309,33 @@ async function publish(target, dir, opts) {
|
|
|
4223
4309
|
printLintReport(report);
|
|
4224
4310
|
}
|
|
4225
4311
|
if (!report.ok) {
|
|
4226
|
-
throw new Error(
|
|
4312
|
+
throw new Error(
|
|
4313
|
+
"dashboard lint failed \u2014 fix the above, or pass --skip-lint" + missingEnvHint(missingEnvRefs(config), "this shell")
|
|
4314
|
+
);
|
|
4227
4315
|
}
|
|
4228
4316
|
}
|
|
4229
4317
|
const git = gitInfo(root);
|
|
4230
4318
|
const dashboards = await gatherDashboards(root);
|
|
4231
4319
|
const body = { files, config, git, dashboards };
|
|
4232
4320
|
const provenance = git.sha ? `${git.branch}@${shortSha(git.sha)}${git.dirty ? " (dirty)" : ""}` : "(no git)";
|
|
4233
|
-
console.log(`\u2192 ${t.url} dataset=${t.dataset}`);
|
|
4321
|
+
console.log(`\u2192 ${t.url} dataset=${t.dataset}${opts.createDataset ? " (create if missing)" : ""}`);
|
|
4234
4322
|
console.log(` ${files.length} file(s) ${provenance}`);
|
|
4235
4323
|
if (opts.dryRun) {
|
|
4236
4324
|
console.log("dry run \u2014 not sending");
|
|
4237
4325
|
return;
|
|
4238
4326
|
}
|
|
4239
|
-
const
|
|
4327
|
+
const push = `${t.url}/api/datasets/${t.dataset}/model/push${opts.createDataset ? "?create=1" : ""}`;
|
|
4328
|
+
const res = await fetch(push, {
|
|
4240
4329
|
method: "POST",
|
|
4241
4330
|
headers: { "content-type": "application/json", authorization: `Bearer ${bearer}` },
|
|
4242
4331
|
body: JSON.stringify(body)
|
|
4243
4332
|
});
|
|
4244
4333
|
const out = await res.json().catch(() => ({}));
|
|
4245
4334
|
if (!res.ok || !out.ok) {
|
|
4246
|
-
throw
|
|
4335
|
+
throw requestFailed("publish", res, out, t, source);
|
|
4336
|
+
}
|
|
4337
|
+
if (out.created) {
|
|
4338
|
+
console.log(`\u2713 created dataset ${out.dataset ?? t.dataset} (private) \u2014 ${t.url}/datasets/${out.dataset ?? t.dataset}`);
|
|
4247
4339
|
}
|
|
4248
4340
|
console.log(
|
|
4249
4341
|
`\u2713 published version ${out.version} \u2014 ${out.sources?.length ?? 0} source(s)` + (dashboards.length ? `, ${dashboards.length} dashboard(s)` : "")
|
|
@@ -4251,12 +4343,14 @@ async function publish(target, dir, opts) {
|
|
|
4251
4343
|
}
|
|
4252
4344
|
async function status(target, opts) {
|
|
4253
4345
|
const t = resolveTarget(resolve2("."), target);
|
|
4346
|
+
const source = tokenSource(t, { tokenFlag: opts.token });
|
|
4254
4347
|
const bearer = await getAccessToken(t, { tokenFlag: opts.token });
|
|
4255
4348
|
const res = await fetch(`${t.url}/api/datasets/${t.dataset}/model/status`, {
|
|
4256
4349
|
headers: { authorization: `Bearer ${bearer}` }
|
|
4257
4350
|
});
|
|
4258
4351
|
if (!res.ok) {
|
|
4259
|
-
|
|
4352
|
+
const body = await res.json().catch(() => ({}));
|
|
4353
|
+
throw requestFailed("status", res, body, t, source);
|
|
4260
4354
|
}
|
|
4261
4355
|
const s = await res.json();
|
|
4262
4356
|
const git = s.git;
|
|
@@ -4277,15 +4371,20 @@ var program = new Command();
|
|
|
4277
4371
|
program.name("malloyyo").description("Publish Malloy models to a Malloyyo instance").version(version);
|
|
4278
4372
|
program.command("login").argument("[target]", "target name or instance URL (optional if the config has one target)").description("sign in to an instance in your browser (stores a token)").action(loginCmd);
|
|
4279
4373
|
program.command("logout").argument("[target]", "target name or instance URL (optional if the config has one target)").description("forget the stored token for an instance").action(logoutCmd);
|
|
4280
|
-
program.command("publish").argument("<target>", "named target from the `malloyyo` config block").argument("[dir]", "directory to publish", ".").option("--token <token>", "bearer token (overrides login/env)").option("--dry-run", "gather and report what would be sent, but don't POST").option("--skip-lint", "skip the pre-publish dashboard lint").description('push the Malloy model in <dir> (default ".") to <target>').action(publish);
|
|
4374
|
+
program.command("publish").argument("<target>", "named target from the `malloyyo` config block").argument("[dir]", "directory to publish", ".").option("--token <token>", "bearer token (overrides login/env)").option("--dry-run", "gather and report what would be sent, but don't POST").option("--skip-lint", "skip the pre-publish dashboard lint").option("--create-dataset", "create the target dataset if it doesn't exist yet (private)").description('push the Malloy model in <dir> (default ".") to <target>').action(publish);
|
|
4281
4375
|
program.command("lint").argument("[dir]", "directory to lint", ".").description("validate ./dashboards against the model (manifest, query, givens, Dashboard.tsx)").action(async (dir) => {
|
|
4282
|
-
const
|
|
4376
|
+
const root = resolve2(dir);
|
|
4377
|
+
const report = await lintDashboards(root);
|
|
4283
4378
|
if (report.dashboards.length === 0) {
|
|
4284
4379
|
console.log("no dashboards to lint");
|
|
4285
4380
|
return;
|
|
4286
4381
|
}
|
|
4287
4382
|
printLintReport(report);
|
|
4288
|
-
if (!report.ok)
|
|
4383
|
+
if (!report.ok) {
|
|
4384
|
+
const hint = missingEnvHint(missingEnvRefs(gatherDirectory(root).config), "this shell");
|
|
4385
|
+
if (hint) console.error(hint.replace(/^\n/, ""));
|
|
4386
|
+
process.exit(1);
|
|
4387
|
+
}
|
|
4289
4388
|
});
|
|
4290
4389
|
program.command("status").argument("<target>", "named target from the `malloyyo` config block").option("--token <token>", "bearer token (overrides login/env)").description("show what's live on <target>: version, commit, compile state").action(status);
|
|
4291
4390
|
program.command("mcp").option("-C, --root <dir>", "project root (default: current directory)").option("--develop", "author surface: compile/prettify/query any .malloy in the project").option("--explore", "explore surface: the claude.ai web preview (index.malloy only) [default]").description(
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) The Malloy Foundation
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//
|
|
4
|
+
// malloy-config.json can hold secrets by reference: `"password": { "env": "PG_PW" }`.
|
|
5
|
+
// Malloy resolves an UNSET reference to empty rather than failing, so the symptom
|
|
6
|
+
// is a baffling connection error — "connect ECONNREFUSED", "password
|
|
7
|
+
// authentication failed" — that never mentions the variable. Both the CLI (this
|
|
8
|
+
// shell) and the push route (that deployment) check for it and say the name.
|
|
9
|
+
|
|
10
|
+
/** `{"env": "NAME"}` references in `configJson` with no value in `env`. Deduped. */
|
|
11
|
+
export function missingEnvRefs(
|
|
12
|
+
configJson: string | undefined,
|
|
13
|
+
env: Record<string, string | undefined> = process.env,
|
|
14
|
+
): string[] {
|
|
15
|
+
if (!configJson) return [];
|
|
16
|
+
let parsed: unknown;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(configJson);
|
|
19
|
+
} catch {
|
|
20
|
+
return []; // Malloy reports the bad JSON itself.
|
|
21
|
+
}
|
|
22
|
+
const missing = new Set<string>();
|
|
23
|
+
const walk = (node: unknown): void => {
|
|
24
|
+
if (Array.isArray(node)) return void node.forEach(walk);
|
|
25
|
+
if (typeof node !== "object" || node === null) return;
|
|
26
|
+
const rec = node as Record<string, unknown>;
|
|
27
|
+
if (typeof rec.env === "string" && !env[rec.env]) missing.add(rec.env);
|
|
28
|
+
for (const v of Object.values(rec)) walk(v);
|
|
29
|
+
};
|
|
30
|
+
walk(parsed);
|
|
31
|
+
return [...missing];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One-line-per-var explanation, or "" when nothing is missing. `where` names
|
|
35
|
+
the environment the check ran against ("this shell", a server URL). */
|
|
36
|
+
export function missingEnvHint(missing: string[], where: string): string {
|
|
37
|
+
if (missing.length === 0) return "";
|
|
38
|
+
const vars = missing.map((v) => `$${v}`).join(", ");
|
|
39
|
+
const isAre = missing.length > 1 ? "are" : "is";
|
|
40
|
+
return (
|
|
41
|
+
`\n malloy-config.json references ${vars}, which ${isAre} NOT set on ${where}.` +
|
|
42
|
+
`\n Malloy reads an unset reference as an empty value, which usually shows up as` +
|
|
43
|
+
`\n the connection error above.`
|
|
44
|
+
);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@malloydata/malloyyo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Publish Malloy models to a Malloyyo instance",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@duckdb/duckdb-wasm": "1.33.1-dev45.0",
|
|
36
|
-
"@malloydata/db-duckdb": "^0.0.
|
|
37
|
-
"@malloydata/malloy": "^0.0.
|
|
38
|
-
"@malloydata/malloy-connections": "^0.0.
|
|
39
|
-
"@malloydata/malloy-filter": "^0.0.
|
|
40
|
-
"@malloydata/render": "^0.0.
|
|
36
|
+
"@malloydata/db-duckdb": "^0.0.430",
|
|
37
|
+
"@malloydata/malloy": "^0.0.430",
|
|
38
|
+
"@malloydata/malloy-connections": "^0.0.430",
|
|
39
|
+
"@malloydata/malloy-filter": "^0.0.430",
|
|
40
|
+
"@malloydata/render": "^0.0.430",
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
42
|
"commander": "^12.1.0",
|
|
43
43
|
"esbuild": "^0.24.0",
|