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
steps:
# - 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
- name: Fail if not a tag
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.production.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
wc -l values.production.yaml
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: Get the latest budibase release version
id: version
@ -48,29 +39,10 @@ jobs:
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
- uses: passeidireto/trigger-external-workflow-action@main
env:
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
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: 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 }}
repository: budibase/budibase-deploys
event: budicloud-prod-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

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"
exit 1
fi
- name: Get the latest budibase release version
id: version
run: |
release_version=$(cat lerna.json | jq -r '.version')
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
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"
]
- uses: passeidireto/trigger-external-workflow-action@main
env:
KUBECONFIG_FILE: '${{ secrets.PREPROD_KUBECONFIG }}'
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v4.0.0
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
with:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod."
embed-title: ${{ env.RELEASE_VERSION }}
repository: budibase/budibase-deploys
event: budicloud-preprod-deploy
github_pat: ${{ secrets.GH_ACCESS_TOKEN }}

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

View File

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

View File

@ -1,5 +1,5 @@
<script>
import { tables } from "stores/backend"
import { datasources, tables } from "stores/backend"
import EditRolesButton from "./buttons/EditRolesButton.svelte"
import { TableNames } from "constants"
import { Grid } from "@budibase/frontend-core"
@ -26,6 +26,18 @@
$: id = $tables.selected?._id
$: isUsersTable = id === TableNames.USERS
$: 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>
<div class="wrapper">
@ -37,7 +49,7 @@
allowDeleteRows={!isUsersTable}
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
showAvatars={false}
on:updatetable={e => tables.replaceTable(id, e.detail)}
on:updatetable={handleGridTableUpdate}
>
<svelte:fragment slot="controls">
{#if isInternal}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,10 +73,6 @@
if (highlighted) {
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>

View File

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

View File

@ -8,7 +8,7 @@
$: platformTitle =
!$auth.user && platformTitleText ? platformTitleText : "Budibase"
$: faviconUrl = $organisation.faviconUrl || "https://i.imgur.com/Xhdt1YP.png"
$: faviconUrl = $organisation.faviconUrl || "/builder/bblogo.png"
onMount(async () => {
await organisation.init()
@ -27,6 +27,6 @@
<link rel="icon" href={faviconUrl} />
{:else}
<!-- 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}
</svelte:head>

View File

@ -44,7 +44,7 @@
<div tabindex="-1" class="exampleApp">
<div class="page">
<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>
</div>
<div class="nav">Home</div>

View File

@ -113,6 +113,10 @@ export function createDatasourcesStore() {
...state,
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

View File

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

View File

@ -1,6 +1,7 @@
import { svelte } from "@sveltejs/vite-plugin-svelte"
import replace from "@rollup/plugin-replace"
import { defineConfig, loadEnv } from "vite"
import { viteStaticCopy } from "vite-plugin-static-copy"
import path from "path"
const ignoredWarnings = [
@ -59,6 +60,18 @@ export default defineConfig(({ mode }) => {
),
"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: {
exclude: ["@roxi/routify"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { writable, derived, get } from "svelte/store"
import { fetchData } from "../../../fetch/fetchData"
import { notifications } from "@budibase/bbui"
import { NewRowID, RowPageSize } from "../lib/constants"
import { tick } from "svelte"
const initialSortState = {
column: null,
@ -124,13 +125,22 @@ export const deriveStores = context => {
})
// Subscribe to changes of this fetch model
unsubscribe = newFetch.subscribe($fetch => {
unsubscribe = newFetch.subscribe(async $fetch => {
if ($fetch.loaded && !$fetch.loading) {
hasNextPage.set($fetch.hasNextPage)
const $instanceLoaded = get(instanceLoaded)
const resetRows = $fetch.resetKey !== lastResetKey
const previousResetKey = lastResetKey
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
if (!$instanceLoaded || resetRows) {
table.set($fetch.definition)

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

View File

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

View File

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

View File

@ -3,10 +3,10 @@ import * as userController from "../user"
import { FieldTypes } from "../../../constants"
import { context } from "@budibase/backend-core"
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 { UserCtx } from "@budibase/types"
import sdk from "../../../sdk"
const validateJs = require("validate.js")
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) {
const datasourceId = json.endpoint.datasourceId
const datasource = await sdk.datasources.get(datasourceId)
@ -65,6 +72,10 @@ export async function validate({
const column = fetchedTable.schema[fieldName]
const constraints = cloneDeep(column.constraints)
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
if (type === FieldTypes.FORMULA || column.autocolumn) {
continue

View File

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

View File

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

View File

@ -26,6 +26,7 @@ import {
RelationshipTypes,
} from "@budibase/types"
import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets"
const { cloneDeep } = require("lodash/fp")
async function makeTableRequest(
@ -318,6 +319,11 @@ export async function save(ctx: UserCtx) {
datasource.entities[tableToSave.name] = tableToSave
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
}
@ -344,6 +350,11 @@ export async function destroy(ctx: UserCtx) {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import {
QueryType,
QueryJson,
SqlQuery,
Table,
ExternalTable,
DatasourcePlus,
DatasourceFeature,
ConnectionInfo,
@ -124,7 +124,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
private readonly config: PostgresConfig
private index: number = 1
private open: boolean
public tables: Record<string, Table> = {}
public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {}
COLUMNS_SQL!: string
@ -239,7 +239,10 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
* @param {*} datasourceId - datasourceId to fetch
* @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[] } = {}
await this.openConnection()
try {
@ -265,7 +268,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
const columnsResponse: { rows: PostgresColumn[] } =
await this.client.query(this.COLUMNS_SQL)
const tables: { [key: string]: Table } = {}
const tables: { [key: string]: ExternalTable } = {}
for (let column of columnsResponse.rows) {
const tableName: string = column.table_name
@ -278,6 +281,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
primary: tableKeys[tableName] || [],
name: tableName,
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")
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 }), {})
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) {
const cloned = cloneDeep(datasource)
const env = await getEnvironmentVariables()
//Do not process entities, as we do not want to process formulas
const { entities, ...clonedWithoutEntities } = cloned
const processed = processObjectSync(
cloned,
clonedWithoutEntities,
{ env },
{ onlyFound: true }
) as Datasource
processed.entities = entities
const definition = await getDefinition(processed.source)
processed.config = checkDatasourceTypes(definition!, processed.config)
return {

View File

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

View File

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

View File

@ -2489,6 +2489,11 @@
minimatch "^3.0.4"
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":
version "6.3.0"
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"
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"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@ -11852,7 +11857,7 @@ fast-glob@3.2.7:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.0.3:
fast-glob@^3.0.3, fast-glob@^3.2.11:
version "3.2.12"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
@ -25402,6 +25407,16 @@ vite-node@0.29.8:
picocolors "^1.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":
version "4.2.2"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.2.2.tgz#014c30e5163844f6e96d7fe18fbb702236516dc6"