Merge branch 'develop' into options-text-columns

This commit is contained in:
Rory Powell 2021-10-18 11:25:35 +01:00
commit 956a826b8c
75 changed files with 1949 additions and 436 deletions

View File

@ -9,7 +9,7 @@ env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN_SELF_HOST }}
jobs: jobs:
release: release:

View File

@ -7,7 +7,7 @@ env:
POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }} POSTHOG_TOKEN: ${{ secrets.POSTHOG_TOKEN }}
INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }} INTERCOM_TOKEN: ${{ secrets.INTERCOM_TOKEN }}
POSTHOG_URL: ${{ secrets.POSTHOG_URL }} POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} SENTRY_DSN: ${{ secrets.SENTRY_DSN_SELF_HOST }}
jobs: jobs:
release: release:

View File

@ -21,7 +21,7 @@ services:
PORT: 4002 PORT: 4002
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
LOG_LEVEL: info LOG_LEVEL: info
SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131 SENTRY_DSN: https://cc54bb0358fd4300ae97ef2273fbaf9f@o420233.ingest.sentry.io/6007553
ENABLE_ANALYTICS: "true" ENABLE_ANALYTICS: "true"
REDIS_URL: redis-service:6379 REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
@ -51,7 +51,6 @@ services:
INTERNAL_API_KEY: ${INTERNAL_API_KEY} INTERNAL_API_KEY: ${INTERNAL_API_KEY}
REDIS_URL: redis-service:6379 REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
ACCOUNT_PORTAL_URL: https://portal.budi.live
volumes: volumes:
- ./logs:/logs - ./logs:/logs
depends_on: depends_on:

View File

@ -1,5 +1,5 @@
{ {
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/auth", "name": "@budibase/auth",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"description": "Authentication middlewares for budibase builder and apps", "description": "Authentication middlewares for budibase builder and apps",
"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": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",

View File

@ -5,7 +5,6 @@
import RelationshipRenderer from "./RelationshipRenderer.svelte" import RelationshipRenderer from "./RelationshipRenderer.svelte"
import AttachmentRenderer from "./AttachmentRenderer.svelte" import AttachmentRenderer from "./AttachmentRenderer.svelte"
import ArrayRenderer from "./ArrayRenderer.svelte" import ArrayRenderer from "./ArrayRenderer.svelte"
import InternalRenderer from "./InternalRenderer.svelte"
export let row export let row
export let schema export let schema
@ -23,9 +22,7 @@
number: StringRenderer, number: StringRenderer,
longform: StringRenderer, longform: StringRenderer,
array: ArrayRenderer, array: ArrayRenderer,
internal: InternalRenderer,
} }
$: type = schema?.type ?? "string" $: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name) $: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer $: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer

View File

@ -8,11 +8,19 @@
const selected = getContext("tab") const selected = getContext("tab")
let tab let tab
let tabInfo let tabInfo
const setTabInfo = () => { const setTabInfo = () => {
tabInfo = tab.getBoundingClientRect() // If the tabs are being rendered inside a component which uses
if ($selected.title === title) { // a svelte transition to enter, then this initial getBoundingClientRect
$selected.info = tabInfo // will return an incorrect position.
} // We just need to get this off the main thread to fix this, by using
// a 0ms timeout.
setTimeout(() => {
tabInfo = tab.getBoundingClientRect()
if ($selected.title === title) {
$selected.info = tabInfo
}
}, 0)
} }
onMount(() => { onMount(() => {

View File

@ -31,7 +31,7 @@ context("Create a Table", () => {
cy.contains("nameupdated ").should("contain", "nameupdated") cy.contains("nameupdated ").should("contain", "nameupdated")
}) })
/*
it("edits a row", () => { it("edits a row", () => {
cy.contains("button", "Edit").click({ force: true }) cy.contains("button", "Edit").click({ force: true })
cy.wait(1000) cy.wait(1000)
@ -40,7 +40,7 @@ context("Create a Table", () => {
cy.contains("Save").click() cy.contains("Save").click()
cy.contains("Updated").should("have.text", "Updated") cy.contains("Updated").should("have.text", "Updated")
}) })
*/
it("deletes a row", () => { it("deletes a row", () => {
cy.get(".spectrum-Checkbox-input").check({ force: true }) cy.get(".spectrum-Checkbox-input").check({ force: true })
cy.contains("Delete 1 row(s)").click() cy.contains("Delete 1 row(s)").click()

View File

@ -36,18 +36,11 @@ Cypress.Commands.add("createApp", name => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`) cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(500) cy.wait(500)
cy.contains(/Start from scratch/).click() cy.contains(/Start from scratch/).click()
cy.get(".spectrum-Modal") cy.get(".spectrum-Modal").within(() => {
.within(() => { cy.get("input").eq(0).type(name).should("have.value", name).blur()
cy.get("input").eq(0).type(name).should("have.value", name).blur() cy.get(".spectrum-ButtonGroup").contains("Create app").click()
cy.get(".spectrum-ButtonGroup").contains("Create app").click() cy.wait(7000)
cy.wait(7000) })
})
.then(() => {
// Because we show the datasource modal on entry, we need to create a table to get rid of the modal in the future
cy.createInitialDatasource("initialTable")
cy.expandBudibaseConnection()
cy.get(".nav-item.selected > .content").should("be.visible")
})
}) })
Cypress.Commands.add("deleteApp", () => { Cypress.Commands.add("deleteApp", () => {
@ -77,22 +70,6 @@ Cypress.Commands.add("createTestTableWithData", () => {
cy.addColumn("dog", "age", "Number") cy.addColumn("dog", "age", "Number")
}) })
Cypress.Commands.add("createInitialDatasource", tableName => {
// Enter table name
cy.get(".spectrum-Modal").within(() => {
cy.contains("Budibase DB").trigger("mouseover").click().click()
cy.wait(1000)
cy.contains("Continue").click()
})
cy.get(".spectrum-Modal").within(() => {
cy.wait(1000)
cy.get("input").first().type(tableName).blur()
cy.get(".spectrum-ButtonGroup").contains("Create").click()
})
cy.contains(tableName).should("be.visible")
})
Cypress.Commands.add("createTable", tableName => { Cypress.Commands.add("createTable", tableName => {
cy.contains("Budibase DB").click() cy.contains("Budibase DB").click()
cy.contains("Create new table").click() cy.contains("Create new table").click()

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^0.9.167-alpha.1", "@budibase/bbui": "^0.9.167-alpha.12",
"@budibase/client": "^0.9.167-alpha.1", "@budibase/client": "^0.9.167-alpha.12",
"@budibase/colorpicker": "1.1.2", "@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.167-alpha.1", "@budibase/string-templates": "^0.9.167-alpha.12",
"@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

@ -15,7 +15,7 @@ const apiCall =
if (resp.status === 403) { if (resp.status === 403) {
removeCookie(Cookies.Auth) removeCookie(Cookies.Auth)
// reload after removing cookie, go to login // reload after removing cookie, go to login
if (!url.includes("self")) { if (!url.includes("self") && !url.includes("login")) {
location.reload() location.reload()
} }
} }

View File

@ -7,11 +7,17 @@ import {
} from "./storeUtils" } from "./storeUtils"
import { store } from "builderStore" import { store } from "builderStore"
import { queries as queriesStores, tables as tablesStore } from "stores/backend" import { queries as queriesStores, tables as tablesStore } from "stores/backend"
import { makePropSafe } from "@budibase/string-templates" import {
makePropSafe,
isJSBinding,
decodeJSBinding,
encodeJSBinding,
} from "@budibase/string-templates"
import { TableNames } from "../constants" import { TableNames } from "../constants"
// 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
const CAPTURE_VAR_INSIDE_JS = /\$\("([^")]+)"\)/g
const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g const CAPTURE_HBS_TEMPLATE = /{{[\S\s]*?}}/g
/** /**
@ -430,6 +436,15 @@ function replaceBetween(string, start, end, replacement) {
* utility function for the readableToRuntimeBinding and runtimeToReadableBinding. * utility function for the readableToRuntimeBinding and runtimeToReadableBinding.
*/ */
function bindingReplacement(bindableProperties, textWithBindings, convertTo) { function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
// Decide from base64 if using JS
const isJS = isJSBinding(textWithBindings)
if (isJS) {
textWithBindings = decodeJSBinding(textWithBindings)
}
// Determine correct regex to find bindings to replace
const regex = isJS ? CAPTURE_VAR_INSIDE_JS : CAPTURE_VAR_INSIDE_TEMPLATE
const convertFrom = const convertFrom =
convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding" convertTo === "runtimeBinding" ? "readableBinding" : "runtimeBinding"
if (typeof textWithBindings !== "string") { if (typeof textWithBindings !== "string") {
@ -441,7 +456,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
.sort((a, b) => { .sort((a, b) => {
return b.length - a.length return b.length - a.length
}) })
const boundValues = textWithBindings.match(CAPTURE_VAR_INSIDE_TEMPLATE) || [] const boundValues = textWithBindings.match(regex) || []
let result = textWithBindings let result = textWithBindings
for (let boundValue of boundValues) { for (let boundValue of boundValues) {
let newBoundValue = boundValue let newBoundValue = boundValue
@ -449,7 +464,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
// in the search, working from longest to shortest so always use best match first // in the search, working from longest to shortest so always use best match first
let searchString = newBoundValue let searchString = newBoundValue
for (let from of convertFromProps) { for (let from of convertFromProps) {
if (shouldReplaceBinding(newBoundValue, from, convertTo)) { if (isJS || shouldReplaceBinding(newBoundValue, from, convertTo)) {
const binding = bindableProperties.find(el => el[convertFrom] === from) const binding = bindableProperties.find(el => el[convertFrom] === from)
let idx let idx
do { do {
@ -472,6 +487,12 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) {
} }
result = result.replace(boundValue, newBoundValue) result = result.replace(boundValue, newBoundValue)
} }
// Re-encode to base64 if using JS
if (isJS) {
result = encodeJSBinding(result)
}
return result return result
} }

View File

@ -103,7 +103,7 @@
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> <Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
</div> </div>
</div> </div>
{#if testResult} {#if testResult && testResult[0]}
<span on:click={() => resultsModal.show()}> <span on:click={() => resultsModal.show()}>
<StatusLight <StatusLight
positive={isTrigger || testResult[0].outputs?.success} positive={isTrigger || testResult[0].outputs?.success}

View File

@ -194,6 +194,7 @@
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
{bindings} {bindings}
allowJS={false}
/> />
{/if} {/if}
{:else if value.customType === "query"} {:else if value.customType === "query"}
@ -259,6 +260,7 @@
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
{bindings} {bindings}
allowJS={false}
/> />
</div> </div>
{/if} {/if}

View File

@ -39,6 +39,7 @@
type="string" type="string"
{bindings} {bindings}
fillWidth={true} fillWidth={true}
allowJS={false}
/> />
{/each} {/each}
</div> </div>

View File

@ -1,6 +1,12 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select, Toggle, DatePicker, Multiselect } from "@budibase/bbui" import {
Select,
Toggle,
DatePicker,
Multiselect,
TextArea,
} from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
@ -52,7 +58,6 @@
getOptionLabel={table => table.name} getOptionLabel={table => table.name}
getOptionValue={table => table._id} getOptionValue={table => table._id}
/> />
{#if schemaFields.length} {#if schemaFields.length}
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
@ -82,6 +87,8 @@
label={field} label={field}
options={schema.constraints.inclusion} options={schema.constraints.inclusion}
/> />
{:else if schema.type === "longform"}
<TextArea label={field} bind:value={value[field]} />
{:else if schema.type === "link"} {:else if schema.type === "link"}
<LinkedRowSelector bind:linkedRows={value[field]} {schema} /> <LinkedRowSelector bind:linkedRows={value[field]} {schema} />
{:else if schema.type === "string" || schema.type === "number"} {:else if schema.type === "string" || schema.type === "number"}
@ -103,6 +110,7 @@
type="string" type="string"
{bindings} {bindings}
fillWidth={true} fillWidth={true}
allowJS={false}
/> />
{/if} {/if}
{/if} {/if}

View File

@ -16,29 +16,11 @@
import { Pagination } from "@budibase/bbui" import { Pagination } from "@budibase/bbui"
let hideAutocolumns = true let hideAutocolumns = true
let schema
$: isUsersTable = $tables.selected?._id === TableNames.USERS $: isUsersTable = $tables.selected?._id === TableNames.USERS
$: type = $tables.selected?.type $: type = $tables.selected?.type
$: isInternal = type !== "external" $: isInternal = type !== "external"
$: { $: schema = $tables.selected?.schema
schema = $tables.selected?.schema
// Manually add these as we don't want them to be 'real' auto-columns
schema._id = {
type: "internal",
editable: false,
displayName: "ID",
autocolumn: true,
}
if (isInternal) {
schema._rev = {
type: "internal",
editable: false,
displayName: "Revision",
autocolumn: true,
}
}
}
$: id = $tables.selected?._id $: id = $tables.selected?._id
$: search = searchTable(id) $: search = searchTable(id)
$: columnOptions = Object.keys($search.schema || {}) $: columnOptions = Object.keys($search.schema || {})

View File

@ -0,0 +1,159 @@
<script context="module">
import { Label } from "@budibase/bbui"
export const EditorModes = {
JS: {
name: "javascript",
json: false,
},
JSON: {
name: "javascript",
json: true,
},
SQL: {
name: "sql",
},
Handlebars: {
name: "handlebars",
base: "text/html",
},
}
</script>
<script>
import CodeMirror from "components/integration/codemirror"
import { themeStore } from "builderStore"
import { createEventDispatcher, onMount } from "svelte"
export let mode = EditorModes.JS
export let value = ""
export let height = 300
export let resize = "none"
export let readonly = false
export let hints = []
export let label
const dispatch = createEventDispatcher()
let textarea
let editor
// Keep editor up to date with value
$: editor?.setValue(value || "")
// Creates an instance of a code mirror editor
async function createEditor(mode, value) {
if (!CodeMirror || !textarea || editor) {
return
}
// Configure CM options
const lightTheme = $themeStore.theme.includes("light")
const options = {
mode,
value: value || "",
readOnly: readonly,
theme: lightTheme ? "default" : "tomorrow-night-eighties",
// Style
lineNumbers: true,
lineWrapping: true,
indentWithTabs: true,
indentUnit: 2,
tabSize: 2,
// QOL addons
extraKeys: { "Ctrl-Space": "autocomplete" },
styleActiveLine: { nonEmpty: true },
autoCloseBrackets: true,
matchBrackets: true,
}
// Register hints plugin if desired
if (hints?.length) {
CodeMirror.registerHelper("hint", "dictionaryHint", function (editor) {
const cursor = editor.getCursor()
return {
list: hints,
from: CodeMirror.Pos(cursor.line, cursor.ch),
to: CodeMirror.Pos(cursor.line, cursor.ch),
}
})
CodeMirror.commands.autocomplete = function (cm) {
CodeMirror.showHint(cm, CodeMirror.hint.dictionaryHint)
}
}
// Construct CM instance
editor = CodeMirror.fromTextArea(textarea, options)
// Use a blur handler to update the value
editor.on("blur", instance => {
dispatch("change", instance.getValue())
})
}
// Export a function to expose caret position
export const getCaretPosition = () => {
const cursor = editor.getCursor()
return {
start: cursor.ch,
end: cursor.ch,
}
}
onMount(() => {
// Create the editor with initial value
createEditor(mode, value)
// Clean up editor on unmount
return () => {
if (editor) {
editor.toTextArea()
}
}
})
</script>
{#if label}
<div style="margin-bottom: var(--spacing-s)">
<Label small>{label}</Label>
</div>
{/if}
<div
style={`--code-mirror-height: ${height}px; --code-mirror-resize: ${resize}`}
>
<textarea tabindex="0" bind:this={textarea} readonly {value} />
</div>
<style>
div :global(.CodeMirror) {
height: var(--code-mirror-height);
min-height: var(--code-mirror-height);
font-family: monospace;
line-height: 1.3;
border: var(--spectrum-alias-border-size-thin) solid;
border-color: var(--spectrum-alias-border-color);
border-radius: var(--border-radius-s);
resize: var(--code-mirror-resize);
overflow: hidden;
}
/* Override default active line highlight colour in dark theme */
div
:global(.CodeMirror-focused.cm-s-tomorrow-night-eighties
.CodeMirror-activeline-background) {
background: rgba(255, 255, 255, 0.075);
}
/* Remove active line styling when not focused */
div
:global(.CodeMirror:not(.CodeMirror-focused)
.CodeMirror-activeline-background) {
background: unset;
}
/* Add a spectrum themed border when focused */
div :global(.CodeMirror-focused) {
border-color: var(--spectrum-alias-border-color-mouse-focus);
}
</style>

View File

@ -1,32 +1,98 @@
<script> <script>
import groupBy from "lodash/fp/groupBy" import groupBy from "lodash/fp/groupBy"
import { Search, TextArea, DrawerContent } from "@budibase/bbui" import {
import { createEventDispatcher } from "svelte" Search,
import { isValid } from "@budibase/string-templates" TextArea,
DrawerContent,
Tabs,
Tab,
Body,
Layout,
} from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte"
import {
isValid,
decodeJSBinding,
encodeJSBinding,
} from "@budibase/string-templates"
import { readableToRuntimeBinding } from "builderStore/dataBinding" import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { handlebarsCompletions } from "constants/completions" import { handlebarsCompletions } from "constants/completions"
import { addToText } from "./utils" import { addHBSBinding, addJSBinding } from "./utils"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let bindableProperties export let bindableProperties
export let value = "" export let value = ""
export let valid export let valid
export let allowJS = false
let helpers = handlebarsCompletions() let helpers = handlebarsCompletions()
let getCaretPosition let getCaretPosition
let search = "" let search = ""
let initialValueJS = value?.startsWith("{{ js ")
let mode = initialValueJS ? "JavaScript" : "Handlebars"
let jsValue = initialValueJS ? value : null
let hbsValue = initialValueJS ? null : value
$: valid = isValid(readableToRuntimeBinding(bindableProperties, value)) $: usingJS = mode === "JavaScript"
$: dispatch("change", value)
$: ({ context } = groupBy("type", bindableProperties)) $: ({ context } = groupBy("type", bindableProperties))
$: searchRgx = new RegExp(search, "ig") $: searchRgx = new RegExp(search, "ig")
$: filteredColumns = context?.filter(context => { $: filteredBindings = context?.filter(context => {
return context.readableBinding.match(searchRgx) return context.readableBinding.match(searchRgx)
}) })
$: filteredHelpers = helpers?.filter(helper => { $: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx) return helper.label.match(searchRgx) || helper.description.match(searchRgx)
}) })
const updateValue = value => {
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
if (valid) {
dispatch("change", value)
}
}
// Adds a HBS helper to the expression
const addHelper = helper => {
hbsValue = addHBSBinding(value, getCaretPosition(), helper.text)
updateValue(hbsValue)
}
// Adds a data binding to the expression
const addBinding = binding => {
if (usingJS) {
let js = decodeJSBinding(jsValue)
js = addJSBinding(js, getCaretPosition(), binding.readableBinding)
jsValue = encodeJSBinding(js)
updateValue(jsValue)
} else {
hbsValue = addHBSBinding(
hbsValue,
getCaretPosition(),
binding.readableBinding
)
updateValue(hbsValue)
}
}
const onChangeMode = e => {
mode = e.detail
updateValue(mode === "JavaScript" ? jsValue : hbsValue)
}
const onChangeHBSValue = e => {
hbsValue = e.detail
updateValue(hbsValue)
}
const onChangeJSValue = e => {
jsValue = encodeJSBinding(e.detail)
updateValue(jsValue)
}
onMount(() => {
valid = isValid(readableToRuntimeBinding(bindableProperties, value))
})
</script> </script>
<DrawerContent> <DrawerContent>
@ -36,32 +102,24 @@
<div class="heading">Search</div> <div class="heading">Search</div>
<Search placeholder="Search" bind:value={search} /> <Search placeholder="Search" bind:value={search} />
</section> </section>
{#if filteredColumns?.length} {#if filteredBindings?.length}
<section> <section>
<div class="heading">Bindable Values</div> <div class="heading">Bindable Values</div>
<ul> <ul>
{#each filteredColumns as { readableBinding }} {#each filteredBindings as binding}
<li <li on:click={() => addBinding(binding)}>
on:click={() => { {binding.readableBinding}
value = addToText(value, getCaretPosition(), readableBinding)
}}
>
{readableBinding}
</li> </li>
{/each} {/each}
</ul> </ul>
</section> </section>
{/if} {/if}
{#if filteredHelpers?.length} {#if filteredHelpers?.length && !usingJS}
<section> <section>
<div class="heading">Helpers</div> <div class="heading">Helpers</div>
<ul> <ul>
{#each filteredHelpers as helper} {#each filteredHelpers as helper}
<li <li on:click={() => addHelper(helper)}>
on:click={() => {
value = addToText(value, getCaretPosition(), helper.text)
}}
>
<div class="helper"> <div class="helper">
<div class="helper__name">{helper.displayText}</div> <div class="helper__name">{helper.displayText}</div>
<div class="helper__description"> <div class="helper__description">
@ -77,24 +135,56 @@
</div> </div>
</svelte:fragment> </svelte:fragment>
<div class="main"> <div class="main">
<TextArea <Tabs selected={mode} on:select={onChangeMode}>
bind:getCaretPosition <Tab title="Handlebars">
bind:value <div class="main-content">
placeholder="Add text, or click the objects on the left to add them to the textbox." <TextArea
/> bind:getCaretPosition
{#if !valid} value={hbsValue}
<p class="syntax-error"> on:change={onChangeHBSValue}
Current Handlebars syntax is invalid, please check the guide placeholder="Add text, or click the objects on the left to add them to the textbox."
<a href="https://handlebarsjs.com/guide/">here</a> />
for more details. {#if !valid}
</p> <p class="syntax-error">
{/if} Current Handlebars syntax is invalid, please check the guide
<a href="https://handlebarsjs.com/guide/">here</a>
for more details.
</p>
{/if}
</div>
</Tab>
{#if allowJS}
<Tab title="JavaScript">
<div class="main-content">
<Layout noPadding gap="XS">
<CodeMirrorEditor
bind:getCaretPosition
height={200}
value={decodeJSBinding(jsValue)}
on:change={onChangeJSValue}
hints={context?.map(x => `$("${x.readableBinding}")`)}
/>
<Body size="S">
JavaScript expressions are executed as functions, so ensure that
your expression returns a value.
</Body>
</Layout>
</div>
</Tab>
{/if}
</Tabs>
</div> </div>
</DrawerContent> </DrawerContent>
<style> <style>
.main :global(textarea) { .main :global(textarea) {
min-height: 150px !important; min-height: 202px !important;
}
.main {
margin: calc(-1 * var(--spacing-xl));
}
.main-content {
padding: var(--spacing-s) var(--spacing-xl);
} }
.container { .container {

View File

@ -6,6 +6,7 @@
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte" import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = BindingPanel export let panel = BindingPanel
export let value = "" export let value = ""
@ -15,11 +16,14 @@
export let label export let label
export let disabled = false export let disabled = false
export let options export let options
export let allowJS = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
$: readableValue = runtimeToReadableBinding(bindings, value) $: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue $: tempValue = readableValue
$: isJS = isJSBinding(value)
const handleClose = () => { const handleClose = () => {
onChange(tempValue) onChange(tempValue)
@ -35,7 +39,7 @@
<Combobox <Combobox
{label} {label}
{disabled} {disabled}
value={readableValue} value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)} on:change={event => onChange(event.detail)}
{placeholder} {placeholder}
{options} {options}
@ -58,6 +62,7 @@
close={handleClose} close={handleClose}
on:change={event => (tempValue = event.detail)} on:change={event => (tempValue = event.detail)}
bindableProperties={bindings} bindableProperties={bindings}
{allowJS}
/> />
</Drawer> </Drawer>

View File

@ -6,6 +6,7 @@
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte" import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = BindingPanel export let panel = BindingPanel
export let value = "" export let value = ""
@ -15,12 +16,15 @@
export let label export let label
export let disabled = false export let disabled = false
export let fillWidth export let fillWidth
export let allowJS = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let bindingDrawer let bindingDrawer
let valid = true let valid = true
$: readableValue = runtimeToReadableBinding(bindings, value) $: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue $: tempValue = readableValue
$: isJS = isJSBinding(value)
const saveBinding = () => { const saveBinding = () => {
onChange(tempValue) onChange(tempValue)
@ -36,7 +40,7 @@
<Input <Input
{label} {label}
{disabled} {disabled}
value={readableValue} value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)} on:change={event => onChange(event.detail)}
{placeholder} {placeholder}
/> />
@ -60,6 +64,7 @@
value={readableValue} value={readableValue}
on:change={event => (tempValue = event.detail)} on:change={event => (tempValue = event.detail)}
bindableProperties={bindings} bindableProperties={bindings}
{allowJS}
/> />
</Drawer> </Drawer>

View File

@ -5,7 +5,7 @@
import { isValid } from "@budibase/string-templates" import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions" import { handlebarsCompletions } from "constants/completions"
import { readableToRuntimeBinding } from "builderStore/dataBinding" import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { addToText } from "./utils" import { addHBSBinding } from "./utils"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -47,7 +47,7 @@
{#each bindings as binding} {#each bindings as binding}
<li <li
on:click={() => { on:click={() => {
value = addToText(value, getCaretPosition(), binding) value = addHBSBinding(value, getCaretPosition(), binding)
}} }}
> >
<span class="binding__label">{binding.label}</span> <span class="binding__label">{binding.label}</span>
@ -71,7 +71,7 @@
{#each filteredHelpers as helper} {#each filteredHelpers as helper}
<li <li
on:click={() => { on:click={() => {
value = addToText(value, getCaretPosition(), helper.text) value = addHBSBinding(value, getCaretPosition(), helper.text)
}} }}
> >
<div class="helper"> <div class="helper">

View File

@ -1,4 +1,4 @@
export function addToText(value, caretPos, binding) { export function addHBSBinding(value, caretPos, binding) {
binding = typeof binding === "string" ? binding : binding.path binding = typeof binding === "string" ? binding : binding.path
value = value == null ? "" : value value = value == null ? "" : value
if (!value.includes("{{") && !value.includes("}}")) { if (!value.includes("{{") && !value.includes("}}")) {
@ -14,3 +14,18 @@ export function addToText(value, caretPos, binding) {
} }
return value return value
} }
export function addJSBinding(value, caretPos, binding) {
binding = typeof binding === "string" ? binding : binding.path
value = value == null ? "" : value
binding = `$("${binding}")`
if (caretPos.start) {
value =
value.substring(0, caretPos.start) +
binding +
value.substring(caretPos.end, value.length)
} else {
value += binding
}
return value
}

View File

@ -0,0 +1,15 @@
<script>
import { Input } from "@budibase/bbui"
import { isJSBinding } from "@budibase/string-templates"
export let value
$: isJS = isJSBinding(value)
</script>
<Input
{...$$props}
value={isJS ? "(JavaScript function)" : value}
readonly={isJS}
on:change
/>

View File

@ -107,6 +107,7 @@
value={safeValue} value={safeValue}
on:change={e => (tempValue = e.detail)} on:change={e => (tempValue = e.detail)}
bindableProperties={bindings} bindableProperties={bindings}
allowJS
/> />
</Drawer> </Drawer>
{/if} {/if}

View File

@ -1,4 +1,4 @@
import { Checkbox, Input, Select, Stepper } from "@budibase/bbui" import { Checkbox, Select, Stepper } from "@budibase/bbui"
import DataSourceSelect from "./DataSourceSelect.svelte" import DataSourceSelect from "./DataSourceSelect.svelte"
import DataProviderSelect from "./DataProviderSelect.svelte" import DataProviderSelect from "./DataProviderSelect.svelte"
import EventsEditor from "./EventsEditor" import EventsEditor from "./EventsEditor"
@ -15,6 +15,7 @@ import URLSelect from "./URLSelect.svelte"
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte" import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./FormFieldSelect.svelte" import FormFieldSelect from "./FormFieldSelect.svelte"
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte" import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
import Input from "./Input.svelte"
const componentMap = { const componentMap = {
text: Input, text: Input,

View File

@ -21,12 +21,15 @@
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
import { datasources, integrations, queries } from "stores/backend" import { datasources, integrations, queries } from "stores/backend"
import { capitalise } from "../../helpers" import { capitalise } from "../../helpers"
import CodeMirrorEditor from "components/common/CodeMirrorEditor.svelte"
export let query export let query
export let fields = [] export let fields = []
let parameters let parameters
let data = [] let data = []
const transformerDocs =
"https://docs.budibase.com/building-apps/data/transformers"
const typeOptions = [ const typeOptions = [
{ label: "Text", value: "STRING" }, { label: "Text", value: "STRING" },
{ label: "Number", value: "NUMBER" }, { label: "Number", value: "NUMBER" },
@ -52,6 +55,11 @@
$: readQuery = query.queryVerb === "read" || query.readable $: readQuery = query.queryVerb === "read" || query.readable
$: queryInvalid = !query.name || (readQuery && data.length === 0) $: queryInvalid = !query.name || (readQuery && data.length === 0)
// seed the transformer
if (query && !query.transformer) {
query.transformer = "return data"
}
function newField() { function newField() {
fields = [...fields, {}] fields = [...fields, {}]
} }
@ -74,6 +82,7 @@
const response = await api.post(`/api/queries/preview`, { const response = await api.post(`/api/queries/preview`, {
fields: query.fields, fields: query.fields,
queryVerb: query.queryVerb, queryVerb: query.queryVerb,
transformer: query.transformer,
parameters: query.parameters.reduce( parameters: query.parameters.reduce(
(acc, next) => ({ (acc, next) => ({
...acc, ...acc,
@ -160,12 +169,34 @@
<IntegrationQueryEditor <IntegrationQueryEditor
{datasource} {datasource}
{query} {query}
height={300} height={200}
schema={queryConfig[query.queryVerb]} schema={queryConfig[query.queryVerb]}
bind:parameters bind:parameters
/> />
<Divider /> <Divider />
</div> </div>
<div class="config">
<div class="help-heading">
<Heading size="S">Transformer</Heading>
<Icon
on:click={() => window.open(transformerDocs)}
hoverable
name="Help"
size="L"
/>
</div>
<Body size="S"
>Add a JavaScript function to transform the query result.</Body
>
<CodeMirrorEditor
height={200}
label="Transformer"
value={query.transformer}
resize="vertical"
on:change={e => (query.transformer = e.detail)}
/>
<Divider />
</div>
<div class="viewer-controls"> <div class="viewer-controls">
<Heading size="S">Results</Heading> <Heading size="S">Results</Heading>
<ButtonGroup> <ButtonGroup>
@ -220,6 +251,7 @@
display: grid; display: grid;
grid-gap: var(--spacing-s); grid-gap: var(--spacing-s);
} }
.config-field { .config-field {
display: grid; display: grid;
grid-template-columns: 20% 1fr; grid-template-columns: 20% 1fr;
@ -227,6 +259,11 @@
align-items: center; align-items: center;
} }
.help-heading {
display: flex;
justify-content: space-between;
}
.field { .field {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 5%; grid-template-columns: 1fr 1fr 5%;

View File

@ -1,12 +1,22 @@
import CodeMirror from "codemirror" import CodeMirror from "codemirror"
import "codemirror/lib/codemirror.css" import "codemirror/lib/codemirror.css"
import "codemirror/theme/tomorrow-night-eighties.css"
import "codemirror/addon/hint/show-hint.css" // Modes
import "codemirror/theme/neo.css" import "codemirror/mode/javascript/javascript"
import "codemirror/mode/sql/sql" import "codemirror/mode/sql/sql"
import "codemirror/mode/css/css" import "codemirror/mode/css/css"
import "codemirror/mode/handlebars/handlebars" import "codemirror/mode/handlebars/handlebars"
import "codemirror/mode/javascript/javascript"
// Hints
import "codemirror/addon/hint/show-hint" import "codemirror/addon/hint/show-hint"
import "codemirror/addon/hint/show-hint.css"
// Theming
import "codemirror/theme/tomorrow-night-eighties.css"
// Functional addons
import "codemirror/addon/selection/active-line"
import "codemirror/addon/edit/closebrackets"
import "codemirror/addon/edit/matchbrackets"
export default CodeMirror export default CodeMirror

View File

@ -150,7 +150,6 @@
showCancelButton={false} showCancelButton={false}
showCloseIcon={false} showCloseIcon={false}
> >
<Body size="M">Select a template below, or start from scratch.</Body>
<TemplateList <TemplateList
onSelect={selected => { onSelect={selected => {
if (!selected) { if (!selected) {

View File

@ -1,5 +1,5 @@
<script> <script>
import { Heading, Layout, Icon } from "@budibase/bbui" import { Heading, Layout, Icon, Body } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import api from "builderStore/api" import api from "builderStore/api"
@ -7,6 +7,7 @@
async function fetchTemplates() { async function fetchTemplates() {
const response = await api.get("/api/templates?type=app") const response = await api.get("/api/templates?type=app")
console.log("Responded")
return await response.json() return await response.json()
} }
@ -19,6 +20,11 @@
<Spinner size="30" /> <Spinner size="30" />
</div> </div>
{:then templates} {:then templates}
{#if templates?.length > 0}
<Body size="M">Select a template below, or start from scratch.</Body>
{:else}
<Body size="M">Start your app from scratch below.</Body>
{/if}
<div class="templates"> <div class="templates">
{#each templates as template} {#each templates as template}
<div class="template" on:click={() => onSelect(template)}> <div class="template" on:click={() => onSelect(template)}>

View File

@ -48,7 +48,6 @@ export const fetchTableData = opts => {
const fetchPage = async bookmark => { const fetchPage = async bookmark => {
lastBookmark = bookmark lastBookmark = bookmark
const { tableId, limit, sortColumn, sortOrder, paginate } = options const { tableId, limit, sortColumn, sortOrder, paginate } = options
store.update($store => ({ ...$store, loading: true }))
const res = await API.post(`/api/${options.tableId}/search`, { const res = await API.post(`/api/${options.tableId}/search`, {
tableId, tableId,
query, query,
@ -59,7 +58,6 @@ export const fetchTableData = opts => {
paginate, paginate,
bookmark, bookmark,
}) })
store.update($store => ({ ...$store, loading: false, loaded: true }))
return await res.json() return await res.json()
} }
@ -103,7 +101,7 @@ export const fetchTableData = opts => {
if (!schema) { if (!schema) {
return return
} }
store.update($store => ({ ...$store, schema })) store.update($store => ({ ...$store, schema, loading: true }))
// Work out what sort type to use // Work out what sort type to use
if (!sortColumn || !schema[sortColumn]) { if (!sortColumn || !schema[sortColumn]) {
@ -135,6 +133,7 @@ export const fetchTableData = opts => {
} }
// Fetch next page // Fetch next page
store.update($store => ({ ...$store, loading: true }))
const page = await fetchPage(state.bookmarks[state.pageNumber + 1]) const page = await fetchPage(state.bookmarks[state.pageNumber + 1])
// Update state // Update state
@ -148,6 +147,7 @@ export const fetchTableData = opts => {
pageNumber: pageNumber + 1, pageNumber: pageNumber + 1,
rows: page.rows, rows: page.rows,
bookmarks, bookmarks,
loading: false,
} }
}) })
} }
@ -160,6 +160,7 @@ export const fetchTableData = opts => {
} }
// Fetch previous page // Fetch previous page
store.update($store => ({ ...$store, loading: true }))
const page = await fetchPage(state.bookmarks[state.pageNumber - 1]) const page = await fetchPage(state.bookmarks[state.pageNumber - 1])
// Update state // Update state
@ -168,6 +169,7 @@ export const fetchTableData = opts => {
...$store, ...$store,
pageNumber: $store.pageNumber - 1, pageNumber: $store.pageNumber - 1,
rows: page.rows, rows: page.rows,
loading: false,
} }
}) })
} }

View File

@ -1,6 +1,7 @@
<script> <script>
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import { admin } from "stores/portal"
import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte" import CreateDatasourceModal from "components/backend/DatasourceNavigator/modals/CreateDatasourceModal.svelte"
import { datasources } from "stores/backend" import { datasources } from "stores/backend"
@ -10,7 +11,7 @@
$datasources.list.length > 1 $datasources.list.length > 1
onMount(() => { onMount(() => {
if (!setupComplete) { if (!setupComplete && !$admin.isDev) {
modal.show() modal.show()
} else { } else {
$goto("./table") $goto("./table")

View File

@ -34,6 +34,12 @@
const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED const publishedAppsOnly = app => app.status === AppStatus.DEPLOYED
$: publishedApps = $apps.filter(publishedAppsOnly) $: publishedApps = $apps.filter(publishedAppsOnly)
$: userApps = $auth.user?.builder?.global
? publishedApps
: publishedApps.filter(app =>
Object.keys($auth.user?.roles).includes(app.prodId)
)
</script> </script>
{#if $auth.user && loaded} {#if $auth.user && loaded}
@ -82,11 +88,11 @@
</Body> </Body>
</Layout> </Layout>
<Divider /> <Divider />
{#if publishedApps.length} {#if userApps.length}
<Heading>Apps</Heading> <Heading>Apps</Heading>
<div class="group"> <div class="group">
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
{#each publishedApps as app, idx (app.appId)} {#each userApps as app, idx (app.appId)}
<a class="app" target="_blank" href={`/${app.prodId}`}> <a class="app" target="_blank" href={`/${app.prodId}`}>
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<div class="app-info"> <div class="app-info">

View File

@ -34,9 +34,13 @@
role: {}, role: {},
} }
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : "BASIC" const noRoleSchema = {
name: { displayName: "App" },
}
$: defaultRoleId = $userFetch?.data?.builder?.global ? "ADMIN" : ""
// Merge the Apps list and the roles response to get something that makes sense for the table // Merge the Apps list and the roles response to get something that makes sense for the table
$: appList = Object.keys($apps?.data).map(id => { $: allAppList = Object.keys($apps?.data).map(id => {
const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId const roleId = $userFetch?.data?.roles?.[id] || defaultRoleId
const role = $apps?.data?.[id].roles.find(role => role._id === roleId) const role = $apps?.data?.[id].roles.find(role => role._id === roleId)
return { return {
@ -45,6 +49,15 @@
role: [role], role: [role],
} }
}) })
$: appList = allAppList.filter(app => !!app.role[0])
$: noRoleAppList = allAppList
.filter(app => !app.role[0])
.map(app => {
delete app.role
return app
})
let selectedApp let selectedApp
const userFetch = fetchData(`/api/global/users/${userId}`) const userFetch = fetchData(`/api/global/users/${userId}`)
@ -173,6 +186,7 @@
<Divider size="S" /> <Divider size="S" />
<Layout gap="S" noPadding> <Layout gap="S" noPadding>
<Heading size="S">Configure roles</Heading> <Heading size="S">Configure roles</Heading>
<Body>Specify a role to grant access to an app.</Body>
<Table <Table
on:click={openUpdateRolesModal} on:click={openUpdateRolesModal}
schema={roleSchema} schema={roleSchema}
@ -183,6 +197,21 @@
customRenderers={[{ column: "role", component: TagsRenderer }]} customRenderers={[{ column: "role", component: TagsRenderer }]}
/> />
</Layout> </Layout>
<Layout gap="S" noPadding>
<Heading size="XS">No Access</Heading>
<Body
>Apps do not appear in the users portal. Public pages may still be viewed
if visited directly.</Body
>
<Table
on:click={openUpdateRolesModal}
schema={noRoleSchema}
data={noRoleAppList}
allowEditColumns={false}
allowEditRows={false}
allowSelectRows={false}
/>
</Layout>
<Divider size="S" /> <Divider size="S" />
<Layout gap="XS" noPadding> <Layout gap="XS" noPadding>
<Heading size="S">Delete user</Heading> <Heading size="S">Delete user</Heading>

View File

@ -6,22 +6,38 @@
export let app export let app
export let user export let user
const NO_ACCESS = "NO_ACCESS"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const roles = app.roles const roles = app.roles
let options = roles let options = roles.map(role => ({ value: role._id, label: role.name }))
.filter(role => role._id !== "PUBLIC") options.push({ value: NO_ACCESS, label: "No Access" })
.map(role => ({ value: role._id, label: role.name }))
let selectedRole = user?.roles?.[app?._id] let selectedRole = user?.roles?.[app?._id]
async function updateUserRoles() { async function updateUserRoles() {
const res = await users.save({ let res
...user, if (selectedRole === NO_ACCESS) {
roles: { // remove the user role
...user.roles, const filteredRoles = { ...user.roles }
[app._id]: selectedRole, delete filteredRoles[app?._id]
}, res = await users.save({
}) ...user,
roles: {
...filteredRoles,
},
})
} else {
// add the user role
res = await users.save({
...user,
roles: {
...user.roles,
[app._id]: selectedRole,
},
})
}
if (res.status === 400) { if (res.status === 400) {
notifications.error("Failed to update role") notifications.error("Failed to update role")
} else { } else {

View File

@ -7,6 +7,7 @@ export function createAdminStore() {
loaded: false, loaded: false,
multiTenancy: false, multiTenancy: false,
cloud: false, cloud: false,
isDev: false,
disableAccountPortal: false, disableAccountPortal: false,
accountPortalUrl: "", accountPortalUrl: "",
importComplete: false, importComplete: false,
@ -62,6 +63,7 @@ export function createAdminStore() {
let cloud = false let cloud = false
let disableAccountPortal = false let disableAccountPortal = false
let accountPortalUrl = "" let accountPortalUrl = ""
let isDev = false
try { try {
const response = await api.get(`/api/system/environment`) const response = await api.get(`/api/system/environment`)
const json = await response.json() const json = await response.json()
@ -69,6 +71,7 @@ export function createAdminStore() {
cloud = json.cloud cloud = json.cloud
disableAccountPortal = json.disableAccountPortal disableAccountPortal = json.disableAccountPortal
accountPortalUrl = json.accountPortalUrl accountPortalUrl = json.accountPortalUrl
isDev = json.isDev
} catch (err) { } catch (err) {
// just let it stay disabled // just let it stay disabled
} }
@ -77,6 +80,7 @@ export function createAdminStore() {
store.cloud = cloud store.cloud = cloud
store.disableAccountPortal = disableAccountPortal store.disableAccountPortal = disableAccountPortal
store.accountPortalUrl = accountPortalUrl store.accountPortalUrl = accountPortalUrl
store.isDev = isDev
return store return store
}) })
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"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

@ -2386,7 +2386,7 @@
}, },
"dataprovider": { "dataprovider": {
"name": "Data Provider", "name": "Data Provider",
"info": "Pagination is only available for data stored in internal tables.", "info": "Pagination is only available for data stored in tables.",
"icon": "Data", "icon": "Data",
"illegalChildren": ["section"], "illegalChildren": ["section"],
"hasChildren": true, "hasChildren": true,

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"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": "^0.9.167-alpha.1", "@budibase/bbui": "^0.9.167-alpha.12",
"@budibase/standard-components": "^0.9.139", "@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.167-alpha.1", "@budibase/string-templates": "^0.9.167-alpha.12",
"regexparam": "^1.3.0", "regexparam": "^1.3.0",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5" "svelte-spa-router": "^3.0.5"

View File

@ -16,7 +16,10 @@
/* Buttons */ /* Buttons */
--spectrum-semantic-cta-color-background-default: var(--primaryColor); --spectrum-semantic-cta-color-background-default: var(--primaryColor);
--spectrum-semantic-cta-color-background-hover: var(--primaryColorHover); --spectrum-semantic-cta-color-background-hover: var(--primaryColorHover);
--spectrum-button-primary-s-border-radius: var(--buttonBorderRadius);
--spectrum-button-primary-m-border-radius: var(--buttonBorderRadius); --spectrum-button-primary-m-border-radius: var(--buttonBorderRadius);
--spectrum-button-primary-l-border-radius: var(--buttonBorderRadius);
--spectrum-button-primary-xl-border-radius: var(--buttonBorderRadius);
/* Loading spinners */ /* Loading spinners */
--spectrum-progresscircle-medium-track-fill-color: var(--primaryColor); --spectrum-progresscircle-medium-track-fill-color: var(--primaryColor);

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -68,9 +68,9 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.167-alpha.1", "@budibase/auth": "^0.9.167-alpha.12",
"@budibase/client": "^0.9.167-alpha.1", "@budibase/client": "^0.9.167-alpha.12",
"@budibase/string-templates": "^0.9.167-alpha.1", "@budibase/string-templates": "^0.9.167-alpha.12",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0", "@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1", "@sendgrid/mail": "7.1.1",
@ -119,6 +119,7 @@
"to-json-schema": "0.2.5", "to-json-schema": "0.2.5",
"uuid": "3.3.2", "uuid": "3.3.2",
"validate.js": "0.13.1", "validate.js": "0.13.1",
"vm2": "^3.9.3",
"yargs": "13.2.4", "yargs": "13.2.4",
"zlib": "1.0.5" "zlib": "1.0.5"
}, },

View File

@ -7,11 +7,13 @@ const { clearLock } = require("../../utilities/redis")
const { Replication } = require("@budibase/auth").db const { Replication } = require("@budibase/auth").db
const { DocumentTypes } = require("../../db/utils") const { DocumentTypes } = require("../../db/utils")
async function redirect(ctx, method) { async function redirect(ctx, method, path = "global") {
const { devPath } = ctx.params const { devPath } = ctx.params
const queryString = ctx.originalUrl.split("?")[1] || "" const queryString = ctx.originalUrl.split("?")[1] || ""
const response = await fetch( const response = await fetch(
checkSlashesInUrl(`${env.WORKER_URL}/api/global/${devPath}?${queryString}`), checkSlashesInUrl(
`${env.WORKER_URL}/api/${path}/${devPath}?${queryString}`
),
request( request(
ctx, ctx,
{ {
@ -41,16 +43,22 @@ async function redirect(ctx, method) {
ctx.cookies ctx.cookies
} }
exports.redirectGet = async ctx => { exports.buildRedirectGet = path => {
await redirect(ctx, "GET") return async ctx => {
await redirect(ctx, "GET", path)
}
} }
exports.redirectPost = async ctx => { exports.buildRedirectPost = path => {
await redirect(ctx, "POST") return async ctx => {
await redirect(ctx, "POST", path)
}
} }
exports.redirectDelete = async ctx => { exports.buildRedirectDelete = path => {
await redirect(ctx, "DELETE") return async ctx => {
await redirect(ctx, "DELETE", path)
}
} }
exports.clearLock = async ctx => { exports.clearLock = async ctx => {

View File

@ -4,6 +4,7 @@ const { generateQueryID, getQueryParams } = require("../../db/utils")
const { integrations } = require("../../integrations") const { integrations } = require("../../integrations")
const { BaseQueryVerbs } = require("../../constants") const { BaseQueryVerbs } = require("../../constants")
const env = require("../../environment") const env = require("../../environment")
const ScriptRunner = require("../../utilities/scriptRunner")
// simple function to append "readable" to all read queries // simple function to append "readable" to all read queries
function enrichQueries(input) { function enrichQueries(input) {
@ -28,12 +29,39 @@ function formatResponse(resp) {
resp = { response: resp } resp = { response: resp }
} }
} }
if (!Array.isArray(resp)) {
resp = [resp]
}
return resp return resp
} }
async function runAndTransform(
integration,
queryVerb,
enrichedQuery,
transformer
) {
let rows = formatResponse(await integration[queryVerb](enrichedQuery))
// transform as required
if (transformer) {
const runner = new ScriptRunner(transformer, { data: rows })
rows = runner.execute()
}
// needs to an array for next step
if (!Array.isArray(rows)) {
rows = [rows]
}
// map into JSON if just raw primitive here
if (rows.find(row => typeof row !== "object")) {
rows = rows.map(value => ({ value }))
}
// get all the potential fields in the schema
let keys = rows.flatMap(Object.keys)
return { rows, keys }
}
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const db = new CouchDB(ctx.appId) const db = new CouchDB(ctx.appId)
@ -122,15 +150,16 @@ exports.preview = async function (ctx) {
ctx.throw(400, "Integration type does not exist.") ctx.throw(400, "Integration type does not exist.")
} }
const { fields, parameters, queryVerb } = ctx.request.body const { fields, parameters, queryVerb, transformer } = ctx.request.body
const enrichedQuery = await enrichQueryFields(fields, parameters) const enrichedQuery = await enrichQueryFields(fields, parameters)
const integration = new Integration(datasource.config) const integration = new Integration(datasource.config)
const rows = formatResponse(await integration[queryVerb](enrichedQuery))
// get all the potential fields in the schema const { rows, keys } = await runAndTransform(
const keys = rows.flatMap(Object.keys) integration,
queryVerb,
enrichedQuery,
transformer
)
ctx.body = { ctx.body = {
rows, rows,
@ -158,10 +187,16 @@ exports.execute = async function (ctx) {
query.fields, query.fields,
ctx.request.body.parameters ctx.request.body.parameters
) )
const integration = new Integration(datasource.config) const integration = new Integration(datasource.config)
// call the relevant CRUD method on the integration class // call the relevant CRUD method on the integration class
ctx.body = formatResponse(await integration[query.queryVerb](enrichedQuery)) const { rows } = await runAndTransform(
integration,
query.queryVerb,
enrichedQuery,
query.transformer
)
ctx.body = rows
// cleanup // cleanup
if (integration.end) { if (integration.end) {
integration.end() integration.end()

View File

@ -1,24 +1,9 @@
const fetch = require("node-fetch") const ScriptRunner = require("../../utilities/scriptRunner")
const vm = require("vm")
class ScriptExecutor {
constructor(body) {
const code = `let fn = () => {\n${body.script}\n}; out = fn();`
this.script = new vm.Script(code)
this.context = vm.createContext(body.context)
this.context.fetch = fetch
}
execute() {
this.script.runInContext(this.context)
return this.context.out
}
}
exports.execute = async function (ctx) { exports.execute = async function (ctx) {
const executor = new ScriptExecutor(ctx.request.body) const { script, context } = ctx.request.body
const runner = new ScriptRunner(script, context)
ctx.body = executor.execute() ctx.body = runner.execute()
} }
exports.save = async function (ctx) { exports.save = async function (ctx) {

View File

@ -91,6 +91,9 @@ exports.save = async function (ctx) {
for (let propKey of Object.keys(tableToSave.schema)) { for (let propKey of Object.keys(tableToSave.schema)) {
let column = tableToSave.schema[propKey] let column = tableToSave.schema[propKey]
let oldColumn = oldTable.schema[propKey] let oldColumn = oldTable.schema[propKey]
if (oldColumn && oldColumn.type === "internal") {
oldColumn.type = "auto"
}
if (oldColumn && oldColumn.type !== column.type) { if (oldColumn && oldColumn.type !== column.type) {
ctx.throw(400, "Cannot change the type of a column") ctx.throw(400, "Cannot change the type of a column")
} }

View File

@ -1,5 +1,4 @@
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
const linkRows = require("../../../db/linkedRows")
const csvParser = require("../../../utilities/csvParser") const csvParser = require("../../../utilities/csvParser")
const { const {
getRowParams, getRowParams,
@ -93,19 +92,10 @@ exports.handleDataImport = async (appId, user, table, dataImport) => {
} }
} }
// make sure link rows are up to date finalData.push(row)
finalData.push(
linkRows.updateLinks({
appId,
eventType: linkRows.EventType.ROW_SAVE,
row,
tableId: row.tableId,
table,
})
)
} }
await db.bulkDocs(await Promise.all(finalData)) await db.bulkDocs(finalData)
let response = await db.put(table) let response = await db.put(table)
table._rev = response._rev table._rev = response._rev
} }

View File

@ -7,11 +7,23 @@ const DEFAULT_TEMPLATES_BUCKET =
exports.fetch = async function (ctx) { exports.fetch = async function (ctx) {
const { type = "app" } = ctx.query const { type = "app" } = ctx.query
const response = await fetch( let response,
`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json` error = false
) try {
const json = await response.json() response = await fetch(`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json`)
ctx.body = Object.values(json.templates[type]) if (response.status !== 200) {
error = true
}
} catch (err) {
error = true
}
// if there is an error, simply return no templates
if (!error && response) {
const json = await response.json()
ctx.body = Object.values(json.templates[type])
} else {
ctx.body = []
}
} }
// can't currently test this, have to ignore from coverage // can't currently test this, have to ignore from coverage

View File

@ -6,11 +6,16 @@ const { BUILDER } = require("@budibase/auth/permissions")
const router = Router() const router = Router()
if (env.isDev() || env.isTest()) { function redirectPath(path) {
router router
.get("/api/global/:devPath(.*)", controller.redirectGet) .get(`/api/${path}/:devPath(.*)`, controller.buildRedirectGet(path))
.post("/api/global/:devPath(.*)", controller.redirectPost) .post(`/api/${path}/:devPath(.*)`, controller.buildRedirectPost(path))
.delete("/api/global/:devPath(.*)", controller.redirectDelete) .delete(`/api/${path}/:devPath(.*)`, controller.buildRedirectDelete(path))
}
if (env.isDev() || env.isTest()) {
redirectPath("global")
redirectPath("system")
} }
router router

View File

@ -31,7 +31,8 @@ function generateQueryValidation() {
})), })),
queryVerb: Joi.string().allow().required(), queryVerb: Joi.string().allow().required(),
extra: Joi.object().optional(), extra: Joi.object().optional(),
schema: Joi.object({}).required().unknown(true) schema: Joi.object({}).required().unknown(true),
transformer: Joi.string().optional(),
})) }))
} }
@ -42,6 +43,7 @@ function generateQueryPreviewValidation() {
queryVerb: Joi.string().allow().required(), queryVerb: Joi.string().allow().required(),
extra: Joi.object().optional(), extra: Joi.object().optional(),
datasourceId: Joi.string().required(), datasourceId: Joi.string().required(),
transformer: Joi.string().optional(),
parameters: Joi.object({}).required().unknown(true) parameters: Joi.object({}).required().unknown(true)
})) }))
} }

View File

@ -3,7 +3,6 @@ const createRow = require("./steps/createRow")
const updateRow = require("./steps/updateRow") const updateRow = require("./steps/updateRow")
const deleteRow = require("./steps/deleteRow") const deleteRow = require("./steps/deleteRow")
const executeScript = require("./steps/executeScript") const executeScript = require("./steps/executeScript")
const bash = require("./steps/bash")
const executeQuery = require("./steps/executeQuery") const executeQuery = require("./steps/executeQuery")
const outgoingWebhook = require("./steps/outgoingWebhook") const outgoingWebhook = require("./steps/outgoingWebhook")
const serverLog = require("./steps/serverLog") const serverLog = require("./steps/serverLog")
@ -14,6 +13,7 @@ const integromat = require("./steps/integromat")
let filter = require("./steps/filter") let filter = require("./steps/filter")
let delay = require("./steps/delay") let delay = require("./steps/delay")
let queryRow = require("./steps/queryRows") let queryRow = require("./steps/queryRows")
const env = require("../environment")
const ACTION_IMPLS = { const ACTION_IMPLS = {
SEND_EMAIL_SMTP: sendSmtpEmail.run, SEND_EMAIL_SMTP: sendSmtpEmail.run,
@ -22,7 +22,6 @@ const ACTION_IMPLS = {
DELETE_ROW: deleteRow.run, DELETE_ROW: deleteRow.run,
OUTGOING_WEBHOOK: outgoingWebhook.run, OUTGOING_WEBHOOK: outgoingWebhook.run,
EXECUTE_SCRIPT: executeScript.run, EXECUTE_SCRIPT: executeScript.run,
EXECUTE_BASH: bash.run,
EXECUTE_QUERY: executeQuery.run, EXECUTE_QUERY: executeQuery.run,
SERVER_LOG: serverLog.run, SERVER_LOG: serverLog.run,
DELAY: delay.run, DELAY: delay.run,
@ -42,7 +41,6 @@ const ACTION_DEFINITIONS = {
OUTGOING_WEBHOOK: outgoingWebhook.definition, OUTGOING_WEBHOOK: outgoingWebhook.definition,
EXECUTE_SCRIPT: executeScript.definition, EXECUTE_SCRIPT: executeScript.definition,
EXECUTE_QUERY: executeQuery.definition, EXECUTE_QUERY: executeQuery.definition,
EXECUTE_BASH: bash.definition,
SERVER_LOG: serverLog.definition, SERVER_LOG: serverLog.definition,
DELAY: delay.definition, DELAY: delay.definition,
FILTER: filter.definition, FILTER: filter.definition,
@ -54,6 +52,15 @@ const ACTION_DEFINITIONS = {
integromat: integromat.definition, integromat: integromat.definition,
} }
// don't add the bash script/definitions unless in self host
// the fact this isn't included in any definitions means it cannot be
// ran at all
if (env.SELF_HOSTED) {
const bash = require("./steps/bash")
ACTION_IMPLS["EXECUTE_BASH"] = bash.run
ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
}
/* istanbul ignore next */ /* istanbul ignore next */
exports.getAction = async function (actionName) { exports.getAction = async function (actionName) {
if (ACTION_IMPLS[actionName] != null) { if (ACTION_IMPLS[actionName] != null) {

View File

@ -10,6 +10,7 @@ const env = require("../environment")
const usage = require("../utilities/usageQuota") const usage = require("../utilities/usageQuota")
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
const STOPPED_STATUS = { success: false, status: "STOPPED" }
/** /**
* The automation orchestrator is a class responsible for executing automations. * The automation orchestrator is a class responsible for executing automations.
@ -68,7 +69,13 @@ class Orchestrator {
async execute() { async execute() {
let automation = this._automation let automation = this._automation
const app = await this.getApp() const app = await this.getApp()
let stopped = false
for (let step of automation.definition.steps) { for (let step of automation.definition.steps) {
// execution stopped, record state for that
if (stopped) {
this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS)
continue
}
let stepFn = await this.getStepFunctionality(step.stepId) let stepFn = await this.getStepFunctionality(step.stepId)
step.inputs = await processObject(step.inputs, this._context) step.inputs = await processObject(step.inputs, this._context)
step.inputs = automationUtils.cleanInputValues( step.inputs = automationUtils.cleanInputValues(
@ -86,10 +93,17 @@ class Orchestrator {
context: this._context, context: this._context,
}) })
}) })
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break
}
this._context.steps.push(outputs) this._context.steps.push(outputs)
// if filter causes us to stop execution don't break the loop, set a var
// so that we can finish iterating through the steps and record that it stopped
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
stopped = true
this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
...outputs,
...STOPPED_STATUS,
})
continue
}
this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs) this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs)
} catch (err) { } catch (err) {
console.error(`Automation error - ${step.stepId} - ${err}`) console.error(`Automation error - ${step.stepId} - ${err}`)
@ -99,7 +113,7 @@ class Orchestrator {
// Increment quota for automation runs // Increment quota for automation runs
if (!env.SELF_HOSTED && !isDevAppID(this._appId)) { if (!env.SELF_HOSTED && !isDevAppID(this._appId)) {
usage.update(usage.Properties.AUTOMATION, 1) await usage.update(usage.Properties.AUTOMATION, 1)
} }
return this.executionOutput return this.executionOutput
} }

View File

@ -42,6 +42,7 @@ exports.FieldTypes = {
FORMULA: "formula", FORMULA: "formula",
AUTO: "auto", AUTO: "auto",
JSON: "json", JSON: "json",
INTERNAL: "internal",
} }
exports.RelationshipTypes = { exports.RelationshipTypes = {

View File

@ -98,7 +98,9 @@ function addFilters(
} }
function addRelationships( function addRelationships(
knex: Knex,
query: KnexQuery, query: KnexQuery,
fields: string | string[],
fromTable: string, fromTable: string,
relationships: RelationshipsJson[] | undefined relationships: RelationshipsJson[] | undefined
): KnexQuery { ): KnexQuery {
@ -114,7 +116,7 @@ function addRelationships(
query = query.leftJoin( query = query.leftJoin(
toTable, toTable,
`${fromTable}.${from}`, `${fromTable}.${from}`,
`${relationship.tableName}.${to}` `${toTable}.${to}`
) )
} else { } else {
const throughTable = relationship.through const throughTable = relationship.through
@ -130,7 +132,7 @@ function addRelationships(
.leftJoin(toTable, `${toTable}.${toPrimary}`, `${throughTable}.${to}`) .leftJoin(toTable, `${toTable}.${toPrimary}`, `${throughTable}.${to}`)
} }
} }
return query return query.limit(BASE_LIMIT)
} }
function buildCreate( function buildCreate(
@ -199,7 +201,7 @@ function buildRead(knex: Knex, json: QueryJson, limit: number): KnexQuery {
[tableName]: query, [tableName]: query,
}).select(selectStatement) }).select(selectStatement)
// handle joins // handle joins
return addRelationships(preQuery, tableName, relationships) return addRelationships(knex, preQuery, selectStatement, tableName, relationships)
} }
function buildUpdate( function buildUpdate(

View File

@ -27,7 +27,7 @@ describe("MongoDB Integration", () => {
const body = { const body = {
name: "Hello" name: "Hello"
} }
const response = await config.integration.create({ await config.integration.create({
index: indexName, index: indexName,
json: body, json: body,
extra: { collection: 'testCollection', actionTypes: 'insertOne'} extra: { collection: 'testCollection', actionTypes: 'insertOne'}
@ -54,7 +54,7 @@ describe("MongoDB Integration", () => {
}, },
extra: { collection: 'testCollection', actionTypes: 'deleteOne'} extra: { collection: 'testCollection', actionTypes: 'deleteOne'}
} }
const response = await config.integration.delete(query) await config.integration.delete(query)
expect(config.integration.client.deleteOne).toHaveBeenCalledWith(query.json) expect(config.integration.client.deleteOne).toHaveBeenCalledWith(query.json)
}) })
@ -65,7 +65,7 @@ describe("MongoDB Integration", () => {
}, },
extra: { collection: 'testCollection', actionTypes: 'updateOne'} extra: { collection: 'testCollection', actionTypes: 'updateOne'}
} }
const response = await config.integration.update(query) await config.integration.update(query)
expect(config.integration.client.updateOne).toHaveBeenCalledWith(query.json) expect(config.integration.client.updateOne).toHaveBeenCalledWith(query.json)
}) })
@ -75,10 +75,14 @@ describe("MongoDB Integration", () => {
const query = { const query = {
extra: { collection: 'testCollection', actionTypes: 'deleteOne'} extra: { collection: 'testCollection', actionTypes: 'deleteOne'}
} }
// Weird, need to do an IIFE for jest to recognize that it throws
expect(() => config.integration.read(query)()).toThrow(expect.any(Object))
let error = null
try {
await config.integration.read(query)
} catch (err) {
error = err
}
expect(error).toBeDefined()
restore() restore()
}) })
}) })

View File

@ -45,7 +45,7 @@ module.exports = async (ctx, next) => {
const globalUser = await getCachedSelf(ctx, requestAppId) const globalUser = await getCachedSelf(ctx, requestAppId)
appId = requestAppId appId = requestAppId
// retrieving global user gets the right role // retrieving global user gets the right role
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC roleId = globalUser.roleId || roleId
} }
// nothing more to do // nothing more to do

View File

@ -127,8 +127,8 @@ describe("Current app middleware", () => {
} else { } else {
expect(cookieFn).not.toHaveBeenCalled() expect(cookieFn).not.toHaveBeenCalled()
} }
expect(config.ctx.roleId).toEqual("BASIC") expect(config.ctx.roleId).toEqual("PUBLIC")
expect(config.ctx.user.role._id).toEqual("BASIC") expect(config.ctx.user.role._id).toEqual("PUBLIC")
expect(config.ctx.appId).toEqual("app_test") expect(config.ctx.appId).toEqual("app_test")
expect(config.next).toHaveBeenCalled() expect(config.next).toHaveBeenCalled()
} }
@ -163,7 +163,7 @@ describe("Current app middleware", () => {
return "app_test" return "app_test"
}, },
setCookie: jest.fn(), setCookie: jest.fn(),
getCookie: () => ({appId: "app_test", roleId: "BASIC"}), getCookie: () => ({appId: "app_test", roleId: "PUBLIC"}),
}, },
constants: { Cookies: {} }, constants: { Cookies: {} },
})) }))

View File

@ -26,7 +26,7 @@ exports.updateAppRole = (appId, user) => {
if (!user.roleId && user.builder && user.builder.global) { if (!user.roleId && user.builder && user.builder.global) {
user.roleId = BUILTIN_ROLE_IDS.ADMIN user.roleId = BUILTIN_ROLE_IDS.ADMIN
} else if (!user.roleId) { } else if (!user.roleId) {
user.roleId = BUILTIN_ROLE_IDS.BASIC user.roleId = BUILTIN_ROLE_IDS.PUBLIC
} }
delete user.roles delete user.roles
return user return user

View File

@ -150,6 +150,10 @@ exports.processAutoColumn = processAutoColumn
* @returns {object} The coerced value * @returns {object} The coerced value
*/ */
exports.coerce = (row, type) => { exports.coerce = (row, type) => {
// no coercion specified for type, skip it
if (!TYPE_TRANSFORM_MAP[type]) {
return row
}
// eslint-disable-next-line no-prototype-builtins // eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) { if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
return TYPE_TRANSFORM_MAP[type][row] return TYPE_TRANSFORM_MAP[type][row]
@ -196,6 +200,12 @@ exports.inputProcessing = (
clonedRow[key] = exports.coerce(value, field.type) clonedRow[key] = exports.coerce(value, field.type)
} }
} }
if (!clonedRow._id || !clonedRow._rev) {
clonedRow._id = row._id
clonedRow._rev = row._rev
}
// handle auto columns - this returns an object like {table, row} // handle auto columns - this returns an object like {table, row}
return processAutoColumn(user, copiedTable, clonedRow, opts) return processAutoColumn(user, copiedTable, clonedRow, opts)
} }

View File

@ -0,0 +1,21 @@
const fetch = require("node-fetch")
const { VM, VMScript } = require("vm2")
class ScriptRunner {
constructor(script, context) {
const code = `let fn = () => {\n${script}\n}; results.out = fn();`
this.vm = new VM()
this.results = { out: "" }
this.vm.setGlobals(context)
this.vm.setGlobal("fetch", fetch)
this.vm.setGlobal("results", this.results)
this.script = new VMScript(code)
}
execute() {
this.vm.run(this.script)
return this.results.out
}
}
module.exports = ScriptRunner

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"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",
@ -24,7 +24,8 @@
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"handlebars-utils": "^1.0.6", "handlebars-utils": "^1.0.6",
"lodash": "^4.17.20" "lodash": "^4.17.20",
"vm2": "^3.9.4"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-commonjs": "^17.1.0",

View File

@ -7,18 +7,6 @@ import globals from "rollup-plugin-node-globals"
const production = !process.env.ROLLUP_WATCH const production = !process.env.ROLLUP_WATCH
const plugins = [
resolve({
preferBuiltins: true,
browser: true,
}),
commonjs(),
globals(),
builtins(),
json(),
production && terser(),
]
export default [ export default [
{ {
input: "src/index.mjs", input: "src/index.mjs",
@ -27,18 +15,16 @@ export default [
format: "esm", format: "esm",
file: "./dist/bundle.mjs", file: "./dist/bundle.mjs",
}, },
plugins, plugins: [
resolve({
preferBuiltins: true,
browser: true,
}),
commonjs(),
globals(),
builtins(),
json(),
production && terser(),
],
}, },
// This is the valid configuration for a CommonJS bundle, but since we have
// no use for this, it's better to leave it out.
// {
// input: "src/index.cjs",
// output: {
// sourcemap: !production,
// format: "cjs",
// file: "./dist/bundle.cjs",
// exports: "named",
// },
// plugins,
// },
] ]

View File

@ -1,13 +1,23 @@
class Helper { class Helper {
constructor(name, fn) { constructor(name, fn, useValueFallback = true) {
this.name = name this.name = name
this.fn = fn this.fn = fn
this.useValueFallback = useValueFallback
} }
register(handlebars) { register(handlebars) {
// wrap the function so that no helper can cause handlebars to break // wrap the function so that no helper can cause handlebars to break
handlebars.registerHelper(this.name, value => { handlebars.registerHelper(this.name, (value, info) => {
return this.fn(value) || value let context = {}
if (info && info.data && info.data.root) {
context = info.data.root
}
const result = this.fn(value, context)
if (result == null) {
return this.useValueFallback ? value : null
} else {
return result
}
}) })
} }

View File

@ -19,6 +19,7 @@ module.exports.HelperFunctionNames = {
OBJECT: "object", OBJECT: "object",
ALL: "all", ALL: "all",
LITERAL: "literal", LITERAL: "literal",
JS: "js",
} }
module.exports.LITERAL_MARKER = "%LITERAL%" module.exports.LITERAL_MARKER = "%LITERAL%"

View File

@ -1,6 +1,7 @@
const Helper = require("./Helper") const Helper = require("./Helper")
const { SafeString } = require("handlebars") const { SafeString } = require("handlebars")
const externalHandlebars = require("./external") const externalHandlebars = require("./external")
const { processJS } = require("./javascript")
const { const {
HelperFunctionNames, HelperFunctionNames,
HelperFunctionBuiltin, HelperFunctionBuiltin,
@ -17,6 +18,8 @@ const HELPERS = [
new Helper(HelperFunctionNames.OBJECT, value => { new Helper(HelperFunctionNames.OBJECT, value => {
return new SafeString(JSON.stringify(value)) return new SafeString(JSON.stringify(value))
}), }),
// javascript helper
new Helper(HelperFunctionNames.JS, processJS, false),
// this help is applied to all statements // this help is applied to all statements
new Helper(HelperFunctionNames.ALL, value => { new Helper(HelperFunctionNames.ALL, value => {
if ( if (

View File

@ -0,0 +1,49 @@
const { atob } = require("../utilities")
// The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.cjs or index.mjs).
let runJS
module.exports.setJSRunner = runner => (runJS = runner)
// Helper utility to strip square brackets from a value
const removeSquareBrackets = value => {
if (!value || typeof value !== "string") {
return value
}
const regex = /\[+(.+)]+/
const matches = value.match(regex)
if (matches && matches[1]) {
return matches[1]
}
return value
}
// Our context getter function provided to JS code as $.
// Extracts a value from context.
const getContextValue = (path, context) => {
let data = context
path.split(".").forEach(key => {
if (data == null || typeof data !== "object") {
return null
}
data = data[removeSquareBrackets(key)]
})
return data
}
// Evaluates JS code against a certain context
module.exports.processJS = (handlebars, context) => {
try {
// Wrap JS in a function and immediately invoke it.
// This is required to allow the final `return` statement to be valid.
const js = `function run(){${atob(handlebars)}};run();`
// Our $ context function gets a value from context
const sandboxContext = { $: path => getContextValue(path, context) }
// Create a sandbox with out context and run the JS
return runJS(js, sandboxContext)
} catch (error) {
return "Error while executing JS"
}
}

View File

@ -1,161 +1,28 @@
const handlebars = require("handlebars") const { VM } = require("vm2")
const { registerAll } = require("./helpers/index") const templates = require("./index.js")
const processors = require("./processors") const { setJSRunner } = require("./helpers/javascript")
const { removeHandlebarsStatements } = require("./utilities")
const manifest = require("../manifest.json")
const hbsInstance = handlebars.create()
registerAll(hbsInstance)
/** /**
* utility function to check if the object is valid * CJS entrypoint for rollup
*/ */
function testObject(object) { module.exports.isValid = templates.isValid
// JSON stringify will fail if there are any cycles, stops infinite recursion module.exports.makePropSafe = templates.makePropSafe
try { module.exports.getManifest = templates.getManifest
JSON.stringify(object) module.exports.isJSBinding = templates.isJSBinding
} catch (err) { module.exports.encodeJSBinding = templates.encodeJSBinding
throw "Unable to process inputs to JSON, cannot recurse" module.exports.decodeJSBinding = templates.decodeJSBinding
} module.exports.processStringSync = templates.processStringSync
} module.exports.processObjectSync = templates.processObjectSync
module.exports.processString = templates.processString
module.exports.processObject = templates.processObject
/** /**
* Given an input object this will recurse through all props to try and update any handlebars statements within. * Use vm2 to run JS scripts in a node env
* @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.
* @param {object} context The context that handlebars should fill data from.
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
*/ */
module.exports.processObject = async (object, context) => { setJSRunner((js, context) => {
testObject(object) const vm = new VM({
for (let key of Object.keys(object || {})) { sandbox: context,
if (object[key] != null) { timeout: 1000
let val = object[key] })
if (typeof val === "string") { return vm.run(js)
object[key] = await module.exports.processString(object[key], context) })
} else if (typeof val === "object") {
object[key] = await module.exports.processObject(object[key], context)
}
}
}
return object
}
/**
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
* then nothing will occur.
* @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.
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
*/
module.exports.processString = async (string, context) => {
// TODO: carry out any async calls before carrying out async call
return module.exports.processStringSync(string, context)
}
/**
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
* a pure sync call and therefore does not have the full functionality of the async call.
* @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.
* @param {object} context The context that handlebars should fill data from.
* @returns {object|array} The structure input, as fully updated as possible.
*/
module.exports.processObjectSync = (object, context) => {
testObject(object)
for (let key of Object.keys(object || {})) {
let val = object[key]
if (typeof val === "string") {
object[key] = module.exports.processStringSync(object[key], context)
} else if (typeof val === "object") {
object[key] = module.exports.processObjectSync(object[key], context)
}
}
return object
}
/**
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
* 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 {object} context An object of information which will be used to enrich the string.
* @returns {string} The enriched string, all templates should have been replaced if they can be.
*/
module.exports.processStringSync = (string, context) => {
if (!exports.isValid(string)) {
return string
}
// take a copy of input incase error
const input = string
if (typeof string !== "string") {
throw "Cannot process non-string types."
}
try {
string = processors.preprocess(string)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
const template = hbsInstance.compile(string, {
strict: false,
})
return processors.postprocess(template({
now: new Date().toISOString(),
...context,
}))
} catch (err) {
return removeHandlebarsStatements(input)
}
}
/**
* 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.
* @returns {string} The wrapped property ready to be added to a templating string.
*/
module.exports.makePropSafe = property => {
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 => {
const validCases = [
"string",
"number",
"object",
"array",
"cannot read property",
"undefined",
]
// 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
const context = {}
try {
hbsInstance.compile(processors.preprocess(string, false))(context)
return true
} catch (err) {
const msg = err && err.message ? err.message : err
if (!msg) {
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
return validCase && !invalidCase
}
}
/**
* We have generated a static manifest file from the helpers that this string templating package makes use of.
* This manifest provides information about each of the helpers and how it can be used.
* @returns The manifest JSON which has been generated from the helpers.
*/
module.exports.getManifest = () => {
return manifest
}

View File

@ -0,0 +1,204 @@
const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index")
const processors = require("./processors")
const { removeHandlebarsStatements, atob, btoa } = require("./utilities")
const manifest = require("../manifest.json")
const hbsInstance = handlebars.create()
registerAll(hbsInstance)
/**
* utility function to check if the object is valid
*/
function testObject(object) {
// JSON stringify will fail if there are any cycles, stops infinite recursion
try {
JSON.stringify(object)
} catch (err) {
throw "Unable to process inputs to JSON, cannot recurse"
}
}
/**
* Given an input object this will recurse through all props to try and update any handlebars statements within.
* @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.
* @param {object} context The context that handlebars should fill data from.
* @returns {Promise<object|array>} The structure input, as fully updated as possible.
*/
module.exports.processObject = async (object, context) => {
testObject(object)
for (let key of Object.keys(object || {})) {
if (object[key] != null) {
let val = object[key]
if (typeof val === "string") {
object[key] = await module.exports.processString(object[key], context)
} else if (typeof val === "object") {
object[key] = await module.exports.processObject(object[key], context)
}
}
}
return object
}
/**
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
* then nothing will occur.
* @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.
* @returns {Promise<string>} The enriched string, all templates should have been replaced if they can be.
*/
module.exports.processString = async (string, context) => {
// TODO: carry out any async calls before carrying out async call
return module.exports.processStringSync(string, context)
}
/**
* Given an input object this will recurse through all props to try and update any handlebars statements within. This is
* a pure sync call and therefore does not have the full functionality of the async call.
* @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.
* @param {object} context The context that handlebars should fill data from.
* @returns {object|array} The structure input, as fully updated as possible.
*/
module.exports.processObjectSync = (object, context) => {
testObject(object)
for (let key of Object.keys(object || {})) {
let val = object[key]
if (typeof val === "string") {
object[key] = module.exports.processStringSync(object[key], context)
} else if (typeof val === "object") {
object[key] = module.exports.processObjectSync(object[key], context)
}
}
return object
}
/**
* This will process a single handlebars containing string. If the string passed in has no valid handlebars statements
* 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 {object} context An object of information which will be used to enrich the string.
* @returns {string} The enriched string, all templates should have been replaced if they can be.
*/
module.exports.processStringSync = (string, context) => {
if (!exports.isValid(string)) {
return string
}
// take a copy of input incase error
const input = string
if (typeof string !== "string") {
throw "Cannot process non-string types."
}
try {
string = processors.preprocess(string)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
const template = hbsInstance.compile(string, {
strict: false,
})
return processors.postprocess(
template({
now: new Date().toISOString(),
...context,
})
)
} catch (err) {
return removeHandlebarsStatements(input)
}
}
/**
* 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.
* @returns {string} The wrapped property ready to be added to a templating string.
*/
module.exports.makePropSafe = property => {
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 => {
const validCases = [
"string",
"number",
"object",
"array",
"cannot read property",
"undefined",
]
// 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
const context = {}
try {
hbsInstance.compile(processors.preprocess(string, false))(context)
return true
} catch (err) {
const msg = err && err.message ? err.message : err
if (!msg) {
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
return validCase && !invalidCase
}
}
/**
* We have generated a static manifest file from the helpers that this string templating package makes use of.
* This manifest provides information about each of the helpers and how it can be used.
* @returns The manifest JSON which has been generated from the helpers.
*/
module.exports.getManifest = () => {
return manifest
}
/**
* Checks if a HBS expression is a valid JS HBS expression
* @param handlebars the HBS expression to check
* @returns {boolean} whether the expression is JS or not
*/
module.exports.isJSBinding = handlebars => {
return module.exports.decodeJSBinding(handlebars) != null
}
/**
* Encodes a raw JS string as a JS HBS expression
* @param javascript the JS code to encode
* @returns {string} the JS HBS expression
*/
module.exports.encodeJSBinding = javascript => {
return `{{ js "${btoa(javascript)}" }}`
}
/**
* Decodes a JS HBS expression to the raw JS code
* @param handlebars the JS HBS expression
* @returns {string|null} the raw JS code
*/
module.exports.decodeJSBinding = handlebars => {
if (!handlebars || typeof handlebars !== "string") {
return null
}
// JS is only valid if it is the only HBS expression
if (!handlebars.trim().startsWith("{{ js ")) {
return null
}
const captureJSRegex = new RegExp(/{{ js "(.*)" }}/)
const match = handlebars.match(captureJSRegex)
if (!match || match.length < 2) {
return null
}
return atob(match[1])
}

View File

@ -1,12 +1,31 @@
import templates from "./index.cjs" import vm from "vm"
import templates from "./index.js"
import { setJSRunner } from "./helpers/javascript"
/** /**
* This file is simply an entrypoint for rollup - makes a lot of cjs problems go away * ES6 entrypoint for rollup
*/ */
export const isValid = templates.isValid export const isValid = templates.isValid
export const makePropSafe = templates.makePropSafe export const makePropSafe = templates.makePropSafe
export const getManifest = templates.getManifest export const getManifest = templates.getManifest
export const isJSBinding = templates.isJSBinding
export const encodeJSBinding = templates.encodeJSBinding
export const decodeJSBinding = templates.decodeJSBinding
export const processStringSync = templates.processStringSync export const processStringSync = templates.processStringSync
export const processObjectSync = templates.processObjectSync export const processObjectSync = templates.processObjectSync
export const processString = templates.processString export const processString = templates.processString
export const processObject = templates.processObject export const processObject = templates.processObject
/**
* Use polyfilled vm to run JS scripts in a browser Env
*/
setJSRunner((js, context) => {
context = {
...context,
alert: undefined,
setInterval: undefined,
setTimeout: undefined,
}
vm.createContext(context)
return vm.runInNewContext(js, context, { timeout: 1000 })
})

View File

@ -22,3 +22,11 @@ module.exports.removeHandlebarsStatements = string => {
} }
return string return string
} }
module.exports.btoa = plainText => {
return Buffer.from(plainText, "utf-8").toString("base64")
}
module.exports.atob = base64 => {
return Buffer.from(base64, "base64").toString("utf-8")
}

View File

@ -0,0 +1,85 @@
const { processStringSync, encodeJSBinding } = require("../src/index.cjs")
const processJS = (js, context) => {
return processStringSync(encodeJSBinding(js), context)
}
describe("Test the JavaScript helper", () => {
it("should execute a simple expression", () => {
const output = processJS(`return 1 + 2`)
expect(output).toBe("3")
})
it("should be able to use primitive bindings", () => {
const output = processJS(`return $("foo")`, {
foo: "bar",
})
expect(output).toBe("bar")
})
it("should be able to use an object binding", () => {
const output = processJS(`return $("foo").bar`, {
foo: {
bar: "baz",
},
})
expect(output).toBe("baz")
})
it("should be able to use a complex object binding", () => {
const output = processJS(`return $("foo").bar[0].baz`, {
foo: {
bar: [
{
baz: "shazbat",
},
],
},
})
expect(output).toBe("shazbat")
})
it("should be able to use a deep binding", () => {
const output = processJS(`return $("foo.bar.baz")`, {
foo: {
bar: {
baz: "shazbat",
},
},
})
expect(output).toBe("shazbat")
})
it("should be able to use a deep array binding", () => {
const output = processJS(`return $("foo.0.bar")`, {
foo: [
{
bar: "baz",
},
],
})
expect(output).toBe("baz")
})
it("should handle errors", () => {
const output = processJS(`throw "Error"`)
expect(output).toBe("Error while executing JS")
})
it("should timeout after one second", () => {
const output = processJS(`while (true) {}`)
expect(output).toBe("Error while executing JS")
})
it("should prevent access to the process global", () => {
const output = processJS(`return process`)
expect(output).toBe("Error while executing JS")
})
it("should prevent sandbox escape", () => {
const output = processJS(
`return this.constructor.constructor("return process")()`
)
expect(output).toBe("Error while executing JS")
})
})

View File

@ -4572,6 +4572,11 @@ vlq@^0.2.2:
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
vm2@^3.9.4:
version "3.9.4"
resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.4.tgz#2e118290fefe7bd8ea09ebe2f5faf53730dbddaa"
integrity sha512-sOdharrJ7KEePIpHekiWaY1DwgueuiBeX/ZBJUPgETsVlJsXuEx0K0/naATq2haFvJrvZnRiORQRubR0b7Ye6g==
w3c-hr-time@^1.0.2: w3c-hr-time@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "0.9.167-alpha.1", "version": "0.9.167-alpha.12",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.js", "main": "src/index.js",
"repository": { "repository": {
@ -29,8 +29,8 @@
"author": "Budibase", "author": "Budibase",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@budibase/auth": "^0.9.167-alpha.1", "@budibase/auth": "^0.9.167-alpha.12",
"@budibase/string-templates": "^0.9.167-alpha.1", "@budibase/string-templates": "^0.9.167-alpha.12",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",
"aws-sdk": "^2.811.0", "aws-sdk": "^2.811.0",

View File

@ -6,5 +6,6 @@ exports.fetch = async ctx => {
cloud: !env.SELF_HOSTED, cloud: !env.SELF_HOSTED,
accountPortalUrl: env.ACCOUNT_PORTAL_URL, accountPortalUrl: env.ACCOUNT_PORTAL_URL,
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL, disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
isDev: env.isDev(),
} }
} }