diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index e82c55679a..25ad67b52e 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -16,8 +16,8 @@ export let value = defaultValue || (meta.type === "boolean" ? false : "") export let readonly - $: type = meta.type - $: label = capitalise(meta.name) + $: type = meta?.type + $: label = meta.name ? capitalise(meta.name) : "" {#if type === "options"} diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte index 54bbb7fadf..9af07fe28c 100644 --- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte +++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte @@ -31,6 +31,9 @@ const AUTO_TYPE = "auto" const FORMULA_TYPE = FIELDS.FORMULA.type const LINK_TYPE = FIELDS.LINK.type + const STRING_TYPE = FIELDS.STRING.type + const NUMBER_TYPE = FIELDS.NUMBER.type + const dispatch = createEventDispatcher() const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev", "tableId"] const { hide } = getContext(Context.Modal) @@ -55,6 +58,7 @@ let confirmDeleteDialog let deletion + $: checkConstraints(field) $: tableOptions = $tables.list.filter( opt => opt._id !== $tables.draft._id && opt.type === table.type ) @@ -180,23 +184,26 @@ } const thisName = truncate(table.name, { length: 14 }), linkName = truncate(linkTable.name, { length: 14 }) - return [ + const options = [ { name: `Many ${thisName} rows → many ${linkName} rows`, alt: `Many ${table.name} rows → many ${linkTable.name} rows`, value: RelationshipTypes.MANY_TO_MANY, }, - { - name: `One ${linkName} row → many ${thisName} rows`, - alt: `One ${linkTable.name} rows → many ${table.name} rows`, - value: RelationshipTypes.ONE_TO_MANY, - }, { name: `One ${thisName} row → many ${linkName} rows`, alt: `One ${table.name} rows → many ${linkTable.name} rows`, value: RelationshipTypes.MANY_TO_ONE, }, ] + if (!external) { + options.push({ + name: `One ${linkName} row → many ${thisName} rows`, + alt: `One ${linkTable.name} rows → many ${table.name} rows`, + value: RelationshipTypes.ONE_TO_MANY, + }) + } + return options } function getAllowedTypes() { @@ -219,6 +226,24 @@ ] } } + + function checkConstraints(fieldToCheck) { + // most types need this, just make sure its always present + if (fieldToCheck && !fieldToCheck.constraints) { + fieldToCheck.constraints = {} + } + // some string types may have been built by server, may not always have constraints + if (fieldToCheck.type === STRING_TYPE && !fieldToCheck.constraints.length) { + fieldToCheck.constraints.length = {} + } + // some number types made server-side will be missing constraints + if ( + fieldToCheck.type === NUMBER_TYPE && + !fieldToCheck.constraints.numericality + ) { + fieldToCheck.constraints.numericality = {} + } + } {/if} - {#if canBeSearched} + {#if canBeSearched && !external}
table.name} getOptionValue={table => table._id} /> - {#if relationshipOptions && relationshipOptions.length > 0 && !external} + {#if relationshipOptions && relationshipOptions.length > 0} {/if}
-
- - New table - -
{#each plusTables as table}
onClickTable(table)}>

{table.name}

@@ -212,6 +206,9 @@

{/each} +
+ +
{#if plusTables?.length !== 0} @@ -343,7 +340,6 @@ } .add-table { - margin-right: 0; - margin-left: auto; + margin-top: var(--spacing-m); } diff --git a/packages/server/src/api/controllers/datasource.js b/packages/server/src/api/controllers/datasource.js index 604fee004e..6f33faf3d4 100644 --- a/packages/server/src/api/controllers/datasource.js +++ b/packages/server/src/api/controllers/datasource.js @@ -152,6 +152,16 @@ const buildSchemaHelper = async datasource => { await connector.buildSchema(datasource._id, datasource.entities) datasource.entities = connector.tables + // make sure they all have a display name selected + for (let entity of Object.values(datasource.entities)) { + if (entity.primaryDisplay) { + continue + } + entity.primaryDisplay = Object.values(entity.schema).find( + schema => !schema.autocolumn + ).name + } + const errors = connector.schemaErrors let error = null if (errors && Object.keys(errors).length > 0) { diff --git a/packages/server/src/api/controllers/table/external.js b/packages/server/src/api/controllers/table/external.js index 5fe923cb96..d0ec01a1d8 100644 --- a/packages/server/src/api/controllers/table/external.js +++ b/packages/server/src/api/controllers/table/external.js @@ -36,6 +36,35 @@ async function makeTableRequest( return makeExternalQuery(datasource, json) } +function cleanupRelationships(table, tables, oldTable = null) { + const tableToIterate = oldTable ? oldTable : table + // clean up relationships in couch table schemas + for (let [key, schema] of Object.entries(tableToIterate.schema)) { + if ( + schema.type === FieldTypes.LINK && + (!oldTable || table.schema[key] == null) + ) { + const relatedTable = Object.values(tables).find( + table => table._id === schema.tableId + ) + const foreignKey = schema.foreignKey + if (!relatedTable || !foreignKey) { + continue + } + for (let [relatedKey, relatedSchema] of Object.entries( + relatedTable.schema + )) { + if ( + relatedSchema.type === FieldTypes.LINK && + relatedSchema.fieldName === foreignKey + ) { + delete relatedSchema[relatedKey] + } + } + } + } +} + function getDatasourceId(table) { if (!table) { throw "No table supplied" @@ -57,6 +86,14 @@ function generateRelatedSchema(linkColumn, table) { return relatedSchema } +function oneToManyRelationshipNeedsSetup(column) { + return ( + column.type === FieldTypes.LINK && + column.relationshipType === RelationshipTypes.ONE_TO_MANY && + !column.foreignKey + ) +} + exports.save = async function (ctx) { const appId = ctx.appId const table = ctx.request.body @@ -81,7 +118,7 @@ exports.save = async function (ctx) { // check if relations need setup for (let schema of Object.values(tableToSave.schema)) { // TODO: many to many handling - if (schema.type === FieldTypes.LINK) { + if (oneToManyRelationshipNeedsSetup(schema)) { const relatedTable = Object.values(tables).find( table => table._id === schema.tableId ) @@ -104,6 +141,8 @@ exports.save = async function (ctx) { } } + cleanupRelationships(tableToSave, tables, oldTable) + const operation = oldTable ? DataSourceOperation.UPDATE_TABLE : DataSourceOperation.CREATE_TABLE @@ -128,6 +167,8 @@ exports.destroy = async function (ctx) { const operation = DataSourceOperation.DELETE_TABLE await makeTableRequest(datasource, operation, tableToDelete, tables) + cleanupRelationships(tableToDelete, tables) + delete datasource.entities[tableToDelete.name] await db.put(datasource) diff --git a/packages/server/src/integrations/base/sqlTable.ts b/packages/server/src/integrations/base/sqlTable.ts index f2e727ce62..87d0f1e3b3 100644 --- a/packages/server/src/integrations/base/sqlTable.ts +++ b/packages/server/src/integrations/base/sqlTable.ts @@ -4,7 +4,7 @@ import { Operation, QueryJson } from "../../definitions/datasource" import { breakExternalTableId } from "../utils" import SchemaBuilder = Knex.SchemaBuilder import CreateTableBuilder = Knex.CreateTableBuilder -const { FieldTypes } = require("../../constants") +const { FieldTypes, RelationshipTypes } = require("../../constants") function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record, oldTable: null | Table = null) { let primaryKey = table && table.primary ? table.primary[0] : null @@ -12,6 +12,8 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record if (primaryKey && !oldTable) { schema.increments(primaryKey).primary() } + + // check if any columns need added const foreignKeys = Object.values(table.schema).map(col => col.foreignKey) for (let [key, column] of Object.entries(table.schema)) { // skip things that are already correct @@ -38,6 +40,10 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record schema.json(key) break case FieldTypes.LINK: + // this side of the relationship doesn't need any SQL work + if (column.relationshipType === RelationshipTypes.MANY_TO_ONE) { + break + } if (!column.foreignKey || !column.tableId) { throw "Invalid relationship schema" } @@ -51,6 +57,17 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record schema.foreign(column.foreignKey).references(`${tableName}.${relatedTable.primary[0]}`) } } + + // need to check if any columns have been deleted + if (oldTable) { + const deletedColumns = Object.entries(oldTable.schema) + .filter(([key, schema]) => schema.type !== FieldTypes.LINK && table.schema[key] == null) + .map(([key]) => key) + deletedColumns.forEach(key => { + schema.dropColumn(key) + }) + } + return schema }