Merge branch 'frontend-core' of github.com:Budibase/budibase into experimental-hbs-caching

This commit is contained in:
Andrew Kingston 2022-02-07 09:50:17 +00:00
commit a76508c76e
27 changed files with 294 additions and 139 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -13,6 +13,7 @@
start: textarea.selectionStart, start: textarea.selectionStart,
end: textarea.selectionEnd, end: textarea.selectionEnd,
}) })
export let align = null
let focus = false let focus = false
let textarea let textarea
@ -46,6 +47,7 @@
bind:this={textarea} bind:this={textarea}
placeholder={placeholder || ""} placeholder={placeholder || ""}
class="spectrum-Textfield-input" class="spectrum-Textfield-input"
style={align ? `text-align: ${align}` : ""}
{disabled} {disabled}
{id} {id}
on:focus={() => (focus = true)} on:focus={() => (focus = true)}

View File

@ -12,6 +12,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let dataCy export let dataCy
export let align
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focus = false let focus = false
@ -92,8 +93,9 @@
on:input={onInput} on:input={onInput}
on:keyup={updateValueOnEnter} on:keyup={updateValueOnEnter}
{type} {type}
inputmode={type === "number" ? "decimal" : "text"}
class="spectrum-Textfield-input" class="spectrum-Textfield-input"
style={align ? `text-align: ${align};` : ""}
inputmode={type === "number" ? "decimal" : "text"}
/> />
</div> </div>

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -64,10 +64,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.49-alpha.8", "@budibase/bbui": "^1.0.49-alpha.9",
"@budibase/client": "^1.0.49-alpha.8", "@budibase/client": "^1.0.49-alpha.9",
"@budibase/frontend-core": "^1.0.49-alpha.8", "@budibase/frontend-core": "^1.0.49-alpha.9",
"@budibase/string-templates": "^1.0.49-alpha.8", "@budibase/string-templates": "^1.0.49-alpha.9",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -188,29 +188,27 @@
{:else} {:else}
<Body size="S"><i>No tables found.</i></Body> <Body size="S"><i>No tables found.</i></Body>
{/if} {/if}
{#if plusTables?.length !== 0 && integration.relationships} <Divider size="S" />
<Divider size="S" /> <div class="query-header">
<div class="query-header"> <Heading size="S">Relationships</Heading>
<Heading size="S">Relationships</Heading> <Button primary on:click={() => openRelationshipModal()}>
<Button primary on:click={openRelationshipModal}> Define relationship
Define relationship </Button>
</Button> </div>
</div> <Body>
<Body> Tell budibase how your tables are related to get even more smart features.
Tell budibase how your tables are related to get even more smart features. </Body>
</Body> {#if relationshipInfo && relationshipInfo.length > 0}
{#if relationshipInfo && relationshipInfo.length > 0} <Table
<Table on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)}
on:click={({ detail }) => openRelationshipModal(detail.from, detail.to)} schema={relationshipSchema}
schema={relationshipSchema} data={relationshipInfo}
data={relationshipInfo} allowEditColumns={false}
allowEditColumns={false} allowEditRows={false}
allowEditRows={false} allowSelectRows={false}
allowSelectRows={false} />
/> {:else}
{:else} <Body size="S"><i>No relationships configured.</i></Body>
<Body size="S"><i>No relationships configured.</i></Body>
{/if}
{/if} {/if}
<style> <style>

View File

@ -22,6 +22,10 @@
let originalFromName = fromRelationship.name, let originalFromName = fromRelationship.name,
originalToName = toRelationship.name originalToName = toRelationship.name
let fromTable, toTable, through, linkTable, tableOptions
let isManyToMany, isManyToOne, relationshipTypes
let errors, valid
let currentTables = {}
if (fromRelationship && !fromRelationship.relationshipType) { if (fromRelationship && !fromRelationship.relationshipType) {
fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE fromRelationship.relationshipType = RelationshipTypes.MANY_TO_ONE
@ -41,61 +45,52 @@
const touched = writable({}) const touched = writable({})
function checkForErrors( function checkForErrors(fromRelate, toRelate) {
fromTable,
toTable,
throughTable,
fromRelate,
toRelate
) {
const isMany = const isMany =
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
const tableNotSet = "Please specify a table" const tableNotSet = "Please specify a table"
const errors = {} const errObj = {}
if ($touched.from && !fromTable) { if ($touched.from && !fromTable) {
errors.from = tableNotSet errObj.from = tableNotSet
} }
if ($touched.to && !toTable) { if ($touched.to && !toTable) {
errors.to = tableNotSet errObj.to = tableNotSet
} }
if ($touched.through && isMany && !fromRelate.through) { if ($touched.through && isMany && !fromRelate.through) {
errors.through = tableNotSet errObj.through = tableNotSet
} }
if ($touched.foreign && !isMany && !fromRelate.fieldName) { if ($touched.foreign && !isMany && !fromRelate.fieldName) {
errors.foreign = "Please pick the foreign key" errObj.foreign = "Please pick the foreign key"
} }
const colNotSet = "Please specify a column name" const colNotSet = "Please specify a column name"
if ($touched.fromCol && !fromRelate.name) { if ($touched.fromCol && !fromRelate.name) {
errors.fromCol = colNotSet errObj.fromCol = colNotSet
} }
if ($touched.toCol && !toRelate.name) { if ($touched.toCol && !toRelate.name) {
errors.toCol = colNotSet errObj.toCol = colNotSet
} }
if ($touched.primary && !fromPrimary) { if ($touched.primary && !fromPrimary) {
errors.primary = "Please pick the primary key" errObj.primary = "Please pick the primary key"
} }
// currently don't support relationships back onto the table itself, needs to relate out // currently don't support relationships back onto the table itself, needs to relate out
const tableError = "From/to/through tables must be different" const tableError = "From/to/through tables must be different"
if (fromTable && (fromTable === toTable || fromTable === throughTable)) { if (fromTable && (fromTable === toTable || fromTable === through)) {
errors.from = tableError errObj.from = tableError
} }
if (toTable && (toTable === fromTable || toTable === throughTable)) { if (toTable && (toTable === fromTable || toTable === through)) {
errors.to = tableError errObj.to = tableError
} }
if ( if (through && (through === fromTable || through === toTable)) {
throughTable && errObj.through = tableError
(throughTable === fromTable || throughTable === toTable)
) {
errors.through = tableError
} }
const colError = "Column name cannot be an existing column" const colError = "Column name cannot be an existing column"
if (inSchema(fromTable, fromRelate.name, originalFromName)) { if (inSchema(fromTable, fromRelate.name, originalFromName)) {
errors.fromCol = colError errObj.fromCol = colError
} }
if (inSchema(toTable, toRelate.name, originalToName)) { if (inSchema(toTable, toRelate.name, originalToName)) {
errors.toCol = colError errObj.toCol = colError
} }
return errors errors = errObj
} }
let fromPrimary let fromPrimary
@ -115,13 +110,7 @@
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId) $: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId) $: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
$: through = plusTables.find(table => table._id === fromRelationship?.through) $: through = plusTables.find(table => table._id === fromRelationship?.through)
$: errors = checkForErrors( $: checkForErrors(fromRelationship, toRelationship)
fromTable,
toTable,
through,
fromRelationship,
toRelationship
)
$: valid = $: valid =
Object.keys(errors).length === 0 && Object.keys($touched).length !== 0 Object.keys(errors).length === 0 && Object.keys($touched).length !== 0
$: linkTable = through || toTable $: linkTable = through || toTable
@ -239,19 +228,19 @@
} }
function tableChanged(fromTbl, toTbl) { function tableChanged(fromTbl, toTbl) {
if (
(currentTables?.from?._id === fromTbl?._id &&
currentTables?.to?._id === toTbl?._id) ||
originalFromName ||
originalToName
) {
return
}
fromRelationship.name = toTbl?.name || "" fromRelationship.name = toTbl?.name || ""
errors.fromCol = "" errors.fromCol = ""
toRelationship.name = fromTbl?.name || "" toRelationship.name = fromTbl?.name || ""
errors.toCol = "" errors.toCol = ""
if (toTbl || fromTbl) { currentTables = { from: fromTbl, to: toTbl }
checkForErrors(
fromTable,
toTable,
through,
fromRelationship,
toRelationship
)
}
} }
</script> </script>

View File

@ -30,6 +30,7 @@
async function getPermissions(queryToFetch) { async function getPermissions(queryToFetch) {
if (fetched?._id === queryToFetch?._id) { if (fetched?._id === queryToFetch?._id) {
loaded = true
return return
} }
fetched = queryToFetch fetched = queryToFetch

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1942,6 +1942,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Alignment",
"key": "align",
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
} }
] ]
}, },
@ -2373,6 +2402,35 @@
"type": "validation/string", "type": "validation/string",
"label": "Validation", "label": "Validation",
"key": "validation" "key": "validation"
},
{
"type": "select",
"label": "Alignment",
"key": "align",
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
} }
] ]
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.49-alpha.8", "@budibase/bbui": "^1.0.49-alpha.9",
"@budibase/frontend-core": "^1.0.49-alpha.8", "@budibase/frontend-core": "^1.0.49-alpha.9",
"@budibase/string-templates": "^1.0.49-alpha.8", "@budibase/string-templates": "^1.0.49-alpha.9",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -9,6 +9,7 @@
export let disabled = false export let disabled = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let align
let fieldState let fieldState
let fieldApi let fieldApi
@ -35,6 +36,7 @@
disabled={fieldState.disabled} disabled={fieldState.disabled}
error={fieldState.error} error={fieldState.error}
id={fieldState.fieldId} id={fieldState.fieldId}
{align}
{placeholder} {placeholder}
/> />
</div> </div>

View File

@ -9,6 +9,7 @@
export let disabled = false export let disabled = false
export let validation export let validation
export let defaultValue = "" export let defaultValue = ""
export let align
let fieldState let fieldState
let fieldApi let fieldApi
@ -34,6 +35,7 @@
id={fieldState.fieldId} id={fieldState.fieldId}
{placeholder} {placeholder}
{type} {type}
{align}
/> />
{/if} {/if}
</Field> </Field>

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.49-alpha.8", "@budibase/bbui": "^1.0.49-alpha.9",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -70,9 +70,9 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.49-alpha.8", "@budibase/backend-core": "^1.0.49-alpha.9",
"@budibase/client": "^1.0.49-alpha.8", "@budibase/client": "^1.0.49-alpha.9",
"@budibase/string-templates": "^1.0.49-alpha.8", "@budibase/string-templates": "^1.0.49-alpha.9",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -9,12 +9,16 @@ CREATE TABLE Persons (
); );
CREATE TABLE Tasks ( CREATE TABLE Tasks (
TaskID SERIAL PRIMARY KEY, TaskID SERIAL PRIMARY KEY,
PersonID INT, ExecutorID INT,
QaID INT,
Completed BOOLEAN, Completed BOOLEAN,
TaskName varchar(255), TaskName varchar(255),
CONSTRAINT fkPersons CONSTRAINT fkexecutor
FOREIGN KEY(PersonID) FOREIGN KEY(ExecutorID)
REFERENCES Persons(PersonID) REFERENCES Persons(PersonID),
CONSTRAINT fkqa
FOREIGN KEY(QaID)
REFERENCES Persons(PersonID)
); );
CREATE TABLE Products ( CREATE TABLE Products (
ProductID SERIAL PRIMARY KEY, ProductID SERIAL PRIMARY KEY,
@ -32,8 +36,9 @@ CREATE TABLE Products_Tasks (
PRIMARY KEY (ProductID, TaskID) PRIMARY KEY (ProductID, TaskID)
); );
INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast'); INSERT INTO Persons (FirstName, LastName, Address, City) VALUES ('Mike', 'Hughes', '123 Fake Street', 'Belfast');
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'assembling', TRUE); INSERT INTO Persons (FirstName, LastName, Address, City) Values ('John', 'Smith', '64 Updown Road', 'Dublin');
INSERT INTO Tasks (PersonID, TaskName, Completed) VALUES (1, 'processing', FALSE); INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (1, 2, 'assembling', TRUE);
INSERT INTO Tasks (ExecutorID, QaID, TaskName, Completed) VALUES (2, 1, 'processing', FALSE);
INSERT INTO Products (ProductName) VALUES ('Computers'); INSERT INTO Products (ProductName) VALUES ('Computers');
INSERT INTO Products (ProductName) VALUES ('Laptops'); INSERT INTO Products (ProductName) VALUES ('Laptops');
INSERT INTO Products (ProductName) VALUES ('Chairs'); INSERT INTO Products (ProductName) VALUES ('Chairs');

View File

@ -184,7 +184,7 @@ module External {
thisRow._id = generateIdForRow(row, table) thisRow._id = generateIdForRow(row, table)
thisRow.tableId = table._id thisRow.tableId = table._id
thisRow._rev = "rev" thisRow._rev = "rev"
return thisRow return processFormulas(table, thisRow)
} }
function fixArrayTypes(row: Row, table: Table) { function fixArrayTypes(row: Row, table: Table) {
@ -327,8 +327,12 @@ module External {
* This iterates through the returned rows and works out what elements of the rows * This iterates through the returned rows and works out what elements of the rows
* actually match up to another row (based on primary keys) - this is pretty specific * actually match up to another row (based on primary keys) - this is pretty specific
* to SQL and the way that SQL relationships are returned based on joins. * to SQL and the way that SQL relationships are returned based on joins.
* This is complicated, but the idea is that when a SQL query returns all the relations
* will be separate rows, with all of the data in each row. We have to decipher what comes
* from where (which tables) and how to convert that into budibase columns.
*/ */
updateRelationshipColumns( updateRelationshipColumns(
table: Table,
row: Row, row: Row,
rows: { [key: string]: Row }, rows: { [key: string]: Row },
relationships: RelationshipsJson[] relationships: RelationshipsJson[]
@ -339,6 +343,13 @@ module External {
if (!linkedTable) { if (!linkedTable) {
continue continue
} }
const fromColumn = `${table.name}.${relationship.from}`
const toColumn = `${linkedTable.name}.${relationship.to}`
// this is important when working with multiple relationships
// between the same tables, don't want to overlap/multiply the relations
if (!relationship.through && row[fromColumn] !== row[toColumn]) {
continue
}
let linked = basicProcessing(row, linkedTable) let linked = basicProcessing(row, linkedTable)
if (!linked._id) { if (!linked._id) {
continue continue
@ -386,6 +397,7 @@ module External {
// this is a relationship of some sort // this is a relationship of some sort
if (finalRows[rowId]) { if (finalRows[rowId]) {
finalRows = this.updateRelationshipColumns( finalRows = this.updateRelationshipColumns(
table,
row, row,
finalRows, finalRows,
relationships relationships
@ -399,6 +411,7 @@ module External {
finalRows[thisRow._id] = thisRow finalRows[thisRow._id] = thisRow
// do this at end once its been added to the final rows // do this at end once its been added to the final rows
finalRows = this.updateRelationshipColumns( finalRows = this.updateRelationshipColumns(
table,
row, row,
finalRows, finalRows,
relationships relationships

View File

@ -191,29 +191,70 @@ class InternalBuilder {
if (!relationships) { if (!relationships) {
return query return query
} }
const tableSets: Record<string, [any]> = {}
// aggregate into table sets (all the same to tables)
for (let relationship of relationships) { for (let relationship of relationships) {
const from = relationship.from, const keyObj: { toTable: string; throughTable: string | undefined } = {
to = relationship.to, toTable: relationship.tableName,
toTable = relationship.tableName throughTable: undefined,
if (!relationship.through) { }
if (relationship.through) {
keyObj.throughTable = relationship.through
}
const key = JSON.stringify(keyObj)
if (tableSets[key]) {
tableSets[key].push(relationship)
} else {
tableSets[key] = [relationship]
}
}
for (let [key, relationships] of Object.entries(tableSets)) {
const { toTable, throughTable } = JSON.parse(key)
if (!throughTable) {
// @ts-ignore // @ts-ignore
query = query.leftJoin( query = query.join(
toTable, toTable,
`${fromTable}.${from}`, function () {
`${toTable}.${to}` for (let relationship of relationships) {
const from = relationship.from,
to = relationship.to
// @ts-ignore
this.orOn(`${fromTable}.${from}`, "=", `${toTable}.${to}`)
}
},
"left"
) )
} else { } else {
const throughTable = relationship.through
const fromPrimary = relationship.fromPrimary
const toPrimary = relationship.toPrimary
query = query query = query
// @ts-ignore // @ts-ignore
.leftJoin( .join(
throughTable, throughTable,
`${fromTable}.${fromPrimary}`, function () {
`${throughTable}.${from}` for (let relationship of relationships) {
const fromPrimary = relationship.fromPrimary
const from = relationship.from
// @ts-ignore
this.orOn(
`${fromTable}.${fromPrimary}`,
"=",
`${throughTable}.${from}`
)
}
},
"left"
)
.join(
toTable,
function () {
for (let relationship of relationships) {
const toPrimary = relationship.toPrimary
const to = relationship.to
// @ts-ignore
this.orOn(`${toTable}.${toPrimary}`, `${throughTable}.${to}`)
}
},
"left"
) )
.leftJoin(toTable, `${toTable}.${toPrimary}`, `${throughTable}.${to}`)
} }
} }
return query.limit(BASE_LIMIT) return query.limit(BASE_LIMIT)

View File

@ -5,9 +5,6 @@ const { integrations } = require("../integrations")
const { processStringSync } = require("@budibase/string-templates") const { processStringSync } = require("@budibase/string-templates")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const IS_TRIPLE_BRACE = new RegExp(/^{{3}.*}{3}$/)
const IS_HANDLEBARS = new RegExp(/^{{2}.*}{2}$/)
class QueryRunner { class QueryRunner {
constructor(input, flags = { noRecursiveQuery: false }) { constructor(input, flags = { noRecursiveQuery: false }) {
this.datasource = input.datasource this.datasource = input.datasource
@ -188,12 +185,8 @@ class QueryRunner {
enrichedQuery[key] = this.enrichQueryFields(fields[key], parameters) enrichedQuery[key] = this.enrichQueryFields(fields[key], parameters)
} else if (typeof fields[key] === "string") { } else if (typeof fields[key] === "string") {
// enrich string value as normal // enrich string value as normal
let value = fields[key] enrichedQuery[key] = processStringSync(fields[key], parameters, {
// add triple brace to avoid escaping e.g. '=' in cookie header noEscaping: true,
if (IS_HANDLEBARS.test(value) && !IS_TRIPLE_BRACE.test(value)) {
value = `{${value}}`
}
enrichedQuery[key] = processStringSync(value, parameters, {
noHelpers: true, noHelpers: true,
}) })
} else { } else {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -17,6 +17,7 @@ module.exports.processString = templates.processString
module.exports.processObject = templates.processObject module.exports.processObject = templates.processObject
module.exports.doesContainStrings = templates.doesContainStrings module.exports.doesContainStrings = templates.doesContainStrings
module.exports.doesContainString = templates.doesContainString module.exports.doesContainString = templates.doesContainString
module.exports.disableEscaping = templates.disableEscaping
/** /**
* Use vm2 to run JS scripts in a node env * Use vm2 to run JS scripts in a node env
@ -27,4 +28,4 @@ setJSRunner((js, context) => {
timeout: 1000 timeout: 1000
}) })
return vm.run(js) return vm.run(js)
}) })

View File

@ -3,12 +3,16 @@ const { registerAll } = require("./helpers/index")
const processors = require("./processors") const processors = require("./processors")
const { atob, btoa } = require("./utilities") const { atob, btoa } = require("./utilities")
const manifest = require("../manifest.json") const manifest = require("../manifest.json")
const { FIND_HBS_REGEX } = require("./utilities") const { FIND_HBS_REGEX, FIND_DOUBLE_HBS_REGEX } = require("./utilities")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
registerAll(hbsInstance) registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create() const hbsInstanceNoHelpers = handlebars.create()
const defaultOpts = { noHelpers: false, cacheTemplates: false } const defaultOpts = {
noHelpers: false,
cacheTemplates: false,
noEscaping: false,
}
/** /**
* Utility function to check if the object is valid. * Utility function to check if the object is valid.
@ -26,21 +30,28 @@ function testObject(object) {
* Creates a HBS template function for a given string, and optionally caches it. * Creates a HBS template function for a given string, and optionally caches it.
*/ */
let templateCache = {} let templateCache = {}
function createTemplate(string, noHelpers, cache) { function createTemplate(string, opts) {
opts = { ...defaultOpts, ...opts }
// Finalising adds a helper, can't do this with no helpers // Finalising adds a helper, can't do this with no helpers
const shouldFinalise = !noHelpers const shouldFinalise = !opts.noHelpers
const key = `${string}${shouldFinalise}` const key = `${string}${shouldFinalise}${opts.noEscaping}`
// Reuse the cached template is possible // Reuse the cached template is possible
if (cache && templateCache[key]) { if (opts.cacheTemplates && templateCache[key]) {
return templateCache[key] return templateCache[key]
} }
string = processors.preprocess(string, shouldFinalise) string = processors.preprocess(string, shouldFinalise)
// Optionally disable built in HBS escaping
if (opts.noEscaping) {
string = exports.disableEscaping(string)
}
// This does not throw an error when template can't be fulfilled, // This does not throw an error when template can't be fulfilled,
// have to try correct beforehand // have to try correct beforehand
const instance = noHelpers ? hbsInstanceNoHelpers : hbsInstance const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const template = instance.compile(string, { const template = instance.compile(string, {
strict: false, strict: false,
}) })
@ -53,7 +64,7 @@ function createTemplate(string, noHelpers, cache) {
* @param {object|array} object The input structure which is to be recursed, it is important to note that * @param {object|array} object The input structure which is to be recursed, it is important to note that
* if the structure contains any cycles then this will fail. * if the structure contains any cycles then this will fail.
* @param {object} context The context that handlebars should fill data from. * @param {object} context The context that handlebars should fill data from.
* @param {object|null} opts optional - specify some options for processing. * @param {object|undefined} opts optional - specify some options for processing.
* @returns {Promise<object|array>} The structure input, as fully updated as possible. * @returns {Promise<object|array>} The structure input, as fully updated as possible.
*/ */
module.exports.processObject = async (object, context, opts) => { module.exports.processObject = async (object, context, opts) => {
@ -84,7 +95,7 @@ module.exports.processObject = async (object, context, opts) => {
* then nothing will occur. * then nothing will occur.
* @param {string} string The template string which is the filled from the context object. * @param {string} string The template string which is the filled from the context object.
* @param {object} context An object of information which will be used to enrich the string. * @param {object} context An object of information which will be used to enrich the string.
* @param {object|null} opts optional - specify some options for processing. * @param {object|undefined} opts optional - specify some options for processing.
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be. * @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
*/ */
module.exports.processString = async (string, context, opts) => { module.exports.processString = async (string, context, opts) => {
@ -98,7 +109,7 @@ module.exports.processString = async (string, context, opts) => {
* @param {object|array} object The input structure which is to be recursed, it is important to note that * @param {object|array} object The input structure which is to be recursed, it is important to note that
* if the structure contains any cycles then this will fail. * if the structure contains any cycles then this will fail.
* @param {object} context The context that handlebars should fill data from. * @param {object} context The context that handlebars should fill data from.
* @param {object|null} opts optional - specify some options for processing. * @param {object|undefined} opts optional - specify some options for processing.
* @returns {object|array} The structure input, as fully updated as possible. * @returns {object|array} The structure input, as fully updated as possible.
*/ */
module.exports.processObjectSync = (object, context, opts) => { module.exports.processObjectSync = (object, context, opts) => {
@ -119,19 +130,17 @@ module.exports.processObjectSync = (object, context, opts) => {
* then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call. * then nothing will occur. This is a pure sync call and therefore does not have the full functionality of the async call.
* @param {string} string The template string which is the filled from the context object. * @param {string} string The template string which is the filled from the context object.
* @param {object} context An object of information which will be used to enrich the string. * @param {object} context An object of information which will be used to enrich the string.
* @param {object|null} opts optional - specify some options for processing. * @param {object|undefined} opts optional - specify some options for processing.
* @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, opts) => { module.exports.processStringSync = (string, context, opts) => {
opts = { ...defaultOpts, ...opts } // Take a copy of input in case of error
// take a copy of input in case of error
const input = string const input = string
if (typeof string !== "string") { if (typeof string !== "string") {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
try { try {
const template = createTemplate(string, opts.noHelpers, opts.cacheTemplates) const template = createTemplate(string, opts)
const now = Math.floor(Date.now() / 1000) * 1000 const now = Math.floor(Date.now() / 1000) * 1000
return processors.postprocess( return processors.postprocess(
template({ template({
@ -144,6 +153,24 @@ module.exports.processStringSync = (string, context, opts) => {
} }
} }
/**
* By default with expressions like {{ name }} handlebars will escape various
* characters, which can be problematic. To fix this we use the syntax {{{ name }}},
* this function will find any double braces and switch to triple.
* @param string the string to have double HBS statements converted to triple.
*/
module.exports.disableEscaping = string => {
let regexp = new RegExp(FIND_DOUBLE_HBS_REGEX)
const matches = string.match(regexp)
if (matches == null) {
return string
}
for (let match of matches) {
string = string.replace(match, `{${match}}`)
}
return string
}
/** /**
* Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly. * Simple utility function which makes sure that a templating property has been wrapped in literal specifiers correctly.
* @param {string} property The property which is to be wrapped. * @param {string} property The property which is to be wrapped.
@ -160,7 +187,6 @@ 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, opts) => { module.exports.isValid = (string, opts) => {
opts = { ...defaultOpts, ...opts }
const validCases = [ const validCases = [
"string", "string",
"number", "number",
@ -174,7 +200,7 @@ module.exports.isValid = (string, opts) => {
// 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 {
const template = createTemplate(string, opts.noHelpers, opts.cache) const template = createTemplate(string, opts)
template(context) template(context)
return true return true
} catch (err) { } catch (err) {

View File

@ -17,6 +17,7 @@ export const processString = templates.processString
export const processObject = templates.processObject export const processObject = templates.processObject
export const doesContainStrings = templates.doesContainStrings export const doesContainStrings = templates.doesContainStrings
export const doesContainString = templates.doesContainString export const doesContainString = templates.doesContainString
export const disableEscaping = templates.disableEscaping
/** /**
* Use polyfilled vm to run JS scripts in a browser Env * Use polyfilled vm to run JS scripts in a browser Env
@ -30,4 +31,4 @@ setJSRunner((js, context) => {
} }
vm.createContext(context) vm.createContext(context)
return vm.runInNewContext(js, context, { timeout: 1000 }) return vm.runInNewContext(js, context, { timeout: 1000 })
}) })

View File

@ -1,6 +1,7 @@
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g
module.exports.FIND_DOUBLE_HBS_REGEX = /(?<!{){{[^{}]+}}(?!})/g
module.exports.isAlphaNumeric = char => { module.exports.isAlphaNumeric = char => {
return char.match(ALPHA_NUMERIC_REGEX) return char.match(ALPHA_NUMERIC_REGEX)

View File

@ -6,6 +6,7 @@ const {
getManifest, getManifest,
encodeJSBinding, encodeJSBinding,
doesContainString, doesContainString,
disableEscaping,
} = require("../src/index.cjs") } = require("../src/index.cjs")
describe("Test that the string processing works correctly", () => { describe("Test that the string processing works correctly", () => {
@ -176,3 +177,22 @@ describe("check does contain string function", () => {
expect(doesContainString(js, "foo")).toEqual(true) expect(doesContainString(js, "foo")).toEqual(true)
}) })
}) })
describe("check that disabling escaping function works", () => {
it("should work for a single statement", () => {
expect(disableEscaping("{{ name }}")).toEqual("{{{ name }}}")
})
it("should work for two statements", () => {
expect(disableEscaping("{{ name }} welcome to {{ platform }}")).toEqual("{{{ name }}} welcome to {{{ platform }}}")
})
it("shouldn't convert triple braces", () => {
expect(disableEscaping("{{{ name }}}")).toEqual("{{{ name }}}")
})
it("should work with a combination", () => {
expect(disableEscaping("{{ name }} welcome to {{{ platform }}}")).toEqual("{{{ name }}} welcome to {{{ platform }}}")
})
})

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.49-alpha.8", "version": "1.0.49-alpha.9",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -34,8 +34,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "^1.0.49-alpha.8", "@budibase/backend-core": "^1.0.49-alpha.9",
"@budibase/string-templates": "^1.0.49-alpha.8", "@budibase/string-templates": "^1.0.49-alpha.9",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",