Adding error checking to our handlebars syntax inputs as well as making all handlebars helpers available due to space pre-processor being removed.

This commit is contained in:
mike12345567 2021-01-22 17:57:38 +00:00
parent 12fa20de8d
commit d3d840e42a
10 changed files with 130 additions and 54 deletions

View File

@ -2,6 +2,7 @@ import { cloneDeep } from "lodash/fp"
import { get } from "svelte/store" import { get } from "svelte/store"
import { backendUiStore, store } from "builderStore" import { backendUiStore, store } from "builderStore"
import { findAllMatchingComponents, findComponentPath } from "./storeUtils" import { findAllMatchingComponents, findComponentPath } from "./storeUtils"
import { makePropSafe } from "@budibase/string-templates"
// Regex to match all instances of template strings // Regex to match all instances of template strings
const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g
@ -105,7 +106,7 @@ export const getContextBindings = (rootComponent, componentId) => {
contextBindings.push({ contextBindings.push({
type: "context", type: "context",
runtimeBinding: `${component._id}.${runtimeBoundKey}`, runtimeBinding: `${makePropSafe(component._id)}.${makePropSafe(runtimeBoundKey)}`,
readableBinding: `${component._instanceName}.${table.name}.${key}`, readableBinding: `${component._instanceName}.${table.name}.${key}`,
fieldSchema, fieldSchema,
providerId: component._id, providerId: component._id,
@ -135,7 +136,7 @@ export const getComponentBindings = rootComponent => {
return { return {
type: "instance", type: "instance",
providerId: component._id, providerId: component._id,
runtimeBinding: `${component._id}`, runtimeBinding: `${makePropSafe(component._id)}`,
readableBinding: `${component._instanceName}`, readableBinding: `${component._instanceName}`,
} }
}) })

View File

@ -2,6 +2,7 @@ import sanitizeUrl from "./utils/sanitizeUrl"
import { rowListUrl } from "./rowListScreen" import { rowListUrl } from "./rowListScreen"
import { Screen } from "./utils/Screen" import { Screen } from "./utils/Screen"
import { Component } from "./utils/Component" import { Component } from "./utils/Component"
import { makePropSafe } from "@budibase/string-templates"
import { import {
makeMainContainer, makeMainContainer,
makeBreadcrumbContainer, makeBreadcrumbContainer,
@ -12,7 +13,7 @@ import {
export default function(tables) { export default function(tables) {
return tables.map(table => { return tables.map(table => {
const heading = table.primaryDisplay const heading = table.primaryDisplay
? `{{ data.${table.primaryDisplay} }}` ? `{{ data.${makePropSafe(table.primaryDisplay)} }}`
: null : null
return { return {
name: `${table.name} - Detail`, name: `${table.name} - Detail`,
@ -60,8 +61,8 @@ function generateTitleContainer(table, title, providerId) {
onClick: [ onClick: [
{ {
parameters: { parameters: {
rowId: `{{ ${providerId}._id }}`, rowId: `{{ ${makePropSafe(providerId)}._id }}`,
revId: `{{ ${providerId}._rev }}`, revId: `{{ ${makePropSafe(providerId)}._rev }}`,
tableId: table._id, tableId: table._id,
}, },
"##eventHandlerType": "Delete Row", "##eventHandlerType": "Delete Row",

View File

@ -10,6 +10,7 @@
Popover, Popover,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value = "" export let value = ""
@ -19,8 +20,10 @@
export let popover = null export let popover = null
let getCaretPosition let getCaretPosition
let validity = true
$: categories = Object.entries(groupBy("category", bindings)) $: categories = Object.entries(groupBy("category", bindings))
$: value && checkValid()
function onClickBinding(binding) { function onClickBinding(binding) {
const position = getCaretPosition() const position = getCaretPosition()
@ -34,6 +37,10 @@
value += toAdd value += toAdd
} }
} }
function checkValid() {
validity = isValid(value)
}
</script> </script>
<Popover {anchor} {align} bind:this={popover}> <Popover {anchor} {align} bind:this={popover}>
@ -70,11 +77,16 @@
bind:getCaretPosition bind:getCaretPosition
bind:value bind:value
placeholder="Add options from the left, type text, or do both" /> placeholder="Add options from the left, type text, or do both" />
{#if !validity}
<p class="syntax-error">Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a> for more details.
</p>
{/if}
<div class="controls"> <div class="controls">
<a href="https://docs.budibase.com/design/binding"> <a href="https://docs.budibase.com/design/binding">
<Body small>Learn more about binding</Body> <Body small>Learn more about binding</Body>
</a> </a>
<Button on:click={popover.hide} primary>Done</Button> <Button on:click={popover.hide} disabled={!validity} primary>Done</Button>
</div> </div>
</div> </div>
</div> </div>
@ -152,4 +164,14 @@
align-items: center; align-items: center;
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
} }
.syntax-error {
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
}
</style> </style>

View File

@ -41,7 +41,7 @@
.icon { .icon {
right: 2px; right: 2px;
top: 2px; top: 26px;
bottom: 2px; bottom: 2px;
position: absolute; position: absolute;
align-items: center; align-items: center;

View File

@ -2,12 +2,29 @@
import groupBy from "lodash/fp/groupBy" import groupBy from "lodash/fp/groupBy"
import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui" import { Button, TextArea, Drawer, Heading, Spacer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import {getBindableProperties, readableToRuntimeBinding} from "builderStore/dataBinding"
import { currentAsset, store } from "../../../builderStore"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let bindableProperties export let bindableProperties
export let value = "" export let value = ""
export let bindingDrawer export let bindingDrawer
let validity = true
$: value && checkValid()
$: bindableProperties = getBindableProperties(
$currentAsset.props,
$store.selectedComponentId
)
function checkValid() {
// TODO: need to convert the value to the runtime binding
const runtimeBinding = readableToRuntimeBinding(bindableProperties, value)
validity = isValid(runtimeBinding)
}
function addToText(readableBinding) { function addToText(readableBinding) {
value = `${value || ""}{{ ${readableBinding} }}` value = `${value || ""}{{ ${readableBinding} }}`
} }
@ -55,6 +72,11 @@
bind:value bind:value
placeholder="Add text, or click the objects on the left to add them to the placeholder="Add text, or click the objects on the left to add them to the
textbox." /> textbox." />
{#if !validity}
<p class="syntax-error">Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a> for more details.
</p>
{/if}
</div> </div>
</div> </div>
</div> </div>
@ -108,4 +130,15 @@
height: 40vh; height: 40vh;
overflow-y: auto; overflow-y: auto;
} }
.syntax-error {
padding-top: var(--spacing-m);
color: var(--red);
font-size: 12px;
}
.syntax-error a {
color: var(--red);
text-decoration: underline;
}
</style> </style>

View File

@ -87,6 +87,7 @@ module.exports.processStringSync = (string, context) => {
if (typeof string !== "string") { if (typeof string !== "string") {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
let template let template
string = processors.preprocess(string) string = processors.preprocess(string)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand // this does not throw an error when template can't be fulfilled, have to try correct beforehand
@ -95,11 +96,26 @@ module.exports.processStringSync = (string, context) => {
} }
/** /**
* Errors can occur if a user of this library attempts to use a helper that has not been added to the system, these errors * Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
* can be captured to alert the user of the mistake. * @param {string} property The property which is to be wrapped.
* @param {function} handler a function which will be called every time an error occurs when processing a handlebars * @returns {string} The wrapped property ready to be added to a templating string.
* statement.
*/ */
module.exports.errorEvents = handler => { module.exports.makePropSafe = property => {
hbsInstance.registerHelper("helperMissing", handler) return `[${property}]`.replace("[[", "[").replace("]]", "]")
}
/**
* Checks whether or not a template string contains totally valid syntax (simply tries running it)
* @param string The string to test for valid syntax - this may contain no templates and will be considered valid.
* @returns {boolean} Whether or not the input string is valid.
*/
module.exports.isValid = string => {
// don't really need a real context to check if its valid
const context = {}
try {
hbsInstance.compile(processors.preprocess(string, false))(context)
return true
} catch (err) {
return false
}
} }

View File

@ -17,8 +17,14 @@ function process(string, processors) {
return string return string
} }
module.exports.preprocess = string => { module.exports.preprocess = (string, finalise = true) => {
return process(string, preprocessor.processors) let processors = preprocessor.processors
// the pre-processor finalisation stops handlebars from ever throwing an error
// might want to pre-process for other benefits but still want to see errors
if (!finalise) {
processors = processors.filter(processor => processor.name !== preprocessor.PreprocessorNames.FINALISE)
}
return process(string, processors)
} }
module.exports.postprocess = string => { module.exports.postprocess = string => {

View File

@ -37,42 +37,7 @@ module.exports.processors = [
return statement return statement
}), }),
new Preprocessor(PreprocessorNames.HANDLE_SPACES, statement => { new Preprocessor(PreprocessorNames.FINALISE, statement => {
// exclude helpers and brackets, regex will only find double brackets
const exclusions = HelperNames()
// find all the parts split by spaces
const splitBySpaces = statement
.split(" ")
.filter(el => el !== "{{" && el !== "}}")
// remove braces if they are found and weren't spaced out
splitBySpaces[0] = splitBySpaces[0].replace("{", "")
splitBySpaces[splitBySpaces.length - 1] = splitBySpaces[
splitBySpaces.length - 1
].replace("}", "")
// remove the excluded elements
const propertyParts = splitBySpaces.filter(
part => exclusions.indexOf(part) === -1
)
// rebuild to get the full property
const fullProperty = propertyParts.join(" ")
// now work out the dot notation layers and split them up
const propertyLayers = fullProperty.split(".")
// find the layers which need to be wrapped and wrap them
for (let layer of propertyLayers) {
if (layer.indexOf(" ") !== -1) {
statement = swapStrings(
statement,
statement.indexOf(layer),
layer.length,
`[${layer}]`
)
}
}
// remove the edge case of double brackets being entered (in-case user already has specified)
return statement.replace(/\[\[/g, "[").replace(/]]/g, "]")
}),
new Preprocessor(Preprocessor.FINALISE, statement => {
let insideStatement = statement.slice(2, statement.length - 2) let insideStatement = statement.slice(2, statement.length - 2)
if (insideStatement.charAt(0) === " ") { if (insideStatement.charAt(0) === " ") {
insideStatement = insideStatement.slice(1) insideStatement = insideStatement.slice(1)
@ -87,3 +52,5 @@ module.exports.processors = [
return `{{ all ${insideStatement} }}` return `{{ all ${insideStatement} }}`
}), }),
] ]
module.exports.PreprocessorNames = PreprocessorNames

View File

@ -1,6 +1,8 @@
const { const {
processObject, processObject,
processString, processString,
isValid,
makePropSafe,
} = require("../src/index") } = require("../src/index")
describe("Test that the string processing works correctly", () => { describe("Test that the string processing works correctly", () => {
@ -82,3 +84,20 @@ describe("Test that the object processing works correctly", () => {
expect(error).not.toBeNull() expect(error).not.toBeNull()
}) })
}) })
describe("check the utility functions", () => {
it("should return false for an invalid template string", () => {
const valid = isValid("{{ table1.thing prop }}")
expect(valid).toBe(false)
})
it("should state this template is valid", () => {
const valid = isValid("{{ thing }}")
expect(valid).toBe(true)
})
it("should make a property safe", () => {
const property = makePropSafe("thing")
expect(property).toEqual("[thing]")
})
})

View File

@ -18,14 +18,14 @@ describe("Handling context properties with spaces in their name", () => {
}) })
it("should be able to handle a property with a space in its name", async () => { it("should be able to handle a property with a space in its name", async () => {
const output = await processString("hello my name is {{ person name }}", { const output = await processString("hello my name is {{ [person name] }}", {
"person name": "Mike", "person name": "Mike",
}) })
expect(output).toBe("hello my name is Mike") expect(output).toBe("hello my name is Mike")
}) })
it("should be able to handle an object with layers that requires escaping", async () => { it("should be able to handle an object with layers that requires escaping", async () => {
const output = await processString("testcase {{ testing.test case }}", { const output = await processString("testcase {{ testing.[test case] }}", {
testing: { testing: {
"test case": 1 "test case": 1
} }
@ -44,8 +44,19 @@ describe("attempt some complex problems", () => {
}, },
}, },
} }
const hbs = "{{ New Repeater.Get Actors.first_name }} {{ New Repeater.Get Actors.last_name }}" const hbs = "{{ [New Repeater].[Get Actors].[first_name] }} {{ [New Repeater].[Get Actors].[last_name] }}"
const output = await processString(hbs, context) const output = await processString(hbs, context)
expect(output).toBe("Bob Bobert") expect(output).toBe("Bob Bobert")
}) })
it("should be able to process an odd string produced by builder", async () => {
const context = {
"c306d140d7e854f388bae056db380a0eb": {
"test prop": "test",
}
}
const hbs = "null{{ [c306d140d7e854f388bae056db380a0eb].[test prop] }}"
const output = await processString(hbs, context)
expect(output).toBe("nulltest")
})
}) })