@@ -70,53 +71,34 @@
diff --git a/packages/builder/tests/createProps.spec.js b/packages/builder/tests/createProps.spec.js
index 269ab8156a..bd468ba41a 100644
--- a/packages/builder/tests/createProps.spec.js
+++ b/packages/builder/tests/createProps.spec.js
@@ -1,6 +1,5 @@
import { createProps } from "../src/components/userInterface/pagesParsing/createProps"
import { keys, some } from "lodash/fp"
-import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/parseBinding"
import { stripStandardProps } from "./testData"
describe("createDefaultProps", () => {
@@ -94,17 +93,6 @@ describe("createDefaultProps", () => {
expect(props.onClick).toEqual([])
})
- it("should create a object with empty state when prop def is 'state' ", () => {
- const comp = getcomponent()
- comp.props.data = "state"
-
- const { props, errors } = createProps(comp)
-
- expect(errors).toEqual([])
- expect(props.data[BB_STATE_BINDINGPATH]).toBeDefined()
- expect(props.data[BB_STATE_BINDINGPATH]).toBe("")
- })
-
it("should create a object children array when children == true ", () => {
const comp = getcomponent()
comp.children = true
diff --git a/packages/builder/tests/generate_css.spec.js b/packages/builder/tests/generate_css.spec.js
index b2a7fca64d..5fcdef25c1 100644
--- a/packages/builder/tests/generate_css.spec.js
+++ b/packages/builder/tests/generate_css.spec.js
@@ -1,14 +1,10 @@
import {
generate_css,
generate_screen_css,
- generate_array_styles
} from "../src/builderStore/generate_css.js"
describe("generate_css", () => {
- test("Check how partially empty arrays are handled", () => {
- expect(["", "5", "", ""].map(generate_array_styles)).toEqual(["0px", "5px", "0px", "0px"])
- })
test("Check how array styles are output", () => {
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;")
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 768402cc2b..464d2a90d3 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -3,7 +3,7 @@
"version": "0.0.32",
"description": "Budibase CLI",
"repository": "https://github.com/Budibase/Budibase",
- "homepage": "https://budibase.com",
+ "homepage": "https://www.budibase.com",
"main": "src/cli.js",
"bin": {
"budi": "bin/budi"
diff --git a/packages/cli/src/commands/new/appDirectoryTemplate/pages/main/page.json b/packages/cli/src/commands/new/appDirectoryTemplate/pages/main/page.json
index af75ddec68..2deddd1c74 100644
--- a/packages/cli/src/commands/new/appDirectoryTemplate/pages/main/page.json
+++ b/packages/cli/src/commands/new/appDirectoryTemplate/pages/main/page.json
@@ -16,6 +16,5 @@
},
"_code": ""
},
- "_css": "",
- "uiFunctions": ""
+ "_css": ""
}
diff --git a/packages/cli/src/commands/new/appDirectoryTemplate/pages/unauthenticated/page.json b/packages/cli/src/commands/new/appDirectoryTemplate/pages/unauthenticated/page.json
index 3452a6df55..95941e2ea5 100644
--- a/packages/cli/src/commands/new/appDirectoryTemplate/pages/unauthenticated/page.json
+++ b/packages/cli/src/commands/new/appDirectoryTemplate/pages/unauthenticated/page.json
@@ -16,6 +16,5 @@
},
"_code": ""
},
- "_css": "",
- "uiFunctions": ""
+ "_css": ""
}
diff --git a/packages/cli/src/commands/new/page.template.json b/packages/cli/src/commands/new/page.template.json
index 6f02b78c62..9467289535 100644
--- a/packages/cli/src/commands/new/page.template.json
+++ b/packages/cli/src/commands/new/page.template.json
@@ -16,6 +16,5 @@
},
"_code": ""
},
- "_css": "",
- "uiFunctions": ""
+ "_css": ""
}
diff --git a/packages/client/package.json b/packages/client/package.json
index 9ee7e32851..01b90a7bc5 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -39,6 +39,7 @@
"deep-equal": "^2.0.1",
"lodash": "^4.17.15",
"lunr": "^2.3.5",
+ "mustache": "^4.0.1",
"regexparam": "^1.3.0",
"shortid": "^2.2.8",
"svelte": "^3.9.2"
diff --git a/packages/client/src/api/index.js b/packages/client/src/api/index.js
index 438a0bf517..f019897494 100644
--- a/packages/client/src/api/index.js
+++ b/packages/client/src/api/index.js
@@ -1,43 +1,33 @@
-import { ERROR } from "../state/standardState"
-import { loadRecord } from "./loadRecord"
-import { listRecords } from "./listRecords"
import { authenticate } from "./authenticate"
-import { saveRecord } from "./saveRecord"
+import { triggerWorkflow } from "./workflow"
export const createApi = ({ rootPath = "", setState, getState }) => {
- const apiCall = method => ({
- url,
- body,
- notFound,
- badRequest,
- forbidden,
- }) => {
- return fetch(`${rootPath}${url}`, {
+ const apiCall = method => async ({ url, body }) => {
+ const response = await fetch(`${rootPath}${url}`, {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
credentials: "same-origin",
- }).then(r => {
- switch (r.status) {
- case 200:
- return r.json()
- case 404:
- return error(notFound || `${url} Not found`)
- case 400:
- return error(badRequest || `${url} Bad Request`)
- case 403:
- return error(forbidden || `${url} Forbidden`)
- default:
- if (
- r.status.toString().startsWith("2") ||
- r.status.toString().startsWith("3")
- )
- return r.json()
- else return error(`${url} - ${r.statusText}`)
- }
})
+
+ switch (response.status) {
+ case 200:
+ return response.json()
+ case 404:
+ return error(`${url} Not found`)
+ case 400:
+ return error(`${url} Bad Request`)
+ case 403:
+ return error(`${url} Forbidden`)
+ default:
+ if (response.status >= 200 && response.status < 400) {
+ return response.json()
+ }
+
+ return error(`${url} - ${response.statusText}`)
+ }
}
const post = apiCall("POST")
@@ -47,10 +37,9 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
const ERROR_MEMBER = "##error"
const error = message => {
- const e = {}
- e[ERROR_MEMBER] = message
- setState(ERROR, message)
- return e
+ const err = { [ERROR_MEMBER]: message }
+ setState("##error_message", message)
+ return err
}
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
@@ -68,9 +57,7 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
}
return {
- loadRecord: loadRecord(apiOpts),
- listRecords: listRecords(apiOpts),
authenticate: authenticate(apiOpts),
- saveRecord: saveRecord(apiOpts),
+ triggerWorkflow: triggerWorkflow(apiOpts),
}
}
diff --git a/packages/client/src/api/listRecords.js b/packages/client/src/api/listRecords.js
deleted file mode 100644
index 24f8ee822a..0000000000
--- a/packages/client/src/api/listRecords.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { trimSlash } from "../common/trimSlash"
-
-export const listRecords = api => async ({ indexKey, statePath }) => {
- if (!indexKey) {
- api.error("Load Record: record key not set")
- return
- }
-
- if (!statePath) {
- api.error("Load Record: state path not set")
- return
- }
-
- const records = await api.get({
- url: `/api/listRecords/${trimSlash(indexKey)}`,
- })
-
- if (api.isSuccess(records)) api.setState(statePath, records)
-}
diff --git a/packages/client/src/api/loadRecord.js b/packages/client/src/api/loadRecord.js
deleted file mode 100644
index 6388801fb0..0000000000
--- a/packages/client/src/api/loadRecord.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { trimSlash } from "../common/trimSlash"
-
-export const loadRecord = api => async ({ recordKey, statePath }) => {
- if (!recordKey) {
- api.error("Load Record: record key not set")
- return
- }
-
- if (!statePath) {
- api.error("Load Record: state path not set")
- return
- }
-
- const record = await api.get({
- url: `/api/record/${trimSlash(recordKey)}`,
- })
-
- if (api.isSuccess(record)) api.setState(statePath, record)
-}
diff --git a/packages/client/src/api/saveRecord.js b/packages/client/src/api/saveRecord.js
deleted file mode 100644
index e8f4af79bd..0000000000
--- a/packages/client/src/api/saveRecord.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { trimSlash } from "../common/trimSlash"
-
-export const saveRecord = api => async ({ statePath }) => {
- if (!statePath) {
- api.error("Load Record: state path not set")
- return
- }
-
- const recordtoSave = api.getState(statePath)
-
- if (!recordtoSave) {
- api.error(`there is no record in state: ${statePath}`)
- return
- }
-
- if (!recordtoSave.key) {
- api.error(
- `item in state does not appear to be a record - it has no key (${statePath})`
- )
- return
- }
-
- const savedRecord = await api.post({
- url: `/api/record/${trimSlash(recordtoSave.key)}`,
- body: recordtoSave,
- })
-
- if (api.isSuccess(savedRecord)) api.setState(statePath, savedRecord)
-}
diff --git a/packages/client/src/api/workflow/actions.js b/packages/client/src/api/workflow/actions.js
new file mode 100644
index 0000000000..794cb184bf
--- /dev/null
+++ b/packages/client/src/api/workflow/actions.js
@@ -0,0 +1,28 @@
+import { setState } from "../../state/setState"
+
+const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
+
+export default {
+ SET_STATE: ({ context, args, id }) => {
+ setState(...Object.values(args))
+ context = {
+ ...context,
+ [id]: args,
+ }
+ return context
+ },
+ NAVIGATE: () => {
+ // TODO client navigation
+ },
+ DELAY: async ({ args }) => await delay(args.time),
+ FILTER: ({ args }) => {
+ const { field, condition, value } = args
+ switch (condition) {
+ case "equals":
+ if (field !== value) return
+ break
+ default:
+ return
+ }
+ },
+}
diff --git a/packages/client/src/api/workflow/index.js b/packages/client/src/api/workflow/index.js
new file mode 100644
index 0000000000..4a2cac6fd8
--- /dev/null
+++ b/packages/client/src/api/workflow/index.js
@@ -0,0 +1,69 @@
+import { get } from "svelte/store"
+import mustache from "mustache"
+import { appStore } from "../../state/store"
+import Orchestrator from "./orchestrator"
+import clientActions from "./actions"
+
+// Execute a workflow from a running budibase app
+export const clientStrategy = ({ api }) => ({
+ context: {},
+ bindContextArgs: function(args) {
+ const mappedArgs = { ...args }
+
+ // bind the workflow action args to the workflow context, if required
+ for (let arg in args) {
+ const argValue = args[arg]
+
+ // We don't want to render mustache templates on non-strings
+ if (typeof argValue !== "string") continue
+
+ // Render the string with values from the workflow context and state
+ mappedArgs[arg] = mustache.render(argValue, {
+ context: this.context,
+ state: get(appStore),
+ })
+ }
+
+ return mappedArgs
+ },
+ run: async function(workflow) {
+ for (let block of workflow.steps) {
+ // This code gets run in the browser
+ if (block.environment === "CLIENT") {
+ const action = clientActions[block.actionId]
+ await action({
+ context: this.context,
+ args: this.bindContextArgs(block.args),
+ id: block.id,
+ })
+ }
+
+ // this workflow block gets executed on the server
+ if (block.environment === "SERVER") {
+ const EXECUTE_WORKFLOW_URL = `/api/workflows/action`
+ const response = await api.post({
+ url: EXECUTE_WORKFLOW_URL,
+ body: {
+ action: block.actionId,
+ args: this.bindContextArgs(block.args, api),
+ },
+ })
+
+ this.context = {
+ ...this.context,
+ [block.actionId]: response,
+ }
+ }
+ }
+ },
+})
+
+export const triggerWorkflow = api => async ({ workflow }) => {
+ const workflowOrchestrator = new Orchestrator(api)
+ workflowOrchestrator.strategy = clientStrategy
+
+ const EXECUTE_WORKFLOW_URL = `/api/workflows/${workflow}`
+ const workflowDefinition = await api.get({ url: EXECUTE_WORKFLOW_URL })
+
+ workflowOrchestrator.execute(workflowDefinition)
+}
diff --git a/packages/client/src/api/workflow/orchestrator.js b/packages/client/src/api/workflow/orchestrator.js
new file mode 100644
index 0000000000..b1d17fac59
--- /dev/null
+++ b/packages/client/src/api/workflow/orchestrator.js
@@ -0,0 +1,22 @@
+/**
+ * The workflow orchestrator is a class responsible for executing workflows.
+ * It relies on the strategy pattern, which allows composable behaviour to be
+ * passed into its execute() function. This allows custom execution behaviour based
+ * on where the orchestrator is run.
+ *
+ */
+export default class Orchestrator {
+ constructor(api) {
+ this.api = api
+ }
+
+ set strategy(strategy) {
+ this._strategy = strategy({ api: this.api })
+ }
+
+ async execute(workflow) {
+ if (workflow.live) {
+ this._strategy.run(workflow.definition)
+ }
+ }
+}
diff --git a/packages/client/src/common/trimSlash.js b/packages/client/src/common/trimSlash.js
deleted file mode 100644
index 5f403ee092..0000000000
--- a/packages/client/src/common/trimSlash.js
+++ /dev/null
@@ -1 +0,0 @@
-export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
diff --git a/packages/client/src/createApp.js b/packages/client/src/createApp.js
index ec7fffafd7..a80c1009c7 100644
--- a/packages/client/src/createApp.js
+++ b/packages/client/src/createApp.js
@@ -1,27 +1,22 @@
-import { writable } from "svelte/store"
import { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager"
-export const createApp = (
+export const createApp = ({
componentLibraries,
frontendDefinition,
- user,
- uiFunctions,
- window
-) => {
+ window,
+}) => {
let routeTo
let currentUrl
let screenStateManager
const onScreenSlotRendered = screenSlotNode => {
- const onScreenSelected = (screen, store, url) => {
+ const onScreenSelected = (screen, url) => {
const stateManager = createStateManager({
- store,
frontendDefinition,
componentLibraries,
- uiFunctions,
onScreenSlotRendered: () => {},
routeTo,
appRootPath: frontendDefinition.appRootPath,
@@ -38,11 +33,11 @@ export const createApp = (
currentUrl = url
}
- routeTo = screenRouter(
- frontendDefinition.screens,
+ routeTo = screenRouter({
+ screens: frontendDefinition.screens,
onScreenSelected,
- frontendDefinition.appRootPath
- )
+ appRootPath: frontendDefinition.appRootPath,
+ })
const fallbackPath = window.location.pathname.replace(
frontendDefinition.appRootPath,
""
@@ -53,7 +48,6 @@ export const createApp = (
const attachChildrenParams = stateManager => {
const getInitialiseParams = treeNode => ({
componentLibraries,
- uiFunctions,
treeNode,
onScreenSlotRendered,
setupState: stateManager.setup,
@@ -65,10 +59,8 @@ export const createApp = (
let rootTreeNode
const pageStateManager = createStateManager({
- store: writable({ _bbuser: user }),
frontendDefinition,
componentLibraries,
- uiFunctions,
onScreenSlotRendered,
appRootPath: frontendDefinition.appRootPath,
// seems weird, but the routeTo variable may not be available at this point
@@ -82,7 +74,6 @@ export const createApp = (
rootTreeNode.props = {
_children: [page.props],
}
- rootTreeNode.rootElement = target
const getInitialiseParams = attachChildrenParams(pageStateManager)
const initChildParams = getInitialiseParams(rootTreeNode)
diff --git a/packages/client/src/index.js b/packages/client/src/index.js
index 0bb762d54a..95f722d445 100644
--- a/packages/client/src/index.js
+++ b/packages/client/src/index.js
@@ -10,9 +10,7 @@ export const loadBudibase = async opts => {
// const _localStorage = (opts && opts.localStorage) || localStorage
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
- const uiFunctions = _window["##BUDIBASE_FRONTEND_FUNCTIONS##"]
- // TODO: update
const user = {}
const componentLibraryModules = (opts && opts.componentLibraries) || {}
@@ -36,14 +34,12 @@ export const loadBudibase = async opts => {
pageStore,
routeTo,
rootNode,
- } = createApp(
- componentLibraryModules,
+ } = createApp({
+ componentLibraries: componentLibraryModules,
frontendDefinition,
user,
- uiFunctions || {},
- _window,
- rootNode
- )
+ window,
+ })
const route = _window.location
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
diff --git a/packages/client/src/render/attachChildren.js b/packages/client/src/render/attachChildren.js
index 22dff2b2a8..3e4ebef103 100644
--- a/packages/client/src/render/attachChildren.js
+++ b/packages/client/src/render/attachChildren.js
@@ -5,17 +5,16 @@ import deepEqual from "deep-equal"
export const attachChildren = initialiseOpts => (htmlElement, options) => {
const {
- uiFunctions,
componentLibraries,
treeNode,
onScreenSlotRendered,
setupState,
- getCurrentState,
} = initialiseOpts
const anchor = options && options.anchor ? options.anchor : null
const force = options ? options.force : false
const hydrate = options ? options.hydrate : true
+ const context = options && options.context
if (!force && treeNode.children.length > 0) return treeNode.children
@@ -31,8 +30,6 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
}
}
- // htmlElement.classList.add(`lay-${treeNode.props._id}`)
-
const childNodes = []
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
@@ -41,18 +38,27 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
const ComponentConstructor = componentLibraries[libName][componentName]
- const childNodesThisIteration = prepareRenderComponent({
- props: childProps,
- parentNode: treeNode,
- ComponentConstructor,
- uiFunctions,
- htmlElement,
- anchor,
- getCurrentState,
- })
+ const prepareNodes = ctx => {
+ const childNodesThisIteration = prepareRenderComponent({
+ props: childProps,
+ parentNode: treeNode,
+ ComponentConstructor,
+ htmlElement,
+ anchor,
+ context: ctx,
+ })
- for (let childNode of childNodesThisIteration) {
- childNodes.push(childNode)
+ for (let childNode of childNodesThisIteration) {
+ childNodes.push(childNode)
+ }
+ }
+
+ if (Array.isArray(context)) {
+ for (let singleCtx of context) {
+ prepareNodes(singleCtx)
+ }
+ } else {
+ prepareNodes(context)
}
}
diff --git a/packages/client/src/render/prepareRenderComponent.js b/packages/client/src/render/prepareRenderComponent.js
index a7c3e6ddcf..c8b9c5d14c 100644
--- a/packages/client/src/render/prepareRenderComponent.js
+++ b/packages/client/src/render/prepareRenderComponent.js
@@ -1,18 +1,18 @@
+import { appStore } from "../state/store"
+import mustache from "mustache"
+
export const prepareRenderComponent = ({
ComponentConstructor,
- uiFunctions,
htmlElement,
anchor,
props,
parentNode,
- getCurrentState,
+ context,
}) => {
- const func = props._id ? uiFunctions[props._id] : undefined
-
const parentContext = (parentNode && parentNode.context) || {}
let nodesToRender = []
- const createNodeAndRender = context => {
+ const createNodeAndRender = () => {
let componentContext = parentContext
if (context) {
componentContext = { ...context }
@@ -39,16 +39,28 @@ export const prepareRenderComponent = ({
if (props._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
}
+
+ // make this node listen to the store
+ if (thisNode.stateBound) {
+ const unsubscribe = appStore.subscribe(state => {
+ const storeBoundProps = { ...initialProps._bb.props }
+ for (let prop in storeBoundProps) {
+ const propValue = storeBoundProps[prop]
+ if (typeof propValue === "string") {
+ storeBoundProps[prop] = mustache.render(propValue, {
+ state,
+ context: componentContext,
+ })
+ }
+ }
+ thisNode.component.$set(storeBoundProps)
+ })
+ thisNode.unsubscribe = unsubscribe
+ }
}
}
- if (func) {
- const state = getCurrentState()
- const routeParams = state["##routeParams"]
- func(createNodeAndRender, parentContext, getCurrentState(), routeParams)
- } else {
- createNodeAndRender()
- }
+ createNodeAndRender()
return nodesToRender
}
diff --git a/packages/client/src/render/screenRouter.js b/packages/client/src/render/screenRouter.js
index a45daca54f..64319102f8 100644
--- a/packages/client/src/render/screenRouter.js
+++ b/packages/client/src/render/screenRouter.js
@@ -1,7 +1,7 @@
import regexparam from "regexparam"
-import { writable } from "svelte/store"
+import { routerStore } from "../state/store"
-export const screenRouter = (screens, onScreenSelected, appRootPath) => {
+export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
const makeRootedPath = url => {
if (appRootPath) {
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
@@ -40,13 +40,14 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => {
})
}
- const storeInitial = {}
- storeInitial["##routeParams"] = params
- const store = writable(storeInitial)
+ routerStore.update(state => {
+ state["##routeParams"] = params
+ return state
+ })
const screenIndex = current !== -1 ? current : fallback
- onScreenSelected(screens[screenIndex], store, _url)
+ onScreenSelected(screens[screenIndex], _url)
try {
!url.state && history.pushState(_url, null, _url)
@@ -69,7 +70,8 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => {
)
return
- if (!y || x.target || x.host !== location.host) return
+ const target = x.target || "_self"
+ if (!y || target !== "_self" || x.host !== location.host) return
e.preventDefault()
route(y)
diff --git a/packages/client/src/state/bbComponentApi.js b/packages/client/src/state/bbComponentApi.js
index 8ada8e2e5b..7366a90f5c 100644
--- a/packages/client/src/state/bbComponentApi.js
+++ b/packages/client/src/state/bbComponentApi.js
@@ -1,16 +1,13 @@
-import { getStateOrValue } from "./getState"
-import { setState, setStateFromBinding } from "./setState"
-import { trimSlash } from "../common/trimSlash"
-import { isBound } from "./parseBinding"
+import { setState } from "./setState"
import { attachChildren } from "../render/attachChildren"
import { getContext, setContext } from "./getSetContext"
+export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
+
export const bbFactory = ({
store,
- getCurrentState,
frontendDefinition,
componentLibraries,
- uiFunctions,
onScreenSlotRendered,
}) => {
const relativeUrl = url => {
@@ -26,10 +23,11 @@ export const bbFactory = ({
}
const apiCall = method => (url, body) =>
- fetch(relativeUrl(url), {
+ fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
+ "x-user-agent": "Budibase Builder",
},
body: body && JSON.stringify(body),
})
@@ -51,11 +49,9 @@ export const bbFactory = ({
return (treeNode, setupState) => {
const attachParams = {
componentLibraries,
- uiFunctions,
treeNode,
onScreenSlotRendered,
setupState,
- getCurrentState,
}
return {
@@ -63,17 +59,12 @@ export const bbFactory = ({
context: treeNode.context,
props: treeNode.props,
call: safeCallEvent,
- setStateFromBinding: (binding, value) =>
- setStateFromBinding(store, binding, value),
- setState: (path, value) => setState(store, path, value),
- getStateOrValue: (prop, currentContext) =>
- getStateOrValue(getCurrentState(), prop, currentContext),
+ setState,
getContext: getContext(treeNode),
setContext: setContext(treeNode),
store: store,
relativeUrl,
api,
- isBound,
parent,
}
}
diff --git a/packages/client/src/state/coreHandlers.js b/packages/client/src/state/coreHandlers.js
deleted file mode 100644
index b4d9500db1..0000000000
--- a/packages/client/src/state/coreHandlers.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { ERROR } from "./standardState"
-
-export const getNewChildRecordToState = (coreApi, setState) => ({
- recordKey,
- collectionName,
- childRecordType,
- statePath,
-}) => {
- const error = errorHandler(setState)
- try {
- if (!recordKey) {
- error("getNewChild > recordKey not set")
- return
- }
-
- if (!collectionName) {
- error("getNewChild > collectionName not set")
- return
- }
-
- if (!childRecordType) {
- error("getNewChild > childRecordType not set")
- return
- }
-
- if (!statePath) {
- error("getNewChild > statePath not set")
- return
- }
-
- const rec = coreApi.recordApi.getNewChild(
- recordKey,
- collectionName,
- childRecordType
- )
- setState(statePath, rec)
- } catch (e) {
- error(e.message)
- }
-}
-
-export const getNewRecordToState = (coreApi, setState) => ({
- collectionKey,
- childRecordType,
- statePath,
-}) => {
- const error = errorHandler(setState)
- try {
- if (!collectionKey) {
- error("getNewChild > collectionKey not set")
- return
- }
-
- if (!childRecordType) {
- error("getNewChild > childRecordType not set")
- return
- }
-
- if (!statePath) {
- error("getNewChild > statePath not set")
- return
- }
-
- const rec = coreApi.recordApi.getNew(collectionKey, childRecordType)
- setState(statePath, rec)
- } catch (e) {
- error(e.message)
- }
-}
-
-const errorHandler = setState => message => setState(ERROR, message)
diff --git a/packages/client/src/state/eventHandlers.js b/packages/client/src/state/eventHandlers.js
index 3f64f779ad..b013956dd4 100644
--- a/packages/client/src/state/eventHandlers.js
+++ b/packages/client/src/state/eventHandlers.js
@@ -6,35 +6,24 @@ import { createApi } from "../api"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
-export const eventHandlers = (store, rootPath, routeTo) => {
+export const eventHandlers = (rootPath, routeTo) => {
const handler = (parameters, execute) => ({
execute,
parameters,
})
- const setStateWithStore = (path, value) => setState(store, path, value)
-
- let currentState
- store.subscribe(state => {
- currentState = state
- })
-
const api = createApi({
rootPath,
- setState: setStateWithStore,
- getState: (path, fallback) => getState(currentState, path, fallback),
+ setState,
+ getState: (path, fallback) => getState(path, fallback),
})
- const setStateHandler = ({ path, value }) => setState(store, path, value)
+ const setStateHandler = ({ path, value }) => setState(path, value)
return {
"Set State": handler(["path", "value"], setStateHandler),
- "Load Record": handler(["recordKey", "statePath"], api.loadRecord),
- "List Records": handler(["indexKey", "statePath"], api.listRecords),
- "Save Record": handler(["statePath"], api.saveRecord),
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
-
- Authenticate: handler(["username", "password"], api.authenticate),
+ "Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
}
}
diff --git a/packages/client/src/state/getState.js b/packages/client/src/state/getState.js
index a4c4b22a83..236b54c6dc 100644
--- a/packages/client/src/state/getState.js
+++ b/packages/client/src/state/getState.js
@@ -1,46 +1,9 @@
-import { isUndefined, isObject } from "lodash/fp"
-import { parseBinding, isStoreBinding } from "./parseBinding"
+import { get } from "svelte/store"
+import getOr from "lodash/fp/getOr"
+import { appStore } from "./store"
-export const getState = (s, path, fallback) => {
- if (!s) return fallback
+export const getState = (path, fallback) => {
if (!path || path.length === 0) return fallback
- if (path === "$") return s
-
- const pathParts = path.split(".")
- const safeGetPath = (obj, currentPartIndex = 0) => {
- const currentKey = pathParts[currentPartIndex]
-
- if (pathParts.length - 1 == currentPartIndex) {
- const value = obj[currentKey]
- if (isUndefined(value)) return fallback
- else return value
- }
-
- if (
- obj[currentKey] === null ||
- obj[currentKey] === undefined ||
- !isObject(obj[currentKey])
- ) {
- return fallback
- }
-
- return safeGetPath(obj[currentKey], currentPartIndex + 1)
- }
-
- return safeGetPath(s)
-}
-
-export const getStateOrValue = (globalState, prop, currentContext) => {
- if (!prop) return prop
-
- const binding = parseBinding(prop)
-
- if (binding) {
- const stateToUse = isStoreBinding(binding) ? globalState : currentContext
-
- return getState(stateToUse, binding.path, binding.fallback)
- }
-
- return prop
+ return getOr(fallback, path, get(appStore))
}
diff --git a/packages/client/src/state/parseBinding.js b/packages/client/src/state/parseBinding.js
deleted file mode 100644
index 76ab786dc9..0000000000
--- a/packages/client/src/state/parseBinding.js
+++ /dev/null
@@ -1,67 +0,0 @@
-export const BB_STATE_BINDINGPATH = "##bbstate"
-export const BB_STATE_BINDINGSOURCE = "##bbsource"
-export const BB_STATE_FALLBACK = "##bbstatefallback"
-
-export const isBound = prop => !!parseBinding(prop)
-
-/**
- *
- * @param {object|string|number} prop - component property to parse for a dynamic state binding
- * @returns {object|boolean}
- */
-export const parseBinding = prop => {
- if (!prop) return false
-
- if (isBindingExpression(prop)) {
- return parseBindingExpression(prop)
- }
-
- if (isAlreadyBinding(prop)) {
- return {
- path: prop.path,
- source: prop.source || "store",
- fallback: prop.fallback,
- }
- }
-
- if (hasBindingObject(prop)) {
- return {
- path: prop[BB_STATE_BINDINGPATH],
- fallback: prop[BB_STATE_FALLBACK] || "",
- source: prop[BB_STATE_BINDINGSOURCE] || "store",
- }
- }
-}
-
-export const isStoreBinding = binding => binding && binding.source === "store"
-export const isContextBinding = binding =>
- binding && binding.source === "context"
-export const isEventBinding = binding => binding && binding.source === "event"
-
-const hasBindingObject = prop =>
- typeof prop === "object" && prop[BB_STATE_BINDINGPATH] !== undefined
-
-const isAlreadyBinding = prop => typeof prop === "object" && prop.path
-
-const isBindingExpression = prop =>
- typeof prop === "string" &&
- (prop.startsWith("state.") ||
- prop.startsWith("context.") ||
- prop.startsWith("event.") ||
- prop.startsWith("route."))
-
-const parseBindingExpression = prop => {
- let [source, ...rest] = prop.split(".")
- let path = rest.join(".")
-
- if (source === "route") {
- source = "state"
- path = `##routeParams.${path}`
- }
-
- return {
- fallback: "", // TODO: provide fallback support
- source,
- path,
- }
-}
diff --git a/packages/client/src/state/setState.js b/packages/client/src/state/setState.js
index 7c23efe8eb..ac17b1a681 100644
--- a/packages/client/src/state/setState.js
+++ b/packages/client/src/state/setState.js
@@ -1,38 +1,11 @@
-import { isObject } from "lodash/fp"
-import { parseBinding } from "./parseBinding"
+import set from "lodash/fp/set"
+import { appStore } from "./store"
-export const setState = (store, path, value) => {
+export const setState = (path, value) => {
if (!path || path.length === 0) return
- const pathParts = path.split(".")
-
- const safeSetPath = (state, currentPartIndex = 0) => {
- const currentKey = pathParts[currentPartIndex]
-
- if (pathParts.length - 1 == currentPartIndex) {
- state[currentKey] = value
- return
- }
-
- if (
- state[currentKey] === null ||
- state[currentKey] === undefined ||
- !isObject(state[currentKey])
- ) {
- state[currentKey] = {}
- }
-
- safeSetPath(state[currentKey], currentPartIndex + 1)
- }
-
- store.update(state => {
- safeSetPath(state)
+ appStore.update(state => {
+ state = set(path, value, state)
return state
})
}
-
-export const setStateFromBinding = (store, binding, value) => {
- const parsedBinding = parseBinding(binding)
- if (!parsedBinding) return
- return setState(store, parsedBinding.path, value)
-}
diff --git a/packages/client/src/state/standardState.js b/packages/client/src/state/standardState.js
deleted file mode 100644
index 9a4805edd5..0000000000
--- a/packages/client/src/state/standardState.js
+++ /dev/null
@@ -1 +0,0 @@
-export const ERROR = "##error_message"
diff --git a/packages/client/src/state/stateManager.js b/packages/client/src/state/stateManager.js
index 4547cbd79c..8e777ca4b9 100644
--- a/packages/client/src/state/stateManager.js
+++ b/packages/client/src/state/stateManager.js
@@ -4,10 +4,9 @@ import {
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
-import { getState } from "./getState"
-import { attachChildren } from "../render/attachChildren"
-
-import { parseBinding } from "./parseBinding"
+import mustache from "mustache"
+import { get } from "svelte/store"
+import { appStore } from "./store"
const doNothing = () => {}
doNothing.isPlaceholder = true
@@ -18,182 +17,62 @@ const isMetaProp = propName =>
propName === "_id" ||
propName === "_style" ||
propName === "_code" ||
- propName === "_codeMeta"
+ propName === "_codeMeta" ||
+ propName === "_styles"
export const createStateManager = ({
- store,
appRootPath,
frontendDefinition,
componentLibraries,
- uiFunctions,
onScreenSlotRendered,
routeTo,
}) => {
- let handlerTypes = eventHandlers(store, appRootPath, routeTo)
+ let handlerTypes = eventHandlers(appRootPath, routeTo)
let currentState
- // any nodes that have props that are bound to the store
- let nodesBoundByProps = []
-
- // any node whose children depend on code, that uses the store
- let nodesWithCodeBoundChildren = []
-
const getCurrentState = () => currentState
- const registerBindings = _registerBindings(
- nodesBoundByProps,
- nodesWithCodeBoundChildren
- )
+
const bb = bbFactory({
- store,
+ store: appStore,
getCurrentState,
frontendDefinition,
componentLibraries,
- uiFunctions,
onScreenSlotRendered,
})
- const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
-
- const unsubscribe = store.subscribe(
- onStoreStateUpdated({
- setCurrentState: s => (currentState = s),
- getCurrentState,
- nodesWithCodeBoundChildren,
- nodesBoundByProps,
- uiFunctions,
- componentLibraries,
- onScreenSlotRendered,
- setupState: setup,
- })
- )
+ const setup = _setup({ handlerTypes, getCurrentState, bb, store: appStore })
return {
setup,
- destroy: () => unsubscribe(),
+ destroy: () => {},
getCurrentState,
- store,
+ store: appStore,
}
}
-const onStoreStateUpdated = ({
- setCurrentState,
- getCurrentState,
- nodesWithCodeBoundChildren,
- nodesBoundByProps,
- uiFunctions,
- componentLibraries,
- onScreenSlotRendered,
- setupState,
-}) => s => {
- setCurrentState(s)
-
- // the original array gets changed by components' destroy()
- // so we make a clone and check if they are still in the original
- const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
- for (let node of nodesWithBoundChildren_clone) {
- if (!nodesWithCodeBoundChildren.includes(node)) continue
- attachChildren({
- uiFunctions,
- componentLibraries,
- treeNode: node,
- onScreenSlotRendered,
- setupState,
- getCurrentState,
- })(node.rootElement, { hydrate: true, force: true })
- }
-
- for (let node of nodesBoundByProps) {
- setNodeState(s, node)
- }
-}
-
-const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
- node,
- bindings
-) => {
- if (bindings.length > 0) {
- node.bindings = bindings
- nodesBoundByProps.push(node)
- const onDestroy = () => {
- nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
- node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
- }
- node.onDestroy.push(onDestroy)
- }
- if (
- node.props._children &&
- node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
- .length > 0
- ) {
- nodesWithCodeBoundChildren.push(node)
- const onDestroy = () => {
- nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
- n => n === node
- )
- node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
- }
- node.onDestroy.push(onDestroy)
- }
-}
-
-const setNodeState = (storeState, node) => {
- if (!node.component) return
- const newProps = { ...node.bindings.initialProps }
-
- for (let binding of node.bindings) {
- const val = getState(storeState, binding.path, binding.fallback)
-
- if (val === undefined && newProps[binding.propName] !== undefined) {
- delete newProps[binding.propName]
- }
-
- if (val !== undefined) {
- newProps[binding.propName] = val
- }
- }
-
- node.component.$set(newProps)
-}
-
-const _setup = (
- handlerTypes,
- getCurrentState,
- registerBindings,
- bb
-) => node => {
+const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
const props = node.props
const context = node.context || {}
const initialProps = { ...props }
- const storeBoundProps = []
- const currentStoreState = getCurrentState()
+ const currentStoreState = get(appStore)
for (let propName in props) {
if (isMetaProp(propName)) continue
const propValue = props[propName]
- const binding = parseBinding(propValue)
- const isBound = !!binding
+ // A little bit of a hack - won't bind if the string doesn't start with {{
+ const isBound = typeof propValue === "string" && propValue.startsWith("{{")
- if (isBound) binding.propName = propName
+ if (isBound) {
+ initialProps[propName] = mustache.render(propValue, {
+ state: currentStoreState,
+ context,
+ })
- if (isBound && binding.source === "state") {
- storeBoundProps.push(binding)
-
- initialProps[propName] = !currentStoreState
- ? binding.fallback
- : getState(
- currentStoreState,
- binding.path,
- binding.fallback,
- binding.source
- )
- }
-
- if (isBound && binding.source === "context") {
- initialProps[propName] = !context
- ? propValue
- : getState(context, binding.path, binding.fallback, binding.source)
+ if (!node.stateBound) {
+ node.stateBound = true
+ }
}
if (isEventType(propValue)) {
@@ -203,33 +82,24 @@ const _setup = (
handlerType: event[EVENT_TYPE_MEMBER_NAME],
parameters: event.parameters,
}
+
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
- const paramBinding = parseBinding(paramValue)
- if (!paramBinding) {
- resolvedParams[paramName] = () => paramValue
- continue
- }
-
- let paramValueSource
-
- if (paramBinding.source === "context") paramValueSource = context
- if (paramBinding.source === "state")
- paramValueSource = getCurrentState()
- if (paramBinding.source === "context") paramValueSource = context
-
- // The new dynamic event parameter bound to the relevant source
resolvedParams[paramName] = () =>
- getState(paramValueSource, paramBinding.path, paramBinding.fallback)
+ mustache.render(paramValue, {
+ state: getCurrentState(),
+ context,
+ })
}
handlerInfo.parameters = resolvedParams
handlersInfos.push(handlerInfo)
}
- if (handlersInfos.length === 0) initialProps[propName] = doNothing
- else {
+ if (handlersInfos.length === 0) {
+ initialProps[propName] = doNothing
+ } else {
initialProps[propName] = async context => {
for (let handlerInfo of handlersInfos) {
const handler = makeHandler(handlerTypes, handlerInfo)
@@ -240,9 +110,7 @@ const _setup = (
}
}
- registerBindings(node, storeBoundProps)
-
- const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
+ const setup = _setup({ handlerTypes, getCurrentState, bb, store })
initialProps._bb = bb(node, setup)
return initialProps
diff --git a/packages/client/src/state/stateManager/index.js b/packages/client/src/state/stateManager/index.js
deleted file mode 100644
index a4bf858ba8..0000000000
--- a/packages/client/src/state/stateManager/index.js
+++ /dev/null
@@ -1,283 +0,0 @@
-import {
- isEventType,
- eventHandlers,
- EVENT_TYPE_MEMBER_NAME,
-} from "./eventHandlers"
-import { bbFactory } from "./bbComponentApi"
-import { getState } from "./getState"
-import { attachChildren } from "../render/attachChildren"
-
-import { parseBinding } from "./parseBinding"
-
-const doNothing = () => {}
-doNothing.isPlaceholder = true
-
-const isMetaProp = propName =>
- propName === "_component" ||
- propName === "_children" ||
- propName === "_id" ||
- propName === "_style" ||
- propName === "_code" ||
- propName === "_codeMeta"
-
-export const createStateManager = ({
- store,
- coreApi,
- rootPath,
- frontendDefinition,
- componentLibraries,
- uiFunctions,
- onScreenSlotRendered,
- routeTo,
-}) => {
- let handlerTypes = eventHandlers(store, coreApi, rootPath, routeTo)
- let currentState
-
- // any nodes that have props that are bound to the store
- let nodesBoundByProps = []
-
- // any node whose children depend on code, that uses the store
- let nodesWithCodeBoundChildren = []
-
- const getCurrentState = () => currentState
- const registerBindings = _registerBindings(
- nodesBoundByProps,
- nodesWithCodeBoundChildren
- )
- const bb = bbFactory({
- store,
- getCurrentState,
- frontendDefinition,
- componentLibraries,
- uiFunctions,
- onScreenSlotRendered,
- })
-
- const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
-
- const unsubscribe = store.subscribe(
- onStoreStateUpdated({
- setCurrentState: s => (currentState = s),
- getCurrentState,
- nodesWithCodeBoundChildren,
- nodesBoundByProps,
- uiFunctions,
- componentLibraries,
- onScreenSlotRendered,
- setupState: setup,
- })
- )
-
- return {
- setup,
- destroy: () => unsubscribe(),
- getCurrentState,
- store,
- }
-}
-
-const onStoreStateUpdated = ({
- setCurrentState,
- getCurrentState,
- nodesWithCodeBoundChildren,
- nodesBoundByProps,
- uiFunctions,
- componentLibraries,
- onScreenSlotRendered,
- setupState,
-}) => s => {
- setCurrentState(s)
-
- // the original array gets changed by components' destroy()
- // so we make a clone and check if they are still in the original
- const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
- for (let node of nodesWithBoundChildren_clone) {
- if (!nodesWithCodeBoundChildren.includes(node)) continue
- attachChildren({
- uiFunctions,
- componentLibraries,
- treeNode: node,
- onScreenSlotRendered,
- setupState,
- getCurrentState,
- })(node.rootElement, { hydrate: true, force: true })
- }
-
- for (let node of nodesBoundByProps) {
- setNodeState(s, node)
- }
-}
-
-const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
- node,
- bindings
-) => {
- if (bindings.length > 0) {
- node.bindings = bindings
- nodesBoundByProps.push(node)
- const onDestroy = () => {
- nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
- node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
- }
- node.onDestroy.push(onDestroy)
- }
- if (
- node.props._children &&
- node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
- .length > 0
- ) {
- nodesWithCodeBoundChildren.push(node)
- const onDestroy = () => {
- nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
- n => n === node
- )
- node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
- }
- node.onDestroy.push(onDestroy)
- }
-}
-
-const setNodeState = (storeState, node) => {
- if (!node.component) return
- const newProps = { ...node.bindings.initialProps }
-
- for (let binding of node.bindings) {
- const val = getState(storeState, binding.path, binding.fallback)
-
- if (val === undefined && newProps[binding.propName] !== undefined) {
- delete newProps[binding.propName]
- }
-
- if (val !== undefined) {
- newProps[binding.propName] = val
- }
- }
-
- node.component.$set(newProps)
-}
-
-/**
- * Bind a components event handler parameters to state, context or the event itself.
- * @param {Array} eventHandlerProp - event handler array from component definition
- */
-function bindComponentEventHandlers(
- eventHandlerProp,
- context,
- getCurrentState
-) {
- const boundEventHandlers = []
- for (let event of eventHandlerProp) {
- const boundEventHandler = {
- handlerType: event[EVENT_TYPE_MEMBER_NAME],
- parameters: event.parameters,
- }
-
- const boundParameters = {}
- for (let paramName in boundEventHandler.parameters) {
- const paramValue = boundEventHandler.parameters[paramName]
- const paramBinding = parseBinding(paramValue)
- if (!paramBinding) {
- boundParameters[paramName] = () => paramValue
- continue
- }
-
- let paramValueSource
-
- if (paramBinding.source === "context") paramValueSource = context
- if (paramBinding.source === "state") paramValueSource = getCurrentState()
-
- // The new dynamic event parameter bound to the relevant source
- boundParameters[paramName] = eventContext =>
- getState(
- paramBinding.source === "event" ? eventContext : paramValueSource,
- paramBinding.path,
- paramBinding.fallback
- )
- }
-
- boundEventHandler.parameters = boundParameters
- boundEventHandlers.push(boundEventHandlers)
-
- return boundEventHandlers
- }
-}
-
-const _setup = (
- handlerTypes,
- getCurrentState,
- registerBindings,
- bb
-) => node => {
- const props = node.props
- const context = node.context || {}
- const initialProps = { ...props }
- const storeBoundProps = []
- const currentStoreState = getCurrentState()
-
- for (let propName in props) {
- if (isMetaProp(propName)) continue
-
- const propValue = props[propName]
-
- const binding = parseBinding(propValue)
- const isBound = !!binding
-
- if (isBound) binding.propName = propName
-
- if (isBound && binding.source === "state") {
- storeBoundProps.push(binding)
-
- initialProps[propName] = !currentStoreState
- ? binding.fallback
- : getState(
- currentStoreState,
- binding.path,
- binding.fallback,
- binding.source
- )
- }
-
- if (isBound && binding.source === "context") {
- initialProps[propName] = !context
- ? propValue
- : getState(context, binding.path, binding.fallback, binding.source)
- }
-
- if (isEventType(propValue)) {
- const boundEventHandlers = bindComponentEventHandlers(
- propValue,
- context,
- getCurrentState
- )
-
- if (boundEventHandlers.length === 0) {
- initialProps[propName] = doNothing
- } else {
- initialProps[propName] = async context => {
- for (let handlerInfo of boundEventHandlers) {
- const handler = makeHandler(handlerTypes, handlerInfo)
- await handler(context)
- }
- }
- }
- }
- }
-
- registerBindings(node, storeBoundProps)
-
- const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
- initialProps._bb = bb(node, setup)
-
- return initialProps
-}
-
-const makeHandler = (handlerTypes, handlerInfo) => {
- const handlerType = handlerTypes[handlerInfo.handlerType]
- return context => {
- const parameters = {}
- for (let paramName in handlerInfo.parameters) {
- parameters[paramName] = handlerInfo.parameters[paramName](context)
- }
- handlerType.execute(parameters)
- }
-}
diff --git a/packages/client/src/state/store.js b/packages/client/src/state/store.js
new file mode 100644
index 0000000000..bdf85c1ae6
--- /dev/null
+++ b/packages/client/src/state/store.js
@@ -0,0 +1,9 @@
+import { writable } from "svelte/store"
+
+const appStore = writable({})
+appStore.actions = {}
+
+const routerStore = writable({})
+routerStore.actions = {}
+
+export { appStore, routerStore }
diff --git a/packages/client/tests/bindingDom.spec.js b/packages/client/tests/bindingDom.spec.js
deleted file mode 100644
index 2013803d27..0000000000
--- a/packages/client/tests/bindingDom.spec.js
+++ /dev/null
@@ -1,363 +0,0 @@
-import { load, makePage, makeScreen } from "./testAppDef"
-import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
-
-describe("initialiseApp (binding)", () => {
- it("should populate root element prop from store value", async () => {
- const { dom } = await load(
- makePage({
- _component: "testlib/div",
- className: {
- "##bbstate": "divClassName",
- "##bbsource": "state",
- "##bbstatefallback": "default",
- },
- })
- )
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.className.includes("default")).toBe(true)
- })
-
- it("should update root element from store", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/div",
- className: {
- "##bbstate": "divClassName",
- "##bbsource": "state",
- "##bbstatefallback": "default",
- },
- })
- )
-
- app.pageStore().update(s => {
- s.divClassName = "newvalue"
- return s
- })
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.className.includes("newvalue")).toBe(true)
- })
-
- it("should update root element from store, using binding expression", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/div",
- className: "state.divClassName",
- })
- )
-
- app.pageStore().update(s => {
- s.divClassName = "newvalue"
- return s
- })
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.className.includes("newvalue")).toBe(true)
- })
-
- it("should populate child component with store value", async () => {
- const { dom } = await load(
- makePage({
- _component: "testlib/div",
- _children: [
- {
- _component: "testlib/h1",
- text: {
- "##bbstate": "headerOneText",
- "##bbsource": "state",
- "##bbstatefallback": "header one",
- },
- },
- {
- _component: "testlib/h1",
- text: {
- "##bbstate": "headerTwoText",
- "##bbsource": "state",
- "##bbstatefallback": "header two",
- },
- },
- ],
- })
- )
-
- const rootDiv = dom.window.document.body.children[0]
-
- expect(rootDiv.children.length).toBe(2)
- expect(rootDiv.children[0].tagName).toBe("H1")
- expect(rootDiv.children[0].innerText).toBe("header one")
- expect(rootDiv.children[1].tagName).toBe("H1")
- expect(rootDiv.children[1].innerText).toBe("header two")
- })
-
- it("should populate child component with store value", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/div",
- _children: [
- {
- _component: "testlib/h1",
- text: {
- "##bbstate": "headerOneText",
- "##bbsource": "state",
- "##bbstatefallback": "header one",
- },
- },
- {
- _component: "testlib/h1",
- text: {
- "##bbstate": "headerTwoText",
- "##bbsource": "state",
- "##bbstatefallback": "header two",
- },
- },
- ],
- })
- )
-
- app.pageStore().update(s => {
- s.headerOneText = "header 1 - new val"
- s.headerTwoText = "header 2 - new val"
- return s
- })
-
- const rootDiv = dom.window.document.body.children[0]
-
- expect(rootDiv.children.length).toBe(2)
- expect(rootDiv.children[0].tagName).toBe("H1")
- expect(rootDiv.children[0].innerText).toBe("header 1 - new val")
- expect(rootDiv.children[1].tagName).toBe("H1")
- expect(rootDiv.children[1].innerText).toBe("header 2 - new val")
- })
-
- it("should populate screen child with store value", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/div",
- _children: [
- {
- _component: "##builtin/screenslot",
- text: "header one",
- },
- ],
- }),
- [
- makeScreen("/", {
- _component: "testlib/div",
- className: "screen-class",
- _children: [
- {
- _component: "testlib/h1",
- text: {
- "##bbstate": "headerOneText",
- "##bbsource": "state",
- "##bbstatefallback": "header one",
- },
- },
- {
- _component: "testlib/h1",
- text: {
- "##bbstate": "headerTwoText",
- "##bbsource": "state",
- "##bbstatefallback": "header two",
- },
- },
- ],
- }),
- ]
- )
-
- app.screenStore().update(s => {
- s.headerOneText = "header 1 - new val"
- s.headerTwoText = "header 2 - new val"
- return s
- })
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.children.length).toBe(1)
-
- const screenRoot = rootDiv.children[0]
-
- expect(screenRoot.children.length).toBe(1)
- expect(screenRoot.children[0].children.length).toBe(2)
- expect(screenRoot.children[0].children[0].innerText).toBe(
- "header 1 - new val"
- )
- expect(screenRoot.children[0].children[1].innerText).toBe(
- "header 2 - new val"
- )
- })
-
- it("should fire events", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/button",
- onClick: [
- event("Set State", {
- path: "address",
- value: "123 Main Street",
- }),
- ],
- })
- )
-
- const button = dom.window.document.body.children[0]
- expect(button.tagName).toBe("BUTTON")
-
- let storeAddress
- app.pageStore().subscribe(s => {
- storeAddress = s.address
- })
- button.dispatchEvent(new dom.window.Event("click"))
- expect(storeAddress).toBe("123 Main Street")
- })
-
- it("should alter event parameters based on store values", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/button",
- onClick: [
- event("Set State", {
- path: "address",
- value: {
- "##bbstate": "sourceaddress",
- "##bbsource": "state",
- "##bbstatefallback": "fallback address",
- },
- }),
- ],
- })
- )
-
- const button = dom.window.document.body.children[0]
- expect(button.tagName).toBe("BUTTON")
-
- let storeAddress
- app.pageStore().subscribe(s => {
- storeAddress = s.address
- })
-
- button.dispatchEvent(new dom.window.Event("click"))
- expect(storeAddress).toBe("fallback address")
-
- app.pageStore().update(s => {
- s.sourceaddress = "new address"
- return s
- })
-
- button.dispatchEvent(new dom.window.Event("click"))
- expect(storeAddress).toBe("new address")
- })
-
- it("should take event parameters from context values", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/button",
- _id: "with_context",
- onClick: [
- event("Set State", {
- path: "address",
- value: {
- "##bbstate": "testKey",
- "##bbsource": "context",
- "##bbstatefallback": "fallback address",
- },
- }),
- ],
- })
- )
-
- const button = dom.window.document.body.children[0]
- expect(button.tagName).toBe("BUTTON")
-
- let storeAddress
- app.pageStore().subscribe(s => {
- storeAddress = s.address
- })
-
- button.dispatchEvent(new dom.window.Event("click"))
- expect(storeAddress).toBe("test value")
- })
-})
-
-it("should rerender components when their code is bound to the store ", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/div",
- _children: [
- {
- _component: "testlib/div",
- _id: "n_clones_based_on_store",
- className: "child_div",
- },
- ],
- })
- )
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.tagName).toBe("DIV")
- expect(rootDiv.children.length).toBe(0)
-
- app.pageStore().update(s => {
- s.componentCount = 3
- return s
- })
-
- expect(rootDiv.children.length).toBe(3)
- expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
-
- app.pageStore().update(s => {
- s.componentCount = 5
- return s
- })
-
- expect(rootDiv.children.length).toBe(5)
- expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
-
- app.pageStore().update(s => {
- s.componentCount = 0
- return s
- })
-
- expect(rootDiv.children.length).toBe(0)
-})
-
-it("should be able to read value from context, passed fromm parent, through code", async () => {
- const { dom, app } = await load(
- makePage({
- _component: "testlib/div",
- _children: [
- {
- _component: "testlib/div",
- _id: "n_clones_based_on_store",
- className: {
- "##bbstate": "index",
- "##bbsource": "context",
- "##bbstatefallback": "nothing",
- },
- },
- ],
- })
- )
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.tagName).toBe("DIV")
- expect(rootDiv.children.length).toBe(0)
-
- app.pageStore().update(s => {
- s.componentCount = 3
- return s
- })
-
- expect(rootDiv.children.length).toBe(3)
- expect(rootDiv.children[0].className.includes("index_0")).toBe(true)
- expect(rootDiv.children[1].className.includes("index_1")).toBe(true)
- expect(rootDiv.children[2].className.includes("index_2")).toBe(true)
-})
-
-const event = (handlerType, parameters) => {
- const e = {}
- e[EVENT_TYPE_MEMBER_NAME] = handlerType
- e.parameters = parameters
- return e
-}
diff --git a/packages/client/tests/domControlFlow.spec.js b/packages/client/tests/domControlFlow.spec.js
deleted file mode 100644
index 3c817d859d..0000000000
--- a/packages/client/tests/domControlFlow.spec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { load, makePage } from "./testAppDef"
-
-describe("controlFlow", () => {
- it("should display simple div, with always true render function", async () => {
- const { dom } = await load(
- makePage({
- _component: "testlib/div",
- className: "my-test-class",
- _id: "always_render",
- })
- )
-
- expect(dom.window.document.body.children.length).toBe(1)
- const child = dom.window.document.body.children[0]
- expect(child.className.includes("my-test-class")).toBeTruthy()
- })
-
- it("should not display div, with always false render function", async () => {
- const { dom } = await load(
- makePage({
- _component: "testlib/div",
- className: "my-test-class",
- _id: "never_render",
- })
- )
-
- expect(dom.window.document.body.children.length).toBe(0)
- })
-
- it("should display 3 divs in a looped render function", async () => {
- const { dom } = await load(
- makePage({
- _component: "testlib/div",
- className: "my-test-class",
- _id: "three_clones",
- })
- )
-
- expect(dom.window.document.body.children.length).toBe(3)
-
- const child0 = dom.window.document.body.children[0]
- expect(child0.className.includes("my-test-class")).toBeTruthy()
-
- const child1 = dom.window.document.body.children[1]
- expect(child1.className.includes("my-test-class")).toBeTruthy()
-
- const child2 = dom.window.document.body.children[2]
- expect(child2.className.includes("my-test-class")).toBeTruthy()
- })
-
- it("should display 3 div, in a looped render, as children", async () => {
- const { dom } = await load(
- makePage({
- _component: "testlib/div",
- _children: [
- {
- _component: "testlib/div",
- className: "my-test-class",
- _id: "three_clones",
- },
- ],
- })
- )
-
- expect(dom.window.document.body.children.length).toBe(1)
-
- const rootDiv = dom.window.document.body.children[0]
- expect(rootDiv.children.length).toBe(3)
-
- expect(rootDiv.children[0].className.includes("my-test-class")).toBeTruthy()
- expect(rootDiv.children[1].className.includes("my-test-class")).toBeTruthy()
- expect(rootDiv.children[2].className.includes("my-test-class")).toBeTruthy()
- })
-})
diff --git a/packages/client/tests/testAppDef.js b/packages/client/tests/testAppDef.js
index f5365d926d..c02c18a3b2 100644
--- a/packages/client/tests/testAppDef.js
+++ b/packages/client/tests/testAppDef.js
@@ -13,7 +13,7 @@ export const load = async (page, screens, url, appRootPath) => {
autoAssignIds(s.props)
}
setAppDef(dom.window, page, screens)
- addWindowGlobals(dom.window, page, screens, appRootPath, uiFunctions, {
+ addWindowGlobals(dom.window, page, screens, appRootPath, {
hierarchy: {},
actions: [],
triggers: [],
@@ -27,13 +27,12 @@ export const load = async (page, screens, url, appRootPath) => {
return { dom, app }
}
-const addWindowGlobals = (window, page, screens, appRootPath, uiFunctions) => {
+const addWindowGlobals = (window, page, screens, appRootPath) => {
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
page,
screens,
appRootPath,
}
- window["##BUDIBASE_FRONTEND_FUNCTIONS##"] = uiFunctions
}
export const makePage = props => ({ props })
@@ -184,28 +183,3 @@ const maketestlib = window => ({
opts.target.appendChild(node)
},
})
-
-const uiFunctions = {
- never_render: () => {},
-
- always_render: render => {
- render()
- },
-
- three_clones: render => {
- for (let i = 0; i < 3; i++) {
- render()
- }
- },
-
- with_context: render => {
- render({ testKey: "test value" })
- },
-
- n_clones_based_on_store: (render, _, state) => {
- const n = state.componentCount || 0
- for (let i = 0; i < n; i++) {
- render({ index: `index_${i}` })
- }
- },
-}
diff --git a/packages/materialdesign-components/src/Templates/indexDatatable.js b/packages/materialdesign-components/src/Templates/indexDatatable.js
deleted file mode 100644
index f242e7d8d6..0000000000
--- a/packages/materialdesign-components/src/Templates/indexDatatable.js
+++ /dev/null
@@ -1,80 +0,0 @@
-export default ({ indexes, helpers }) =>
- indexes.map(i => ({
- name: `Table based on view: ${i.name} `,
- props: tableProps(
- i,
- helpers.indexSchema(i).filter(c => !excludedColumns.includes(c.name))
- ),
- }))
-
-const excludedColumns = ["id", "key", "sortKey", "type", "isNew"]
-
-const tableProps = (index, indexSchema) => ({
- _component: "@budibase/materialdesign-components/Datatable",
- _children: [
- {
- _component: "@budibase/materialdesign-components/DatatableHead",
- _children: [
- {
- _component: "@budibase/materialdesign-components/DatatableRow",
- isHeader: true,
- _children: columnHeaders(indexSchema),
- },
- ],
- },
- {
- _component: "@budibase/materialdesign-components/DatatableBody",
- _children: [
- {
- _code: rowCode(index),
- _component: "@budibase/materialdesign-components/DatatableRow",
- _children: dataCells(index, indexSchema),
- },
- ],
- },
- ],
- onLoad: [
- {
- "##eventHandlerType": "List Records",
- parameters: {
- indexKey: index.nodeKey(),
- statePath: index.name,
- },
- },
- ],
-})
-
-const columnHeaders = indexSchema =>
- indexSchema.map(col => ({
- _component: "@budibase/materialdesign-components/DatatableCell",
- isHeader: true,
- _children: [
- {
- _component: "@budibase/standard-components/text",
- type: "none",
- text: col.name,
- formattingTag: "
- bold",
- },
- ],
- }))
-
-const dataCells = (index, indexSchema) =>
- indexSchema.map(col => ({
- _component: "@budibase/materialdesign-components/DatatableCell",
- _children: [
- {
- _component: "@budibase/standard-components/text",
- type: "none",
- text: `context.${dataItem(index)}.${col.name}`,
- },
- ],
- }))
-
-const dataItem = index => `${index.name}_item`
-const dataCollection = index => `state.${index.name}`
-const rowCode = index =>
- `
-if (!${dataCollection(index)}) return
-
-for (let ${dataItem(index)} of ${dataCollection(index)})
- render( { ${dataItem(index)} } )`
diff --git a/packages/materialdesign-components/src/Templates/recordForm.js b/packages/materialdesign-components/src/Templates/recordForm.js
deleted file mode 100644
index 38ab602a8e..0000000000
--- a/packages/materialdesign-components/src/Templates/recordForm.js
+++ /dev/null
@@ -1,149 +0,0 @@
-export default ({ records }) =>
- records.map(r => ({
- name: `Form for Record: ${r.nodeName()}`,
- props: outerContainer(r),
- }))
-
-const outerContainer = record => ({
- _component: "@budibase/standard-components/container",
- _code: "",
- type: "div",
- onLoad: [
- {
- "##eventHandlerType": "Get New Record",
- parameters: {
- collectionKey: record.collectionNodeKey(),
- childRecordType: record.name,
- statePath: record.name,
- },
- },
- ],
- _children: [
- heading(record),
- ...record.fields.map(f => field(record, f)),
- buttons(record),
- ],
-})
-
-const heading = record => ({
- _component: "@budibase/materialdesign-components/H3",
- text: capitalize(record.name),
-})
-
-const field = (record, f) => {
- if (f.type === "bool") return checkbox(record, f)
- if (
- f.type === "string" &&
- f.typeOptions &&
- f.typeOptions.values &&
- f.typeOptions.values.length > 0
- )
- return select(record, f)
- return textField(record, f)
-}
-
-const textField = (record, f) => ({
- _component: "@budibase/materialdesign-components/Textfield",
- label: f.label,
- variant: "filled",
- disabled: false,
- fullwidth: false,
- colour: "primary",
- maxLength:
- f.typeOptions && f.typeOptions.maxLength ? f.typeOptions.maxLength : 0,
- placeholder: f.label,
- value: fieldValueBinding(record, f),
-})
-
-const checkbox = (record, f) => ({
- _component: "@budibase/materialdesign-components/Checkbox",
- label: f.label,
- checked: fieldValueBinding(record, f),
-})
-
-const select = (record, f) => ({
- _component: "@budibase/materialdesign-components/Select",
- value: fieldValueBinding(record, f),
- _children: f.typeOptions.values.map(val => ({
- _component: "@budibase/materialdesign-components/ListItem",
- value: val,
- text: val,
- })),
-})
-
-const fieldValueBinding = (record, f) => `state.${record.name}.${f.name}`
-
-const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1)
-
-const buttons = record => ({
- _component: "@budibase/standard-components/container",
- borderWidth: "1px 0px 0px 0px",
- borderColor: "lightgray",
- borderStyle: "solid",
- _styles: {
- position: {
- column: ["", ""],
- row: ["", ""],
- margin: ["", "", "", ""],
- padding: ["30px", "", "", ""],
- height: [""],
- width: [""],
- zindex: [""],
- },
- layout: {
- templaterows: [""],
- templatecolumns: [""],
- },
- },
- _children: [
- {
- _component: "@budibase/materialdesign-components/Button",
- onClick: [
- {
- "##eventHandlerType": "Save Record",
- parameters: {
- statePath: `${record.name}`,
- },
- },
- {
- "##eventHandlerType": "Navigate To",
- parameters: {
- url: `/${record.name}s`,
- },
- },
- ],
- variant: "raised",
- colour: "primary",
- size: "medium",
- text: `Save ${capitalize(record.name)}`,
- },
- {
- _component: "@budibase/materialdesign-components/Button",
- _styles: {
- position: {
- row: ["", ""],
- column: ["", ""],
- padding: ["", "", "", ""],
- margin: ["", "", "", "10px"],
- width: [""],
- height: [""],
- zindex: [""],
- },
- layout: {
- templatecolumns: [""],
- templaterows: [""],
- },
- },
- onClick: [
- {
- "##eventHandlerType": "Navigate To",
- parameters: {
- url: `/${record.name}s`,
- },
- },
- ],
- colour: "secondary",
- text: "Cancel",
- },
- ],
-})
diff --git a/packages/materialdesign-components/src/index.js b/packages/materialdesign-components/src/index.js
index c53413bd19..a259af171f 100644
--- a/packages/materialdesign-components/src/index.js
+++ b/packages/materialdesign-components/src/index.js
@@ -15,8 +15,6 @@ export {
DatatableCell,
DatatableRow,
} from "./Datatable"
-export { default as indexDatatable } from "./Templates/indexDatatable"
-export { default as recordForm } from "./Templates/recordForm"
export { List, ListItem } from "./List"
export { Menu } from "./Menu"
export { Select } from "./Select"
diff --git a/packages/server/.env.template b/packages/server/.env.template
index bc2e7954a4..170d391520 100644
--- a/packages/server/.env.template
+++ b/packages/server/.env.template
@@ -12,4 +12,7 @@ ADMIN_SECRET={{adminSecret}}
JWT_SECRET={{cookieKey1}}
# port to run http server on
-PORT=4001
\ No newline at end of file
+PORT=4001
+
+# error level for koa-pino
+LOG_LEVEL=error
\ No newline at end of file
diff --git a/packages/server/builder/assets/10-1.png b/packages/server/builder/assets/10-1.png
deleted file mode 100644
index 3531495668..0000000000
Binary files a/packages/server/builder/assets/10-1.png and /dev/null differ
diff --git a/packages/server/builder/assets/budibase-emblem-white.svg b/packages/server/builder/assets/budibase-emblem-white.svg
deleted file mode 100644
index 728e859260..0000000000
--- a/packages/server/builder/assets/budibase-emblem-white.svg
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
diff --git a/packages/server/builder/assets/budibase-logo-only.png b/packages/server/builder/assets/budibase-logo-only.png
deleted file mode 100644
index 652444e5bd..0000000000
Binary files a/packages/server/builder/assets/budibase-logo-only.png and /dev/null differ
diff --git a/packages/server/builder/assets/budibase-logo-white.png b/packages/server/builder/assets/budibase-logo-white.png
deleted file mode 100644
index da8440c09e..0000000000
Binary files a/packages/server/builder/assets/budibase-logo-white.png and /dev/null differ
diff --git a/packages/server/builder/assets/budibase-logo.png b/packages/server/builder/assets/budibase-logo.png
deleted file mode 100644
index 1d6fc3d1cc..0000000000
Binary files a/packages/server/builder/assets/budibase-logo.png and /dev/null differ
diff --git a/packages/server/builder/assets/budibase-logo.svg b/packages/server/builder/assets/budibase-logo.svg
new file mode 100644
index 0000000000..66b5b3bb39
--- /dev/null
+++ b/packages/server/builder/assets/budibase-logo.svg
@@ -0,0 +1,44 @@
+
+
+
diff --git a/packages/server/builder/assets/rocket.jpg b/packages/server/builder/assets/rocket.jpg
new file mode 100644
index 0000000000..cc2edf02a4
Binary files /dev/null and b/packages/server/builder/assets/rocket.jpg differ
diff --git a/packages/server/builder/assets/space.jpeg b/packages/server/builder/assets/space.jpeg
deleted file mode 100644
index 427aeb13c7..0000000000
Binary files a/packages/server/builder/assets/space.jpeg and /dev/null differ
diff --git a/packages/server/builder/assets/space.jpg b/packages/server/builder/assets/space.jpg
deleted file mode 100644
index 6fbde46033..0000000000
Binary files a/packages/server/builder/assets/space.jpg and /dev/null differ
diff --git a/packages/server/builder/assets/spacex.jpg b/packages/server/builder/assets/spacex.jpg
new file mode 100644
index 0000000000..30004eadd9
Binary files /dev/null and b/packages/server/builder/assets/spacex.jpg differ
diff --git a/packages/server/package.json b/packages/server/package.json
index 156a25aa0b..49a5626f6f 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -25,7 +25,7 @@
"scripts": {
"test": "jest routes --runInBand",
"test:integration": "jest workflow --runInBand",
- "test:watch": "jest -w",
+ "test:watch": "jest --watch",
"initialise": "node ../cli/bin/budi init -b local -q",
"budi": "node ../cli/bin/budi",
"dev:builder": "nodemon ../cli/bin/budi run",
@@ -44,7 +44,7 @@
"@budibase/client": "^0.0.32",
"@budibase/core": "^0.0.32",
"@koa/router": "^8.0.0",
- "ajv": "^6.12.2",
+ "@sendgrid/mail": "^7.1.1",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"electron-is-dev": "^1.2.0",
@@ -60,12 +60,14 @@
"koa-session": "^5.12.0",
"koa-static": "^5.0.0",
"lodash": "^4.17.13",
+ "mustache": "^4.0.1",
"pino-pretty": "^4.0.0",
"pouchdb": "^7.2.1",
"pouchdb-all-dbs": "^1.0.2",
"squirrelly": "^7.5.0",
"tar-fs": "^2.0.0",
"uuid": "^3.3.2",
+ "validate.js": "^0.13.1",
"yargs": "^13.2.4",
"zlib": "^1.0.5"
},
diff --git a/packages/server/scripts/jestSetup.js b/packages/server/scripts/jestSetup.js
index 5289aef9f4..ab5bc83885 100644
--- a/packages/server/scripts/jestSetup.js
+++ b/packages/server/scripts/jestSetup.js
@@ -5,3 +5,4 @@ process.env.JWT_SECRET = "test-jwtsecret"
process.env.CLIENT_ID = "test-client-id"
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
process.env.ADMIN_SECRET = "test-admin-secret"
+process.env.LOG_LEVEL = "silent"
diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js
index 63422c468a..0f604be0e7 100644
--- a/packages/server/src/api/controllers/application.js
+++ b/packages/server/src/api/controllers/application.js
@@ -5,11 +5,13 @@ const newid = require("../../db/newid")
const env = require("../../environment")
const instanceController = require("./instance")
const { resolve, join } = require("path")
-const { copy, readJSON, writeJSON, exists } = require("fs-extra")
+const { copy, exists, readFile, writeFile } = require("fs-extra")
+const { budibaseAppsDir } = require("../../utilities/budibaseDir")
const { exec } = require("child_process")
+const sqrl = require("squirrelly")
exports.fetch = async function(ctx) {
- const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
+ const db = new CouchDB(ClientDb.name(getClientId(ctx)))
const body = await db.query("client/by_type", {
include_docs: true,
key: ["app"],
@@ -19,16 +21,30 @@ exports.fetch = async function(ctx) {
}
exports.fetchAppPackage = async function(ctx) {
- const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
+ const clientId = await lookupClientId(ctx.params.applicationId)
+ const db = new CouchDB(ClientDb.name(clientId))
const application = await db.get(ctx.params.applicationId)
ctx.body = await getPackageForBuilder(ctx.config, application)
}
exports.create = async function(ctx) {
- const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
+ const clientId =
+ (ctx.request.body && ctx.request.body.clientId) || env.CLIENT_ID
+
+ if (!clientId) {
+ ctx.throw(400, "ClientId not suplied")
+ }
+ const appId = newid()
+ // insert an appId -> clientId lookup
+ const masterDb = new CouchDB("clientAppLookup")
+ await masterDb.put({
+ _id: appId,
+ clientId,
+ })
+ const db = new CouchDB(ClientDb.name(clientId))
const newApplication = {
- _id: newid(),
+ _id: appId,
type: "app",
instances: [],
userInstanceMap: {},
@@ -45,11 +61,10 @@ exports.create = async function(ctx) {
const createInstCtx = {
params: {
- clientId: env.CLIENT_ID,
applicationId: newApplication._id,
},
request: {
- body: { name: `dev-${env.CLIENT_ID}` },
+ body: { name: `dev-${clientId}` },
},
}
await instanceController.create(createInstCtx)
@@ -59,6 +74,7 @@ exports.create = async function(ctx) {
await runNpmInstall(newAppFolder)
}
+ ctx.status = 200
ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully`
}
@@ -72,26 +88,54 @@ const createEmptyAppPackage = async (ctx, app) => {
"appDirectoryTemplate"
)
- const appsFolder = env.BUDIBASE_DIR
+ const appsFolder = budibaseAppsDir()
const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
- return
}
await copy(templateFolder, newAppFolder)
- const packageJsonPath = join(appsFolder, app._id, "package.json")
- const packageJson = await readJSON(packageJsonPath)
-
- packageJson.name = npmFriendlyAppName(app.name)
-
- await writeJSON(packageJsonPath, packageJson)
+ await updateJsonFile(join(appsFolder, app._id, "package.json"), {
+ name: npmFriendlyAppName(app.name),
+ })
+ await updateJsonFile(
+ join(appsFolder, app._id, "pages", "main", "page.json"),
+ app
+ )
+ await updateJsonFile(
+ join(appsFolder, app._id, "pages", "unauthenticated", "page.json"),
+ app
+ )
return newAppFolder
}
+const lookupClientId = async appId => {
+ const masterDb = new CouchDB("clientAppLookup")
+ const { clientId } = await masterDb.get(appId)
+ return clientId
+}
+
+const getClientId = ctx => {
+ const clientId =
+ (ctx.request.body && ctx.request.body.clientId) ||
+ (ctx.query && ctx.query.clientId) ||
+ env.CLIENT_ID
+
+ if (!clientId) {
+ ctx.throw(400, "ClientId not suplied")
+ }
+ return clientId
+}
+
+const updateJsonFile = async (filePath, app) => {
+ const json = await readFile(filePath, "utf8")
+ const newJson = sqrl.Render(json, app)
+ await writeFile(filePath, newJson, "utf8")
+}
+
const runNpmInstall = async newAppFolder => {
return new Promise((resolve, reject) => {
const cmd = `cd ${newAppFolder} && npm install`
diff --git a/packages/server/src/api/controllers/auth.js b/packages/server/src/api/controllers/auth.js
index b9e1150648..5ec3aea617 100644
--- a/packages/server/src/api/controllers/auth.js
+++ b/packages/server/src/api/controllers/auth.js
@@ -2,7 +2,6 @@ const jwt = require("jsonwebtoken")
const CouchDB = require("../../db")
const ClientDb = require("../../db/clientDb")
const bcrypt = require("../../utilities/bcrypt")
-const env = require("../../environment")
exports.authenticate = async ctx => {
const { username, password } = ctx.request.body
@@ -10,12 +9,15 @@ exports.authenticate = async ctx => {
if (!username) ctx.throw(400, "Username Required.")
if (!password) ctx.throw(400, "Password Required")
- // TODO: Don't use this. It can't be relied on
- const referer = ctx.request.headers.referer.split("/")
- const appId = referer[3]
+ const masterDb = new CouchDB("clientAppLookup")
+ const { clientId } = await masterDb.get(ctx.params.appId)
+ if (!clientId) {
+ ctx.throw(400, "ClientId not suplied")
+ }
// find the instance that the user is associated with
- const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
+ const db = new CouchDB(ClientDb.name(clientId))
+ const appId = ctx.params.appId
const app = await db.get(appId)
const instanceId = app.userInstanceMap[username]
@@ -24,14 +26,15 @@ exports.authenticate = async ctx => {
// Check the user exists in the instance DB by username
const instanceDb = new CouchDB(instanceId)
- const { rows } = await instanceDb.query("database/by_username", {
- include_docs: true,
- username,
- })
- if (rows.length === 0) ctx.throw(500, `User does not exist.`)
-
- const dbUser = rows[0].doc
+ let dbUser
+ try {
+ dbUser = await instanceDb.get(`user_${username}`)
+ } catch (_) {
+ // do not want to throw a 404 - as this could be
+ // used to dtermine valid usernames
+ ctx.throw(401, "Invalid Credentials")
+ }
// authenticate
if (await bcrypt.compare(password, dbUser.password)) {
diff --git a/packages/server/src/api/controllers/client.js b/packages/server/src/api/controllers/client.js
index 433bf58c3b..400ccde358 100644
--- a/packages/server/src/api/controllers/client.js
+++ b/packages/server/src/api/controllers/client.js
@@ -6,13 +6,26 @@ exports.getClientId = async function(ctx) {
}
exports.create = async function(ctx) {
- await create(env.CLIENT_ID)
+ const clientId = getClientId(ctx)
+ await create(clientId)
ctx.status = 200
- ctx.message = `Client Database ${env.CLIENT_ID} successfully provisioned.`
+ ctx.message = `Client Database ${clientId} successfully provisioned.`
}
exports.destroy = async function(ctx) {
- await destroy(env.CLIENT_ID)
+ const clientId = getClientId(ctx)
+ await destroy(clientId)
ctx.status = 200
- ctx.message = `Client Database ${env.CLIENT_ID} successfully deleted.`
+ ctx.message = `Client Database ${clientId} successfully deleted.`
+}
+
+const getClientId = ctx => {
+ const clientId =
+ (ctx.query && ctx.query.clientId) ||
+ (ctx.body && ctx.body.clientId) ||
+ env.CLIENT_ID
+ if (!clientId) {
+ ctx.throw(400, "ClientId not suplied")
+ }
+ return clientId
}
diff --git a/packages/server/src/api/controllers/component.js b/packages/server/src/api/controllers/component.js
index 0b07826555..a3b4d9c2bb 100644
--- a/packages/server/src/api/controllers/component.js
+++ b/packages/server/src/api/controllers/component.js
@@ -5,10 +5,11 @@ const {
budibaseTempDir,
budibaseAppsDir,
} = require("../../utilities/budibaseDir")
-const env = require("../../environment")
exports.fetchAppComponentDefinitions = async function(ctx) {
- const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
+ const masterDb = new CouchDB("clientAppLookup")
+ const { clientId } = await masterDb.get(ctx.params.appId)
+ const db = new CouchDB(ClientDb.name(clientId))
const app = await db.get(ctx.params.appId)
const componentDefinitions = app.componentLibraries.reduce(
diff --git a/packages/server/src/api/controllers/instance.js b/packages/server/src/api/controllers/instance.js
index 573860438c..155437371f 100644
--- a/packages/server/src/api/controllers/instance.js
+++ b/packages/server/src/api/controllers/instance.js
@@ -1,14 +1,16 @@
const CouchDB = require("../../db")
const client = require("../../db/clientDb")
const newid = require("../../db/newid")
-const env = require("../../environment")
exports.create = async function(ctx) {
const instanceName = ctx.request.body.name
const appShortId = ctx.params.applicationId.substring(0, 7)
const instanceId = `inst_${appShortId}_${newid()}`
const { applicationId } = ctx.params
- const clientId = env.CLIENT_ID
+
+ const masterDb = new CouchDB("clientAppLookup")
+ const { clientId } = await masterDb.get(applicationId)
+
const db = new CouchDB(instanceId)
await db.put({
_id: "_design/database",
@@ -29,6 +31,16 @@ exports.create = async function(ctx) {
emit([doc.type], doc._id)
}.toString(),
},
+ by_workflow_trigger: {
+ map: function(doc) {
+ if (doc.type === "workflow") {
+ const trigger = doc.definition.trigger
+ if (trigger) {
+ emit([trigger.event], trigger)
+ }
+ }
+ }.toString(),
+ },
},
})
diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js
index a30026a645..650342b33c 100644
--- a/packages/server/src/api/controllers/model.js
+++ b/packages/server/src/api/controllers/model.js
@@ -10,6 +10,12 @@ exports.fetch = async function(ctx) {
ctx.body = body.rows.map(row => row.doc)
}
+exports.find = async function(ctx) {
+ const db = new CouchDB(ctx.params.instanceId)
+ const model = await db.get(ctx.params.id)
+ ctx.body = model
+}
+
exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const newModel = {
@@ -21,6 +27,23 @@ exports.create = async function(ctx) {
const result = await db.post(newModel)
newModel._rev = result.rev
+ const { schema } = ctx.request.body
+ for (let key in schema) {
+ // model has a linked record
+ if (schema[key].type === "link") {
+ // create the link field in the other model
+ const linkedModel = await db.get(schema[key].modelId)
+ linkedModel.schema[newModel.name] = {
+ type: "link",
+ modelId: newModel._id,
+ constraints: {
+ type: "array",
+ },
+ }
+ await db.put(linkedModel)
+ }
+ }
+
const designDoc = await db.get("_design/database")
designDoc.views = {
...designDoc.views,
@@ -44,7 +67,10 @@ exports.update = async function() {}
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
- await db.remove(ctx.params.modelId, ctx.params.revId)
+ const modelToDelete = await db.get(ctx.params.modelId)
+
+ await db.remove(modelToDelete)
+
const modelViewId = `all_${ctx.params.modelId}`
// Delete all records for that model
@@ -53,6 +79,16 @@ exports.destroy = async function(ctx) {
records.rows.map(record => ({ id: record.id, _deleted: true }))
)
+ // Delete linked record fields in dependent models
+ for (let key in modelToDelete.schema) {
+ const { type, modelId } = modelToDelete.schema[key]
+ if (type === "link") {
+ const linkedModel = await db.get(modelId)
+ delete linkedModel.schema[modelToDelete.name]
+ await db.put(linkedModel)
+ }
+ }
+
// delete the "all" view
const designDoc = await db.get("_design/database")
delete designDoc.views[modelViewId]
diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js
index 7a5b045136..cf8eb27606 100644
--- a/packages/server/src/api/controllers/record.js
+++ b/packages/server/src/api/controllers/record.js
@@ -1,9 +1,7 @@
const CouchDB = require("../../db")
-const Ajv = require("ajv")
+const validateJs = require("validate.js")
const newid = require("../../db/newid")
-const ajv = new Ajv()
-
exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body
@@ -13,18 +11,18 @@ exports.save = async function(ctx) {
record._id = newid()
}
- // validation with ajv
const model = await db.get(record.modelId)
- const validate = ajv.compile({
- properties: model.schema,
- })
- const valid = validate(record)
- if (!valid) {
+ const validateResult = await validate({
+ record,
+ model,
+ })
+
+ if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
- errors: validate.errors,
+ errors: validateResult.errors,
}
return
}
@@ -44,6 +42,12 @@ exports.save = async function(ctx) {
record.type = "record"
const response = await db.post(record)
record._rev = response.rev
+
+ ctx.eventEmitter &&
+ ctx.eventEmitter.emit(`record:save`, {
+ record,
+ instanceId: ctx.params.instanceId,
+ })
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} created successfully`
@@ -57,7 +61,7 @@ exports.fetchView = async function(ctx) {
ctx.body = response.rows.map(row => row.doc)
}
-exports.fetchModel = async function(ctx) {
+exports.fetchModelRecords = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/all_${ctx.params.modelId}`, {
include_docs: true,
@@ -65,6 +69,15 @@ exports.fetchModel = async function(ctx) {
ctx.body = response.rows.map(row => row.doc)
}
+exports.search = async function(ctx) {
+ const db = new CouchDB(ctx.params.instanceId)
+ const response = await db.allDocs({
+ include_docs: true,
+ ...ctx.request.body,
+ })
+ ctx.body = response.rows.map(row => row.doc)
+}
+
exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const record = await db.get(ctx.params.recordId)
@@ -83,4 +96,31 @@ exports.destroy = async function(ctx) {
return
}
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
+ ctx.eventEmitter && ctx.eventEmitter.emit(`record:delete`, record)
+}
+
+exports.validate = async function(ctx) {
+ const errors = await validate({
+ instanceId: ctx.params.instanceId,
+ modelId: ctx.params.modelId,
+ record: ctx.request.body,
+ })
+ ctx.status = 200
+ ctx.body = errors
+}
+
+async function validate({ instanceId, modelId, record, model }) {
+ if (!model) {
+ const db = new CouchDB(instanceId)
+ model = await db.get(modelId)
+ }
+ const errors = {}
+ for (let fieldName in model.schema) {
+ const res = validateJs.single(
+ record[fieldName],
+ model.schema[fieldName].constraints
+ )
+ if (res) errors[fieldName] = res
+ }
+ return { valid: Object.keys(errors).length === 0, errors }
}
diff --git a/packages/server/src/api/controllers/static.js b/packages/server/src/api/controllers/static.js
index 32542de546..557a9efe7a 100644
--- a/packages/server/src/api/controllers/static.js
+++ b/packages/server/src/api/controllers/static.js
@@ -13,7 +13,6 @@ exports.serveBuilder = async function(ctx) {
}
exports.serveApp = async function(ctx) {
- // TODO: update homedir stuff to wherever budi is run
// default to homedir
const appPath = resolve(
budibaseAppsDir(),
@@ -26,7 +25,6 @@ exports.serveApp = async function(ctx) {
}
exports.serveComponentLibrary = async function(ctx) {
- // TODO: update homedir stuff to wherever budi is run
// default to homedir
let componentLibraryPath = resolve(
budibaseAppsDir(),
diff --git a/packages/server/src/api/controllers/user.js b/packages/server/src/api/controllers/user.js
index 5810872989..1b66cf1cca 100644
--- a/packages/server/src/api/controllers/user.js
+++ b/packages/server/src/api/controllers/user.js
@@ -1,7 +1,6 @@
const CouchDB = require("../../db")
const clientDb = require("../../db/clientDb")
const bcrypt = require("../../utilities/bcrypt")
-const env = require("../../environment")
const getUserId = userName => `user_${userName}`
const {
POWERUSER_LEVEL_ID,
@@ -42,8 +41,11 @@ exports.create = async function(ctx) {
const response = await database.post(user)
+ const masterDb = new CouchDB("clientAppLookup")
+ const { clientId } = await masterDb.get(appId)
+
// the clientDB needs to store a map of users against the app
- const db = new CouchDB(clientDb.name(env.CLIENT_ID))
+ const db = new CouchDB(clientDb.name(clientId))
const app = await db.get(appId)
app.userInstanceMap = {
diff --git a/packages/server/src/api/controllers/view.js b/packages/server/src/api/controllers/view.js
index 8456837c6a..9e847da358 100644
--- a/packages/server/src/api/controllers/view.js
+++ b/packages/server/src/api/controllers/view.js
@@ -11,7 +11,8 @@ const controller = {
if (
!name.startsWith("all") &&
name !== "by_type" &&
- name !== "by_username"
+ name !== "by_username" &&
+ name !== "by_workflow_trigger"
) {
response.push({
name,
diff --git a/packages/server/src/api/controllers/workflow/actions/CREATE_USER.js b/packages/server/src/api/controllers/workflow/actions/CREATE_USER.js
new file mode 100644
index 0000000000..be78275133
--- /dev/null
+++ b/packages/server/src/api/controllers/workflow/actions/CREATE_USER.js
@@ -0,0 +1,24 @@
+const userController = require("../../user")
+
+module.exports = async function createUser({ args, instanceId }) {
+ const ctx = {
+ params: {
+ instanceId,
+ },
+ request: {
+ body: args.user,
+ },
+ }
+
+ try {
+ const response = await userController.create(ctx)
+ return {
+ user: response,
+ }
+ } catch (err) {
+ console.error(err)
+ return {
+ user: null,
+ }
+ }
+}
diff --git a/packages/server/src/api/controllers/workflow/actions/SAVE_RECORD.js b/packages/server/src/api/controllers/workflow/actions/SAVE_RECORD.js
new file mode 100644
index 0000000000..45e0c1ee65
--- /dev/null
+++ b/packages/server/src/api/controllers/workflow/actions/SAVE_RECORD.js
@@ -0,0 +1,29 @@
+const recordController = require("../../record")
+
+module.exports = async function saveRecord({ args, instanceId }) {
+ const { model, ...record } = args.record
+
+ const ctx = {
+ params: {
+ instanceId,
+ modelId: model._id,
+ },
+ request: {
+ body: record,
+ },
+ }
+
+ await recordController.save(ctx)
+
+ try {
+ return {
+ record: ctx.body,
+ }
+ } catch (err) {
+ console.error(err)
+ return {
+ record: null,
+ error: err.message,
+ }
+ }
+}
diff --git a/packages/server/src/api/controllers/workflow/actions/SEND_EMAIL.js b/packages/server/src/api/controllers/workflow/actions/SEND_EMAIL.js
new file mode 100644
index 0000000000..39cb7a8432
--- /dev/null
+++ b/packages/server/src/api/controllers/workflow/actions/SEND_EMAIL.js
@@ -0,0 +1,26 @@
+const sgMail = require("@sendgrid/mail")
+
+sgMail.setApiKey(process.env.SENDGRID_API_KEY)
+
+module.exports = async function sendEmail({ args }) {
+ const msg = {
+ to: args.to,
+ from: args.from,
+ subject: args.subject,
+ text: args.text,
+ }
+
+ try {
+ await sgMail.send(msg)
+ return {
+ success: true,
+ ...args,
+ }
+ } catch (err) {
+ console.error(err)
+ return {
+ success: false,
+ error: err.message,
+ }
+ }
+}
diff --git a/packages/server/src/api/controllers/workflow.js b/packages/server/src/api/controllers/workflow/index.js
similarity index 52%
rename from packages/server/src/api/controllers/workflow.js
rename to packages/server/src/api/controllers/workflow/index.js
index 61543afc0d..f1dd1bcb89 100644
--- a/packages/server/src/api/controllers/workflow.js
+++ b/packages/server/src/api/controllers/workflow/index.js
@@ -1,5 +1,5 @@
-const CouchDB = require("../../db")
-const newid = require("../../db/newid")
+const CouchDB = require("../../../db")
+const newid = require("../../../db/newid")
exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
@@ -7,24 +7,6 @@ exports.create = async function(ctx) {
workflow._id = newid()
- // TODO: Possibly validate the workflow against a schema
-
- // // validation with ajv
- // const model = await db.get(record.modelId)
- // const validate = ajv.compile({
- // properties: model.schema,
- // })
- // const valid = validate(record)
-
- // if (!valid) {
- // ctx.status = 400
- // ctx.body = {
- // status: 400,
- // errors: validate.errors,
- // }
- // return
- // }
-
workflow.type = "workflow"
const response = await db.post(workflow)
workflow._rev = response.rev
@@ -41,23 +23,51 @@ exports.create = async function(ctx) {
exports.update = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
- ctx.body = await db.get(ctx.params.recordId)
+ const workflow = ctx.request.body
+
+ const response = await db.put(workflow)
+ workflow._rev = response.rev
+
+ ctx.status = 200
+ ctx.body = {
+ message: `Workflow ${workflow._id} updated successfully.`,
+ workflow: {
+ ...workflow,
+ _rev: response.rev,
+ _id: response.id,
+ },
+ }
}
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/by_type`, {
- type: "workflow",
+ key: ["workflow"],
include_docs: true,
})
ctx.body = response.rows.map(row => row.doc)
}
exports.find = async function(ctx) {
- const db = new CouchDB(ctx.params.instanceId)
+ const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.get(ctx.params.id)
}
+exports.executeAction = async function(ctx) {
+ const { args, action } = ctx.request.body
+ const workflowAction = require(`./actions/${action}`)
+ const response = await workflowAction({
+ args,
+ instanceId: ctx.user.instanceId,
+ })
+ ctx.body = response
+}
+
+exports.fetchActionScript = async function(ctx) {
+ const workflowAction = require(`./actions/${ctx.action}`)
+ ctx.body = workflowAction
+}
+
exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId)
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js
index 66c3168f23..e6143d6725 100644
--- a/packages/server/src/api/index.js
+++ b/packages/server/src/api/index.js
@@ -10,6 +10,7 @@ const {
instanceRoutes,
clientRoutes,
applicationRoutes,
+ recordRoutes,
modelRoutes,
viewRoutes,
staticRoutes,
@@ -38,6 +39,7 @@ router
ctx.config = {
latestPackagesFolder: budibaseAppsDir(),
jwtSecret: env.JWT_SECRET,
+ useAppRootPath: true,
}
ctx.isDev = env.NODE_ENV !== "production" && env.NODE_ENV !== "jest"
await next()
@@ -68,6 +70,9 @@ router.use(viewRoutes.allowedMethods())
router.use(modelRoutes.routes())
router.use(modelRoutes.allowedMethods())
+router.use(recordRoutes.routes())
+router.use(recordRoutes.allowedMethods())
+
router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods())
diff --git a/packages/server/src/api/routes/auth.js b/packages/server/src/api/routes/auth.js
index b4b68e8929..fa95a3a5e6 100644
--- a/packages/server/src/api/routes/auth.js
+++ b/packages/server/src/api/routes/auth.js
@@ -3,6 +3,6 @@ const controller = require("../controllers/auth")
const router = Router()
-router.post("/api/authenticate", controller.authenticate)
+router.post("/:appId/api/authenticate", controller.authenticate)
module.exports = router
diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js
index c515d5f437..b50fee788a 100644
--- a/packages/server/src/api/routes/index.js
+++ b/packages/server/src/api/routes/index.js
@@ -5,6 +5,7 @@ const instanceRoutes = require("./instance")
const clientRoutes = require("./client")
const applicationRoutes = require("./application")
const modelRoutes = require("./model")
+const recordRoutes = require("./record")
const viewRoutes = require("./view")
const staticRoutes = require("./static")
const componentRoutes = require("./component")
@@ -18,6 +19,7 @@ module.exports = {
instanceRoutes,
clientRoutes,
applicationRoutes,
+ recordRoutes,
modelRoutes,
viewRoutes,
staticRoutes,
diff --git a/packages/server/src/api/routes/model.js b/packages/server/src/api/routes/model.js
index d9eb5cf798..f1ec46dbe5 100644
--- a/packages/server/src/api/routes/model.js
+++ b/packages/server/src/api/routes/model.js
@@ -1,43 +1,13 @@
const Router = require("@koa/router")
const modelController = require("../controllers/model")
-const recordController = require("../controllers/record")
const authorized = require("../../middleware/authorized")
-const {
- READ_MODEL,
- WRITE_MODEL,
- BUILDER,
-} = require("../../utilities/accessLevels")
+const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
-// records
-
-router
- .get(
- "/api/:instanceId/:modelId/records",
- authorized(READ_MODEL, ctx => ctx.params.modelId),
- recordController.fetchModel
- )
- .get(
- "/api/:instanceId/:modelId/records/:recordId",
- authorized(READ_MODEL, ctx => ctx.params.modelId),
- recordController.find
- )
- .post(
- "/api/:instanceId/:modelId/records",
- authorized(WRITE_MODEL, ctx => ctx.params.modelId),
- recordController.save
- )
- .delete(
- "/api/:instanceId/:modelId/records/:recordId/:revId",
- authorized(WRITE_MODEL, ctx => ctx.params.modelId),
- recordController.destroy
- )
-
-// models
-
router
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
+ .get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find)
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
// .patch("/api/:instanceId/models", controller.update)
.delete(
diff --git a/packages/server/src/api/routes/record.js b/packages/server/src/api/routes/record.js
new file mode 100644
index 0000000000..d555d3d8c8
--- /dev/null
+++ b/packages/server/src/api/routes/record.js
@@ -0,0 +1,36 @@
+const Router = require("@koa/router")
+const recordController = require("../controllers/record")
+const authorized = require("../../middleware/authorized")
+const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels")
+
+const router = Router()
+
+router
+ .get(
+ "/api/:instanceId/:modelId/records",
+ authorized(READ_MODEL, ctx => ctx.params.modelId),
+ recordController.fetchModelRecords
+ )
+ .get(
+ "/api/:instanceId/:modelId/records/:recordId",
+ authorized(READ_MODEL, ctx => ctx.params.modelId),
+ recordController.find
+ )
+ .post("/api/:instanceId/records/search", recordController.search)
+ .post(
+ "/api/:instanceId/:modelId/records",
+ authorized(WRITE_MODEL, ctx => ctx.params.modelId),
+ recordController.save
+ )
+ .post(
+ "/api/:instanceId/:modelId/records/validate",
+ authorized(WRITE_MODEL, ctx => ctx.params.modelId),
+ recordController.validate
+ )
+ .delete(
+ "/api/:instanceId/:modelId/records/:recordId/:revId",
+ authorized(WRITE_MODEL, ctx => ctx.params.modelId),
+ recordController.destroy
+ )
+
+module.exports = router
diff --git a/packages/server/src/api/routes/tests/application.spec.js b/packages/server/src/api/routes/tests/application.spec.js
index 4e6766f71c..7f64419778 100644
--- a/packages/server/src/api/routes/tests/application.spec.js
+++ b/packages/server/src/api/routes/tests/application.spec.js
@@ -1,9 +1,12 @@
const {
createClientDatabase,
createApplication,
+ createInstance,
destroyClientDatabase,
+ builderEndpointShouldBlockNormalUsers,
supertest,
- defaultHeaders
+ TEST_CLIENT_ID,
+ defaultHeaders,
} = require("./couchTestUtils")
describe("/applications", () => {
@@ -37,6 +40,19 @@ describe("/applications", () => {
expect(res.res.statusMessage).toEqual("Application My App created successfully")
expect(res.body._id).toBeDefined()
})
+
+ it("should apply authorization to endpoint", async () => {
+ const otherApplication = await createApplication(request)
+ const instance = await createInstance(request, otherApplication._id)
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "POST",
+ url: `/api/applications`,
+ instanceId: instance._id,
+ body: { name: "My App" }
+ })
+ })
+
})
describe("fetch", () => {
@@ -53,6 +69,48 @@ describe("/applications", () => {
expect(res.body.length).toBe(2)
})
+
+ it("lists only applications in requested client databse", async () => {
+ await createApplication(request, "app1")
+ await createClientDatabase("new_client")
+
+ const blah = await request
+ .post("/api/applications")
+ .send({ name: "app2", clientId: "new_client"})
+ .set(defaultHeaders)
+ .expect('Content-Type', /json/)
+ //.expect(200)
+
+ const client1Res = await request
+ .get(`/api/applications?clientId=${TEST_CLIENT_ID}`)
+ .set(defaultHeaders)
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ expect(client1Res.body.length).toBe(1)
+ expect(client1Res.body[0].name).toBe("app1")
+
+ const client2Res = await request
+ .get(`/api/applications?clientId=new_client`)
+ .set(defaultHeaders)
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ expect(client2Res.body.length).toBe(1)
+ expect(client2Res.body[0].name).toBe("app2")
+
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ const otherApplication = await createApplication(request)
+ const instance = await createInstance(request, otherApplication._id)
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "GET",
+ url: `/api/applications`,
+ instanceId: instance._id,
+ })
+ })
})
})
diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js
index de4ed7f87e..495b841b10 100644
--- a/packages/server/src/api/routes/tests/couchTestUtils.js
+++ b/packages/server/src/api/routes/tests/couchTestUtils.js
@@ -2,10 +2,14 @@ const CouchDB = require("../../../db")
const { create, destroy } = require("../../../db/clientDb")
const supertest = require("supertest")
const app = require("../../../app")
-const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
+const {
+ POWERUSER_LEVEL_ID,
+ generateAdminPermissions,
+} = require("../../../utilities/accessLevels")
const TEST_CLIENT_ID = "test-client-id"
+exports.TEST_CLIENT_ID = TEST_CLIENT_ID
exports.supertest = async () => {
let request
let port = 4002
@@ -19,6 +23,7 @@ exports.supertest = async () => {
exports.defaultHeaders = {
Accept: "application/json",
Cookie: ["builder:token=test-admin-secret"],
+ "x-user-agent": "Budibase Builder",
}
exports.createModel = async (request, instanceId, model) => {
@@ -27,7 +32,12 @@ exports.createModel = async (request, instanceId, model) => {
type: "model",
key: "name",
schema: {
- name: { type: "string" },
+ name: {
+ type: "text",
+ constraints: {
+ type: "string",
+ },
+ },
},
}
@@ -50,7 +60,7 @@ exports.createView = async (request, instanceId, view) => {
return res.body
}
-exports.createClientDatabase = async () => await create(TEST_CLIENT_ID)
+exports.createClientDatabase = async id => await create(id || TEST_CLIENT_ID)
exports.createApplication = async (request, name = "test_application") => {
const res = await request
@@ -77,8 +87,8 @@ exports.createInstance = async (request, appId) => {
exports.createUser = async (
request,
instanceId,
- username = "bill",
- password = "bills_password"
+ username = "babs",
+ password = "babs_password"
) => {
const res = await request
.post(`/api/${instanceId}/users`)
@@ -92,6 +102,149 @@ exports.createUser = async (
return res.body
}
+const createUserWithOnePermission = async (
+ request,
+ instanceId,
+ permName,
+ itemId
+) => {
+ let permissions = await generateAdminPermissions(instanceId)
+ permissions = permissions.filter(
+ p => p.name === permName && p.itemId === itemId
+ )
+
+ return await createUserWithPermissions(
+ request,
+ instanceId,
+ permissions,
+ "onePermOnlyUser"
+ )
+}
+
+const createUserWithAdminPermissions = async (request, instanceId) => {
+ let permissions = await generateAdminPermissions(instanceId)
+
+ return await createUserWithPermissions(
+ request,
+ instanceId,
+ permissions,
+ "adminUser"
+ )
+}
+
+const createUserWithAllPermissionExceptOne = async (
+ request,
+ instanceId,
+ permName,
+ itemId
+) => {
+ let permissions = await generateAdminPermissions(instanceId)
+ permissions = permissions.filter(
+ p => !(p.name === permName && p.itemId === itemId)
+ )
+
+ return await createUserWithPermissions(
+ request,
+ instanceId,
+ permissions,
+ "allPermsExceptOneUser"
+ )
+}
+
+const createUserWithPermissions = async (
+ request,
+ instanceId,
+ permissions,
+ username
+) => {
+ const accessRes = await request
+ .post(`/api/${instanceId}/accesslevels`)
+ .send({ name: "TestLevel", permissions })
+ .set(exports.defaultHeaders)
+
+ const password = `password_${username}`
+ await request
+ .post(`/api/${instanceId}/users`)
+ .set(exports.defaultHeaders)
+ .send({
+ name: username,
+ username,
+ password,
+ accessLevelId: accessRes.body._id,
+ })
+
+ const db = new CouchDB(instanceId)
+ const designDoc = await db.get("_design/database")
+
+ const loginResult = await request
+ .post(`/${designDoc.metadata.applicationId}/api/authenticate`)
+ .send({ username, password })
+
+ // returning necessary request headers
+ return {
+ Accept: "application/json",
+ Cookie: loginResult.headers["set-cookie"],
+ }
+}
+
+exports.testPermissionsForEndpoint = async ({
+ request,
+ method,
+ url,
+ body,
+ instanceId,
+ permissionName,
+ itemId,
+}) => {
+ const headers = await createUserWithOnePermission(
+ request,
+ instanceId,
+ permissionName,
+ itemId
+ )
+
+ await createRequest(request, method, url, body)
+ .set(headers)
+ .expect(200)
+
+ const noPermsHeaders = await createUserWithAllPermissionExceptOne(
+ request,
+ instanceId,
+ permissionName,
+ itemId
+ )
+
+ await createRequest(request, method, url, body)
+ .set(noPermsHeaders)
+ .expect(403)
+}
+
+exports.builderEndpointShouldBlockNormalUsers = async ({
+ request,
+ method,
+ url,
+ body,
+ instanceId,
+}) => {
+ const headers = await createUserWithAdminPermissions(request, instanceId)
+
+ await createRequest(request, method, url, body)
+ .set(headers)
+ .expect(403)
+}
+
+const createRequest = (request, method, url, body) => {
+ let req
+
+ if (method === "POST") req = request.post(url).send(body)
+ else if (method === "GET") req = request.get(url)
+ else if (method === "DELETE") req = request.delete(url)
+ else if (method === "PATCH") req = request.patch(url).send(body)
+ else if (method === "PUT") req = request.put(url).send(body)
+
+ return req
+}
+
exports.insertDocument = async (databaseId, document) => {
const { id, ...documentFields } = document
return await new CouchDB(databaseId).put({ _id: id, ...documentFields })
@@ -100,3 +253,7 @@ exports.insertDocument = async (databaseId, document) => {
exports.destroyDocument = async (databaseId, documentId) => {
return await new CouchDB(databaseId).destroy(documentId)
}
+
+exports.getDocument = async (databaseId, documentId) => {
+ return await new CouchDB(databaseId).get(documentId)
+}
diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js
index 3e224536ed..7134245fb3 100644
--- a/packages/server/src/api/routes/tests/model.spec.js
+++ b/packages/server/src/api/routes/tests/model.spec.js
@@ -3,8 +3,10 @@ const {
createModel,
supertest,
createClientDatabase,
- createApplication ,
- defaultHeaders
+ createApplication,
+ defaultHeaders,
+ builderEndpointShouldBlockNormalUsers,
+ getDocument
} = require("./couchTestUtils")
describe("/models", () => {
@@ -48,6 +50,22 @@ describe("/models", () => {
done();
});
})
+
+ it("should apply authorization to endpoint", async () => {
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "POST",
+ url: `/api/${instance._id}/models`,
+ instanceId: instance._id,
+ body: {
+ name: "TestModel",
+ key: "name",
+ schema: {
+ name: { type: "string" }
+ }
+ }
+ })
+ })
});
describe("fetch", () => {
@@ -70,6 +88,15 @@ describe("/models", () => {
expect(fetchedModel.type).toEqual("model");
done();
});
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "GET",
+ url: `/api/${instance._id}/models`,
+ instanceId: instance._id,
+ })
})
});
@@ -81,7 +108,11 @@ describe("/models", () => {
testModel = await createModel(request, instance._id, testModel)
});
- it("returns a success response when a model is deleted.", done => {
+ afterEach(() => {
+ delete testModel._rev
+ })
+
+ it("returns a success response when a model is deleted.", async done => {
request
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
.set(defaultHeaders)
@@ -92,5 +123,50 @@ describe("/models", () => {
done();
});
})
- });
+
+ it("deletes linked references to the model after deletion", async done => {
+ const linkedModel = await createModel(request, instance._id, {
+ name: "LinkedModel",
+ type: "model",
+ key: "name",
+ schema: {
+ name: {
+ type: "text",
+ constraints: {
+ type: "string",
+ },
+ },
+ TestModel: {
+ type: "link",
+ modelId: testModel._id,
+ constraints: {
+ type: "array"
+ }
+ }
+ },
+ })
+
+ request
+ .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
+ .set(defaultHeaders)
+ .expect('Content-Type', /json/)
+ .expect(200)
+ .end(async (_, res) => {
+ expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`);
+ const dependentModel = await getDocument(instance._id, linkedModel._id)
+ expect(dependentModel.schema.TestModel).not.toBeDefined();
+ done();
+ });
+ })
+
+ it("should apply authorization to endpoint", async () => {
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "DELETE",
+ url: `/api/${instance._id}/models/${testModel._id}/${testModel._rev}`,
+ instanceId: instance._id,
+ })
+ })
+
+ });
});
diff --git a/packages/server/src/api/routes/tests/record.spec.js b/packages/server/src/api/routes/tests/record.spec.js
index 16af0b6031..22ac67ecdc 100644
--- a/packages/server/src/api/routes/tests/record.spec.js
+++ b/packages/server/src/api/routes/tests/record.spec.js
@@ -25,17 +25,17 @@ describe("/records", () => {
server.close();
})
- describe("save, load, update, delete", () => {
+ beforeEach(async () => {
+ instance = await createInstance(request, app._id)
+ model = await createModel(request, instance._id)
+ record = {
+ name: "Test Contact",
+ status: "new",
+ modelId: model._id
+ }
+ })
- beforeEach(async () => {
- instance = await createInstance(request, app._id)
- model = await createModel(request, instance._id)
- record = {
- name: "Test Contact",
- status: "new",
- modelId: model._id
- }
- })
+ describe("save, load, update, delete", () => {
const createRecord = async r =>
await request
@@ -110,6 +110,30 @@ describe("/records", () => {
expect(res.body.find(r => r.name === record.name)).toBeDefined()
})
+ it("lists records when queried by their ID", async () => {
+ const newRecord = {
+ modelId: model._id,
+ name: "Second Contact",
+ status: "new"
+ }
+ const record = await createRecord()
+ const secondRecord = await createRecord(newRecord)
+
+ const recordIds = [record.body._id, secondRecord.body._id]
+
+ const res = await request
+ .post(`/api/${instance._id}/records/search`)
+ .set(defaultHeaders)
+ .send({
+ keys: recordIds
+ })
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ expect(res.body.length).toBe(2)
+ expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(recordIds))
+ })
+
it("load should return 404 when record does not exist", async () => {
await createRecord()
await request
@@ -119,4 +143,31 @@ describe("/records", () => {
.expect(404)
})
})
-})
+
+ describe("validate", () => {
+ it("should return no errors on valid record", async () => {
+ const result = await request
+ .post(`/api/${instance._id}/${model._id}/records/validate`)
+ .send({ name: "ivan" })
+ .set(defaultHeaders)
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ expect(result.body.valid).toBe(true)
+ expect(Object.keys(result.body.errors)).toEqual([])
+ })
+
+ it("should errors on invalid record", async () => {
+ const result = await request
+ .post(`/api/${instance._id}/${model._id}/records/validate`)
+ .send({ name: 1 })
+ .set(defaultHeaders)
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ expect(result.body.valid).toBe(false)
+ expect(Object.keys(result.body.errors)).toEqual(["name"])
+
+ })
+ })
+})
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js
index f994798c2e..d950f03c3d 100644
--- a/packages/server/src/api/routes/tests/user.spec.js
+++ b/packages/server/src/api/routes/tests/user.spec.js
@@ -5,8 +5,13 @@ const {
supertest,
defaultHeaders,
createUser,
+ testPermissionsForEndpoint,
} = require("./couchTestUtils")
-const { POWERUSER_LEVEL_ID } = require("../../../utilities/accessLevels")
+const {
+ POWERUSER_LEVEL_ID,
+ LIST_USERS,
+ USER_MANAGEMENT
+} = require("../../../utilities/accessLevels")
describe("/users", () => {
let request
@@ -44,6 +49,17 @@ describe("/users", () => {
expect(res.body.find(u => u.username === "pam")).toBeDefined()
})
+ it("should apply authorization to endpoint", async () => {
+ await createUser(request, instance._id, "brenda", "brendas_password")
+ await testPermissionsForEndpoint({
+ request,
+ method: "GET",
+ url: `/api/${instance._id}/users`,
+ instanceId: instance._id,
+ permissionName: LIST_USERS,
+ })
+ })
+
})
describe("create", () => {
@@ -59,5 +75,17 @@ describe("/users", () => {
expect(res.res.statusMessage).toEqual("User created successfully.");
expect(res.body._id).toBeUndefined()
})
+
+ it("should apply authorization to endpoint", async () => {
+ await testPermissionsForEndpoint({
+ request,
+ method: "POST",
+ body: { name: "brandNewUser", username: "brandNewUser", password: "yeeooo", accessLevelId: POWERUSER_LEVEL_ID },
+ url: `/api/${instance._id}/users`,
+ instanceId: instance._id,
+ permissionName: USER_MANAGEMENT,
+ })
+ })
+
});
})
diff --git a/packages/server/src/api/routes/tests/view.spec.js b/packages/server/src/api/routes/tests/view.spec.js
index e2aeececce..b8fd47972a 100644
--- a/packages/server/src/api/routes/tests/view.spec.js
+++ b/packages/server/src/api/routes/tests/view.spec.js
@@ -70,4 +70,4 @@ describe("/views", () => {
expect(res.body.find(v => v.name === view.body.name)).toBeDefined()
})
});
-});
+});
\ No newline at end of file
diff --git a/packages/server/src/api/routes/tests/workflow.spec.js b/packages/server/src/api/routes/tests/workflow.spec.js
index 5b09009479..1f345698bf 100644
--- a/packages/server/src/api/routes/tests/workflow.spec.js
+++ b/packages/server/src/api/routes/tests/workflow.spec.js
@@ -5,7 +5,8 @@ const {
defaultHeaders,
supertest,
insertDocument,
- destroyDocument
+ destroyDocument,
+ builderEndpointShouldBlockNormalUsers
} = require("./couchTestUtils")
const TEST_WORKFLOW = {
@@ -71,6 +72,35 @@ describe("/workflows", () => {
expect(res.body.message).toEqual("Workflow created successfully");
expect(res.body.workflow.name).toEqual("My Workflow");
})
+
+ it("should apply authorization to endpoint", async () => {
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "POST",
+ url: `/api/${instance._id}/workflows`,
+ instanceId: instance._id,
+ body: TEST_WORKFLOW
+ })
+ })
+ })
+
+ describe("update", () => {
+ it("updates a workflows data", async () => {
+ await createWorkflow();
+ workflow._id = workflow.id
+ workflow._rev = workflow.rev
+ workflow.name = "Updated Name";
+
+ const res = await request
+ .put(`/api/${instance._id}/workflows`)
+ .set(defaultHeaders)
+ .send(workflow)
+ .expect('Content-Type', /json/)
+ .expect(200)
+
+ expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.");
+ expect(res.body.workflow.name).toEqual("Updated Name");
+ })
})
describe("fetch", () => {
@@ -84,18 +114,14 @@ describe("/workflows", () => {
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW));
})
- })
- describe("find", () => {
- it("returns a workflow when queried by ID", async () => {
- await createWorkflow();
- const res = await request
- .get(`/api/${instance._id}/workflows/${workflow.id}`)
- .set(defaultHeaders)
- .expect('Content-Type', /json/)
- .expect(200)
-
- expect(res.body).toEqual(expect.objectContaining(TEST_WORKFLOW));
+ it("should apply authorization to endpoint", async () => {
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "GET",
+ url: `/api/${instance._id}/workflows`,
+ instanceId: instance._id,
+ })
})
})
@@ -110,5 +136,15 @@ describe("/workflows", () => {
expect(res.body.id).toEqual(TEST_WORKFLOW._id);
})
+
+ it("should apply authorization to endpoint", async () => {
+ await createWorkflow();
+ await builderEndpointShouldBlockNormalUsers({
+ request,
+ method: "DELETE",
+ url: `/api/${instance._id}/workflows/${workflow.id}/${workflow._rev}`,
+ instanceId: instance._id,
+ })
+ })
})
});
diff --git a/packages/server/src/api/routes/view.js b/packages/server/src/api/routes/view.js
index 3bae02c4b3..193ece1cdf 100644
--- a/packages/server/src/api/routes/view.js
+++ b/packages/server/src/api/routes/view.js
@@ -8,7 +8,7 @@ const router = Router()
router
.get(
- "/api/:instanceId/view/:viewName",
+ "/api/:instanceId/views/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView
)
diff --git a/packages/server/src/api/routes/workflow.js b/packages/server/src/api/routes/workflow.js
index 86332e89aa..fcb1d6e182 100644
--- a/packages/server/src/api/routes/workflow.js
+++ b/packages/server/src/api/routes/workflow.js
@@ -1,13 +1,25 @@
const Router = require("@koa/router")
const controller = require("../controllers/workflow")
+const authorized = require("../../middleware/authorized")
+const { BUILDER } = require("../../utilities/accessLevels")
const router = Router()
router
- .get("/api/:instanceId/workflows", controller.fetch)
- .get("/api/:instanceId/workflows/:id", controller.find)
- .post("/api/:instanceId/workflows", controller.create)
- .put("/api/:instanceId/workflows/:id", controller.update)
- .delete("/api/:instanceId/workflows/:id/:rev", controller.destroy)
+ .get("/api/:instanceId/workflows", authorized(BUILDER), controller.fetch)
+ .get("/api/workflows/:id", authorized(BUILDER), controller.find)
+ .get(
+ "/api/:instanceId/workflows/:id/:action",
+ authorized(BUILDER),
+ controller.fetchActionScript
+ )
+ .put("/api/:instanceId/workflows", authorized(BUILDER), controller.update)
+ .post("/api/:instanceId/workflows", authorized(BUILDER), controller.create)
+ .post("/api/workflows/action", controller.executeAction)
+ .delete(
+ "/api/:instanceId/workflows/:id/:rev",
+ authorized(BUILDER),
+ controller.destroy
+ )
module.exports = router
diff --git a/packages/server/src/app.js b/packages/server/src/app.js
index bec5f8d16e..3a298d854b 100644
--- a/packages/server/src/app.js
+++ b/packages/server/src/app.js
@@ -4,6 +4,7 @@ const logger = require("koa-pino-logger")
const http = require("http")
const api = require("./api")
const env = require("./environment")
+const eventEmitter = require("./events")
const app = new Koa()
@@ -15,10 +16,12 @@ app.use(
prettyPrint: {
levelFirst: true,
},
- level: process.env.NODE_ENV === "jest" ? "silent" : "info",
+ level: env.LOG_LEVEL || "error",
})
)
+app.context.eventEmitter = eventEmitter
+
// api routes
app.use(api.routes())
diff --git a/packages/server/src/electron.js b/packages/server/src/electron.js
index 96fe7cd6c3..d496c276cc 100644
--- a/packages/server/src/electron.js
+++ b/packages/server/src/electron.js
@@ -1,4 +1,4 @@
-const { app, BrowserWindow } = require("electron")
+const { app, BrowserWindow, shell } = require("electron")
const { join } = require("path")
const { homedir } = require("os")
const isDev = require("electron-is-dev")
@@ -18,6 +18,11 @@ const APP_TITLE = "Budibase Builder"
let win
+function handleRedirect(e, url) {
+ e.preventDefault()
+ shell.openExternal(url)
+}
+
async function createWindow() {
app.server = await require("./app")()
win = new BrowserWindow({ width: 1920, height: 1080 })
@@ -28,6 +33,10 @@ async function createWindow() {
} else {
autoUpdater.checkForUpdatesAndNotify()
}
+
+ // open _blank in default browser
+ win.webContents.on("new-window", handleRedirect)
+ win.webContents.on("will-navigate", handleRedirect)
}
app.whenReady().then(createWindow)
diff --git a/packages/server/src/events/index.js b/packages/server/src/events/index.js
new file mode 100644
index 0000000000..d537209c05
--- /dev/null
+++ b/packages/server/src/events/index.js
@@ -0,0 +1,33 @@
+const EventEmitter = require("events").EventEmitter
+const CouchDB = require("../db")
+const { Orchestrator, serverStrategy } = require("./workflow")
+
+const emitter = new EventEmitter()
+
+async function executeRelevantWorkflows(event, eventType) {
+ const db = new CouchDB(event.instanceId)
+ const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
+ key: [eventType],
+ include_docs: true,
+ })
+
+ const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
+
+ // Create orchestrator
+ const workflowOrchestrator = new Orchestrator()
+ workflowOrchestrator.strategy = serverStrategy
+
+ for (let workflow of workflows) {
+ workflowOrchestrator.execute(workflow)
+ }
+}
+
+emitter.on("record:save", async function(event) {
+ await executeRelevantWorkflows(event, "record:save")
+})
+
+emitter.on("record:delete", async function(event) {
+ await executeRelevantWorkflows(event, "record:delete")
+})
+
+module.exports = emitter
diff --git a/packages/server/src/events/workflow.js b/packages/server/src/events/workflow.js
new file mode 100644
index 0000000000..ea66295e4d
--- /dev/null
+++ b/packages/server/src/events/workflow.js
@@ -0,0 +1,51 @@
+const mustache = require("mustache")
+
+/**
+ * The workflow orchestrator is a class responsible for executing workflows.
+ * It relies on the strategy pattern, which allows composable behaviour to be
+ * passed into its execute() function. This allows custom execution behaviour based
+ * on where the orchestrator is run.
+ *
+ */
+exports.Orchestrator = class Orchestrator {
+ set strategy(strategy) {
+ this._strategy = strategy()
+ }
+
+ async execute(workflow) {
+ if (workflow.live) {
+ this._strategy.run(workflow.definition)
+ }
+ }
+}
+
+exports.serverStrategy = () => ({
+ context: {},
+ bindContextArgs: function(args) {
+ const mappedArgs = { ...args }
+
+ // bind the workflow action args to the workflow context, if required
+ for (let arg in args) {
+ const argValue = args[arg]
+ // We don't want to render mustache templates on non-strings
+ if (typeof argValue !== "string") continue
+
+ mappedArgs[arg] = mustache.render(argValue, { context: this.context })
+ }
+
+ return mappedArgs
+ },
+ run: async function(workflow) {
+ for (let block of workflow.steps) {
+ if (block.type === "CLIENT") continue
+
+ const action = require(`../api/controllers/workflow/actions/${block.actionId}`)
+ const response = await action({ args: this.bindContextArgs(block.args) })
+
+ this.context = {
+ ...this.context,
+ [block.id]: response,
+ }
+ }
+ },
+})
diff --git a/packages/server/src/middleware/authenticated.js b/packages/server/src/middleware/authenticated.js
index 1a28e6a418..d0ce1e2f30 100644
--- a/packages/server/src/middleware/authenticated.js
+++ b/packages/server/src/middleware/authenticated.js
@@ -13,27 +13,38 @@ module.exports = async (ctx, next) => {
return
}
- if (ctx.cookies.get("builder:token") === env.ADMIN_SECRET) {
- ctx.isAuthenticated = true
- ctx.isBuilder = true
+ const appToken = ctx.cookies.get("budibase:token")
+ const builderToken = ctx.cookies.get("builder:token")
+ const isBuilderAgent = ctx.headers["x-user-agent"] === "Budibase Builder"
+
+ // all admin api access should auth with buildertoken and 'Budibase Builder user agent
+ const shouldAuthAsBuilder = isBuilderAgent && builderToken
+
+ if (shouldAuthAsBuilder) {
+ const builderTokenValid = builderToken === env.ADMIN_SECRET
+
+ ctx.isAuthenticated = builderTokenValid
+ ctx.isBuilder = builderTokenValid
+
await next()
return
}
- const token = ctx.cookies.get("budibase:token")
-
- if (!token) {
+ if (!appToken) {
ctx.isAuthenticated = false
await next()
return
}
try {
- const jwtPayload = jwt.verify(token, ctx.config.jwtSecret)
+ const jwtPayload = jwt.verify(appToken, ctx.config.jwtSecret)
ctx.user = {
...jwtPayload,
- accessLevel: await getAccessLevel(jwtPayload.accessLevelId),
+ accessLevel: await getAccessLevel(
+ jwtPayload.instanceId,
+ jwtPayload.accessLevelId
+ ),
}
ctx.isAuthenticated = true
} catch (err) {
@@ -43,7 +54,7 @@ module.exports = async (ctx, next) => {
await next()
}
-const getAccessLevel = async accessLevelId => {
+const getAccessLevel = async (instanceId, accessLevelId) => {
if (
accessLevelId === POWERUSER_LEVEL_ID ||
accessLevelId === ADMIN_LEVEL_ID
@@ -56,7 +67,10 @@ const getAccessLevel = async accessLevelId => {
}
const findAccessContext = {
- params: { levelId: accessLevelId },
+ params: {
+ levelId: accessLevelId,
+ instanceId,
+ },
}
await accessLevelController.find(findAccessContext)
return findAccessContext.body
diff --git a/packages/server/src/middleware/authorized.js b/packages/server/src/middleware/authorized.js
index 15a7b25458..b07715af36 100644
--- a/packages/server/src/middleware/authorized.js
+++ b/packages/server/src/middleware/authorized.js
@@ -31,10 +31,10 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
return
}
- const thisPermissionId = {
+ const thisPermissionId = permissionId({
name: permName,
itemId: getItemId && getItemId(ctx),
- }
+ })
// power user has everything, except the admin specific perms
if (
diff --git a/packages/server/src/schemas/index.js b/packages/server/src/schemas/index.js
deleted file mode 100644
index 9ab8736a7a..0000000000
--- a/packages/server/src/schemas/index.js
+++ /dev/null
@@ -1,38 +0,0 @@
-const WORKFLOW_SCHEMA = {
- properties: {
- type: "workflow",
- pageId: {
- type: "string",
- },
- screenId: {
- type: "string",
- },
- live: {
- type: "boolean",
- },
- uiTree: {
- type: "object",
- },
- definition: {
- type: "object",
- properties: {
- triggers: { type: "array" },
- next: {
- type: "object",
- properties: {
- type: { type: "string" },
- actionId: { type: "string" },
- args: { type: "object" },
- conditions: { type: "array" },
- errorHandling: { type: "object" },
- next: { type: "object" },
- },
- },
- },
- },
- },
-}
-
-module.exports = {
- WORKFLOW_SCHEMA,
-}
diff --git a/packages/server/src/utilities/accessLevels.js b/packages/server/src/utilities/accessLevels.js
index 25a231ed73..9fff76e531 100644
--- a/packages/server/src/utilities/accessLevels.js
+++ b/packages/server/src/utilities/accessLevels.js
@@ -1,29 +1,32 @@
const viewController = require("../api/controllers/view")
const modelController = require("../api/controllers/model")
+const workflowController = require("../api/controllers/workflow")
-exports.ADMIN_LEVEL_ID = "ADMIN"
-exports.POWERUSER_LEVEL_ID = "POWER_USER"
+// Access Level IDs
+const ADMIN_LEVEL_ID = "ADMIN"
+const POWERUSER_LEVEL_ID = "POWER_USER"
-exports.READ_MODEL = "read-model"
-exports.WRITE_MODEL = "write-model"
-exports.READ_VIEW = "read-view"
-exports.EXECUTE_WORKFLOW = "execute-workflow"
-exports.USER_MANAGEMENT = "user-management"
-exports.BUILDER = "builder"
-exports.LIST_USERS = "list-users"
+// Permissions
+const READ_MODEL = "read-model"
+const WRITE_MODEL = "write-model"
+const READ_VIEW = "read-view"
+const EXECUTE_WORKFLOW = "execute-workflow"
+const USER_MANAGEMENT = "user-management"
+const BUILDER = "builder"
+const LIST_USERS = "list-users"
-exports.adminPermissions = [
+const adminPermissions = [
{
- name: exports.USER_MANAGEMENT,
+ name: USER_MANAGEMENT,
},
]
-exports.generateAdminPermissions = async instanceId => [
- ...exports.adminPermissions,
- ...(await exports.generatePowerUserPermissions(instanceId)),
+const generateAdminPermissions = async instanceId => [
+ ...adminPermissions,
+ ...(await generatePowerUserPermissions(instanceId)),
]
-exports.generatePowerUserPermissions = async instanceId => {
+const generatePowerUserPermissions = async instanceId => {
const fetchModelsCtx = {
params: {
instanceId,
@@ -40,25 +43,53 @@ exports.generatePowerUserPermissions = async instanceId => {
await viewController.fetch(fetchViewsCtx)
const views = fetchViewsCtx.body
+ const fetchWorkflowsCtx = {
+ params: {
+ instanceId,
+ },
+ }
+ await workflowController.fetch(fetchWorkflowsCtx)
+ const workflows = fetchWorkflowsCtx.body
+
const readModelPermissions = models.map(m => ({
itemId: m._id,
- name: exports.READ_MODEL,
+ name: READ_MODEL,
}))
const writeModelPermissions = models.map(m => ({
itemId: m._id,
- name: exports.WRITE_MODEL,
+ name: WRITE_MODEL,
}))
const viewPermissions = views.map(v => ({
itemId: v.name,
- name: exports.READ_VIEW,
+ name: READ_VIEW,
+ }))
+
+ const executeWorkflowPermissions = workflows.map(w => ({
+ itemId: w._id,
+ name: EXECUTE_WORKFLOW,
}))
return [
...readModelPermissions,
...writeModelPermissions,
...viewPermissions,
- { name: exports.LIST_USERS },
+ ...executeWorkflowPermissions,
+ { name: LIST_USERS },
]
}
+
+module.exports = {
+ ADMIN_LEVEL_ID,
+ POWERUSER_LEVEL_ID,
+ READ_MODEL,
+ WRITE_MODEL,
+ READ_VIEW,
+ EXECUTE_WORKFLOW,
+ USER_MANAGEMENT,
+ BUILDER,
+ LIST_USERS,
+ generateAdminPermissions,
+ generatePowerUserPermissions,
+}
diff --git a/packages/server/src/utilities/appDirectoryTemplate/package.json b/packages/server/src/utilities/appDirectoryTemplate/package.json
index f49e35d23f..e67d5e0c17 100644
--- a/packages/server/src/utilities/appDirectoryTemplate/package.json
+++ b/packages/server/src/utilities/appDirectoryTemplate/package.json
@@ -1,5 +1,5 @@
{
- "name": "name",
+ "name": "{{ name }}",
"version": "1.0.0",
"description": "",
"author": "",
diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json
index 89a23a78e5..1ae407774e 100644
--- a/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json
+++ b/packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json
@@ -1,5 +1,5 @@
{
- "title": "Test App",
+ "title": "{{ name }}",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
@@ -9,8 +9,10 @@
"_id": 0,
"type": "div",
"_styles": {
- "layout": {},
- "position": {}
+ "active": {},
+ "hover": {},
+ "normal": {},
+ "selected": {}
},
"_code": ""
},
diff --git a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json
index 14d0301c24..6ff1bfcd98 100644
--- a/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json
+++ b/packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json
@@ -1,19 +1,45 @@
{
- "title": "Test App",
- "favicon": "./_shared/favicon.png",
- "stylesheets": [],
- "componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
- "props" : {
- "_component": "@budibase/standard-components/container",
- "_children": [],
- "_id": 1,
- "type": "div",
- "_styles": {
- "layout": {},
- "position": {}
- },
- "_code": ""
- },
- "_css": "",
- "uiFunctions": ""
+ "componentLibraries": [
+ "@budibase/standard-components",
+ "@budibase/materialdesign-components"
+ ],
+ "title": "{{ name }}",
+ "favicon": "./_shared/favicon.png",
+ "stylesheets": [],
+ "props": {
+ "_component": "@budibase/standard-components/container",
+ "_children": [
+ {
+ "_id": "686c252d-dbf2-4e28-9078-414ba4719759",
+ "_component": "@budibase/standard-components/login",
+ "_styles": {
+ "normal": {},
+ "hover": {},
+ "active": {},
+ "selected": {}
+ },
+ "_code": "",
+ "loginRedirect": "",
+ "usernameLabel": "Username",
+ "passwordLabel": "Password",
+ "loginButtonLabel": "Login",
+ "buttonClass": "",
+ "inputClass": "",
+ "_children": [],
+ "name": "{{ name }}",
+ "logo": ""
+ }
+ ],
+ "_id": 1,
+ "type": "div",
+ "_styles": {
+ "layout": {},
+ "position": {}
+ },
+ "_code": "",
+ "className": "",
+ "onLoad": []
+ },
+ "_css": "",
+ "uiFunctions": ""
}
diff --git a/packages/server/src/utilities/builder/buildPage.js b/packages/server/src/utilities/builder/buildPage.js
index 0540fd064f..3d44cb8072 100644
--- a/packages/server/src/utilities/builder/buildPage.js
+++ b/packages/server/src/utilities/builder/buildPage.js
@@ -28,8 +28,7 @@ module.exports = async (config, appId, pageName, pkg) => {
await savePageJson(appPath, pageName, pkg)
}
-const rootPath = (config, appname) =>
- config.useAppRootPath ? `/${appname}` : ""
+const rootPath = (config, appId) => (config.useAppRootPath ? `/${appId}` : "")
const copyClientLib = async (appPath, pageName) => {
const sourcepath = require.resolve("@budibase/client")
@@ -46,7 +45,7 @@ const copyClientLib = async (appPath, pageName) => {
const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
const appPublicPath = publicPath(appPath, pageName)
- const appRootPath = appId
+ const appRootPath = rootPath(config, appId)
const stylesheetUrl = s =>
s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}`
@@ -103,7 +102,6 @@ const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
filename,
`
window['##BUDIBASE_FRONTEND_DEFINITION##'] = ${clientUiDefinition};
- window['##BUDIBASE_FRONTEND_FUNCTIONS##'] = ${pkg.uiFunctions};
`
)
}
diff --git a/packages/server/src/utilities/builder/index.template.html b/packages/server/src/utilities/builder/index.template.html
index 951ed8fdf4..2f1f7ed87c 100644
--- a/packages/server/src/utilities/builder/index.template.html
+++ b/packages/server/src/utilities/builder/index.template.html
@@ -1,14 +1,17 @@
-
-
-
- {{ title }}
+
+
+
+
+ {{ title }}
-
+
diff --git a/packages/standard-components/src/DataList.svelte b/packages/standard-components/src/DataList.svelte
index 112332c6d0..03bcd5ccc4 100644
--- a/packages/standard-components/src/DataList.svelte
+++ b/packages/standard-components/src/DataList.svelte
@@ -10,13 +10,15 @@
let store = _bb.store
async function fetchData() {
- const FETCH_RECORDS_URL = `/api/${_instanceId}/all_${model._id}/records`
+ if (!model || !model.length) return
+
+ const FETCH_RECORDS_URL = `/api/${_instanceId}/views/all_${model}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
- state[model._id] = json
+ state[model] = json
return state
})
} else {
@@ -24,7 +26,8 @@
}
}
- $: data = $store[model._id] || []
+ $: data = $store[model] || []
+ $: if (model) fetchData()
onMount(async () => {
await fetchData()
diff --git a/packages/standard-components/src/DataTable.svelte b/packages/standard-components/src/DataTable.svelte
index 2d9097eb40..90716383da 100644
--- a/packages/standard-components/src/DataTable.svelte
+++ b/packages/standard-components/src/DataTable.svelte
@@ -10,13 +10,14 @@
let store = _bb.store
async function fetchData() {
- const FETCH_RECORDS_URL = `/api/${_instanceId}/all_${model._id}/records`
+ const FETCH_RECORDS_URL = `/api/${_instanceId}/views/all_${model}`
+
const response = await _bb.api.get(FETCH_RECORDS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
- state[model._id] = json
+ state[model] = json
return state
})
@@ -26,7 +27,8 @@
}
}
- $: data = $store[model._id] || []
+ $: data = $store[model] || []
+ $: if (model) fetchData()
onMount(async () => {
await fetchData()
@@ -63,24 +65,31 @@
}
thead {
- background: #f9f9f9;
+ background: #393c44;
border: 1px solid #ccc;
+ height: 40px;
+ text-align: left;
+ margin-right: 60px;
}
thead th {
- color: var(--button-text);
+ color: #ffffff;
text-transform: capitalize;
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
letter-spacing: 1px;
+ justify-content: left;
+ padding: 16px 20px 16px 8px;
+ margin-right: 20px;
}
tbody tr {
border-bottom: 1px solid #ccc;
transition: 0.3s background-color;
- color: var(--secondary100);
+ color: #393c44;
font-size: 14px;
+ height: 40px;
}
tbody tr:hover {
diff --git a/packages/standard-components/src/Embed.svelte b/packages/standard-components/src/Embed.svelte
new file mode 100644
index 0000000000..2640864681
--- /dev/null
+++ b/packages/standard-components/src/Embed.svelte
@@ -0,0 +1,5 @@
+
+
+{@html embed}
diff --git a/packages/standard-components/src/Image.svelte b/packages/standard-components/src/Image.svelte
index 84f515b287..cd204f5fc2 100644
--- a/packages/standard-components/src/Image.svelte
+++ b/packages/standard-components/src/Image.svelte
@@ -7,6 +7,8 @@
export let height
export let width
+ export let _bb
+
$: style = buildStyle({ height, width })
diff --git a/packages/standard-components/src/List.svelte b/packages/standard-components/src/List.svelte
new file mode 100644
index 0000000000..7720094999
--- /dev/null
+++ b/packages/standard-components/src/List.svelte
@@ -0,0 +1,72 @@
+
+
+
+
+
diff --git a/packages/standard-components/src/Login.svelte b/packages/standard-components/src/Login.svelte
index 65aa5302eb..b52cc154ea 100644
--- a/packages/standard-components/src/Login.svelte
+++ b/packages/standard-components/src/Login.svelte
@@ -1,10 +1,9 @@