Merge remote-tracking branch 'origin/master' into fix/oidc-custom-icons-fixed

This commit is contained in:
Dean 2023-06-23 14:58:47 +01:00
commit a61cf5fa45
50 changed files with 382 additions and 238 deletions

View File

@ -12,31 +12,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# - name: Fail if not a tag - name: Fail if not a tag
# run: |
# if [[ $GITHUB_REF != refs/tags/* ]]; then
# echo "Workflow Dispatch can only be run on tags"
# exit 1
# fi
- uses: actions/checkout@v2
# with:
# fetch-depth: 0
# - name: Fail if tag is not in master
# run: |
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
# exit 1
# fi
- name: Pull values.yaml from budibase-infra
run: | run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \ if [[ $GITHUB_REF != refs/tags/* ]]; then
-H 'Accept: application/vnd.github.v3.raw' \ echo "Workflow Dispatch can only be run on tags"
-o values.production.yaml \ exit 1
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml fi
wc -l values.production.yaml - uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Get the latest budibase release version - name: Get the latest budibase release version
id: version id: version
@ -48,29 +39,10 @@ jobs:
fi fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials - uses: passeidireto/trigger-external-workflow-action@main
uses: aws-actions/configure-aws-credentials@v1 env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with: with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} repository: budibase/budibase-deploys
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} event: budicloud-prod-deploy
aws-region: eu-west-1 github_pat: ${{ secrets.GH_ACCESS_TOKEN }}
- name: Deploy to EKS
uses: craftech-io/eks-helm-deploy-action@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS__KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
cluster-name: budibase-eks-production
config-files: values.production.yaml
chart-path: charts/budibase
namespace: budibase
values: globals.appVersion=v${{ env.RELEASE_VERSION }},services.couchdb.url=${{ secrets.PRODUCTION_COUCHDB_URL }},services.couchdb.password=${{ secrets.PRODUCTION_COUCHDB_PASSWORD }}
name: budibase-prod
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
embed-title: ${{ env.RELEASE_VERSION }}

View File

@ -24,51 +24,18 @@ jobs:
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch" echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1 exit 1
fi fi
- name: Get the latest budibase release version - name: Get the latest budibase release version
id: version id: version
run: | run: |
release_version=$(cat lerna.json | jq -r '.version') release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: eu-west-1
- name: Pull values.yaml from budibase-infra - uses: passeidireto/trigger-external-workflow-action@main
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.preprod.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml
wc -l values.preprod.yaml
- name: Deploy to Preprod Environment
uses: budibase/helm@v1.8.0
with:
release: budibase-preprod
namespace: budibase
chart: charts/budibase
token: ${{ github.token }}
helm: helm3
values: |
globals:
appVersion: v${{ env.RELEASE_VERSION }}
ingress:
enabled: true
nginx: true
value-files: >-
[
"values.preprod.yaml"
]
env: env:
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}' PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
with: with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} repository: budibase/budibase-deploys
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod." event: budicloud-preprod-deploy
embed-title: ${{ env.RELEASE_VERSION }} github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

@ -1,5 +1,5 @@
{ {
"version": "2.7.11", "version": "2.7.33",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/backend-core", "packages/backend-core",

View File

@ -87,7 +87,7 @@
border-color: var(--spectrum-global-color-gray-400); border-color: var(--spectrum-global-color-gray-400);
} }
/* Toolbar button color */ /* Toolbar button color */
:global(.EasyMDEContainer .editor-toolbar button i) { :global(.EasyMDEContainer .editor-toolbar button) {
color: var(--spectrum-global-color-gray-800); color: var(--spectrum-global-color-gray-800);
} }
/* Separator between toolbar buttons*/ /* Separator between toolbar buttons*/

View File

@ -5,9 +5,10 @@
<meta charset='utf8'> <meta charset='utf8'>
<meta name='viewport' content='width=device-width'> <meta name='viewport' content='width=device-width'>
<title>Budibase</title> <title>Budibase</title>
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" <link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
rel="stylesheet" /> <link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
<link href="/builder/fonts/remixicon.css" rel="stylesheet" />
</head> </head>
<body id="app"> <body id="app">

View File

@ -70,6 +70,7 @@
"@codemirror/state": "^6.2.0", "@codemirror/state": "^6.2.0",
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/view": "^6.11.2", "@codemirror/view": "^6.11.2",
"@fontsource/source-sans-pro": "^5.0.3",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",
@ -122,6 +123,7 @@
"tsconfig-paths": "4.0.0", "tsconfig-paths": "4.0.0",
"typescript": "4.7.3", "typescript": "4.7.3",
"vite": "^3.0.8", "vite": "^3.0.8",
"vite-plugin-static-copy": "^0.16.0",
"vitest": "^0.29.2" "vitest": "^0.29.2"
}, },
"nx": { "nx": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

@ -12,7 +12,7 @@
import { automationStore, selectedAutomation } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
import { admin, licensing } from "stores/portal" import { admin, licensing } from "stores/portal"
import { externalActions } from "./ExternalActions" import { externalActions } from "./ExternalActions"
import { TriggerStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { checkForCollectStep } from "builderStore/utils" import { checkForCollectStep } from "builderStore/utils"
export let blockIdx export let blockIdx
@ -149,7 +149,7 @@
<div class="item-body"> <div class="item-body">
<Icon name={action.icon} /> <Icon name={action.icon} />
<Body size="XS">{action.name}</Body> <Body size="XS">{action.name}</Body>
{#if isDisabled && !syncAutomationsEnabled} {#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
<div class="tag-color"> <div class="tag-color">
<Tags> <Tags>
<Tag icon="LockClosed">Business</Tag> <Tag icon="LockClosed">Business</Tag>

View File

@ -1,5 +1,5 @@
<script> <script>
import { tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import EditRolesButton from "./buttons/EditRolesButton.svelte" import EditRolesButton from "./buttons/EditRolesButton.svelte"
import { TableNames } from "constants" import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core" import { Grid } from "@budibase/frontend-core"
@ -26,6 +26,18 @@
$: id = $tables.selected?._id $: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS $: isUsersTable = id === TableNames.USERS
$: isInternal = $tables.selected?.type !== "external" $: isInternal = $tables.selected?.type !== "external"
const handleGridTableUpdate = async e => {
tables.replaceTable(id, e.detail)
// We need to refresh datasources when an external table changes.
// Type "external" may exist - sometimes type is "table" and sometimes it
// is "external" - it has different meanings in different endpoints.
// If we check both these then we hopefully catch all external tables.
if (e.detail?.type === "external" || e.detail?.sql) {
await datasources.fetch()
}
}
</script> </script>
<div class="wrapper"> <div class="wrapper">
@ -37,7 +49,7 @@
allowDeleteRows={!isUsersTable} allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null} schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false} showAvatars={false}
on:updatetable={e => tables.replaceTable(id, e.detail)} on:updatetable={handleGridTableUpdate}
> >
<svelte:fragment slot="controls"> <svelte:fragment slot="controls">
{#if isInternal} {#if isInternal}

View File

@ -76,6 +76,10 @@ export function getBindings({
// will be replaced by the main array binding // will be replaced by the main array binding
readableBinding: label, readableBinding: label,
runtimeBinding: binding, runtimeBinding: binding,
display: {
name: label,
type: field.name === FIELDS.LINK.name ? "Array" : field.name,
},
}) })
} }
return bindings return bindings

View File

@ -59,7 +59,6 @@
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet() $: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE $: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
$: toRelationship.relationshipType = fromRelationship?.relationshipType
function getTable(id) { function getTable(id) {
return plusTables.find(table => table._id === id) return plusTables.find(table => table._id === id)
@ -180,6 +179,16 @@
return getErrorCount(errors) === 0 return getErrorCount(errors) === 0
} }
function otherRelationshipType(type) {
if (type === RelationshipTypes.MANY_TO_ONE) {
return RelationshipTypes.ONE_TO_MANY
} else if (type === RelationshipTypes.ONE_TO_MANY) {
return RelationshipTypes.MANY_TO_ONE
} else if (type === RelationshipTypes.MANY_TO_MANY) {
return RelationshipTypes.MANY_TO_MANY
}
}
function buildRelationships() { function buildRelationships() {
const id = Helpers.uuid() const id = Helpers.uuid()
//Map temporary variables //Map temporary variables
@ -200,6 +209,7 @@
...toRelationship, ...toRelationship,
tableId: fromId, tableId: fromId,
name: fromColumn, name: fromColumn,
relationshipType: otherRelationshipType(relationshipType),
through: throughId, through: throughId,
type: "link", type: "link",
_id: id, _id: id,

View File

@ -93,6 +93,7 @@
try { try {
await beforeSave() await beforeSave()
table = await tables.save(newTable) table = await tables.save(newTable)
await datasources.fetch()
await afterSave(table) await afterSave(table)
} catch (e) { } catch (e) {
notifications.error(e) notifications.error(e)

View File

@ -65,6 +65,7 @@
const updatedTable = cloneDeep(table) const updatedTable = cloneDeep(table)
updatedTable.name = updatedName updatedTable.name = updatedName
await tables.save(updatedTable) await tables.save(updatedTable)
await datasources.fetch()
notifications.success("Table renamed successfully") notifications.success("Table renamed successfully")
} }

View File

@ -9,6 +9,18 @@
faFileArrowUp, faFileArrowUp,
faChevronLeft, faChevronLeft,
faCircleInfo, faCircleInfo,
faBold,
faItalic,
faHeading,
faQuoteLeft,
faListUl,
faListOl,
faLink,
faImage,
faEye,
faColumns,
faArrowsAlt,
faQuestionCircle,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons" import { faGithub, faDiscord } from "@fortawesome/free-brands-svg-icons"
@ -22,7 +34,22 @@
faEnvelope, faEnvelope,
faFileArrowUp, faFileArrowUp,
faChevronLeft, faChevronLeft,
faCircleInfo faCircleInfo,
// -- Required for easyMDE use in the builder.
faBold,
faItalic,
faHeading,
faQuoteLeft,
faListUl,
faListOl,
faLink,
faImage,
faEye,
faColumns,
faArrowsAlt,
faQuestionCircle
// --
) )
dom.watch() dom.watch()
</script> </script>

View File

@ -19,7 +19,7 @@
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { store } from "builderStore"
import { convertToJS } from "@budibase/string-templates" import { convertToJS } from "@budibase/string-templates"
import { admin } from "stores/portal" import { admin } from "stores/portal"
import CodeEditor from "../CodeEditor/CodeEditor.svelte" import CodeEditor from "../CodeEditor/CodeEditor.svelte"
@ -339,25 +339,28 @@
</Tab> </Tab>
{/if} {/if}
<div class="drawer-actions"> <div class="drawer-actions">
<Button {#if drawerActions?.hide}
secondary <Button
quiet secondary
on:click={() => { quiet
store.actions.settings.propertyFocus(null) on:click={() => {
drawerActions.hide() drawerActions.hide()
}} }}
> >
Cancel Cancel
</Button> </Button>
<Button {/if}
cta {#if bindingDrawerActions?.save}
disabled={!valid} <Button
on:click={() => { cta
bindingDrawerActions.save() disabled={!valid}
}} on:click={() => {
> bindingDrawerActions.save()
Save }}
</Button> >
Save
</Button>
{/if}
</div> </div>
</Tabs> </Tabs>
</div> </div>

View File

@ -36,7 +36,7 @@
.map(([name, categoryBindings]) => ({ .map(([name, categoryBindings]) => ({
name, name,
bindings: categoryBindings?.filter(binding => { bindings: categoryBindings?.filter(binding => {
return binding.readableBinding.match(searchRgx) return !search || binding.readableBinding.match(searchRgx)
}), }),
})) }))
.filter(category => { .filter(category => {
@ -46,7 +46,11 @@
) )
}) })
$: filteredHelpers = helpers?.filter(helper => { $: filteredHelpers = helpers?.filter(helper => {
return helper.label.match(searchRgx) || helper.description.match(searchRgx) return (
!search ||
helper.label.match(searchRgx) ||
helper.description.match(searchRgx)
)
}) })
const getHelperExample = (helper, js) => { const getHelperExample = (helper, js) => {
@ -124,9 +128,6 @@
<span <span
class="search-input-icon" class="search-input-icon"
on:click={() => { on:click={() => {
if (!search) {
return
}
search = null search = null
}} }}
class:searching={search} class:searching={search}

View File

@ -76,7 +76,7 @@
{/if} {/if}
</div> </div>
<Drawer bind:this={bindingDrawer} {title}> <Drawer bind:this={bindingDrawer} {title} headless>
<svelte:fragment slot="description"> <svelte:fragment slot="description">
Add the objects on the left to enrich your text. Add the objects on the left to enrich your text.
</svelte:fragment> </svelte:fragment>

View File

@ -5,8 +5,6 @@
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { store } from "builderStore"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte" import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates" import { isJSBinding } from "@budibase/string-templates"
@ -36,7 +34,6 @@
const saveBinding = () => { const saveBinding = () => {
onChange(tempValue) onChange(tempValue)
store.actions.settings.propertyFocus(null)
onBlur() onBlur()
bindingDrawer.hide() bindingDrawer.hide()
} }
@ -70,7 +67,6 @@
<div <div
class="icon" class="icon"
on:click={() => { on:click={() => {
store.actions.settings.propertyFocus(key)
bindingDrawer.show() bindingDrawer.show()
}} }}
> >

View File

@ -73,10 +73,6 @@
if (highlighted) { if (highlighted) {
store.actions.settings.highlight(null) store.actions.settings.highlight(null)
} }
// To fix focus 'affect' when property is target of a drawer other actions in the builder.
if (propertyFocus) {
store.actions.settings.propertyFocus(null)
}
}) })
</script> </script>

View File

@ -186,7 +186,6 @@
} }
div :global(.CodeMirror) { div :global(.CodeMirror) {
width: var(--code-mirror-width) !important;
height: var(--code-mirror-height) !important; height: var(--code-mirror-height) !important;
border-radius: var(--border-radius-s); border-radius: var(--border-radius-s);
font-family: var(--font-mono); font-family: var(--font-mono);

View File

@ -8,7 +8,7 @@
$: platformTitle = $: platformTitle =
!$auth.user && platformTitleText ? platformTitleText : "Budibase" !$auth.user && platformTitleText ? platformTitleText : "Budibase"
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png" $: faviconUrl = $organisation.faviconUrl || "/builder/bblogo.png"
onMount(async () => { onMount(async () => {
await organisation.init() await organisation.init()
@ -27,6 +27,6 @@
<link rel="icon" href={faviconUrl} /> <link rel="icon" href={faviconUrl} />
{:else} {:else}
<!-- A default must be set or the browser defaults to favicon.ico behaviour --> <!-- A default must be set or the browser defaults to favicon.ico behaviour -->
<link rel="icon" href={"https://i.imgur.com/Xhdt1YP.png"} /> <link rel="icon" href={"/builder/bblogo.png"} />
{/if} {/if}
</svelte:head> </svelte:head>

View File

@ -44,7 +44,7 @@
<div tabindex="-1" class="exampleApp"> <div tabindex="-1" class="exampleApp">
<div class="page"> <div class="page">
<div class="header"> <div class="header">
<img alt="Budibase Logo" src={"https://i.imgur.com/Xhdt1YP.png"} /> <img alt="Budibase Logo" src={"/builder/bblogo.png"} />
<h1>{name}</h1> <h1>{name}</h1>
</div> </div>
<div class="nav">Home</div> <div class="nav">Home</div>

View File

@ -113,6 +113,10 @@ export function createDatasourcesStore() {
...state, ...state,
list: [...state.list, datasource], list: [...state.list, datasource],
})) }))
// If this is a new datasource then we should refresh the tables list,
// because otherwise we'll never see the new tables
tables.fetch()
} }
// Update existing datasource // Update existing datasource

View File

@ -1,5 +1,4 @@
import { get, writable, derived } from "svelte/store" import { get, writable, derived } from "svelte/store"
import { datasources } from "./"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"
import { API } from "api" import { API } from "api"
import { SWITCHABLE_TYPES } from "constants/backend" import { SWITCHABLE_TYPES } from "constants/backend"
@ -63,7 +62,6 @@ export function createTablesStore() {
const savedTable = await API.saveTable(updatedTable) const savedTable = await API.saveTable(updatedTable)
replaceTable(savedTable._id, savedTable) replaceTable(savedTable._id, savedTable)
await datasources.fetch()
select(savedTable._id) select(savedTable._id)
return savedTable return savedTable
} }

View File

@ -1,6 +1,7 @@
import { svelte } from "@sveltejs/vite-plugin-svelte" import { svelte } from "@sveltejs/vite-plugin-svelte"
import replace from "@rollup/plugin-replace" import replace from "@rollup/plugin-replace"
import { defineConfig, loadEnv } from "vite" import { defineConfig, loadEnv } from "vite"
import { viteStaticCopy } from "vite-plugin-static-copy"
import path from "path" import path from "path"
const ignoredWarnings = [ const ignoredWarnings = [
@ -59,6 +60,18 @@ export default defineConfig(({ mode }) => {
), ),
"process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN), "process.env.SENTRY_DSN": JSON.stringify(process.env.SENTRY_DSN),
}), }),
viteStaticCopy({
targets: [
{
src: "../../node_modules/@fontsource/source-sans-pro",
dest: "fonts",
},
{
src: "../../node_modules/remixicon/fonts/*",
dest: "fonts",
},
],
}),
], ],
optimizeDeps: { optimizeDeps: {
exclude: ["@roxi/routify"], exclude: ["@roxi/routify"],

View File

@ -180,10 +180,7 @@
{/if} {/if}
<div class="logo"> <div class="logo">
{#if !hideLogo} {#if !hideLogo}
<img <img src={logoUrl || "/builder/bblogo.png"} alt={title} />
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"}
alt={title}
/>
{/if} {/if}
{#if !hideTitle && title} {#if !hideTitle && title}
<Heading size="S">{title}</Heading> <Heading size="S">{title}</Heading>

View File

@ -18,7 +18,7 @@
<img <img
class="logo" class="logo"
alt="logo" alt="logo"
src={logoUrl || "https://i.imgur.com/Xhdt1YP.png"} src={logoUrl || "/builder/bblogo.png"}
height="48" height="48"
/> />
</a> </a>

View File

@ -283,7 +283,7 @@
// Skip if the value is the same // Skip if the value is the same
if (!skipCheck && fieldState.value === value) { if (!skipCheck && fieldState.value === value) {
return true return false
} }
// Update field state // Update field state
@ -295,7 +295,7 @@
return state return state
}) })
return !error return true
} }
// Clears the value of a certain field back to the default value // Clears the value of a certain field back to the default value
@ -376,8 +376,9 @@
deregister, deregister,
validate: () => { validate: () => {
// Validate the field by force setting the same value again // Validate the field by force setting the same value again
const { fieldState } = get(getField(field)) const fieldInfo = getField(field)
return setValue(fieldState.value, true) setValue(get(fieldInfo).fieldState.value, true)
return !get(fieldInfo).fieldState.error
}, },
} }
} }

View File

@ -1,18 +1,30 @@
import { authStore } from "../stores/auth.js" import { authStore } from "../stores/auth.js"
import { appStore } from "../stores/app.js"
import { get } from "svelte/store" import { get } from "svelte/store"
import { Constants } from "@budibase/frontend-core" import { Constants } from "@budibase/frontend-core"
const getLicense = () => { const getUserLicense = () => {
const user = get(authStore) const user = get(authStore)
if (user) { if (user) {
return user.license return user.license
} }
} }
const getAppLicenseType = () => {
const appDef = get(appStore)
if (appDef?.licenseType) {
return appDef.licenseType
}
}
export const isFreePlan = () => { export const isFreePlan = () => {
const license = getLicense() let licenseType = getAppLicenseType()
if (license) { if (!licenseType) {
return license.plan.type === Constants.PlanType.FREE const license = getUserLicense()
licenseType = license?.plan?.type
}
if (licenseType) {
return licenseType === Constants.PlanType.FREE
} else { } else {
// safety net - no license means free plan // safety net - no license means free plan
return true return true

View File

@ -90,12 +90,12 @@ export const deriveStores = context => {
// Update local state // Update local state
table.set(newTable) table.set(newTable)
// Update server
await API.saveTable(newTable)
// Broadcast change to external state can be updated, as this change // Broadcast change to external state can be updated, as this change
// will not be received by the builder websocket because we caused it ourselves // will not be received by the builder websocket because we caused it ourselves
dispatch("updatetable", newTable) dispatch("updatetable", newTable)
// Update server
await API.saveTable(newTable)
} }
return { return {

View File

@ -2,6 +2,7 @@ import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData" import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { NewRowID, RowPageSize } from "../lib/constants" import { NewRowID, RowPageSize } from "../lib/constants"
import { tick } from "svelte"
const initialSortState = { const initialSortState = {
column: null, column: null,
@ -124,13 +125,22 @@ export const deriveStores = context => {
}) })
// Subscribe to changes of this fetch model // Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe($fetch => { unsubscribe = newFetch.subscribe(async $fetch => {
if ($fetch.loaded && !$fetch.loading) { if ($fetch.loaded && !$fetch.loading) {
hasNextPage.set($fetch.hasNextPage) hasNextPage.set($fetch.hasNextPage)
const $instanceLoaded = get(instanceLoaded) const $instanceLoaded = get(instanceLoaded)
const resetRows = $fetch.resetKey !== lastResetKey const resetRows = $fetch.resetKey !== lastResetKey
const previousResetKey = lastResetKey
lastResetKey = $fetch.resetKey lastResetKey = $fetch.resetKey
// If resetting rows due to a table change, wipe data and wait for
// derived stores to compute. This prevents stale data being passed
// to cells when we save the new schema.
if (!$instanceLoaded && previousResetKey) {
rows.set([])
await tick()
}
// Reset state properties when dataset changes // Reset state properties when dataset changes
if (!$instanceLoaded || resetRows) { if (!$instanceLoaded || resetRows) {
table.set($fetch.definition) table.set($fetch.definition)

@ -1 +1 @@
Subproject commit 01fbc8670021c5a275c2a1a36ee18b984eeafad5 Subproject commit f4b8449aac9bd265214396afbdce7ff984a2ae34

View File

@ -1,53 +1,54 @@
import env from "../../environment" import env from "../../environment"
import { import {
createAllSearchIndex,
createLinkView, createLinkView,
createRoutingView, createRoutingView,
createAllSearchIndex,
} from "../../db/views/staticViews" } from "../../db/views/staticViews"
import { createApp, deleteApp } from "../../utilities/fileSystem"
import { import {
backupClientLibrary,
createApp,
deleteApp,
revertClientLibrary,
updateClientLibrary,
} from "../../utilities/fileSystem"
import {
AppStatus,
DocumentType,
generateAppID, generateAppID,
generateDevAppID,
getLayoutParams, getLayoutParams,
getScreenParams, getScreenParams,
generateDevAppID,
DocumentType,
AppStatus,
} from "../../db/utils" } from "../../db/utils"
import { import {
db as dbCore,
roles,
cache, cache,
tenancy,
context, context,
db as dbCore,
env as envCore,
ErrorCode,
events, events,
migrations, migrations,
objectStore, objectStore,
ErrorCode, roles,
env as envCore, tenancy,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { USERS_TABLE_SCHEMA } from "../../constants" import { USERS_TABLE_SCHEMA } from "../../constants"
import { import {
DEFAULT_BB_DATASOURCE_ID,
buildDefaultDocs, buildDefaultDocs,
DEFAULT_BB_DATASOURCE_ID,
} from "../../db/defaultData/datasource_bb_default" } from "../../db/defaultData/datasource_bb_default"
import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { removeAppFromUserRoles } from "../../utilities/workerRequests"
import { stringToReadStream, isQsTrue } from "../../utilities" import { stringToReadStream } from "../../utilities"
import { getLocksById, doesUserHaveLock } from "../../utilities/redis" import { doesUserHaveLock, getLocksById } from "../../utilities/redis"
import {
updateClientLibrary,
backupClientLibrary,
revertClientLibrary,
} from "../../utilities/fileSystem"
import { cleanupAutomations } from "../../automations/utils" import { cleanupAutomations } from "../../automations/utils"
import { checkAppMetadata } from "../../automations/logging" import { checkAppMetadata } from "../../automations/logging"
import { getUniqueRows } from "../../utilities/usageQuota/rows" import { getUniqueRows } from "../../utilities/usageQuota/rows"
import { quotas, groups } from "@budibase/pro" import { groups, licensing, quotas } from "@budibase/pro"
import { import {
App, App,
Layout, Layout,
Screen,
MigrationType, MigrationType,
Database, PlanType,
Screen,
UserCtx, UserCtx,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
@ -207,6 +208,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
let application = await db.get(DocumentType.APP_METADATA) let application = await db.get(DocumentType.APP_METADATA)
const layouts = await getLayouts() const layouts = await getLayouts()
let screens = await getScreens() let screens = await getScreens()
const license = await licensing.cache.getCachedLicense()
// Enrich plugin URLs // Enrich plugin URLs
application.usedPlugins = objectStore.enrichPluginURLs( application.usedPlugins = objectStore.enrichPluginURLs(
@ -227,6 +229,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
ctx.body = { ctx.body = {
application: { ...application, upgradableVersion: envCore.VERSION }, application: { ...application, upgradableVersion: envCore.VERSION },
licenseType: license?.plan.type || PlanType.FREE,
screens, screens,
layouts, layouts,
clientLibPath, clientLibPath,

View File

@ -19,6 +19,7 @@ import {
breakRowIdField, breakRowIdField,
convertRowId, convertRowId,
generateRowIdField, generateRowIdField,
getPrimaryDisplay,
isRowId, isRowId,
isSQL, isSQL,
} from "../../../integrations/utils" } from "../../../integrations/utils"
@ -391,7 +392,10 @@ export class ExternalRequest {
} }
} }
relatedRow = processFormulas(linkedTable, relatedRow) relatedRow = processFormulas(linkedTable, relatedRow)
const relatedDisplay = display ? relatedRow[display] : undefined let relatedDisplay
if (display) {
relatedDisplay = getPrimaryDisplay(relatedRow[display])
}
row[relationship.column][key] = { row[relationship.column][key] = {
primaryDisplay: relatedDisplay || "Invalid display column", primaryDisplay: relatedDisplay || "Invalid display column",
_id: relatedRow._id, _id: relatedRow._id,

View File

@ -3,10 +3,10 @@ import * as userController from "../user"
import { FieldTypes } from "../../../constants" import { FieldTypes } from "../../../constants"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { makeExternalQuery } from "../../../integrations/base/query" import { makeExternalQuery } from "../../../integrations/base/query"
import { Row, Table } from "@budibase/types" import { FieldType, Row, Table, UserCtx } from "@budibase/types"
import { Format } from "../view/exporters" import { Format } from "../view/exporters"
import { UserCtx } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
const validateJs = require("validate.js") const validateJs = require("validate.js")
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
@ -20,6 +20,13 @@ validateJs.extend(validateJs.validators.datetime, {
}, },
}) })
function isForeignKey(key: string, table: Table) {
const relationships = Object.values(table.schema).filter(
column => column.type === FieldType.LINK
)
return relationships.some(relationship => relationship.foreignKey === key)
}
export async function getDatasourceAndQuery(json: any) { export async function getDatasourceAndQuery(json: any) {
const datasourceId = json.endpoint.datasourceId const datasourceId = json.endpoint.datasourceId
const datasource = await sdk.datasources.get(datasourceId) const datasource = await sdk.datasources.get(datasourceId)
@ -65,6 +72,10 @@ export async function validate({
const column = fetchedTable.schema[fieldName] const column = fetchedTable.schema[fieldName]
const constraints = cloneDeep(column.constraints) const constraints = cloneDeep(column.constraints)
const type = column.type const type = column.type
// foreign keys are likely to be enriched
if (isForeignKey(fieldName, fetchedTable)) {
continue
}
// formulas shouldn't validated, data will be deleted anyway // formulas shouldn't validated, data will be deleted anyway
if (type === FieldTypes.FORMULA || column.autocolumn) { if (type === FieldTypes.FORMULA || column.autocolumn) {
continue continue

View File

@ -40,19 +40,14 @@
{#if favicon !== ""} {#if favicon !== ""}
<link rel="icon" type="image/png" href={favicon} /> <link rel="icon" type="image/png" href={favicon} />
{:else} {:else}
<link rel="icon" type="image/png" href="https://i.imgur.com/Xhdt1YP.png" /> <link rel="icon" type="image/png" href="/builder/bblogo.png" />
{/if} {/if}
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
<link <link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" <link href="/builder/fonts/remixicon.css" rel="stylesheet" />
rel="stylesheet"
/>
<link
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
rel="stylesheet"
/>
<style> <style>
html, html,
body { body {

View File

@ -1,16 +1,10 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Budibase Builder Preview</title> <title>Budibase Builder Preview</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" /> <link href="/builder/fonts/source-sans-pro/400.css" rel="stylesheet" />
<link rel="preconnect" href="https://fonts.gstatic.com" /> <link href="/builder/fonts/source-sans-pro/600.css" rel="stylesheet" />
<link <link href="/builder/fonts/source-sans-pro/700.css" rel="stylesheet" />
href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap" <link href="/builder/fonts/remixicon.css" rel="stylesheet" />
rel="stylesheet"
/>
<link
href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css"
rel="stylesheet"
/>
<style> <style>
html, body { html, body {
padding: 0; padding: 0;

View File

@ -26,6 +26,7 @@ import {
RelationshipTypes, RelationshipTypes,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets"
const { cloneDeep } = require("lodash/fp") const { cloneDeep } = require("lodash/fp")
async function makeTableRequest( async function makeTableRequest(
@ -318,6 +319,11 @@ export async function save(ctx: UserCtx) {
datasource.entities[tableToSave.name] = tableToSave datasource.entities[tableToSave.name] = tableToSave
await db.put(datasource) await db.put(datasource)
// Since tables are stored inside datasources, we need to notify clients
// that the datasource definition changed
const updatedDatasource = await db.get(datasource._id)
builderSocket?.emitDatasourceUpdate(ctx, updatedDatasource)
return tableToSave return tableToSave
} }
@ -344,6 +350,11 @@ export async function destroy(ctx: UserCtx) {
await db.put(datasource) await db.put(datasource)
// Since tables are stored inside datasources, we need to notify clients
// that the datasource definition changed
const updatedDatasource = await db.get(datasource._id)
builderSocket?.emitDatasourceUpdate(ctx, updatedDatasource)
return tableToDelete return tableToDelete
} }

View File

@ -12,7 +12,7 @@ import {
Row, Row,
SearchFilters, SearchFilters,
SortJson, SortJson,
Table, ExternalTable,
TableRequest, TableRequest,
} from "@budibase/types" } from "@budibase/types"
import { OAuth2Client } from "google-auth-library" import { OAuth2Client } from "google-auth-library"
@ -136,7 +136,7 @@ const SCHEMA: Integration = {
class GoogleSheetsIntegration implements DatasourcePlus { class GoogleSheetsIntegration implements DatasourcePlus {
private readonly config: GoogleSheetsConfig private readonly config: GoogleSheetsConfig
private client: GoogleSpreadsheet private client: GoogleSpreadsheet
public tables: Record<string, Table> = {} public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
constructor(config: GoogleSheetsConfig) { constructor(config: GoogleSheetsConfig) {
@ -248,12 +248,18 @@ class GoogleSheetsIntegration implements DatasourcePlus {
return sheets.map(s => s.title) return sheets.map(s => s.title)
} }
getTableSchema(title: string, headerValues: string[], id?: string) { getTableSchema(
title: string,
headerValues: string[],
datasourceId: string,
id?: string
) {
// base table // base table
const table: Table = { const table: ExternalTable = {
name: title, name: title,
primary: [GOOGLE_SHEETS_PRIMARY_KEY], primary: [GOOGLE_SHEETS_PRIMARY_KEY],
schema: {}, schema: {},
sourceId: datasourceId,
} }
if (id) { if (id) {
table._id = id table._id = id
@ -268,14 +274,17 @@ class GoogleSheetsIntegration implements DatasourcePlus {
return table return table
} }
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(
datasourceId: string,
entities: Record<string, ExternalTable>
) {
// not fully configured yet // not fully configured yet
if (!this.config.auth) { if (!this.config.auth) {
return return
} }
await this.connect() await this.connect()
const sheets = this.client.sheetsByIndex const sheets = this.client.sheetsByIndex
const tables: Record<string, Table> = {} const tables: Record<string, ExternalTable> = {}
for (let sheet of sheets) { for (let sheet of sheets) {
// must fetch rows to determine schema // must fetch rows to determine schema
await sheet.getRows() await sheet.getRows()
@ -284,6 +293,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
tables[sheet.title] = this.getTableSchema( tables[sheet.title] = this.getTableSchema(
sheet.title, sheet.title,
sheet.headerValues, sheet.headerValues,
datasourceId,
id id
) )
} }

View File

@ -2,7 +2,7 @@ import {
DatasourceFieldType, DatasourceFieldType,
Integration, Integration,
Operation, Operation,
Table, ExternalTable,
TableSchema, TableSchema,
QueryJson, QueryJson,
QueryType, QueryType,
@ -97,7 +97,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
private index: number = 0 private index: number = 0
private readonly pool: any private readonly pool: any
private client: any private client: any
public tables: Record<string, Table> = {} public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
MASTER_TABLES = [ MASTER_TABLES = [
@ -220,7 +220,10 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
* @param {*} datasourceId - datasourceId to fetch * @param {*} datasourceId - datasourceId to fetch
* @param entities - the tables that are to be built * @param entities - the tables that are to be built
*/ */
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(
datasourceId: string,
entities: Record<string, ExternalTable>
) {
await this.connect() await this.connect()
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL) let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
if (tableInfo == null || !Array.isArray(tableInfo)) { if (tableInfo == null || !Array.isArray(tableInfo)) {
@ -233,7 +236,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
.map((record: any) => record.TABLE_NAME) .map((record: any) => record.TABLE_NAME)
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1) .filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
const tables: Record<string, Table> = {} const tables: Record<string, ExternalTable> = {}
for (let tableName of tableNames) { for (let tableName of tableNames) {
// get the column definition (type) // get the column definition (type)
const definition = await this.runSQL(this.getDefinitionSQL(tableName)) const definition = await this.runSQL(this.getDefinitionSQL(tableName))
@ -276,6 +279,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
} }
tables[tableName] = { tables[tableName] = {
_id: buildExternalTableId(datasourceId, tableName), _id: buildExternalTableId(datasourceId, tableName),
sourceId: datasourceId,
primary: primaryKeys, primary: primaryKeys,
name: tableName, name: tableName,
schema, schema,

View File

@ -383,7 +383,7 @@ class MongoIntegration implements IntegrationBase {
createObjectIds(json: any): object { createObjectIds(json: any): object {
const self = this const self = this
function interpolateObjectIds(json: any) { function interpolateObjectIds(json: any) {
for (let field of Object.keys(json)) { for (let field of Object.keys(json || {})) {
if (json[field] instanceof Object) { if (json[field] instanceof Object) {
json[field] = self.createObjectIds(json[field]) json[field] = self.createObjectIds(json[field])
} }

View File

@ -4,7 +4,7 @@ import {
QueryType, QueryType,
QueryJson, QueryJson,
SqlQuery, SqlQuery,
Table, ExternalTable,
TableSchema, TableSchema,
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
@ -91,7 +91,7 @@ const SCHEMA: Integration = {
}, },
} }
function bindingTypeCoerce(bindings: any[]) { export function bindingTypeCoerce(bindings: any[]) {
for (let i = 0; i < bindings.length; i++) { for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i] const binding = bindings[i]
if (typeof binding !== "string") { if (typeof binding !== "string") {
@ -109,7 +109,12 @@ function bindingTypeCoerce(bindings: any[]) {
dayjs(binding).isValid() && dayjs(binding).isValid() &&
!binding.includes(",") !binding.includes(",")
) { ) {
bindings[i] = dayjs(binding).toDate() let value: any
value = new Date(binding)
if (isNaN(value)) {
value = binding
}
bindings[i] = value
} }
} }
return bindings return bindings
@ -118,7 +123,7 @@ function bindingTypeCoerce(bindings: any[]) {
class MySQLIntegration extends Sql implements DatasourcePlus { class MySQLIntegration extends Sql implements DatasourcePlus {
private config: MySQLConfig private config: MySQLConfig
private client?: mysql.Connection private client?: mysql.Connection
public tables: Record<string, Table> = {} public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
constructor(config: MySQLConfig) { constructor(config: MySQLConfig) {
@ -215,8 +220,11 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
} }
} }
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(
const tables: { [key: string]: Table } = {} datasourceId: string,
entities: Record<string, ExternalTable>
) {
const tables: { [key: string]: ExternalTable } = {}
await this.connect() await this.connect()
try { try {
@ -254,6 +262,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
if (!tables[tableName]) { if (!tables[tableName]) {
tables[tableName] = { tables[tableName] = {
_id: buildExternalTableId(datasourceId, tableName), _id: buildExternalTableId(datasourceId, tableName),
sourceId: datasourceId,
primary: primaryKeys, primary: primaryKeys,
name: tableName, name: tableName,
schema, schema,

View File

@ -5,7 +5,7 @@ import {
QueryJson, QueryJson,
QueryType, QueryType,
SqlQuery, SqlQuery,
Table, ExternalTable,
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
@ -108,7 +108,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
private readonly config: OracleConfig private readonly config: OracleConfig
private index: number = 1 private index: number = 1
public tables: Record<string, Table> = {} public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
private readonly COLUMNS_SQL = ` private readonly COLUMNS_SQL = `
@ -262,13 +262,16 @@ class OracleIntegration extends Sql implements DatasourcePlus {
* @param {*} datasourceId - datasourceId to fetch * @param {*} datasourceId - datasourceId to fetch
* @param entities - the tables that are to be built * @param entities - the tables that are to be built
*/ */
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(
datasourceId: string,
entities: Record<string, ExternalTable>
) {
const columnsResponse = await this.internalQuery<OracleColumnsResponse>({ const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
sql: this.COLUMNS_SQL, sql: this.COLUMNS_SQL,
}) })
const oracleTables = this.mapColumns(columnsResponse) const oracleTables = this.mapColumns(columnsResponse)
const tables: { [key: string]: Table } = {} const tables: { [key: string]: ExternalTable } = {}
// iterate each table // iterate each table
Object.values(oracleTables).forEach(oracleTable => { Object.values(oracleTables).forEach(oracleTable => {
@ -279,6 +282,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
primary: [], primary: [],
name: oracleTable.name, name: oracleTable.name,
schema: {}, schema: {},
sourceId: datasourceId,
} }
tables[oracleTable.name] = table tables[oracleTable.name] = table
} }

View File

@ -4,7 +4,7 @@ import {
QueryType, QueryType,
QueryJson, QueryJson,
SqlQuery, SqlQuery,
Table, ExternalTable,
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
@ -124,7 +124,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
private readonly config: PostgresConfig private readonly config: PostgresConfig
private index: number = 1 private index: number = 1
private open: boolean private open: boolean
public tables: Record<string, Table> = {} public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {} public schemaErrors: Record<string, string> = {}
COLUMNS_SQL!: string COLUMNS_SQL!: string
@ -239,7 +239,10 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
* @param {*} datasourceId - datasourceId to fetch * @param {*} datasourceId - datasourceId to fetch
* @param entities - the tables that are to be built * @param entities - the tables that are to be built
*/ */
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(
datasourceId: string,
entities: Record<string, ExternalTable>
) {
let tableKeys: { [key: string]: string[] } = {} let tableKeys: { [key: string]: string[] } = {}
await this.openConnection() await this.openConnection()
try { try {
@ -265,7 +268,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
const columnsResponse: { rows: PostgresColumn[] } = const columnsResponse: { rows: PostgresColumn[] } =
await this.client.query(this.COLUMNS_SQL) await this.client.query(this.COLUMNS_SQL)
const tables: { [key: string]: Table } = {} const tables: { [key: string]: ExternalTable } = {}
for (let column of columnsResponse.rows) { for (let column of columnsResponse.rows) {
const tableName: string = column.table_name const tableName: string = column.table_name
@ -278,6 +281,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
primary: tableKeys[tableName] || [], primary: tableKeys[tableName] || [],
name: tableName, name: tableName,
schema: {}, schema: {},
sourceId: datasourceId,
} }
} }

View File

@ -1,4 +1,4 @@
import { default as MySQLIntegration } from "../mysql" import { default as MySQLIntegration, bindingTypeCoerce } from "../mysql"
jest.mock("mysql2") jest.mock("mysql2")
class TestConfiguration { class TestConfiguration {
@ -131,3 +131,21 @@ describe("MySQL Integration", () => {
}) })
}) })
}) })
describe("bindingTypeCoercion", () => {
it("shouldn't coerce something that looks like a date", () => {
const response = bindingTypeCoerce(["202205-1500"])
expect(response[0]).toBe("202205-1500")
})
it("should coerce an actual date", () => {
const date = new Date("2023-06-13T14:24:22.620Z")
const response = bindingTypeCoerce(["2023-06-13T14:24:22.620Z"])
expect(response[0]).toEqual(date)
})
it("should coerce numbers", () => {
const response = bindingTypeCoerce(["0"])
expect(response[0]).toEqual(0)
})
})

View File

@ -328,3 +328,27 @@ export function finaliseExternalTables(
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {}) .reduce((r, [k, v]) => ({ ...r, [k]: v }), {})
return { tables: finalTables, errors } return { tables: finalTables, errors }
} }
/**
* Checks if the provided input is an object, but specifically not a date type object.
* Used during coercion of types and relationship handling, dates are considered valid
* and can be used as a display field, but objects and arrays cannot.
* @param testValue an unknown type which this function will attempt to extract
* a valid primary display string from.
*/
export function getPrimaryDisplay(testValue: unknown): string | undefined {
if (testValue instanceof Date) {
return testValue.toISOString()
}
if (
Array.isArray(testValue) &&
testValue[0] &&
typeof testValue[0] !== "object"
) {
return testValue.join(", ")
}
if (typeof testValue === "object") {
return undefined
}
return testValue as string
}

View File

@ -36,11 +36,14 @@ export function checkDatasourceTypes(schema: Integration, config: any) {
async function enrichDatasourceWithValues(datasource: Datasource) { async function enrichDatasourceWithValues(datasource: Datasource) {
const cloned = cloneDeep(datasource) const cloned = cloneDeep(datasource)
const env = await getEnvironmentVariables() const env = await getEnvironmentVariables()
//Do not process entities, as we do not want to process formulas
const { entities, ...clonedWithoutEntities } = cloned
const processed = processObjectSync( const processed = processObjectSync(
cloned, clonedWithoutEntities,
{ env }, { env },
{ onlyFound: true } { onlyFound: true }
) as Datasource ) as Datasource
processed.entities = entities
const definition = await getDefinition(processed.source) const definition = await getDefinition(processed.source)
processed.config = checkDatasourceTypes(definition!, processed.config) processed.config = checkDatasourceTypes(definition!, processed.config)
return { return {

View File

@ -9,7 +9,7 @@ import {
import env from "../environment" import env from "../environment"
import { groups } from "@budibase/pro" import { groups } from "@budibase/pro"
import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types" import { UserCtx, ContextUser, User, UserGroup } from "@budibase/types"
import { global } from "yargs" import { cloneDeep } from "lodash"
export function updateAppRole( export function updateAppRole(
user: ContextUser, user: ContextUser,
@ -65,16 +65,20 @@ export async function processUser(
user: ContextUser, user: ContextUser,
opts: { appId?: string; groups?: UserGroup[] } = {} opts: { appId?: string; groups?: UserGroup[] } = {}
) { ) {
if (user) { let clonedUser = cloneDeep(user)
delete user.password if (clonedUser) {
delete clonedUser.password
} }
const appId = opts.appId || context.getAppId() const appId = opts.appId || context.getAppId()
user = updateAppRole(user, { appId }) clonedUser = updateAppRole(clonedUser, { appId })
if (!user.roleId && user?.userGroups?.length) { if (!clonedUser.roleId && clonedUser?.userGroups?.length) {
user = await checkGroupRoles(user, { appId, groups: opts?.groups }) clonedUser = await checkGroupRoles(clonedUser, {
appId,
groups: opts?.groups,
})
} }
return user return clonedUser
} }
export async function getCachedSelf(ctx: UserCtx, appId: string) { export async function getCachedSelf(ctx: UserCtx, appId: string) {

View File

@ -82,6 +82,10 @@ export interface Table extends Document {
rowHeight?: number rowHeight?: number
} }
export interface ExternalTable extends Table {
sourceId: string
}
export interface TableRequest extends Table { export interface TableRequest extends Table {
_rename?: RenameColumn _rename?: RenameColumn
created?: boolean created?: boolean

View File

@ -2489,6 +2489,11 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@fontsource/source-sans-pro@^5.0.3":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@fontsource/source-sans-pro/-/source-sans-pro-5.0.3.tgz#7d6e84a8169ba12fa5e6ce70757aa2ca7e74d855"
integrity sha512-mQnjuif/37VxwRloHZ+wQdoozd2VPWutbFSt1AuSkk7nFXIBQxHJLw80rgCF/osL0t7N/3Gx1V7UJuOX2zxzhQ==
"@fortawesome/fontawesome-common-types@6.3.0": "@fortawesome/fontawesome-common-types@6.3.0":
version "6.3.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz#51f734e64511dbc3674cd347044d02f4dd26e86b" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.3.0.tgz#51f734e64511dbc3674cd347044d02f4dd26e86b"
@ -8408,7 +8413,7 @@ chmodr@1.2.0:
resolved "https://registry.yarnpkg.com/chmodr/-/chmodr-1.2.0.tgz#720e96caa09b7f1cdbb01529b7d0ab6bc5e118b9" resolved "https://registry.yarnpkg.com/chmodr/-/chmodr-1.2.0.tgz#720e96caa09b7f1cdbb01529b7d0ab6bc5e118b9"
integrity sha512-Y5uI7Iq/Az6HgJEL6pdw7THVd7jbVOTPwsmcPOBjQL8e3N+pz872kzK5QxYGEy21iRys+iHWV0UZQXDFJo1hyA== integrity sha512-Y5uI7Iq/Az6HgJEL6pdw7THVd7jbVOTPwsmcPOBjQL8e3N+pz872kzK5QxYGEy21iRys+iHWV0UZQXDFJo1hyA==
chokidar@3.5.3, chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.2: chokidar@3.5.3, chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.2, chokidar@^3.5.3:
version "3.5.3" version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -11852,7 +11857,7 @@ fast-glob@3.2.7:
merge2 "^1.3.0" merge2 "^1.3.0"
micromatch "^4.0.4" micromatch "^4.0.4"
fast-glob@^3.0.3: fast-glob@^3.0.3, fast-glob@^3.2.11:
version "3.2.12" version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@ -25402,6 +25407,16 @@ vite-node@0.29.8:
picocolors "^1.0.0" picocolors "^1.0.0"
vite "^3.0.0 || ^4.0.0" vite "^3.0.0 || ^4.0.0"
vite-plugin-static-copy@^0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/vite-plugin-static-copy/-/vite-plugin-static-copy-0.16.0.tgz#2f65227037f17fc99c0782fd0b344e962935e69e"
integrity sha512-dMVEg5Z2SwYRgQnHZaeokvSKB4p/TOTf65JU4sP3U6ccSBsukqdtDOjpmT+xzTFHAA8WJjcS31RMLjUdWQCBzw==
dependencies:
chokidar "^3.5.3"
fast-glob "^3.2.11"
fs-extra "^11.1.0"
picocolors "^1.0.0"
"vite@^3.0.0 || ^4.0.0": "vite@^3.0.0 || ^4.0.0":
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.2.tgz#014c30e5163844f6e96d7fe18fbb702236516dc6" resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.2.tgz#014c30e5163844f6e96d7fe18fbb702236516dc6"