Skip to content

Commit 512ed8c

Browse files
committed
feat: add support for insecureRawNoSQL filter in MongoDB and update documentation
1 parent 2520b37 commit 512ed8c

File tree

4 files changed

+73
-15
lines changed

4 files changed

+73
-15
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,26 +94,30 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
9494
}, { ok: true, error: '' });
9595
}
9696

97-
if ((filters as IAdminForthSingleFilter).field) {
97+
const filtersAsSingle = filters as IAdminForthSingleFilter;
98+
if (filtersAsSingle.field) {
9899
// if "field" is present, filter must be Single
99100
if (!filters.operator) {
100101
return { ok: false, error: `Field "operator" not specified in filter object: ${JSON.stringify(filters)}` };
101102
}
102-
if ((filters as IAdminForthSingleFilter).value === undefined) {
103+
if (filtersAsSingle.value === undefined) {
103104
return { ok: false, error: `Field "value" not specified in filter object: ${JSON.stringify(filters)}` };
104105
}
105-
if ((filters as IAdminForthSingleFilter).insecureRawSQL) {
106+
if (filtersAsSingle.insecureRawSQL) {
106107
return { ok: false, error: `Field "insecureRawSQL" should not be specified in filter object alongside "field": ${JSON.stringify(filters)}` };
107108
}
109+
if (filtersAsSingle.insecureRawNoSQL) {
110+
return { ok: false, error: `Field "insecureRawNoSQL" should not be specified in filter object alongside "field": ${JSON.stringify(filters)}` };
111+
}
108112
if (![AdminForthFilterOperators.EQ, AdminForthFilterOperators.NE, AdminForthFilterOperators.GT,
109113
AdminForthFilterOperators.LT, AdminForthFilterOperators.GTE, AdminForthFilterOperators.LTE,
110114
AdminForthFilterOperators.LIKE, AdminForthFilterOperators.ILIKE, AdminForthFilterOperators.IN,
111115
AdminForthFilterOperators.NIN].includes(filters.operator)) {
112116
return { ok: false, error: `Field "operator" has wrong value in filter object: ${JSON.stringify(filters)}` };
113117
}
114-
const fieldObj = resource.dataSourceColumns.find((col) => col.name == (filters as IAdminForthSingleFilter).field);
118+
const fieldObj = resource.dataSourceColumns.find((col) => col.name == filtersAsSingle.field);
115119
if (!fieldObj) {
116-
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), (filters as IAdminForthSingleFilter).field);
120+
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), filtersAsSingle.field);
117121

118122
let isPolymorphicTarget = false;
119123
if (global.adminforth?.config?.resources) {
@@ -126,10 +130,10 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
126130
);
127131
}
128132
if (isPolymorphicTarget) {
129-
process.env.HEAVY_DEBUG && console.log(`⚠️ Field '${(filters as IAdminForthSingleFilter).field}' not found in polymorphic target resource '${resource.resourceId}', allowing query to proceed.`);
133+
process.env.HEAVY_DEBUG && console.log(`⚠️ Field '${filtersAsSingle.field}' not found in polymorphic target resource '${resource.resourceId}', allowing query to proceed.`);
130134
return { ok: true, error: '' };
131135
} else {
132-
throw new Error(`Field '${(filters as IAdminForthSingleFilter).field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
136+
throw new Error(`Field '${filtersAsSingle.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
133137
}
134138
}
135139
// value normalization
@@ -139,7 +143,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
139143
}
140144
if (filters.value.length === 0) {
141145
// nonsense, and some databases might not accept IN []
142-
const colType = resource.dataSourceColumns.find((col) => col.name == (filters as IAdminForthSingleFilter).field)?.type;
146+
const colType = resource.dataSourceColumns.find((col) => col.name == filtersAsSingle.field)?.type;
143147
if (colType === AdminForthDataTypes.STRING || colType === AdminForthDataTypes.TEXT) {
144148
filters.value = [randomUUID()];
145149
return { ok: true, error: `` };
@@ -149,15 +153,15 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
149153
}
150154
filters.value = filters.value.map((val: any) => this.setFieldValue(fieldObj, val));
151155
} else {
152-
(filters as IAdminForthSingleFilter).value = this.setFieldValue(fieldObj, (filters as IAdminForthSingleFilter).value);
156+
filtersAsSingle.value = this.setFieldValue(fieldObj, filtersAsSingle.value);
153157
}
154-
} else if ((filters as IAdminForthSingleFilter).insecureRawSQL) {
158+
} else if (filtersAsSingle.insecureRawSQL || filtersAsSingle.insecureRawNoSQL) {
155159
// if "insecureRawSQL" filter is insecure sql string
156-
if ((filters as IAdminForthSingleFilter).operator) {
157-
return { ok: false, error: `Field "operator" should not be specified in filter object alongside "insecureRawSQL": ${JSON.stringify(filters)}` };
160+
if (filtersAsSingle.operator) {
161+
return { ok: false, error: `Field "operator" should not be specified in filter object alongside "insecureRawSQL" or "insecureRawNoSQL": ${JSON.stringify(filters)}` };
158162
}
159-
if ((filters as IAdminForthSingleFilter).value !== undefined) {
160-
return { ok: false, error: `Field "value" should not be specified in filter object alongside "insecureRawSQL": ${JSON.stringify(filters)}` };
163+
if (filtersAsSingle.value !== undefined) {
164+
return { ok: false, error: `Field "value" should not be specified in filter object alongside "insecureRawSQL" or "insecureRawNoSQL": ${JSON.stringify(filters)}` };
161165
}
162166
} else if ((filters as IAdminForthAndOrFilter).subFilters) {
163167
// if "subFilters" is present, filter must be AndOr

adminforth/dataConnectors/mongo.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
212212
}
213213

214214
getFilterQuery(resource: AdminForthResource, filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any {
215+
// accept raw NoSQL filters for MongoDB
216+
if ((filter as IAdminForthSingleFilter).insecureRawNoSQL !== undefined) {
217+
return (filter as IAdminForthSingleFilter).insecureRawNoSQL;
218+
}
219+
220+
// explicitly ignore raw SQL filters for MongoDB
221+
if ((filter as IAdminForthSingleFilter).insecureRawSQL !== undefined) {
222+
console.warn('⚠️ Ignoring insecureRawSQL filter for MongoDB:', (filter as IAdminForthSingleFilter).insecureRawSQL);
223+
return {};
224+
}
225+
215226
if ((filter as IAdminForthSingleFilter).field) {
216227
const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field);
217228
if (['integer', 'decimal', 'float'].includes(column.type)) {
@@ -222,7 +233,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
222233

223234
// filter is a AndOr filter
224235
return this.OperatorsMap[filter.operator]((filter as IAdminForthAndOrFilter).subFilters
225-
// mongodb should ignore raw sql
236+
// mongodb should ignore raw SQL, but allow raw NoSQL
226237
.filter((f) => (f as IAdminForthSingleFilter).insecureRawSQL === undefined)
227238
.map((f) => this.getFilterQuery(resource, f)));
228239
}

adminforth/documentation/docs/tutorial/03-Customization/03-virtualColumns.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,48 @@ import sqlstring from 'sqlstring';
176176
This example will allow to search for some nested field in JSONB column, however you can use any SQL query here.
177177
178178
179+
### Custom Mongo queries with `insecureRawNoSQL`
180+
181+
For MongoDB data sources, you can inject a raw Mongo filter object via `insecureRawNoSQL`. This is useful when the built-in filters are not enough or you need dot-notation and operators not covered by AdminForth helpers.
182+
183+
Important: The object you provide is sent directly to MongoDB. Validate and sanitize any user inputs to prevent abuse of operators like `$where`, `$regex`, etc.
184+
185+
Example — filter by nested field using dot-notation:
186+
187+
```ts title='./resources/apartments.ts'
188+
...
189+
hooks: {
190+
list: {
191+
beforeDatasourceRequest: async ({ query, body }: { query: any, body: any }) => {
192+
// Add raw Mongo filter: meta.is_active must equal body.is_active
193+
query.filters.push({
194+
insecureRawNoSQL: { 'meta.is_active': body.is_active },
195+
});
196+
return { ok: true, error: '' };
197+
},
198+
},
199+
},
200+
```
201+
202+
You can combine it with other AdminForth filters using AND/OR:
203+
204+
```ts
205+
import { Filters } from 'adminforth';
206+
207+
query.filters = [
208+
Filters.AND(
209+
{ insecureRawNoSQL: { 'meta.is_active': true } },
210+
Filters.EQ('status', 'active'),
211+
)
212+
];
213+
```
214+
215+
Notes:
216+
- `insecureRawNoSQL` is Mongo-only. For SQL databases, use `insecureRawSQL`.
217+
- If both `field`/`operator`/`value` and `insecureRawNoSQL` are present in one filter object, validation will fail.
218+
- `insecureRawSQL` is ignored by the Mongo connector.
219+
220+
179221
180222
## Virtual columns for editing.
181223

adminforth/types/Back.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export interface IAdminForthSingleFilter {
131131
| AdminForthFilterOperators.IN | AdminForthFilterOperators.NIN;
132132
value?: any;
133133
insecureRawSQL?: string;
134+
insecureRawNoSQL?: any;
134135
}
135136
export interface IAdminForthAndOrFilter {
136137
operator: AdminForthFilterOperators.AND | AdminForthFilterOperators.OR;

0 commit comments

Comments
 (0)