Merge remote-tracking branch 'origin/develop' into feat/relationship-configuration
This commit is contained in:
commit
eca0089cb9
|
@ -14,7 +14,7 @@ jobs:
|
|||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||
PAYLOAD_PR_NUMBER: ${{ github.ref }}
|
||||
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
repository: budibase/budibase-deploys
|
||||
event: featurebranch-qa-close
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_BRANCH: ${{ github.head_ref }}
|
||||
PAYLOAD_PR_NUMBER: ${{ github.ref }}
|
||||
PAYLOAD_PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
repository: budibase/budibase-deploys
|
||||
event: featurebranch-qa-deploy
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.10.16-alpha.14",
|
||||
"version": "2.11.5-alpha.2",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import { Select, Checkbox } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -108,14 +110,29 @@
|
|||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
{#if !schema.autocolumn && schema.type !== "attachment"}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
<DrawerBindableSlot
|
||||
fillWidth
|
||||
title={value.title}
|
||||
label={field}
|
||||
panel={AutomationBindingPanel}
|
||||
type={schema.type}
|
||||
{schema}
|
||||
bindings={parsedBindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
>
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
bindings={parsedBindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
</DrawerBindableSlot>
|
||||
{/if}
|
||||
{#if isUpdateRow && schema.type === "link"}
|
||||
<div class="checkbox-field">
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
} from "@budibase/bbui"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import DrawerBindableInput from "../../common/bindings/DrawerBindableInput.svelte"
|
||||
import DrawerBindableSlot from "../../common/bindings/DrawerBindableSlot.svelte"
|
||||
import ModalBindableInput from "../../common/bindings/ModalBindableInput.svelte"
|
||||
import AutomationBindingPanel from "../../common/bindings/ServerBindingPanel.svelte"
|
||||
import Editor from "components/integration/QueryEditor.svelte"
|
||||
|
@ -31,88 +30,73 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<DrawerBindableSlot
|
||||
fillWidth
|
||||
title={value.title}
|
||||
label={field}
|
||||
panel={AutomationBindingPanel}
|
||||
type={schema.type}
|
||||
{schema}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
{bindings}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
>
|
||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||
<Select
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
{#if schemaHasOptions(schema) && schema.type !== "array"}
|
||||
<Select
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
value={value[field]}
|
||||
options={schema.constraints.inclusion}
|
||||
/>
|
||||
{:else if schema.type === "datetime"}
|
||||
<DatePicker
|
||||
label={field}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "boolean"}
|
||||
<Select
|
||||
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"}
|
||||
<Multiselect
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
options={schema.constraints.inclusion}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "longform"}
|
||||
<TextArea
|
||||
label={field}
|
||||
bind:value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "json"}
|
||||
<span>
|
||||
<Label>{field}</Label>
|
||||
<Editor
|
||||
editorHeight="150"
|
||||
mode="json"
|
||||
on:change={e => {
|
||||
if (e.detail?.value !== value[field]) {
|
||||
onChange(e, field, schema.type)
|
||||
}
|
||||
}}
|
||||
value={value[field]}
|
||||
options={schema.constraints.inclusion}
|
||||
/>
|
||||
{:else if schema.type === "datetime"}
|
||||
<DatePicker
|
||||
label={field}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "boolean"}
|
||||
<Select
|
||||
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"}
|
||||
<Multiselect
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
options={schema.constraints.inclusion}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "longform"}
|
||||
<TextArea
|
||||
label={field}
|
||||
bind:value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "json"}
|
||||
<span>
|
||||
<Label>{field}</Label>
|
||||
<Editor
|
||||
editorHeight="150"
|
||||
mode="json"
|
||||
on:change={e => {
|
||||
if (e.detail?.value !== value[field]) {
|
||||
onChange(e, field, schema.type)
|
||||
}
|
||||
}}
|
||||
value={value[field]}
|
||||
/>
|
||||
</span>
|
||||
{:else if schema.type === "link"}
|
||||
<LinkedRowSelector
|
||||
bind:linkedRows={value[field]}
|
||||
{schema}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "string" || schema.type === "number"}
|
||||
<svelte:component
|
||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||
panel={AutomationBindingPanel}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
type="string"
|
||||
bindings={parsedBindings}
|
||||
fillWidth={true}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
</DrawerBindableSlot>
|
||||
</span>
|
||||
{:else if schema.type === "link"}
|
||||
<LinkedRowSelector
|
||||
bind:linkedRows={value[field]}
|
||||
{schema}
|
||||
on:change={e => onChange(e, field)}
|
||||
/>
|
||||
{:else if schema.type === "string" || schema.type === "number"}
|
||||
<svelte:component
|
||||
this={isTestModal ? ModalBindableInput : DrawerBindableInput}
|
||||
panel={AutomationBindingPanel}
|
||||
value={value[field]}
|
||||
on:change={e => onChange(e, field)}
|
||||
label={field}
|
||||
type="string"
|
||||
bindings={parsedBindings}
|
||||
fillWidth={true}
|
||||
allowJS={true}
|
||||
updateOnChange={false}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -277,10 +277,7 @@
|
|||
dispatch("updatecolumns")
|
||||
gridDispatch("close-edit-column")
|
||||
|
||||
if (
|
||||
saveColumn.type === LINK_TYPE &&
|
||||
saveColumn.relationshipType === RelationshipType.MANY_TO_MANY
|
||||
) {
|
||||
if (saveColumn.type === LINK_TYPE) {
|
||||
// Fetching the new tables
|
||||
tables.fetch()
|
||||
// Fetching the new relationships
|
||||
|
@ -312,6 +309,11 @@
|
|||
confirmDeleteDialog.hide()
|
||||
dispatch("updatecolumns")
|
||||
gridDispatch("close-edit-column")
|
||||
|
||||
if (editableColumn.type === LINK_TYPE) {
|
||||
// Updating the relationships
|
||||
datasources.fetch()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error(`Error deleting column: ${error.message}`)
|
||||
|
|
|
@ -82,7 +82,8 @@
|
|||
hasValidated = false
|
||||
})
|
||||
}
|
||||
$: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
|
||||
$: valid =
|
||||
getErrorCount(errors) === 0 && allRequiredAttributesSet(relationshipType)
|
||||
$: isManyToMany = relationshipType === RelationshipType.MANY_TO_MANY
|
||||
$: isManyToOne = relationshipType === RelationshipType.MANY_TO_ONE
|
||||
|
||||
|
@ -139,7 +140,7 @@
|
|||
return Object.entries(errors).filter(entry => !!entry[1]).length
|
||||
}
|
||||
|
||||
function allRequiredAttributesSet() {
|
||||
function allRequiredAttributesSet(relationshipType) {
|
||||
const base = getTable(fromId) && getTable(toId) && fromColumn && toColumn
|
||||
if (relationshipType === RelationshipType.MANY_TO_ONE) {
|
||||
return base && fromPrimary && fromForeign
|
||||
|
@ -149,9 +150,10 @@
|
|||
}
|
||||
|
||||
function validate() {
|
||||
if (!allRequiredAttributesSet() && !hasValidated) {
|
||||
if (!allRequiredAttributesSet(relationshipType) && !hasValidated) {
|
||||
return
|
||||
}
|
||||
|
||||
hasValidated = true
|
||||
errorChecker.setType(relationshipType)
|
||||
const fromTable = getTable(fromId),
|
||||
|
|
|
@ -21,15 +21,22 @@
|
|||
function getRelationships(tables) {
|
||||
const relatedColumns = {}
|
||||
|
||||
tables.forEach(({ name: tableName, schema }) => {
|
||||
tables.forEach(({ name: tableName, schema, _id: tableId }) => {
|
||||
Object.values(schema).forEach(column => {
|
||||
if (column.type !== "link") return
|
||||
|
||||
relatedColumns[column._id] ??= {}
|
||||
relatedColumns[column._id].through =
|
||||
relatedColumns[column._id].through || column.through
|
||||
const columnId =
|
||||
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,
|
||||
tableName,
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { Heading, Icon, clickOutside } from "@budibase/bbui"
|
||||
import { FieldTypes } from "constants"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import active from "svelte-spa-router/active"
|
||||
|
||||
const sdk = getContext("sdk")
|
||||
|
@ -103,7 +104,8 @@
|
|||
let validLinks = (allLinks || []).filter(link => link.text && link.url)
|
||||
// Filter to only links allowed by the current role
|
||||
return validLinks.filter(link => {
|
||||
return userRoleHierarchy?.find(roleId => roleId === link.roleId)
|
||||
const role = link.roleId || Constants.Roles.BASIC
|
||||
return userRoleHierarchy?.find(roleId => roleId === role)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -60,6 +60,12 @@
|
|||
// even if they are not in the inital fetch results
|
||||
initialValuesProcessed = true
|
||||
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] = {
|
||||
_id: value._id,
|
||||
[primaryDisplay]: value.primaryDisplay,
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
value: roleId,
|
||||
})
|
||||
}
|
||||
devToolsStore.actions.changeRole(SELF_ROLE)
|
||||
return list
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { createLocalStorageStore } from "@budibase/frontend-core"
|
|||
import { initialise } from "./initialise"
|
||||
import { authStore } from "./auth"
|
||||
import { API } from "../api"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const initialState = {
|
||||
visible: false,
|
||||
|
@ -27,9 +28,15 @@ const createDevToolStore = () => {
|
|||
}
|
||||
|
||||
const changeRole = async role => {
|
||||
if (role === "self") {
|
||||
role = null
|
||||
}
|
||||
if (role === get(store).role) {
|
||||
return
|
||||
}
|
||||
store.update(state => ({
|
||||
...state,
|
||||
role: role === "self" ? null : role,
|
||||
role,
|
||||
}))
|
||||
API.invalidateCache()
|
||||
await authStore.actions.fetchUser()
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import RelationshipCell from "./RelationshipCell.svelte"
|
||||
import { FieldSubtype } from "@budibase/types"
|
||||
|
||||
export let api
|
||||
|
||||
const { API } = getContext("grid")
|
||||
const { subtype } = $$props.schema
|
||||
|
||||
|
@ -17,8 +19,11 @@
|
|||
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({
|
||||
...searchParams,
|
||||
email,
|
||||
})
|
||||
|
||||
// Mapping to the expected data within RelationshipCell
|
||||
|
@ -31,6 +36,7 @@
|
|||
</script>
|
||||
|
||||
<RelationshipCell
|
||||
bind:api
|
||||
{...$$props}
|
||||
{schema}
|
||||
{searchFunction}
|
||||
|
|
|
@ -19,7 +19,7 @@ docker run --rm \
|
|||
-v ${PWD}/generated:/generated \
|
||||
-v ${PWD}/config.json:/config.json \
|
||||
-u $(id -u):$(id -g) \
|
||||
swaggerapi/swagger-codegen-cli-v3 generate \
|
||||
swaggerapi/swagger-codegen-cli-v3:3.0.46 generate \
|
||||
-i /openapi.yml \
|
||||
-l javascript \
|
||||
-o /generated \
|
||||
|
|
|
@ -9,7 +9,7 @@ import { quotas } from "@budibase/pro"
|
|||
import { events, context, utils, constants } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { QueryEvent } from "../../../threads/definitions"
|
||||
import { Query } from "@budibase/types"
|
||||
import { ConfigType, Query, UserCtx } from "@budibase/types"
|
||||
import { ValidQueryNameRegex } from "@budibase/shared-core"
|
||||
|
||||
const Runner = new Thread(ThreadType.QUERY, {
|
||||
|
@ -28,11 +28,11 @@ function enrichQueries(input: any) {
|
|||
return wasArray ? queries : queries[0]
|
||||
}
|
||||
|
||||
export async function fetch(ctx: any) {
|
||||
export async function fetch(ctx: UserCtx) {
|
||||
ctx.body = await sdk.queries.fetch()
|
||||
}
|
||||
|
||||
const _import = async (ctx: any) => {
|
||||
const _import = async (ctx: UserCtx) => {
|
||||
const body = ctx.request.body
|
||||
const data = body.data
|
||||
|
||||
|
@ -73,7 +73,7 @@ const _import = async (ctx: any) => {
|
|||
}
|
||||
export { _import as import }
|
||||
|
||||
export async function save(ctx: any) {
|
||||
export async function save(ctx: UserCtx) {
|
||||
const db = context.getAppDB()
|
||||
const query = ctx.request.body
|
||||
|
||||
|
@ -100,19 +100,19 @@ export async function save(ctx: any) {
|
|||
ctx.message = `Query ${query.name} saved successfully.`
|
||||
}
|
||||
|
||||
export async function find(ctx: any) {
|
||||
export async function find(ctx: UserCtx) {
|
||||
const queryId = ctx.params.queryId
|
||||
ctx.body = await sdk.queries.find(queryId)
|
||||
}
|
||||
|
||||
//Required to discern between OIDC OAuth config entries
|
||||
function getOAuthConfigCookieId(ctx: any) {
|
||||
if (ctx.user.providerType === constants.Config.OIDC) {
|
||||
function getOAuthConfigCookieId(ctx: UserCtx) {
|
||||
if (ctx.user.providerType === ConfigType.OIDC) {
|
||||
return utils.getCookie(ctx, constants.Cookie.OIDC_CONFIG)
|
||||
}
|
||||
}
|
||||
|
||||
function getAuthConfig(ctx: any) {
|
||||
function getAuthConfig(ctx: UserCtx) {
|
||||
const authCookie = utils.getCookie(ctx, constants.Cookie.Auth)
|
||||
let authConfigCtx: any = {}
|
||||
authConfigCtx["configId"] = getOAuthConfigCookieId(ctx)
|
||||
|
@ -120,7 +120,7 @@ function getAuthConfig(ctx: any) {
|
|||
return authConfigCtx
|
||||
}
|
||||
|
||||
export async function preview(ctx: any) {
|
||||
export async function preview(ctx: UserCtx) {
|
||||
const { datasource, envVars } = await sdk.datasources.getWithEnvVars(
|
||||
ctx.request.body.datasourceId
|
||||
)
|
||||
|
@ -129,6 +129,19 @@ export async function preview(ctx: any) {
|
|||
// this stops dynamic variables from calling the same 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)
|
||||
|
||||
try {
|
||||
|
@ -180,6 +193,14 @@ export async function preview(ctx: any) {
|
|||
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
|
||||
delete datasource.config
|
||||
await events.query.previewed(datasource, query)
|
||||
|
@ -189,13 +210,13 @@ export async function preview(ctx: any) {
|
|||
info,
|
||||
extra,
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(
|
||||
ctx: any,
|
||||
ctx: UserCtx,
|
||||
opts: any = { rowsOnly: false, isAutomation: false }
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
|
@ -255,17 +276,17 @@ async function execute(
|
|||
} else {
|
||||
ctx.body = { data: rows, pagination, ...extra, ...info }
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
ctx.throw(400, err)
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeV1(ctx: any) {
|
||||
export async function executeV1(ctx: UserCtx) {
|
||||
return execute(ctx, { rowsOnly: true, isAutomation: false })
|
||||
}
|
||||
|
||||
export async function executeV2(
|
||||
ctx: any,
|
||||
ctx: UserCtx,
|
||||
{ isAutomation }: { isAutomation?: boolean } = {}
|
||||
) {
|
||||
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 queryId = ctx.params.queryId
|
||||
await removeDynamicVariables(queryId)
|
||||
|
|
|
@ -340,10 +340,16 @@ export class ExternalRequest<T extends Operation> {
|
|||
// one to many
|
||||
if (isOneSide(field)) {
|
||||
let id = row[key][0]
|
||||
if (typeof row[key] === "string") {
|
||||
id = decodeURIComponent(row[key]).match(/\[(.*?)\]/)?.[1]
|
||||
if (id) {
|
||||
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
|
||||
else if (field.through) {
|
||||
|
@ -830,10 +836,7 @@ export class ExternalRequest<T extends Operation> {
|
|||
// can't really use response right now
|
||||
const response = await getDatasourceAndQuery(json)
|
||||
// handle many to many relationships now if we know the ID (could be auto increment)
|
||||
if (
|
||||
operation !== Operation.READ &&
|
||||
processed.manyRelationships?.length > 0
|
||||
) {
|
||||
if (operation !== Operation.READ) {
|
||||
await this.handleManyRelationships(
|
||||
table._id || "",
|
||||
response[0],
|
||||
|
|
|
@ -108,13 +108,11 @@ export async function save(ctx: UserCtx) {
|
|||
row,
|
||||
})
|
||||
|
||||
const responseRow = response as { row: Row }
|
||||
|
||||
if (!isEqual(table, updatedTable)) {
|
||||
await sdk.tables.saveTable(updatedTable)
|
||||
}
|
||||
|
||||
const rowId = responseRow.row._id
|
||||
const rowId = response.row._id
|
||||
if (rowId) {
|
||||
const row = await sdk.rows.external.getRow(tableId, rowId, {
|
||||
relationships: true,
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
Table,
|
||||
TableResponse,
|
||||
UserCtx,
|
||||
Datasource,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../../sdk"
|
||||
import { jsonFromCsvString } from "../../../utilities/csv"
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
SortType,
|
||||
StaticQuotaName,
|
||||
Table,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
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 users: User[]
|
||||
let o2mData: Row[]
|
||||
let m2mData: Row[]
|
||||
|
||||
beforeAll(async () => {
|
||||
const tableConfig = generateTableConfig()
|
||||
|
@ -1532,31 +1604,27 @@ describe.each([
|
|||
...tableConfig,
|
||||
schema: {
|
||||
...tableConfig.schema,
|
||||
user: {
|
||||
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,
|
||||
},
|
||||
...relSchema(),
|
||||
},
|
||||
})
|
||||
tableId = table._id!
|
||||
|
||||
users = [
|
||||
await config.createUser(),
|
||||
await config.createUser(),
|
||||
await config.createUser(),
|
||||
await config.createUser(),
|
||||
o2mData = [
|
||||
await dataGenerator(o2mTable._id!),
|
||||
await dataGenerator(o2mTable._id!),
|
||||
await dataGenerator(o2mTable._id!),
|
||||
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 = {
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
|
@ -1575,13 +1643,13 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
it("can save a row with a single BB reference field", async () => {
|
||||
const user = _.sample(users)!
|
||||
it("can save a row with a single relationship field", async () => {
|
||||
const user = _.sample(o2mData)!
|
||||
const rowData = {
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
user: user,
|
||||
user: [user],
|
||||
}
|
||||
const row = await config.api.row.save(tableId, rowData)
|
||||
|
||||
|
@ -1589,24 +1657,17 @@ describe.each([
|
|||
name: rowData.name,
|
||||
description: rowData.description,
|
||||
tableId,
|
||||
user: [
|
||||
{
|
||||
_id: user._id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
primaryDisplay: user.email,
|
||||
},
|
||||
],
|
||||
user: [user].map(u => resultMapper(u)),
|
||||
_id: expect.any(String),
|
||||
_rev: expect.any(String),
|
||||
id: isInternal ? undefined : expect.any(Number),
|
||||
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 () => {
|
||||
const selectedUsers = _.sampleSize(users, 2)
|
||||
it("can save a row with a multiple relationship field", async () => {
|
||||
const selectedUsers = _.sampleSize(m2mData, 2)
|
||||
const rowData = {
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
|
@ -1619,13 +1680,7 @@ describe.each([
|
|||
name: rowData.name,
|
||||
description: rowData.description,
|
||||
tableId,
|
||||
users: selectedUsers.map(u => ({
|
||||
_id: u._id,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
lastName: u.lastName,
|
||||
primaryDisplay: u.email,
|
||||
})),
|
||||
users: expect.arrayContaining(selectedUsers.map(u => resultMapper(u))),
|
||||
_id: expect.any(String),
|
||||
_rev: expect.any(String),
|
||||
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 = {
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
|
@ -1655,14 +1710,15 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
it("can retrieve rows with populated BB references", async () => {
|
||||
const [user1, user2] = _.sampleSize(users, 2)
|
||||
it("can retrieve rows with populated relationships", async () => {
|
||||
const user1 = _.sample(o2mData)!
|
||||
const [user2, user3] = _.sampleSize(m2mData, 2)
|
||||
|
||||
const rowData = {
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
users: [user1, user2],
|
||||
users: [user2, user3],
|
||||
user: [user1],
|
||||
}
|
||||
const row = await config.api.row.save(tableId, rowData)
|
||||
|
@ -1672,72 +1728,51 @@ describe.each([
|
|||
name: rowData.name,
|
||||
description: rowData.description,
|
||||
tableId,
|
||||
user: [user1].map(u => ({
|
||||
_id: u._id,
|
||||
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,
|
||||
})),
|
||||
user: expect.arrayContaining([user1].map(u => resultMapper(u))),
|
||||
users: expect.arrayContaining([user2, user3].map(u => resultMapper(u))),
|
||||
_id: row._id,
|
||||
_rev: expect.any(String),
|
||||
id: isInternal ? undefined : expect.any(Number),
|
||||
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user1.id,
|
||||
...defaultRowFields,
|
||||
})
|
||||
})
|
||||
|
||||
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 = {
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
users: [user1, user2],
|
||||
users: [users1, users2],
|
||||
}
|
||||
const row = await config.api.row.save(tableId, rowData)
|
||||
|
||||
const updatedRow = await config.api.row.save(tableId, {
|
||||
...row,
|
||||
user: [user3],
|
||||
users: [user3, user2],
|
||||
user: [user],
|
||||
users: [users3, users1],
|
||||
})
|
||||
expect(updatedRow).toEqual({
|
||||
name: rowData.name,
|
||||
description: rowData.description,
|
||||
tableId,
|
||||
user: [
|
||||
{
|
||||
_id: user3._id,
|
||||
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,
|
||||
})),
|
||||
user: expect.arrayContaining([user].map(u => resultMapper(u))),
|
||||
users: expect.arrayContaining(
|
||||
[users3, users1].map(u => resultMapper(u))
|
||||
),
|
||||
_id: row._id,
|
||||
_rev: expect.any(String),
|
||||
id: isInternal ? undefined : expect.any(Number),
|
||||
type: isInternal ? "row" : undefined,
|
||||
[`fk_${o2mTable.name}_fk_o2m`]: isInternal ? undefined : user.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("can wipe an existing populated BB references in row", async () => {
|
||||
const [user1, user2] = _.sampleSize(users, 2)
|
||||
it("can wipe an existing populated relationships in row", async () => {
|
||||
const [user1, user2] = _.sampleSize(m2mData, 2)
|
||||
|
||||
const rowData = {
|
||||
...basicRow(tableId),
|
||||
|
@ -1756,8 +1791,6 @@ describe.each([
|
|||
name: rowData.name,
|
||||
description: rowData.description,
|
||||
tableId,
|
||||
user: isInternal ? null : undefined,
|
||||
users: isInternal ? null : undefined,
|
||||
_id: row._id,
|
||||
_rev: expect.any(String),
|
||||
id: isInternal ? undefined : expect.any(Number),
|
||||
|
@ -1765,34 +1798,35 @@ describe.each([
|
|||
})
|
||||
})
|
||||
|
||||
it("fetch all will populate the BB references", async () => {
|
||||
const [user1, user2, user3] = _.sampleSize(users, 3)
|
||||
it("fetch all will populate the relationships", async () => {
|
||||
const [user1] = _.sampleSize(o2mData, 1)
|
||||
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
|
||||
|
||||
const rows: {
|
||||
name: string
|
||||
description: string
|
||||
user?: User[]
|
||||
users?: User[]
|
||||
user?: Row[]
|
||||
users?: Row[]
|
||||
tableId: string
|
||||
}[] = [
|
||||
{
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
users: [user1, user2],
|
||||
users: [users1, users2],
|
||||
},
|
||||
{
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
user: [user1],
|
||||
users: [user1, user3],
|
||||
users: [users1, users3],
|
||||
},
|
||||
{
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
users: [user3],
|
||||
users: [users3],
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -1808,57 +1842,50 @@ describe.each([
|
|||
name: r.name,
|
||||
description: r.description,
|
||||
tableId,
|
||||
user: r.user?.map(u => ({
|
||||
_id: u._id,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
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,
|
||||
})),
|
||||
user: r.user?.map(u => resultMapper(u)),
|
||||
users: r.users?.length
|
||||
? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
|
||||
: undefined,
|
||||
_id: expect.any(String),
|
||||
_rev: expect.any(String),
|
||||
id: isInternal ? undefined : expect.any(Number),
|
||||
[`fk_${o2mTable.name}_fk_o2m`]:
|
||||
isInternal || !r.user?.length ? undefined : r.user[0].id,
|
||||
...defaultRowFields,
|
||||
}))
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it("search all will populate the BB references", async () => {
|
||||
const [user1, user2, user3] = _.sampleSize(users, 3)
|
||||
it("search all will populate the relationships", async () => {
|
||||
const [user1] = _.sampleSize(o2mData, 1)
|
||||
const [users1, users2, users3] = _.sampleSize(m2mData, 3)
|
||||
|
||||
const rows: {
|
||||
name: string
|
||||
description: string
|
||||
user?: User[]
|
||||
users?: User[]
|
||||
user?: Row[]
|
||||
users?: Row[]
|
||||
tableId: string
|
||||
}[] = [
|
||||
{
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
users: [user1, user2],
|
||||
users: [users1, users2],
|
||||
},
|
||||
{
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
user: [user1],
|
||||
users: [user1, user3],
|
||||
users: [users1, users3],
|
||||
},
|
||||
{
|
||||
...basicRow(tableId),
|
||||
name: generator.name(),
|
||||
description: generator.name(),
|
||||
users: [user3],
|
||||
users: [users3],
|
||||
},
|
||||
]
|
||||
|
||||
|
@ -1874,23 +1901,15 @@ describe.each([
|
|||
name: r.name,
|
||||
description: r.description,
|
||||
tableId,
|
||||
user: r.user?.map(u => ({
|
||||
_id: u._id,
|
||||
email: u.email,
|
||||
firstName: u.firstName,
|
||||
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,
|
||||
})),
|
||||
user: r.user?.map(u => resultMapper(u)),
|
||||
users: r.users?.length
|
||||
? expect.arrayContaining(r.users?.map(u => resultMapper(u)))
|
||||
: undefined,
|
||||
_id: expect.any(String),
|
||||
_rev: expect.any(String),
|
||||
id: isInternal ? undefined : expect.any(Number),
|
||||
[`fk_${o2mTable.name}_fk_o2m`]:
|
||||
isInternal || !r.user?.length ? undefined : r.user[0].id,
|
||||
...defaultRowFields,
|
||||
}))
|
||||
),
|
||||
|
|
|
@ -11,12 +11,7 @@ export interface QueryEvent {
|
|||
queryId: string
|
||||
environmentVariables?: Record<string, string>
|
||||
ctx?: any
|
||||
schema?: {
|
||||
[key: string]: {
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
}
|
||||
schema?: Record<string, { name?: string; type: string }>
|
||||
}
|
||||
|
||||
export interface QueryVariable {
|
||||
|
|
|
@ -48,7 +48,7 @@ export async function processOutputBBReferences(
|
|||
) {
|
||||
if (typeof value !== "string") {
|
||||
// Already processed or nothing to process
|
||||
return value
|
||||
return value || undefined
|
||||
}
|
||||
|
||||
const ids = value.split(",").filter(id => !!id)
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
processInputBBReferences,
|
||||
processOutputBBReferences,
|
||||
} from "./bbReferenceProcessor"
|
||||
import { isExternalTable } from "../../integrations/utils"
|
||||
export * from "./utils"
|
||||
|
||||
type AutoColumnProcessingOpts = {
|
||||
|
@ -234,9 +235,6 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
}
|
||||
} else if (column.type == FieldTypes.BB_REFERENCE) {
|
||||
for (let row of enriched) {
|
||||
if (row[property] == null) {
|
||||
continue
|
||||
}
|
||||
row[property] = await processOutputBBReferences(
|
||||
row[property],
|
||||
column.subtype as FieldSubtype
|
||||
|
@ -250,6 +248,16 @@ export async function outputProcessing<T extends Row[] | Row>(
|
|||
enriched
|
||||
)) 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
_id: generator.guid(),
|
||||
name: "TestTable",
|
||||
|
@ -100,7 +100,7 @@ describe("rowProcessor - outputProcessing", () => {
|
|||
|
||||
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 () => {
|
||||
|
|
|
@ -6,7 +6,7 @@ export interface Query extends Document {
|
|||
parameters: QueryParameter[]
|
||||
fields: RestQueryFields | any
|
||||
transformer: string | null
|
||||
schema: any
|
||||
schema: Record<string, { name?: string; type: string }>
|
||||
readable: boolean
|
||||
queryVerb: string
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { Files } from "formidable"
|
||||
|
||||
|
@ -13,6 +13,7 @@ export interface ContextUser extends Omit<User, "roles"> {
|
|||
csrfToken?: string
|
||||
featureFlags?: FeatureFlag[]
|
||||
accountPortalAccess?: boolean
|
||||
providerType?: ConfigType
|
||||
account?: Account
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { TestConfiguration } from "../../../../tests"
|
|||
import { events } from "@budibase/backend-core"
|
||||
|
||||
// this test can 409 - retries reduce issues with this
|
||||
jest.retryTimes(2)
|
||||
jest.retryTimes(2, { logErrorsBeforeRetry: true })
|
||||
jest.setTimeout(30000)
|
||||
|
||||
mocks.licenses.useScimIntegration()
|
||||
|
|
Loading…
Reference in New Issue