From 79337de1c73796874259e1e519bbbf89dad6dc14 Mon Sep 17 00:00:00 2001 From: Ariya Hidayat <ariya@metabase.com> Date: Fri, 24 Jun 2022 07:47:54 -0700 Subject: [PATCH] Refactor and tests template tags extractions (#23524) --- .../metabase-lib/lib/queries/NativeQuery.ts | 42 +++++++++---------- .../metabase-lib/lib/queries/TemplateTag.ts | 12 ++++++ .../lib/queries/TemplateTag.unit.spec.ts | 11 +++++ .../lib/queries/NativeQuery.unit.spec.js | 24 ++++++++++- 4 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 frontend/src/metabase-lib/lib/queries/TemplateTag.ts create mode 100644 frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts index c48208aded3..6e6c7d73d7f 100644 --- a/frontend/src/metabase-lib/lib/queries/NativeQuery.ts +++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.ts @@ -25,6 +25,7 @@ import { DatabaseEngine, DatabaseId } from "metabase-types/types/Database"; import AtomicQuery from "metabase-lib/lib/queries/AtomicQuery"; import Dimension, { TemplateTagDimension, FieldDimension } from "../Dimension"; import Variable, { TemplateTagVariable } from "../Variable"; +import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag"; import DimensionOptions from "../DimensionOptions"; import { ValidationError } from "metabase-lib/lib/ValidationError"; @@ -422,21 +423,7 @@ export default class NativeQuery extends AtomicQuery { */ _getUpdatedTemplateTags(queryText: string): TemplateTags { if (queryText && this.supportsNativeParameters()) { - let tags = []; - // look for variable usage in the query (like '{{varname}}'). we only allow alphanumeric characters for the variable name - // a variable name can optionally end with :start or :end which is not considered part of the actual variable name - // expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}} - // anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored - // variables referencing other questions, by their card ID, are also supported: {{#123}} references question with ID 123 - let match; - const re = /\{\{\s*((snippet:\s*[^}]+)|[A-Za-z0-9_\.]+?|#[0-9]*)\s*\}\}/g; - - while ((match = re.exec(queryText)) != null) { - tags.push(match[1]); - } - - // eliminate any duplicates since it's allowed for a user to reference the same variable multiple times - tags = _.uniq(tags); + const tags = recognizeTemplateTags(queryText); const existingTemplateTags = this.templateTagsMap(); const existingTags = Object.keys(existingTemplateTags); @@ -476,12 +463,7 @@ export default class NativeQuery extends AtomicQuery { // create new vars for (const tagName of newTags) { - templateTags[tagName] = { - id: Utils.uuid(), - name: tagName, - "display-name": humanize(tagName), - type: "text", - }; + templateTags[tagName] = createTemplateTag(tagName); // parse card ID from tag name for card query template tags if (isCardQueryName(tagName)) { @@ -533,3 +515,21 @@ export default class NativeQuery extends AtomicQuery { }); } } + +// look for variable usage in the query (like '{{varname}}'). we only allow alphanumeric characters for the variable name +// a variable name can optionally end with :start or :end which is not considered part of the actual variable name +// expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}} +// anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored +// variables referencing other questions, by their card ID, are also supported: {{#123}} references question with ID 123 +export function recognizeTemplateTags(queryText: string): string[] { + const tagNames = []; + let match; + const re = /\{\{\s*((snippet:\s*[^}]+)|[A-Za-z0-9_\.]+?|#[0-9]*)\s*\}\}/g; + + while ((match = re.exec(queryText)) != null) { + tagNames.push(match[1]); + } + + // eliminate any duplicates since it's allowed for a user to reference the same variable multiple times + return _.uniq(tagNames); +} diff --git a/frontend/src/metabase-lib/lib/queries/TemplateTag.ts b/frontend/src/metabase-lib/lib/queries/TemplateTag.ts new file mode 100644 index 00000000000..eae0aeeecc7 --- /dev/null +++ b/frontend/src/metabase-lib/lib/queries/TemplateTag.ts @@ -0,0 +1,12 @@ +import Utils from "metabase/lib/utils"; +import { humanize } from "metabase/lib/formatting"; +import { TemplateTag } from "metabase-types/types/Query"; + +export const createTemplateTag = (tagName: string): TemplateTag => { + return { + id: Utils.uuid(), + name: tagName, + "display-name": humanize(tagName), + type: "text", + }; +}; diff --git a/frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts b/frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts new file mode 100644 index 00000000000..b516139fc6f --- /dev/null +++ b/frontend/src/metabase-lib/lib/queries/TemplateTag.unit.spec.ts @@ -0,0 +1,11 @@ +import { createTemplateTag } from "metabase-lib/lib/queries/TemplateTag"; + +describe("createTemplateTag", () => { + it("should create a proper template tag", () => { + const tag = createTemplateTag("stars"); + expect(tag.name).toEqual("stars"); + expect(tag.type).toEqual("text"); + expect(typeof tag.id).toEqual("string"); + expect(tag["display-name"]).toEqual("Stars"); + }); +}); diff --git a/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js b/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js index 3ed07478ba6..187470ef19e 100644 --- a/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js +++ b/frontend/test/metabase-lib/lib/queries/NativeQuery.unit.spec.js @@ -6,7 +6,9 @@ import { MONGO_DATABASE, } from "__support__/sample_database_fixture"; -import NativeQuery from "metabase-lib/lib/queries/NativeQuery"; +import NativeQuery, { + recognizeTemplateTags, +} from "metabase-lib/lib/queries/NativeQuery"; function makeDatasetQuery(queryText, templateTags, databaseId) { return { @@ -339,4 +341,24 @@ describe("NativeQuery", () => { ]); }); }); + + describe("recognizeTemplateTags", () => { + it("should handle standard variable names", () => { + expect(recognizeTemplateTags("SELECT * from {{products}}")).toEqual([ + "products", + ]); + }); + + it("should allow duplicated variables", () => { + expect( + recognizeTemplateTags("SELECT {{col}} FROM {{t}} ORDER BY {{col}} "), + ).toEqual(["col", "t"]); + }); + + it("should ignore non-alphanumeric markers", () => { + expect(recognizeTemplateTags("SELECT * from X -- {{&universe}}")).toEqual( + [], + ); + }); + }); }); -- GitLab