@malloy-publisher/server 0.0.180 → 0.0.182

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 (27) hide show
  1. package/dist/app/api-doc.yaml +91 -1
  2. package/dist/app/assets/{HomePage-DRmAsRAP.js → HomePage-or6BbD5P.js} +1 -1
  3. package/dist/app/assets/{MainPage-BLhfzy47.js → MainPage-DINuSDg0.js} +2 -2
  4. package/dist/app/assets/{ModelPage-bgdjxhyc.js → ModelPage-BMcaV1YQ.js} +1 -1
  5. package/dist/app/assets/{PackagePage-rPw0OAJY.js → PackagePage-DXxlQcCj.js} +1 -1
  6. package/dist/app/assets/{ProjectPage-D0DYloUr.js → ProjectPage-vfZc_Kvu.js} +1 -1
  7. package/dist/app/assets/{RouteError-CsFH2AdT.js → RouteError-r14osUo0.js} +1 -1
  8. package/dist/app/assets/{WorkbookPage-CQ37Bfli.js → WorkbookPage-HI39NTWs.js} +1 -1
  9. package/dist/app/assets/{index-C2IkGoJ8.js → index-Bw1lh09G.js} +78 -78
  10. package/dist/app/assets/{index-Cev5PtEG.js → index-Dd6uCk_C.js} +1 -1
  11. package/dist/app/assets/{index-DcnbmCmI.js → index-JqHhhRqY.js} +168 -166
  12. package/dist/app/assets/index.umd-lwkX_kFe.js +1145 -0
  13. package/dist/app/index.html +1 -1
  14. package/dist/server.js +323 -31
  15. package/package.json +10 -10
  16. package/src/controller/model.controller.ts +4 -1
  17. package/src/controller/query.controller.ts +5 -0
  18. package/src/mcp/resources/model_resource.ts +12 -9
  19. package/src/mcp/resources/source_resource.ts +7 -6
  20. package/src/mcp/resources/view_resource.ts +0 -1
  21. package/src/mcp/tools/execute_query_tool.ts +9 -0
  22. package/src/server.ts +21 -0
  23. package/src/service/filter.spec.ts +392 -0
  24. package/src/service/filter.ts +332 -0
  25. package/src/service/filter_integration.spec.ts +622 -0
  26. package/src/service/model.ts +180 -43
  27. package/dist/app/assets/index.umd-BwIMLH79.js +0 -1145
@@ -12,7 +12,7 @@
12
12
  href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
13
13
  />
14
14
  <title>Malloy Publisher</title>
15
- <script type="module" crossorigin src="/assets/index-C2IkGoJ8.js"></script>
15
+ <script type="module" crossorigin src="/assets/index-Bw1lh09G.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="/assets/index-CMlGQMcl.css">
17
17
  </head>
18
18
  <body>
package/dist/server.js CHANGED
@@ -222749,7 +222749,7 @@ class ModelController {
222749
222749
  }
222750
222750
  return model.getNotebook();
222751
222751
  }
222752
- async executeNotebookCell(projectName, packageName, notebookPath, cellIndex) {
222752
+ async executeNotebookCell(projectName, packageName, notebookPath, cellIndex, filterParams, bypassFilters) {
222753
222753
  const project = await this.projectStore.getProject(projectName, false);
222754
222754
  const p = await project.getPackage(packageName, false);
222755
222755
  const model = p.getModel(notebookPath);
@@ -222759,7 +222759,7 @@ class ModelController {
222759
222759
  if (model.getType() === "model") {
222760
222760
  throw new ModelNotFoundError(`${notebookPath} is a model`);
222761
222761
  }
222762
- return model.executeNotebookCell(cellIndex);
222762
+ return model.executeNotebookCell(cellIndex, filterParams, bypassFilters);
222763
222763
  }
222764
222764
  }
222765
222765
 
@@ -222849,14 +222849,14 @@ class QueryController {
222849
222849
  constructor(projectStore) {
222850
222850
  this.projectStore = projectStore;
222851
222851
  }
222852
- async getQuery(projectName, packageName, modelPath, sourceName, queryName, query, compactJson = false) {
222852
+ async getQuery(projectName, packageName, modelPath, sourceName, queryName, query, compactJson = false, filterParams, bypassFilters) {
222853
222853
  const project = await this.projectStore.getProject(projectName, false);
222854
222854
  const p = await project.getPackage(packageName, false);
222855
222855
  const model = p.getModel(modelPath);
222856
222856
  if (!model) {
222857
222857
  throw new ModelNotFoundError(`${modelPath} does not exist`);
222858
222858
  } else {
222859
- const { result, compactResult } = await model.getQueryResults(sourceName, queryName, query);
222859
+ const { result, compactResult } = await model.getQueryResults(sourceName, queryName, query, filterParams, bypassFilters);
222860
222860
  const renderLogs = import_render_validator.validateRenderTags(result);
222861
222861
  return {
222862
222862
  result: compactJson ? JSON.stringify(compactResult, bigIntReplacer) : JSON.stringify(result),
@@ -229601,6 +229601,196 @@ class HackyDataStylesAccumulator {
229601
229601
  }
229602
229602
  }
229603
229603
 
229604
+ // src/service/filter.ts
229605
+ var VALID_FILTER_TYPES = new Set([
229606
+ "equal",
229607
+ "in",
229608
+ "like",
229609
+ "greater_than",
229610
+ "less_than"
229611
+ ]);
229612
+ var ANNOTATION_PREFIX = "#(filter)";
229613
+ function parseFilterAnnotation(annotation) {
229614
+ const trimmed2 = annotation.trim();
229615
+ if (!trimmed2.startsWith(ANNOTATION_PREFIX)) {
229616
+ return null;
229617
+ }
229618
+ const body = trimmed2.slice(ANNOTATION_PREFIX.length).trim();
229619
+ const tokens = tokenize(body);
229620
+ let name;
229621
+ let dimension;
229622
+ let type;
229623
+ let implicit = false;
229624
+ let required = false;
229625
+ for (const token of tokens) {
229626
+ if (token.includes("=")) {
229627
+ const eqIndex = token.indexOf("=");
229628
+ const key = token.slice(0, eqIndex).toLowerCase();
229629
+ const value = token.slice(eqIndex + 1);
229630
+ switch (key) {
229631
+ case "name":
229632
+ name = value;
229633
+ break;
229634
+ case "dimension":
229635
+ dimension = value;
229636
+ break;
229637
+ case "type":
229638
+ if (!VALID_FILTER_TYPES.has(value)) {
229639
+ throw new Error(`Invalid filter type "${value}". Must be one of: ${[...VALID_FILTER_TYPES].join(", ")}`);
229640
+ }
229641
+ type = value;
229642
+ break;
229643
+ default:
229644
+ throw new Error(`Unknown filter parameter "${key}"`);
229645
+ }
229646
+ } else {
229647
+ const flag = token.toLowerCase();
229648
+ if (flag === "implicit") {
229649
+ implicit = true;
229650
+ } else if (flag === "required") {
229651
+ required = true;
229652
+ } else {
229653
+ throw new Error(`Unknown filter flag "${token}"`);
229654
+ }
229655
+ }
229656
+ }
229657
+ if (!dimension) {
229658
+ throw new Error("filter annotation missing required 'dimension' parameter");
229659
+ }
229660
+ if (!type) {
229661
+ throw new Error("filter annotation missing required 'type' parameter");
229662
+ }
229663
+ return {
229664
+ name: name ?? dimension,
229665
+ dimension,
229666
+ type,
229667
+ implicit,
229668
+ required
229669
+ };
229670
+ }
229671
+ function parseFilters(annotations) {
229672
+ const filters = [];
229673
+ for (const annotation of annotations) {
229674
+ const parsed = parseFilterAnnotation(annotation);
229675
+ if (parsed) {
229676
+ filters.push(parsed);
229677
+ }
229678
+ }
229679
+ return filters;
229680
+ }
229681
+ function escapeMalloyString(value) {
229682
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
229683
+ }
229684
+ function isBooleanLiteral(v) {
229685
+ const lower = v.toLowerCase();
229686
+ return lower === "true" || lower === "false";
229687
+ }
229688
+ var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
229689
+ var ISO_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
229690
+ function isDateLiteral(v) {
229691
+ return ISO_DATE_RE.test(v) || ISO_TIMESTAMP_RE.test(v);
229692
+ }
229693
+ function malloyLiteral(v) {
229694
+ if (isBooleanLiteral(v)) {
229695
+ return v.toLowerCase();
229696
+ }
229697
+ if (isDateLiteral(v)) {
229698
+ return `@${v.slice(0, 10)}`;
229699
+ }
229700
+ return `'${escapeMalloyString(v)}'`;
229701
+ }
229702
+ function buildPredicate(filter2, value) {
229703
+ const dim = `\`${filter2.dimension}\``;
229704
+ switch (filter2.type) {
229705
+ case "equal": {
229706
+ const v = Array.isArray(value) ? value[0] : value;
229707
+ return `${dim} = ${malloyLiteral(v)}`;
229708
+ }
229709
+ case "in": {
229710
+ const values = Array.isArray(value) ? value : [value];
229711
+ if (values.length === 1) {
229712
+ return `${dim} = ${malloyLiteral(values[0])}`;
229713
+ }
229714
+ const conditions = values.map((v) => `${dim} = ${malloyLiteral(v)}`);
229715
+ return `(${conditions.join(" or ")})`;
229716
+ }
229717
+ case "like": {
229718
+ const v = Array.isArray(value) ? value[0] : value;
229719
+ const escaped = escapeMalloyString(v.toLowerCase());
229720
+ const pattern = escaped.startsWith("%") || escaped.endsWith("%") ? escaped : `%${escaped}%`;
229721
+ return `lower(${dim}) ~ '${pattern}'`;
229722
+ }
229723
+ case "greater_than": {
229724
+ const v = Array.isArray(value) ? value[0] : value;
229725
+ return `${dim} > ${malloyLiteral(v)}`;
229726
+ }
229727
+ case "less_than": {
229728
+ const v = Array.isArray(value) ? value[0] : value;
229729
+ return `${dim} < ${malloyLiteral(v)}`;
229730
+ }
229731
+ }
229732
+ }
229733
+ function buildFilterClause(filters, params) {
229734
+ const predicates2 = [];
229735
+ for (const filter2 of filters) {
229736
+ const value = params[filter2.name];
229737
+ const hasValue = value !== undefined && value !== null && (Array.isArray(value) ? value.length > 0 : value !== "");
229738
+ if (!hasValue) {
229739
+ if (filter2.required) {
229740
+ throw new FilterValidationError(`Required filter "${filter2.name}" (dimension: ${filter2.dimension}) was not provided`);
229741
+ }
229742
+ continue;
229743
+ }
229744
+ predicates2.push(buildPredicate(filter2, value));
229745
+ }
229746
+ if (predicates2.length === 0) {
229747
+ return "";
229748
+ }
229749
+ return predicates2.join(" and ");
229750
+ }
229751
+ function injectFilterRefinement(query, filterClause) {
229752
+ if (!filterClause) {
229753
+ return query;
229754
+ }
229755
+ return `${query.trimEnd()} + {where: ${filterClause}}`;
229756
+ }
229757
+
229758
+ class FilterValidationError extends Error {
229759
+ constructor(message) {
229760
+ super(message);
229761
+ this.name = "FilterValidationError";
229762
+ }
229763
+ }
229764
+ function tokenize(input) {
229765
+ const tokens = [];
229766
+ let current = "";
229767
+ let inQuote = false;
229768
+ let quoteChar = "";
229769
+ for (const ch of input) {
229770
+ if (inQuote) {
229771
+ if (ch === quoteChar) {
229772
+ inQuote = false;
229773
+ } else {
229774
+ current += ch;
229775
+ }
229776
+ } else if (ch === '"' || ch === "'") {
229777
+ inQuote = true;
229778
+ quoteChar = ch;
229779
+ } else if (ch === " " || ch === "\t") {
229780
+ if (current) {
229781
+ tokens.push(current);
229782
+ current = "";
229783
+ }
229784
+ } else {
229785
+ current += ch;
229786
+ }
229787
+ }
229788
+ if (current) {
229789
+ tokens.push(current);
229790
+ }
229791
+ return tokens;
229792
+ }
229793
+
229604
229794
  // src/service/model.ts
229605
229795
  var MALLOY_VERSION = import__package.default.version;
229606
229796
 
@@ -229617,12 +229807,13 @@ class Model {
229617
229807
  sourceInfos;
229618
229808
  runnableNotebookCells;
229619
229809
  compilationError;
229810
+ filterMap;
229620
229811
  meter = import_api2.metrics.getMeter("publisher");
229621
229812
  queryExecutionHistogram = this.meter.createHistogram("malloy_model_query_duration", {
229622
229813
  description: "How long it takes to execute a Malloy model query",
229623
229814
  unit: "ms"
229624
229815
  });
229625
- constructor(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, compilationError) {
229816
+ constructor(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos, runnableNotebookCells, compilationError, filterMap) {
229626
229817
  this.packageName = packageName;
229627
229818
  this.modelPath = modelPath;
229628
229819
  this.dataStyles = dataStyles;
@@ -229634,8 +229825,19 @@ class Model {
229634
229825
  this.sourceInfos = sourceInfos;
229635
229826
  this.runnableNotebookCells = runnableNotebookCells;
229636
229827
  this.compilationError = compilationError;
229828
+ this.filterMap = filterMap ?? new Map;
229637
229829
  this.modelInfo = this.modelDef ? import_malloy2.modelDefToModelInfo(this.modelDef) : undefined;
229638
229830
  }
229831
+ getFilters(sourceName) {
229832
+ return this.filterMap.get(sourceName) ?? [];
229833
+ }
229834
+ extractSourceName(query) {
229835
+ if (!query)
229836
+ return;
229837
+ const runMatch = query.match(/run\s*:\s*(\w+)\s*->/);
229838
+ const arrowMatch = query.match(/^\s*(\w+)\s*->/m);
229839
+ return runMatch?.[1] ?? arrowMatch?.[1];
229840
+ }
229639
229841
  static async create(packageName, packagePath, modelPath, connections) {
229640
229842
  const { runtime, modelURL, importBaseURL, dataStyles, modelType } = await Model.getModelRuntime(packagePath, modelPath, connections);
229641
229843
  try {
@@ -229643,10 +229845,13 @@ class Model {
229643
229845
  let modelDef = undefined;
229644
229846
  let sources = undefined;
229645
229847
  let queries = undefined;
229848
+ let filterMap;
229646
229849
  const sourceInfos = [];
229647
229850
  if (modelMaterializer) {
229648
229851
  modelDef = (await modelMaterializer.getModel())._modelDef;
229649
- sources = Model.getSources(modelPath, modelDef);
229852
+ const sourceResult = Model.getSources(modelPath, modelDef);
229853
+ sources = sourceResult.sources;
229854
+ filterMap = sourceResult.filterMap;
229650
229855
  queries = Model.getQueries(modelPath, modelDef);
229651
229856
  const imports = modelDef.imports || [];
229652
229857
  const importedSourceNames = new Set;
@@ -229677,7 +229882,7 @@ class Model {
229677
229882
  }
229678
229883
  }
229679
229884
  }
229680
- return new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos.length > 0 ? sourceInfos : undefined, runnableNotebookCells, undefined);
229885
+ return new Model(packageName, modelPath, dataStyles, modelType, modelMaterializer, modelDef, sources, queries, sourceInfos.length > 0 ? sourceInfos : undefined, runnableNotebookCells, undefined, filterMap);
229681
229886
  } catch (error) {
229682
229887
  let computedError = error;
229683
229888
  if (error instanceof Error && error.stack) {
@@ -229731,7 +229936,7 @@ class Model {
229731
229936
  throw new ModelNotFoundError(`${this.modelPath} is not a valid notebook name. Notebook files must end in .malloynb.`);
229732
229937
  }
229733
229938
  }
229734
- async getQueryResults(sourceName, queryName, query) {
229939
+ async getQueryResults(sourceName, queryName, query, filterParams, bypassFilters) {
229735
229940
  const startTime = performance.now();
229736
229941
  if (this.compilationError) {
229737
229942
  if (this.compilationError instanceof import_malloy2.MalloyError || this.compilationError instanceof ModelCompilationError) {
@@ -229743,12 +229948,13 @@ class Model {
229743
229948
  if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
229744
229949
  throw new BadRequestError("Model has no queryable entities.");
229745
229950
  try {
229951
+ let queryString;
229746
229952
  if (!sourceName && !queryName && query) {
229747
- runnable = this.modelMaterializer.loadQuery(`
229748
- ` + query);
229953
+ queryString = `
229954
+ ` + query;
229749
229955
  } else if (queryName && !query) {
229750
- runnable = this.modelMaterializer.loadQuery(`
229751
- run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
229956
+ queryString = `
229957
+ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
229752
229958
  } else {
229753
229959
  const endTime2 = performance.now();
229754
229960
  const executionTime2 = endTime2 - startTime;
@@ -229761,10 +229967,24 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
229761
229967
  });
229762
229968
  throw new BadRequestError("Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.");
229763
229969
  }
229970
+ if (!bypassFilters) {
229971
+ const effectiveSource = sourceName ?? this.extractSourceName(query);
229972
+ if (effectiveSource) {
229973
+ const filters = this.getFilters(effectiveSource);
229974
+ if (filters.length > 0) {
229975
+ const filterClause = buildFilterClause(filters, filterParams ?? {});
229976
+ queryString = injectFilterRefinement(queryString, filterClause);
229977
+ }
229978
+ }
229979
+ }
229980
+ runnable = this.modelMaterializer.loadQuery(queryString);
229764
229981
  } catch (error) {
229765
229982
  if (error instanceof BadRequestError) {
229766
229983
  throw error;
229767
229984
  }
229985
+ if (error instanceof FilterValidationError) {
229986
+ throw new BadRequestError(error.message);
229987
+ }
229768
229988
  if (error instanceof import_malloy2.MalloyError) {
229769
229989
  throw error;
229770
229990
  }
@@ -229874,7 +230094,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
229874
230094
  notebookCells
229875
230095
  };
229876
230096
  }
229877
- async executeNotebookCell(cellIndex) {
230097
+ async executeNotebookCell(cellIndex, filterParams, bypassFilters) {
229878
230098
  if (this.compilationError) {
229879
230099
  throw this.compilationError;
229880
230100
  }
@@ -229895,12 +230115,29 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
229895
230115
  let queryResult = undefined;
229896
230116
  if (cell.runnable) {
229897
230117
  try {
229898
- const rowLimit = (await cell.runnable.getPreparedResult()).resultExplore.limit || ROW_LIMIT;
229899
- const result = await cell.runnable.run({ rowLimit });
229900
- const query = (await cell.runnable.getPreparedQuery())._query;
230118
+ let runnableToExecute = cell.runnable;
230119
+ if (!bypassFilters && cell.modelMaterializer) {
230120
+ const effectiveSource = this.extractSourceName(cell.text);
230121
+ if (effectiveSource) {
230122
+ const filters = this.getFilters(effectiveSource);
230123
+ if (filters.length > 0) {
230124
+ const filterClause = buildFilterClause(filters, filterParams ?? {});
230125
+ if (filterClause) {
230126
+ const refinedQuery = injectFilterRefinement(cell.text, filterClause);
230127
+ runnableToExecute = cell.modelMaterializer.loadQuery(refinedQuery);
230128
+ }
230129
+ }
230130
+ }
230131
+ }
230132
+ const rowLimit = (await runnableToExecute.getPreparedResult()).resultExplore.limit || ROW_LIMIT;
230133
+ const result = await runnableToExecute.run({ rowLimit });
230134
+ const query = (await runnableToExecute.getPreparedQuery())._query;
229901
230135
  queryName = query.as || query.name;
229902
230136
  queryResult = result?._queryResult && this.modelInfo && JSON.stringify(import_malloy2.API.util.wrapResult(result));
229903
230137
  } catch (error) {
230138
+ if (error instanceof FilterValidationError) {
230139
+ throw new BadRequestError(error.message);
230140
+ }
229904
230141
  if (error instanceof import_malloy2.MalloyError) {
229905
230142
  throw error;
229906
230143
  }
@@ -229966,14 +230203,46 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
229966
230203
  }));
229967
230204
  }
229968
230205
  static getSources(modelPath, modelDef) {
229969
- return Object.values(modelDef.contents).filter((obj) => import_malloy2.isSourceDef(obj)).map((sourceObj) => ({
229970
- name: sourceObj.as || sourceObj.name,
229971
- annotations: sourceObj.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text),
229972
- views: sourceObj.fields.filter((turtleObj) => turtleObj.type === "turtle").filter((turtleObj) => turtleObj.pipeline.map((stage) => stage.type).every((type) => type == "reduce")).map((turtleObj) => ({
230206
+ const filterMap = new Map;
230207
+ const sources = Object.values(modelDef.contents).filter((obj) => import_malloy2.isSourceDef(obj)).map((sourceObj) => {
230208
+ const sourceName = sourceObj.as || sourceObj.name;
230209
+ const annotations = sourceObj.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text);
230210
+ const allAnnotations = sourceObj.annotation?.blockNotes?.map((note) => note.text);
230211
+ let filters;
230212
+ if (allAnnotations && allAnnotations.length > 0) {
230213
+ try {
230214
+ const parsed = parseFilters(allAnnotations);
230215
+ if (parsed.length > 0) {
230216
+ filterMap.set(sourceName, parsed);
230217
+ const structFields = sourceObj.fields;
230218
+ filters = parsed.map((f) => {
230219
+ const field = structFields.find((fd) => (fd.as || fd.name) === f.dimension);
230220
+ return {
230221
+ name: f.name,
230222
+ dimension: f.dimension,
230223
+ type: f.type,
230224
+ implicit: f.implicit,
230225
+ required: f.required,
230226
+ dimensionType: field?.type
230227
+ };
230228
+ });
230229
+ }
230230
+ } catch (err) {
230231
+ logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err });
230232
+ }
230233
+ }
230234
+ const views = sourceObj.fields.filter((turtleObj) => turtleObj.type === "turtle").filter((turtleObj) => turtleObj.pipeline.map((stage) => stage.type).every((type) => type == "reduce")).map((turtleObj) => ({
229973
230235
  name: turtleObj.as || turtleObj.name,
229974
230236
  annotations: turtleObj?.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text)
229975
- }))
229976
- }));
230237
+ }));
230238
+ return {
230239
+ name: sourceName,
230240
+ annotations,
230241
+ views,
230242
+ filters
230243
+ };
230244
+ });
230245
+ return { sources, filterMap };
229977
230246
  }
229978
230247
  static async getModelMaterializer(runtime, importBaseURL, modelURL, modelPath) {
229979
230248
  if (modelPath.endsWith(MODEL_FILE_SUFFIX)) {
@@ -230072,6 +230341,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`);
230072
230341
  type: "code",
230073
230342
  text: stmt.text,
230074
230343
  runnable,
230344
+ modelMaterializer: localMM,
230075
230345
  newSources,
230076
230346
  queryInfo
230077
230347
  };
@@ -234789,9 +235059,14 @@ function registerModelResource(mcpServer, projectStore) {
234789
235059
  throw new ModelNotFoundError(modelPath);
234790
235060
  }
234791
235061
  const compiledModelDefinition = await modelInstance.getModel();
234792
- return {
234793
- ...compiledModelDefinition
234794
- };
235062
+ if (compiledModelDefinition.sources) {
235063
+ for (const source of compiledModelDefinition.sources) {
235064
+ if (source.filters) {
235065
+ source.filters = source.filters.filter((f) => !f.implicit);
235066
+ }
235067
+ }
235068
+ }
235069
+ return compiledModelDefinition;
234795
235070
  } catch (error) {
234796
235071
  let errorDetails;
234797
235072
  const safeProjectName = typeof projectName === "string" ? projectName : "unknown";
@@ -235190,6 +235465,9 @@ function registerSourceResource(mcpServer, projectStore) {
235190
235465
  const errorDetails = getNotFoundError(`Source '${sourceName}' in model '${modelPath}' package '${packageName}' project '${projectName}'`);
235191
235466
  throw new McpGetResourceError(errorDetails);
235192
235467
  }
235468
+ if (source.filters) {
235469
+ source.filters = source.filters.filter((f) => !f.implicit);
235470
+ }
235193
235471
  return source;
235194
235472
  } catch (error) {
235195
235473
  if (error instanceof McpGetResourceError) {
@@ -235422,7 +235700,8 @@ var executeQueryShape = {
235422
235700
  modelPath: exports_external.string().describe("Path to the .malloy model file"),
235423
235701
  query: exports_external.string().optional().describe("Ad-hoc Malloy query code"),
235424
235702
  sourceName: exports_external.string().optional().describe("Source name for a view"),
235425
- queryName: exports_external.string().optional().describe("Named query or view")
235703
+ queryName: exports_external.string().optional().describe("Named query or view"),
235704
+ filterParams: exports_external.record(exports_external.union([exports_external.string(), exports_external.array(exports_external.string())])).optional().describe("Filter parameter values keyed by filter name. Used with sources that declare #(filter) annotations.")
235426
235705
  };
235427
235706
  function registerExecuteQueryTool(mcpServer, projectStore) {
235428
235707
  mcpServer.tool("malloy_executeQuery", "Executes a Malloy query (either ad-hoc or a named query/view defined in a model) against the specified model and returns the results as JSON.", executeQueryShape, async (params) => {
@@ -235432,7 +235711,8 @@ function registerExecuteQueryTool(mcpServer, projectStore) {
235432
235711
  modelPath,
235433
235712
  query,
235434
235713
  sourceName,
235435
- queryName
235714
+ queryName,
235715
+ filterParams
235436
235716
  } = params;
235437
235717
  logger.info("[MCP Tool executeQuery] Received params:", { params });
235438
235718
  const hasAdhocQuery = !!query;
@@ -235468,7 +235748,7 @@ function registerExecuteQueryTool(mcpServer, projectStore) {
235468
235748
  logger.info(`[MCP Tool executeQuery] Model found. Proceeding to execute query.`);
235469
235749
  try {
235470
235750
  if (query) {
235471
- const { result } = await model.getQueryResults(undefined, undefined, query);
235751
+ const { result } = await model.getQueryResults(undefined, undefined, query, filterParams);
235472
235752
  const { validateRenderTags: validateRenderTags2 } = await Promise.resolve().then(() => __toESM(require_dist10()));
235473
235753
  const renderLogs = validateRenderTags2(result);
235474
235754
  const baseUriComponents = {
@@ -235504,7 +235784,7 @@ ${JSON.stringify(renderLogs, null, 2)}`
235504
235784
  }
235505
235785
  return { isError: false, content };
235506
235786
  } else if (queryName) {
235507
- const { result } = await model.getQueryResults(sourceName, queryName, undefined);
235787
+ const { result } = await model.getQueryResults(sourceName, queryName, undefined, filterParams);
235508
235788
  const { validateRenderTags: validateRenderTags2 } = await Promise.resolve().then(() => __toESM(require_dist10()));
235509
235789
  const renderLogs = validateRenderTags2(result);
235510
235790
  const baseUriComponents = {
@@ -236085,7 +236365,19 @@ app.get(`${API_PREFIX2}/projects/:projectName/packages/:packageName/notebooks/*/
236085
236365
  return;
236086
236366
  }
236087
236367
  const notebookPath = req.params["0"];
236088
- res.status(200).json(await modelController.executeNotebookCell(req.params.projectName, req.params.packageName, notebookPath, cellIndex));
236368
+ let filterParams;
236369
+ if (typeof req.query.filter_params === "string") {
236370
+ try {
236371
+ filterParams = JSON.parse(req.query.filter_params);
236372
+ } catch {
236373
+ res.status(400).json({
236374
+ error: "Invalid filter_params: must be valid JSON"
236375
+ });
236376
+ return;
236377
+ }
236378
+ }
236379
+ const bypassFilters = req.query.bypass_filters === "true" ? true : undefined;
236380
+ res.status(200).json(await modelController.executeNotebookCell(req.params.projectName, req.params.packageName, notebookPath, cellIndex, filterParams, bypassFilters));
236089
236381
  } catch (error) {
236090
236382
  logger.error(error);
236091
236383
  const { json: json2, status } = internalErrorToHttpError(error);
@@ -236113,7 +236405,7 @@ app.post(`${API_PREFIX2}/projects/:projectName/packages/:packageName/models/*?/q
236113
236405
  }
236114
236406
  try {
236115
236407
  const modelPath = req.params["0"];
236116
- res.status(200).json(await queryController.getQuery(req.params.projectName, req.params.packageName, modelPath, req.body.sourceName, req.body.queryName, req.body.query, req.body.compactJson === true));
236408
+ res.status(200).json(await queryController.getQuery(req.params.projectName, req.params.packageName, modelPath, req.body.sourceName, req.body.queryName, req.body.query, req.body.compactJson === true, req.body.filterParams ?? req.body.sourceFilters, req.body.bypassFilters === true ? true : undefined));
236117
236409
  } catch (error) {
236118
236410
  logger.error(error);
236119
236411
  const { json: json2, status } = internalErrorToHttpError(error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/server",
3
3
  "description": "Malloy Publisher Server",
4
- "version": "0.0.180",
4
+ "version": "0.0.182",
5
5
  "main": "dist/server.js",
6
6
  "bin": {
7
7
  "malloy-publisher": "dist/server.js"
@@ -32,15 +32,15 @@
32
32
  "@azure/identity": "^4.13.0",
33
33
  "@azure/storage-blob": "^12.26.0",
34
34
  "@google-cloud/storage": "^7.16.0",
35
- "@malloydata/db-bigquery": "^0.0.370",
36
- "@malloydata/db-duckdb": "^0.0.370",
37
- "@malloydata/db-mysql": "^0.0.370",
38
- "@malloydata/db-postgres": "^0.0.370",
39
- "@malloydata/db-snowflake": "^0.0.370",
40
- "@malloydata/db-trino": "^0.0.370",
41
- "@malloydata/malloy": "^0.0.370",
42
- "@malloydata/malloy-sql": "^0.0.370",
43
- "@malloydata/render-validator": "^0.0.370",
35
+ "@malloydata/db-bigquery": "^0.0.374",
36
+ "@malloydata/db-duckdb": "^0.0.374",
37
+ "@malloydata/db-mysql": "^0.0.374",
38
+ "@malloydata/db-postgres": "^0.0.374",
39
+ "@malloydata/db-snowflake": "^0.0.374",
40
+ "@malloydata/db-trino": "^0.0.374",
41
+ "@malloydata/malloy": "^0.0.374",
42
+ "@malloydata/malloy-sql": "^0.0.374",
43
+ "@malloydata/render-validator": "^0.0.374",
44
44
  "@modelcontextprotocol/sdk": "^1.13.2",
45
45
  "@opentelemetry/api": "^1.9.0",
46
46
  "@opentelemetry/auto-instrumentations-node": "^0.57.0",
@@ -1,6 +1,7 @@
1
1
  import { components } from "../api";
2
2
  import { ModelNotFoundError } from "../errors";
3
3
  import { ProjectStore } from "../service/project_store";
4
+ import type { FilterParams } from "../service/filter";
4
5
 
5
6
  type ApiNotebook = components["schemas"]["Notebook"];
6
7
  type ApiModel = components["schemas"]["Model"];
@@ -84,6 +85,8 @@ export class ModelController {
84
85
  packageName: string,
85
86
  notebookPath: string,
86
87
  cellIndex: number,
88
+ filterParams?: FilterParams,
89
+ bypassFilters?: boolean,
87
90
  ): Promise<{
88
91
  type: "code" | "markdown";
89
92
  text: string;
@@ -101,6 +104,6 @@ export class ModelController {
101
104
  throw new ModelNotFoundError(`${notebookPath} is a model`);
102
105
  }
103
106
 
104
- return model.executeNotebookCell(cellIndex);
107
+ return model.executeNotebookCell(cellIndex, filterParams, bypassFilters);
105
108
  }
106
109
  }
@@ -3,6 +3,7 @@ import { components } from "../api";
3
3
  import { API_PREFIX } from "../constants";
4
4
  import { ModelNotFoundError } from "../errors";
5
5
  import { ProjectStore } from "../service/project_store";
6
+ import type { FilterParams } from "../service/filter";
6
7
 
7
8
  type ApiQuery = components["schemas"]["QueryResult"];
8
9
 
@@ -29,6 +30,8 @@ export class QueryController {
29
30
  queryName: string,
30
31
  query: string,
31
32
  compactJson: boolean = false,
33
+ filterParams?: FilterParams,
34
+ bypassFilters?: boolean,
32
35
  ): Promise<ApiQuery> {
33
36
  const project = await this.projectStore.getProject(projectName, false);
34
37
  const p = await project.getPackage(packageName, false);
@@ -41,6 +44,8 @@ export class QueryController {
41
44
  sourceName,
42
45
  queryName,
43
46
  query,
47
+ filterParams,
48
+ bypassFilters,
44
49
  );
45
50
  const renderLogs = validateRenderTags(result);
46
51
  return {
@@ -82,15 +82,18 @@ export function registerModelResource(
82
82
  const compiledModelDefinition: components["schemas"]["CompiledModel"] =
83
83
  await modelInstance.getModel();
84
84
 
85
- // Return the definition, potentially adding identifiers back if needed
86
- // (Check if compiledModelDefinition already includes them)
87
- return {
88
- ...compiledModelDefinition,
89
- // Ensure these are present if not already in compiledModelDefinition
90
- // path: modelPath,
91
- // packageName: packageName,
92
- // projectName: projectName
93
- };
85
+ // Strip implicit filters from agent-facing responses
86
+ if (compiledModelDefinition.sources) {
87
+ for (const source of compiledModelDefinition.sources) {
88
+ if (source.filters) {
89
+ source.filters = source.filters.filter(
90
+ (f) => !f.implicit,
91
+ );
92
+ }
93
+ }
94
+ }
95
+
96
+ return compiledModelDefinition;
94
97
  } catch (error) {
95
98
  let errorDetails;
96
99
  // Provide specific context for error messages