From ca65f67c8d5f19385e899a8a642c5aab5dec35aa Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Wed, 25 Feb 2026 16:14:21 +0200 Subject: [PATCH 01/16] feat: implement cascading deletion for related records in delete endpoint --- adminforth/modules/restApi.ts | 41 ++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 6b6f2f5d..7be09f7b 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1452,6 +1452,30 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } } }); + async function handleCascadeOnDelete(parentResource: AdminForthResource, parentId: any) { + const adminforth = this.adminforth; + for (const resource of adminforth.config.resources) { + if (resource.resourceId === parentResource.resourceId) continue; + const foreignKeyColumn = resource.columns.find(c => c.foreignResource?.resourceId === parentResource.resourceId + ); + if (!foreignKeyColumn) continue; + const deleteStrategy = foreignKeyColumn.foreignResource?.onDelete ?? 'null'; + const primaryKeyColumn = resource.columns.find(c => c.primaryKey); + if (!primaryKeyColumn) continue; + const childRecords = await adminforth.resource(resource.resourceId).list([Filters.EQ(foreignKeyColumn.name, parentId)]); + for (const record of childRecords) { + const childId = record[primaryKeyColumn.name]; + if (deleteStrategy === 'cascade') { + await handleCascadeOnDelete.call(this, resource, childId); + await adminforth.resource(resource.resourceId).delete(childId); + continue; + } + if (deleteStrategy === 'setNull') { + await adminforth.resource(resource.resourceId).update(childId, {[foreignKeyColumn.name]: null,}); + } + } + } + } server.endpoint({ method: 'POST', path: '/delete_record', @@ -1464,23 +1488,10 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (!record){ return { error: `Record with ${body['primaryKey']} not found` }; } - if (resource.options.allowedActions.delete === false) { + if (!resource.options.allowedActions.delete) { return { error: `Resource '${resource.resourceId}' does not allow delete action` }; } - - const { allowedActions } = await interpretResource( - adminUser, - resource, - { requestBody: body, record: record }, - ActionCheckSource.DeleteRequest, - this.adminforth - ); - - const { allowed, error } = checkAccess(AllowedActionsEnum.delete, allowedActions); - if (!allowed) { - return { error }; - } - + await handleCascadeOnDelete.call(this, resource, body['primaryKey']); const { error: deleteError } = await this.adminforth.deleteResourceRecord({ resource, record, adminUser, recordId: body['primaryKey'], response, extra: { body, query, headers, cookies, requestUrl, response } From bda166a40306519f1617bae7f72fa677154d5621 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 11:46:20 +0200 Subject: [PATCH 02/16] add alowwedAction check --- adminforth/modules/restApi.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 7be09f7b..4e4644eb 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1490,6 +1490,17 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } if (!resource.options.allowedActions.delete) { return { error: `Resource '${resource.resourceId}' does not allow delete action` }; + } + const { allowedActions } = await interpretResource( + adminUser, + resource, + { requestBody: body, record: record }, + ActionCheckSource.DeleteRequest, + this.adminforth + ); + const { allowed, error } = checkAccess(AllowedActionsEnum.delete, allowedActions); + if (!allowed) { + return { error }; } await handleCascadeOnDelete.call(this, resource, body['primaryKey']); const { error: deleteError } = await this.adminforth.deleteResourceRecord({ From 12d2ba683b81e5b779d69f322d5e352f1d710db2 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 11:47:05 +0200 Subject: [PATCH 03/16] add missing spaces --- adminforth/modules/restApi.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 4e4644eb..5180f43a 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1498,10 +1498,12 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { ActionCheckSource.DeleteRequest, this.adminforth ); + const { allowed, error } = checkAccess(AllowedActionsEnum.delete, allowedActions); if (!allowed) { return { error }; } + await handleCascadeOnDelete.call(this, resource, body['primaryKey']); const { error: deleteError } = await this.adminforth.deleteResourceRecord({ resource, record, adminUser, recordId: body['primaryKey'], response, From 02e216ba6176a2b2b1ea7df890d5a2406c270a50 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 12:46:01 +0200 Subject: [PATCH 04/16] feat: implement cascading deletion logic in delete endpoint --- adminforth/modules/restApi.ts | 73 ++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 5180f43a..7505944e 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1452,30 +1452,31 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } } }); - async function handleCascadeOnDelete(parentResource: AdminForthResource, parentId: any) { - const adminforth = this.adminforth; - for (const resource of adminforth.config.resources) { - if (resource.resourceId === parentResource.resourceId) continue; - const foreignKeyColumn = resource.columns.find(c => c.foreignResource?.resourceId === parentResource.resourceId - ); - if (!foreignKeyColumn) continue; - const deleteStrategy = foreignKeyColumn.foreignResource?.onDelete ?? 'null'; - const primaryKeyColumn = resource.columns.find(c => c.primaryKey); - if (!primaryKeyColumn) continue; - const childRecords = await adminforth.resource(resource.resourceId).list([Filters.EQ(foreignKeyColumn.name, parentId)]); - for (const record of childRecords) { - const childId = record[primaryKeyColumn.name]; - if (deleteStrategy === 'cascade') { - await handleCascadeOnDelete.call(this, resource, childId); - await adminforth.resource(resource.resourceId).delete(childId); - continue; - } - if (deleteStrategy === 'setNull') { - await adminforth.resource(resource.resourceId).update(childId, {[foreignKeyColumn.name]: null,}); - } - } - } - } + // async function handleCascadeOnDelete(parentResource: AdminForthResource, parentId: any) { + // const adminforth = this.adminforth; + // for (const resource of adminforth.config.resources) { + // if (resource.resourceId === parentResource.resourceId) continue; + // const foreignKeyColumn = resource.columns.find(c => c.foreignResource?.resourceId === parentResource.resourceId + // ); + // if (!foreignKeyColumn) continue; + // const deleteStrategy = foreignKeyColumn.foreignResource?.onDelete ?? 'null'; + // const primaryKeyColumn = resource.columns.find(c => c.primaryKey); + // if (!primaryKeyColumn) continue; + // const childRecords = await adminforth.resource(resource.resourceId).list([Filters.EQ(foreignKeyColumn.name, parentId)]); + // for (const record of childRecords) + + // const childId = record[primaryKeyColumn.name]; + // if (deleteStrategy === 'cascade') { + // await handleCascadeOnDelete.call(this, resource, childId); + // await adminforth.resource(resource.resourceId).delete(childId); + // continue; + // } + // if (deleteStrategy === 'setNull') { + // await adminforth.resource(resource.resourceId).update(childId, {[foreignKeyColumn.name]: null,}); + // } + // } + // } + // } server.endpoint({ method: 'POST', path: '/delete_record', @@ -1491,6 +1492,27 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (!resource.options.allowedActions.delete) { return { error: `Resource '${resource.resourceId}' does not allow delete action` }; } + + for (const childRes of this.adminforth.config.resources) { + for (const foreignKeyColumn of childRes.columns.filter(c => c.foreignResource?.resourceId === resource.resourceId)) { + const onDelete = (foreignKeyColumn.foreignResource as any).onDelete ?? 'setNull'; + const primaryKeyColumn = childRes.columns.find(c => c.primaryKey); + if (!primaryKeyColumn) continue; + + const childRecords = await this.adminforth.resource(childRes.resourceId).list([Filters.EQ(foreignKeyColumn.name, body['primaryKey'])]); + + for (const childRecord of childRecords) { + const childId = childRecord[primaryKeyColumn.name]; + if (onDelete === 'cascade') { + await this.adminforth.resource(childRes.resourceId).delete(childId); + } + if (onDelete === 'setNull') { + await this.adminforth.resource(resource.resourceId).update(childId, {[foreignKeyColumn.name]: null,}); + } + } + } + } + const { allowedActions } = await interpretResource( adminUser, resource, @@ -1503,8 +1525,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (!allowed) { return { error }; } - - await handleCascadeOnDelete.call(this, resource, body['primaryKey']); + const { error: deleteError } = await this.adminforth.deleteResourceRecord({ resource, record, adminUser, recordId: body['primaryKey'], response, extra: { body, query, headers, cookies, requestUrl, response } From 23d178ba51bd8c43ca8551057b31adfc87287845 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 13:25:37 +0200 Subject: [PATCH 05/16] fix: update check strategy --- adminforth/modules/restApi.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 7505944e..ce2405e9 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1494,20 +1494,26 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } for (const childRes of this.adminforth.config.resources) { - for (const foreignKeyColumn of childRes.columns.filter(c => c.foreignResource?.resourceId === resource.resourceId)) { - const onDelete = (foreignKeyColumn.foreignResource as any).onDelete ?? 'setNull'; - const primaryKeyColumn = childRes.columns.find(c => c.primaryKey); - if (!primaryKeyColumn) continue; + const foreignKeyColumns = childRes.columns.filter(c => c.foreignResource?.resourceId === resource.resourceId); + if (!foreignKeyColumns.length) continue; + const primaryKeyColumn = childRes.columns.find(c => c.primaryKey); + if (!primaryKeyColumn) continue; + + for (const foreignKeyColumn of foreignKeyColumns) { const childRecords = await this.adminforth.resource(childRes.resourceId).list([Filters.EQ(foreignKeyColumn.name, body['primaryKey'])]); - for (const childRecord of childRecords) { - const childId = childRecord[primaryKeyColumn.name]; + const onDelete = (foreignKeyColumn.foreignResource as any).onDelete; + + for (const child of childRecords) { + const childId = child[primaryKeyColumn.name]; + if (onDelete === 'cascade') { await this.adminforth.resource(childRes.resourceId).delete(childId); - } - if (onDelete === 'setNull') { - await this.adminforth.resource(resource.resourceId).update(childId, {[foreignKeyColumn.name]: null,}); + } else if (onDelete === 'setNull') { + await this.adminforth.resource(childRes.resourceId).update(childId, {[foreignKeyColumn.name]: null}); + } else { + return { error: `Wrong onDelete strategy ${onDelete} in resource '${childRes.resourceId}'` }; } } } From 8b5b7b5e03f4246d885edf158a614ee6c1aec9fd Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 16:19:25 +0200 Subject: [PATCH 06/16] feat: refine cascading deletion logic in delete endpoint --- adminforth/modules/restApi.ts | 63 +++++++++-------------------------- 1 file changed, 15 insertions(+), 48 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index ce2405e9..7a5b7cad 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1452,31 +1452,6 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { } } }); - // async function handleCascadeOnDelete(parentResource: AdminForthResource, parentId: any) { - // const adminforth = this.adminforth; - // for (const resource of adminforth.config.resources) { - // if (resource.resourceId === parentResource.resourceId) continue; - // const foreignKeyColumn = resource.columns.find(c => c.foreignResource?.resourceId === parentResource.resourceId - // ); - // if (!foreignKeyColumn) continue; - // const deleteStrategy = foreignKeyColumn.foreignResource?.onDelete ?? 'null'; - // const primaryKeyColumn = resource.columns.find(c => c.primaryKey); - // if (!primaryKeyColumn) continue; - // const childRecords = await adminforth.resource(resource.resourceId).list([Filters.EQ(foreignKeyColumn.name, parentId)]); - // for (const record of childRecords) - - // const childId = record[primaryKeyColumn.name]; - // if (deleteStrategy === 'cascade') { - // await handleCascadeOnDelete.call(this, resource, childId); - // await adminforth.resource(resource.resourceId).delete(childId); - // continue; - // } - // if (deleteStrategy === 'setNull') { - // await adminforth.resource(resource.resourceId).update(childId, {[foreignKeyColumn.name]: null,}); - // } - // } - // } - // } server.endpoint({ method: 'POST', path: '/delete_record', @@ -1492,30 +1467,22 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (!resource.options.allowedActions.delete) { return { error: `Resource '${resource.resourceId}' does not allow delete action` }; } - - for (const childRes of this.adminforth.config.resources) { - const foreignKeyColumns = childRes.columns.filter(c => c.foreignResource?.resourceId === resource.resourceId); - if (!foreignKeyColumns.length) continue; - - const primaryKeyColumn = childRes.columns.find(c => c.primaryKey); - if (!primaryKeyColumn) continue; - - for (const foreignKeyColumn of foreignKeyColumns) { - const childRecords = await this.adminforth.resource(childRes.resourceId).list([Filters.EQ(foreignKeyColumn.name, body['primaryKey'])]); - - const onDelete = (foreignKeyColumn.foreignResource as any).onDelete; - - for (const child of childRecords) { - const childId = child[primaryKeyColumn.name]; - - if (onDelete === 'cascade') { - await this.adminforth.resource(childRes.resourceId).delete(childId); - } else if (onDelete === 'setNull') { - await this.adminforth.resource(childRes.resourceId).update(childId, {[foreignKeyColumn.name]: null}); - } else { - return { error: `Wrong onDelete strategy ${onDelete} in resource '${childRes.resourceId}'` }; - } + const childResources = this.adminforth.config.resources.filter(r => r.columns.some(c => c.foreignResource?.resourceId === resource.resourceId)); + if (!childResources.length) return; + for (const childRes of childResources) { + const foreignKeyColumn = childRes.columns.find(c => c.foreignResource?.resourceId === resource.resourceId); + const onDeleteStrategy = foreignKeyColumn.foreignResource.onDelete; + const childRecords = await this.adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignKeyColumn.name, body['primaryKey'])) + if (onDeleteStrategy === 'cascade') { + for (const childRecord of childRecords) { + await this.adminforth.resource(childRes.resourceId).delete(childRecord.id); } + } else if (onDeleteStrategy === 'setNull') { + for (const childRecord of childRecords) { + await this.adminforth.resource(childRes.resourceId).update(childRecord.id, {[foreignKeyColumn.name]: null}); + } + } else { + return { error: `Wrong onDelete strategy: ${onDeleteStrategy}` }; } } From 01dfcfd6c45bce35273dcaddf635839766f05053 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 16:26:49 +0200 Subject: [PATCH 07/16] fix: update condition --- adminforth/modules/restApi.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 7a5b7cad..bcae598e 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1468,21 +1468,22 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { return { error: `Resource '${resource.resourceId}' does not allow delete action` }; } const childResources = this.adminforth.config.resources.filter(r => r.columns.some(c => c.foreignResource?.resourceId === resource.resourceId)); - if (!childResources.length) return; - for (const childRes of childResources) { - const foreignKeyColumn = childRes.columns.find(c => c.foreignResource?.resourceId === resource.resourceId); - const onDeleteStrategy = foreignKeyColumn.foreignResource.onDelete; - const childRecords = await this.adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignKeyColumn.name, body['primaryKey'])) - if (onDeleteStrategy === 'cascade') { - for (const childRecord of childRecords) { - await this.adminforth.resource(childRes.resourceId).delete(childRecord.id); - } - } else if (onDeleteStrategy === 'setNull') { - for (const childRecord of childRecords) { - await this.adminforth.resource(childRes.resourceId).update(childRecord.id, {[foreignKeyColumn.name]: null}); + if (childResources.length){ + for (const childRes of childResources) { + const foreignKeyColumn = childRes.columns.find(c => c.foreignResource?.resourceId === resource.resourceId); + const onDeleteStrategy = foreignKeyColumn.foreignResource.onDelete; + const childRecords = await this.adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignKeyColumn.name, body['primaryKey'])) + if (onDeleteStrategy === 'cascade') { + for (const childRecord of childRecords) { + await this.adminforth.resource(childRes.resourceId).delete(childRecord.id); + } + } else if (onDeleteStrategy === 'setNull') { + for (const childRecord of childRecords) { + await this.adminforth.resource(childRes.resourceId).update(childRecord.id, {[foreignKeyColumn.name]: null}); + } + } else { + return { error: `Wrong onDelete strategy: ${onDeleteStrategy}` }; } - } else { - return { error: `Wrong onDelete strategy: ${onDeleteStrategy}` }; } } From ee04911621c57b36fb3013eeb730166726c125bf Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Thu, 26 Feb 2026 16:37:10 +0200 Subject: [PATCH 08/16] fix: change variable name foreignKeyColumn to foreignResourceColumn --- adminforth/modules/restApi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index bcae598e..67eee34f 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1470,16 +1470,16 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { const childResources = this.adminforth.config.resources.filter(r => r.columns.some(c => c.foreignResource?.resourceId === resource.resourceId)); if (childResources.length){ for (const childRes of childResources) { - const foreignKeyColumn = childRes.columns.find(c => c.foreignResource?.resourceId === resource.resourceId); - const onDeleteStrategy = foreignKeyColumn.foreignResource.onDelete; - const childRecords = await this.adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignKeyColumn.name, body['primaryKey'])) + const foreignResourceColumn = childRes.columns.find(c => c.foreignResource?.resourceId === resource.resourceId); + const onDeleteStrategy = foreignResourceColumn.foreignResource.onDelete; + const childRecords = await this.adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignResourceColumn.name, body['primaryKey'])) if (onDeleteStrategy === 'cascade') { for (const childRecord of childRecords) { await this.adminforth.resource(childRes.resourceId).delete(childRecord.id); } } else if (onDeleteStrategy === 'setNull') { for (const childRecord of childRecords) { - await this.adminforth.resource(childRes.resourceId).update(childRecord.id, {[foreignKeyColumn.name]: null}); + await this.adminforth.resource(childRes.resourceId).update(childRecord.id, {[foreignResourceColumn.name]: null}); } } else { return { error: `Wrong onDelete strategy: ${onDeleteStrategy}` }; From 96c2c8f4f031f1d2aea5eaf9e052c797f2da0da2 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 27 Feb 2026 09:50:37 +0200 Subject: [PATCH 09/16] fix: add check for foreign resource onDelete strategy --- adminforth/modules/restApi.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 67eee34f..a6df347b 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1471,6 +1471,7 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { if (childResources.length){ for (const childRes of childResources) { const foreignResourceColumn = childRes.columns.find(c => c.foreignResource?.resourceId === resource.resourceId); + if (!foreignResourceColumn.foreignResource.onDelete) continue; const onDeleteStrategy = foreignResourceColumn.foreignResource.onDelete; const childRecords = await this.adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignResourceColumn.name, body['primaryKey'])) if (onDeleteStrategy === 'cascade') { From a300f83709f6c60ae55e1bd6d46aaaba9a45448d Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 27 Feb 2026 10:04:10 +0200 Subject: [PATCH 10/16] feat: add onDelete type --- adminforth/types/Back.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/adminforth/types/Back.ts b/adminforth/types/Back.ts index ae566074..5631d5c4 100644 --- a/adminforth/types/Back.ts +++ b/adminforth/types/Back.ts @@ -2037,6 +2037,7 @@ export interface AdminForthForeignResource extends AdminForthForeignResourceComm afterDatasourceResponse?: AfterDataSourceResponseFunction | Array, }, }, + onDelete: 'cascade' | 'setNull' } export type ShowInModernInput = { From 8ef2973fc909fe82e63f7e33168dfab4832aa27a Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 27 Feb 2026 10:27:24 +0200 Subject: [PATCH 11/16] fix: delete strategy check --- adminforth/modules/restApi.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index a6df347b..4d757348 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -1482,8 +1482,6 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { for (const childRecord of childRecords) { await this.adminforth.resource(childRes.resourceId).update(childRecord.id, {[foreignResourceColumn.name]: null}); } - } else { - return { error: `Wrong onDelete strategy: ${onDeleteStrategy}` }; } } } From 552ecdce3c05766a18798c3c46014b7e95b190de Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Fri, 27 Feb 2026 12:00:36 +0200 Subject: [PATCH 12/16] fix: add check for cascade strategy --- adminforth/modules/configValidator.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 4cefe723..710535be 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -618,7 +618,13 @@ export default class ConfigValidator implements IConfigValidator { errors.push(`Resource "${res.resourceId}" column "${col.name}" isArray is enabled but suggestOnCreate is not an array`); } } - + if (col.foreignResource){ + if (col.foreignResource.onDelete){ + if (col.foreignResource.onDelete !== 'cascade' && col.foreignResource.onDelete !== 'setNull'){ + errors.push (`Wrong delete strategy you can use 'onDelete' or 'cascade'`); + } + } + } if (col.foreignResource) { if (!col.foreignResource.resourceId) { // resourceId is absent or empty From 423d6a0028a4893949a52f0e3faa33dd5746bdec Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 2 Mar 2026 13:44:23 +0200 Subject: [PATCH 13/16] fix: delete mistake in error message --- adminforth/modules/configValidator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 710535be..9e29962e 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -621,7 +621,7 @@ export default class ConfigValidator implements IConfigValidator { if (col.foreignResource){ if (col.foreignResource.onDelete){ if (col.foreignResource.onDelete !== 'cascade' && col.foreignResource.onDelete !== 'setNull'){ - errors.push (`Wrong delete strategy you can use 'onDelete' or 'cascade'`); + errors.push (`Wrong delete strategy you can use 'setNull' or 'cascade'`); } } } From ff63b6ce664a9e3026b0c3988f63f9f2b377298d Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 2 Mar 2026 15:16:00 +0200 Subject: [PATCH 14/16] fix: streamline foreign resource onDelete strategy validation --- adminforth/modules/configValidator.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 9e29962e..d49ec2ec 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -618,14 +618,10 @@ export default class ConfigValidator implements IConfigValidator { errors.push(`Resource "${res.resourceId}" column "${col.name}" isArray is enabled but suggestOnCreate is not an array`); } } - if (col.foreignResource){ - if (col.foreignResource.onDelete){ - if (col.foreignResource.onDelete !== 'cascade' && col.foreignResource.onDelete !== 'setNull'){ + if (col.foreignResource) { + if (col.foreignResource.onDelete && (col.foreignResource.onDelete !== 'cascade' && col.foreignResource.onDelete !== 'setNull')){ errors.push (`Wrong delete strategy you can use 'setNull' or 'cascade'`); - } } - } - if (col.foreignResource) { if (!col.foreignResource.resourceId) { // resourceId is absent or empty if (!col.foreignResource.polymorphicResources && !col.foreignResource.polymorphicOn) { From 9520f80a77a4949c70397a985145eac0a276fd5b Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 2 Mar 2026 15:17:02 +0200 Subject: [PATCH 15/16] add missing space --- adminforth/modules/configValidator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index d49ec2ec..5b585ee4 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -618,6 +618,7 @@ export default class ConfigValidator implements IConfigValidator { errors.push(`Resource "${res.resourceId}" column "${col.name}" isArray is enabled but suggestOnCreate is not an array`); } } + if (col.foreignResource) { if (col.foreignResource.onDelete && (col.foreignResource.onDelete !== 'cascade' && col.foreignResource.onDelete !== 'setNull')){ errors.push (`Wrong delete strategy you can use 'setNull' or 'cascade'`); From d6502d3f8ca58f9364f0ca3f169ff46006d75d29 Mon Sep 17 00:00:00 2001 From: Pavlo Kulyk Date: Mon, 2 Mar 2026 15:29:11 +0200 Subject: [PATCH 16/16] fix: implement cascading deletion checks for MySQL, PostgreSQL, and SQLite connectors --- adminforth/dataConnectors/mysql.ts | 22 ++++++++++++++ adminforth/dataConnectors/postgres.ts | 42 +++++++++++++++++++++++++++ adminforth/dataConnectors/sqlite.ts | 11 +++++++ 3 files changed, 75 insertions(+) diff --git a/adminforth/dataConnectors/mysql.ts b/adminforth/dataConnectors/mysql.ts index ed97a856..40a5949c 100644 --- a/adminforth/dataConnectors/mysql.ts +++ b/adminforth/dataConnectors/mysql.ts @@ -76,6 +76,28 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS async discoverFields(resource) { const [results] = await this.client.execute("SHOW COLUMNS FROM " + resource.table); + const [fkResults] = await this.client.execute(` + SELECT + kcu.TABLE_NAME AS child_table, + kcu.COLUMN_NAME AS column_name, + rc.DELETE_RULE AS delete_rule + FROM information_schema.KEY_COLUMN_USAGE kcu + JOIN information_schema.REFERENTIAL_CONSTRAINTS rc + ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + WHERE kcu.REFERENCED_TABLE_NAME = ? + AND kcu.TABLE_SCHEMA = DATABASE() + `, [resource.table]); + + const fkMap: Record = {}; + for (const fk of fkResults as any[]) { + fkMap[String(fk.column_name)] = { + cascade: String(fk.delete_rule).toUpperCase() === 'CASCADE', + childTable: fk.child_table + }; + if (fkMap[fk.column_name].cascade) { + afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`); } + } const fieldTypes = {}; results.forEach((row) => { const field: any = {}; diff --git a/adminforth/dataConnectors/postgres.ts b/adminforth/dataConnectors/postgres.ts index 082e3cad..925f7571 100644 --- a/adminforth/dataConnectors/postgres.ts +++ b/adminforth/dataConnectors/postgres.ts @@ -69,9 +69,51 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa return res.rows.map(row => ({ name: row.column_name, sampleValue: sampleRow[row.column_name] })); } + private async getPgFkCascadeMap( + tableName: string, + schema = 'public' + ): Promise> { + const res = await this.client.query( + ` + SELECT + att.attname AS column_name, + rel.relname AS child_table, + p.relname AS parent_table, + con.confdeltype AS confdeltype + FROM pg_constraint con + JOIN pg_class rel ON rel.oid = con.conrelid + JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace + JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON TRUE + JOIN pg_attribute att + ON att.attrelid = con.conrelid AND att.attnum = k.attnum + JOIN pg_class p ON p.oid = con.confrelid + WHERE con.contype = 'f' + AND nsp.nspname = $2 + AND p.relname = $1 + `, + [tableName, schema] + ); + + const fkMap: Record = {}; + + for (const row of res.rows) { + fkMap[row.column_name.toLowerCase()] = { + cascade: row.confdeltype === 'c', + targetTable: row.parent_table, + }; + } + return fkMap; + } + async discoverFields(resource) { const tableName = resource.table; + const fkMap = await this.getPgFkCascadeMap(tableName); + const hasCascade = Object.values(fkMap).some(fk => fk.cascade); + const cascadeWarningShownMap: Record = {}; + if (hasCascade && !cascadeWarningShownMap[tableName]) { + afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`); + } const stmt = await this.client.query(` SELECT a.attname AS name, diff --git a/adminforth/dataConnectors/sqlite.ts b/adminforth/dataConnectors/sqlite.ts index 0846b65b..73486772 100644 --- a/adminforth/dataConnectors/sqlite.ts +++ b/adminforth/dataConnectors/sqlite.ts @@ -42,6 +42,12 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData const tableName = resource.table; const stmt = this.client.prepare(`PRAGMA table_info(${tableName})`); const rows = await stmt.all(); + const fkStmt = this.client.prepare(`PRAGMA foreign_key_list(${tableName})`); + const fkRows = await fkStmt.all(); + const fkMap: { [colName: string]: boolean } = {}; + fkRows.forEach(fk => { + fkMap[fk.from] = fk.on_delete?.toUpperCase() === 'CASCADE'; + }); const fieldTypes = {}; rows.forEach((row) => { const field: any = {}; @@ -86,6 +92,11 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData field._baseTypeDebug = baseType; field.required = row.notnull == 1; field.primaryKey = row.pk == 1; + + field.cascade = fkMap[row.name] || false; + if (field.cascade) { + afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`); + } field.default = row.dflt_value; fieldTypes[row.name] = field });