@malloydata/malloyyo 0.2.13 → 0.2.14

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 (2) hide show
  1. package/dist/index.js +87 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -945,7 +945,9 @@ async function run(runtime, entry, opts = {}) {
945
945
  return { ok: false, problems: [errorProblem(e, entry.href)] };
946
946
  }
947
947
  let query;
948
- if (opts.name !== void 0) {
948
+ if (opts.runExpr !== void 0) {
949
+ query = materializer.loadQuery(`run: ${opts.runExpr}`);
950
+ } else if (opts.name !== void 0) {
949
951
  if (!modelQueries.named.includes(opts.name)) {
950
952
  return {
951
953
  ok: false,
@@ -1129,18 +1131,10 @@ function describeGivenSpec(name, g) {
1129
1131
  }
1130
1132
  return spec;
1131
1133
  }
1132
- async function dashboardGivenSpecs(runtime, entry, queryName) {
1134
+ async function dashboardGivenSpecs(runtime, entry, runExpr) {
1133
1135
  try {
1134
1136
  const mm = runtime.loadModel(entry);
1135
- const model = await mm.getModel();
1136
- const named = [...model.queries().named];
1137
- if (!named.includes(queryName)) {
1138
- return {
1139
- ok: false,
1140
- error: `no query named '${queryName}' (model has: ${named.join(", ") || "none"})`
1141
- };
1142
- }
1143
- const pq = await mm.loadQueryByName(queryName).getPreparedQuery();
1137
+ const pq = await mm.loadQuery(`run: ${runExpr}`).getPreparedQuery();
1144
1138
  const specs = [];
1145
1139
  for (const [name, g] of pq.givens) {
1146
1140
  specs.push(describeGivenSpec(name, g));
@@ -1158,7 +1152,7 @@ function docText(t) {
1158
1152
  return void 0;
1159
1153
  }
1160
1154
  }
1161
- function readArtifactTag(queryName, q) {
1155
+ function readArtifactTag(ident, q) {
1162
1156
  let tag;
1163
1157
  try {
1164
1158
  tag = q.annotations.parseAsTag().tag;
@@ -1168,9 +1162,11 @@ function readArtifactTag(queryName, q) {
1168
1162
  if (!tag.has("artifact")) return void 0;
1169
1163
  const nested = tag.tag("artifact");
1170
1164
  const description = docText(q);
1171
- const name = nested?.text("name") ?? tag.text("name") ?? queryName;
1172
- const title = nested?.text("title") ?? tag.text("title") ?? description?.split("\n")[0] ?? queryName;
1173
- const info = { name, query: queryName, title };
1165
+ const name = nested?.text("name") ?? tag.text("name") ?? ident.defaultName;
1166
+ const title = nested?.text("title") ?? tag.text("title") ?? description?.split("\n")[0] ?? ident.defaultName;
1167
+ const info = { name, query: ident.runExpr, title };
1168
+ if (ident.source) info.source = ident.source;
1169
+ if (ident.view) info.view = ident.view;
1174
1170
  if (description) info.description = description;
1175
1171
  const givensTag = tag.tag("artifact", "givens");
1176
1172
  if (givensTag) {
@@ -1191,9 +1187,25 @@ async function artifactQueries(runtime, entry) {
1191
1187
  const artifacts = [];
1192
1188
  for (const queryName of model.queries().named) {
1193
1189
  const pq = model.getPreparedQueryByName(queryName);
1194
- const info = readArtifactTag(queryName, pq);
1190
+ const info = readArtifactTag({ runExpr: queryName, defaultName: queryName }, pq);
1195
1191
  if (info) artifacts.push(info);
1196
1192
  }
1193
+ for (const src of model.explores) {
1194
+ for (const field of src.allFields) {
1195
+ if (!field.isQueryField()) continue;
1196
+ const view = field;
1197
+ const info = readArtifactTag(
1198
+ {
1199
+ runExpr: `${src.name} -> ${view.name}`,
1200
+ defaultName: view.name,
1201
+ source: src.name,
1202
+ view: view.name
1203
+ },
1204
+ view
1205
+ );
1206
+ if (info) artifacts.push(info);
1207
+ }
1208
+ }
1197
1209
  return { ok: true, artifacts };
1198
1210
  } catch (e) {
1199
1211
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
@@ -1680,9 +1692,9 @@ async function makeRunner(root) {
1680
1692
  return {
1681
1693
  root: abs,
1682
1694
  entryExists: () => fs.existsSync(path2.join(abs, ENTRY)),
1683
- run(queryName, givens) {
1695
+ run(runExpr, givens) {
1684
1696
  return lease(
1685
- (runtime, entry) => run(runtime, entry, { name: queryName, givens, stableResult: true, rowLimit: 5e3 })
1697
+ (runtime, entry) => run(runtime, entry, { runExpr, givens, stableResult: true, rowLimit: 5e3 })
1686
1698
  );
1687
1699
  },
1688
1700
  runText(malloy, givens) {
@@ -1698,22 +1710,16 @@ async function makeRunner(root) {
1698
1710
  return { ok: false, error: msg || "restricted query failed to compile" };
1699
1711
  });
1700
1712
  },
1701
- givensForQuery(queryName) {
1702
- return lease((runtime, entry) => dashboardGivenSpecs(runtime, entry, queryName));
1713
+ givensForQuery(runExpr) {
1714
+ return lease((runtime, entry) => dashboardGivenSpecs(runtime, entry, runExpr));
1703
1715
  },
1704
1716
  artifacts() {
1705
1717
  return lease((runtime, entry) => artifactQueries(runtime, entry));
1706
1718
  },
1707
- validate(queryName, givens) {
1719
+ validate(runExpr, givens) {
1708
1720
  return lease(async (runtime, entry) => {
1709
1721
  try {
1710
- const mm = runtime.loadModel(entry);
1711
- const model = await mm.getModel();
1712
- const named = [...model.queries().named];
1713
- if (!named.includes(queryName)) {
1714
- return { ok: false, error: `no query named '${queryName}' (model has: ${named.join(", ") || "none"})` };
1715
- }
1716
- const q = mm.loadQueryByName(queryName);
1722
+ const q = runtime.loadModel(entry).loadQuery(`run: ${runExpr}`);
1717
1723
  const has = givens && Object.keys(givens).length > 0;
1718
1724
  await q.getSQL(has ? { givens } : void 0);
1719
1725
  return { ok: true };
@@ -1760,6 +1766,8 @@ async function gatherDashboards(dir) {
1760
1766
  return res.artifacts.map((a) => {
1761
1767
  const tsxPath = join2(dir, "dashboards", a.name, "Dashboard.tsx");
1762
1768
  const manifest = { title: a.title, query: a.query };
1769
+ if (a.source) manifest.source = a.source;
1770
+ if (a.view) manifest.view = a.view;
1763
1771
  if (a.description) manifest.description = a.description;
1764
1772
  if (a.givens) manifest.givens = a.givens;
1765
1773
  return {
@@ -2174,9 +2182,11 @@ with \`malloyyo lint\`.
2174
2182
  server only see what that file EXPORTS. Three things must all be surfaced
2175
2183
  (imported AND exported) through it, or the feature looks broken:
2176
2184
 
2177
- 1. **The \`# artifact\`-tagged queries.** Declared in another file and not
2178
- exported \u2192 \`dashboard dev\` says "No dashboards declared" and \`lint\` says
2179
- "no dashboards to lint", even though the model compiles clean.
2185
+ 1. **Whatever holds each \`# artifact\` tag.** A tag on a \`view:\` rides along
2186
+ with its SOURCE (export the source \u2014 you can't export a view on its own); a
2187
+ tag on a top-level \`query:\` needs that query exported. Not surfaced \u2192
2188
+ \`dashboard dev\` says "No dashboards declared" and \`lint\` says "no dashboards
2189
+ to lint", even though the model compiles clean.
2180
2190
  2. **Every filter given the dashboards reference.** An unexported given
2181
2191
  silently resolves to its declaration default \u2014 the control still renders
2182
2192
  but CAN'T CHANGE THE QUERY (the filter looks inert).
@@ -2187,49 +2197,61 @@ server only see what that file EXPORTS. Three things must all be surfaced
2187
2197
  \`\`\`malloy
2188
2198
  ##! experimental.givens
2189
2199
  import {
2190
- order_items,
2200
+ order_items, // the source \u2014 carries its # artifact views
2191
2201
  BRAND, CATEGORY, PERIOD, // the filter givens
2192
- brand_suggest, // backs a suggest {query=\u2026}
2193
- overview_dashboard // the # artifact query
2202
+ brand_suggest // backs a suggest {query=\u2026}
2194
2203
  } from 'ecommerce.malloy'
2195
- export { order_items, BRAND, CATEGORY, PERIOD, brand_suggest, overview_dashboard }
2204
+ export { order_items, BRAND, CATEGORY, PERIOD, brand_suggest }
2196
2205
  \`\`\`
2197
2206
 
2207
+ Exporting the source is often the whole job: its \`# artifact\` views, its
2208
+ dimensions (for \`suggest {source=\u2026}\`), and its measures all travel with it.
2209
+
2198
2210
  Prefer \`suggest { query=<named-query> \u2026 }\` over \`suggest { source=\u2026 }\` for
2199
2211
  anything beyond a throwaway: you export one small governed query instead of a
2200
2212
  whole base source.
2201
2213
 
2202
2214
  ## The model is the whole contract
2203
2215
 
2204
- **1. Tag a top-level query** with \`# artifact\` to declare a dashboard. For
2205
- the common overview shape (top-level aggregates + nests), ALSO tag it
2206
- \`# dashboard\` so the result renders as KPI tiles + a card grid instead of one
2207
- flat table \u2014 they're partners: \`# artifact\` declares the dashboard,
2208
- \`# dashboard\` is the renderer tag that draws it like one:
2216
+ **1. Tag a \`view:\` inside a source** with \`# artifact\` to declare a dashboard
2217
+ (the idiomatic form \u2014 a view is reusable, nestable, and explorable through the
2218
+ normal \`query\`/\`describe_source\` surface). For the common overview shape
2219
+ (top-level aggregates + nests), ALSO tag it \`# dashboard\` so the result
2220
+ renders as KPI tiles + a card grid instead of one flat table \u2014 they're
2221
+ partners: \`# artifact\` declares the dashboard, \`# dashboard\` is the renderer
2222
+ tag that draws it like one:
2209
2223
 
2210
2224
  \`\`\`malloy
2211
- #" Business health at a glance \u2014 sales, margin, orders.
2212
- # artifact { title="Business Overview" } dashboard
2213
- query: overview_dashboard is order_items -> {
2214
- where:
2215
- inventory_items.product_brand ~ $BRAND, // multi-filter where: is
2216
- inventory_items.product_category ~ $CATEGORY, // COMMA separated
2217
- created_at ~ $PERIOD
2218
- aggregate: total_sales, total_gross_margin, order_count
2219
- nest:
2220
- # line_chart
2221
- sales_trend is by_month
2222
- top_brands
2223
- # shape_map
2224
- sales_by_state
2225
+ source: order_items is \u2026 extend {
2226
+ #" Business health at a glance \u2014 sales, margin, orders.
2227
+ # artifact { title="Business Overview" } dashboard
2228
+ view: overview_dashboard is {
2229
+ where:
2230
+ inventory_items.product_brand ~ $BRAND, // multi-filter where: is
2231
+ inventory_items.product_category ~ $CATEGORY, // COMMA separated
2232
+ created_at ~ $PERIOD
2233
+ aggregate: total_sales, total_gross_margin, order_count
2234
+ nest:
2235
+ # line_chart
2236
+ sales_trend is by_month
2237
+ top_brands
2238
+ # shape_map
2239
+ sales_by_state
2240
+ }
2225
2241
  }
2226
2242
  \`\`\`
2227
2243
 
2228
2244
  That's a complete dashboard: the runtime auto-renders a title (the tag's
2229
- \`title\`, else the \`#"\` doc comment), a control for every given the query
2230
- references, and the result panel. \`name="slug"\` overrides the URL/directory
2231
- slug (default: the query name). Note the \`where:\` clauses applying givens are
2232
- COMMA-separated \u2014 newline-separated conditions do not parse.
2245
+ \`title\`, else the \`#"\` doc comment), a control for every given the view
2246
+ references, and the result panel. It runs as \`run: <source> -> <view>\` (here
2247
+ \`order_items -> overview_dashboard\`). \`name="slug"\` overrides the
2248
+ URL/directory slug (default: the view name). Note the \`where:\` clauses
2249
+ applying givens are COMMA-separated \u2014 newline-separated conditions do not
2250
+ parse.
2251
+
2252
+ Tagging a **top-level \`query:\`** still works and behaves identically (it runs
2253
+ as \`run: <name>\`) \u2014 reach for it only when the dashboard query doesn't belong
2254
+ to any one source.
2233
2255
 
2234
2256
  Two dashboards can share a given but start on different values \u2014 a \`givens\`
2235
2257
  block in the tag sets PER-DASHBOARD defaults (given values, i.e. filter
@@ -2400,11 +2422,12 @@ usually means the \`# artifact\` queries aren't exported through
2400
2422
 
2401
2423
  Validation loop that works well: the local \`malloyyo mcp\` server hot-reloads
2402
2424
  working-directory edits \u2014 \`query(execute:false)\` to compile-check,
2403
- \`execute:true\` to run. A top-level \`# artifact\` query runs as
2404
- \`run: <name>\` (not \`source -> <name>\`), and is only visible once exported
2405
- through the entry. Don't validate local edits against a hosted/claude.ai
2406
- connector \u2014 that serves the PUBLISHED model, which is stale until
2407
- \`malloyyo publish\`.
2425
+ \`execute:true\` to run. A \`# artifact\` view runs as
2426
+ \`run: <source> -> <view>\`; a top-level \`# artifact\` query runs as
2427
+ \`run: <name>\`. Either is only visible once surfaced through the entry (export
2428
+ the source for a view, the query for a top-level query). Don't validate local
2429
+ edits against a hosted/claude.ai connector \u2014 that serves the PUBLISHED model,
2430
+ which is stale until \`malloyyo publish\`.
2408
2431
  `;
2409
2432
 
2410
2433
  // src/mcp.ts
@@ -2790,7 +2813,7 @@ async function serveDashboard(opts) {
2790
2813
  }
2791
2814
 
2792
2815
  // package.json
2793
- var version = "0.2.13";
2816
+ var version = "0.2.14";
2794
2817
 
2795
2818
  // src/index.ts
2796
2819
  function shortSha(sha) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloyyo",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Publish Malloy models to a Malloyyo instance",
5
5
  "license": "MIT",
6
6
  "repository": {