Skip to content

Commit fc0e92d

Browse files
guguclaude
andcommitted
Add BETWEEN filter support for range filtering across all database drivers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d75e120 commit fc0e92d

16 files changed

Lines changed: 401 additions & 0 deletions

backend/src/entities/table/utils/find-filtering-fields.util.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,19 @@ export function findFilteringFieldsUtil(
100100
.map((v) => v.trim()),
101101
});
102102
}
103+
104+
if (isObjectPropertyExists(filters, `f_${fieldname}__between`)) {
105+
const rawValue = filters[`f_${fieldname}__between`];
106+
filteringItems.push({
107+
field: fieldname,
108+
criteria: FilterCriteriaEnum.between,
109+
value: Array.isArray(rawValue)
110+
? rawValue
111+
: String(rawValue)
112+
.split(',')
113+
.map((v) => v.trim()),
114+
});
115+
}
103116
}
104117
return filteringItems;
105118
}

backend/src/enums/filter-criteria.enum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export enum FilterCriteriaEnum {
1010
eq = 'eq',
1111
empty = 'empty',
1212
in = 'in',
13+
between = 'between',
1314
}

backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3663,3 +3663,131 @@ test.serial(`${currentTest} should test connection and return result`, async (t)
36633663
const { message } = JSON.parse(testConnectionResponse.text);
36643664
t.is(message, 'Successfully connected');
36653665
});
3666+
3667+
currentTest = 'POST /table/rows/find/:slug';
3668+
3669+
test.serial(`${currentTest} should return rows filtered by IN operator in body`, async (t) => {
3670+
const connectionToTestDB = getTestData(mockFactory).connectionToMySQL;
3671+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
3672+
const { testTableName } = await createTestTable(connectionToTestDB);
3673+
3674+
testTables.push(testTableName);
3675+
3676+
const createConnectionResponse = await request(app.getHttpServer())
3677+
.post('/connection')
3678+
.send(connectionToTestDB)
3679+
.set('Cookie', firstUserToken)
3680+
.set('Content-Type', 'application/json')
3681+
.set('Accept', 'application/json');
3682+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
3683+
t.is(createConnectionResponse.status, 201);
3684+
3685+
const filters = {
3686+
id: { in: ['1', '22', '38'] },
3687+
};
3688+
3689+
const getTableRowsResponse = await request(app.getHttpServer())
3690+
.post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`)
3691+
.send({ filters })
3692+
.set('Cookie', firstUserToken)
3693+
.set('Content-Type', 'application/json')
3694+
.set('Accept', 'application/json');
3695+
3696+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
3697+
t.is(getTableRowsResponse.status, 200);
3698+
t.is(getTableRowsRO.rows.length, 3);
3699+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
3700+
t.deepEqual(returnedIds, [1, 22, 38]);
3701+
});
3702+
3703+
test.serial(`${currentTest} should return rows filtered by IN operator in query params`, async (t) => {
3704+
const connectionToTestDB = getTestData(mockFactory).connectionToMySQL;
3705+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
3706+
const { testTableName } = await createTestTable(connectionToTestDB);
3707+
3708+
testTables.push(testTableName);
3709+
3710+
const createConnectionResponse = await request(app.getHttpServer())
3711+
.post('/connection')
3712+
.send(connectionToTestDB)
3713+
.set('Cookie', firstUserToken)
3714+
.set('Content-Type', 'application/json')
3715+
.set('Accept', 'application/json');
3716+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
3717+
t.is(createConnectionResponse.status, 201);
3718+
3719+
const getTableRowsResponse = await request(app.getHttpServer())
3720+
.get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__in=1,22,38`)
3721+
.set('Cookie', firstUserToken)
3722+
.set('Content-Type', 'application/json')
3723+
.set('Accept', 'application/json');
3724+
3725+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
3726+
t.is(getTableRowsResponse.status, 200);
3727+
t.is(getTableRowsRO.rows.length, 3);
3728+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
3729+
t.deepEqual(returnedIds, [1, 22, 38]);
3730+
});
3731+
3732+
test.serial(`${currentTest} should return rows filtered by BETWEEN operator in body`, async (t) => {
3733+
const connectionToTestDB = getTestData(mockFactory).connectionToMySQL;
3734+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
3735+
const { testTableName } = await createTestTable(connectionToTestDB);
3736+
3737+
testTables.push(testTableName);
3738+
3739+
const createConnectionResponse = await request(app.getHttpServer())
3740+
.post('/connection')
3741+
.send(connectionToTestDB)
3742+
.set('Cookie', firstUserToken)
3743+
.set('Content-Type', 'application/json')
3744+
.set('Accept', 'application/json');
3745+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
3746+
t.is(createConnectionResponse.status, 201);
3747+
3748+
const filters = {
3749+
id: { between: ['20', '25'] },
3750+
};
3751+
3752+
const getTableRowsResponse = await request(app.getHttpServer())
3753+
.post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`)
3754+
.send({ filters })
3755+
.set('Cookie', firstUserToken)
3756+
.set('Content-Type', 'application/json')
3757+
.set('Accept', 'application/json');
3758+
3759+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
3760+
t.is(getTableRowsResponse.status, 200);
3761+
t.is(getTableRowsRO.rows.length, 6);
3762+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
3763+
t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]);
3764+
});
3765+
3766+
test.serial(`${currentTest} should return rows filtered by BETWEEN operator in query params`, async (t) => {
3767+
const connectionToTestDB = getTestData(mockFactory).connectionToMySQL;
3768+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
3769+
const { testTableName } = await createTestTable(connectionToTestDB);
3770+
3771+
testTables.push(testTableName);
3772+
3773+
const createConnectionResponse = await request(app.getHttpServer())
3774+
.post('/connection')
3775+
.send(connectionToTestDB)
3776+
.set('Cookie', firstUserToken)
3777+
.set('Content-Type', 'application/json')
3778+
.set('Accept', 'application/json');
3779+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
3780+
t.is(createConnectionResponse.status, 201);
3781+
3782+
const getTableRowsResponse = await request(app.getHttpServer())
3783+
.get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__between=20,25`)
3784+
.set('Cookie', firstUserToken)
3785+
.set('Content-Type', 'application/json')
3786+
.set('Accept', 'application/json');
3787+
3788+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
3789+
t.is(getTableRowsResponse.status, 200);
3790+
t.is(getTableRowsRO.rows.length, 6);
3791+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
3792+
t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]);
3793+
});

backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4550,3 +4550,131 @@ test.serial(`${currentTest} should throw exception whe csv import is disabled`,
45504550
const { message } = JSON.parse(importCsvResponse.text);
45514551
t.is(message, Messages.CSV_IMPORT_DISABLED);
45524552
});
4553+
4554+
currentTest = 'POST /table/rows/find/:slug';
4555+
4556+
test.serial(`${currentTest} should return rows filtered by IN operator in body`, async (t) => {
4557+
const connectionToTestDB = getTestData(mockFactory).connectionToPostgres;
4558+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
4559+
const { testTableName } = await createTestTable(connectionToTestDB);
4560+
4561+
testTables.push(testTableName);
4562+
4563+
const createConnectionResponse = await request(app.getHttpServer())
4564+
.post('/connection')
4565+
.send(connectionToTestDB)
4566+
.set('Cookie', firstUserToken)
4567+
.set('Content-Type', 'application/json')
4568+
.set('Accept', 'application/json');
4569+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
4570+
t.is(createConnectionResponse.status, 201);
4571+
4572+
const filters = {
4573+
id: { in: ['1', '22', '38'] },
4574+
};
4575+
4576+
const getTableRowsResponse = await request(app.getHttpServer())
4577+
.post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`)
4578+
.send({ filters })
4579+
.set('Cookie', firstUserToken)
4580+
.set('Content-Type', 'application/json')
4581+
.set('Accept', 'application/json');
4582+
4583+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
4584+
t.is(getTableRowsResponse.status, 200);
4585+
t.is(getTableRowsRO.rows.length, 3);
4586+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
4587+
t.deepEqual(returnedIds, [1, 22, 38]);
4588+
});
4589+
4590+
test.serial(`${currentTest} should return rows filtered by IN operator in query params`, async (t) => {
4591+
const connectionToTestDB = getTestData(mockFactory).connectionToPostgres;
4592+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
4593+
const { testTableName } = await createTestTable(connectionToTestDB);
4594+
4595+
testTables.push(testTableName);
4596+
4597+
const createConnectionResponse = await request(app.getHttpServer())
4598+
.post('/connection')
4599+
.send(connectionToTestDB)
4600+
.set('Cookie', firstUserToken)
4601+
.set('Content-Type', 'application/json')
4602+
.set('Accept', 'application/json');
4603+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
4604+
t.is(createConnectionResponse.status, 201);
4605+
4606+
const getTableRowsResponse = await request(app.getHttpServer())
4607+
.get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__in=1,22,38`)
4608+
.set('Cookie', firstUserToken)
4609+
.set('Content-Type', 'application/json')
4610+
.set('Accept', 'application/json');
4611+
4612+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
4613+
t.is(getTableRowsResponse.status, 200);
4614+
t.is(getTableRowsRO.rows.length, 3);
4615+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
4616+
t.deepEqual(returnedIds, [1, 22, 38]);
4617+
});
4618+
4619+
test.serial(`${currentTest} should return rows filtered by BETWEEN operator in body`, async (t) => {
4620+
const connectionToTestDB = getTestData(mockFactory).connectionToPostgres;
4621+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
4622+
const { testTableName } = await createTestTable(connectionToTestDB);
4623+
4624+
testTables.push(testTableName);
4625+
4626+
const createConnectionResponse = await request(app.getHttpServer())
4627+
.post('/connection')
4628+
.send(connectionToTestDB)
4629+
.set('Cookie', firstUserToken)
4630+
.set('Content-Type', 'application/json')
4631+
.set('Accept', 'application/json');
4632+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
4633+
t.is(createConnectionResponse.status, 201);
4634+
4635+
const filters = {
4636+
id: { between: ['20', '25'] },
4637+
};
4638+
4639+
const getTableRowsResponse = await request(app.getHttpServer())
4640+
.post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`)
4641+
.send({ filters })
4642+
.set('Cookie', firstUserToken)
4643+
.set('Content-Type', 'application/json')
4644+
.set('Accept', 'application/json');
4645+
4646+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
4647+
t.is(getTableRowsResponse.status, 200);
4648+
t.is(getTableRowsRO.rows.length, 6);
4649+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
4650+
t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]);
4651+
});
4652+
4653+
test.serial(`${currentTest} should return rows filtered by BETWEEN operator in query params`, async (t) => {
4654+
const connectionToTestDB = getTestData(mockFactory).connectionToPostgres;
4655+
const firstUserToken = (await registerUserAndReturnUserInfo(app)).token;
4656+
const { testTableName } = await createTestTable(connectionToTestDB);
4657+
4658+
testTables.push(testTableName);
4659+
4660+
const createConnectionResponse = await request(app.getHttpServer())
4661+
.post('/connection')
4662+
.send(connectionToTestDB)
4663+
.set('Cookie', firstUserToken)
4664+
.set('Content-Type', 'application/json')
4665+
.set('Accept', 'application/json');
4666+
const createConnectionRO = JSON.parse(createConnectionResponse.text);
4667+
t.is(createConnectionResponse.status, 201);
4668+
4669+
const getTableRowsResponse = await request(app.getHttpServer())
4670+
.get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__between=20,25`)
4671+
.set('Cookie', firstUserToken)
4672+
.set('Content-Type', 'application/json')
4673+
.set('Accept', 'application/json');
4674+
4675+
const getTableRowsRO = JSON.parse(getTableRowsResponse.text);
4676+
t.is(getTableRowsResponse.status, 200);
4677+
t.is(getTableRowsRO.rows.length, 6);
4678+
const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b);
4679+
t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]);
4680+
});

shared-code/src/data-access-layer/data-access-objects/data-access-object-cassandra.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,16 @@ export class DataAccessObjectCassandra extends BasicDataAccessObject implements
291291
params.push(...inValues);
292292
break;
293293
}
294+
case FilterCriteriaEnum.between: {
295+
const betweenValues = Array.isArray(filter.value)
296+
? filter.value
297+
: String(filter.value)
298+
.split(',')
299+
.map((v) => v.trim());
300+
whereConditions.push(`${filter.field.toLowerCase()} >= ? AND ${filter.field.toLowerCase()} <= ?`);
301+
params.push(betweenValues[0], betweenValues[1]);
302+
break;
303+
}
294304
default:
295305
break;
296306
}

shared-code/src/data-access-layer/data-access-objects/data-access-object-clickhouse.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,17 @@ export class DataAccessObjectClickHouse extends BasicDataAccessObject implements
271271
whereClauses.push(`${escapedField} IN (${sanitizedValues})`);
272272
break;
273273
}
274+
case FilterCriteriaEnum.between: {
275+
const betweenValues = Array.isArray(value)
276+
? value
277+
: String(value)
278+
.split(',')
279+
.map((v) => v.trim());
280+
whereClauses.push(
281+
`${escapedField} BETWEEN ${this.sanitizeValue(betweenValues[0])} AND ${this.sanitizeValue(betweenValues[1])}`,
282+
);
283+
break;
284+
}
274285
}
275286
}
276287
}

shared-code/src/data-access-layer/data-access-objects/data-access-object-dynamodb.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,21 @@ export class DataAccessObjectDynamoDB extends BasicDataAccessObject implements I
359359
});
360360
break;
361361
}
362+
case FilterCriteriaEnum.between: {
363+
const betweenValues = Array.isArray(value)
364+
? value
365+
: String(value)
366+
.split(',')
367+
.map((v) => v.trim());
368+
filterExpression += ` AND #${field} BETWEEN ${uniquePlaceholder}_low AND ${uniquePlaceholder}_high`;
369+
expressionAttributeValues[`${uniquePlaceholder}_low`] = isNumberField
370+
? { N: String(betweenValues[0]) }
371+
: { S: String(betweenValues[0]) };
372+
expressionAttributeValues[`${uniquePlaceholder}_high`] = isNumberField
373+
? { N: String(betweenValues[1]) }
374+
: { S: String(betweenValues[1]) };
375+
break;
376+
}
362377
default:
363378
break;
364379
}

shared-code/src/data-access-layer/data-access-objects/data-access-object-elasticsearch.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,17 @@ export class DataAccessObjectElasticsearch extends BasicDataAccessObject impleme
293293
searchQuery.query.bool.must.push({ terms: { [field]: inValues } });
294294
break;
295295
}
296+
case FilterCriteriaEnum.between: {
297+
const betweenValues = Array.isArray(value)
298+
? value
299+
: String(value)
300+
.split(',')
301+
.map((v) => v.trim());
302+
searchQuery.query.bool.must.push({
303+
range: { [field]: { gte: betweenValues[0], lte: betweenValues[1] } },
304+
});
305+
break;
306+
}
296307
}
297308
}
298309
}

shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,15 @@ export class DataAccessObjectIbmDb2 extends BasicDataAccessObject implements IDa
326326
queryParams.push(...(inValues as SQLParam[]));
327327
return `${filterObject.field} IN (${placeholders})`;
328328
}
329+
case FilterCriteriaEnum.between: {
330+
const betweenValues = Array.isArray(filterObject.value)
331+
? filterObject.value
332+
: String(filterObject.value)
333+
.split(',')
334+
.map((v) => v.trim());
335+
queryParams.push(betweenValues[0] as SQLParam, betweenValues[1] as SQLParam);
336+
return `${filterObject.field} BETWEEN ? AND ?`;
337+
}
329338
}
330339
})
331340
.join(' AND ');

shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,15 @@ export class DataAccessObjectMongo extends BasicDataAccessObject implements IDat
284284
acc[field] = { $in: inValues };
285285
break;
286286
}
287+
case FilterCriteriaEnum.between: {
288+
const betweenValues = Array.isArray(value)
289+
? value
290+
: String(value)
291+
.split(',')
292+
.map((v) => v.trim());
293+
acc[field] = { $gte: betweenValues[0], $lte: betweenValues[1] };
294+
break;
295+
}
287296
default:
288297
break;
289298
}

0 commit comments

Comments
 (0)