Skip to content

Commit 1cbb653

Browse files
committed
feat: implement cascading deletion checks for MySQL, Postgres, and SQLite connectors
1 parent 1641242 commit 1cbb653

File tree

3 files changed

+102
-29
lines changed

3 files changed

+102
-29
lines changed

adminforth/dataConnectors/mysql.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import dayjs from 'dayjs';
2-
import { AdminForthResource, IAdminForthSingleFilter, IAdminForthAndOrFilter, IAdminForthDataSourceConnector } from '../types/Back.js';
2+
import { AdminForthResource, IAdminForthSingleFilter, IAdminForthAndOrFilter, IAdminForthDataSourceConnector, AdminForthConfig } from '../types/Back.js';
33
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, } from '../types/Common.js';
44
import AdminForthBaseConnector from './baseConnector.js';
55
import mysql from 'mysql2/promise';
@@ -74,28 +74,52 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
7474
}));
7575
}
7676

77-
private async hasPgCascadeFk(tableName: string): Promise<void> {
78-
const [fkResults] = await this.client.execute(
77+
async checkCascadeWhenUploadPlugin(resource: AdminForthResource, config: AdminForthConfig) {
78+
const currentResource = config.resources.find(r => r.resourceId === resource.resourceId);
79+
if (!currentResource) return;
80+
const hasUploadPlugin = currentResource.plugins?.some(p => p.className === "UploadPlugin");
81+
82+
if (hasUploadPlugin) {
83+
const tableName = (resource.table);
84+
afLogger.warn(`Table "${tableName}" has ON DELETE CASCADE, which may conflict with adminForth UploadPlugin.`);
85+
}
86+
}
87+
88+
async hasMySQLCascadeFk(resource: AdminForthResource, config: AdminForthConfig): Promise<boolean> {
89+
90+
const cascadeColumn = resource.columns.find(c => c.foreignResource?.onDelete === 'cascade');
91+
if (!cascadeColumn) return false;
92+
93+
const parentResource = config.resources.find(r => r.resourceId === cascadeColumn.foreignResource.resourceId);
94+
95+
if (!parentResource) return false;
96+
97+
const [rows] = await this.client.execute(
7998
`
80-
SELECT
81-
TABLE_NAME AS child_table,
82-
CONSTRAINT_NAME
99+
SELECT 1
83100
FROM information_schema.REFERENTIAL_CONSTRAINTS
84101
WHERE CONSTRAINT_SCHEMA = DATABASE()
85102
AND REFERENCED_TABLE_NAME = ?
86103
AND DELETE_RULE = 'CASCADE'
104+
LIMIT 1
87105
`,
88-
[tableName]
106+
[parentResource.table]
89107
);
90108

91-
for (const fk of fkResults as any[]) {
92-
afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`);
109+
const hasCascade = (rows as any[]).length > 0;
110+
111+
if (hasCascade) {
112+
afLogger.warn(`Table "${parentResource.table}" has ON DELETE CASCADE, which may conflict with adminForth cascade deletion.`);
93113
}
114+
115+
return hasCascade;
94116
}
95117

96-
async discoverFields(resource) {
118+
async discoverFields(resource: AdminForthResource, config: AdminForthConfig) {
119+
await this.checkCascadeWhenUploadPlugin(resource, config);
120+
97121
const [results] = await this.client.execute("SHOW COLUMNS FROM " + resource.table);
98-
await this.hasPgCascadeFk(resource.table);
122+
await this.hasMySQLCascadeFk(resource, config);
99123
const fieldTypes = {};
100124
results.forEach((row) => {
101125
const field: any = {};

adminforth/dataConnectors/postgres.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import dayjs from 'dayjs';
2-
import { AdminForthResource, IAdminForthSingleFilter, IAdminForthAndOrFilter, IAdminForthDataSourceConnector } from '../types/Back.js';
2+
import { AdminForthResource, IAdminForthSingleFilter, IAdminForthAndOrFilter, IAdminForthDataSourceConnector, AdminForthConfig } from '../types/Back.js';
33
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections, } from '../types/Common.js';
44
import AdminForthBaseConnector from './baseConnector.js';
55
import pkg from 'pg';
@@ -69,7 +69,11 @@ 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 hasPgCascadeFk(tableName: string, schema = 'public'): Promise<boolean> {
72+
async checkForeignResourceCascade(resource: AdminForthResource, config: AdminForthConfig, schema = 'public'): Promise<boolean> {
73+
const cascadeColumn = resource.columns.find(c => c.foreignResource?.onDelete === 'cascade');
74+
if (!cascadeColumn) return;
75+
76+
const parentResource = config.resources.find(r => r.resourceId === cascadeColumn.foreignResource.resourceId);
7377
const res = await this.client.query(
7478
`
7579
SELECT 1
@@ -79,20 +83,37 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
7983
AND confdeltype = 'c'
8084
LIMIT 1
8185
`,
82-
[tableName, schema]
86+
[parentResource.table, schema]
8387
);
8488

85-
return res.rowCount > 0;
86-
}
89+
const hasCascade = res.rowCount > 0;
90+
if (hasCascade) {
91+
afLogger.warn(
92+
`Table "${parentResource.table}" has ON DELETE CASCADE, which may conflict with adminForth cascade deletion`
93+
);
94+
}
8795

88-
async discoverFields(resource) {
96+
return hasCascade;
97+
}
8998

90-
const tableName = resource.table;
91-
const hasCascade = await this.hasPgCascadeFk(tableName);
99+
async checkCascadeWhenUploadPlugin(resource: AdminForthResource, config: AdminForthConfig) {
100+
const currentResource = config.resources.find(r => r.resourceId === resource.resourceId);
101+
if (!currentResource) return;
102+
const hasUploadPlugin = currentResource.plugins?.some(p => p.className === "UploadPlugin");
92103

93-
if (hasCascade) {
94-
afLogger.warn(`The database has ON DELETE CASCADE, which may conflict with adminForth cascade deletion and upload logic. Please remove it.`);
104+
if (hasUploadPlugin) {
105+
const tableName = (resource.table);
106+
afLogger.warn(
107+
`Table "${tableName}" has ON DELETE CASCADE, which may conflict with adminForth UploadPlugin.`
108+
);
95109
}
110+
}
111+
112+
async discoverFields(resource: AdminForthResource, config: AdminForthConfig) {
113+
await this.checkForeignResourceCascade(resource, config);
114+
await this.checkCascadeWhenUploadPlugin(resource, config);
115+
116+
const tableName = resource.table;
96117
const stmt = await this.client.query(`
97118
SELECT
98119
a.attname AS name,

adminforth/dataConnectors/sqlite.ts

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import betterSqlite3 from 'better-sqlite3';
2-
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource, AdminForthResourceColumn } from '../types/Back.js';
2+
import { IAdminForthDataSourceConnector, IAdminForthSingleFilter, IAdminForthAndOrFilter, AdminForthResource, AdminForthResourceColumn, AdminForthConfig } from '../types/Back.js';
33
import AdminForthBaseConnector from './baseConnector.js';
44
import dayjs from 'dayjs';
55
import { AdminForthDataTypes, AdminForthFilterOperators, AdminForthSortDirections } from '../types/Common.js';
@@ -38,16 +38,47 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
3838
}));
3939
}
4040

41-
async discoverFields(resource: AdminForthResource): Promise<{[key: string]: AdminForthResourceColumn}> {
41+
async checkCascadeWhenUploadPlugin(resource: AdminForthResource, config: AdminForthConfig) {
42+
const currentResource = config.resources.find(r => r.resourceId === resource.resourceId);
43+
if (!currentResource) return;
44+
const hasUploadPlugin = currentResource.plugins?.some(p => p.className === "UploadPlugin");
45+
46+
if (hasUploadPlugin) {
47+
const tableName = (resource.table);
48+
afLogger.warn(`Table "${tableName}" has ON DELETE CASCADE, which may conflict with adminForth UploadPlugin.`);
49+
}
50+
}
51+
52+
async hasSQLiteCascadeFk(resource: AdminForthResource, config: AdminForthConfig, fkMap: { [colName: string]: boolean }): Promise<boolean> {
53+
54+
const hasAdminCascade = resource.columns?.some(c => c.foreignResource?.onDelete === 'cascade');
55+
if (!hasAdminCascade) return false;
56+
57+
const hasDbCascade = Object.values(fkMap).some(v => v);
58+
console.log("resource.resourceId" , resource.resourceId);
59+
60+
if (hasDbCascade) {
61+
const tableName = (resource.table);
62+
afLogger.warn(
63+
64+
`Table "${tableName}" has ON DELETE CASCADE, which may conflict with adminForth cascade deletion`
65+
);
66+
}
67+
68+
return hasDbCascade;
69+
}
70+
71+
async discoverFields(resource: AdminForthResource, config: AdminForthConfig): Promise<{[key: string]: AdminForthResourceColumn}> {
72+
await this.checkCascadeWhenUploadPlugin(resource, config);
73+
4274
const tableName = resource.table;
4375
const stmt = this.client.prepare(`PRAGMA table_info(${tableName})`);
4476
const rows = await stmt.all();
4577
const fkStmt = this.client.prepare(`PRAGMA foreign_key_list(${tableName})`);
4678
const fkRows = await fkStmt.all();
4779
const fkMap: { [colName: string]: boolean } = {};
48-
fkRows.forEach(fk => {
49-
fkMap[fk.from] = fk.on_delete?.toUpperCase() === 'CASCADE';
50-
});
80+
fkRows.forEach(fk => {fkMap[fk.from] = fk.on_delete?.toUpperCase() === 'CASCADE';})
81+
await this.hasSQLiteCascadeFk(resource, config, fkMap);
5182
const fieldTypes = {};
5283
rows.forEach((row) => {
5384
const field: any = {};
@@ -94,9 +125,6 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
94125
field.primaryKey = row.pk == 1;
95126

96127
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-
}
100128
field.default = row.dflt_value;
101129
fieldTypes[row.name] = field
102130
});

0 commit comments

Comments
 (0)