Skip to content

Commit

Permalink
Merge pull request #15439 from Budibase/BUDI-8986/validate-screen-dat…
Browse files Browse the repository at this point in the history
…asources

Validate screen relationship datasource settings
  • Loading branch information
adrinr authored Jan 27, 2025
2 parents c90296d + 92606c6 commit 303beec
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
import IntegrationQueryEditor from "@/components/integration/index.svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
import { findAllComponents } from "@/helpers/components"
import {
extractFields,
extractJSONArrayFields,
extractRelationships,
} from "@/helpers/bindings"
import ClientBindingPanel from "@/components/common/bindings/ClientBindingPanel.svelte"
import DataSourceCategory from "@/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
import { API } from "@/api"
Expand Down Expand Up @@ -81,67 +86,9 @@
value: `{{ literal ${safe(provider._id)} }}`,
type: "provider",
}))
$: links = bindings
// Get only link bindings
.filter(x => x.fieldSchema?.type === "link")
// Filter out bindings provided by forms
.filter(x => !x.component?.endsWith("/form"))
.map(binding => {
const { providerId, readableBinding, fieldSchema } = binding || {}
const { name, tableId } = fieldSchema || {}
const safeProviderId = safe(providerId)
return {
providerId,
label: readableBinding,
fieldName: name,
tableId,
type: "link",
// These properties will be enriched by the client library and provide
// details of the parent row of the relationship field, from context
rowId: `{{ ${safeProviderId}.${safe("_id")} }}`,
rowTableId: `{{ ${safeProviderId}.${safe("tableId")} }}`,
}
})
$: fields = bindings
.filter(
x =>
x.fieldSchema?.type === "attachment" ||
(x.fieldSchema?.type === "array" && x.tableId)
)
.map(binding => {
const { providerId, readableBinding, runtimeBinding } = binding
const { name, type, tableId } = binding.fieldSchema
return {
providerId,
label: readableBinding,
fieldName: name,
fieldType: type,
tableId,
type: "field",
value: `{{ literal ${runtimeBinding} }}`,
}
})
$: jsonArrays = bindings
.filter(
x =>
x.fieldSchema?.type === "jsonarray" ||
(x.fieldSchema?.type === "json" && x.fieldSchema?.subtype === "array")
)
.map(binding => {
const { providerId, readableBinding, runtimeBinding, tableId } = binding
const { name, type, prefixKeys, subtype } = binding.fieldSchema
return {
providerId,
label: readableBinding,
fieldName: name,
fieldType: type,
tableId,
prefixKeys,
type: type === "jsonarray" ? "jsonarray" : "queryarray",
subtype,
value: `{{ literal ${runtimeBinding} }}`,
}
})
$: links = extractRelationships(bindings)
$: fields = extractFields(bindings)
$: jsonArrays = extractJSONArrayFields(bindings)
$: custom = {
type: "custom",
label: "JSON / CSV",
Expand Down
74 changes: 74 additions & 0 deletions packages/builder/src/helpers/bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { makePropSafe } from "@budibase/string-templates"
import { UIBinding } from "@budibase/types"

export function extractRelationships(bindings: UIBinding[]) {
return (
bindings
// Get only link bindings
.filter(x => x.fieldSchema?.type === "link")
// Filter out bindings provided by forms
.filter(x => !x.component?.endsWith("/form"))
.map(binding => {
const { providerId, readableBinding, fieldSchema } = binding || {}
const { name, tableId } = fieldSchema || {}
const safeProviderId = makePropSafe(providerId)
return {
providerId,
label: readableBinding,
fieldName: name,
tableId,
type: "link",
// These properties will be enriched by the client library and provide
// details of the parent row of the relationship field, from context
rowId: `{{ ${safeProviderId}.${makePropSafe("_id")} }}`,
rowTableId: `{{ ${safeProviderId}.${makePropSafe("tableId")} }}`,
}
})
)
}

export function extractFields(bindings: UIBinding[]) {
return bindings
.filter(
x =>
x.fieldSchema?.type === "attachment" ||
(x.fieldSchema?.type === "array" && x.tableId)
)
.map(binding => {
const { providerId, readableBinding, runtimeBinding } = binding
const { name, type, tableId } = binding.fieldSchema!
return {
providerId,
label: readableBinding,
fieldName: name,
fieldType: type,
tableId,
type: "field",
value: `{{ literal ${runtimeBinding} }}`,
}
})
}

export function extractJSONArrayFields(bindings: UIBinding[]) {
return bindings
.filter(
x =>
x.fieldSchema?.type === "jsonarray" ||
(x.fieldSchema?.type === "json" && x.fieldSchema?.subtype === "array")
)
.map(binding => {
const { providerId, readableBinding, runtimeBinding, tableId } = binding
const { name, type, prefixKeys, subtype } = binding.fieldSchema!
return {
providerId,
label: readableBinding,
fieldName: name,
fieldType: type,
tableId,
prefixKeys,
type: type === "jsonarray" ? "jsonarray" : "queryarray",
subtype,
value: `{{ literal ${runtimeBinding} }}`,
}
})
}
1 change: 1 addition & 0 deletions packages/builder/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
isBuilderInputFocused,
} from "./helpers"
export * as featureFlag from "./featureFlags"
export * as bindings from "./bindings"
21 changes: 18 additions & 3 deletions packages/builder/src/stores/builder/screenComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { findComponentsBySettingsType } from "@/helpers/screen"
import { UIDatasourceType, Screen } from "@budibase/types"
import { queries } from "./queries"
import { views } from "./views"
import { featureFlag } from "@/helpers"
import { bindings, featureFlag } from "@/helpers"
import { getBindableProperties } from "@/dataBinding"

function reduceBy<TItem extends {}, TKey extends keyof TItem>(
key: TKey,
list: TItem[]
) {
): Record<string, any> {
return list.reduce(
(result, item) => ({
...result,
Expand All @@ -31,6 +32,7 @@ const validationKeyByType: Record<UIDatasourceType, string | null> = {
viewV2: "id",
query: "_id",
custom: null,
link: "rowId",
}

export const screenComponentErrors = derived(
Expand Down Expand Up @@ -59,8 +61,21 @@ export const screenComponentErrors = derived(
if (!validationKey) {
continue
}

const componentBindings = getBindableProperties(
$selectedScreen,
component._id
)

const componentDatasources = {
...reduceBy(
"rowId",
bindings.extractRelationships(componentBindings)
),
}

const resourceId = componentSettings[validationKey]
if (!datasources[resourceId]) {
if (!{ ...datasources, ...componentDatasources }[resourceId]) {
const friendlyTypeName = friendlyNameByType[type] ?? type
result[component._id!] = [
`The ${friendlyTypeName} named "${label}" could not be found`,
Expand Down
15 changes: 15 additions & 0 deletions packages/types/src/ui/bindings/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ export type InsertAtPositionFn = (_: {
value: string
cursor?: { anchor: number }
}) => void

export interface UIBinding {
tableId?: string
fieldSchema?: {
name: string
tableId: string
type: string
subtype?: string
prefixKeys?: string
}
component?: string
providerId: string
readableBinding?: string
runtimeBinding?: string
}
8 changes: 7 additions & 1 deletion packages/types/src/ui/datasource.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export type UIDatasourceType = "table" | "view" | "viewV2" | "query" | "custom"
export type UIDatasourceType =
| "table"
| "view"
| "viewV2"
| "query"
| "custom"
| "link"

0 comments on commit 303beec

Please sign in to comment.