Skip to content

Commit 3d5a0b7

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents 0418ec5 + 3709472 commit 3d5a0b7

File tree

11 files changed

+345
-190
lines changed

11 files changed

+345
-190
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -94,26 +94,35 @@ 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+
// Either compare with value or with rightField (field-to-field). If rightField is set, value must be undefined.
104+
const comparingWithRightField = filtersAsSingle.rightField !== undefined && filtersAsSingle.rightField !== null;
105+
if (!comparingWithRightField && filtersAsSingle.value === undefined) {
103106
return { ok: false, error: `Field "value" not specified in filter object: ${JSON.stringify(filters)}` };
104107
}
105-
if ((filters as IAdminForthSingleFilter).insecureRawSQL) {
108+
if (comparingWithRightField && filtersAsSingle.value !== undefined) {
109+
return { ok: false, error: `Specify either "value" or "rightField", not both: ${JSON.stringify(filters)}` };
110+
}
111+
if (filtersAsSingle.insecureRawSQL) {
106112
return { ok: false, error: `Field "insecureRawSQL" should not be specified in filter object alongside "field": ${JSON.stringify(filters)}` };
107113
}
114+
if (filtersAsSingle.insecureRawNoSQL) {
115+
return { ok: false, error: `Field "insecureRawNoSQL" should not be specified in filter object alongside "field": ${JSON.stringify(filters)}` };
116+
}
108117
if (![AdminForthFilterOperators.EQ, AdminForthFilterOperators.NE, AdminForthFilterOperators.GT,
109118
AdminForthFilterOperators.LT, AdminForthFilterOperators.GTE, AdminForthFilterOperators.LTE,
110119
AdminForthFilterOperators.LIKE, AdminForthFilterOperators.ILIKE, AdminForthFilterOperators.IN,
111120
AdminForthFilterOperators.NIN].includes(filters.operator)) {
112121
return { ok: false, error: `Field "operator" has wrong value in filter object: ${JSON.stringify(filters)}` };
113122
}
114-
const fieldObj = resource.dataSourceColumns.find((col) => col.name == (filters as IAdminForthSingleFilter).field);
123+
const fieldObj = resource.dataSourceColumns.find((col) => col.name == filtersAsSingle.field);
115124
if (!fieldObj) {
116-
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), (filters as IAdminForthSingleFilter).field);
125+
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), filtersAsSingle.field);
117126

118127
let isPolymorphicTarget = false;
119128
if (global.adminforth?.config?.resources) {
@@ -126,20 +135,28 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
126135
);
127136
}
128137
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.`);
138+
process.env.HEAVY_DEBUG && console.log(`⚠️ Field '${filtersAsSingle.field}' not found in polymorphic target resource '${resource.resourceId}', allowing query to proceed.`);
130139
return { ok: true, error: '' };
131140
} else {
132-
throw new Error(`Field '${(filters as IAdminForthSingleFilter).field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
141+
throw new Error(`Field '${filtersAsSingle.field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
133142
}
134143
}
135144
// value normalization
136-
if (filters.operator == AdminForthFilterOperators.IN || filters.operator == AdminForthFilterOperators.NIN) {
145+
if (comparingWithRightField) {
146+
// ensure rightField exists in resource
147+
const rightFieldObj = resource.dataSourceColumns.find((col) => col.name == filtersAsSingle.rightField);
148+
if (!rightFieldObj) {
149+
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), filtersAsSingle.rightField as string);
150+
throw new Error(`Field '${filtersAsSingle.rightField}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
151+
}
152+
// No value conversion needed for field-to-field comparison here
153+
} else if (filters.operator == AdminForthFilterOperators.IN || filters.operator == AdminForthFilterOperators.NIN) {
137154
if (!Array.isArray(filters.value)) {
138155
return { ok: false, error: `Value for operator '${filters.operator}' should be an array, in filter object: ${JSON.stringify(filters) }` };
139156
}
140157
if (filters.value.length === 0) {
141158
// nonsense, and some databases might not accept IN []
142-
const colType = resource.dataSourceColumns.find((col) => col.name == (filters as IAdminForthSingleFilter).field)?.type;
159+
const colType = resource.dataSourceColumns.find((col) => col.name == filtersAsSingle.field)?.type;
143160
if (colType === AdminForthDataTypes.STRING || colType === AdminForthDataTypes.TEXT) {
144161
filters.value = [randomUUID()];
145162
return { ok: true, error: `` };
@@ -149,15 +166,15 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
149166
}
150167
filters.value = filters.value.map((val: any) => this.setFieldValue(fieldObj, val));
151168
} else {
152-
(filters as IAdminForthSingleFilter).value = this.setFieldValue(fieldObj, (filters as IAdminForthSingleFilter).value);
169+
filtersAsSingle.value = this.setFieldValue(fieldObj, filtersAsSingle.value);
153170
}
154-
} else if ((filters as IAdminForthSingleFilter).insecureRawSQL) {
171+
} else if (filtersAsSingle.insecureRawSQL || filtersAsSingle.insecureRawNoSQL) {
155172
// 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)}` };
173+
if (filtersAsSingle.operator) {
174+
return { ok: false, error: `Field "operator" should not be specified in filter object alongside "insecureRawSQL" or "insecureRawNoSQL": ${JSON.stringify(filters)}` };
158175
}
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)}` };
176+
if (filtersAsSingle.value !== undefined) {
177+
return { ok: false, error: `Field "value" should not be specified in filter object alongside "insecureRawSQL" or "insecureRawNoSQL": ${JSON.stringify(filters)}` };
161178
}
162179
} else if ((filters as IAdminForthAndOrFilter).subFilters) {
163180
// if "subFilters" is present, filter must be AndOr

adminforth/dataConnectors/clickhouse.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,13 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
226226

227227
getFilterString(resource: AdminForthResource, filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): string {
228228
if ((filter as IAdminForthSingleFilter).field) {
229+
// Field-to-field comparison support
230+
if ((filter as IAdminForthSingleFilter).rightField) {
231+
const left = (filter as IAdminForthSingleFilter).field;
232+
const right = (filter as IAdminForthSingleFilter).rightField;
233+
const operator = this.OperatorsMap[filter.operator];
234+
return `${left} ${operator} ${right}`;
235+
}
229236
// filter is a Single filter
230237
let field = (filter as IAdminForthSingleFilter).field;
231238
const column = resource.dataSourceColumns.find((col) => col.name == field);
@@ -280,6 +287,10 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
280287

281288
getFilterParams(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any[] {
282289
if ((filter as IAdminForthSingleFilter).field) {
290+
if ((filter as IAdminForthSingleFilter).rightField) {
291+
// No params for field-to-field comparisons
292+
return [];
293+
}
283294
// filter is a Single filter
284295
if (filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE) {
285296
return [{ 'f': `%${filter.value}%` }];

adminforth/dataConnectors/mongo.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,38 @@ 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) {
227+
// Field-to-field comparisons via $expr
228+
if ((filter as IAdminForthSingleFilter).rightField) {
229+
const left = `$${(filter as IAdminForthSingleFilter).field}`;
230+
const right = `$${(filter as IAdminForthSingleFilter).rightField}`;
231+
const op = (filter as IAdminForthSingleFilter).operator;
232+
const exprOpMap = {
233+
[AdminForthFilterOperators.GT]: '$gt',
234+
[AdminForthFilterOperators.GTE]: '$gte',
235+
[AdminForthFilterOperators.LT]: '$lt',
236+
[AdminForthFilterOperators.LTE]: '$lte',
237+
[AdminForthFilterOperators.EQ]: '$eq',
238+
[AdminForthFilterOperators.NE]: '$ne',
239+
} as const;
240+
const mongoExprOp = exprOpMap[op];
241+
if (!mongoExprOp) {
242+
// For unsupported ops with rightField, return empty condition
243+
return {};
244+
}
245+
return { $expr: { [mongoExprOp]: [left, right] } };
246+
}
216247
const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field);
217248
if (['integer', 'decimal', 'float'].includes(column.type)) {
218249
return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator](+(filter as IAdminForthSingleFilter).value) };
@@ -222,7 +253,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
222253

223254
// filter is a AndOr filter
224255
return this.OperatorsMap[filter.operator]((filter as IAdminForthAndOrFilter).subFilters
225-
// mongodb should ignore raw sql
256+
// mongodb should ignore raw SQL, but allow raw NoSQL
226257
.filter((f) => (f as IAdminForthSingleFilter).insecureRawSQL === undefined)
227258
.map((f) => this.getFilterQuery(resource, f)));
228259
}

adminforth/dataConnectors/mysql.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,13 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
204204

205205
getFilterString(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): string {
206206
if ((filter as IAdminForthSingleFilter).field) {
207+
// Field-to-field comparison support
208+
if ((filter as IAdminForthSingleFilter).rightField) {
209+
const left = (filter as IAdminForthSingleFilter).field;
210+
const right = (filter as IAdminForthSingleFilter).rightField;
211+
const operator = this.OperatorsMap[filter.operator];
212+
return `${left} ${operator} ${right}`;
213+
}
207214
// filter is a Single filter
208215
let placeholder = '?';
209216
let field = (filter as IAdminForthSingleFilter).field;
@@ -249,6 +256,10 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
249256
}
250257
getFilterParams(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any[] {
251258
if ((filter as IAdminForthSingleFilter).field) {
259+
if ((filter as IAdminForthSingleFilter).rightField) {
260+
// No params for field-to-field comparisons
261+
return [];
262+
}
252263
// filter is a Single filter
253264
if (filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE) {
254265
return [`%${filter.value}%`];

adminforth/dataConnectors/postgres.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
225225

226226
getFilterString(resource: AdminForthResource, filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): string {
227227
if ((filter as IAdminForthSingleFilter).field) {
228+
// Field-to-field comparison support
229+
if ((filter as IAdminForthSingleFilter).rightField) {
230+
const left = `"${(filter as IAdminForthSingleFilter).field}"`;
231+
const right = `"${(filter as IAdminForthSingleFilter).rightField}"`;
232+
const operator = this.OperatorsMap[filter.operator];
233+
return `${left} ${operator} ${right}`;
234+
}
228235
let placeholder = '$?';
229236
let field = (filter as IAdminForthSingleFilter).field;
230237
const fieldData = resource.dataSourceColumns.find((col) => col.name == field);
@@ -265,6 +272,10 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
265272

266273
getFilterParams(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any[] {
267274
if ((filter as IAdminForthSingleFilter).field) {
275+
if ((filter as IAdminForthSingleFilter).rightField) {
276+
// No params for field-to-field comparisons
277+
return [];
278+
}
268279
// filter is a Single filter
269280
if (filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE) {
270281
return [`%${filter.value}%`];

adminforth/dataConnectors/sqlite.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
179179

180180
getFilterString(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): string {
181181
if ((filter as IAdminForthSingleFilter).field) {
182+
// Field-to-field comparison support
183+
if ((filter as IAdminForthSingleFilter).rightField) {
184+
const left = (filter as IAdminForthSingleFilter).field;
185+
const right = (filter as IAdminForthSingleFilter).rightField;
186+
const operator = this.OperatorsMap[filter.operator];
187+
return `${left} ${operator} ${right}`;
188+
}
182189
// filter is a Single filter
183190
let placeholder = '?';
184191
let field = (filter as IAdminForthSingleFilter).field;
@@ -222,6 +229,10 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
222229
}
223230
getFilterParams(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any[] {
224231
if ((filter as IAdminForthSingleFilter).field) {
232+
if ((filter as IAdminForthSingleFilter).rightField) {
233+
// No params for field-to-field comparisons
234+
return [];
235+
}
225236
// filter is a Single filter
226237
if (filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE) {
227238
return [`%${filter.value}%`];

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/documentation/docs/tutorial/07-Plugins/17-bulk-ai-flow.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,4 +311,24 @@ new BulkAiFlowPlugin({
311311
```
312312
313313
- Consider using lower resolution (`512x512`) for faster generation and lower costs
314-
- Test prompts thoroughly before applying to large datasets
314+
- Test prompts thoroughly before applying to large datasets
315+
316+
## Comparing new and old images
317+
318+
If you want to compare a generated image with an image stored in your storage, you need to add the preview prop in your upload plugin setup:
319+
320+
```ts
321+
new UploadPlugin({
322+
...
323+
//diff-add
324+
preview: {
325+
//diff-add
326+
previewUrl: ({filePath}) => `https://static.my-domain.com/${filePath}`,
327+
//diff-add
328+
}
329+
...
330+
})
331+
```
332+
After generation, you’ll see a button labeled "old image". Clicking it will open a pop-up where you can compare the generated image with the stored one:
333+
334+
![alt text](Bulk-vision-4.png)
532 KB
Loading

0 commit comments

Comments
 (0)