From ed2e2b9887073e01eb8273f112936d0699d31f87 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Tue, 5 Aug 2025 09:17:15 +0000 Subject: [PATCH] fix dynamodb filters --- .../saas-tests/table-dynamodb-e2e.test.ts | 90 +++++++++++++++++++ backend/test/utils/create-test-table.ts | 10 ++- .../data-access-object-dynamodb.ts | 17 ++-- 3 files changed, 110 insertions(+), 7 deletions(-) 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 f747df2ca..60cd5300e 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 @@ -1526,6 +1526,96 @@ should return all found rows with search, pagination: page=1, perPage=2 and DESC }, ); +test.serial(`${currentTest} with search, with pagination, with sorting and with filtering by id +should return all found rows with search, pagination: page=1, perPage=2 and DESC sorting and with multi filtering`, async (t) => { + try { + const connectionToTestDB = getTestData(mockFactory).dynamoDBConnection; + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + const { testTableName, testTableColumnName, insertedSearchedIds } = 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 createTableSettingsDTO = mockFactory.generateTableSettings( + createConnectionRO.id, + testTableName, + [], + undefined, + undefined, + 3, + QueryOrderingEnum.DESC, + 'id', + undefined, + undefined, + undefined, + undefined, + undefined, + ); + + const createTableSettingsResponse = await request(app.getHttpServer()) + .post(`/settings?connectionId=${createConnectionRO.id}&tableName=${testTableName}`) + .send(createTableSettingsDTO) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(createTableSettingsResponse.status, 201); + + const fieldname = 'age'; + const idFieldName = 'id'; + const idFieldValue = 21; + const fieldGtvalue = 14; + // const fieldLtvalue = 95; + + const filters = { + // [fieldname]: { gt: fieldGtvalue }, + [idFieldName]: { eq: idFieldValue }, + }; + + const getTableRowsResponse = await request(app.getHttpServer()) + .post( + `/table/rows/find/${createConnectionRO.id}?tableName=${testTableName}&page=1&perPage=3`, + ) + .send({ filters }) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getTableRowsResponse.status, 201); + + const getTableRowsRO = JSON.parse(getTableRowsResponse.text); + console.log('🚀 ~ getTableRowsRO:', getTableRowsRO.rows); + + t.is(typeof getTableRowsRO, 'object'); + t.is(getTableRowsRO.hasOwnProperty('rows'), true); + t.is(getTableRowsRO.hasOwnProperty('primaryColumns'), true); + t.is(getTableRowsRO.hasOwnProperty('pagination'), true); + t.is(getTableRowsRO.rows.length, 1); + t.is(Object.keys(getTableRowsRO.rows[0]).length, 11); + + const findRowId = 21; + + t.is(getTableRowsRO.rows[0].id, findRowId); + t.is(getTableRowsRO.rows[0][testTableColumnName], testSearchedUserName); + + t.is(getTableRowsRO.pagination.currentPage, 1); + t.is(getTableRowsRO.pagination.perPage, 3); + + t.is(typeof getTableRowsRO.primaryColumns, 'object'); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('column_name'), true); + t.is(getTableRowsRO.primaryColumns[0].hasOwnProperty('data_type'), true); + } catch (e) { + console.error(e); + throw e; + } +}); + test.serial(`${currentTest} should throw an exception when connection id is not passed in request`, async (t) => { try { const connectionToTestDB = getTestData(mockFactory).dynamoDBConnection; diff --git a/backend/test/utils/create-test-table.ts b/backend/test/utils/create-test-table.ts index ff920a63a..9fa14a9a7 100644 --- a/backend/test/utils/create-test-table.ts +++ b/backend/test/utils/create-test-table.ts @@ -525,7 +525,7 @@ export async function createTestDynamoDBTable( } catch (error) { console.error(`Error creating dynamodb table: ${error.message}`); } - + const insertedSearchedIds: Array<{ number: number; id: string }> = []; const documentClient = DynamoDBDocumentClient.from(dynamoDb); try { for (let i = 0; i < testEntitiesSeedsCount; i++) { @@ -552,6 +552,13 @@ export async function createTestDynamoDBTable( binary_set_column: { BS: [Buffer.from('value1'), Buffer.from('value2')] }, }; + if (isSearchedUser) { + insertedSearchedIds.push({ + number: i, + id: String(item.id.N), + }); + } + const params: PutItemCommandInput = { TableName: testTableName, Item: item as any, @@ -568,6 +575,7 @@ export async function createTestDynamoDBTable( testTableColumnName: testTableColumnName, testTableSecondColumnName: testTableSecondColumnName, testEntitiesSeedsCount: testEntitiesSeedsCount, + insertedSearchedIds, }; } 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 f50c972e8..579887144 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 @@ -305,10 +305,13 @@ export class DataAccessObjectDynamoDB extends BasicDataAccessObject implements I const { field, criteria, value } = filter; expressionAttributeNames[`#${field}`] = field; const uniquePlaceholder = `:${field}${index}`; + const fieldInfo = tableStructure.find((el) => el.column_name === field); + const isNumberField = fieldInfo?.data_type === 'number'; + switch (criteria) { case FilterCriteriaEnum.eq: filterExpression += ` AND #${field} = ${uniquePlaceholder}`; - expressionAttributeValues[uniquePlaceholder] = { S: value }; + expressionAttributeValues[uniquePlaceholder] = isNumberField ? { N: String(value) } : { S: value }; break; case FilterCriteriaEnum.contains: filterExpression += ` AND contains(#${field}, ${uniquePlaceholder})`; @@ -316,19 +319,19 @@ export class DataAccessObjectDynamoDB extends BasicDataAccessObject implements I break; case FilterCriteriaEnum.gt: filterExpression += ` AND #${field} > ${uniquePlaceholder}`; - expressionAttributeValues[uniquePlaceholder] = { N: value }; + expressionAttributeValues[uniquePlaceholder] = isNumberField ? { N: String(value) } : { S: value }; break; case FilterCriteriaEnum.lt: filterExpression += ` AND #${field} < ${uniquePlaceholder}`; - expressionAttributeValues[uniquePlaceholder] = { N: value }; + expressionAttributeValues[uniquePlaceholder] = isNumberField ? { N: String(value) } : { S: value }; break; case FilterCriteriaEnum.gte: filterExpression += ` AND #${field} >= ${uniquePlaceholder}`; - expressionAttributeValues[uniquePlaceholder] = { N: value }; + expressionAttributeValues[uniquePlaceholder] = isNumberField ? { N: String(value) } : { S: value }; break; case FilterCriteriaEnum.lte: filterExpression += ` AND #${field} <= ${uniquePlaceholder}`; - expressionAttributeValues[uniquePlaceholder] = { N: value }; + expressionAttributeValues[uniquePlaceholder] = isNumberField ? { N: String(value) } : { S: value }; break; case FilterCriteriaEnum.icontains: filterExpression += ` AND NOT contains(#${field}, ${uniquePlaceholder})`; @@ -348,6 +351,9 @@ export class DataAccessObjectDynamoDB extends BasicDataAccessObject implements I default: break; } + if (index === 0) { + filterExpression = filterExpression.replace(/^ AND /, ''); + } }); } @@ -383,7 +389,6 @@ export class DataAccessObjectDynamoDB extends BasicDataAccessObject implements I const result = await dynamoDb.scan(params); rows = rows.concat(result.Items); - lastEvaluatedKey = result.LastEvaluatedKey; } while (lastEvaluatedKey);