Merge pull request #1932 from Budibase/fix/sql-relationship-validation

Adding validation to SQL relationship modal
This commit is contained in:
Michael Drury 2021-07-06 15:55:41 +01:00 committed by GitHub
commit df97a2572b
5 changed files with 115 additions and 19 deletions

View File

@ -1,8 +1,16 @@
<script> <script>
import { RelationshipTypes } from "constants/backend" import { RelationshipTypes } from "constants/backend"
import { Button, Input, ModalContent, Select, Detail } from "@budibase/bbui" import {
Button,
Input,
ModalContent,
Select,
Detail,
Body,
} from "@budibase/bbui"
import { tables } from "stores/backend" import { tables } from "stores/backend"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
import { writable } from "svelte/store"
export let save export let save
export let datasource export let datasource
@ -14,16 +22,68 @@
let originalFromName = fromRelationship.name, let originalFromName = fromRelationship.name,
originalToName = toRelationship.name originalToName = toRelationship.name
function isValid(relationship) { function inSchema(table, prop, ogName) {
if ( if (!table || !prop || prop === ogName) {
relationship.relationshipType === RelationshipTypes.MANY_TO_MANY &&
!relationship.through
) {
return false return false
} }
return ( const keys = Object.keys(table.schema).map(key => key.toLowerCase())
relationship.name && relationship.tableId && relationship.relationshipType return keys.indexOf(prop.toLowerCase()) !== -1
) }
const touched = writable({})
function checkForErrors(
fromTable,
toTable,
throughTable,
fromRelate,
toRelate
) {
const isMany =
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
const tableNotSet = "Please specify a table"
const errors = {}
if ($touched.from && !fromTable) {
errors.from = tableNotSet
}
if ($touched.to && !toTable) {
errors.to = tableNotSet
}
if ($touched.through && isMany && !fromRelate.through) {
errors.through = tableNotSet
}
if ($touched.foreign && !isMany && !fromRelate.fieldName) {
errors.foreign = "Please pick the foreign key"
}
const colNotSet = "Please specify a column name"
if ($touched.fromCol && !fromRelate.name) {
errors.fromCol = colNotSet
}
if ($touched.toCol && !toRelate.name) {
errors.toCol = colNotSet
}
// currently don't support relationships back onto the table itself, needs to relate out
const tableError = "From/to/through tables must be different"
if (fromTable && (fromTable === toTable || fromTable === throughTable)) {
errors.from = tableError
}
if (toTable && (toTable === fromTable || toTable === throughTable)) {
errors.to = tableError
}
if (
throughTable &&
(throughTable === fromTable || throughTable === toTable)
) {
errors.through = tableError
}
const colError = "Column name cannot be an existing column"
if (inSchema(fromTable, fromRelate.name, originalFromName)) {
errors.fromCol = colError
}
if (inSchema(toTable, toRelate.name, originalToName)) {
errors.toCol = colError
}
return errors
} }
$: tableOptions = plusTables.map(table => ({ $: tableOptions = plusTables.map(table => ({
@ -33,7 +93,15 @@
$: fromTable = plusTables.find(table => table._id === toRelationship?.tableId) $: fromTable = plusTables.find(table => table._id === toRelationship?.tableId)
$: toTable = plusTables.find(table => table._id === fromRelationship?.tableId) $: toTable = plusTables.find(table => table._id === fromRelationship?.tableId)
$: through = plusTables.find(table => table._id === fromRelationship?.through) $: through = plusTables.find(table => table._id === fromRelationship?.through)
$: valid = toTable && fromTable && isValid(fromRelationship) $: errors = checkForErrors(
fromTable,
toTable,
through,
fromRelationship,
toRelationship
)
$: valid =
Object.keys(errors).length === 0 && Object.keys($touched).length !== 0
$: linkTable = through || toTable $: linkTable = through || toTable
$: relationshipTypes = [ $: relationshipTypes = [
{ {
@ -155,31 +223,55 @@
<Select <Select
label="Select from table" label="Select from table"
options={tableOptions} options={tableOptions}
on:change={() => ($touched.from = true)}
bind:error={errors.from}
bind:value={toRelationship.tableId} bind:value={toRelationship.tableId}
/> />
<Select <Select
label={"Select to table"} label={"Select to table"}
options={tableOptions} options={tableOptions}
on:change={() => ($touched.to = true)}
bind:error={errors.to}
bind:value={fromRelationship.tableId} bind:value={fromRelationship.tableId}
/> />
{#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY} {#if fromRelationship?.relationshipType === RelationshipTypes.MANY_TO_MANY}
<Select <Select
label={"Through"} label={"Through"}
options={tableOptions} options={tableOptions}
on:change={() => ($touched.through = true)}
bind:error={errors.through}
bind:value={fromRelationship.through} bind:value={fromRelationship.through}
/> />
{:else if toTable} {:else if fromRelationship?.relationshipType && toTable}
<Select <Select
label={`Foreign Key (${toTable?.name})`} label={`Foreign Key (${toTable?.name})`}
options={Object.keys(toTable?.schema)} options={Object.keys(toTable?.schema).filter(
field => toTable?.primary.indexOf(field) === -1
)}
on:change={() => ($touched.foreign = true)}
bind:error={errors.foreign}
bind:value={fromRelationship.fieldName} bind:value={fromRelationship.fieldName}
/> />
{/if} {/if}
<div class="headings"> <div class="headings">
<Detail>Column names</Detail> <Detail>Column names</Detail>
</div> </div>
<Input label="From table column" bind:value={fromRelationship.name} /> <Body>
<Input label="To table column" bind:value={toRelationship.name} /> Budibase manages SQL relationships as a new column in the table, please
provide a name for these columns.
</Body>
<Input
on:blur={() => ($touched.fromCol = true)}
bind:error={errors.fromCol}
label="From table column"
bind:value={fromRelationship.name}
/>
<Input
on:blur={() => ($touched.toCol = true)}
bind:error={errors.toCol}
label="To table column"
bind:value={toRelationship.name}
/>
<div slot="footer"> <div slot="footer">
{#if originalFromName != null} {#if originalFromName != null}
<Button warning text on:click={deleteRelationship}>Delete</Button> <Button warning text on:click={deleteRelationship}>Delete</Button>

View File

@ -162,7 +162,7 @@ module External {
manyRelationships: ManyRelationship[] = [] manyRelationships: ManyRelationship[] = []
for (let [key, field] of Object.entries(table.schema)) { for (let [key, field] of Object.entries(table.schema)) {
// if set already, or not set just skip it // if set already, or not set just skip it
if (!row[key] || newRow[key]) { if (!row[key] || newRow[key] || field.autocolumn) {
continue continue
} }
// if its not a link then just copy it over // if its not a link then just copy it over

View File

@ -163,7 +163,7 @@ module MySQLModule {
) )
for (let column of descResp) { for (let column of descResp) {
const columnName = column.Field const columnName = column.Field
if (column.Key === "PRI") { if (column.Key === "PRI" && primaryKeys.indexOf(column.Key) === -1) {
primaryKeys.push(columnName) primaryKeys.push(columnName)
} }
const constraints = { const constraints = {

View File

@ -147,7 +147,11 @@ module PostgresModule {
if (!tableKeys[tableName]) { if (!tableKeys[tableName]) {
tableKeys[tableName] = [] tableKeys[tableName] = []
} }
tableKeys[tableName].push(table.column_name || table.primary_key) const key = table.column_name || table.primary_key
// only add the unique keys
if (key && tableKeys[tableName].indexOf(key) === -1) {
tableKeys[tableName].push(key)
}
} }
} catch (err) { } catch (err) {
tableKeys = {} tableKeys = {}

View File

@ -17,7 +17,7 @@ describe("Postgres Integration", () => {
it("calls the create method with the correct params", async () => { it("calls the create method with the correct params", async () => {
const sql = "insert into users (name, age) values ('Joe', 123);" const sql = "insert into users (name, age) values ('Joe', 123);"
const response = await config.integration.create({ await config.integration.create({
sql sql
}) })
expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) expect(pg.queryMock).toHaveBeenCalledWith(sql, {})
@ -25,7 +25,7 @@ describe("Postgres Integration", () => {
it("calls the read method with the correct params", async () => { it("calls the read method with the correct params", async () => {
const sql = "select * from users;" const sql = "select * from users;"
const response = await config.integration.read({ await config.integration.read({
sql sql
}) })
expect(pg.queryMock).toHaveBeenCalledWith(sql, {}) expect(pg.queryMock).toHaveBeenCalledWith(sql, {})