@malloy-publisher/server 0.0.181 → 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.
- package/dist/app/api-doc.yaml +91 -1
- package/dist/app/assets/{HomePage-B0C6gwGj.js → HomePage-or6BbD5P.js} +1 -1
- package/dist/app/assets/{MainPage-B53xidTF.js → MainPage-DINuSDg0.js} +2 -2
- package/dist/app/assets/{ModelPage-UMuQe8qY.js → ModelPage-BMcaV1YQ.js} +1 -1
- package/dist/app/assets/{PackagePage-BEDvm_je.js → PackagePage-DXxlQcCj.js} +1 -1
- package/dist/app/assets/{ProjectPage-DzN4P86H.js → ProjectPage-vfZc_Kvu.js} +1 -1
- package/dist/app/assets/{RouteError-Cv58zNpb.js → RouteError-r14osUo0.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DZ1StqsX.js → WorkbookPage-HI39NTWs.js} +1 -1
- package/dist/app/assets/{index-D-xPyBUA.js → index-Bw1lh09G.js} +78 -78
- package/dist/app/assets/{index-DPThhVfX.js → index-Dd6uCk_C.js} +1 -1
- package/dist/app/assets/{index-M3Zo817E.js → index-JqHhhRqY.js} +1 -1
- package/dist/app/assets/{index.umd-DnfBsVqO.js → index.umd-lwkX_kFe.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/server.js +323 -31
- package/package.json +1 -1
- package/src/controller/model.controller.ts +4 -1
- package/src/controller/query.controller.ts +5 -0
- package/src/mcp/resources/model_resource.ts +12 -9
- package/src/mcp/resources/source_resource.ts +7 -6
- package/src/mcp/resources/view_resource.ts +0 -1
- package/src/mcp/tools/execute_query_tool.ts +9 -0
- package/src/server.ts +21 -0
- package/src/service/filter.spec.ts +392 -0
- package/src/service/filter.ts +332 -0
- package/src/service/filter_integration.spec.ts +622 -0
- package/src/service/model.ts +180 -43
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
|
-
|
|
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
|
-
|
|
229748
|
-
` + query
|
|
229953
|
+
queryString = `
|
|
229954
|
+
` + query;
|
|
229749
229955
|
} else if (queryName && !query) {
|
|
229750
|
-
|
|
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
|
-
|
|
229899
|
-
|
|
229900
|
-
|
|
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
|
-
|
|
229970
|
-
|
|
229971
|
-
|
|
229972
|
-
|
|
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
|
-
|
|
234793
|
-
|
|
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
|
-
|
|
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,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
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
McpServer,
|
|
3
3
|
ResourceTemplate,
|
|
4
4
|
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { URL } from "url";
|
|
6
|
-
import type { components } from "../../api";
|
|
5
|
+
import { URL } from "url";
|
|
6
|
+
import type { components } from "../../api";
|
|
7
7
|
import { ModelCompilationError } from "../../errors";
|
|
8
8
|
import { logger } from "../../logger";
|
|
9
9
|
import { ProjectStore } from "../../service/project_store";
|
|
@@ -89,22 +89,23 @@ export function registerSourceResource(
|
|
|
89
89
|
if (!sources) {
|
|
90
90
|
throw new Error("Could not retrieve sources from model.");
|
|
91
91
|
}
|
|
92
|
-
// Add type annotation for 's'
|
|
93
92
|
const source = sources.find(
|
|
94
|
-
// @ts-expect-error TODO: Fix missing Source type in API
|
|
95
93
|
(s: components["schemas"]["Source"]) =>
|
|
96
94
|
s.name === sourceName,
|
|
97
95
|
);
|
|
98
96
|
|
|
99
97
|
if (!source) {
|
|
100
|
-
// Specific "Source not found" error
|
|
101
98
|
const errorDetails = getNotFoundError(
|
|
102
99
|
`Source '${sourceName}' in model '${modelPath}' package '${packageName}' project '${projectName}'`,
|
|
103
100
|
);
|
|
104
101
|
throw new McpGetResourceError(errorDetails);
|
|
105
102
|
}
|
|
106
103
|
|
|
107
|
-
//
|
|
104
|
+
// Strip implicit filters from agent-facing responses
|
|
105
|
+
if (source.filters) {
|
|
106
|
+
source.filters = source.filters.filter((f) => !f.implicit);
|
|
107
|
+
}
|
|
108
|
+
|
|
108
109
|
return source;
|
|
109
110
|
} catch (error) {
|
|
110
111
|
// Catch errors from getModelForQuery or finding the source
|
|
@@ -83,7 +83,6 @@ export function registerViewResource(
|
|
|
83
83
|
throw new Error("Could not retrieve sources from model.");
|
|
84
84
|
}
|
|
85
85
|
const source = sources.find(
|
|
86
|
-
// @ts-expect-error TODO: Fix missing Source type in API
|
|
87
86
|
(s: components["schemas"]["Source"]) =>
|
|
88
87
|
s.name === sourceName,
|
|
89
88
|
);
|