Skip to content

Commit f9e8109

Browse files
authored
Merge pull request #171 from devforth/add-insecure-raw-sql
feat: add support for raw sql filter
2 parents a06e5bf + 6e1525f commit f9e8109

File tree

7 files changed

+102
-13
lines changed

7 files changed

+102
-13
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,17 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
5656
}, { ok: true, error: '' });
5757
}
5858

59-
if (!filters.operator) {
60-
return { ok: false, error: `Field "operator" not specified in filter object: ${JSON.stringify(filters)}` };
61-
}
62-
6359
if ((filters as IAdminForthSingleFilter).field) {
6460
// if "field" is present, filter must be Single
61+
if (!filters.operator) {
62+
return { ok: false, error: `Field "operator" not specified in filter object: ${JSON.stringify(filters)}` };
63+
}
64+
if ((filters as IAdminForthSingleFilter).value === undefined) {
65+
return { ok: false, error: `Field "value" not specified in filter object: ${JSON.stringify(filters)}` };
66+
}
67+
if ((filters as IAdminForthSingleFilter).insecureRawSQL) {
68+
return { ok: false, error: `Field "insecureRawSQL" should not be specified in filter object alongside "field": ${JSON.stringify(filters)}` };
69+
}
6570
if (![AdminForthFilterOperators.EQ, AdminForthFilterOperators.NE, AdminForthFilterOperators.GT,
6671
AdminForthFilterOperators.LT, AdminForthFilterOperators.GTE, AdminForthFilterOperators.LTE,
6772
AdminForthFilterOperators.LIKE, AdminForthFilterOperators.ILIKE, AdminForthFilterOperators.IN,
@@ -73,6 +78,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
7378
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), (filters as IAdminForthSingleFilter).field);
7479
throw new Error(`Field '${(filters as IAdminForthSingleFilter).field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
7580
}
81+
// value normalization
7682
if (filters.operator == AdminForthFilterOperators.IN || filters.operator == AdminForthFilterOperators.NIN) {
7783
if (!Array.isArray(filters.value)) {
7884
return { ok: false, error: `Value for operator '${filters.operator}' should be an array, in filter object: ${JSON.stringify(filters) }` };
@@ -85,8 +91,19 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
8591
} else {
8692
(filters as IAdminForthSingleFilter).value = this.setFieldValue(fieldObj, (filters as IAdminForthSingleFilter).value);
8793
}
94+
} else if ((filters as IAdminForthSingleFilter).insecureRawSQL) {
95+
// if "insecureRawSQL" filter is insecure sql string
96+
if ((filters as IAdminForthSingleFilter).operator) {
97+
return { ok: false, error: `Field "operator" should not be specified in filter object alongside "insecureRawSQL": ${JSON.stringify(filters)}` };
98+
}
99+
if ((filters as IAdminForthSingleFilter).value !== undefined) {
100+
return { ok: false, error: `Field "value" should not be specified in filter object alongside "insecureRawSQL": ${JSON.stringify(filters)}` };
101+
}
88102
} else if ((filters as IAdminForthAndOrFilter).subFilters) {
89103
// if "subFilters" is present, filter must be AndOr
104+
if (!filters.operator) {
105+
return { ok: false, error: `Field "operator" not specified in filter object: ${JSON.stringify(filters)}` };
106+
}
90107
if (![AdminForthFilterOperators.AND, AdminForthFilterOperators.OR].includes(filters.operator)) {
91108
return { ok: false, error: `Field "operator" has wrong value in filter object: ${JSON.stringify(filters)}` };
92109
}

adminforth/dataConnectors/clickhouse.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,14 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
185185
return `${field} ${operator} ${placeholder}`;
186186
}
187187

188+
// filter is a single insecure raw sql
189+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
190+
return (filter as IAdminForthSingleFilter).insecureRawSQL;
191+
}
192+
188193
// filter is a AndOr filter
189194
return (filter as IAdminForthAndOrFilter).subFilters.map((f) => {
190-
if ((f as IAdminForthSingleFilter).field) {
195+
if ((f as IAdminForthSingleFilter).field || (f as IAdminForthSingleFilter).insecureRawSQL) {
191196
// subFilter is a Single filter
192197
return this.getFilterString(resource, f);
193198
}
@@ -209,6 +214,11 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
209214
}
210215
}
211216

217+
// filter is a Single insecure raw sql
218+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
219+
return [];
220+
}
221+
212222
// filter is a AndOrFilter
213223
return (filter as IAdminForthAndOrFilter).subFilters.reduce((params: any[], f: IAdminForthSingleFilter | IAdminForthAndOrFilter) => {
214224
return params.concat(this.getFilterParams(f));
@@ -310,6 +320,13 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
310320
filters: IAdminForthAndOrFilter;
311321
}): Promise<number> {
312322
const tableName = resource.table;
323+
// validate and normalize in case this method is called from dataAPI
324+
if (filters) {
325+
const filterValidation = this.validateAndNormalizeFilters(filters, resource);
326+
if (!filterValidation.ok) {
327+
throw new Error(filterValidation.error);
328+
}
329+
}
313330
const { where, params } = this.whereClause(resource, filters);
314331

315332
const countQ = await this.client.query({

adminforth/dataConnectors/mongo.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
125125
}
126126

127127
// filter is a AndOr filter
128-
return this.OperatorsMap[filter.operator]((filter as IAdminForthAndOrFilter).subFilters.map((f) => this.getFilterQuery(resource, f)));
128+
return this.OperatorsMap[filter.operator]((filter as IAdminForthAndOrFilter).subFilters
129+
// mongodb should ignore raw sql
130+
.filter((f) => (f as IAdminForthSingleFilter).insecureRawSQL === undefined)
131+
.map((f) => this.getFilterQuery(resource, f)));
129132
}
130133

131134
async getDataWithOriginalTypes({ resource, limit, offset, sort, filters }:
@@ -162,7 +165,13 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
162165
resource: AdminForthResource,
163166
filters: IAdminForthAndOrFilter,
164167
}): Promise<number> {
165-
168+
if (filters) {
169+
// validate and normalize in case this method is called from dataAPI
170+
const filterValidation = this.validateAndNormalizeFilters(filters, resource);
171+
if (!filterValidation.ok) {
172+
throw new Error(filterValidation.error);
173+
}
174+
}
166175
const collection = this.client.db().collection(resource.table);
167176
const query = filters.subFilters.length ? this.getFilterQuery(resource, filters) : {};
168177
return await collection.countDocuments(query);

adminforth/dataConnectors/mysql.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,14 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
186186
return `${field} ${operator} ${placeholder}`;
187187
}
188188

189+
// filter is a single insecure raw sql
190+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
191+
return (filter as IAdminForthSingleFilter).insecureRawSQL;
192+
}
193+
189194
// filter is a AndOr filter
190195
return (filter as IAdminForthAndOrFilter).subFilters.map((f) => {
191-
if ((f as IAdminForthSingleFilter).field) {
196+
if ((f as IAdminForthSingleFilter).field || (f as IAdminForthSingleFilter).insecureRawSQL) {
192197
// subFilter is a Single filter
193198
return this.getFilterString(f);
194199
}
@@ -209,6 +214,11 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
209214
}
210215
}
211216

217+
// filter is a Single insecure raw sql
218+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
219+
return [];
220+
}
221+
212222
// filter is a AndOrFilter
213223
return (filter as IAdminForthAndOrFilter).subFilters.reduce((params: any[], f: IAdminForthSingleFilter | IAdminForthAndOrFilter) => {
214224
return params.concat(this.getFilterParams(f));
@@ -252,6 +262,13 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
252262

253263
async getCount({ resource, filters }: { resource: AdminForthResource; filters: IAdminForthAndOrFilter; }): Promise<number> {
254264
const tableName = resource.table;
265+
// validate and normalize in case this method is called from dataAPI
266+
if (filters) {
267+
const filterValidation = this.validateAndNormalizeFilters(filters, resource);
268+
if (!filterValidation.ok) {
269+
throw new Error(filterValidation.error);
270+
}
271+
}
255272
const { sql: where, values: filterValues } = this.whereClauseAndValues(filters);
256273
const q = `SELECT COUNT(*) FROM ${tableName} ${where}`;
257274
if (process.env.HEAVY_DEBUG_QUERY) {

adminforth/dataConnectors/postgres.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,14 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
219219
return `${field} ${operator} ${placeholder}`;
220220
}
221221

222+
// filter is a single insecure raw sql
223+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
224+
return (filter as IAdminForthSingleFilter).insecureRawSQL;
225+
}
226+
222227
// filter is a AndOr filter
223228
return (filter as IAdminForthAndOrFilter).subFilters.map((f) => {
224-
if ((f as IAdminForthSingleFilter).field) {
229+
if ((f as IAdminForthSingleFilter).field || (f as IAdminForthSingleFilter).insecureRawSQL) {
225230
// subFilter is a Single filter
226231
return this.getFilterString(resource, f);
227232
}
@@ -243,6 +248,11 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
243248
}
244249
}
245250

251+
// filter is a single insecure raw sql
252+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
253+
return [];
254+
}
255+
246256
// filter is a AndOrFilter
247257
return (filter as IAdminForthAndOrFilter).subFilters.reduce((params: any[], f: IAdminForthSingleFilter | IAdminForthAndOrFilter) => {
248258
return params.concat(this.getFilterParams(f));
@@ -291,6 +301,13 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
291301

292302
async getCount({ resource, filters }: { resource: AdminForthResource; filters: IAdminForthAndOrFilter; }): Promise<number> {
293303
const tableName = resource.table;
304+
// validate and normalize in case this method is called from dataAPI
305+
if (filters) {
306+
const filterValidation = this.validateAndNormalizeFilters(filters, resource);
307+
if (!filterValidation.ok) {
308+
throw new Error(filterValidation.error);
309+
}
310+
}
294311
const { sql: where, values: filterValues } = this.whereClauseAndValues(resource, filters);
295312
const q = `SELECT COUNT(*) FROM "${tableName}" ${where}`;
296313
if (process.env.HEAVY_DEBUG_QUERY) {

adminforth/dataConnectors/sqlite.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,14 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
172172
return `${field} ${operator} ${placeholder}`;
173173
}
174174

175+
// filter is a single insecure raw sql
176+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
177+
return (filter as IAdminForthSingleFilter).insecureRawSQL;
178+
}
179+
175180
// filter is a AndOr filter
176181
return (filter as IAdminForthAndOrFilter).subFilters.map((f) => {
177-
if ((f as IAdminForthSingleFilter).field) {
182+
if ((f as IAdminForthSingleFilter).field || (f as IAdminForthSingleFilter).insecureRawSQL) {
178183
// subFilter is a Single filter
179184
return this.getFilterString(f);
180185
}
@@ -195,6 +200,11 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
195200
}
196201
}
197202

203+
// filter is a Single insecure raw sql
204+
if ((filter as IAdminForthSingleFilter).insecureRawSQL) {
205+
return [];
206+
}
207+
198208
// filter is a AndOrFilter
199209
return (filter as IAdminForthAndOrFilter).subFilters.reduce((params: any[], f: IAdminForthSingleFilter | IAdminForthAndOrFilter) => {
200210
return params.concat(this.getFilterParams(f));
@@ -234,6 +244,7 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
234244

235245
async getCount({ resource, filters }) {
236246
if (filters) {
247+
// validate and normalize in case this method is called from dataAPI
237248
const filterValidation = this.validateAndNormalizeFilters(filters, resource);
238249
if (!filterValidation.ok) {
239250
throw new Error(filterValidation.error);

adminforth/types/Back.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,13 @@ export interface IExpressHttpServer extends IHttpServer {
107107

108108

109109
export interface IAdminForthSingleFilter {
110-
field: string;
111-
operator: AdminForthFilterOperators.EQ | AdminForthFilterOperators.NE
110+
field?: string;
111+
operator?: AdminForthFilterOperators.EQ | AdminForthFilterOperators.NE
112112
| AdminForthFilterOperators.GT | AdminForthFilterOperators.LT | AdminForthFilterOperators.GTE
113113
| AdminForthFilterOperators.LTE | AdminForthFilterOperators.LIKE | AdminForthFilterOperators.ILIKE
114114
| AdminForthFilterOperators.IN | AdminForthFilterOperators.NIN;
115-
value: any;
115+
value?: any;
116+
insecureRawSQL?: string;
116117
}
117118
export interface IAdminForthAndOrFilter {
118119
operator: AdminForthFilterOperators.AND | AdminForthFilterOperators.OR;

0 commit comments

Comments
 (0)