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:
mike12345567 2024-02-05 18:57:16 +00:00
parent e8e7eea47a
commit 9a8c31a2a4
1 changed files with 76 additions and 15 deletions

View File

@ -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) {