removal of appRoot - appId comes in cookie

This commit is contained in:
Michael Shanks 2020-06-12 20:42:55 +01:00
parent 108fa4ca13
commit 19d132c6c2
44 changed files with 616 additions and 235 deletions

View File

@ -0,0 +1,21 @@
/// <reference types="cypress" />
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

View File

@ -5,7 +5,6 @@ import { writable, get } from "svelte/store"
import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants"
import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
import { rename } from "components/userInterface/pagesParsing/renameScreen"
import {
createProps,
makePropsSafe,
@ -24,6 +23,7 @@ import {
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen,
renameCurrentScreen,
} from "../storeUtils"
export const getStore = () => {
@ -52,7 +52,6 @@ export const getStore = () => {
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store)
store.renameScreen = renameScreen(store)
store.deleteScreen = deleteScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store)
@ -63,6 +62,7 @@ export const getStore = () => {
store.addChildComponent = addChildComponent(store)
store.selectComponent = selectComponent(store)
store.setComponentProp = setComponentProp(store)
store.setPageOrScreenProp = setPageOrScreenProp(store)
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
@ -207,46 +207,6 @@ const deleteScreen = store => name => {
})
}
const renameScreen = store => (oldname, newname) => {
store.update(s => {
const { screens, pages, error, changedScreens } = rename(
s.pages,
s.screens,
oldname,
newname
)
if (error) {
// should really do something with this
return s
}
s.screens = screens
s.pages = pages
if (s.currentPreviewItem.name === oldname)
s.currentPreviewItem.name = newname
const saveAllChanged = async () => {
for (let screenName of changedScreens) {
const changedScreen = getExactComponent(screens, screenName)
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen)
}
}
api
.patch(`/_builder/api/${s.appId}/screen`, {
oldname,
newname,
})
.then(() => saveAllChanged())
.then(() => {
_savePage(s)
})
return s
})
}
const savePage = store => async page => {
store.update(state => {
if (state.currentFrontEndType !== "page" || !state.currentPageName) {
@ -400,6 +360,18 @@ const setComponentProp = store => (name, value) => {
})
}
const setPageOrScreenProp = store => (name, value) => {
store.update(state => {
if (name === "name" && state.currentFrontEndType === "screen") {
state = renameCurrentScreen(value, state)
} else {
state.currentPreviewItem[name] = value
_saveCurrentPreviewItem(state)
}
return state
})
}
const setComponentStyle = store => (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {

View File

@ -45,6 +45,19 @@ export const saveScreenApi = (screen, s) => {
.then(() => savePage(s))
}
export const renameCurrentScreen = (newname, state) => {
const oldname = state.currentPreviewItem.name
state.currentPreviewItem.name = newname
api.patch(
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`,
{
oldname,
newname,
}
)
return state
}
export const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {

View File

@ -19,9 +19,10 @@
onChange(_value)
}
$: displayValues = value && suffix
? value.map(v => v.replace(new RegExp(`${suffix}$`), ""))
: value || []
$: displayValues =
value && suffix
? value.map(v => v.replace(new RegExp(`${suffix}$`), ""))
: value || []
</script>
<div class="input-container">

View File

@ -21,7 +21,7 @@
return componentName || "element"
}
const screenPlaceholder = {
const screenPlaceholder = {
name: "Screen Placeholder",
route: "*",
props: {
@ -60,9 +60,8 @@
},
}
$: hasComponent = !!$store.currentPreviewItem
$: {
styles = ""
// Apply the CSS from the currently selected page and its screens
@ -88,11 +87,10 @@
libraries: $store.libraries,
page: $store.pages[$store.currentPageName],
screens: [
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
],
appRootPath: "",
}
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
@ -102,20 +100,26 @@
: ""
const refreshContent = () => {
iframe.contentWindow.postMessage(JSON.stringify({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
}))
iframe.contentWindow.postMessage(
JSON.stringify({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
appId: $store.appId,
})
)
}
$: if(iframe) iframe.contentWindow.addEventListener("bb-ready", refreshContent, { once: true })
$: if (iframe)
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
once: true,
})
$: if(iframe && frontendDefinition) {
refreshContent()
}
$: if (iframe && frontendDefinition) {
refreshContent()
}
</script>
<div class="component-container">

View File

@ -0,0 +1,161 @@
<script>
import { store, backendUiStore } from "builderStore"
import { map, join } from "lodash/fp"
import iframeTemplate from "./iframeTemplate"
import { pipe } from "components/common/core"
let iframe
let styles = ""
function transform_component(comp) {
const props = comp.props || comp
if (props && props._children && props._children.length) {
props._children = props._children.map(transform_component)
}
return props
}
const getComponentTypeName = component => {
let [componentName] = component._component.match(/[a-z]*$/)
return componentName || "element"
}
const screenPlaceholder = {
name: "Screen Placeholder",
route: "*",
props: {
_component: "@budibase/standard-components/container",
type: "div",
_children: [
{
_component: "@budibase/standard-components/container",
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_id: "__screenslot__text",
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
_component: "@budibase/standard-components/text",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_id: "__screenslot__text_2",
_code: "",
text: "content",
font: "",
color: "",
textAlign: "inline",
verticalAlign: "inline",
formattingTag: "none",
},
],
},
],
},
}
$: hasComponent = !!$store.currentPreviewItem
$: {
styles = ""
// Apply the CSS from the currently selected page and its screens
const currentPage = $store.pages[$store.currentPageName]
styles += currentPage._css
for (let screen of currentPage._screens) {
styles += screen._css
}
styles = styles
}
$: stylesheetLinks = pipe($store.pages.stylesheets, [
map(s => `<link rel="stylesheet" href="${s}"/>`),
join("\n"),
])
$: screensExist =
$store.currentPreviewItem._screens &&
$store.currentPreviewItem._screens.length > 0
$: frontendDefinition = {
appId: $store.appId,
libraries: $store.libraries,
page: $store.pages[$store.currentPageName],
screens: [
$store.currentFrontEndType === "page"
? screenPlaceholder
: $store.currentPreviewItem,
],
}
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
$: selectedComponentId = $store.currentComponentInfo
? $store.currentComponentInfo._id
: ""
const refreshContent = () => {
<<<<<<< HEAD
iframe.contentWindow.postMessage(JSON.stringify({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
appId: $store.appId
}))
=======
iframe.contentWindow.postMessage(
JSON.stringify({
styles,
stylesheetLinks,
selectedComponentType,
selectedComponentId,
frontendDefinition,
})
)
>>>>>>> master
}
$: if (iframe)
iframe.contentWindow.addEventListener("bb-ready", refreshContent, {
once: true,
})
$: if (iframe && frontendDefinition) {
refreshContent()
}
</script>
<div class="component-container">
{#if hasComponent && $store.currentPreviewItem}
<iframe
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={iframeTemplate} />
{/if}
</div>
<style>
.component-container {
grid-row-start: middle;
grid-column-start: middle;
position: relative;
overflow: hidden;
margin: auto;
height: 100%;
}
.component-container iframe {
border: 0;
left: 0;
top: 0;
width: 100%;
}
</style>

View File

@ -44,6 +44,7 @@ export default `<html>
document.head.appendChild(styles)
styles.appendChild(document.createTextNode(data.styles))
document.cookie = "budibase:appid=" + data.appId
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
if (clientModule) {
clientModule.loadBudibase({ window, localStorage })

View File

@ -13,7 +13,6 @@
import CodeEditor from "./CodeEditor.svelte"
import LayoutEditor from "./LayoutEditor.svelte"
import EventsEditor from "./EventsEditor"
import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte"
@ -37,26 +36,11 @@
//use for getting controls for each component property
c => c._component === componentInstance._component
) || {}
let panelDefinition = {}
$: {
if(componentPropDefinition.properties) {
if(selectedCategory.value === "design") {
panelDefinition = componentPropDefinition.properties["design"]
}else{
let panelDef = componentPropDefinition.properties["settings"]
if($store.currentFrontEndType === "page" && $store.currentView !== "component") {
panelDefinition = [...page,...panelDef]
}else if($store.currentFrontEndType === "screen" && $store.currentView !== "component") {
panelDefinition = [...screen, ...panelDef]
}else {
panelDefinition = panelDef
}
}
}
}
$: panelDefinition = componentPropDefinition.properties &&
componentPropDefinition.properties[selectedCategory.value]
const onStyleChanged = store.setComponentStyle
const onPropChanged = store.setComponentProp
@ -102,7 +86,9 @@
{componentInstance}
{componentDefinition}
{panelDefinition}
onChange={onPropChanged} />
onChange={onPropChanged}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== "component" && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
{/if}

View File

@ -9,11 +9,11 @@
const pages = [
{
title: "Main",
title: "Private",
id: "main",
},
{
title: "Login",
title: "Public",
id: "unauthenticated",
},
]

View File

@ -2,20 +2,60 @@
import PropertyControl from "./PropertyControl.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte"
import Colorpicker from "../common/Colorpicker.svelte"
import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js"
import Input from "../common/Input.svelte"
export let panelDefinition = []
export let componentDefinition = {}
export let componentInstance = {}
export let onChange = () => {}
export let onScreenPropChange = () => {}
export let screenOrPageInstance
const propExistsOnComponentDef = prop => prop in componentDefinition.props
function handleChange(key, data) {
data.target ? onChange(key, data.target.value) : onChange(key, data)
}
function handleScreenPropChange (name, value) {
onScreenPropChange(name,value)
if(!isPage && name === "name") {
// screen name is changed... change URL
$goto(`./:page/${value}`)
}
}
const screenDefinition = [
{ key: "name", label: "Name", control: Input },
{ key: "description", label: "Description", control: Input },
{ key: "route", label: "Route", control: Input },
]
const pageDefinition = [
{ key: "title", label: "Title", control: Input },
{ key: "favicon", label: "Favicon", control: Input },
]
$: isPage = screenOrPageInstance && screenOrPageInstance.favicon
$: screenOrPageDefinition = isPage ? pageDefinition : screenDefinition
</script>
{#if screenOrPageInstance}
{#each screenOrPageDefinition as def}
<PropertyControl
control={def.control}
label={def.label}
key={def.key}
value={screenOrPageInstance[def.key]}
onChange={handleScreenPropChange}
props={{ ...excludeProps(def, ['control', 'label']) }} />
{/each}
<hr/>
{/if}
{#if panelDefinition && panelDefinition.length > 0}
{#each panelDefinition as definition}
{#if propExistsOnComponentDef(definition.key)}

View File

@ -12,8 +12,9 @@ export const layout = [
label: "Display",
key: "display",
control: OptionSelect,
initialValue: "Flex",
initialValue: "",
options: [
{ label: "", value: "" },
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
@ -39,6 +40,7 @@ export const layout = [
control: OptionSelect,
initialValue: "Flex Start",
options: [
{ label: "", value: "" },
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
@ -317,19 +319,31 @@ export const border = [
{
label: "Radius",
key: "border-radius",
control: Input,
width: "48px",
placeholder: "px",
textAlign: "center",
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "0" },
{ label: "small", value: "0.125rem" },
{ label: "Medium", value: "0.25rem;" },
{ label: "Large", value: "0.375rem" },
{ label: "Extra large", value: "0.5rem" },
{ label: "Full", value: "5678px" },
],
},
{
label: "Width",
key: "border-width",
control: Input,
width: "48px",
placeholder: "px",
textAlign: "center",
}, //custom
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "0" },
{ label: "Extra small", value: "0.5px" },
{ label: "Small", value: "1px" },
{ label: "Medium", value: "2px" },
{ label: "Large", value: "4px" },
{ label: "Extra large", value: "8px" },
],
},
{
label: "Color",
key: "border-color",
@ -339,6 +353,7 @@ export const border = [
label: "Style",
key: "border-style",
control: OptionSelect,
defaultValue: "None",
options: [
"none",
"hidden",
@ -365,17 +380,50 @@ export const effects = [
},
{
label: "Rotate",
key: "transform",
control: Input,
width: "48px",
textAlign: "center",
placeholder: "deg",
key: "transform-rotate",
control: OptionSelect,
defaultValue: "0",
options: [
"0",
"45deg",
"90deg",
"90deg",
"135deg",
"180deg",
"225deg",
"270deg",
"315dev",
],
}, //needs special control
{
label: "Shadow",
key: "box-shadow",
control: InputGroup,
meta: [{ placeholder: "X" }, { placeholder: "Y" }, { placeholder: "B" }],
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "none" },
{ label: "Extra small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
{
label: "Small",
value:
"0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
},
{
label: "Medium",
value:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
},
{
label: "Large",
value:
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
},
{
label: "Extra large",
value:
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
},
],
},
]

View File

@ -11,6 +11,18 @@ export default {
name: "Basic",
isCategory: true,
children: [
{
_component: "@budibase/standard-components/embed",
icon: "ri-code-line",
name: "Embed",
description: "Embed content from 3rd party sources",
properties: {
design: {
...all,
},
settings: [{ label: "Embed", key: "embed", control: Input }],
},
},
{
_component: "@budibase/standard-components/container",
name: "Container",

View File

@ -1,9 +1,9 @@
import { authenticate } from "./authenticate"
import { triggerWorkflow } from "./workflow"
export const createApi = ({ rootPath = "", setState, getState }) => {
export const createApi = ({ setState, getState }) => {
const apiCall = method => async ({ url, body }) => {
const response = await fetch(`${rootPath}${url}`, {
const response = await fetch(url, {
method: method,
headers: {
"Content-Type": "application/json",
@ -45,7 +45,6 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
const apiOpts = {
rootPath,
setState,
getState,
isSuccess,

View File

@ -2,6 +2,7 @@ import { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager"
import { getAppId } from "./render/getAppId"
export const createApp = ({
componentLibraries,
@ -15,11 +16,9 @@ export const createApp = ({
const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, url) => {
const stateManager = createStateManager({
frontendDefinition,
componentLibraries,
onScreenSlotRendered: () => {},
routeTo,
appRootPath: frontendDefinition.appRootPath,
})
const getAttachChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props]
@ -36,12 +35,8 @@ export const createApp = ({
routeTo = screenRouter({
screens: frontendDefinition.screens,
onScreenSelected,
appRootPath: frontendDefinition.appRootPath,
})
const fallbackPath = window.location.pathname.replace(
frontendDefinition.appRootPath,
""
)
const fallbackPath = window.location.pathname.replace(getAppId(), "")
routeTo(currentUrl || fallbackPath)
}
@ -59,10 +54,8 @@ export const createApp = ({
let rootTreeNode
const pageStateManager = createStateManager({
frontendDefinition,
componentLibraries,
onScreenSlotRendered,
appRootPath: frontendDefinition.appRootPath,
// seems weird, but the routeTo variable may not be available at this point
routeTo: url => routeTo(url),
})

View File

@ -1,5 +1,6 @@
import { createApp } from "./createApp"
import { builtins, builtinLibName } from "./render/builtinComponents"
import { getAppId } from "./render/getAppId"
/**
* create a web application from static budibase definition files.
@ -8,7 +9,7 @@ import { builtins, builtinLibName } from "./render/builtinComponents"
export const loadBudibase = async opts => {
const _window = (opts && opts.window) || window
// const _localStorage = (opts && opts.localStorage) || localStorage
const appId = getAppId()
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
const user = {}
@ -20,9 +21,7 @@ export const loadBudibase = async opts => {
for (let library of libraries) {
// fetch the JavaScript for the component libraries from the server
componentLibraryModules[library] = await import(
`/${frontendDefinition.appId}/componentlibrary?library=${encodeURI(
library
)}`
`/componentlibrary?library=${encodeURI(library)}`
)
}
@ -42,7 +41,7 @@ export const loadBudibase = async opts => {
})
const route = _window.location
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
: ""
initialisePage(frontendDefinition.page, _window.document.body, route)

View File

@ -0,0 +1,5 @@
export const getAppId = () =>
document.cookie
.split(";")
.find(c => c.trim().startsWith("budibase:appid"))
.split("=")[1]

View File

@ -1,11 +1,19 @@
import regexparam from "regexparam"
import { routerStore } from "../state/store"
import { getAppId } from "./getAppId"
export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
export const screenRouter = ({ screens, onScreenSelected }) => {
const makeRootedPath = url => {
if (appRootPath) {
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
return appRootPath
if (
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1"
) {
const appId = getAppId()
if (url) {
if (url.startsWith(appId)) return url
return `/${appId}${url.startsWith("/") ? "" : "/"}${url}`
}
return appId
}
return url
}

View File

@ -6,21 +6,9 @@ export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
export const bbFactory = ({
store,
frontendDefinition,
componentLibraries,
onScreenSlotRendered,
}) => {
const relativeUrl = url => {
if (!frontendDefinition.appRootPath) return url
if (
url.startsWith("http:") ||
url.startsWith("https:") ||
url.startsWith("./")
)
return url
return frontendDefinition.appRootPath + "/" + trimSlash(url)
}
const apiCall = method => (url, body) =>
fetch(url, {
@ -30,6 +18,7 @@ export const bbFactory = ({
"x-user-agent": "Budibase Builder",
},
body: body && JSON.stringify(body),
credentials: "same-origin",
})
const api = {
@ -63,7 +52,6 @@ export const bbFactory = ({
getContext: getContext(treeNode),
setContext: setContext(treeNode),
store: store,
relativeUrl,
api,
parent,
}

View File

@ -6,14 +6,13 @@ import { createApi } from "../api"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = (rootPath, routeTo) => {
export const eventHandlers = routeTo => {
const handler = (parameters, execute) => ({
execute,
parameters,
})
const api = createApi({
rootPath,
setState,
getState: (path, fallback) => getState(path, fallback),
})

View File

@ -21,13 +21,11 @@ const isMetaProp = propName =>
propName === "_styles"
export const createStateManager = ({
appRootPath,
frontendDefinition,
componentLibraries,
onScreenSlotRendered,
routeTo,
}) => {
let handlerTypes = eventHandlers(appRootPath, routeTo)
let handlerTypes = eventHandlers(routeTo)
let currentState
const getCurrentState = () => currentState
@ -35,7 +33,6 @@ export const createStateManager = ({
const bb = bbFactory({
store: appStore,
getCurrentState,
frontendDefinition,
componentLibraries,
onScreenSlotRendered,
})

View File

@ -1,10 +1,9 @@
import { JSDOM } from "jsdom"
import { loadBudibase } from "../src/index"
export const load = async (page, screens, url, appRootPath) => {
export const load = async (page, screens, url) => {
screens = screens || []
url = url || "/"
appRootPath = appRootPath || ""
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
url: `http://test${url}`,
})
@ -13,7 +12,7 @@ export const load = async (page, screens, url, appRootPath) => {
autoAssignIds(s.props)
}
setAppDef(dom.window, page, screens)
addWindowGlobals(dom.window, page, screens, appRootPath, {
addWindowGlobals(dom.window, page, screens, {
hierarchy: {},
actions: [],
triggers: [],
@ -27,11 +26,10 @@ export const load = async (page, screens, url, appRootPath) => {
return { dom, app }
}
const addWindowGlobals = (window, page, screens, appRootPath) => {
const addWindowGlobals = (window, page, screens) => {
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
page,
screens,
appRootPath,
}
}
@ -88,7 +86,6 @@ const setAppDef = (window, page, screens) => {
componentLibraries: [],
page,
screens,
appRootPath: "",
}
}

View File

@ -14,12 +14,6 @@ export default async () => {
componentLibraries["@budibase/standard-components"] = standardcomponents
const appDef = { hierarchy: {}, actions: {} }
const user = { name: "yeo", permissions: [] }
const { initialisePage } = createApp(
componentLibraries,
{ appRootPath: "" },
appDef,
user,
{}
)
const { initialisePage } = createApp(componentLibraries, {}, appDef, user, {})
return initialisePage
}

View File

@ -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",

View File

@ -4,25 +4,27 @@ const ClientDb = require("../../db/clientDb")
const bcrypt = require("../../utilities/bcrypt")
exports.authenticate = async ctx => {
if (!ctx.appId) ctx.throw(400, "No appId")
const { username, password } = ctx.request.body
if (!username) ctx.throw(400, "Username Required.")
if (!password) ctx.throw(400, "Password Required")
const masterDb = new CouchDB("clientAppLookup")
const { clientId } = await masterDb.get(ctx.params.appId)
const { clientId } = await masterDb.get(ctx.appId)
if (!clientId) {
ctx.throw(400, "ClientId not suplied")
}
// find the instance that the user is associated with
const db = new CouchDB(ClientDb.name(clientId))
const appId = ctx.params.appId
const app = await db.get(appId)
const app = await db.get(ctx.appId)
const instanceId = app.userInstanceMap[username]
if (!instanceId)
ctx.throw(500, "User is not associated with an instance of app", appId)
ctx.throw(500, "User is not associated with an instance of app", ctx.appId)
// Check the user exists in the instance DB by username
const instanceDb = new CouchDB(instanceId)
@ -41,7 +43,7 @@ exports.authenticate = async ctx => {
const payload = {
userId: dbUser._id,
accessLevelId: dbUser.accessLevelId,
instanceId: instanceId,
instanceId,
}
const token = jwt.sign(payload, ctx.config.jwtSecret, {

View File

@ -27,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,
@ -50,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
@ -59,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]

View File

@ -61,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,
@ -69,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)

View File

@ -20,6 +20,27 @@ exports.serveApp = async function(ctx) {
"public",
ctx.isAuthenticated ? "main" : "unauthenticated"
)
// only set the appId cookie for /appId .. we COULD check for valid appIds
// but would like to avoid that DB hit
if (looksLikeAppId(ctx.params.appId)) {
ctx.cookies.set("budibase:appid", ctx.params.appId, {
path: "/",
httpOnly: false,
expires: new Date(2099, 1, 1),
})
}
await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath })
}
exports.serveAppAsset = async function(ctx) {
// default to homedir
const appPath = resolve(
budibaseAppsDir(),
ctx.appId,
"public",
ctx.isAuthenticated ? "main" : "unauthenticated"
)
await send(ctx, ctx.file, { root: ctx.devPath || appPath })
}
@ -28,7 +49,7 @@ exports.serveComponentLibrary = async function(ctx) {
// default to homedir
let componentLibraryPath = resolve(
budibaseAppsDir(),
ctx.params.appId,
ctx.appId,
"node_modules",
decodeURI(ctx.query.library),
"dist"
@ -44,3 +65,10 @@ exports.serveComponentLibrary = async function(ctx) {
await send(ctx, "/index.js", { root: componentLibraryPath })
}
const looksLikeAppId = appId => {
const allowedChars = "0123456789abcdef".split("")
return (
appId.length === 32 && !appId.split("").some(c => allowedChars.includes(c))
)
}

View File

@ -10,6 +10,7 @@ const {
instanceRoutes,
clientRoutes,
applicationRoutes,
recordRoutes,
modelRoutes,
viewRoutes,
staticRoutes,
@ -59,6 +60,11 @@ router.use(async (ctx, next) => {
}
})
router.use(async (ctx, next) => {
ctx.appId = ctx.cookies.get("budibase:appid")
await next()
})
router.use(authRoutes.routes())
router.use(authRoutes.allowedMethods())
@ -69,6 +75,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())

View File

@ -3,6 +3,6 @@ const controller = require("../controllers/auth")
const router = Router()
router.post("/:appId/api/authenticate", controller.authenticate)
router.post("/api/authenticate", controller.authenticate)
module.exports = router

View File

@ -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,

View File

@ -1,46 +1,10 @@
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
)
.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
)
// models
router
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
.get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find)

View File

@ -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

View File

@ -21,7 +21,8 @@ if (env.NODE_ENV !== "production") {
}
router
.get("/:appId/componentlibrary", controller.serveComponentLibrary)
.get("/:appId/:file*", controller.serveApp)
.get("/componentlibrary", controller.serveComponentLibrary)
.get("/assets/:file*", controller.serveAppAsset)
.get("/:appId/:path*", controller.serveApp)
module.exports = router

View File

@ -253,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)
}

View File

@ -3,9 +3,10 @@ const {
createModel,
supertest,
createClientDatabase,
createApplication ,
createApplication,
defaultHeaders,
builderEndpointShouldBlockNormalUsers
builderEndpointShouldBlockNormalUsers,
getDocument
} = require("./couchTestUtils")
describe("/models", () => {
@ -97,7 +98,6 @@ describe("/models", () => {
instanceId: instance._id,
})
})
});
describe("destroy", () => {
@ -108,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)
@ -120,6 +124,41 @@ describe("/models", () => {
});
})
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,

View File

@ -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

View File

@ -45,18 +45,16 @@ const copyClientLib = async (appPath, pageName) => {
const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
const appPublicPath = publicPath(appPath, pageName)
const appRootPath = rootPath(config, appId)
const stylesheetUrl = s =>
s.startsWith("http") ? s : `/${rootPath(config, appId)}/${s}`
const templateObj = {
title: pkg.page.title || "Budibase App",
favicon: `${appRootPath}/${pkg.page.favicon || "/_shared/favicon.png"}`,
favicon: `${pkg.page.favicon || "/_shared/favicon.png"}`,
stylesheets: (pkg.page.stylesheets || []).map(stylesheetUrl),
screenStyles: pkg.screens.filter(s => s._css).map(s => s._css),
pageStyle: pkg.page._css,
appRootPath,
}
const indexHtmlTemplate = await readFile(
@ -74,7 +72,6 @@ const buildIndexHtml = async (config, appId, pageName, appPath, pkg) => {
const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
const appPath = appPackageFolder(config, appId)
const appPublicPath = publicPath(appPath, pageName)
const appRootPath = rootPath(config, appId)
const filename = join(appPublicPath, "clientFrontendDefinition.js")
@ -89,7 +86,6 @@ const buildFrontendAppDefinition = async (config, appId, pageName, pkg) => {
}
const clientUiDefinition = JSON.stringify({
appRootPath: appRootPath,
page: pkg.page,
screens: pkg.screens,
libraries: [

View File

@ -24,15 +24,15 @@
{{ /each }}
{{ each(options.screenStyles) }}
<link rel='stylesheet' href='{{ appRootPath }}{{ @this }}'>
<link rel='stylesheet' href='/assets{{ @this }}'>
{{ /each }}
{{ if(options.pageStyle) }}
<link rel='stylesheet' href='{{ appRootPath }}{{ pageStyle }}'>
<link rel='stylesheet' href='/assets{{ pageStyle }}'>
{{ /if }}
<script src='{{ appRootPath }}/clientFrontendDefinition.js'></script>
<script src='{{ appRootPath }}/budibase-client.js'></script>
<script src='/assets/clientFrontendDefinition.js'></script>
<script src='/assets/budibase-client.js'></script>
</head>

View File

@ -6,6 +6,13 @@
"component": "button"
}
},
"embed": {
"name": "Embed",
"description": "Embed stuff",
"props": {
"embed": "string"
}
},
"Navigation": {
"name": "Navigation",
"description": "A basic header navigation component",

View File

@ -98,6 +98,5 @@ window["##BUDIBASE_APPDEFINITION##"] = {
nodeId: 0,
},
componentLibraries: ["budibase-standard-components"],
appRootPath: "/testApp2",
props: {},
}

View File

@ -0,0 +1,5 @@
<script>
export let embed
</script>
{@html embed}

View File

@ -13,30 +13,17 @@
let password = ""
let loading = false
let error = false
let _logo = ""
let _buttonClass = ""
let _inputClass = ""
$: {
_logo = _bb.relativeUrl(logo)
_buttonClass = buttonClass || "default-button"
_inputClass = inputClass || "default-input"
}
const login = async () => {
loading = true
const response = await fetch(_bb.relativeUrl("/api/authenticate"), {
body: JSON.stringify({
username,
password,
}),
headers: {
"Content-Type": "application/json",
"x-user-agent": "Budibase Builder",
},
method: "POST",
})
const response = await _bb.api.post("/api/authenticate", { username, password })
if (response.status === 200) {
const json = await response.json()
localStorage.setItem("budibase:token", json.token)
@ -51,9 +38,9 @@
<div class="root">
<div class="content">
{#if _logo}
{#if logo}
<div class="logo-container">
<img src={_logo} alt="logo" />
<img src={logo} alt="logo" />
</div>
{/if}

View File

@ -15,7 +15,7 @@ export default async () => {
const { initialisePage } = createApp(
window.document,
componentLibraries,
{ appRootPath: "" },
{},
appDef,
user,
{},

View File

@ -21,3 +21,4 @@ export { default as datalist } from "./DataList.svelte"
export { default as list } from "./List.svelte"
export { default as datasearch } from "./DataSearch.svelte"
export { default as datamap } from "./DataMap.svelte"
export { default as embed } from "./Embed.svelte"