Handling deletion of rows that violate constraints, this has been an issue in Budibase for some time and causes some confusion, attempting to resolve this when deleting rows.
This commit is contained in:
parent
e8e7eea47a
commit
9a8c31a2a4
|
@ -7,6 +7,7 @@ import {
|
||||||
FilterType,
|
FilterType,
|
||||||
IncludeRelationship,
|
IncludeRelationship,
|
||||||
ManyToManyRelationshipFieldMetadata,
|
ManyToManyRelationshipFieldMetadata,
|
||||||
|
ManyToOneRelationshipFieldMetadata,
|
||||||
OneToManyRelationshipFieldMetadata,
|
OneToManyRelationshipFieldMetadata,
|
||||||
Operation,
|
Operation,
|
||||||
PaginationJson,
|
PaginationJson,
|
||||||
|
@ -102,6 +103,26 @@ function buildFilters(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeRelationships(
|
||||||
|
rowId: string,
|
||||||
|
table: Table,
|
||||||
|
isManyToMany: boolean,
|
||||||
|
colName?: string
|
||||||
|
) {
|
||||||
|
const tableId = table._id!
|
||||||
|
const filters = buildFilters(rowId, {}, table)
|
||||||
|
// safety check, if there are no filters on deletion bad things happen
|
||||||
|
if (Object.keys(filters).length !== 0) {
|
||||||
|
const op = isManyToMany ? Operation.DELETE : Operation.UPDATE
|
||||||
|
const body = colName && !isManyToMany ? { [colName]: null } : undefined
|
||||||
|
return getDatasourceAndQuery({
|
||||||
|
endpoint: getEndpoint(tableId, op),
|
||||||
|
body,
|
||||||
|
filters,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function checks the incoming parameters to make sure all the inputs are
|
* This function checks the incoming parameters to make sure all the inputs are
|
||||||
* valid based on on the table schema. The main thing this is looking for is when a
|
* valid based on on the table schema. The main thing this is looking for is when a
|
||||||
|
@ -305,6 +326,18 @@ export class ExternalRequest<T extends Operation> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRow(table: Table, rowId: string): Promise<Row> {
|
||||||
|
const response = await getDatasourceAndQuery({
|
||||||
|
endpoint: getEndpoint(table._id!, Operation.READ),
|
||||||
|
filters: buildFilters(rowId, {}, table),
|
||||||
|
})
|
||||||
|
if (response.length > 0) {
|
||||||
|
return response[0]
|
||||||
|
} else {
|
||||||
|
throw new Error(`Cannot fetch row by ID "${rowId}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inputProcessing(row: Row | undefined, table: Table) {
|
inputProcessing(row: Row | undefined, table: Table) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return { row, manyRelationships: [] }
|
return { row, manyRelationships: [] }
|
||||||
|
@ -572,7 +605,9 @@ export class ExternalRequest<T extends Operation> {
|
||||||
* information.
|
* information.
|
||||||
*/
|
*/
|
||||||
async lookupRelations(tableId: string, row: Row) {
|
async lookupRelations(tableId: string, row: Row) {
|
||||||
const related: { [key: string]: any } = {}
|
const related: {
|
||||||
|
[key: string]: { rows: Row[]; isMany: boolean; tableId: string }
|
||||||
|
} = {}
|
||||||
const { tableName } = breakExternalTableId(tableId)
|
const { tableName } = breakExternalTableId(tableId)
|
||||||
if (!tableName) {
|
if (!tableName) {
|
||||||
return related
|
return related
|
||||||
|
@ -591,7 +626,7 @@ export class ExternalRequest<T extends Operation> {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const isMany = field.relationshipType === RelationshipType.MANY_TO_MANY
|
const isMany = field.relationshipType === RelationshipType.MANY_TO_MANY
|
||||||
const tableId = isMany ? field.through : field.tableId
|
const tableId = isMany ? field.through! : field.tableId!
|
||||||
const { tableName: relatedTableName } = breakExternalTableId(tableId)
|
const { tableName: relatedTableName } = breakExternalTableId(tableId)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
|
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
|
||||||
|
@ -610,7 +645,7 @@ export class ExternalRequest<T extends Operation> {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// this is the response from knex if no rows found
|
// this is the response from knex if no rows found
|
||||||
const rows = !response[0].read ? response : []
|
const rows: Row[] = !response[0].read ? response : []
|
||||||
const storeTo = isMany ? field.throughFrom || linkPrimaryKey : fieldName
|
const storeTo = isMany ? field.throughFrom || linkPrimaryKey : fieldName
|
||||||
related[storeTo] = { rows, isMany, tableId }
|
related[storeTo] = { rows, isMany, tableId }
|
||||||
}
|
}
|
||||||
|
@ -698,24 +733,46 @@ export class ExternalRequest<T extends Operation> {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for (let row of rows) {
|
for (let row of rows) {
|
||||||
const filters = buildFilters(generateIdForRow(row, table), {}, table)
|
const promise = removeRelationships(
|
||||||
// safety check, if there are no filters on deletion bad things happen
|
generateIdForRow(row, table),
|
||||||
if (Object.keys(filters).length !== 0) {
|
table,
|
||||||
const op = isMany ? Operation.DELETE : Operation.UPDATE
|
isMany,
|
||||||
const body = isMany ? undefined : { [colName]: null }
|
colName
|
||||||
promises.push(
|
)
|
||||||
getDatasourceAndQuery({
|
if (promise) {
|
||||||
endpoint: getEndpoint(tableId, op),
|
promises.push(promise)
|
||||||
body,
|
|
||||||
filters,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
await Promise.all(promises)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async removeRelationshipsToRow(table: Table, rowId: string) {
|
||||||
|
const row = await this.getRow(table, rowId)
|
||||||
|
const related = await this.lookupRelations(table._id!, row)
|
||||||
|
for (let column of Object.values(table.schema)) {
|
||||||
|
if (
|
||||||
|
column.type !== FieldType.LINK ||
|
||||||
|
column.relationshipType === RelationshipType.ONE_TO_MANY
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const relationshipColumn = column as ManyToOneRelationshipFieldMetadata
|
||||||
|
const { rows, isMany, tableId } = related[relationshipColumn.fieldName]
|
||||||
|
const table = this.getTable(tableId)!
|
||||||
|
await Promise.all(
|
||||||
|
rows.map(row =>
|
||||||
|
removeRelationships(
|
||||||
|
generateIdForRow(row, table),
|
||||||
|
table,
|
||||||
|
isMany,
|
||||||
|
relationshipColumn.fieldName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is a bit crazy, but the exact purpose of it is to protect against the scenario in which
|
* This function is a bit crazy, but the exact purpose of it is to protect against the scenario in which
|
||||||
* you have column overlap in relationships, e.g. we join a few different tables and they all have the
|
* you have column overlap in relationships, e.g. we join a few different tables and they all have the
|
||||||
|
@ -828,6 +885,10 @@ export class ExternalRequest<T extends Operation> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const aliasing = new AliasTables(Object.keys(this.tables))
|
const aliasing = new AliasTables(Object.keys(this.tables))
|
||||||
|
// remove any relationships that could block deletion
|
||||||
|
if (operation === Operation.DELETE && id) {
|
||||||
|
await this.removeRelationshipsToRow(table, generateRowIdField(id))
|
||||||
|
}
|
||||||
const response = await aliasing.queryWithAliasing(json)
|
const response = await aliasing.queryWithAliasing(json)
|
||||||
// handle many-to-many relationships now if we know the ID (could be auto increment)
|
// handle many-to-many relationships now if we know the ID (could be auto increment)
|
||||||
if (operation !== Operation.READ) {
|
if (operation !== Operation.READ) {
|
||||||
|
|
Loading…
Reference in New Issue