1
0
Fork 0
mirror of synced 2024-08-18 19:41:30 +12:00

Re-working the error handling for the SQL relationship modal, as well as adding some better defaults for the majority of the options to make the UI a bit easier to use.

This commit is contained in:
mike12345567 2023-02-01 19:09:36 +00:00
parent 50b3c06dbd
commit 3a51933801
2 changed files with 208 additions and 141 deletions

View file

@ -10,17 +10,17 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { RelationshipErrorChecker } from "./relationshipErrors"
import { onMount } from "svelte"
export let save export let save
export let datasource export let datasource
export let plusTables = [] export let plusTables = []
export let fromRelationship = {} export let fromRelationship = {}
export let toRelationship = {} export let toRelationship = {}
export let selectedFromTable
export let close export let close
const colNotSet = "Please specify a column name"
const relationshipAlreadyExists =
"A relationship between these tables already exists."
const relationshipTypes = [ const relationshipTypes = [
{ {
label: "One to Many", label: "One to Many",
@ -42,8 +42,11 @@
) )
let tableOptions let tableOptions
let errorChecker = new RelationshipErrorChecker(
invalidThroughTable,
relationshipExists
)
let errors = {} let errors = {}
let hasClickedSave = !!fromRelationship.relationshipType
let fromPrimary, let fromPrimary,
fromForeign, fromForeign,
fromTable, fromTable,
@ -51,12 +54,19 @@
throughTable, throughTable,
fromColumn, fromColumn,
toColumn toColumn
let fromId, toId, throughId, throughToKey, throughFromKey let fromId = selectedFromTable?._id,
toId,
throughId,
throughToKey,
throughFromKey
let isManyToMany, isManyToOne, relationshipType let isManyToMany, isManyToOne, relationshipType
let hasValidated = false
$: { $: {
if (!fromPrimary) { if (!fromPrimary) {
fromPrimary = fromRelationship.foreignKey fromPrimary = fromRelationship.foreignKey
}
if (!fromForeign) {
fromForeign = toRelationship.foreignKey fromForeign = toRelationship.foreignKey
} }
if (!fromColumn && !errors.fromColumn) { if (!fromColumn && !errors.fromColumn) {
@ -77,7 +87,8 @@
throughToKey = fromRelationship.throughTo throughToKey = fromRelationship.throughTo
} }
if (!relationshipType) { if (!relationshipType) {
relationshipType = fromRelationship.relationshipType relationshipType =
fromRelationship.relationshipType || RelationshipTypes.MANY_TO_ONE
} }
} }
@ -85,7 +96,7 @@
label: table.name, label: table.name,
value: table._id, value: table._id,
})) }))
$: valid = getErrorCount(errors) === 0 || !hasClickedSave $: valid = getErrorCount(errors) === 0 && allRequiredAttributesSet()
$: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY $: isManyToMany = relationshipType === RelationshipTypes.MANY_TO_MANY
$: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE $: isManyToOne = relationshipType === RelationshipTypes.MANY_TO_ONE
@ -95,10 +106,20 @@
$: toRelationship.relationshipType = fromRelationship?.relationshipType $: toRelationship.relationshipType = fromRelationship?.relationshipType
const getErrorCount = errors => function getErrorCount(errors) {
Object.entries(errors) return Object.entries(errors)
.filter(entry => !!entry[1]) .filter(entry => !!entry[1])
.map(entry => entry[0]).length .map(entry => entry[0]).length
}
function allRequiredAttributesSet() {
const base = fromTable && toTable && fromColumn && toColumn
if (isManyToOne) {
return base && fromPrimary && fromForeign
} else {
return base && throughTable && throughFromKey && throughToKey
}
}
function invalidThroughTable() { function invalidThroughTable() {
// need to know the foreign key columns to check error // need to know the foreign key columns to check error
@ -118,93 +139,48 @@
} }
function validate() { function validate() {
const isMany = relationshipType === RelationshipTypes.MANY_TO_MANY if (!allRequiredAttributesSet() && !hasValidated) {
const tableNotSet = "Please specify a table" return
const foreignKeyNotSet = "Please pick a foreign key" }
hasValidated = true
errorChecker.setType(relationshipType)
const errObj = {} const errObj = {}
if (!relationshipType) { errObj.relationshipType = errorChecker.relationshipTypeSet(relationshipType)
errObj.relationshipType = "Please specify a relationship type" errObj.fromTable = errorChecker.tableSet(fromTable)
} errObj.toTable = errorChecker.tableSet(toTable)
if (!fromTable) { errObj.throughTable = errorChecker.throughTableSet(throughTable)
errObj.fromTable = tableNotSet errObj.throughFromKey = errorChecker.manyForeignKeySet(throughFromKey)
} errObj.throughToKey = errorChecker.manyForeignKeySet(throughToKey)
if (!toTable) { errObj.throughTable = errorChecker.throughIsNullable()
errObj.toTable = tableNotSet errObj.fromForeign = errorChecker.foreignKeySet(fromForeign)
} errObj.fromPrimary = errorChecker.foreignKeySet(fromPrimary)
if (isMany && !throughTable) { errObj.fromTable = errorChecker.doesRelationshipExists()
errObj.throughTable = tableNotSet errObj.toTable = errorChecker.doesRelationshipExists()
}
if (isMany && !throughFromKey) {
errObj.throughFromKey = foreignKeyNotSet
}
if (isMany && !throughToKey) {
errObj.throughToKey = foreignKeyNotSet
}
if (invalidThroughTable()) {
errObj.throughTable =
"Ensure non-key columns are nullable or auto-generated"
}
if (!isMany && !fromForeign) {
errObj.fromForeign = foreignKeyNotSet
}
if (!fromColumn) {
errObj.fromColumn = colNotSet
}
if (!toColumn) {
errObj.toColumn = colNotSet
}
if (!isMany && !fromPrimary) {
errObj.fromPrimary = "Please pick the primary key"
}
if (isMany && relationshipExists()) {
errObj.fromTable = relationshipAlreadyExists
errObj.toTable = relationshipAlreadyExists
}
// currently don't support relationships back onto the table itself, needs to relate out // currently don't support relationships back onto the table itself, needs to relate out
const tableError = "From/to/through tables must be different" errObj.fromTable = errorChecker.differentTables(fromId, toId, throughId)
if (fromTable && (fromTable === toTable || fromTable === throughTable)) { errObj.toTable = errorChecker.differentTables(toId, fromId, throughId)
errObj.fromTable = tableError errObj.throughTable = errorChecker.differentTables(throughId, fromId, toId)
} errObj.fromColumn = errorChecker.columnBeingUsed(
if (toTable && (toTable === fromTable || toTable === throughTable)) { toTable,
errObj.toTable = tableError fromColumn,
} originalFromColumnName
if ( )
throughTable && errObj.toColumn = errorChecker.columnBeingUsed(
(throughTable === fromTable || throughTable === toTable) fromTable,
) { toColumn,
errObj.throughTable = tableError originalToColumnName
} )
const colError = "Column name cannot be an existing column" errObj.fromForeign = errorChecker.typeMismatch(
if (isColumnNameBeingUsed(toTable, fromColumn, originalFromColumnName)) { fromTable,
errObj.fromColumn = colError toTable,
} fromPrimary,
if (isColumnNameBeingUsed(fromTable, toColumn, originalToColumnName)) { fromForeign
errObj.toColumn = colError )
} console.log(errObj)
let fromType, toType
if (fromPrimary && fromForeign) {
fromType = fromTable?.schema[fromPrimary]?.type
toType = toTable?.schema[fromForeign]?.type
}
if (fromType && toType && fromType !== toType) {
errObj.fromForeign =
"Column type of the foreign key must match the primary key"
}
errors = errObj errors = errObj
return getErrorCount(errors) === 0 return getErrorCount(errors) === 0
} }
function isColumnNameBeingUsed(table, columnName, originalName) {
if (!table || !columnName || columnName === originalName) {
return false
}
const keys = Object.keys(table.schema).map(key => key.toLowerCase())
return keys.indexOf(columnName.toLowerCase()) !== -1
}
function buildRelationships() { function buildRelationships() {
const id = Helpers.uuid() const id = Helpers.uuid()
//Map temporary variables //Map temporary variables
@ -320,7 +296,6 @@
} }
async function saveRelationship() { async function saveRelationship() {
hasClickedSave = true
if (!validate()) { if (!validate()) {
return false return false
} }
@ -342,6 +317,20 @@
await tables.fetch() await tables.fetch()
close() close()
} }
function changed(fn) {
if (fn && typeof fn === "function") {
fn()
}
validate()
}
onMount(() => {
if (selectedFromTable) {
fromColumn = selectedFromTable.name
fromPrimary = selectedFromTable?.primary[0] || null
}
})
</script> </script>
<ModalContent <ModalContent
@ -355,34 +344,32 @@
options={relationshipTypes} options={relationshipTypes}
bind:value={relationshipType} bind:value={relationshipType}
bind:error={errors.relationshipType} bind:error={errors.relationshipType}
on:change={() => (errors.relationshipType = null)} on:change={changed}
/> />
<div class="headings"> <div class="headings">
<Detail>Tables</Detail> <Detail>Tables</Detail>
</div> </div>
<Select {#if !selectedFromTable}
label="Select from table" <Select
options={tableOptions} label="Select from table"
bind:value={fromId} options={tableOptions}
bind:error={errors.fromTable} bind:value={fromId}
on:change={e => { bind:error={errors.fromTable}
fromColumn = tableOptions.find(opt => opt.value === e.detail)?.label || "" on:change={e =>
if (errors.fromTable === relationshipAlreadyExists) { changed(() => {
errors.toColumn = null const table = plusTables.find(tbl => tbl._id === e.detail)
} fromColumn = table?.name || ""
errors.fromTable = null fromPrimary = table?.primary?.[0]
errors.fromColumn = null })}
errors.toTable = null />
errors.throughTable = null {/if}
}}
/>
{#if isManyToOne && fromTable} {#if isManyToOne && fromTable}
<Select <Select
label={`Primary Key (${fromTable.name})`} label={`Primary Key (${fromTable.name})`}
options={Object.keys(fromTable.schema)} options={Object.keys(fromTable.schema)}
bind:value={fromPrimary} bind:value={fromPrimary}
bind:error={errors.fromPrimary} bind:error={errors.fromPrimary}
on:change={() => (errors.fromPrimary = null)} on:change={changed}
/> />
{/if} {/if}
<Select <Select
@ -390,16 +377,12 @@
options={tableOptions} options={tableOptions}
bind:value={toId} bind:value={toId}
bind:error={errors.toTable} bind:error={errors.toTable}
on:change={e => { on:change={e =>
toColumn = tableOptions.find(opt => opt.value === e.detail)?.label || "" changed(() => {
if (errors.toTable === relationshipAlreadyExists) { const table = plusTables.find(tbl => tbl._id === e.detail)
errors.fromColumn = null toColumn = table.name || ""
} fromForeign = null
errors.toTable = null })}
errors.toColumn = null
errors.fromTable = null
errors.throughTable = null
}}
/> />
{#if isManyToMany} {#if isManyToMany}
<Select <Select
@ -407,11 +390,11 @@
options={tableOptions} options={tableOptions}
bind:value={throughId} bind:value={throughId}
bind:error={errors.throughTable} bind:error={errors.throughTable}
on:change={() => { on:change={() =>
errors.fromTable = null changed(() => {
errors.toTable = null throughToKey = null
errors.throughTable = null throughFromKey = null
}} })}
/> />
{#if fromTable && toTable && throughTable} {#if fromTable && toTable && throughTable}
<Select <Select
@ -419,24 +402,24 @@
options={Object.keys(throughTable?.schema)} options={Object.keys(throughTable?.schema)}
bind:value={throughToKey} bind:value={throughToKey}
bind:error={errors.throughToKey} bind:error={errors.throughToKey}
on:change={e => { on:change={e =>
if (throughFromKey === e.detail) { changed(() => {
throughFromKey = null if (throughFromKey === e.detail) {
} throughFromKey = null
errors.throughToKey = null }
}} })}
/> />
<Select <Select
label={`Foreign Key (${toTable?.name})`} label={`Foreign Key (${toTable?.name})`}
options={Object.keys(throughTable?.schema)} options={Object.keys(throughTable?.schema)}
bind:value={throughFromKey} bind:value={throughFromKey}
bind:error={errors.throughFromKey} bind:error={errors.throughFromKey}
on:change={e => { on:change={e =>
if (throughToKey === e.detail) { changed(() => {
throughToKey = null if (throughToKey === e.detail) {
} throughToKey = null
errors.throughFromKey = null }
}} })}
/> />
{/if} {/if}
{:else if isManyToOne && toTable} {:else if isManyToOne && toTable}
@ -445,7 +428,7 @@
options={Object.keys(toTable?.schema)} options={Object.keys(toTable?.schema)}
bind:value={fromForeign} bind:value={fromForeign}
bind:error={errors.fromForeign} bind:error={errors.fromForeign}
on:change={() => (errors.fromForeign = null)} on:change={changed}
/> />
{/if} {/if}
<div class="headings"> <div class="headings">
@ -459,15 +442,13 @@
label="From table column" label="From table column"
bind:value={fromColumn} bind:value={fromColumn}
bind:error={errors.fromColumn} bind:error={errors.fromColumn}
on:change={e => { on:change={changed}
errors.fromColumn = e.detail?.length > 0 ? null : colNotSet
}}
/> />
<Input <Input
label="To table column" label="To table column"
bind:value={toColumn} bind:value={toColumn}
bind:error={errors.toColumn} bind:error={errors.toColumn}
on:change={e => (errors.toColumn = e.detail?.length > 0 ? null : colNotSet)} on:change={changed}
/> />
<div slot="footer"> <div slot="footer">
{#if originalFromColumnName != null} {#if originalFromColumnName != null}

View file

@ -0,0 +1,86 @@
import { RelationshipTypes } from "constants/backend"
export const typeMismatch =
"Column type of the foreign key must match the primary key"
export const columnCantExist = "Column name cannot be an existing column"
export const mustBeDifferentTables = "From/to/through tables must be different"
export const primaryKeyNotSet = "Please pick the primary key"
export const throughNotNullable =
"Ensure non-key columns are nullable or auto-generated"
export const noRelationshipType = "Please specify a relationship type"
export const tableNotSet = "Please specify a table"
export const foreignKeyNotSet = "Please pick a foreign key"
export const relationshipAlreadyExists =
"A relationship between these tables already exists"
function isColumnNameBeingUsed(table, columnName, originalName) {
if (!table || !columnName || columnName === originalName) {
return false
}
const keys = Object.keys(table.schema).map(key => key.toLowerCase())
return keys.indexOf(columnName.toLowerCase()) !== -1
}
export class RelationshipErrorChecker {
constructor(invalidThroughTableFn, relationshipExistsFn) {
this.invalidThroughTable = invalidThroughTableFn
this.relationshipExists = relationshipExistsFn
}
setType(type) {
this.type = type
}
isMany() {
return this.type === RelationshipTypes.MANY_TO_MANY
}
relationshipTypeSet(type) {
return !type ? noRelationshipType : null
}
tableSet(table) {
return !table ? tableNotSet : null
}
throughTableSet(table) {
return this.isMany() && !table ? tableNotSet : null
}
manyForeignKeySet(key) {
return this.isMany() && !key ? foreignKeyNotSet : null
}
foreignKeySet(key) {
return !this.isMany() && !key ? foreignKeyNotSet : null
}
throughIsNullable() {
return this.invalidThroughTable() ? throughNotNullable : null
}
doesRelationshipExists() {
return this.isMany() && this.relationshipExists()
? relationshipAlreadyExists
: null
}
differentTables(table1, table2, table3) {
// currently don't support relationships back onto the table itself, needs to relate out
const error = table1 && (table1 === table2 || (table3 && table1 === table3))
return error ? mustBeDifferentTables : null
}
columnBeingUsed(table, column, ogName) {
return isColumnNameBeingUsed(table, column, ogName) ? columnCantExist : null
}
typeMismatch(fromTable, toTable, primary, foreign) {
let fromType, toType
if (primary && foreign) {
fromType = fromTable?.schema[primary]?.type
toType = toTable?.schema[foreign]?.type
}
return fromType && toType && fromType !== toType ? typeMismatch : null
}
}