Skip to content

Commit 037384b

Browse files
committed
feat: add field-to-field comparison support in filters across data connectors
1 parent 32dc418 commit 037384b

File tree

7 files changed

+92
-2
lines changed

7 files changed

+92
-2
lines changed

adminforth/dataConnectors/baseConnector.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,14 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
100100
if (!filters.operator) {
101101
return { ok: false, error: `Field "operator" not specified in filter object: ${JSON.stringify(filters)}` };
102102
}
103-
if (filtersAsSingle.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) {
104106
return { ok: false, error: `Field "value" not specified in filter object: ${JSON.stringify(filters)}` };
105107
}
108+
if (comparingWithRightField && filtersAsSingle.value !== undefined) {
109+
return { ok: false, error: `Specify either "value" or "rightField", not both: ${JSON.stringify(filters)}` };
110+
}
106111
if (filtersAsSingle.insecureRawSQL) {
107112
return { ok: false, error: `Field "insecureRawSQL" should not be specified in filter object alongside "field": ${JSON.stringify(filters)}` };
108113
}
@@ -137,7 +142,15 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
137142
}
138143
}
139144
// value normalization
140-
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) {
141154
if (!Array.isArray(filters.value)) {
142155
return { ok: false, error: `Value for operator '${filters.operator}' should be an array, in filter object: ${JSON.stringify(filters) }` };
143156
}

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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,26 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
224224
}
225225

226226
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+
}
227247
const column = resource.dataSourceColumns.find((col) => col.name === (filter as IAdminForthSingleFilter).field);
228248
if (['integer', 'decimal', 'float'].includes(column.type)) {
229249
return { [(filter as IAdminForthSingleFilter).field]: this.OperatorsMap[filter.operator](+(filter as IAdminForthSingleFilter).value) };

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/types/Back.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface IAdminForthSingleFilter {
130130
| AdminForthFilterOperators.LTE | AdminForthFilterOperators.LIKE | AdminForthFilterOperators.ILIKE
131131
| AdminForthFilterOperators.IN | AdminForthFilterOperators.NIN;
132132
value?: any;
133+
rightField?: string;
133134
insecureRawSQL?: string;
134135
insecureRawNoSQL?: any;
135136
}
@@ -1168,6 +1169,18 @@ export class Filters {
11681169
static ILIKE(field: string, value: any): IAdminForthSingleFilter {
11691170
return { field, operator: AdminForthFilterOperators.ILIKE, value };
11701171
}
1172+
static COMPARE_GT(leftField: string, rightField: string): IAdminForthSingleFilter {
1173+
return { field: leftField, operator: AdminForthFilterOperators.GT, rightField };
1174+
}
1175+
static COMPARE_GTE(leftField: string, rightField: string): IAdminForthSingleFilter {
1176+
return { field: leftField, operator: AdminForthFilterOperators.GTE, rightField };
1177+
}
1178+
static COMPARE_LT(leftField: string, rightField: string): IAdminForthSingleFilter {
1179+
return { field: leftField, operator: AdminForthFilterOperators.LT, rightField };
1180+
}
1181+
static COMPARE_LTE(leftField: string, rightField: string): IAdminForthSingleFilter {
1182+
return { field: leftField, operator: AdminForthFilterOperators.LTE, rightField };
1183+
}
11711184
static AND(
11721185
...args: (IAdminForthSingleFilter | IAdminForthAndOrFilter | Array<IAdminForthSingleFilter | IAdminForthAndOrFilter>)[]
11731186
): IAdminForthAndOrFilter {

0 commit comments

Comments
 (0)