Merge pull request #1082 from Budibase/bugs/quick-fixes
Bunch of bug fixes - relationships and handlebars
This commit is contained in:
commit
1e54a3d57c
|
@ -84,6 +84,13 @@ exports.getLinkDocuments = async function({
|
||||||
// filter to get unique entries
|
// filter to get unique entries
|
||||||
const foundIds = []
|
const foundIds = []
|
||||||
linkRows = linkRows.filter(link => {
|
linkRows = linkRows.filter(link => {
|
||||||
|
// make sure anything unique is the correct key
|
||||||
|
if (
|
||||||
|
(tableId && link.key[0] !== tableId) ||
|
||||||
|
(rowId && link.key[1] !== rowId)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
const unique = foundIds.indexOf(link.id) === -1
|
const unique = foundIds.indexOf(link.id) === -1
|
||||||
if (unique) {
|
if (unique) {
|
||||||
foundIds.push(link.id)
|
foundIds.push(link.id)
|
||||||
|
|
|
@ -1097,8 +1097,8 @@
|
||||||
"format"
|
"format"
|
||||||
],
|
],
|
||||||
"numArgs": 2,
|
"numArgs": 2,
|
||||||
"example": "{{date now \"YYYY\"}}",
|
"example": "{{date now \"DD-MM-YYYY\"}}",
|
||||||
"description": "<p>Format a date using moment.js data formatting.</p>\n"
|
"description": "<p>Format a date using moment.js date formatting.</p>\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,9 +15,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/handlebars-helpers": "^0.11.3",
|
"@budibase/handlebars-helpers": "^0.11.3",
|
||||||
|
"dayjs": "^1.10.4",
|
||||||
"handlebars": "^4.7.6",
|
"handlebars": "^4.7.6",
|
||||||
"handlebars-utils": "^1.0.6",
|
"handlebars-utils": "^1.0.6",
|
||||||
"helper-date": "^1.0.1",
|
|
||||||
"lodash": "^4.17.20"
|
"lodash": "^4.17.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -139,8 +139,8 @@ function run() {
|
||||||
date: {
|
date: {
|
||||||
args: ["datetime", "format"],
|
args: ["datetime", "format"],
|
||||||
numArgs: 2,
|
numArgs: 2,
|
||||||
example: '{{date now "YYYY"}}',
|
example: '{{date now "DD-MM-YYYY"}}',
|
||||||
description: "Format a date using moment.js data formatting.",
|
description: "Format a date using moment.js date formatting.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// convert all markdown to HTML
|
// convert all markdown to HTML
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
const dayjs = require("dayjs")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file was largely taken from the helper-date package - we did this for two reasons:
|
||||||
|
* 1. It made use of both moment of date.js - this caused some weird bugs with some relatively simple
|
||||||
|
* syntax and didn't offer much in return.
|
||||||
|
* 2. Replacing moment with dayjs helps massively reduce bundle size.
|
||||||
|
* The original package can be found here:
|
||||||
|
* https://github.com/helpers/helper-date
|
||||||
|
*/
|
||||||
|
|
||||||
|
function isOptions(val) {
|
||||||
|
return typeof val === "object" && typeof val.hash === "object"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isApp(thisArg) {
|
||||||
|
return (
|
||||||
|
typeof thisArg === "object" &&
|
||||||
|
typeof thisArg.options === "object" &&
|
||||||
|
typeof thisArg.app === "object"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContext(thisArg, locals, options) {
|
||||||
|
if (isOptions(thisArg)) {
|
||||||
|
return getContext({}, locals, thisArg)
|
||||||
|
}
|
||||||
|
// ensure args are in the correct order
|
||||||
|
if (isOptions(locals)) {
|
||||||
|
return getContext(thisArg, options, locals)
|
||||||
|
}
|
||||||
|
const appContext = isApp(thisArg) ? thisArg.context : {}
|
||||||
|
options = options || {}
|
||||||
|
|
||||||
|
// if "options" is not handlebars options, merge it onto locals
|
||||||
|
if (!isOptions(options)) {
|
||||||
|
locals = Object.assign({}, locals, options)
|
||||||
|
}
|
||||||
|
// merge handlebars root data onto locals if specified on the hash
|
||||||
|
if (isOptions(options) && options.hash.root === true) {
|
||||||
|
locals = Object.assign({}, options.data.root, locals)
|
||||||
|
}
|
||||||
|
let context = Object.assign({}, appContext, locals, options.hash)
|
||||||
|
if (!isApp(thisArg)) {
|
||||||
|
context = Object.assign({}, thisArg, context)
|
||||||
|
}
|
||||||
|
if (isApp(thisArg) && thisArg.view && thisArg.view.data) {
|
||||||
|
context = Object.assign({}, context, thisArg.view.data)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function dateHelper(str, pattern, options) {
|
||||||
|
if (isOptions(pattern)) {
|
||||||
|
options = pattern
|
||||||
|
pattern = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOptions(str)) {
|
||||||
|
options = str
|
||||||
|
pattern = null
|
||||||
|
str = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no args are passed, return a formatted date
|
||||||
|
if (str == null && pattern == null) {
|
||||||
|
dayjs.locale("en")
|
||||||
|
return dayjs().format("MMMM DD, YYYY")
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = { lang: "en", date: new Date(str) }
|
||||||
|
const opts = getContext(this, defaults, options)
|
||||||
|
|
||||||
|
// set the language to use
|
||||||
|
dayjs.locale(opts.lang || opts.language)
|
||||||
|
|
||||||
|
return dayjs(new Date(str)).format(pattern)
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
const helpers = require("@budibase/handlebars-helpers")
|
const helpers = require("@budibase/handlebars-helpers")
|
||||||
const dateHelper = require("helper-date")
|
const dateHelper = require("./date")
|
||||||
const { HelperFunctionBuiltin } = require("./constants")
|
const { HelperFunctionBuiltin } = require("./constants")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,11 @@ const handlebars = require("handlebars")
|
||||||
const { registerAll } = require("./helpers/index")
|
const { registerAll } = require("./helpers/index")
|
||||||
const processors = require("./processors")
|
const processors = require("./processors")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const { removeNull, addConstants } = require("./utilities")
|
const {
|
||||||
|
removeNull,
|
||||||
|
addConstants,
|
||||||
|
removeHandlebarsStatements,
|
||||||
|
} = require("./utilities")
|
||||||
const manifest = require("../manifest.json")
|
const manifest = require("../manifest.json")
|
||||||
|
|
||||||
const hbsInstance = handlebars.create()
|
const hbsInstance = handlebars.create()
|
||||||
|
@ -83,16 +87,27 @@ module.exports.processObjectSync = (object, context) => {
|
||||||
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
* @returns {string} The enriched string, all templates should have been replaced if they can be.
|
||||||
*/
|
*/
|
||||||
module.exports.processStringSync = (string, context) => {
|
module.exports.processStringSync = (string, context) => {
|
||||||
|
if (!exports.isValid(string)) {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
// take a copy of input incase error
|
||||||
|
const input = string
|
||||||
let clonedContext = removeNull(cloneDeep(context))
|
let clonedContext = removeNull(cloneDeep(context))
|
||||||
clonedContext = addConstants(clonedContext)
|
clonedContext = addConstants(clonedContext)
|
||||||
// remove any null/undefined properties
|
// remove any null/undefined properties
|
||||||
if (typeof string !== "string") {
|
if (typeof string !== "string") {
|
||||||
throw "Cannot process non-string types."
|
throw "Cannot process non-string types."
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
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
|
||||||
const template = hbsInstance.compile(string)
|
const template = hbsInstance.compile(string, {
|
||||||
|
strict: false,
|
||||||
|
})
|
||||||
return processors.postprocess(template(clonedContext))
|
return processors.postprocess(template(clonedContext))
|
||||||
|
} catch (err) {
|
||||||
|
return removeHandlebarsStatements(input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,19 +125,33 @@ module.exports.makePropSafe = property => {
|
||||||
* @returns {boolean} Whether or not the input string is valid.
|
* @returns {boolean} Whether or not the input string is valid.
|
||||||
*/
|
*/
|
||||||
module.exports.isValid = string => {
|
module.exports.isValid = string => {
|
||||||
const specialCases = ["string", "number", "object", "array"]
|
const validCases = [
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"object",
|
||||||
|
"array",
|
||||||
|
"cannot read property",
|
||||||
|
]
|
||||||
|
// this is a portion of a specific string always output by handlebars in the case of a syntax error
|
||||||
|
const invalidCases = [`expecting '`]
|
||||||
// don't really need a real context to check if its valid
|
// don't really need a real context to check if its valid
|
||||||
const context = {}
|
const context = {}
|
||||||
try {
|
try {
|
||||||
hbsInstance.compile(processors.preprocess(string, false))(context)
|
hbsInstance.compile(processors.preprocess(string, false))(context)
|
||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err ? err.message : ""
|
const msg = err && err.message ? err.message : err
|
||||||
const foundCase = specialCases.find(spCase =>
|
if (!msg) {
|
||||||
msg.toLowerCase().includes(spCase)
|
return false
|
||||||
|
}
|
||||||
|
const invalidCase = invalidCases.some(invalidCase =>
|
||||||
|
msg.toLowerCase().includes(invalidCase)
|
||||||
|
)
|
||||||
|
const validCase = validCases.some(validCase =>
|
||||||
|
msg.toLowerCase().includes(validCase)
|
||||||
)
|
)
|
||||||
// special case for maths functions - don't have inputs yet
|
// special case for maths functions - don't have inputs yet
|
||||||
return !!foundCase
|
return validCase && !invalidCase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ module.exports.processors = [
|
||||||
return statement
|
return statement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (HelperNames().some(option => possibleHelper.includes(option))) {
|
if (HelperNames().some(option => option.includes(possibleHelper))) {
|
||||||
insideStatement = `(${insideStatement})`
|
insideStatement = `(${insideStatement})`
|
||||||
}
|
}
|
||||||
return `{{ all ${insideStatement} }}`
|
return `{{ all ${insideStatement} }}`
|
||||||
|
|
|
@ -32,3 +32,16 @@ module.exports.addConstants = obj => {
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports.removeHandlebarsStatements = string => {
|
||||||
|
let regexp = new RegExp(exports.FIND_HBS_REGEX)
|
||||||
|
let matches = string.match(regexp)
|
||||||
|
if (matches == null) {
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
for (let match of matches) {
|
||||||
|
const idx = string.indexOf(match)
|
||||||
|
string = exports.swapStrings(string, idx, match.length, "Invalid Binding")
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const {
|
const {
|
||||||
processString,
|
processString,
|
||||||
|
processObject,
|
||||||
isValid,
|
isValid,
|
||||||
} = require("../src/index")
|
} = require("../src/index")
|
||||||
|
|
||||||
|
@ -316,4 +317,25 @@ describe("Cover a few complex use cases", () => {
|
||||||
const validity = isValid("{{ subtract [c390c23a7f1b6441c98d2fe2a51248ef3].[total profit] [c390c23a7f1b6441c98d2fe2a51248ef3].[total revenue] }}")
|
const validity = isValid("{{ subtract [c390c23a7f1b6441c98d2fe2a51248ef3].[total profit] [c390c23a7f1b6441c98d2fe2a51248ef3].[total revenue] }}")
|
||||||
expect(validity).toBe(true)
|
expect(validity).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should confirm a bunch of invalid strings", () => {
|
||||||
|
const invalids = ["{{ awd )", "{{ awdd () ", "{{ awdwad ", "{{ awddawd }"]
|
||||||
|
for (let invalid of invalids) {
|
||||||
|
const validity = isValid(invalid)
|
||||||
|
expect(validity).toBe(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("input a garbage string, expect it to be returned", async () => {
|
||||||
|
const input = `{{{{{{ } {{ ]] ] ] }}} {{ ] {{ { } { dsa { dddddd }}}}}}} }DDD`
|
||||||
|
const output = await processString(input, {})
|
||||||
|
expect(output).toBe(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("getting a nice date from the user", async () => {
|
||||||
|
const input = {text: `{{ date user.subscriptionDue "DD-MM" }}`}
|
||||||
|
const context = JSON.parse(`{"user":{"email":"test@test.com","roleId":"ADMIN","type":"user","tableId":"ta_users","subscriptionDue":"2021-01-12T12:00:00.000Z","_id":"ro_ta_users_us_test@test.com","_rev":"2-24cc794985eb54183ecb93e148563f3d"}}`)
|
||||||
|
const output = await processObject(input, context)
|
||||||
|
expect(output.text).toBe("12-01")
|
||||||
|
})
|
||||||
})
|
})
|
|
@ -1596,6 +1596,11 @@ date.js@^0.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug "~3.1.0"
|
debug "~3.1.0"
|
||||||
|
|
||||||
|
dayjs@^1.10.4:
|
||||||
|
version "1.10.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
|
||||||
|
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
|
||||||
|
|
||||||
debug@^2.2.0, debug@^2.3.3:
|
debug@^2.2.0, debug@^2.3.3:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
|
|
Loading…
Reference in New Issue