Merge pull request #1932 from Budibase/fix/sql-relationship-validation
Adding validation to SQL relationship modal
This commit is contained in:
commit
df97a2572b
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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, {})
|
||||||
|
|
Loading…
Reference in New Issue