Adding many to many support, generating junction table and setting up constraints.
This commit is contained in:
parent
a94376ce43
commit
f071cc5219
|
@ -93,10 +93,6 @@
|
||||||
if (field.type === AUTO_TYPE) {
|
if (field.type === AUTO_TYPE) {
|
||||||
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
field = buildAutoColumn($tables.draft.name, field.name, field.subtype)
|
||||||
}
|
}
|
||||||
// for now you can't create other options, just many to many for SQL
|
|
||||||
if (field.type === LINK_TYPE && external) {
|
|
||||||
field.relationshipType = RelationshipTypes.ONE_TO_MANY
|
|
||||||
}
|
|
||||||
await tables.saveField({
|
await tables.saveField({
|
||||||
originalName,
|
originalName,
|
||||||
field,
|
field,
|
||||||
|
@ -184,26 +180,23 @@
|
||||||
}
|
}
|
||||||
const thisName = truncate(table.name, { length: 14 }),
|
const thisName = truncate(table.name, { length: 14 }),
|
||||||
linkName = truncate(linkTable.name, { length: 14 })
|
linkName = truncate(linkTable.name, { length: 14 })
|
||||||
const options = [
|
return [
|
||||||
{
|
{
|
||||||
name: `Many ${thisName} rows → many ${linkName} rows`,
|
name: `Many ${thisName} rows → many ${linkName} rows`,
|
||||||
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
alt: `Many ${table.name} rows → many ${linkTable.name} rows`,
|
||||||
value: RelationshipTypes.MANY_TO_MANY,
|
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`,
|
name: `One ${thisName} row → many ${linkName} rows`,
|
||||||
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
alt: `One ${table.name} rows → many ${linkTable.name} rows`,
|
||||||
value: RelationshipTypes.MANY_TO_ONE,
|
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() {
|
function getAllowedTypes() {
|
||||||
|
|
|
@ -157,9 +157,12 @@ const buildSchemaHelper = async datasource => {
|
||||||
if (entity.primaryDisplay) {
|
if (entity.primaryDisplay) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entity.primaryDisplay = Object.values(entity.schema).find(
|
const notAutoColumn = Object.values(entity.schema).find(
|
||||||
schema => !schema.autocolumn
|
schema => !schema.autocolumn
|
||||||
).name
|
)
|
||||||
|
if (notAutoColumn) {
|
||||||
|
entity.primaryDisplay = notAutoColumn.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = connector.schemaErrors
|
const errors = connector.schemaErrors
|
||||||
|
|
|
@ -3,7 +3,12 @@ const {
|
||||||
buildExternalTableId,
|
buildExternalTableId,
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
} = require("../../../integrations/utils")
|
} = require("../../../integrations/utils")
|
||||||
const { getTable } = require("./utils")
|
const {
|
||||||
|
getTable,
|
||||||
|
generateForeignKey,
|
||||||
|
generateJunctionTableName,
|
||||||
|
foreignKeyStructure,
|
||||||
|
} = require("./utils")
|
||||||
const {
|
const {
|
||||||
DataSourceOperation,
|
DataSourceOperation,
|
||||||
FieldTypes,
|
FieldTypes,
|
||||||
|
@ -58,7 +63,7 @@ function cleanupRelationships(table, tables, oldTable = null) {
|
||||||
relatedSchema.type === FieldTypes.LINK &&
|
relatedSchema.type === FieldTypes.LINK &&
|
||||||
relatedSchema.fieldName === foreignKey
|
relatedSchema.fieldName === foreignKey
|
||||||
) {
|
) {
|
||||||
delete relatedSchema[relatedKey]
|
delete relatedTable.schema[relatedKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,23 +80,78 @@ function getDatasourceId(table) {
|
||||||
return breakExternalTableId(table._id).datasourceId
|
return breakExternalTableId(table._id).datasourceId
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRelatedSchema(linkColumn, table) {
|
function otherRelationshipType(type) {
|
||||||
// generate column for other table
|
if (type === RelationshipTypes.MANY_TO_MANY) {
|
||||||
const relatedSchema = cloneDeep(linkColumn)
|
return RelationshipTypes.MANY_TO_MANY
|
||||||
relatedSchema.fieldName = linkColumn.foreignKey
|
}
|
||||||
relatedSchema.foreignKey = linkColumn.fieldName
|
return type === RelationshipTypes.ONE_TO_MANY
|
||||||
relatedSchema.relationshipType = RelationshipTypes.MANY_TO_ONE
|
? RelationshipTypes.MANY_TO_ONE
|
||||||
relatedSchema.tableId = table._id
|
: RelationshipTypes.ONE_TO_MANY
|
||||||
delete relatedSchema.main
|
|
||||||
return relatedSchema
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function oneToManyRelationshipNeedsSetup(column) {
|
function generateManyLinkSchema(datasource, column, table, relatedTable) {
|
||||||
return (
|
const primary = table.name + table.primary[0]
|
||||||
column.type === FieldTypes.LINK &&
|
const relatedPrimary = relatedTable.name + relatedTable.primary[0]
|
||||||
column.relationshipType === RelationshipTypes.ONE_TO_MANY &&
|
const jcTblName = generateJunctionTableName(column, table, relatedTable)
|
||||||
!column.foreignKey
|
// first create the new table
|
||||||
|
const junctionTable = {
|
||||||
|
_id: buildExternalTableId(datasource._id, jcTblName),
|
||||||
|
name: jcTblName,
|
||||||
|
primary: [primary, relatedPrimary],
|
||||||
|
schema: {
|
||||||
|
[primary]: foreignKeyStructure(primary, {
|
||||||
|
toTable: table.name,
|
||||||
|
toKey: table.primary[0],
|
||||||
|
}),
|
||||||
|
[relatedPrimary]: foreignKeyStructure(relatedPrimary, {
|
||||||
|
toTable: relatedTable.name,
|
||||||
|
toKey: relatedTable.primary[0],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
column.through = junctionTable._id
|
||||||
|
column.throughFrom = primary
|
||||||
|
column.throughTo = relatedPrimary
|
||||||
|
column.fieldName = relatedPrimary
|
||||||
|
return junctionTable
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateLinkSchema(column, table, relatedTable, type) {
|
||||||
|
const isOneSide = type === RelationshipTypes.ONE_TO_MANY
|
||||||
|
const primary = isOneSide ? relatedTable.primary[0] : table.primary[0]
|
||||||
|
// generate a foreign key
|
||||||
|
const foreignKey = generateForeignKey(column, relatedTable)
|
||||||
|
column.relationshipType = type
|
||||||
|
column.foreignKey = isOneSide ? foreignKey : primary
|
||||||
|
column.fieldName = isOneSide ? primary : foreignKey
|
||||||
|
return foreignKey
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRelatedSchema(linkColumn, table, relatedTable, columnName) {
|
||||||
|
// generate column for other table
|
||||||
|
const relatedSchema = cloneDeep(linkColumn)
|
||||||
|
// swap them from the main link
|
||||||
|
if (linkColumn.foreignKey) {
|
||||||
|
relatedSchema.fieldName = linkColumn.foreignKey
|
||||||
|
relatedSchema.foreignKey = linkColumn.fieldName
|
||||||
|
}
|
||||||
|
// is many to many
|
||||||
|
else {
|
||||||
|
// don't need to copy through, already got it
|
||||||
|
relatedSchema.fieldName = linkColumn.throughFrom
|
||||||
|
relatedSchema.throughTo = linkColumn.throughFrom
|
||||||
|
relatedSchema.throughFrom = linkColumn.throughTo
|
||||||
|
}
|
||||||
|
relatedSchema.relationshipType = otherRelationshipType(
|
||||||
|
linkColumn.relationshipType
|
||||||
)
|
)
|
||||||
|
relatedSchema.tableId = relatedTable._id
|
||||||
|
relatedSchema.name = columnName
|
||||||
|
table.schema[columnName] = relatedSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRelationshipSetup(column) {
|
||||||
|
return column.foreignKey || column.through
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.save = async function (ctx) {
|
exports.save = async function (ctx) {
|
||||||
|
@ -113,32 +173,50 @@ exports.save = async function (ctx) {
|
||||||
|
|
||||||
const db = new CouchDB(appId)
|
const db = new CouchDB(appId)
|
||||||
const datasource = await db.get(datasourceId)
|
const datasource = await db.get(datasourceId)
|
||||||
|
const oldTables = cloneDeep(datasource.entities)
|
||||||
const tables = datasource.entities
|
const tables = datasource.entities
|
||||||
|
|
||||||
|
const extraTablesToUpdate = []
|
||||||
|
|
||||||
// check if relations need setup
|
// check if relations need setup
|
||||||
for (let schema of Object.values(tableToSave.schema)) {
|
for (let schema of Object.values(tableToSave.schema)) {
|
||||||
// TODO: many to many handling
|
if (schema.type !== FieldTypes.LINK || isRelationshipSetup(schema)) {
|
||||||
if (oneToManyRelationshipNeedsSetup(schema)) {
|
continue
|
||||||
const relatedTable = Object.values(tables).find(
|
}
|
||||||
table => table._id === schema.tableId
|
const relatedTable = Object.values(tables).find(
|
||||||
|
table => table._id === schema.tableId
|
||||||
|
)
|
||||||
|
const relatedColumnName = schema.fieldName
|
||||||
|
const relationType = schema.relationshipType
|
||||||
|
if (relationType === RelationshipTypes.MANY_TO_MANY) {
|
||||||
|
const junctionTable = generateManyLinkSchema(
|
||||||
|
datasource,
|
||||||
|
schema,
|
||||||
|
table,
|
||||||
|
relatedTable
|
||||||
)
|
)
|
||||||
// setup the schema in this table
|
if (tables[junctionTable.name]) {
|
||||||
const relatedField = schema.fieldName
|
throw "Junction table already exists, cannot create another relationship."
|
||||||
const relatedPrimary = relatedTable.primary[0]
|
}
|
||||||
// generate a foreign key
|
tables[junctionTable.name] = junctionTable
|
||||||
const foreignKey = `fk_${relatedTable.name}_${schema.fieldName}`
|
extraTablesToUpdate.push(junctionTable)
|
||||||
|
} else {
|
||||||
schema.relationshipType = RelationshipTypes.ONE_TO_MANY
|
const fkTable =
|
||||||
schema.foreignKey = foreignKey
|
relationType === RelationshipTypes.ONE_TO_MANY ? table : relatedTable
|
||||||
schema.fieldName = relatedPrimary
|
const foreignKey = generateLinkSchema(
|
||||||
schema.main = true
|
schema,
|
||||||
|
table,
|
||||||
relatedTable.schema[relatedField] = generateRelatedSchema(schema, table)
|
relatedTable,
|
||||||
tableToSave.schema[foreignKey] = {
|
relationType
|
||||||
type: FieldTypes.NUMBER,
|
)
|
||||||
constraints: {},
|
fkTable.schema[foreignKey] = foreignKeyStructure(foreignKey)
|
||||||
|
// foreign key is in other table, need to save it to external
|
||||||
|
if (fkTable._id !== table._id) {
|
||||||
|
extraTablesToUpdate.push(fkTable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
generateRelatedSchema(schema, relatedTable, table, relatedColumnName)
|
||||||
|
schema.main = true
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupRelationships(tableToSave, tables, oldTable)
|
cleanupRelationships(tableToSave, tables, oldTable)
|
||||||
|
@ -147,6 +225,14 @@ exports.save = async function (ctx) {
|
||||||
? DataSourceOperation.UPDATE_TABLE
|
? DataSourceOperation.UPDATE_TABLE
|
||||||
: DataSourceOperation.CREATE_TABLE
|
: DataSourceOperation.CREATE_TABLE
|
||||||
await makeTableRequest(datasource, operation, tableToSave, tables, oldTable)
|
await makeTableRequest(datasource, operation, tableToSave, tables, oldTable)
|
||||||
|
// update any extra tables (like foreign keys in other tables)
|
||||||
|
for (let extraTable of extraTablesToUpdate) {
|
||||||
|
const oldExtraTable = oldTables[extraTable.name]
|
||||||
|
let op = oldExtraTable
|
||||||
|
? DataSourceOperation.UPDATE_TABLE
|
||||||
|
: DataSourceOperation.CREATE_TABLE
|
||||||
|
await makeTableRequest(datasource, op, extraTable, tables, oldExtraTable)
|
||||||
|
}
|
||||||
|
|
||||||
// store it into couch now for budibase reference
|
// store it into couch now for budibase reference
|
||||||
datasource.entities[tableToSave.name] = tableToSave
|
datasource.entities[tableToSave.name] = tableToSave
|
||||||
|
|
|
@ -315,4 +315,24 @@ exports.checkForViewUpdates = async (db, table, rename, deletedColumns) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.generateForeignKey = (column, relatedTable) => {
|
||||||
|
return `fk_${relatedTable.name}_${column.fieldName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.generateJunctionTableName = (column, table, relatedTable) => {
|
||||||
|
return `jt_${table.name}_${relatedTable.name}_${column.name}_${column.fieldName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.foreignKeyStructure = (keyName, meta = null) => {
|
||||||
|
const structure = {
|
||||||
|
type: FieldTypes.NUMBER,
|
||||||
|
constraints: {},
|
||||||
|
name: keyName,
|
||||||
|
}
|
||||||
|
if (meta) {
|
||||||
|
structure.meta = meta
|
||||||
|
}
|
||||||
|
return structure
|
||||||
|
}
|
||||||
|
|
||||||
exports.TableSaveFunctions = TableSaveFunctions
|
exports.TableSaveFunctions = TableSaveFunctions
|
||||||
|
|
|
@ -17,6 +17,11 @@ export interface FieldSchema {
|
||||||
autocolumn?: boolean
|
autocolumn?: boolean
|
||||||
throughFrom?: string
|
throughFrom?: string
|
||||||
throughTo?: string
|
throughTo?: string
|
||||||
|
main?: boolean
|
||||||
|
meta?: {
|
||||||
|
toTable: string
|
||||||
|
toKey: string
|
||||||
|
}
|
||||||
constraints?: {
|
constraints?: {
|
||||||
type?: string
|
type?: string
|
||||||
email?: boolean
|
email?: boolean
|
||||||
|
|
|
@ -8,9 +8,15 @@ const { FieldTypes, RelationshipTypes } = require("../../constants")
|
||||||
|
|
||||||
function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record<string, Table>, oldTable: null | Table = null) {
|
function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record<string, Table>, oldTable: null | Table = null) {
|
||||||
let primaryKey = table && table.primary ? table.primary[0] : null
|
let primaryKey = table && table.primary ? table.primary[0] : null
|
||||||
|
const columns = Object.values(table.schema)
|
||||||
|
// all columns in a junction table will be meta
|
||||||
|
let metaCols = columns.filter(col => col.meta)
|
||||||
|
let isJunction = metaCols.length === columns.length
|
||||||
// can't change primary once its set for now
|
// can't change primary once its set for now
|
||||||
if (primaryKey && !oldTable) {
|
if (primaryKey && !oldTable && !isJunction) {
|
||||||
schema.increments(primaryKey).primary()
|
schema.increments(primaryKey).primary()
|
||||||
|
} else if (!oldTable && isJunction) {
|
||||||
|
schema.primary(metaCols.map(col => col.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if any columns need added
|
// check if any columns need added
|
||||||
|
@ -18,7 +24,7 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
|
||||||
for (let [key, column] of Object.entries(table.schema)) {
|
for (let [key, column] of Object.entries(table.schema)) {
|
||||||
// skip things that are already correct
|
// skip things that are already correct
|
||||||
const oldColumn = oldTable ? oldTable.schema[key] : null
|
const oldColumn = oldTable ? oldTable.schema[key] : null
|
||||||
if ((oldColumn && oldColumn.type === column.type) || primaryKey === key) {
|
if ((oldColumn && oldColumn.type === column.type) || (primaryKey === key && !isJunction)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch (column.type) {
|
switch (column.type) {
|
||||||
|
@ -26,7 +32,12 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
|
||||||
schema.string(key)
|
schema.string(key)
|
||||||
break
|
break
|
||||||
case FieldTypes.NUMBER:
|
case FieldTypes.NUMBER:
|
||||||
if (foreignKeys.indexOf(key) === -1) {
|
// if meta is specified then this is a junction table entry
|
||||||
|
if (column.meta && column.meta.toKey && column.meta.toTable) {
|
||||||
|
const { toKey, toTable } = column.meta
|
||||||
|
schema.integer(key).unsigned()
|
||||||
|
schema.foreign(key).references(`${toTable}.${toKey}`)
|
||||||
|
} else if (foreignKeys.indexOf(key) === -1) {
|
||||||
schema.float(key)
|
schema.float(key)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
@ -41,20 +52,23 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
|
||||||
break
|
break
|
||||||
case FieldTypes.LINK:
|
case FieldTypes.LINK:
|
||||||
// this side of the relationship doesn't need any SQL work
|
// this side of the relationship doesn't need any SQL work
|
||||||
if (column.relationshipType === RelationshipTypes.MANY_TO_ONE) {
|
if (
|
||||||
break
|
column.relationshipType !== RelationshipTypes.MANY_TO_ONE &&
|
||||||
|
column.relationshipType !== RelationshipTypes.MANY_TO_MANY
|
||||||
|
) {
|
||||||
|
if (!column.foreignKey || !column.tableId) {
|
||||||
|
throw "Invalid relationship schema"
|
||||||
|
}
|
||||||
|
const { tableName } = breakExternalTableId(column.tableId)
|
||||||
|
// @ts-ignore
|
||||||
|
const relatedTable = tables[tableName]
|
||||||
|
if (!relatedTable) {
|
||||||
|
throw "Referenced table doesn't exist"
|
||||||
|
}
|
||||||
|
schema.integer(column.foreignKey).unsigned()
|
||||||
|
schema.foreign(column.foreignKey).references(`${tableName}.${relatedTable.primary[0]}`)
|
||||||
}
|
}
|
||||||
if (!column.foreignKey || !column.tableId) {
|
break
|
||||||
throw "Invalid relationship schema"
|
|
||||||
}
|
|
||||||
const { tableName } = breakExternalTableId(column.tableId)
|
|
||||||
// @ts-ignore
|
|
||||||
const relatedTable = tables[tableName]
|
|
||||||
if (!relatedTable) {
|
|
||||||
throw "Referenced table doesn't exist"
|
|
||||||
}
|
|
||||||
schema.integer(column.foreignKey).unsigned()
|
|
||||||
schema.foreign(column.foreignKey).references(`${tableName}.${relatedTable.primary[0]}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue