Merge branch 'develop' into grid-clipboard

This commit is contained in:
Martin McKeaveney 2023-10-04 11:57:45 +01:00 committed by GitHub
commit d86c251699
40 changed files with 616 additions and 379 deletions

View File

@ -14,7 +14,7 @@ jobs:
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
PAYLOAD_BRANCH: ${{ github.head_ref }} PAYLOAD_BRANCH: ${{ github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.ref }} PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: featurebranch-qa-close event: featurebranch-qa-close

View File

@ -13,7 +13,7 @@ jobs:
- uses: passeidireto/trigger-external-workflow-action@main - uses: passeidireto/trigger-external-workflow-action@main
env: env:
PAYLOAD_BRANCH: ${{ github.head_ref }} PAYLOAD_BRANCH: ${{ github.head_ref }}
PAYLOAD_PR_NUMBER: ${{ github.ref }} PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
with: with:
repository: budibase/budibase-deploys repository: budibase/budibase-deploys
event: featurebranch-qa-deploy event: featurebranch-qa-deploy

View File

@ -1,5 +1,5 @@
{ {
"version": "2.10.16-alpha.11", "version": "2.11.5-alpha.3",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -6,6 +6,7 @@ import {
AutomationStepIdArray, AutomationStepIdArray,
AutomationIOType, AutomationIOType,
AutomationCustomIOType, AutomationCustomIOType,
DatasourceFeature,
} from "@budibase/types" } from "@budibase/types"
import joi from "joi" import joi from "joi"
@ -67,9 +68,27 @@ function validateDatasource(schema: any) {
version: joi.string().optional(), version: joi.string().optional(),
schema: joi.object({ schema: joi.object({
docs: joi.string(), docs: joi.string(),
plus: joi.boolean().optional(),
isSQL: joi.boolean().optional(),
auth: joi
.object({
type: joi.string().required(),
})
.optional(),
features: joi
.object(
Object.fromEntries(
Object.values(DatasourceFeature).map(key => [
key,
joi.boolean().optional(),
])
)
)
.optional(),
relationships: joi.boolean().optional(),
description: joi.string().required(),
friendlyName: joi.string().required(), friendlyName: joi.string().required(),
type: joi.string().allow(...DATASOURCE_TYPES), type: joi.string().allow(...DATASOURCE_TYPES),
description: joi.string().required(),
datasource: joi.object().pattern(joi.string(), fieldValidator).required(), datasource: joi.object().pattern(joi.string(), fieldValidator).required(),
query: joi query: joi
.object() .object()

View File

@ -221,18 +221,6 @@ const automationActions = store => ({
newAutomation.definition.steps.splice(blockIdx, 0, block) newAutomation.definition.steps.splice(blockIdx, 0, block)
await store.actions.save(newAutomation) await store.actions.save(newAutomation)
}, },
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
toggleRowControl: async (block, rowControl) => {
const newBlock = { ...block, rowControl }
const newAutomation = store.actions.getUpdatedDefinition(
get(selectedAutomation),
newBlock
)
await store.actions.save(newAutomation)
},
deleteAutomationBlock: async block => { deleteAutomationBlock: async block => {
const automation = get(selectedAutomation) const automation = get(selectedAutomation)
let newAutomation = cloneDeep(automation) let newAutomation = cloneDeep(automation)

View File

@ -7,7 +7,6 @@
Detail, Detail,
Modal, Modal,
Button, Button,
Select,
ActionButton, ActionButton,
notifications, notifications,
Label, Label,
@ -39,9 +38,6 @@
step => step.stepId === ActionStepID.COLLECT step => step.stepId === ActionStepID.COLLECT
) )
$: automationId = $selectedAutomation?._id $: automationId = $selectedAutomation?._id
$: showBindingPicker =
block.stepId === ActionStepID.CREATE_ROW ||
block.stepId === ActionStepID.UPDATE_ROW
$: isTrigger = block.type === "TRIGGER" $: isTrigger = block.type === "TRIGGER"
$: steps = $selectedAutomation?.definition?.steps ?? [] $: steps = $selectedAutomation?.definition?.steps ?? []
$: blockIdx = steps.findIndex(step => step.id === block.id) $: blockIdx = steps.findIndex(step => step.id === block.id)
@ -96,15 +92,6 @@
} }
} }
/**
* "rowControl" appears to be the name of the flag used to determine whether
* a certain automation block uses values or bindings as inputs
*/
function toggleRowControl(evt) {
const rowControl = evt.detail !== "Use values"
automationStore.actions.toggleRowControl(block, rowControl)
}
async function addLooping() { async function addLooping() {
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
const loopBlock = automationStore.actions.constructBlock( const loopBlock = automationStore.actions.constructBlock(
@ -189,16 +176,6 @@
Add Looping Add Looping
</ActionButton> </ActionButton>
{/if} {/if}
{#if showBindingPicker}
<Select
on:change={toggleRowControl}
defaultValue="Use values"
autoWidth
value={block.rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]}
placeholder={null}
/>
{/if}
<ActionButton <ActionButton
on:click={() => deleteStep()} on:click={() => deleteStep()}
icon="DeleteOutline" icon="DeleteOutline"

View File

@ -23,6 +23,7 @@
import { environment, licensing } from "stores/portal" import { environment, licensing } from "stores/portal"
import WebhookDisplay from "../Shared/WebhookDisplay.svelte" import WebhookDisplay from "../Shared/WebhookDisplay.svelte"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte" import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte" import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import CodeEditorModal from "./CodeEditorModal.svelte" import CodeEditorModal from "./CodeEditorModal.svelte"
import QuerySelector from "./QuerySelector.svelte" import QuerySelector from "./QuerySelector.svelte"
@ -82,33 +83,6 @@
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])] ? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
: [] : []
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
let deprecatedSchemaProperties
$: {
if (block?.stepId === "integromat" || block?.stepId === "zapier") {
deprecatedSchemaProperties = schemaProperties.filter(
prop => !prop[0].startsWith("value")
)
if (!deprecatedSchemaProperties.map(entry => entry[0]).includes("body")) {
deprecatedSchemaProperties.push([
"body",
{
title: "Payload",
type: "json",
},
])
}
} else {
deprecatedSchemaProperties = schemaProperties
}
}
/****************************************************/
const getInputData = (testData, blockInputs) => { const getInputData = (testData, blockInputs) => {
// Test data is not cloned for reactivity // Test data is not cloned for reactivity
let newInputData = testData || cloneDeep(blockInputs) let newInputData = testData || cloneDeep(blockInputs)
@ -118,30 +92,6 @@
newInputData = cloneDeep(blockInputs) newInputData = cloneDeep(blockInputs)
} }
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
if (
(block?.stepId === "integromat" || block?.stepId === "zapier") &&
!newInputData?.body?.value
) {
let deprecatedValues = {
...newInputData,
}
delete deprecatedValues.url
delete deprecatedValues.body
newInputData = {
url: newInputData.url,
body: {
value: JSON.stringify(deprecatedValues),
},
}
}
/**********************************/
inputData = newInputData inputData = newInputData
setDefaultEnumValues() setDefaultEnumValues()
} }
@ -337,7 +287,7 @@
</script> </script>
<div class="fields"> <div class="fields">
{#each deprecatedSchemaProperties as [key, value]} {#each schemaProperties as [key, value]}
{#if canShowField(key, value)} {#if canShowField(key, value)}
<div class="block-field"> <div class="block-field">
{#if key !== "fields" && value.type !== "boolean"} {#if key !== "fields" && value.type !== "boolean"}
@ -362,18 +312,6 @@
mode="json" mode="json"
value={inputData[key]?.value} value={inputData[key]?.value}
on:change={e => { on:change={e => {
/**
* TODO - Remove after November 2023
* *******************************
* Code added to provide backwards compatibility between Values 1,2,3,4,5
* and the new JSON body.
*/
delete inputData.value1
delete inputData.value2
delete inputData.value3
delete inputData.value4
delete inputData.value5
/***********************/
onChange(e, key) onChange(e, key)
}} }}
/> />
@ -386,10 +324,23 @@
/> />
</div> </div>
{:else if value.type === "date"} {:else if value.type === "date"}
<DatePicker <DrawerBindableSlot
fillWidth
title={value.title}
panel={AutomationBindingPanel}
type={"date"}
value={inputData[key]} value={inputData[key]}
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
/> {bindings}
allowJS={true}
updateOnChange={false}
drawerLeft="260px"
>
<DatePicker
value={inputData[key]}
on:change={e => onChange(e, key)}
/>
</DrawerBindableSlot>
{:else if value.customType === "column"} {:else if value.customType === "column"}
<Select <Select
on:change={e => onChange(e, key)} on:change={e => onChange(e, key)}
@ -469,7 +420,6 @@
/> />
{:else if value.customType === "row"} {:else if value.customType === "row"}
<RowSelector <RowSelector
{block}
value={inputData[key]} value={inputData[key]}
meta={inputData["meta"] || {}} meta={inputData["meta"] || {}}
on:change={e => { on:change={e => {

View File

@ -1,18 +1,16 @@
<script> <script>
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Select, Checkbox } from "@budibase/bbui" import { Select, Checkbox } from "@budibase/bbui"
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import RowSelectorTypes from "./RowSelectorTypes.svelte" import RowSelectorTypes from "./RowSelectorTypes.svelte"
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte" import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value
export let meta export let meta
export let bindings export let bindings
export let block
export let isTestModal export let isTestModal
export let isUpdateRow export let isUpdateRow
@ -25,16 +23,6 @@
let table let table
let schemaFields let schemaFields
let placeholders = {
number: 10,
boolean: "true",
datetime: "2022-02-16T12:00:00.000Z ",
options: "1",
array: "1 2 3 4",
link: "ro_ta_123_456",
longform: "long form text",
}
$: rowControl = block.rowControl
$: { $: {
table = $tables.list.find(table => table._id === value?.tableId) table = $tables.list.find(table => table._id === value?.tableId)
schemaFields = Object.entries(table?.schema ?? {}) schemaFields = Object.entries(table?.schema ?? {})
@ -57,19 +45,13 @@
return value return value
} }
if (type === "boolean") {
if (typeof value === "boolean") {
return value
}
return value === "true"
}
if (type === "number") { if (type === "number") {
if (typeof value === "number") { if (typeof value === "number") {
return value return value
} }
return Number(value) return Number(value)
} }
if (type === "options") { if (type === "options" || type === "boolean") {
return value return value
} }
if (type === "array") { if (type === "array") {
@ -127,47 +109,40 @@
{#if schemaFields.length} {#if schemaFields.length}
<div class="schema-fields"> <div class="schema-fields">
{#each schemaFields as [field, schema]} {#each schemaFields as [field, schema]}
{#if !schema.autocolumn} {#if !schema.autocolumn && schema.type !== "attachment"}
{#if schema.type !== "attachment"} <DrawerBindableSlot
{#if !rowControl} fillWidth
<RowSelectorTypes title={value.title}
{isTestModal} label={field}
{field} panel={AutomationBindingPanel}
{schema} type={schema.type}
bindings={parsedBindings} {schema}
{value} value={value[field]}
{onChange} on:change={e => onChange(e, field)}
/> {bindings}
{:else} allowJS={true}
<div> updateOnChange={false}
<svelte:component drawerLeft="260px"
this={isTestModal ? ModalBindableInput : DrawerBindableInput} >
placeholder={placeholders[schema.type]} <RowSelectorTypes
panel={AutomationBindingPanel} {isTestModal}
value={Array.isArray(value[field]) {field}
? value[field].join(",") {schema}
: value[field]} bindings={parsedBindings}
on:change={e => onChange(e, field, schema.type)} {value}
label={field} {onChange}
type="string" />
bindings={parsedBindings} </DrawerBindableSlot>
fillWidth={true} {/if}
allowJS={true} {#if isUpdateRow && schema.type === "link"}
updateOnChange={false} <div class="checkbox-field">
/> <Checkbox
{#if isUpdateRow && schema.type === "link"} value={meta.fields?.[field]?.clearRelationships}
<div class="checkbox-field"> text={"Clear relationships if empty?"}
<Checkbox size={"S"}
value={meta.fields?.[field]?.clearRelationships} on:change={e => onChangeSetting(e, field)}
text={"Clear relationships if empty?"} />
size={"S"} </div>
on:change={e => onChangeSetting(e, field)}
/>
</div>
{/if}
</div>
{/if}
{/if}
{/if} {/if}
{/each} {/each}
</div> </div>

View File

@ -1,7 +1,6 @@
<script> <script>
import { import {
Select, Select,
Toggle,
DatePicker, DatePicker,
Multiselect, Multiselect,
TextArea, TextArea,
@ -45,10 +44,14 @@
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
/> />
{:else if schema.type === "boolean"} {:else if schema.type === "boolean"}
<Toggle <Select
text={field}
value={value[field]}
on:change={e => onChange(e, field)} on:change={e => onChange(e, field)}
label={field}
value={value[field]}
options={[
{ label: "True", value: "true" },
{ label: "False", value: "false" },
]}
/> />
{:else if schema.type === "array"} {:else if schema.type === "array"}
<Multiselect <Multiselect

View File

@ -85,7 +85,7 @@
} }
let relationshipMap = { let relationshipMap = {
[RelationshipType.MANY_TO_ONE]: { [RelationshipType.ONE_TO_MANY]: {
part1: PrettyRelationshipDefinitions.MANY, part1: PrettyRelationshipDefinitions.MANY,
part2: PrettyRelationshipDefinitions.ONE, part2: PrettyRelationshipDefinitions.ONE,
}, },
@ -93,7 +93,7 @@
part1: PrettyRelationshipDefinitions.MANY, part1: PrettyRelationshipDefinitions.MANY,
part2: PrettyRelationshipDefinitions.MANY, part2: PrettyRelationshipDefinitions.MANY,
}, },
[RelationshipType.ONE_TO_MANY]: { [RelationshipType.MANY_TO_ONE]: {
part1: PrettyRelationshipDefinitions.ONE, part1: PrettyRelationshipDefinitions.ONE,
part2: PrettyRelationshipDefinitions.MANY, part2: PrettyRelationshipDefinitions.MANY,
}, },
@ -277,10 +277,7 @@
dispatch("updatecolumns") dispatch("updatecolumns")
gridDispatch("close-edit-column") gridDispatch("close-edit-column")
if ( if (saveColumn.type === LINK_TYPE) {
saveColumn.type === LINK_TYPE &&
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
) {
// Fetching the new tables // Fetching the new tables
tables.fetch() tables.fetch()
// Fetching the new relationships // Fetching the new relationships
@ -312,6 +309,11 @@
confirmDeleteDialog.hide() confirmDeleteDialog.hide()
dispatch("updatecolumns") dispatch("updatecolumns")
gridDispatch("close-edit-column") gridDispatch("close-edit-column")
if (editableColumn.type === LINK_TYPE) {
// Updating the relationships
datasources.fetch()
}
} }
} catch (error) { } catch (error) {
notifications.error(`Error deleting column: ${error.message}`) notifications.error(`Error deleting column: ${error.message}`)

View File

@ -57,7 +57,8 @@
label: table.name, label: table.name,
value: table._id, value: table._id,
})) }))
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet() $: valid =
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE $: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
@ -114,7 +115,7 @@
return Object.entries(errors).filter(entry => !!entry[1]).length return Object.entries(errors).filter(entry => !!entry[1]).length
} }
function allRequiredAttributesSet() { function allRequiredAttributesSet(relationshipType) {
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
if (relationshipType === RelationshipType.MANY_TO_ONE) { if (relationshipType === RelationshipType.MANY_TO_ONE) {
return base && fromPrimary && fromForeign return base && fromPrimary && fromForeign
@ -124,9 +125,10 @@
} }
function validate() { function validate() {
if (!allRequiredAttributesSet() && !hasValidated) { if (!allRequiredAttributesSet(relationshipType) && !hasValidated) {
return return
} }
hasValidated = true hasValidated = true
errorChecker.setType(relationshipType) errorChecker.setType(relationshipType)
const fromTable = getTable(fromId), const fromTable = getTable(fromId),

View File

@ -11,11 +11,11 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let rows = [] let rows = []
let linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map( let linkedIds = []
$: linkedIds = (Array.isArray(linkedRows) ? linkedRows : [])?.map(
row => row?._id || row row => row?._id || row
) )
$: linkedRows = linkedIds
$: label = capitalise(schema.name) $: label = capitalise(schema.name)
$: linkedTableId = schema.tableId $: linkedTableId = schema.tableId
$: linkedTable = $tables.list.find(table => table._id === linkedTableId) $: linkedTable = $tables.list.find(table => table._id === linkedTableId)

View File

@ -0,0 +1,250 @@
<script>
import { Icon, Input, Drawer, Button } from "@budibase/bbui"
import {
readableToRuntimeBinding,
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
import { createEventDispatcher, setContext } from "svelte"
import { isJSBinding } from "@budibase/string-templates"
export let panel = ClientBindingPanel
export let value = ""
export let bindings = []
export let title = "Bindings"
export let placeholder
export let label
export let disabled = false
export let fillWidth
export let allowJS = true
export let allowHelpers = true
export let updateOnChange = true
export let drawerLeft
export let type
export let schema
const dispatch = createEventDispatcher()
let bindingDrawer
let valid = true
let currentVal = value
$: readableValue = runtimeToReadableBinding(bindings, value)
$: tempValue = readableValue
$: isJS = isJSBinding(value)
const saveBinding = () => {
onChange(tempValue)
onBlur()
bindingDrawer.hide()
}
setContext("binding-drawer-actions", {
save: saveBinding,
})
const onChange = value => {
if (type === "link" && value && hasValidLinks(value)) {
currentVal = value.split(",")
} else if (type === "array" && value && hasValidOptions(value)) {
currentVal = value.split(",")
} else {
currentVal = readableToRuntimeBinding(bindings, value)
}
dispatch("change", currentVal)
}
const onBlur = () => {
dispatch("blur", currentVal)
}
const isValidDate = value => {
return !value || !isNaN(new Date(value).valueOf())
}
const hasValidLinks = value => {
let links = []
if (Array.isArray(value)) {
links = value
} else if (value && typeof value === "string") {
links = value.split(",")
} else {
return !value
}
return links.every(link => link.startsWith("ro_"))
}
const hasValidOptions = value => {
let links = []
if (Array.isArray(value)) {
links = value
} else if (value && typeof value === "string") {
links = value.split(",")
} else {
return !value
}
return links.every(link => schema?.constraints?.inclusion?.includes(link))
}
const isValidBoolean = value => {
return value === "false" || value === "true" || value == ""
}
const validationMap = {
date: isValidDate,
datetime: isValidDate,
link: hasValidLinks,
array: hasValidOptions,
longform: value => !isJSBinding(value),
json: value => !isJSBinding(value),
boolean: isValidBoolean,
}
const isValid = value => {
const validate = validationMap[type]
return validate ? validate(value) : true
}
const getIconClass = (value, type) => {
if (type === "longform" && !isJSBinding(value)) {
return "text-area-slot-icon"
}
if (type === "json" && !isJSBinding(value)) {
return "json-slot-icon"
}
if (type !== "string" && type !== "number") {
return "slot-icon"
}
return ""
}
</script>
<div class="control" class:disabled>
{#if !isValid(value)}
<Input
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
on:change={event => onChange(event.detail)}
on:blur={onBlur}
{placeholder}
{updateOnChange}
/>
<div
class="icon"
on:click={() => {
if (!isJS) {
dispatch("change", "")
}
}}
>
<Icon disabled={isJS} size="S" name="Close" />
</div>
{:else}
<slot
{label}
{disabled}
readonly={isJS}
value={isJS ? "(JavaScript function)" : readableValue}
{placeholder}
{updateOnChange}
/>
{/if}
{#if !disabled && type !== "formula"}
<div
class={`icon ${getIconClass(value, type)}`}
on:click={() => {
bindingDrawer.show()
}}
>
<Icon size="S" name="FlashOn" />
</div>
{/if}
</div>
<Drawer
on:drawerHide
on:drawerShow
{fillWidth}
bind:this={bindingDrawer}
{title}
left={drawerLeft}
headless
>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={saveBinding}>
Save
</Button>
<svelte:component
this={panel}
slot="body"
bind:valid
value={readableValue}
on:change={event => (tempValue = event.detail)}
{bindings}
{allowJS}
{allowHelpers}
/>
</Drawer>
<style>
.control {
flex: 1;
position: relative;
}
.slot-icon {
right: 31px !important;
border-right: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
.text-area-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important;
top: 26px !important;
}
.json-slot-icon {
border-bottom: 1px solid var(--spectrum-alias-border-color);
border-bottom-right-radius: 0px !important;
top: 23px !important;
right: 0px !important;
}
.icon {
right: 1px;
bottom: 1px;
position: absolute;
justify-content: center;
align-items: center;
display: flex;
flex-direction: row;
box-sizing: border-box;
border-left: 1px solid var(--spectrum-alias-border-color);
border-top-right-radius: var(--spectrum-alias-border-radius-regular);
border-bottom-right-radius: var(--spectrum-alias-border-radius-regular);
width: 31px;
color: var(--spectrum-alias-text-color);
background-color: var(--spectrum-global-color-gray-75);
transition: background-color
var(--spectrum-global-animation-duration-100, 130ms),
box-shadow var(--spectrum-global-animation-duration-100, 130ms),
border-color var(--spectrum-global-animation-duration-100, 130ms);
height: calc(var(--spectrum-alias-item-height-m) - 2px);
}
.icon:hover {
cursor: pointer;
color: var(--spectrum-alias-text-color-hover);
background-color: var(--spectrum-global-color-gray-50);
border-color: var(--spectrum-alias-border-color-hover);
}
.control:not(.disabled) :global(.spectrum-Textfield-input) {
padding-right: 40px;
}
</style>

View File

@ -20,7 +20,9 @@
const getSortableFields = schema => { const getSortableFields = schema => {
return Object.entries(schema || {}) return Object.entries(schema || {})
.filter(entry => !UNSORTABLE_TYPES.includes(entry[1].type)) .filter(
entry => !UNSORTABLE_TYPES.includes(entry[1].type) && entry[1].sortable
)
.map(entry => entry[0]) .map(entry => entry[0])
} }

View File

@ -62,7 +62,14 @@
</div> </div>
{/if} {/if}
<div class="truncate"> <div class="truncate">
<Body>{getSubtitle(datasource)}</Body> <Body>
{@const subtitle = getSubtitle(datasource)}
{#if subtitle}
{subtitle}
{:else}
{Object.values(datasource.config).join(" / ")}
{/if}
</Body>
</div> </div>
</div> </div>
<div class="right"> <div class="right">

View File

@ -21,15 +21,22 @@
function getRelationships(tables) { function getRelationships(tables) {
const relatedColumns = {} const relatedColumns = {}
tables.forEach(({ name: tableName, schema }) => { tables.forEach(({ name: tableName, schema, _id: tableId }) => {
Object.values(schema).forEach(column => { Object.values(schema).forEach(column => {
if (column.type !== "link") return if (column.type !== "link") return
relatedColumns[column._id] ??= {} const columnId =
relatedColumns[column._id].through = column.through ||
relatedColumns[column._id].through || column.through column._id ||
(column.main
? `${tableId}_${column.fieldName}__${column.tableId}_${column.foreignKey}`
: `${column.tableId}_${column.foreignKey}__${tableId}_${column.fieldName}`)
relatedColumns[column._id][column.main ? "from" : "to"] = { relatedColumns[columnId] ??= {}
relatedColumns[columnId].through =
relatedColumns[columnId].through || column.through
relatedColumns[columnId][column.main ? "from" : "to"] = {
...column, ...column,
tableName, tableName,
} }

View File

@ -136,6 +136,7 @@ export function createDatasourcesStore() {
config, config,
name: `${integration.friendlyName}${nameModifier}`, name: `${integration.friendlyName}${nameModifier}`,
plus: integration.plus && integration.name !== IntegrationTypes.REST, plus: integration.plus && integration.name !== IntegrationTypes.REST,
isSQL: integration.isSQL,
} }
if (await checkDatasourceValidity(integration, datasource)) { if (await checkDatasourceValidity(integration, datasource)) {

View File

@ -3,6 +3,7 @@
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { Heading, Icon, clickOutside } from "@budibase/bbui" import { Heading, Icon, clickOutside } from "@budibase/bbui"
import { FieldTypes } from "constants" import { FieldTypes } from "constants"
import { Constants } from "@budibase/frontend-core"
import active from "svelte-spa-router/active" import active from "svelte-spa-router/active"
const sdk = getContext("sdk") const sdk = getContext("sdk")
@ -103,7 +104,8 @@
let validLinks = (allLinks || []).filter(link => link.text && link.url) let validLinks = (allLinks || []).filter(link => link.text && link.url)
// Filter to only links allowed by the current role // Filter to only links allowed by the current role
return validLinks.filter(link => { return validLinks.filter(link => {
return userRoleHierarchy?.find(roleId => roleId === link.roleId) const role = link.roleId || Constants.Roles.BASIC
return userRoleHierarchy?.find(roleId => roleId === role)
}) })
} }

View File

@ -60,6 +60,12 @@
// even if they are not in the inital fetch results // even if they are not in the inital fetch results
initialValuesProcessed = true initialValuesProcessed = true
optionsObj = (fieldState?.value || []).reduce((accumulator, value) => { optionsObj = (fieldState?.value || []).reduce((accumulator, value) => {
// fieldState has to be an array of strings to be valid for an update
// therefore we cannot guarantee value will be an object
// https://linear.app/budibase/issue/BUDI-7577/refactor-the-relationshipfield-component-to-have-better-support-for
if (!value._id) {
return accumulator
}
accumulator[value._id] = { accumulator[value._id] = {
_id: value._id, _id: value._id,
[primaryDisplay]: value.primaryDisplay, [primaryDisplay]: value.primaryDisplay,

View File

@ -25,7 +25,6 @@
value: roleId, value: roleId,
}) })
} }
devToolsStore.actions.changeRole(SELF_ROLE)
return list return list
} }

View File

@ -2,6 +2,7 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
import { initialise } from "./initialise" import { initialise } from "./initialise"
import { authStore } from "./auth" import { authStore } from "./auth"
import { API } from "../api" import { API } from "../api"
import { get } from "svelte/store"
const initialState = { const initialState = {
visible: false, visible: false,
@ -27,9 +28,15 @@ const createDevToolStore = () => {
} }
const changeRole = async role => { const changeRole = async role => {
if (role === "self") {
role = null
}
if (role === get(store).role) {
return
}
store.update(state => ({ store.update(state => ({
...state, ...state,
role: role === "self" ? null : role, role,
})) }))
API.invalidateCache() API.invalidateCache()
await authStore.actions.fetchUser() await authStore.actions.fetchUser()

View File

@ -3,6 +3,8 @@
import RelationshipCell from "./RelationshipCell.svelte" import RelationshipCell from "./RelationshipCell.svelte"
import { FieldSubtype } from "@budibase/types" import { FieldSubtype } from "@budibase/types"
export let api
const { API } = getContext("grid") const { API } = getContext("grid")
const { subtype } = $$props.schema const { subtype } = $$props.schema
@ -17,8 +19,11 @@
throw `Search for '${subtype}' not implemented` throw `Search for '${subtype}' not implemented`
} }
// As we are overriding the search function from RelationshipCell, we want to map one shape to the expected one for the specific API
const email = Object.values(searchParams.query.string)[0]
const results = await API.searchUsers({ const results = await API.searchUsers({
...searchParams, email,
}) })
// Mapping to the expected data within RelationshipCell // Mapping to the expected data within RelationshipCell
@ -31,6 +36,7 @@
</script> </script>
<RelationshipCell <RelationshipCell
bind:api
{...$$props} {...$$props}
{schema} {schema}
{searchFunction} {searchFunction}

View File

@ -19,7 +19,7 @@ docker run --rm \
-v ${PWD}/generated:/generated \ -v ${PWD}/generated:/generated \
-v ${PWD}/config.json:/config.json \ -v ${PWD}/config.json:/config.json \
-u $(id -u):$(id -g) \ -u $(id -u):$(id -g) \
swaggerapi/swagger-codegen-cli-v3 generate \ swaggerapi/swagger-codegen-cli-v3:3.0.46 generate \
-i /openapi.yml \ -i /openapi.yml \
-l javascript \ -l javascript \
-o /generated \ -o /generated \
@ -34,4 +34,4 @@ if [[ -f "openapi.yaml" ]]; then
fi fi
if [[ -d "generated" ]]; then if [[ -d "generated" ]]; then
rm -r generated rm -r generated
fi fi

View File

@ -9,7 +9,7 @@ import { quotas } from "@budibase/pro"
import { events, context, utils, constants } from "@budibase/backend-core" import { events, context, utils, constants } from "@budibase/backend-core"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { QueryEvent } from "../../../threads/definitions" import { QueryEvent } from "../../../threads/definitions"
import { Query } from "@budibase/types" import { ConfigType, Query, UserCtx } from "@budibase/types"
import { ValidQueryNameRegex } from "@budibase/shared-core" import { ValidQueryNameRegex } from "@budibase/shared-core"
const Runner = new Thread(ThreadType.QUERY, { const Runner = new Thread(ThreadType.QUERY, {
@ -28,11 +28,11 @@ function enrichQueries(input: any) {
return wasArray ? queries : queries[0] return wasArray ? queries : queries[0]
} }
export async function fetch(ctx: any) { export async function fetch(ctx: UserCtx) {
ctx.body = await sdk.queries.fetch() ctx.body = await sdk.queries.fetch()
} }
const _import = async (ctx: any) => { const _import = async (ctx: UserCtx) => {
const body = ctx.request.body const body = ctx.request.body
const data = body.data const data = body.data
@ -73,7 +73,7 @@ const _import = async (ctx: any) => {
} }
export { _import as import } export { _import as import }
export async function save(ctx: any) { export async function save(ctx: UserCtx) {
const db = context.getAppDB() const db = context.getAppDB()
const query = ctx.request.body const query = ctx.request.body
@ -100,19 +100,19 @@ export async function save(ctx: any) {
ctx.message = `Query ${query.name} saved successfully.` ctx.message = `Query ${query.name} saved successfully.`
} }
export async function find(ctx: any) { export async function find(ctx: UserCtx) {
const queryId = ctx.params.queryId const queryId = ctx.params.queryId
ctx.body = await sdk.queries.find(queryId) ctx.body = await sdk.queries.find(queryId)
} }
//Required to discern between OIDC OAuth config entries //Required to discern between OIDC OAuth config entries
function getOAuthConfigCookieId(ctx: any) { function getOAuthConfigCookieId(ctx: UserCtx) {
if (ctx.user.providerType === constants.Config.OIDC) { if (ctx.user.providerType === ConfigType.OIDC) {
return utils.getCookie(ctx, constants.Cookie.OIDC_CONFIG) return utils.getCookie(ctx, constants.Cookie.OIDC_CONFIG)
} }
} }
function getAuthConfig(ctx: any) { function getAuthConfig(ctx: UserCtx) {
const authCookie = utils.getCookie(ctx, constants.Cookie.Auth) const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
let authConfigCtx: any = {} let authConfigCtx: any = {}
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx) authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
@ -120,7 +120,7 @@ function getAuthConfig(ctx: any) {
return authConfigCtx return authConfigCtx
} }
export async function preview(ctx: any) { export async function preview(ctx: UserCtx) {
const { datasource, envVars } = await sdk.datasources.getWithEnvVars( const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
ctx.request.body.datasourceId ctx.request.body.datasourceId
) )
@ -129,6 +129,19 @@ export async function preview(ctx: any) {
// this stops dynamic variables from calling the same query // this stops dynamic variables from calling the same query
const { fields, parameters, queryVerb, transformer, queryId, schema } = query const { fields, parameters, queryVerb, transformer, queryId, schema } = query
let existingSchema = schema
if (queryId && !existingSchema) {
try {
const db = context.getAppDB()
const existing = (await db.get(queryId)) as Query
existingSchema = existing.schema
} catch (err: any) {
if (err.status !== 404) {
ctx.throw(500, "Unable to retrieve existing query")
}
}
}
const authConfigCtx: any = getAuthConfig(ctx) const authConfigCtx: any = getAuthConfig(ctx)
try { try {
@ -180,6 +193,14 @@ export async function preview(ctx: any) {
schemaFields[key] = fieldType schemaFields[key] = fieldType
} }
} }
// if existing schema, update to include any previous schema keys
if (existingSchema) {
for (let key of Object.keys(schemaFields)) {
if (existingSchema[key]?.type) {
schemaFields[key] = existingSchema[key].type
}
}
}
// remove configuration before sending event // remove configuration before sending event
delete datasource.config delete datasource.config
await events.query.previewed(datasource, query) await events.query.previewed(datasource, query)
@ -189,13 +210,13 @@ export async function preview(ctx: any) {
info, info,
extra, extra,
} }
} catch (err) { } catch (err: any) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }
async function execute( async function execute(
ctx: any, ctx: UserCtx,
opts: any = { rowsOnly: false, isAutomation: false } opts: any = { rowsOnly: false, isAutomation: false }
) { ) {
const db = context.getAppDB() const db = context.getAppDB()
@ -255,17 +276,17 @@ async function execute(
} else { } else {
ctx.body = { data: rows, pagination, ...extra, ...info } ctx.body = { data: rows, pagination, ...extra, ...info }
} }
} catch (err) { } catch (err: any) {
ctx.throw(400, err) ctx.throw(400, err)
} }
} }
export async function executeV1(ctx: any) { export async function executeV1(ctx: UserCtx) {
return execute(ctx, { rowsOnly: true, isAutomation: false }) return execute(ctx, { rowsOnly: true, isAutomation: false })
} }
export async function executeV2( export async function executeV2(
ctx: any, ctx: UserCtx,
{ isAutomation }: { isAutomation?: boolean } = {} { isAutomation }: { isAutomation?: boolean } = {}
) { ) {
return execute(ctx, { rowsOnly: false, isAutomation }) return execute(ctx, { rowsOnly: false, isAutomation })
@ -292,7 +313,7 @@ const removeDynamicVariables = async (queryId: any) => {
} }
} }
export async function destroy(ctx: any) { export async function destroy(ctx: UserCtx) {
const db = context.getAppDB() const db = context.getAppDB()
const queryId = ctx.params.queryId const queryId = ctx.params.queryId
await removeDynamicVariables(queryId) await removeDynamicVariables(queryId)

View File

@ -340,10 +340,16 @@ export class ExternalRequest<T extends Operation> {
// one to many // one to many
if (isOneSide(field)) { if (isOneSide(field)) {
let id = row[key][0] let id = row[key][0]
if (typeof row[key] === "string") { if (id) {
id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1] if (typeof row[key] === "string") {
id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1]
}
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(id)[0]
} else {
// Removing from both new and row, as we don't know if it has already been processed
row[field.foreignKey || linkTablePrimary] = null
newRow[field.foreignKey || linkTablePrimary] = null
} }
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(id)[0]
} }
// many to many // many to many
else if (field.through) { else if (field.through) {
@ -830,10 +836,7 @@ export class ExternalRequest<T extends Operation> {
// can't really use response right now // can't really use response right now
const response = await getDatasourceAndQuery(json) const response = await getDatasourceAndQuery(json)
// handle many to many relationships now if we know the ID (could be auto increment) // handle many to many relationships now if we know the ID (could be auto increment)
if ( if (operation !== Operation.READ) {
operation !== Operation.READ &&
processed.manyRelationships?.length > 0
) {
await this.handleManyRelationships( await this.handleManyRelationships(
table._id || "", table._id || "",
response[0], response[0],

View File

@ -108,13 +108,11 @@ export async function save(ctx: UserCtx) {
row, row,
}) })
const responseRow = response as { row: Row }
if (!isEqual(table, updatedTable)) { if (!isEqual(table, updatedTable)) {
await sdk.tables.saveTable(updatedTable) await sdk.tables.saveTable(updatedTable)
} }
const rowId = responseRow.row._id const rowId = response.row._id
if (rowId) { if (rowId) {
const row = await sdk.rows.external.getRow(tableId, rowId, { const row = await sdk.rows.external.getRow(tableId, rowId, {
relationships: true, relationships: true,

View File

@ -14,7 +14,6 @@ import {
Table, Table,
TableResponse, TableResponse,
UserCtx, UserCtx,
Datasource,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { jsonFromCsvString } from "../../../utilities/csv" import { jsonFromCsvString } from "../../../utilities/csv"

View File

@ -18,7 +18,6 @@ import {
SortType, SortType,
StaticQuotaName, StaticQuotaName,
Table, Table,
User,
} from "@budibase/types" } from "@budibase/types"
import { import {
expectAnyExternalColsAttributes, expectAnyExternalColsAttributes,
@ -1515,9 +1514,82 @@ describe.each([
}) })
}) })
describe("bb reference fields", () => { let o2mTable: Table
let m2mTable: Table
beforeAll(async () => {
o2mTable = await config.createTable(
{ ...generateTableConfig(), name: "o2m" },
{
skipReassigning: true,
}
)
m2mTable = await config.createTable(
{ ...generateTableConfig(), name: "m2m" },
{
skipReassigning: true,
}
)
})
describe.each([
[
"relationship fields",
() => ({
user: {
name: "user",
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.LINK,
tableId: o2mTable._id!,
fieldName: "fk_o2m",
},
users: {
name: "users",
relationshipType: RelationshipType.MANY_TO_MANY,
type: FieldType.LINK,
tableId: m2mTable._id!,
fieldName: "fk_m2m",
},
}),
(tableId: string) =>
config.api.row.save(tableId, {
name: generator.word(),
description: generator.paragraph(),
tableId,
}),
(row: Row) => ({
_id: row._id,
primaryDisplay: row.name,
}),
],
[
"bb reference fields",
() => ({
user: {
name: "user",
relationshipType: RelationshipType.ONE_TO_MANY,
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
},
users: {
name: "users",
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
relationshipType: RelationshipType.MANY_TO_MANY,
},
}),
() => config.createUser(),
(row: Row) => ({
_id: row._id,
email: row.email,
firstName: row.firstName,
lastName: row.lastName,
primaryDisplay: row.email,
}),
],
])("links - %s", (__, relSchema, dataGenerator, resultMapper) => {
let tableId: string let tableId: string
let users: User[] let o2mData: Row[]
let m2mData: Row[]
beforeAll(async () => { beforeAll(async () => {
const tableConfig = generateTableConfig() const tableConfig = generateTableConfig()
@ -1532,31 +1604,27 @@ describe.each([
...tableConfig, ...tableConfig,
schema: { schema: {
...tableConfig.schema, ...tableConfig.schema,
user: { ...relSchema(),
name: "user",
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
relationshipType: RelationshipType.ONE_TO_MANY,
},
users: {
name: "users",
type: FieldType.BB_REFERENCE,
subtype: FieldTypeSubtypes.BB_REFERENCE.USER,
relationshipType: RelationshipType.MANY_TO_MANY,
},
}, },
}) })
tableId = table._id! tableId = table._id!
users = [ o2mData = [
await config.createUser(), await dataGenerator(o2mTable._id!),
await config.createUser(), await dataGenerator(o2mTable._id!),
await config.createUser(), await dataGenerator(o2mTable._id!),
await config.createUser(), await dataGenerator(o2mTable._id!),
]
m2mData = [
await dataGenerator(m2mTable._id!),
await dataGenerator(m2mTable._id!),
await dataGenerator(m2mTable._id!),
await dataGenerator(m2mTable._id!),
] ]
}) })
it("can save a row when BB reference fields are empty", async () => { it("can save a row when relationship fields are empty", async () => {
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
@ -1575,13 +1643,13 @@ describe.each([
}) })
}) })
it("can save a row with a single BB reference field", async () => { it("can save a row with a single relationship field", async () => {
const user = _.sample(users)! const user = _.sample(o2mData)!
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
user: user, user: [user],
} }
const row = await config.api.row.save(tableId, rowData) const row = await config.api.row.save(tableId, rowData)
@ -1589,24 +1657,17 @@ describe.each([
name: rowData.name, name: rowData.name,
description: rowData.description, description: rowData.description,
tableId, tableId,
user: [ user: [user].map(u => resultMapper(u)),
{
_id: user._id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
primaryDisplay: user.email,
},
],
_id: expect.any(String), _id: expect.any(String),
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined, type: isInternal ? "row" : undefined,
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
}) })
}) })
it("can save a row with a multiple BB reference field", async () => { it("can save a row with a multiple relationship field", async () => {
const selectedUsers = _.sampleSize(users, 2) const selectedUsers = _.sampleSize(m2mData, 2)
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
@ -1619,13 +1680,7 @@ describe.each([
name: rowData.name, name: rowData.name,
description: rowData.description, description: rowData.description,
tableId, tableId,
users: selectedUsers.map(u => ({ users: expect.arrayContaining(selectedUsers.map(u => resultMapper(u))),
_id: u._id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
primaryDisplay: u.email,
})),
_id: expect.any(String), _id: expect.any(String),
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
@ -1633,7 +1688,7 @@ describe.each([
}) })
}) })
it("can retrieve rows with no populated BB references", async () => { it("can retrieve rows with no populated relationships", async () => {
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
@ -1655,14 +1710,15 @@ describe.each([
}) })
}) })
it("can retrieve rows with populated BB references", async () => { it("can retrieve rows with populated relationships", async () => {
const [user1, user2] = _.sampleSize(users, 2) const user1 = _.sample(o2mData)!
const [user2, user3] = _.sampleSize(m2mData, 2)
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
users: [user1, user2], users: [user2, user3],
user: [user1], user: [user1],
} }
const row = await config.api.row.save(tableId, rowData) const row = await config.api.row.save(tableId, rowData)
@ -1672,72 +1728,51 @@ describe.each([
name: rowData.name, name: rowData.name,
description: rowData.description, description: rowData.description,
tableId, tableId,
user: [user1].map(u => ({ user: expect.arrayContaining([user1].map(u => resultMapper(u))),
_id: u._id, users: expect.arrayContaining([user2, user3].map(u => resultMapper(u))),
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
primaryDisplay: u.email,
})),
users: [user1, user2].map(u => ({
_id: u._id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
primaryDisplay: u.email,
})),
_id: row._id, _id: row._id,
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user1.id,
...defaultRowFields, ...defaultRowFields,
}) })
}) })
it("can update an existing populated row", async () => { it("can update an existing populated row", async () => {
const [user1, user2, user3] = _.sampleSize(users, 3) const user = _.sample(o2mData)!
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
users: [user1, user2], users: [users1, users2],
} }
const row = await config.api.row.save(tableId, rowData) const row = await config.api.row.save(tableId, rowData)
const updatedRow = await config.api.row.save(tableId, { const updatedRow = await config.api.row.save(tableId, {
...row, ...row,
user: [user3], user: [user],
users: [user3, user2], users: [users3, users1],
}) })
expect(updatedRow).toEqual({ expect(updatedRow).toEqual({
name: rowData.name, name: rowData.name,
description: rowData.description, description: rowData.description,
tableId, tableId,
user: [ user: expect.arrayContaining([user].map(u => resultMapper(u))),
{ users: expect.arrayContaining(
_id: user3._id, [users3, users1].map(u => resultMapper(u))
email: user3.email, ),
firstName: user3.firstName,
lastName: user3.lastName,
primaryDisplay: user3.email,
},
],
users: [user3, user2].map(u => ({
_id: u._id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
primaryDisplay: u.email,
})),
_id: row._id, _id: row._id,
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
type: isInternal ? "row" : undefined, type: isInternal ? "row" : undefined,
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
}) })
}) })
it("can wipe an existing populated BB references in row", async () => { it("can wipe an existing populated relationships in row", async () => {
const [user1, user2] = _.sampleSize(users, 2) const [user1, user2] = _.sampleSize(m2mData, 2)
const rowData = { const rowData = {
...basicRow(tableId), ...basicRow(tableId),
@ -1756,8 +1791,6 @@ describe.each([
name: rowData.name, name: rowData.name,
description: rowData.description, description: rowData.description,
tableId, tableId,
user: isInternal ? null : undefined,
users: isInternal ? null : undefined,
_id: row._id, _id: row._id,
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
@ -1765,34 +1798,35 @@ describe.each([
}) })
}) })
it("fetch all will populate the BB references", async () => { it("fetch all will populate the relationships", async () => {
const [user1, user2, user3] = _.sampleSize(users, 3) const [user1] = _.sampleSize(o2mData, 1)
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
const rows: { const rows: {
name: string name: string
description: string description: string
user?: User[] user?: Row[]
users?: User[] users?: Row[]
tableId: string tableId: string
}[] = [ }[] = [
{ {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
users: [user1, user2], users: [users1, users2],
}, },
{ {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
user: [user1], user: [user1],
users: [user1, user3], users: [users1, users3],
}, },
{ {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
users: [user3], users: [users3],
}, },
] ]
@ -1808,57 +1842,50 @@ describe.each([
name: r.name, name: r.name,
description: r.description, description: r.description,
tableId, tableId,
user: r.user?.map(u => ({ user: r.user?.map(u => resultMapper(u)),
_id: u._id, users: r.users?.length
email: u.email, ? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
firstName: u.firstName, : undefined,
lastName: u.lastName,
primaryDisplay: u.email,
})),
users: r.users?.map(u => ({
_id: u._id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
primaryDisplay: u.email,
})),
_id: expect.any(String), _id: expect.any(String),
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
[`fk_${o2mTable.name}_fk_o2m`]:
isInternal || !r.user?.length ? undefined : r.user[0].id,
...defaultRowFields, ...defaultRowFields,
})) }))
) )
) )
}) })
it("search all will populate the BB references", async () => { it("search all will populate the relationships", async () => {
const [user1, user2, user3] = _.sampleSize(users, 3) const [user1] = _.sampleSize(o2mData, 1)
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
const rows: { const rows: {
name: string name: string
description: string description: string
user?: User[] user?: Row[]
users?: User[] users?: Row[]
tableId: string tableId: string
}[] = [ }[] = [
{ {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
users: [user1, user2], users: [users1, users2],
}, },
{ {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
user: [user1], user: [user1],
users: [user1, user3], users: [users1, users3],
}, },
{ {
...basicRow(tableId), ...basicRow(tableId),
name: generator.name(), name: generator.name(),
description: generator.name(), description: generator.name(),
users: [user3], users: [users3],
}, },
] ]
@ -1874,23 +1901,15 @@ describe.each([
name: r.name, name: r.name,
description: r.description, description: r.description,
tableId, tableId,
user: r.user?.map(u => ({ user: r.user?.map(u => resultMapper(u)),
_id: u._id, users: r.users?.length
email: u.email, ? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
firstName: u.firstName, : undefined,
lastName: u.lastName,
primaryDisplay: u.email,
})),
users: r.users?.map(u => ({
_id: u._id,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
primaryDisplay: u.email,
})),
_id: expect.any(String), _id: expect.any(String),
_rev: expect.any(String), _rev: expect.any(String),
id: isInternal ? undefined : expect.any(Number), id: isInternal ? undefined : expect.any(Number),
[`fk_${o2mTable.name}_fk_o2m`]:
isInternal || !r.user?.length ? undefined : r.user[0].id,
...defaultRowFields, ...defaultRowFields,
})) }))
), ),

View File

@ -78,8 +78,7 @@ export const definition: AutomationStepSchema = {
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({ inputs }: AutomationStepInput) {
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023 const { url, body } = inputs
const { url, value1, value2, value3, value4, value5, body } = inputs
let payload = {} let payload = {}
try { try {
@ -104,11 +103,6 @@ export async function run({ inputs }: AutomationStepInput) {
response = await fetch(url, { response = await fetch(url, {
method: "post", method: "post",
body: JSON.stringify({ body: JSON.stringify({
value1,
value2,
value3,
value4,
value5,
...payload, ...payload,
}), }),
headers: { headers: {

View File

@ -71,8 +71,7 @@ export const definition: AutomationStepSchema = {
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({ inputs }: AutomationStepInput) {
//TODO - Remove deprecated values 1,2,3,4,5 after November 2023 const { url, body } = inputs
const { url, value1, value2, value3, value4, value5, body } = inputs
let payload = {} let payload = {}
try { try {
@ -100,11 +99,6 @@ export async function run({ inputs }: AutomationStepInput) {
method: "post", method: "post",
body: JSON.stringify({ body: JSON.stringify({
platform: "budibase", platform: "budibase",
value1,
value2,
value3,
value4,
value5,
...payload, ...payload,
}), }),
headers: { headers: {

View File

@ -11,12 +11,7 @@ export interface QueryEvent {
queryId: string queryId: string
environmentVariables?: Record<string, string> environmentVariables?: Record<string, string>
ctx?: any ctx?: any
schema?: { schema?: Record<string, { name?: string; type: string }>
[key: string]: {
name: string
type: string
}
}
} }
export interface QueryVariable { export interface QueryVariable {

View File

@ -48,7 +48,7 @@ export async function processOutputBBReferences(
) { ) {
if (typeof value !== "string") { if (typeof value !== "string") {
// Already processed or nothing to process // Already processed or nothing to process
return value return value || undefined
} }
const ids = value.split(",").filter(id => !!id) const ids = value.split(",").filter(id => !!id)

View File

@ -11,6 +11,7 @@ import {
processInputBBReferences, processInputBBReferences,
processOutputBBReferences, processOutputBBReferences,
} from "./bbReferenceProcessor" } from "./bbReferenceProcessor"
import { isExternalTable } from "../../integrations/utils"
export * from "./utils" export * from "./utils"
type AutoColumnProcessingOpts = { type AutoColumnProcessingOpts = {
@ -234,9 +235,6 @@ export async function outputProcessing<T extends Row[] | Row>(
} }
} else if (column.type == FieldTypes.BB_REFERENCE) { } else if (column.type == FieldTypes.BB_REFERENCE) {
for (let row of enriched) { for (let row of enriched) {
if (row[property] == null) {
continue
}
row[property] = await processOutputBBReferences( row[property] = await processOutputBBReferences(
row[property], row[property],
column.subtype as FieldSubtype column.subtype as FieldSubtype
@ -250,6 +248,16 @@ export async function outputProcessing<T extends Row[] | Row>(
enriched enriched
)) as Row[] )) as Row[]
} }
// remove null properties to match internal API
if (isExternalTable(table._id!)) {
for (let row of enriched) {
for (let key of Object.keys(row)) {
if (row[key] === null) {
delete row[key]
}
}
}
}
return (wasArray ? enriched : enriched[0]) as T return (wasArray ? enriched : enriched[0]) as T
} }

View File

@ -66,7 +66,7 @@ describe("rowProcessor - outputProcessing", () => {
) )
}) })
it("does not fetch bb references when fields are empty", async () => { it("process output even when the field is not empty", async () => {
const table: Table = { const table: Table = {
_id: generator.guid(), _id: generator.guid(),
name: "TestTable", name: "TestTable",
@ -100,7 +100,7 @@ describe("rowProcessor - outputProcessing", () => {
expect(result).toEqual({ name: "Jack" }) expect(result).toEqual({ name: "Jack" })
expect(bbReferenceProcessor.processOutputBBReferences).not.toBeCalled() expect(bbReferenceProcessor.processOutputBBReferences).toBeCalledTimes(1)
}) })
it("does not fetch bb references when not in the schema", async () => { it("does not fetch bb references when not in the schema", async () => {

View File

@ -14,5 +14,5 @@ export function isSQL(datasource: Datasource): boolean {
SourceName.MYSQL, SourceName.MYSQL,
SourceName.ORACLE, SourceName.ORACLE,
] ]
return SQL.indexOf(datasource.source) !== -1 return SQL.indexOf(datasource.source) !== -1 || datasource.isSQL === true
} }

View File

@ -9,6 +9,7 @@ export interface Datasource extends Document {
// the config is defined by the schema // the config is defined by the schema
config?: Record<string, any> config?: Record<string, any>
plus?: boolean plus?: boolean
isSQL?: boolean
entities?: { entities?: {
[key: string]: Table [key: string]: Table
} }

View File

@ -6,7 +6,7 @@ export interface Query extends Document {
parameters: QueryParameter[] parameters: QueryParameter[]
fields: RestQueryFields | any fields: RestQueryFields | any
transformer: string | null transformer: string | null
schema: any schema: Record<string, { name?: string; type: string }>
readable: boolean readable: boolean
queryVerb: string queryVerb: string
} }

View File

@ -140,6 +140,7 @@ export interface DatasourceConfig {
export interface Integration { export interface Integration {
docs: string docs: string
plus?: boolean plus?: boolean
isSQL?: boolean
auth?: { type: string } auth?: { type: string }
features?: Partial<Record<DatasourceFeature, boolean>> features?: Partial<Record<DatasourceFeature, boolean>>
relationships?: boolean relationships?: boolean

View File

@ -1,5 +1,5 @@
import { Context, Request } from "koa" import { Context, Request } from "koa"
import { User, Role, UserRoles, Account } from "../documents" import { User, Role, UserRoles, Account, ConfigType } from "../documents"
import { FeatureFlag, License } from "../sdk" import { FeatureFlag, License } from "../sdk"
import { Files } from "formidable" import { Files } from "formidable"
@ -13,6 +13,7 @@ export interface ContextUser extends Omit<User, "roles"> {
csrfToken?: string csrfToken?: string
featureFlags?: FeatureFlag[] featureFlags?: FeatureFlag[]
accountPortalAccess?: boolean accountPortalAccess?: boolean
providerType?: ConfigType
account?: Account account?: Account
} }

View File

@ -11,7 +11,7 @@ import { TestConfiguration } from "../../../../tests"
import { events } from "@budibase/backend-core" import { events } from "@budibase/backend-core"
// this test can 409 - retries reduce issues with this // this test can 409 - retries reduce issues with this
jest.retryTimes(2) jest.retryTimes(2, { logErrorsBeforeRetry: true })
jest.setTimeout(30000) jest.setTimeout(30000)
mocks.licenses.useScimIntegration() mocks.licenses.useScimIntegration()