Skip to content

Commit 1acfe3e

Browse files
committed
feat: enhance cascading deletion logic to prevent infinite loops and improve foreign key checks
1 parent 66b0377 commit 1acfe3e

File tree

4 files changed

+21
-11
lines changed

4 files changed

+21
-11
lines changed

adminforth/dataConnectors/mysql.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,12 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
8787
SELECT 1
8888
FROM information_schema.REFERENTIAL_CONSTRAINTS
8989
WHERE CONSTRAINT_SCHEMA = DATABASE()
90+
AND TABLE_NAME = ?
9091
AND REFERENCED_TABLE_NAME = ?
9192
AND DELETE_RULE = 'CASCADE'
9293
LIMIT 1
9394
`,
94-
[parentResource.table]
95+
[resource.table, parentResource.table]
9596
);
9697

9798
const hasCascadeOnTable = (rows as any[]).length > 0;
@@ -101,6 +102,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
101102
if (hasCascadeOnTable && isUploadPluginInstalled) {
102103
afLogger.warn(`Table "${resource.table}" has ON DELETE CASCADE and UploadPlugin installed, which may conflict with adminForth cascade deletion`);
103104
}
105+
return hasCascadeOnTable;
104106
}
105107

106108
async discoverFields(resource: AdminForthResource, config: AdminForthConfig) {

adminforth/dataConnectors/postgres.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
6969
return res.rows.map(row => ({ name: row.column_name, sampleValue: sampleRow[row.column_name] }));
7070
}
7171

72-
async checkForeignResourceCascade(resource: AdminForthResource, config: AdminForthConfig, schema = 'public'): Promise<boolean> {
72+
async checkForeignResourceCascade(resource: AdminForthResource, config: AdminForthConfig, schema = 'public'): Promise<void> {
7373
const cascadeColumn = resource.columns.find(c => c.foreignResource?.onDelete === 'cascade');
7474
if (!cascadeColumn) return;
7575

@@ -81,11 +81,12 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
8181
SELECT 1
8282
FROM pg_constraint
8383
WHERE contype = 'f'
84-
AND confrelid = ($2 || '.' || $1)::regclass
85-
AND confdeltype = 'c'
84+
AND confrelid = ($2 || '.' || $1)::regclass
85+
AND conrelid = ($2 || '.' || $3)::regclass
86+
AND confdeltype = 'c'
8687
LIMIT 1
87-
`,
88-
[parentResource.table, schema]
88+
`,
89+
[parentResource.table, schema, resource.table ]
8990
);
9091

9192
const hasCascadeOnTable = res.rowCount > 0;

adminforth/modules/configValidator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import AdminForth from "adminforth";
3131
import { AdminForthConfigMenuItem } from "adminforth";
3232
import { afLogger } from "./logger.js";
3333
import {cascadeChildrenDelete} from './utils.js'
34-
import AdminForthRestAPI from './restApi.js';
3534

3635
export default class ConfigValidator implements IConfigValidator {
3736

@@ -283,8 +282,8 @@ export default class ConfigValidator implements IConfigValidator {
283282
return;
284283
}
285284

286-
await connector.deleteRecord({ resource: res as AdminForthResource, recordId });
287285
await cascadeChildrenDelete(res as AdminForthResource, recordId, { adminUser, response}, this.adminforth);
286+
await connector.deleteRecord({ resource: res as AdminForthResource, recordId });
288287

289288
await Promise.all(
290289
(res.hooks.delete.afterSave).map(

adminforth/modules/utils.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,15 @@ export function slugifyString(str: string): string {
480480
.replace(/[^a-z0-9-_]/g, '-');
481481
}
482482

483-
export async function cascadeChildrenDelete(resource: AdminForthResource, primaryKey: string, context: {adminUser: any, response: any}, adminforth: IAdminForth): Promise<{ error: string | null }> {
483+
export async function cascadeChildrenDelete(resource: AdminForthResource, primaryKey: string, context: {adminUser: any, response: any}, adminforth: IAdminForth, visitedResources: Set<string> = new Set()): Promise<{ error: string | null }> {
484484
const { adminUser, response } = context;
485485

486+
if (visitedResources.has(resource.resourceId)) {
487+
return { error: null };
488+
}
489+
490+
visitedResources.add(resource.resourceId);
491+
486492
const childResources = adminforth.config.resources.filter(r =>r.columns.some(c => c.foreignResource?.resourceId === resource.resourceId));
487493

488494
for (const childRes of childResources) {
@@ -495,11 +501,13 @@ export async function cascadeChildrenDelete(resource: AdminForthResource, primar
495501
const childRecords = await adminforth.resource(childRes.resourceId).list(Filters.EQ(foreignColumn.name, primaryKey));
496502

497503
const childPk = childRes.columns.find(c => c.primaryKey)?.name;
498-
if (!childPk) continue;
499504

500505
if (strategy === 'cascade') {
501506
for (const childRecord of childRecords) {
502-
const childResult = await cascadeChildrenDelete(childRes, childRecord[childPk], context, adminforth);
507+
const childResult = await cascadeChildrenDelete(childRes, childRecord[childPk], context, adminforth, visitedResources);
508+
if (childResult?.error) {
509+
return childResult;
510+
}
503511
const deleteChild = await adminforth.deleteResourceRecord({resource: childRes, record: childRecord, adminUser, recordId: childRecord[childPk], response});
504512
if (deleteChild.error) return { error: deleteChild.error };
505513
if (childResult?.error) {

0 commit comments

Comments
 (0)