diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index a9f55aa07..cfc0e49e3 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,7 +1,6 @@ name: Code quality on: - push: pull_request: jobs: @@ -14,6 +13,7 @@ jobs: uses: actions/checkout@v5 with: persist-credentials: false + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -25,5 +25,16 @@ jobs: uses: biomejs/setup-biome@v2 with: version: 2.4.0 + - name: Get changed backend files + id: changed + run: | + files=$(git diff --name-only --diff-filter=ACMR \ + "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" \ + -- 'backend/' \ + | grep -E '\.(ts|tsx|js|jsx|json)$' \ + | tr '\n' ' ' || true) + echo "files=$files" >> "$GITHUB_OUTPUT" + echo "Changed backend files: $files" - name: Run Biome - run: biome ci --formatter-enabled=false --assist-enabled=false . + if: steps.changed.outputs.files != '' + run: biome ci --formatter-enabled=false --assist-enabled=false ${{ steps.changed.outputs.files }} diff --git a/backend/src/entities/table/application/data-structures/found-table-rows.ds.ts b/backend/src/entities/table/application/data-structures/found-table-rows.ds.ts index 0e81e15c9..3dc2908e9 100644 --- a/backend/src/entities/table/application/data-structures/found-table-rows.ds.ts +++ b/backend/src/entities/table/application/data-structures/found-table-rows.ds.ts @@ -127,7 +127,7 @@ export class FilteringFieldsDs { field: string; @ApiProperty() - value: string; + value: unknown; } export class AutocompleteFieldsDs { diff --git a/backend/src/entities/table/table-datastructures.ts b/backend/src/entities/table/table-datastructures.ts index a45f2136e..3b64971bf 100644 --- a/backend/src/entities/table/table-datastructures.ts +++ b/backend/src/entities/table/table-datastructures.ts @@ -13,7 +13,7 @@ export class FilteringFieldsDs { criteria: FilterCriteriaEnum; @ApiProperty() - value: string; + value: unknown; } export class ForeignKeyDSInfo { diff --git a/backend/src/entities/table/table.controller.ts b/backend/src/entities/table/table.controller.ts index 5e37dee77..91725acec 100644 --- a/backend/src/entities/table/table.controller.ts +++ b/backend/src/entities/table/table.controller.ts @@ -3,6 +3,7 @@ import { Controller, Delete, Get, + HttpCode, HttpException, HttpStatus, Inject, @@ -248,6 +249,7 @@ export class TableController { @ApiQuery({ name: 'search', required: false }) @UseGuards(TableReadGuard) @Timeout(TimeoutDefaults.EXTENDED) + @HttpCode(HttpStatus.OK) @Post('/table/rows/find/:connectionId') async findAllRowsWithBodyFilter( @QueryTableName() tableName: string, diff --git a/backend/src/entities/table/utils/find-filtering-fields.util.ts b/backend/src/entities/table/utils/find-filtering-fields.util.ts index f16af041b..eb8cede93 100644 --- a/backend/src/entities/table/utils/find-filtering-fields.util.ts +++ b/backend/src/entities/table/utils/find-filtering-fields.util.ts @@ -87,6 +87,32 @@ export function findFilteringFieldsUtil( value: filters[`f_${fieldname}__empty`], }); } + + if (isObjectPropertyExists(filters, `f_${fieldname}__in`)) { + const rawValue = filters[`f_${fieldname}__in`]; + filteringItems.push({ + field: fieldname, + criteria: FilterCriteriaEnum.in, + value: Array.isArray(rawValue) + ? rawValue + : String(rawValue) + .split(',') + .map((v) => v.trim()), + }); + } + + if (isObjectPropertyExists(filters, `f_${fieldname}__between`)) { + const rawValue = filters[`f_${fieldname}__between`]; + filteringItems.push({ + field: fieldname, + criteria: FilterCriteriaEnum.between, + value: Array.isArray(rawValue) + ? rawValue + : String(rawValue) + .split(',') + .map((v) => v.trim()), + }); + } } return filteringItems; } @@ -99,7 +125,7 @@ export function parseFilteringFieldsFromBodyData( const rowNames = tableStructure.map((el) => el.column_name); rowNames.forEach((rowName) => { if (isObjectPropertyExists(filtersDataFromBody, rowName)) { - const filterData = filtersDataFromBody[rowName] as Record; + const filterData = filtersDataFromBody[rowName] as Record; for (const key in filterData) { if (!validateStringWithEnum(key, FilterCriteriaEnum)) { throw new Error(`Invalid filter criteria: "${key}".`); diff --git a/backend/src/enums/filter-criteria.enum.ts b/backend/src/enums/filter-criteria.enum.ts index 85b725133..99bf56672 100644 --- a/backend/src/enums/filter-criteria.enum.ts +++ b/backend/src/enums/filter-criteria.enum.ts @@ -9,4 +9,6 @@ export enum FilterCriteriaEnum { icontains = 'icontains', eq = 'eq', empty = 'empty', + in = 'in', + between = 'between', } diff --git a/backend/test/ava-tests/complex-table-tests/complex-ibmdb2-table-e2e.test.ts b/backend/test/ava-tests/complex-table-tests/complex-ibmdb2-table-e2e.test.ts index 5ed96eb03..26f8da8b3 100644 --- a/backend/test/ava-tests/complex-table-tests/complex-ibmdb2-table-e2e.test.ts +++ b/backend/test/ava-tests/complex-table-tests/complex-ibmdb2-table-e2e.test.ts @@ -844,7 +844,7 @@ test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows f .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); t.is(findRowsRO.rows.length, 1); @@ -886,7 +886,7 @@ test.serial( .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); for (const row of findRowsRO.rows) { diff --git a/backend/test/ava-tests/complex-table-tests/complex-mssql-table-e2e.test.ts b/backend/test/ava-tests/complex-table-tests/complex-mssql-table-e2e.test.ts index 38eea6be4..340df59e6 100644 --- a/backend/test/ava-tests/complex-table-tests/complex-mssql-table-e2e.test.ts +++ b/backend/test/ava-tests/complex-table-tests/complex-mssql-table-e2e.test.ts @@ -844,7 +844,7 @@ test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows f .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); t.is(findRowsRO.rows.length, 1); @@ -886,7 +886,7 @@ test.serial( .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); for (const row of findRowsRO.rows) { diff --git a/backend/test/ava-tests/complex-table-tests/complex-mysql-table-e2e.test.ts b/backend/test/ava-tests/complex-table-tests/complex-mysql-table-e2e.test.ts index e9226ddd0..8be50bbb2 100644 --- a/backend/test/ava-tests/complex-table-tests/complex-mysql-table-e2e.test.ts +++ b/backend/test/ava-tests/complex-table-tests/complex-mysql-table-e2e.test.ts @@ -844,7 +844,7 @@ test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows f .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); t.is(findRowsRO.rows.length, 1); @@ -886,7 +886,7 @@ test.serial( .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); for (const row of findRowsRO.rows) { diff --git a/backend/test/ava-tests/complex-table-tests/complex-oracle-table-e2e.test.ts b/backend/test/ava-tests/complex-table-tests/complex-oracle-table-e2e.test.ts index 7f2d4a92c..3392dacbb 100644 --- a/backend/test/ava-tests/complex-table-tests/complex-oracle-table-e2e.test.ts +++ b/backend/test/ava-tests/complex-table-tests/complex-oracle-table-e2e.test.ts @@ -844,7 +844,7 @@ test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows f .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); t.is(findRowsRO.rows.length, 1); @@ -886,7 +886,7 @@ test.serial( .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); for (const row of findRowsRO.rows) { diff --git a/backend/test/ava-tests/complex-table-tests/complex-postgres-table-e2e.test.ts b/backend/test/ava-tests/complex-table-tests/complex-postgres-table-e2e.test.ts index 72af23295..a8e6dfdfd 100644 --- a/backend/test/ava-tests/complex-table-tests/complex-postgres-table-e2e.test.ts +++ b/backend/test/ava-tests/complex-table-tests/complex-postgres-table-e2e.test.ts @@ -844,7 +844,7 @@ test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows f .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); t.is(findRowsRO.rows.length, 1); @@ -886,7 +886,7 @@ test.serial( .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(findRowsResponse.status, 201); + t.is(findRowsResponse.status, 200); const findRowsRO = JSON.parse(findRowsResponse.text); t.truthy(findRowsRO.rows); for (const row of findRowsRO.rows) { diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts index a83a20231..6591cf176 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mongodb-e2e.test.ts @@ -1647,7 +1647,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1749,7 +1749,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts index 3e4b3caba..89bd75ebb 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-mysql-e2e.test.ts @@ -3663,3 +3663,131 @@ test.serial(`${currentTest} should test connection and return result`, async (t) const { message } = JSON.parse(testConnectionResponse.text); t.is(message, 'Successfully connected'); }); + +currentTest = 'POST /table/rows/find/:slug'; + +test.serial(`${currentTest} should return rows filtered by IN operator in body`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToMySQL; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const filters = { + id: { in: ['1', '22', '38'] }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 3); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [1, 22, 38]); +}); + +test.serial(`${currentTest} should return rows filtered by IN operator in query params`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToMySQL; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__in=1,22,38`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 3); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [1, 22, 38]); +}); + +test.serial(`${currentTest} should return rows filtered by BETWEEN operator in body`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToMySQL; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const filters = { + id: { between: ['20', '25'] }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 6); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]); +}); + +test.serial(`${currentTest} should return rows filtered by BETWEEN operator in query params`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToMySQL; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__between=20,25`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 6); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]); +}); diff --git a/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts b/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts index 7b0cc8b17..a522fef6d 100644 --- a/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts +++ b/backend/test/ava-tests/non-saas-tests/non-saas-table-redis-e2e.test.ts @@ -1643,7 +1643,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1745,7 +1745,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); console.log('🚀 ~ getTableRowsRO:', getTableRowsRO); diff --git a/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts index 2cfaf1228..9aa573e99 100644 --- a/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-dynamodb-e2e.test.ts @@ -1616,7 +1616,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1718,7 +1718,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); @@ -1821,7 +1821,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); console.log('🚀 ~ getTableRowsRO:', getTableRowsRO.rows); diff --git a/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts index 5a594f3a2..4a5584e61 100644 --- a/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-elasticsearch-e2e.test.ts @@ -1622,7 +1622,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1724,7 +1724,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); diff --git a/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts index dbf652c8f..fe8a76fa6 100644 --- a/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mongodb-agent-e2e.test.ts @@ -244,7 +244,7 @@ test.serial(`${currentTest} should return rows of selected table with search and .set('Accept', 'application/json'); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); t.is(typeof getTableRowsRO, 'object'); t.is(Object.hasOwn(getTableRowsRO, 'rows'), true); t.is(Object.hasOwn(getTableRowsRO, 'primaryColumns'), true); @@ -1370,7 +1370,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1457,7 +1457,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); diff --git a/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts index 64de095c5..65c979400 100644 --- a/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-mongodb-e2e.test.ts @@ -1640,7 +1640,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1742,7 +1742,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); diff --git a/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts index bba32a320..e675cfcc1 100644 --- a/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-oracledb-e2e.test.ts @@ -1613,7 +1613,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Accept', 'application/json'); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); t.is(typeof getTableRowsRO, 'object'); t.is(Object.hasOwn(getTableRowsRO, 'rows'), true); t.is(Object.hasOwn(getTableRowsRO, 'primaryColumns'), true); diff --git a/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts index 279e56381..8fe3bbdaa 100644 --- a/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-postgres-e2e.test.ts @@ -4550,3 +4550,131 @@ test.serial(`${currentTest} should throw exception whe csv import is disabled`, const { message } = JSON.parse(importCsvResponse.text); t.is(message, Messages.CSV_IMPORT_DISABLED); }); + +currentTest = 'POST /table/rows/find/:slug'; + +test.serial(`${currentTest} should return rows filtered by IN operator in body`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const filters = { + id: { in: ['1', '22', '38'] }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 3); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [1, 22, 38]); +}); + +test.serial(`${currentTest} should return rows filtered by IN operator in query params`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__in=1,22,38`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 3); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [1, 22, 38]); +}); + +test.serial(`${currentTest} should return rows filtered by BETWEEN operator in body`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const filters = { + id: { between: ['20', '25'] }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20`) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 6); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]); +}); + +test.serial(`${currentTest} should return rows filtered by BETWEEN operator in query params`, async (t) => { + const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName } = await createTestTable(connectionToTestDB); + + testTables.push(testTableName); + + const createConnectionResponse = await request(app.getHttpServer()) + .post('/connection') + .send(connectionToTestDB) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + const createConnectionRO = JSON.parse(createConnectionResponse.text); + t.is(createConnectionResponse.status, 201); + + const getTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=20&f_id__between=20,25`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + t.is(getTableRowsResponse.status, 200); + t.is(getTableRowsRO.rows.length, 6); + const returnedIds = getTableRowsRO.rows.map((row) => row.id).sort((a, b) => a - b); + t.deepEqual(returnedIds, [20, 21, 22, 23, 24, 25]); +}); diff --git a/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts index 61ed8bc5a..396d15220 100644 --- a/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-redis-agent-e2e.test.ts @@ -1361,7 +1361,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1448,7 +1448,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); diff --git a/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts b/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts index 933b73657..bc77bfd1a 100644 --- a/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/table-redis-e2e.test.ts @@ -1631,7 +1631,7 @@ should return all found rows with search, pagination: page=1, perPage=10 and DES .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); t.is(typeof getTableRowsRO, 'object'); @@ -1733,7 +1733,7 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC .set('Cookie', firstUserToken) .set('Content-Type', 'application/json') .set('Accept', 'application/json'); - t.is(getTableRowsResponse.status, 201); + t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); diff --git a/rocketadmin-agent/src/enums/filter-criteria.enum.ts b/rocketadmin-agent/src/enums/filter-criteria.enum.ts index a6898b14a..7f861187c 100644 --- a/rocketadmin-agent/src/enums/filter-criteria.enum.ts +++ b/rocketadmin-agent/src/enums/filter-criteria.enum.ts @@ -1,12 +1,13 @@ export enum FilterCriteriaEnum { - startswith = 'startswith', - endswith = 'endswith', - gt = 'gt', - lt = 'lt', - lte = 'lte', - gte = 'gte', - contains = 'contains', - icontains = 'icontains', - eq = 'eq', - empty = 'empty', + startswith = 'startswith', + endswith = 'endswith', + gt = 'gt', + lt = 'lt', + lte = 'lte', + gte = 'gte', + contains = 'contains', + icontains = 'icontains', + eq = 'eq', + empty = 'empty', + in = 'in', } diff --git a/rocketadmin-agent/src/interfaces/interfaces.ts b/rocketadmin-agent/src/interfaces/interfaces.ts index 5a1489bd4..5c1afb382 100644 --- a/rocketadmin-agent/src/interfaces/interfaces.ts +++ b/rocketadmin-agent/src/interfaces/interfaces.ts @@ -1,132 +1,132 @@ +import { AutocompleteFieldsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/autocomplete-fields.ds.js'; +import { FilteringFieldsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/filtering-fields.ds.js'; +import { TableSettingsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/table-settings.ds.js'; +import { TableStructureDS } from '@rocketadmin/shared-code/src/data-access-layer/shared/data-structures/table-structure.ds.js'; import { FilterCriteriaEnum } from '../enums/filter-criteria.enum.js'; +import { OperationTypeEnum } from '../enums/operation-type.enum.js'; import { QueryOrderingEnum } from '../enums/query-ordering.enum.js'; import { WidgetTypeEnum } from '../enums/widget-type.enum.js'; -import { OperationTypeEnum } from '../enums/operation-type.enum.js'; -import { TableSettingsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/table-settings.ds.js'; -import { FilteringFieldsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/filtering-fields.ds.js'; -import { AutocompleteFieldsDS } from '@rocketadmin/shared-code/dist/src/data-access-layer/shared/data-structures/autocomplete-fields.ds.js'; -import { TableStructureDS } from '@rocketadmin/shared-code/src/data-access-layer/shared/data-structures/table-structure.ds.js'; export interface IAutocompleteFields { - fields: Array; - value: string; + fields: Array; + value: string; } export interface ICLIConnectionCredentials { - type: string; - host: string; - port: number; - username: string; - password: string; - database: string; - schema: string; - sid: string; - ssl: boolean; - cert: string; - token: string; - app_port: number; - azure_encryption: boolean; - application_save_option: boolean; - config_encryption_option: boolean; - encryption_password: string; - saving_logs_option: boolean; - dataCenter: string; - authSource: string; + type: string; + host: string; + port: number; + username: string; + password: string; + database: string; + schema: string; + sid: string; + ssl: boolean; + cert: string; + token: string; + app_port: number; + azure_encryption: boolean; + application_save_option: boolean; + config_encryption_option: boolean; + encryption_password: string; + saving_logs_option: boolean; + dataCenter: string; + authSource: string; } export interface ISavedCLIConnectionCredentials { - encrypted: boolean; - hash: string | null; - credentials: string | ICLIConnectionCredentials; + encrypted: boolean; + hash: string | null; + credentials: string | ICLIConnectionCredentials; } export interface ICustomFields { - id: string; - type: string; - template_string: string; - text: string; - settings: ITableSettings; + id: string; + type: string; + template_string: string; + text: string; + settings: ITableSettings; } export interface IFilteringFields { - field: string; - criteria: FilterCriteriaEnum; - value: string; + field: string; + criteria: FilterCriteriaEnum; + value: string | string[]; } export interface ForeignKeyDSInfo { - referenced_column_name: string; - referenced_table_name: string; - constraint_name: string; - column_name: string; + referenced_column_name: string; + referenced_table_name: string; + constraint_name: string; + column_name: string; } export interface IMessageData { - data: IMessageDataInfo; + data: IMessageDataInfo; } export interface IMessageDataInfo { - operationType: OperationTypeEnum; - tableName: string; - row: any; - primaryKey: any; - tableSettings: TableSettingsDS; - page: number; - perPage: number; - searchedFieldValue: string; - filteringFields: Array; - autocompleteFields: AutocompleteFieldsDS; - fieldValues: Array; - identityColumnName: string; - referencedFieldName: string; - email: string; - tableStructure: TableStructureDS[] | null; + operationType: OperationTypeEnum; + tableName: string; + row: any; + primaryKey: any; + tableSettings: TableSettingsDS; + page: number; + perPage: number; + searchedFieldValue: string; + filteringFields: Array; + autocompleteFields: AutocompleteFieldsDS; + fieldValues: Array; + identityColumnName: string; + referencedFieldName: string; + email: string; + tableStructure: TableStructureDS[] | null; } export interface IPaginationRO { - total: number; - lastPage: number; - perPage: number; - currentPage: number; + total: number; + lastPage: number; + perPage: number; + currentPage: number; } export interface IStructureInfo { - column_type: string; - data_type: string; - column_default: string; - column_name: string; - allow_null: boolean; + column_type: string; + data_type: string; + column_default: string; + column_name: string; + allow_null: boolean; } export interface ITablePrimaryColumnInfo { - data_type: string; - column_name: string; + data_type: string; + column_name: string; } export interface ITableSettings { - connection_id: string; - table_name: string; - display_name: string; - search_fields: Array; - excluded_fields: Array; - list_fields: Array; - identification_fields: Array; - list_per_page: number; - ordering: QueryOrderingEnum; - ordering_field: string; - readonly_fields: string[]; - sortable_by: string[]; - autocomplete_columns: string[]; - custom_fields?: Array; - table_widgets?: Array; + connection_id: string; + table_name: string; + display_name: string; + search_fields: Array; + excluded_fields: Array; + list_fields: Array; + identification_fields: Array; + list_per_page: number; + ordering: QueryOrderingEnum; + ordering_field: string; + readonly_fields: string[]; + sortable_by: string[]; + autocomplete_columns: string[]; + custom_fields?: Array; + table_widgets?: Array; } export interface ITableWidget { - id: string; - field_name: string; - widget_type: WidgetTypeEnum; - widget_params: Array; - name?: string; - description?: string; - settings: ITableSettings; + id: string; + field_name: string; + widget_type: WidgetTypeEnum; + widget_params: Array; + name?: string; + description?: string; + settings: ITableSettings; } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-cassandra.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-cassandra.ts index 84f9e1f6e..1c3ee0344 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-cassandra.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-cassandra.ts @@ -8,20 +8,20 @@ import { LRUStorage } from '../../caching/lru-storage.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { getTunnel } from '../../helpers/get-ssh-tunnel.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { QueryOrderingEnum } from '../../shared/enums/query-ordering.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { QueryOrderingEnum } from '../../shared/enums/query-ordering.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; export class DataAccessObjectCassandra extends BasicDataAccessObject implements IDataAccessObject { @@ -280,6 +280,27 @@ export class DataAccessObjectCassandra extends BasicDataAccessObject implements whereConditions.push(`${filter.field.toLowerCase()} <= ?`); params.push(filter.value); break; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(filter.value) + ? filter.value + : String(filter.value) + .split(',') + .map((v) => v.trim()); + const placeholders = inValues.map(() => '?').join(', '); + whereConditions.push(`${filter.field.toLowerCase()} IN (${placeholders})`); + params.push(...inValues); + break; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(filter.value) + ? filter.value + : String(filter.value) + .split(',') + .map((v) => v.trim()); + whereConditions.push(`${filter.field.toLowerCase()} >= ? AND ${filter.field.toLowerCase()} <= ?`); + params.push(betweenValues[0], betweenValues[1]); + break; + } default: break; } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-clickhouse.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-clickhouse.ts index be6bc3f88..1807efecd 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-clickhouse.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-clickhouse.ts @@ -1,28 +1,28 @@ /* eslint-disable security/detect-object-injection */ -import { createClient, ClickHouseClient } from '@clickhouse/client'; +import { ClickHouseClient, createClient } from '@clickhouse/client'; +import { NodeClickHouseClientConfigOptions } from '@clickhouse/client/dist/config.js'; import * as csv from 'csv'; import getPort from 'get-port'; import { Readable, Stream } from 'stream'; import { LRUStorage } from '../../caching/lru-storage.js'; -import { getTunnel } from '../../helpers/get-ssh-tunnel.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; +import { getTunnel } from '../../helpers/get-ssh-tunnel.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; -import { NodeClickHouseClientConfigOptions } from '@clickhouse/client/dist/config.js'; export class DataAccessObjectClickHouse extends BasicDataAccessObject implements IDataAccessObject { public async addRowInTable(tableName: string, row: Record): Promise> { @@ -261,6 +261,27 @@ export class DataAccessObjectClickHouse extends BasicDataAccessObject implements case FilterCriteriaEnum.empty: whereClauses.push(`${escapedField} IS NULL`); break; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + const sanitizedValues = inValues.map((v) => this.sanitizeValue(v)).join(', '); + whereClauses.push(`${escapedField} IN (${sanitizedValues})`); + break; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + whereClauses.push( + `${escapedField} BETWEEN ${this.sanitizeValue(betweenValues[0])} AND ${this.sanitizeValue(betweenValues[1])}`, + ); + break; + } } } } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-dynamodb.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-dynamodb.ts index d19d0fa04..9927fe1f8 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-dynamodb.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-dynamodb.ts @@ -1,12 +1,12 @@ /* eslint-disable security/detect-object-injection */ import { + BatchGetItemCommand, DeleteItemCommand, DynamoDB, GetItemCommand, PutItemCommand, ScanCommand, UpdateItemCommand, - BatchGetItemCommand, } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; import { marshall } from '@aws-sdk/util-dynamodb'; @@ -16,20 +16,20 @@ import { binaryToHex, hexToBinary } from '../../helpers/binary-hex-string-conver import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { isObjectEmpty } from '../../helpers/is-object-empty.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { QueryOrderingEnum } from '../../shared/enums/query-ordering.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { DynamoDBType, TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { QueryOrderingEnum } from '../../shared/enums/query-ordering.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; export type DdAndClient = { @@ -344,6 +344,36 @@ export class DataAccessObjectDynamoDB extends BasicDataAccessObject implements I case FilterCriteriaEnum.empty: filterExpression += ` AND attribute_not_exists(#${field})`; break; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + const placeholders = inValues.map((_, i) => `${uniquePlaceholder}_${i}`); + filterExpression += ` AND #${field} IN (${placeholders.join(', ')})`; + inValues.forEach((val, i) => { + expressionAttributeValues[`${uniquePlaceholder}_${i}`] = isNumberField + ? { N: String(val) } + : { S: String(val) }; + }); + break; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + filterExpression += ` AND #${field} BETWEEN ${uniquePlaceholder}_low AND ${uniquePlaceholder}_high`; + expressionAttributeValues[`${uniquePlaceholder}_low`] = isNumberField + ? { N: String(betweenValues[0]) } + : { S: String(betweenValues[0]) }; + expressionAttributeValues[`${uniquePlaceholder}_high`] = isNumberField + ? { N: String(betweenValues[1]) } + : { S: String(betweenValues[1]) }; + break; + } default: break; } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-elasticsearch.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-elasticsearch.ts index 501cfa8c5..4615b9af4 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-elasticsearch.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-elasticsearch.ts @@ -4,19 +4,19 @@ import * as csv from 'csv'; import { Readable, Stream } from 'stream'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; export class DataAccessObjectElasticsearch extends BasicDataAccessObject implements IDataAccessObject { @@ -284,6 +284,26 @@ export class DataAccessObjectElasticsearch extends BasicDataAccessObject impleme exists: { field }, }); break; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + searchQuery.query.bool.must.push({ terms: { [field]: inValues } }); + break; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + searchQuery.query.bool.must.push({ + range: { [field]: { gte: betweenValues[0], lte: betweenValues[1] } }, + }); + break; + } } } } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts index b228d7bfb..e6c1427d9 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts @@ -316,6 +316,25 @@ export class DataAccessObjectIbmDb2 extends BasicDataAccessObject implements IDa return `${filterObject.field} NOT LIKE ?`; case FilterCriteriaEnum.empty: return `(${filterObject.field} IS NULL)`; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(filterObject.value) + ? filterObject.value + : String(filterObject.value) + .split(',') + .map((v) => v.trim()); + const placeholders = inValues.map(() => '?').join(', '); + queryParams.push(...(inValues as SQLParam[])); + return `${filterObject.field} IN (${placeholders})`; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(filterObject.value) + ? filterObject.value + : String(filterObject.value) + .split(',') + .map((v) => v.trim()); + queryParams.push(betweenValues[0] as SQLParam, betweenValues[1] as SQLParam); + return `${filterObject.field} BETWEEN ? AND ?`; + } } }) .join(' AND '); diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts index 3adf63c21..5d7c814cf 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts @@ -11,6 +11,8 @@ import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; import { getTunnel } from '../../helpers/get-ssh-tunnel.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { ConnectionParams } from '../shared/data-structures/connections-params.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; @@ -18,13 +20,11 @@ import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; export type MongoClientDB = { @@ -275,6 +275,24 @@ export class DataAccessObjectMongo extends BasicDataAccessObject implements IDat case FilterCriteriaEnum.empty: acc[field].$exists = false; break; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + acc[field] = { $in: inValues }; + break; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + acc[field] = { $gte: betweenValues[0], $lte: betweenValues[1] }; + break; + } default: break; } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mssql.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mssql.ts index a3499f76b..0b64730cb 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mssql.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mssql.ts @@ -1,8 +1,9 @@ /* eslint-disable security/detect-object-injection */ + +import { Readable, Stream } from 'node:stream'; import * as csv from 'csv'; import { Knex } from 'knex'; import { nanoid } from 'nanoid'; -import { Readable, Stream } from 'node:stream'; import { LRUStorage } from '../../caching/lru-storage.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; @@ -10,20 +11,20 @@ import { isMSSQLDateOrTimeType, isMSSQLDateStringByRegexp } from '../../helpers/ import { objectKeysToLowercase } from '../../helpers/object-kyes-to-lowercase.js'; import { renameObjectKeyName } from '../../helpers/rename-object-keyname.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { QueryOrderingEnum } from '../../shared/enums/query-ordering.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { QueryOrderingEnum } from '../../shared/enums/query-ordering.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; export class DataAccessObjectMssql extends BasicDataAccessObject implements IDataAccessObject { @@ -216,7 +217,23 @@ export class DataAccessObjectMssql extends BasicDataAccessObject implements IDat [FilterCriteriaEnum.icontains]: `%${value}%`, [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }); @@ -592,7 +609,23 @@ WHERE TABLE_TYPE = 'VIEW' [FilterCriteriaEnum.icontains]: `%${value}%`, [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }) diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mysql.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mysql.ts index 8bc453d3a..a69784039 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mysql.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mysql.ts @@ -1,33 +1,34 @@ /* eslint-disable security/detect-object-injection */ + +import { Readable, Stream } from 'node:stream'; +import * as csv from 'csv'; +import { Knex } from 'knex'; +import { nanoid } from 'nanoid'; import { LRUStorage } from '../../caching/lru-storage.js'; import { checkFieldAutoincrement } from '../../helpers/check-field-autoincrement.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; +import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; +import { getNumbersFromString } from '../../helpers/get-numbers-from-string.js'; import { getPropertyValueByDescriptor } from '../../helpers/get-property-value-by-descriptor.js'; +import { isMySQLDateStringByRegexp, isMySqlDateOrTimeType } from '../../helpers/is-database-date.js'; +import { objectKeysToLowercase } from '../../helpers/object-kyes-to-lowercase.js'; +import { renameObjectKeyName } from '../../helpers/rename-object-keyname.js'; import { setPropertyValue } from '../../helpers/set-property-value.js'; +import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; -import { objectKeysToLowercase } from '../../helpers/object-kyes-to-lowercase.js'; -import { Knex } from 'knex'; -import { renameObjectKeyName } from '../../helpers/rename-object-keyname.js'; -import { getNumbersFromString } from '../../helpers/get-numbers-from-string.js'; -import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; -import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; -import { Stream, Readable } from 'node:stream'; -import * as csv from 'csv'; -import { isMySqlDateOrTimeType, isMySQLDateStringByRegexp } from '../../helpers/is-database-date.js'; -import { nanoid } from 'nanoid'; export class DataAccessObjectMysql extends BasicDataAccessObject implements IDataAccessObject { public async addRowInTable( @@ -258,7 +259,23 @@ export class DataAccessObjectMysql extends BasicDataAccessObject implements IDat [FilterCriteriaEnum.icontains]: `%${value}%`, [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } return builder; @@ -640,7 +657,23 @@ export class DataAccessObjectMysql extends BasicDataAccessObject implements IDat [FilterCriteriaEnum.icontains]: `%${value}%`, [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }) diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts index b97f97f04..1a784c8bc 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts @@ -1,8 +1,9 @@ /* eslint-disable security/detect-object-injection */ + +import { Readable, Stream } from 'node:stream'; import * as csv from 'csv'; import { Knex } from 'knex'; import { nanoid } from 'nanoid'; -import { Readable, Stream } from 'node:stream'; import { LRUStorage } from '../../caching/lru-storage.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; @@ -15,19 +16,19 @@ import { import { objectKeysToLowercase } from '../../helpers/object-kyes-to-lowercase.js'; import { renameObjectKeyName } from '../../helpers/rename-object-keyname.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; type RefererencedConstraint = { @@ -334,7 +335,23 @@ export class DataAccessObjectOracle extends BasicDataAccessObject implements IDa [FilterCriteriaEnum.icontains]: `%${value}%`, [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }; @@ -800,7 +817,23 @@ export class DataAccessObjectOracle extends BasicDataAccessObject implements IDa [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }) diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts index 186846279..e11f55334 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts @@ -1,27 +1,28 @@ /* eslint-disable security/detect-object-injection */ + +import { Readable, Stream } from 'node:stream'; import * as csv from 'csv'; import { Knex } from 'knex'; -import { Readable, Stream } from 'node:stream'; +import { nanoid } from 'nanoid'; import { LRUStorage } from '../../caching/lru-storage.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; import { isPostgresDateOrTimeType, isPostgresDateStringByRegexp } from '../../helpers/is-database-date.js'; import { tableSettingsFieldValidator } from '../../helpers/validation/table-settings-validator.js'; +import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; +import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { AutocompleteFieldsDS } from '../shared/data-structures/autocomplete-fields.ds.js'; import { FilteringFieldsDS } from '../shared/data-structures/filtering-fields.ds.js'; import { ForeignKeyDS } from '../shared/data-structures/foreign-key.ds.js'; import { FoundRowsDS } from '../shared/data-structures/found-rows.ds.js'; import { PrimaryKeyDS } from '../shared/data-structures/primary-key.ds.js'; import { ReferencedTableNamesAndColumnsDS } from '../shared/data-structures/referenced-table-names-columns.ds.js'; +import { TableDS } from '../shared/data-structures/table.ds.js'; import { TableSettingsDS } from '../shared/data-structures/table-settings.ds.js'; import { TableStructureDS } from '../shared/data-structures/table-structure.ds.js'; -import { TableDS } from '../shared/data-structures/table.ds.js'; import { TestConnectionResultDS } from '../shared/data-structures/test-result-connection.ds.js'; import { ValidateTableSettingsDS } from '../shared/data-structures/validate-table-settings.ds.js'; -import { FilterCriteriaEnum } from '../../shared/enums/filter-criteria.enum.js'; -import { IDataAccessObject } from '../../shared/interfaces/data-access-object.interface.js'; import { BasicDataAccessObject } from './basic-data-access-object.js'; -import { nanoid } from 'nanoid'; export class DataAccessObjectPostgres extends BasicDataAccessObject implements IDataAccessObject { public async addRowInTable( @@ -224,7 +225,23 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I [FilterCriteriaEnum.icontains]: `%${value}%`, [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }); @@ -660,7 +677,23 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I [FilterCriteriaEnum.empty]: null, }; - builder.where(field, operators[criteria], values[criteria] || value); + if (criteria === FilterCriteriaEnum.in) { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereIn(field, inValues); + } else if (criteria === FilterCriteriaEnum.between) { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + builder.whereBetween(field, [betweenValues[0], betweenValues[1]]); + } else { + builder.where(field, operators[criteria], values[criteria] || value); + } } } }) diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts index fb9ebba52..94ec0702f 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-redis.ts @@ -1849,6 +1849,24 @@ export class DataAccessObjectRedis extends BasicDataAccessObject implements IDat case FilterCriteriaEnum.empty: result = rowValue === null || rowValue === undefined || rowValue === ''; break; + case FilterCriteriaEnum.in: { + const inValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + result = inValues.some((v) => String(rowValue) === String(v)); + break; + } + case FilterCriteriaEnum.between: { + const betweenValues = Array.isArray(value) + ? value + : String(value) + .split(',') + .map((v) => v.trim()); + result = Number(rowValue) >= Number(betweenValues[0]) && Number(rowValue) <= Number(betweenValues[1]); + break; + } default: result = true; break; diff --git a/shared-code/src/shared/enums/filter-criteria.enum.ts b/shared-code/src/shared/enums/filter-criteria.enum.ts index 85b725133..99bf56672 100644 --- a/shared-code/src/shared/enums/filter-criteria.enum.ts +++ b/shared-code/src/shared/enums/filter-criteria.enum.ts @@ -9,4 +9,6 @@ export enum FilterCriteriaEnum { icontains = 'icontains', eq = 'eq', empty = 'empty', + in = 'in', + between = 'between', }