Browse Source

Merge branch 'develop' of github.com:Budibase/budibase into feature/query-rbac-timeouts

master
mike12345567 5 years ago
parent
commit
5c8670c7f4
  1. 2
      lerna.json
  2. 2
      packages/auth/package.json
  3. 2
      packages/bbui/package.json
  4. 2
      packages/bbui/src/Form/Combobox.svelte
  5. 15
      packages/bbui/src/Form/Core/Combobox.svelte
  6. 3
      packages/builder/cypress/integration/renameAnApplication.spec.js
  7. 10
      packages/builder/cypress/support/commands.js
  8. 8
      packages/builder/package.json
  9. 2
      packages/builder/src/analytics/PosthogClient.js
  10. 30
      packages/builder/src/builderStore/dataBinding.js
  11. 1
      packages/builder/src/builderStore/store/frontend.js
  12. 2
      packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte
  13. 3
      packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
  14. 19
      packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
  15. 2
      packages/builder/src/components/common/ConfirmDialog.svelte
  16. 29
      packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte
  17. 18
      packages/builder/src/components/deploy/RevertModal.svelte
  18. 30
      packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
  19. 4
      packages/builder/src/components/design/AppPreview/componentStructure.json
  20. 1
      packages/builder/src/components/design/AppPreview/iframeTemplate.js
  21. 1
      packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte
  22. 26
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte
  23. 15
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/Input.svelte
  24. 72
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte
  25. 8
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/URLSelect.svelte
  26. 4
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js
  27. 1
      packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte
  28. 1
      packages/builder/src/components/design/PropertiesPanel/StyleSection.svelte
  29. 8
      packages/builder/src/components/design/PropertiesPanel/componentStyles.js
  30. 7
      packages/builder/src/components/start/CreateAppModal.svelte
  31. 46
      packages/builder/src/components/start/TemplateList.svelte
  32. 16
      packages/builder/src/constants/backend/index.js
  33. 8
      packages/builder/src/pages/builder/_layout.svelte
  34. 6
      packages/builder/src/pages/builder/portal/apps/index.svelte
  35. 7
      packages/builder/src/stores/backend/tables.js
  36. 37
      packages/builder/src/stores/portal/auth.js
  37. 2
      packages/cli/package.json
  38. 72
      packages/client/manifest.json
  39. 6
      packages/client/package.json
  40. 2
      packages/client/src/api/rows.js
  41. 42
      packages/client/src/components/app/SpectrumCard.svelte
  42. 41
      packages/client/src/components/app/blocks/CardsBlock.svelte
  43. 17
      packages/client/src/components/app/blocks/TableBlock.svelte
  44. 4
      packages/client/src/components/app/blocks/index.js
  45. 3
      packages/client/src/utils/linkable.js
  46. 3
      packages/server/__mocks__/pg.ts
  47. 8
      packages/server/package.json
  48. 18
      packages/server/src/api/controllers/application.js
  49. 7
      packages/server/src/api/controllers/dev.js
  50. 2
      packages/server/src/api/controllers/static/index.js
  51. 5
      packages/server/src/api/controllers/table/external.js
  52. 6
      packages/server/src/api/controllers/table/internal.js
  53. 19
      packages/server/src/api/controllers/table/utils.js
  54. 7
      packages/server/src/constants/index.js
  55. 2
      packages/server/src/integrations/base/sqlTable.ts
  56. 20
      packages/server/src/integrations/postgres.ts
  57. 4
      packages/server/src/integrations/tests/postgres.spec.js
  58. 9
      packages/server/src/utilities/index.js
  59. 2
      packages/string-templates/package.json
  60. 6
      packages/worker/package.json
  61. 2
      packages/worker/src/utilities/appService.js

2
lerna.json

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

2
packages/auth/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/auth",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"description": "Authentication middlewares for budibase builder and apps",
"main": "src/index.js",
"author": "Budibase",

2
packages/bbui/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"license": "AGPL-3.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",

2
packages/bbui/src/Form/Combobox.svelte

@ -36,5 +36,7 @@
{getOptionLabel}
{getOptionValue}
on:change={onChange}
on:pick
on:type
/>
</Field>

15
packages/bbui/src/Form/Core/Combobox.svelte

@ -40,8 +40,15 @@
open = false
}
const onChange = e => {
selectOption(e.target.value)
const onType = e => {
const value = e.target.value
dispatch("type", value)
selectOption(value)
}
const onPick = value => {
dispatch("pick", value)
selectOption(value)
}
</script>
@ -62,7 +69,7 @@
type="text"
on:focus={() => (focus = true)}
on:blur={() => (focus = false)}
on:change={onChange}
on:change={onType}
value={value || ""}
placeholder={placeholder || ""}
{disabled}
@ -99,7 +106,7 @@
role="option"
aria-selected="true"
tabindex="0"
on:click={() => selectOption(getOptionValue(option))}
on:click={() => onPick(getOptionValue(option))}
>
<span class="spectrum-Menu-itemLabel"
>{getOptionLabel(option)}</span

3
packages/builder/cypress/integration/renameAnApplication.spec.js

@ -11,7 +11,8 @@ it("should rename an unpublished application", () => {
renameApp(appRename)
cy.searchForApplication(appRename)
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
})
cy.deleteApp(appRename)
})
xit("Should rename a published application", () => {
// It is not possible to rename a published application

10
packages/builder/cypress/support/commands.js

@ -43,24 +43,26 @@ Cypress.Commands.add("createApp", name => {
})
})
Cypress.Commands.add("deleteApp", () => {
Cypress.Commands.add("deleteApp", appName => {
cy.visit(`localhost:${Cypress.env("PORT")}/builder`)
cy.wait(1000)
cy.request(`localhost:${Cypress.env("PORT")}/api/applications?status=all`)
.its("body")
.then(val => {
console.log(val)
if (val.length > 0) {
cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
cy.contains("Delete").click()
cy.get(".spectrum-Button--warning").click()
cy.get(".spectrum-Modal").within(() => {
cy.get("input").type(appName)
cy.get(".spectrum-Button--warning").click()
})
}
})
})
Cypress.Commands.add("createTestApp", () => {
const appName = "Cypress Tests"
cy.deleteApp()
cy.deleteApp(appName)
cy.createApp(appName, "This app is used for Cypress testing.")
})

8
packages/builder/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@ -65,10 +65,10 @@
}
},
"dependencies": {
"@budibase/bbui": "^0.9.180-alpha.1",
"@budibase/client": "^0.9.180-alpha.1",
"@budibase/bbui": "^0.9.184",
"@budibase/client": "^0.9.184",
"@budibase/colorpicker": "1.1.2",
"@budibase/string-templates": "^0.9.180-alpha.1",
"@budibase/string-templates": "^0.9.184",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",

2
packages/builder/src/analytics/PosthogClient.js

@ -12,7 +12,7 @@ export default class PosthogClient {
posthog.init(this.token, {
autocapture: false,
capture_pageview: false,
capture_pageview: true,
api_host: this.url,
})
posthog.set_config({ persistence: "cookie" })

30
packages/builder/src/builderStore/dataBinding.js

@ -31,11 +31,11 @@ export const getBindableProperties = (asset, componentId) => {
const deviceBindings = getDeviceBindings()
const stateBindings = getStateBindings()
return [
...stateBindings,
...deviceBindings,
...urlBindings,
...contextBindings,
...urlBindings,
...stateBindings,
...userBindings,
...deviceBindings,
]
}
@ -217,18 +217,8 @@ const getProviderContextBindings = (asset, dataProviders) => {
keys.forEach(key => {
const fieldSchema = schema[key]
// Make safe runtime binding and replace certain bindings with a
// new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
const runtimeBinding = `${safeComponentId}.${makePropSafe(
runtimeBoundKey
)}`
// Make safe runtime binding
const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}`
// Optionally use a prefix with readable bindings
let readableBinding = component._instanceName
@ -267,17 +257,9 @@ const getUserBindings = () => {
const safeUser = makePropSafe("user")
keys.forEach(key => {
const fieldSchema = schema[key]
// Replace certain bindings with a new property to help display components
let runtimeBoundKey = key
if (fieldSchema.type === "link") {
runtimeBoundKey = `${key}_text`
} else if (fieldSchema.type === "attachment") {
runtimeBoundKey = `${key}_first`
}
bindings.push({
type: "context",
runtimeBinding: `${safeUser}.${makePropSafe(runtimeBoundKey)}`,
runtimeBinding: `${safeUser}.${makePropSafe(key)}`,
readableBinding: `Current User.${key}`,
// Field schema and provider are required to construct relationship
// datasource options, based on bindable properties

1
packages/builder/src/builderStore/store/frontend.js

@ -45,6 +45,7 @@ const INITIAL_FRONTEND_STATE = {
state: false,
customThemes: false,
devicePreview: false,
messagePassing: false,
},
currentFrontEndType: "none",
selectedScreenId: "",

2
packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowItem.svelte

@ -202,7 +202,7 @@
display: inline-block;
}
.block {
width: 360px;
width: 480px;
font-size: 16px;
background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300);

3
packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte

@ -234,7 +234,8 @@
<Editor
mode="javascript"
on:change={e => {
onChange(e, key)
// need to pass without the value inside
onChange({ detail: e.detail.value }, key)
inputData[key] = e.detail.value
}}
value={inputData[key]}

19
packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte

@ -18,6 +18,11 @@
FIELDS,
AUTO_COLUMN_SUB_TYPES,
RelationshipTypes,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES,
SWITCHABLE_TYPES,
} from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
import { notifications } from "@budibase/bbui"
@ -92,6 +97,9 @@
opt.type === table.type &&
table.sourceId === opt.sourceId
)
$: typeEnabled =
!originalName ||
(originalName && SWITCHABLE_TYPES.indexOf(field.type) !== -1)
async function saveColumn() {
if (field.type === AUTO_TYPE) {
@ -204,7 +212,14 @@
}
function getAllowedTypes() {
if (!external) {
if (originalName && ALLOWABLE_STRING_TYPES.indexOf(field.type) !== -1) {
return ALLOWABLE_STRING_OPTIONS
} else if (
originalName &&
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
) {
return ALLOWABLE_NUMBER_OPTIONS
} else if (!external) {
return [
...Object.values(fieldDefinitions),
{ name: "Auto Column", type: AUTO_TYPE },
@ -259,7 +274,7 @@
/>
<Select
disabled={originalName}
disabled={!typeEnabled}
label="Type"
bind:value={field.type}
on:change={handleTypeChange}

2
packages/builder/src/components/common/ConfirmDialog.svelte

@ -8,6 +8,7 @@
export let onOk = undefined
export let onCancel = undefined
export let warning = true
export let disabled
let modal
@ -26,6 +27,7 @@
confirmText={okText}
{cancelText}
{warning}
{disabled}
>
<Body size="S">
{body}

29
packages/builder/src/components/common/bindings/DrawerBindableCombobox.svelte

@ -17,6 +17,7 @@
export let disabled = false
export let options
export let allowJS = true
export let appendBindingsAsOptions = true
const dispatch = createEventDispatcher()
let bindingDrawer
@ -24,15 +25,30 @@
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
$: allOptions = buildOptions(options, bindings, appendBindingsAsOptions)
const handleClose = () => {
onChange(tempValue)
bindingDrawer.hide()
}
const onChange = value => {
const onChange = (value, optionPicked) => {
// Add HBS braces if picking binding
if (optionPicked && !options?.includes(value)) {
value = `{{ ${value} }}`
}
dispatch("change", readableToRuntimeBinding(bindings, value))
}
const buildOptions = (options, bindings, appendBindingsAsOptions) => {
if (!appendBindingsAsOptions) {
return options
}
return []
.concat(options || [])
.concat(bindings?.map(binding => binding.readableBinding) || [])
}
</script>
<div class="control">
@ -40,12 +56,17 @@
{label}
{disabled}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
on:type={e => onChange(e.detail, false)}
on:pick={e => onChange(e.detail, true)}
{placeholder}
{options}
options={allOptions}
/>
{#if !disabled}
<div class="icon" on:click={bindingDrawer.show}>
<div
class="icon"
on:click={bindingDrawer.show}
data-cy="text-binding-button"
>
<Icon size="S" name="FlashOn" />
</div>
{/if}

18
packages/builder/src/components/deploy/RevertModal.svelte

@ -1,9 +1,16 @@
<script>
import { Icon, Modal, notifications, ModalContent } from "@budibase/bbui"
import {
Icon,
Input,
Modal,
notifications,
ModalContent,
} from "@budibase/bbui"
import { store } from "builderStore"
import api from "builderStore/api"
let revertModal
let appName
$: appId = $store.appId
@ -33,10 +40,17 @@
<Icon name="Revert" hoverable on:click={revertModal.show} />
<Modal bind:this={revertModal}>
<ModalContent title="Revert Changes" confirmText="Revert" onConfirm={revert}>
<ModalContent
title="Revert Changes"
confirmText="Revert"
onConfirm={revert}
disabled={appName !== $store.name}
>
<span
>The changes you have made will be deleted and the application reverted
back to its production state.</span
>
<span>Please enter your app name to continue.</span>
<Input bind:value={appName} />
</ModalContent>
</Modal>

30
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte

@ -69,6 +69,7 @@
theme: $store.theme,
customTheme: $store.customTheme,
previewDevice: $store.previewDevice,
messagePassing: $store.clientFeatures.messagePassing
}
// Saving pages and screens to the DB causes them to have _revs.
@ -94,10 +95,12 @@
const handlers = {
[MessageTypes.READY]: () => {
// Initialise the app when mounted
if ($store.clientFeatures.messagePassing) {
if (!loading) return
}
// Display preview immediately if the intelligent loading feature
// is not supported
if (!loading) return
if (!$store.clientFeatures.intelligentLoading) {
loading = false
}
@ -117,17 +120,34 @@
onMount(() => {
window.addEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.addEventListener("ready", () => {
receiveMessage({ data: { type: MessageTypes.READY }})
}, { once: true })
iframe.contentWindow.addEventListener("error", event => {
receiveMessage({ data: { type: MessageTypes.ERROR, error: event.detail }})
}, { once: true })
// Add listener for events sent by client library in preview
iframe.contentWindow.addEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
}
})
// Remove all iframe event listeners on component destroy
onDestroy(() => {
if (iframe.contentWindow) {
window.removeEventListener("message", receiveMessage) //
window.removeEventListener("message", receiveMessage)
if (!$store.clientFeatures.messagePassing) {
// Legacy - remove in later versions of BB
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
iframe.contentWindow.removeEventListener("keydown", handleKeydownEvent)
}
}
})
const handleBudibaseEvent = event => {
const { type, data } = event.data
const { type, data } = event.data || event.detail
if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") {
@ -164,7 +184,7 @@
}
const handleKeydownEvent = event => {
const { key } = event.data
const { key } = event.data || event
if (
(key === "Delete" || key === "Backspace") &&
selectedComponentId &&

4
packages/builder/src/components/design/AppPreview/componentStructure.json

@ -3,8 +3,8 @@
"name": "Blocks",
"icon": "Article",
"children": [
"tablewithsearch",
"cardlistwithsearch"
"tableblock",
"cardsblock"
]
},
"section",

1
packages/builder/src/components/design/AppPreview/iframeTemplate.js

@ -97,6 +97,7 @@ export default `
window.addEventListener("keydown", evt => {
window.parent.postMessage({ type: "keydown", key: event.key })
})
window.parent.postMessage({ type: "ready" })
</script>
</head>

1
packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte

@ -78,7 +78,6 @@
<DetailSummary name={section.name} collapsible={false}>
{#if idx === 0 && !componentInstance._component.endsWith("/layout")}
<PropertyControl
bindable={false}
control={Input}
label="Name"
key="_instanceName"

26
packages/builder/src/components/design/PropertiesPanel/PropertyControls/ConditionalUIDrawer.svelte

@ -13,9 +13,10 @@
import { generate } from "shortid"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { OperatorOptions, getValidOperatorsForType } from "constants/lucene"
import { selectedComponent, store } from "builderStore"
import { selectedComponent } from "builderStore"
import { getComponentForSettingType } from "./componentSettings"
import PropertyControl from "./PropertyControl.svelte"
import { getComponentSettings } from "builderStore/storeUtils"
export let conditions = []
export let bindings = []
@ -55,15 +56,11 @@
]
let dragDisabled = true
$: definition = store.actions.components.getDefinition(
$selectedComponent?._component
)
$: settings = (definition?.settings ?? []).map(setting => {
return {
label: setting.label,
value: setting.key,
}
})
$: settings = getComponentSettings($selectedComponent?._component)
$: settingOptions = settings.map(setting => ({
label: setting.label,
value: setting.key,
}))
$: conditions.forEach(link => {
if (!link.id) {
link.id = generate()
@ -71,9 +68,7 @@
})
const getSettingDefinition = key => {
return definition?.settings?.find(setting => {
return setting.key === key
})
return settings.find(setting => setting.key === key)
}
const getComponentForSetting = key => {
@ -175,7 +170,10 @@
bind:value={condition.action}
/>
{#if condition.action === "update"}
<Select options={settings} bind:value={condition.setting} />
<Select
options={settingOptions}
bind:value={condition.setting}
/>
<div>TO</div>
{#if getSettingDefinition(condition.setting)}
<PropertyControl

15
packages/builder/src/components/design/PropertiesPanel/PropertyControls/Input.svelte

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

72
packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte

@ -1,11 +1,9 @@
<script>
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
import { Label } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "helpers"
export let label = ""
export let bindable = true
@ -20,10 +18,6 @@
export let componentBindings = []
export let nested = false
let bindingDrawer
let anchor
let valid
$: allBindings = getAllBindings(bindings, componentBindings, nested)
$: safeValue = getSafeValue(value, props.defaultValue, allBindings)
$: tempValue = safeValue
@ -33,12 +27,7 @@
if (!nested) {
return bindings
}
return [...(bindings || []), ...(componentBindings || [])]
}
const handleClose = () => {
handleChange(tempValue)
bindingDrawer.hide()
return [...(componentBindings || []), ...(bindings || [])]
}
// Handle a value change of any type
@ -74,7 +63,7 @@
}
</script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
<div class="property-control" data-cy={`setting-${key}`}>
{#if type !== "boolean" && label}
<div class="label">
<Label>{label}</Label>
@ -94,31 +83,6 @@
{type}
{...props}
/>
{#if bindable && !key.startsWith("_") && type === "text"}
<div
class="icon"
data-cy={`${key}-binding-button`}
on:click={bindingDrawer.show}
>
<Icon size="S" name="FlashOn" />
</div>
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
Save
</Button>
<BindingPanel
slot="body"
bind:valid
value={safeValue}
on:change={e => (tempValue = e.detail)}
bindableProperties={allBindings}
allowJS
/>
</Drawer>
{/if}
</div>
</div>
@ -130,40 +94,10 @@
justify-content: flex-start;
align-items: stretch;
}
.label {
padding-bottom: var(--spectrum-global-dimension-size-65);
}
.control {
position: relative;
}
.icon {
right: 1px;
top: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
</style>

8
packages/builder/src/components/design/PropertiesPanel/PropertyControls/URLSelect.svelte

@ -10,4 +10,10 @@
.filter(x => x != null)
</script>
<DrawerBindableCombobox {value} {bindings} on:change options={urlOptions} />
<DrawerBindableCombobox
{value}
{bindings}
on:change
options={urlOptions}
appendBindingsAsOptions={false}
/>

4
packages/builder/src/components/design/PropertiesPanel/PropertyControls/componentSettings.js

@ -15,10 +15,10 @@ import URLSelect from "./URLSelect.svelte"
import OptionsEditor from "./OptionsEditor/OptionsEditor.svelte"
import FormFieldSelect from "./FormFieldSelect.svelte"
import ValidationEditor from "./ValidationEditor/ValidationEditor.svelte"
import Input from "./Input.svelte"
import DrawerBindableCombobox from "components/common/bindings/DrawerBindableCombobox.svelte"
const componentMap = {
text: Input,
text: DrawerBindableCombobox,
select: Select,
dataSource: DataSourceSelect,
dataProvider: DataProviderSelect,

1
packages/builder/src/components/design/PropertiesPanel/ScreenSettingsSection.svelte

@ -54,7 +54,6 @@
<DetailSummary name="Screen" collapsible={false}>
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}

1
packages/builder/src/components/design/PropertiesPanel/StyleSection.svelte

@ -30,7 +30,6 @@
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<div style="grid-column: {prop.column || 'auto'}">
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
control={prop.control}
key={prop.key}

8
packages/builder/src/components/design/PropertiesPanel/componentStyles.js

@ -9,7 +9,6 @@ export const margin = {
label: "Top",
key: "margin-top",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -30,7 +29,6 @@ export const margin = {
label: "Right",
key: "margin-right",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -51,7 +49,6 @@ export const margin = {
label: "Bottom",
key: "margin-bottom",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -72,7 +69,6 @@ export const margin = {
label: "Left",
key: "margin-left",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -100,7 +96,6 @@ export const padding = {
label: "Top",
key: "padding-top",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -121,7 +116,6 @@ export const padding = {
label: "Right",
key: "padding-right",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -142,7 +136,6 @@ export const padding = {
label: "Bottom",
key: "padding-bottom",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },
@ -163,7 +156,6 @@ export const padding = {
label: "Left",
key: "padding-left",
control: Select,
bindable: false,
placeholder: "None",
options: [
{ label: "4px", value: "4px" },

7
packages/builder/src/components/start/CreateAppModal.svelte

@ -157,6 +157,11 @@
}
return title
}
async function onCancel() {
template = null
await auth.setInitInfo({})
}
</script>
{#if showTemplateSelection}
@ -186,7 +191,7 @@
title={getModalTitle()}
confirmText={template?.fromFile ? "Import app" : "Create app"}
onConfirm={createNewApp}
onCancel={inline ? () => (template = null) : null}
onCancel={inline ? onCancel : null}
cancelText={inline ? "Back" : undefined}
showCloseIcon={!inline}
disabled={!valid}

46
packages/builder/src/components/start/TemplateList.svelte

@ -37,33 +37,33 @@
<p class="detail">{template?.category?.toUpperCase()}</p>
</div>
{/each}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Start from scratch</Heading>
<p class="detail">BLANK</p>
</div>
<div
class="template import"
on:click={() => onSelect(null, { useImport: true })}
>
<div
class="background-icon"
style={`background: rgb(50, 50, 50); color: white;`}
>
<Icon name="Add" />
</div>
<Heading size="XS">Import an app</Heading>
<p class="detail">BLANK</p>
</div>
</Layout>
<style>

16
packages/builder/src/constants/backend/index.js

@ -138,3 +138,19 @@ export const RelationshipTypes = {
ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one",
}
export const ALLOWABLE_STRING_OPTIONS = [FIELDS.STRING, FIELDS.OPTIONS]
export const ALLOWABLE_STRING_TYPES = ALLOWABLE_STRING_OPTIONS.map(
opt => opt.type
)
export const ALLOWABLE_NUMBER_OPTIONS = [FIELDS.NUMBER, FIELDS.BOOLEAN]
export const ALLOWABLE_NUMBER_TYPES = ALLOWABLE_NUMBER_OPTIONS.map(
opt => opt.type
)
export const SWITCHABLE_TYPES = ALLOWABLE_NUMBER_TYPES.concat(
ALLOWABLE_STRING_TYPES
)

8
packages/builder/src/pages/builder/_layout.svelte

@ -28,9 +28,13 @@
}
if (user && user.tenantId) {
// no tenant in the url - send to account portal to fix this
if (!urlTenantId) {
window.location.href = $admin.accountPortalUrl
// redirect to correct tenantId subdomain
if (!window.location.host.includes("localhost")) {
let redirectUrl = window.location.href
redirectUrl = redirectUrl.replace("://", `://${user.tenantId}.`)
window.location.href = redirectUrl
}
return
}

6
packages/builder/src/pages/builder/portal/apps/index.svelte

@ -6,6 +6,7 @@
ActionButton,
ActionGroup,
ButtonGroup,
Input,
Select,
Modal,
Page,
@ -36,6 +37,7 @@
let loaded = false
let searchTerm = ""
let cloud = $admin.cloud
let appName = ""
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(app =>
@ -296,8 +298,12 @@
title="Confirm deletion"
okText="Delete app"
onOk={confirmDeleteApp}
disabled={appName !== selectedApp?.name}
>
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
<p>Please enter the app name below to confirm.</p>
<Input bind:value={appName} data-cy="delete-app-confirmation" />
</ConfirmDialog>
<ConfirmDialog
bind:this={unpublishModal}

7
packages/builder/src/stores/backend/tables.js

@ -2,6 +2,7 @@ import { writable, get } from "svelte/store"
import { views, queries, datasources } from "./"
import { cloneDeep } from "lodash/fp"
import api from "builderStore/api"
import { SWITCHABLE_TYPES } from "../../constants/backend"
export function createTablesStore() {
const store = writable({})
@ -47,7 +48,11 @@ export function createTablesStore() {
const field = updatedTable.schema[key]
const oldField = oldTable?.schema[key]
// if the type has changed then revert back to the old field
if (oldField != null && oldField?.type !== field.type) {
if (
oldField != null &&
oldField?.type !== field.type &&
SWITCHABLE_TYPES.indexOf(oldField?.type) === -1
) {
updatedTable.schema[key] = oldField
}
// field has been renamed

37
packages/builder/src/stores/portal/auth.js

@ -57,11 +57,11 @@ export function createAuthStore() {
analytics.showChat({
email: user.email,
created_at: (user.createdAt || Date.now()) / 1000,
name: user.name,
name: user.account?.name,
user_id: user._id,
tenant: user.tenantId,
"Company size": user.size,
"Job role": user.profession,
"Company size": user.account?.size,
"Job role": user.account?.profession,
})
})
}
@ -80,16 +80,30 @@ export function createAuthStore() {
}
}
async function setInitInfo(info) {
await api.post(`/api/global/auth/init`, info)
auth.update(store => {
store.initInfo = info
return store
})
return info
}
async function getInitInfo() {
const response = await api.get(`/api/global/auth/init`)
const json = response.json()
auth.update(store => {
store.initInfo = json
return store
})
return json
}
return {
subscribe: store.subscribe,
setOrganisation: setOrganisation,
getInitInfo: async () => {
const response = await api.get(`/api/global/auth/init`)
return await response.json()
},
setInitInfo: async info => {
await api.post(`/api/global/auth/init`, info)
},
setOrganisation,
getInitInfo,
setInitInfo,
checkQueryString: async () => {
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.has("tenantId")) {
@ -129,6 +143,7 @@ export function createAuthStore() {
throw "Unable to create logout"
}
await response.json()
await setInitInfo({})
setUser(null)
},
updateSelf: async fields => {

2
packages/cli/package.json

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

72
packages/client/manifest.json

@ -5,7 +5,8 @@
"deviceAwareness": true,
"state": true,
"customThemes": true,
"devicePreview": true
"devicePreview": true,
"messagePassing": true
},
"layout": {
"name": "Layout",
@ -2595,6 +2596,11 @@
"key": "linkURL",
"label": "Link URL"
},
{
"type": "boolean",
"key": "linkPeek",
"label": "Open link in modal"
},
{
"type": "boolean",
"key": "horizontal",
@ -2617,9 +2623,9 @@
}
]
},
"tablewithsearch": {
"tableblock": {
"block": true,
"name": "Table with search",
"name": "Table block",
"icon": "Table",
"styles": ["size"],
"info": "Only the first 3 search columns will be used.",
@ -2730,18 +2736,23 @@
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show button",
"label": "Show link button",
"defaultValue": false
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{
"type": "text",
"key": "titleButtonText",
"label": "Button text"
},
{
"type": "event",
"label": "Button action",
"key": "titleButtonOnClick"
"type": "url",
"label": "Button link",
"key": "titleButtonURL"
}
]
},
@ -2759,9 +2770,9 @@
}
]
},
"cardlistwithsearch": {
"cardsblock": {
"block": true,
"name": "Card list with search",
"name": "Cards block",
"icon": "Table",
"styles": ["size"],
"info": "Only the first 3 search columns will be used.",
@ -2838,7 +2849,22 @@
"key": "cardImageURL",
"label": "Image URL",
"nested": true
},
{
"type": "boolean",
"key": "linkCardTitle",
"label": "Link card title"
},
{
"type": "boolean",
"key": "cardPeek",
"label": "Open link in modal"
},
{
"type": "url",
"label": "Link screen",
"key": "cardURL",
"nested": true
},
{
"type": "boolean",
@ -2855,7 +2881,6 @@
"key": "cardButtonText",
"label": "Button text",
"nested": true
},
{
"type": "event",
@ -2872,7 +2897,12 @@
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show button"
"label": "Show link button"
},
{
"type": "boolean",
"label": "Open link in modal",
"key": "titleButtonPeek"
},
{
"type": "text",
@ -2880,9 +2910,21 @@
"label": "Button text"
},
{
"type": "event",
"label": "Button action",
"key": "titleButtonOnClick"
"type": "url",
"label": "Button link",
"key": "titleButtonURL"
}
]
},
{
"section": true,
"name": "Advanced",
"settings": [
{
"type": "field",
"label": "ID column for linking (appended to URL)",
"key": "linkColumn",
"placeholder": "Default"
}
]
}

6
packages/client/package.json

@ -1,6 +1,6 @@
{
"name": "@budibase/client",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"license": "MPL-2.0",
"module": "dist/budibase-client.js",
"main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw"
},
"dependencies": {
"@budibase/bbui": "^0.9.180-alpha.1",
"@budibase/bbui": "^0.9.184",
"@budibase/standard-components": "^0.9.139",
"@budibase/string-templates": "^0.9.180-alpha.1",
"@budibase/string-templates": "^0.9.184",
"regexparam": "^1.3.0",
"shortid": "^2.2.15",
"svelte-spa-router": "^3.0.5"

2
packages/client/src/api/rows.js

@ -108,6 +108,8 @@ export const deleteRows = async ({ tableId, rows }) => {
/**
* Enriches rows which contain certain field types so that they can
* be properly displayed.
* The ability to create these bindings has been removed, but they will still
* exist in client apps to support backwards compatibility.
*/
export const enrichRows = async (rows, tableId) => {
if (!Array.isArray(rows)) {

42
packages/client/src/components/app/SpectrumCard.svelte

@ -8,15 +8,22 @@
export let description
export let imageURL
export let linkURL
export let linkPeek
export let horizontal
export let showButton
export let buttonText
export let buttonOnClick
const { styleable, linkable } = getContext("sdk")
const { styleable, routeStore } = getContext("sdk")
const component = getContext("component")
$: external = linkURL && !linkURL.startsWith("/")
const handleLink = e => {
if (!linkURL) {
return
}
e.preventDefault()
routeStore.actions.navigate(linkURL, linkPeek)
}
</script>
<div
@ -37,16 +44,10 @@
<div class="spectrum-Card-header">
<div
class="spectrum-Card-title spectrum-Heading spectrum-Heading--sizeXS"
on:click={handleLink}
class:link={linkURL}
>
{#if linkURL}
{#if external}
<a href={linkURL}>{title || "Card Title"}</a>
{:else}
<a use:linkable href={linkURL}>{title || "Card Title"}</a>
{/if}
{:else}
{title || "Card Title"}
{/if}
{title || "Card Title"}
</div>
</div>
{#if subtitle}
@ -88,11 +89,12 @@
.spectrum-Card-container {
padding: var(--spectrum-global-dimension-size-50) 0;
}
.spectrum-Card-title :global(a) {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
.spectrum-Card-title.link {
transition: color 130ms ease-in-out;
}
.spectrum-Card-title.link:hover {
cursor: pointer;
color: var(--spectrum-link-primary-m-text-color-hover);
}
.spectrum-Card-subtitle {
text-overflow: ellipsis;
@ -103,14 +105,6 @@
word-wrap: anywhere;
white-space: pre-wrap;
}
a {
transition: color 130ms ease-in-out;
color: var(--spectrum-alias-text-color);
}
a:hover {
color: var(--spectrum-link-primary-m-text-color-hover);
}
.horizontal .spectrum-Card-coverPhoto {
flex: 0 0 160px;
height: auto;

41
packages/client/src/components/app/blocks/CardListWithSearch.svelte → packages/client/src/components/app/blocks/CardsBlock.svelte

@ -14,15 +14,20 @@
export let limit
export let showTitleButton
export let titleButtonText
export let titleButtonOnClick
export let titleButtonURL
export let titleButtonPeek
export let cardTitle
export let cardSubtitle
export let cardDescription
export let cardImageURL
export let linkCardTitle
export let cardURL
export let cardPeek
export let cardHorizontal
export let showCardButton
export let cardButtonText
export let cardButtonOnClick
export let linkColumn
const { API, styleable } = getContext("sdk")
const context = getContext("context")
@ -37,11 +42,27 @@
let formId
let dataProviderId
let repeaterId
let schema
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
$: cardWidth = cardHorizontal ? 420 : 300
$: fullCardURL = buildFullCardUrl(
linkCardTitle,
cardURL,
repeaterId,
linkColumn
)
$: titleButtonAction = [
{
"##eventHandlerType": "Navigate To",
parameters: {
peek: titleButtonPeek,
url: titleButtonURL,
},
},
]
// Enrich the default filter with the specified search fields
const enrichFilter = (filter, columns, formId) => {
@ -49,7 +70,7 @@
columns?.forEach(column => {
enrichedFilter.push({
field: column.name,
operator: "equal",
operator: column.type === "string" ? "string" : "equal",
type: "string",
valueType: "Binding",
value: `{{ [${formId}].[${column.name}] }}`,
@ -68,12 +89,23 @@
enrichedColumns.push({
name: column,
componentType,
type: schemaType,
})
}
})
return enrichedColumns.slice(0, 3)
}
// Builds a full details page URL for the card title
const buildFullCardUrl = (link, url, repeaterId, linkColumn) => {
if (!link || !url || !repeaterId) {
return null
}
const col = linkColumn || "_id"
const split = url.split("/:")
return `${split[0]}/{{ [${repeaterId}].[${col}] }}`
}
// Load the datasource schema on mount so we can determine column types
onMount(async () => {
if (dataSource) {
@ -113,7 +145,7 @@
<BlockComponent
type="button"
props={{
onClick: titleButtonOnClick,
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}
@ -136,6 +168,7 @@
>
<BlockComponent
type="repeater"
bind:id={repeaterId}
context="repeater"
props={{
dataProvider: `{{ literal [${dataProviderId}] }}`,
@ -161,6 +194,8 @@
showButton: showCardButton,
buttonText: cardButtonText,
buttonOnClick: cardButtonOnClick,
linkURL: fullCardURL,
linkPeek: cardPeek,
}}
styles={{
width: "auto",

17
packages/client/src/components/app/blocks/TableWithSearch.svelte → packages/client/src/components/app/blocks/TableBlock.svelte

@ -22,7 +22,8 @@
export let linkPeek
export let showTitleButton
export let titleButtonText
export let titleButtonOnClick
export let titleButtonURL
export let titleButtonPeek
const { API, styleable } = getContext("sdk")
const context = getContext("context")
@ -41,6 +42,15 @@
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
$: titleButtonAction = [
{
"##eventHandlerType": "Navigate To",
parameters: {
peek: titleButtonPeek,
url: titleButtonURL,
},
},
]
// Enrich the default filter with the specified search fields
const enrichFilter = (filter, columns, formId) => {
@ -48,7 +58,7 @@
columns?.forEach(column => {
enrichedFilter.push({
field: column.name,
operator: "equal",
operator: column.type === "string" ? "string" : "equal",
type: "string",
valueType: "Binding",
value: `{{ [${formId}].[${column.name}] }}`,
@ -67,6 +77,7 @@
enrichedColumns.push({
name: column,
componentType,
type: schemaType,
})
}
})
@ -112,7 +123,7 @@
<BlockComponent
type="button"
props={{
onClick: titleButtonOnClick,
onClick: titleButtonAction,
text: titleButtonText,
type: "cta",
}}

4
packages/client/src/components/app/blocks/index.js

@ -1,2 +1,2 @@
export { default as tablewithsearch } from "./TableWithSearch.svelte"
export { default as cardlistwithsearch } from "./CardListWithSearch.svelte"
export { default as tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte"

3
packages/client/src/utils/linkable.js

@ -4,6 +4,9 @@ import { builderStore } from "stores"
export const linkable = (node, href) => {
if (get(builderStore).inBuilder) {
node.onclick = e => {
e.preventDefault()
}
return
}
link(node, href)

3
packages/server/__mocks__/pg.ts

@ -21,15 +21,18 @@ module PgMock {
function Pool() {
}
const on = jest.fn()
Pool.prototype.query = query
Pool.prototype.connect = jest.fn(() => {
// @ts-ignore
return new Client()
})
Pool.prototype.on = on
pg.Client = Client
pg.Pool = Pool
pg.queryMock = query
pg.on = on
module.exports = pg
}

8
packages/server/package.json

@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
"version": "0.9.180-alpha.1",
"version": "0.9.184",
"description": "Budibase Web Server",
"main": "src/index.js",
"repository": {
@ -68,9 +68,9 @@
"author": "Budibase",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@budibase/auth": "^0.9.180-alpha.1",
"@budibase/client": "^0.9.180-alpha.1",
"@budibase/string-templates": "^0.9.180-alpha.1",
"@budibase/auth": "^0.9.184",
"@budibase/client": "^0.9.184",
"@budibase/string-templates": "^0.9.184",
"@elastic/elasticsearch": "7.10.0",
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",

18
packages/server/src/api/controllers/application.js

@ -198,7 +198,7 @@ exports.fetchAppPackage = async ctx => {
application,
screens,
layouts,
clientLibPath: clientLibraryPath(ctx.params.appId),
clientLibPath: clientLibraryPath(ctx.params.appId, application.version),
}
}
@ -324,7 +324,7 @@ exports.delete = async ctx => {
ctx.body = result
}
exports.sync = async ctx => {
exports.sync = async (ctx, next) => {
const appId = ctx.params.appId
if (!isDevAppID(appId)) {
ctx.throw(400, "This action cannot be performed for production apps")
@ -332,6 +332,20 @@ exports.sync = async ctx => {
// replicate prod to dev
const prodAppId = getDeployedAppID(appId)
try {
const prodDb = new CouchDB(prodAppId, { skip_setup: true })
const info = await prodDb.info()
if (info.error) throw info.error
} catch (err) {
// the database doesn't exist. Don't replicate
ctx.status = 200
ctx.body = {
message: "App sync not required, app not deployed.",
}
return next()
}
const replication = new Replication({
source: prodAppId,
target: appId,

7
packages/server/src/api/controllers/dev.js

@ -82,6 +82,13 @@ exports.revert = async ctx => {
const db = new CouchDB(productionAppId, { skip_setup: true })
const info = await db.info()
if (info.error) throw info.error
const deploymentDoc = await db.get(DocumentTypes.DEPLOYMENTS)
if (
!deploymentDoc.history ||
Object.keys(deploymentDoc.history).length === 0
) {
throw new Error("No deployments for app")
}
} catch (err) {
return ctx.throw(400, "App has not yet been deployed")
}

2
packages/server/src/api/controllers/static/index.js

@ -87,7 +87,7 @@ exports.serveApp = async function (ctx) {
title: appInfo.name,
production: env.isProd(),
appId,
clientLibPath: clientLibraryPath(appId),
clientLibPath: clientLibraryPath(appId, appInfo.version),
})
const appHbs = loadHandlebarsFile(`${__dirname}/templates/app.hbs`)

5
packages/server/src/api/controllers/table/external.js

@ -8,6 +8,7 @@ const {
generateForeignKey,
generateJunctionTableName,
foreignKeyStructure,
hasTypeChanged,
} = require("./utils")
const {
DataSourceOperation,
@ -172,6 +173,10 @@ exports.save = async function (ctx) {
oldTable = await getTable(appId, ctx.request.body._id)
}
if (hasTypeChanged(tableToSave, oldTable)) {
ctx.throw(400, "A column type has changed.")
}
const db = new CouchDB(appId)
const datasource = await db.get(datasourceId)
const oldTables = cloneDeep(datasource.entities)

6
packages/server/src/api/controllers/table/internal.js

@ -2,7 +2,7 @@ const CouchDB = require("../../../db")
const linkRows = require("../../../db/linkedRows")
const { getRowParams, generateTableID } = require("../../../db/utils")
const { FieldTypes } = require("../../../constants")
const { TableSaveFunctions } = require("./utils")
const { TableSaveFunctions, hasTypeChanged } = require("./utils")
exports.save = async function (ctx) {
const appId = ctx.appId
@ -21,6 +21,10 @@ exports.save = async function (ctx) {
oldTable = await db.get(ctx.request.body._id)
}
if (hasTypeChanged(tableToSave, oldTable)) {
ctx.throw(400, "A column type has changed.")
}
// saving a table is a complex operation, involving many different steps, this
// has been broken out into a utility to make it more obvious/easier to manipulate
const tableSaveFunctions = new TableSaveFunctions({

19
packages/server/src/api/controllers/table/utils.js

@ -8,7 +8,7 @@ const {
const { isEqual } = require("lodash/fp")
const { AutoFieldSubTypes, FieldTypes } = require("../../../constants")
const { inputProcessing } = require("../../../utilities/rowProcessor")
const { USERS_TABLE_SCHEMA } = require("../../../constants")
const { USERS_TABLE_SCHEMA, SwitchableTypes } = require("../../../constants")
const {
isExternalTable,
breakExternalTableId,
@ -335,4 +335,21 @@ exports.foreignKeyStructure = (keyName, meta = null) => {
return structure
}
exports.hasTypeChanged = (table, oldTable) => {
if (!oldTable) {
return false
}
for (let [key, field] of Object.entries(oldTable.schema)) {
const oldType = field.type
if (!table.schema[key]) {
continue
}
const newType = table.schema[key].type
if (oldType !== newType && SwitchableTypes.indexOf(oldType) === -1) {
return true
}
}
return false
}
exports.TableSaveFunctions = TableSaveFunctions

7
packages/server/src/constants/index.js

@ -45,6 +45,13 @@ exports.FieldTypes = {
INTERNAL: "internal",
}
exports.SwitchableTypes = [
exports.FieldTypes.STRING,
exports.FieldTypes.OPTIONS,
exports.FieldTypes.NUMBER,
exports.FieldTypes.BOOLEAN,
]
exports.RelationshipTypes = {
ONE_TO_MANY: "one-to-many",
MANY_TO_ONE: "many-to-one",

2
packages/server/src/integrations/base/sqlTable.ts

@ -30,7 +30,7 @@ function generateSchema(
// skip things that are already correct
const oldColumn = oldTable ? oldTable.schema[key] : null
if (
(oldColumn && oldColumn.type === column.type) ||
(oldColumn && oldColumn.type) ||
(primaryKey === key && !isJunction)
) {
continue

20
packages/server/src/integrations/postgres.ts

@ -28,6 +28,7 @@ module PostgresModule {
database: string
user: string
password: string
schema: string
ssl?: boolean
ca?: string
rejectUnauthorized?: boolean
@ -65,6 +66,11 @@ module PostgresModule {
default: "root",
required: true,
},
schema: {
type: DatasourceFieldTypes.STRING,
default: "public",
required: true,
},
ssl: {
type: DatasourceFieldTypes.BOOLEAN,
default: false,
@ -124,8 +130,7 @@ module PostgresModule {
public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {}
COLUMNS_SQL =
"select * from information_schema.columns where not table_schema = 'information_schema' and not table_schema = 'pg_catalog'"
COLUMNS_SQL!: string
PRIMARY_KEYS_SQL = `
select tc.table_schema, tc.table_name, kc.column_name as primary_key
@ -155,6 +160,17 @@ module PostgresModule {
}
this.client = this.pool
this.setSchema()
}
setSchema() {
if (!this.config.schema) {
this.config.schema = 'public'
}
this.client.on('connect', (client: any) => {
client.query(`SET search_path TO ${this.config.schema}`);
});
this.COLUMNS_SQL = `select * from information_schema.columns where table_schema = '${this.config.schema}'`
}
/**

4
packages/server/src/integrations/tests/postgres.spec.js

@ -15,6 +15,10 @@ describe("Postgres Integration", () => {
config = new TestConfiguration()
})
it("calls the connection callback", async () => {
expect(pg.on).toHaveBeenCalledWith('connect', expect.anything())
})
it("calls the create method with the correct params", async () => {
const sql = "insert into users (name, age) values ('Joe', 123);"
await config.integration.create({

9
packages/server/src/utilities/index.js

@ -51,11 +51,16 @@ exports.objectStoreUrl = () => {
* @return {string} The URL to be inserted into appPackage response or server rendered
* app index file.
*/
exports.clientLibraryPath = appId => {
exports.clientLibraryPath = (appId, version) => {
if (env.isProd()) {
return `${exports.objectStoreUrl()}/${sanitizeKey(
let url = `${exports.objectStoreUrl()}/${sanitizeKey(
appId
)}/budibase-client.js`
// append app version to bust the cache
if (version) {
url += `?v=${version}`
}
return url
} else {
return `/api/assets/client`
}

2
packages/string-templates/package.json

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

6
packages/worker/package.json

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

2
packages/worker/src/utilities/appService.js

@ -27,7 +27,7 @@ exports.syncUserInApps = async userId => {
"POST",
{}
)
if (response.status !== 200) {
if (response && response.status !== 200) {
throw "Unable to sync user."
}
}

Loading…
Cancel
Save