Skip to content

Commit d6502d3

Browse files
author
Pavlo Kulyk
committed
fix: implement cascading deletion checks for MySQL, PostgreSQL, and SQLite connectors
1 parent 9520f80 commit d6502d3

File tree

3 files changed

+75
-0
lines changed

3 files changed

+75
-0
lines changed

adminforth/dataConnectors/mysql.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,28 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
7676

7777
async discoverFields(resource) {
7878
const [results] = await this.client.execute("SHOW COLUMNS FROM " + resource.table);
79+
const [fkResults] = await this.client.execute(`
80+
SELECT
81+
kcu.TABLE_NAME AS child_table,
82+
kcu.COLUMN_NAME AS column_name,
83+
rc.DELETE_RULE AS delete_rule
84+
FROM information_schema.KEY_COLUMN_USAGE kcu
85+
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
86+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
87+
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
88+
WHERE kcu.REFERENCED_TABLE_NAME = ?
89+
AND kcu.TABLE_SCHEMA = DATABASE()
90+
`, [resource.table]);
91+
92+
const fkMap: Record<string, { cascade: boolean; childTable: string }> = {};
93+
for (const fk of fkResults as any[]) {
94+
fkMap[String(fk.column_name)] = {
95+
cascade: String(fk.delete_rule).toUpperCase() === 'CASCADE',
96+
childTable: fk.child_table
97+
};
98+
if (fkMap[fk.column_name].cascade) {
99+
afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`); }
100+
}
79101
const fieldTypes = {};
80102
results.forEach((row) => {
81103
const field: any = {};

adminforth/dataConnectors/postgres.ts

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

72+
private async getPgFkCascadeMap(
73+
tableName: string,
74+
schema = 'public'
75+
): Promise<Record<string, { cascade: boolean; targetTable: string }>> {
76+
const res = await this.client.query(
77+
`
78+
SELECT
79+
att.attname AS column_name,
80+
rel.relname AS child_table,
81+
p.relname AS parent_table,
82+
con.confdeltype AS confdeltype
83+
FROM pg_constraint con
84+
JOIN pg_class rel ON rel.oid = con.conrelid
85+
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
86+
JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord) ON TRUE
87+
JOIN pg_attribute att
88+
ON att.attrelid = con.conrelid AND att.attnum = k.attnum
89+
JOIN pg_class p ON p.oid = con.confrelid
90+
WHERE con.contype = 'f'
91+
AND nsp.nspname = $2
92+
AND p.relname = $1
93+
`,
94+
[tableName, schema]
95+
);
96+
97+
const fkMap: Record<string, { cascade: boolean; targetTable: string }> = {};
98+
99+
for (const row of res.rows) {
100+
fkMap[row.column_name.toLowerCase()] = {
101+
cascade: row.confdeltype === 'c',
102+
targetTable: row.parent_table,
103+
};
104+
}
105+
return fkMap;
106+
}
107+
72108
async discoverFields(resource) {
73109

74110
const tableName = resource.table;
111+
const fkMap = await this.getPgFkCascadeMap(tableName);
112+
const hasCascade = Object.values(fkMap).some(fk => fk.cascade);
113+
const cascadeWarningShownMap: Record<string, boolean> = {};
114+
if (hasCascade && !cascadeWarningShownMap[tableName]) {
115+
afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`);
116+
}
75117
const stmt = await this.client.query(`
76118
SELECT
77119
a.attname AS name,

adminforth/dataConnectors/sqlite.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
4242
const tableName = resource.table;
4343
const stmt = this.client.prepare(`PRAGMA table_info(${tableName})`);
4444
const rows = await stmt.all();
45+
const fkStmt = this.client.prepare(`PRAGMA foreign_key_list(${tableName})`);
46+
const fkRows = await fkStmt.all();
47+
const fkMap: { [colName: string]: boolean } = {};
48+
fkRows.forEach(fk => {
49+
fkMap[fk.from] = fk.on_delete?.toUpperCase() === 'CASCADE';
50+
});
4551
const fieldTypes = {};
4652
rows.forEach((row) => {
4753
const field: any = {};
@@ -86,6 +92,11 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
8692
field._baseTypeDebug = baseType;
8793
field.required = row.notnull == 1;
8894
field.primaryKey = row.pk == 1;
95+
96+
field.cascade = fkMap[row.name] || false;
97+
if (field.cascade) {
98+
afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`);
99+
}
89100
field.default = row.dflt_value;
90101
fieldTypes[row.name] = field
91102
});

0 commit comments

Comments
 (0)