From 0e198bae37718248a06c9a633d13ac8be1f16e0a Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 12 Mar 2026 09:17:15 +0000 Subject: [PATCH 1/2] Add comprehensive end-to-end tests for composite primary key and referenced tables - Implement tests for GET, POST, PUT, and DELETE operations on composite primary key tables. - Validate table structure retrieval for composite primary key tables and referenced tables with foreign keys. - Add pagination tests for retrieving rows from composite key tables. - Include bulk delete and bulk update tests for composite primary key tables. - Ensure error handling for missing parameters and duplicate keys. - Test cascade delete behavior when removing rows from main composite key tables. - Validate response structures for various endpoints. --- .../complex-ibmdb2-table-e2e.test.ts | 936 ++++++++++++++++++ .../complex-mssql-table-e2e.test.ts | 936 ++++++++++++++++++ .../complex-mysql-table-e2e.test.ts | 936 ++++++++++++++++++ .../complex-oracle-table-e2e.test.ts | 936 ++++++++++++++++++ .../complex-postgres-table-e2e.test.ts | 936 ++++++++++++++++++ 5 files changed, 4680 insertions(+) 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 01bc6d58e..fc325c88c 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 @@ -230,3 +230,939 @@ test.serial( } }, ); + +// GET /table/structure/:connectionId + +test.serial( + `GET /table/structure/:connectionId - Should return table structure for composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const structureRO = JSON.parse(getStructureResponse.text); + t.is(getStructureResponse.status, 200); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + t.truthy(structureRO.structure.length > 0); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + for (const expectedCol of main_table.column_names) { + t.truthy(columnNames.includes(expectedCol), `Structure should include column ${expectedCol}`); + } + + const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'order_id'); + const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'customer_id'); + t.truthy(orderIdCol); + t.truthy(customerIdCol); + + // Validate primaryColumns + t.truthy(structureRO.primaryColumns); + const primaryColumnNames = structureRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `GET /table/structure/:connectionId - Should return structure for referenced table with foreign keys`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getStructureResponse.status, 200); + const structureRO = JSON.parse(getStructureResponse.text); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + t.truthy(columnNames.includes('item_id')); + t.truthy(columnNames.includes('order_id')); + t.truthy(columnNames.includes('customer_id')); + t.truthy(columnNames.includes('product_name')); + t.truthy(columnNames.includes('quantity')); + t.truthy(columnNames.includes('price_per_unit')); + + // Validate foreignKeys are present for referenced table + t.truthy(structureRO.foreignKeys); + t.truthy(structureRO.foreignKeys.length > 0); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/rows/:connectionId - Pagination tests + +test.serial(`GET /table/rows/:connectionId - Should return paginated rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const perPage = 10; + const getRowsPage1Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage1Response.status, 200); + const page1RO = JSON.parse(getRowsPage1Response.text); + t.truthy(page1RO.rows); + t.is(page1RO.rows.length, perPage); + t.truthy(page1RO.pagination); + t.is(page1RO.pagination.currentPage, 1); + t.is(page1RO.pagination.perPage, perPage); + t.truthy(page1RO.pagination.total >= 42); + + const getRowsPage2Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage2Response.status, 200); + const page2RO = JSON.parse(getRowsPage2Response.text); + t.truthy(page2RO.rows); + t.is(page2RO.rows.length, perPage); + t.is(page2RO.pagination.currentPage, 2); + + const page1OrderIds = page1RO.rows.map((r: any) => r.ORDER_ID); + const page2OrderIds = page2RO.rows.map((r: any) => r.ORDER_ID); + for (const id of page2OrderIds) { + t.falsy(page1OrderIds.includes(id), 'Page 2 rows should not overlap with page 1'); + } + } catch (e) { + console.error(e); + throw e; + } +}); + +// POST /table/row/:connectionId - Add row + +test.serial(`POST /table/row/:connectionId - Should add a new row to composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const newRow = { + order_id: 9999, + customer_id: 9999, + order_date: '2025-01-15', + status: 'Pending', + total_amount: 150.5, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.is(addRowRO.row.ORDER_ID, 9999); + t.is(addRowRO.row.CUSTOMER_ID, 9999); + t.is(addRowRO.row.STATUS, 'Pending'); + t.truthy(addRowRO.structure); + t.truthy(addRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/row/:connectionId - Should add a new row to simple foreign key referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesSimpleKeysData; + + // First get an existing ORDER_ID + const getMainRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const mainRowsRO = JSON.parse(getMainRowsResponse.text); + const existingOrderId = mainRowsRO.rows[0].ORDER_ID; + + const newOrderItem = { + order_id: existingOrderId, + product_name: 'Test Product', + quantity: 3, + price_per_unit: 25.0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newOrderItem) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.truthy(addRowRO.row.ITEM_ID); + t.is(addRowRO.row.PRODUCT_NAME, 'Test Product'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/row/:connectionId - Get single row + +test.serial(`GET /table/row/:connectionId - Should return a single row by composite primary key`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=1&CUSTOMER_ID=101`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.is(getRowRO.row.ORDER_ID, 1); + t.is(getRowRO.row.CUSTOMER_ID, 101); + t.truthy(getRowRO.row.STATUS); + t.truthy(getRowRO.structure); + t.truthy(getRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return a single row by simple primary key from referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesSimpleKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&ITEM_ID=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.truthy(getRowRO.row.ITEM_ID); + t.truthy(getRowRO.row.ORDER_ID); + t.truthy(getRowRO.structure); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/row/:connectionId - Update row + +test.serial(`PUT /table/row/:connectionId - Should update a row in composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const updatedValues = { + status: 'Delivered', + total_amount: 999.99, + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=1&CUSTOMER_ID=101`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.STATUS, 'Delivered'); + t.is(parseFloat(updateRowRO.row.TOTAL_AMOUNT), 999.99); + t.is(updateRowRO.row.ORDER_ID, 1); + t.is(updateRowRO.row.CUSTOMER_ID, 101); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`PUT /table/row/:connectionId - Should update a row in simple primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesSimpleKeysData; + + const updatedValues = { + status: 'Cancelled', + total_amount: 555.55, + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=1`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.STATUS, 'Cancelled'); + t.is(parseFloat(updateRowRO.row.TOTAL_AMOUNT), 555.55); + } catch (e) { + console.error(e); + throw e; + } +}); + +// DELETE /table/row/:connectionId - Delete row + +test.serial(`DELETE /table/row/:connectionId - Should delete a row from composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Delete the row we added earlier (9999, 9999) + const deleteRowResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=9999&CUSTOMER_ID=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowResponse.status, 200); + const deleteRowRO = JSON.parse(deleteRowResponse.text); + t.truthy(deleteRowRO.row); + + // Verify the row is deleted + const getDeletedRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=9999&CUSTOMER_ID=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getDeletedRowResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +// PUT /table/rows/delete/:connectionId - Bulk delete + +test.serial( + `PUT /table/rows/delete/:connectionId - Should bulk delete rows from composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // First add rows to delete + const rowsToAdd = [ + { order_id: 8881, customer_id: 8881, status: 'Pending', total_amount: 10.0 }, + { order_id: 8882, customer_id: 8882, status: 'Pending', total_amount: 20.0 }, + { order_id: 8883, customer_id: 8883, status: 'Pending', total_amount: 30.0 }, + ]; + + for (const row of rowsToAdd) { + const addResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(row) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addResp.status, 201); + } + + const primaryKeysToDelete = [ + { ORDER_ID: 8881, CUSTOMER_ID: 8881 }, + { ORDER_ID: 8882, CUSTOMER_ID: 8882 }, + { ORDER_ID: 8883, CUSTOMER_ID: 8883 }, + ]; + + const bulkDeleteResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(primaryKeysToDelete) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkDeleteResponse.status, 200); + + // Verify rows are deleted + for (const pk of primaryKeysToDelete) { + const getResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=${pk.ORDER_ID}&CUSTOMER_ID=${pk.CUSTOMER_ID}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getResp.status, 400); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/rows/update/:connectionId - Bulk update + +test.serial( + `PUT /table/rows/update/:connectionId - Should bulk update rows in composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const bulkUpdateBody = { + primaryKeys: [ + { ORDER_ID: 2, CUSTOMER_ID: 102 }, + { ORDER_ID: 3, CUSTOMER_ID: 103 }, + ], + newValues: { + status: 'Cancelled', + }, + }; + + const bulkUpdateResponse = await request(app.getHttpServer()) + .put(`/table/rows/update/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(bulkUpdateBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkUpdateResponse.status, 200); + + // Verify rows are updated + const getRow1Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=2&CUSTOMER_ID=102`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow1Response.status, 200); + const row1RO = JSON.parse(getRow1Response.text); + t.is(row1RO.row.STATUS, 'Cancelled'); + + const getRow2Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=3&CUSTOMER_ID=103`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow2Response.status, 200); + const row2RO = JSON.parse(getRow2Response.text); + t.is(row2RO.row.STATUS, 'Cancelled'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// POST /table/rows/find/:connectionId - Rows with body filters + +test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + ORDER_ID: { eq: 1 }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + t.is(findRowsRO.rows.length, 1); + t.is(findRowsRO.rows[0].ORDER_ID, 1); + t.is(findRowsRO.rows[0].CUSTOMER_ID, 101); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/rows/find/:connectionId - Should return rows filtered by status for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + STATUS: { eq: 'Cancelled' }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + for (const row of findRowsRO.rows) { + t.is(row.STATUS, 'Cancelled'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /connection/tables/:connectionId - List tables + +test.serial(`GET /connection/tables/:connectionId - Should list all complex test tables in connection`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getTablesResponse.status, 200); + const tablesRO = JSON.parse(getTablesResponse.text); + t.truthy(Array.isArray(tablesRO)); + + const tableNames = tablesRO.map((t: any) => t.table); + + const { main_table: compositeMain } = testTablesCompositeKeysData; + const { main_table: simpleMain } = testTablesSimpleKeysData; + + t.truthy(tableNames.includes(compositeMain.table_name), `Tables should include ${compositeMain.table_name}`); + t.truthy(tableNames.includes(simpleMain.table_name), `Tables should include ${simpleMain.table_name}`); + } catch (e) { + console.error(e); + throw e; + } +}); + +// GET /table/rows/:connectionId - Response structure validation + +test.serial( + `GET /table/rows/:connectionId - Should return correct response structure with primaryColumns and pagination for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 200); + const rowsRO = JSON.parse(getRowsResponse.text); + + // Validate response has required fields + t.truthy(rowsRO.rows); + t.truthy(rowsRO.primaryColumns); + t.truthy(rowsRO.pagination); + + // Validate primaryColumns includes both composite key columns + const primaryColumnNames = rowsRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + t.is(rowsRO.primaryColumns.length, 2); + + // Validate row data + t.truthy(rowsRO.rows.length > 0); + const firstRow = rowsRO.rows[0]; + t.truthy('ORDER_ID' in firstRow); + t.truthy('CUSTOMER_ID' in firstRow); + t.truthy('STATUS' in firstRow); + t.truthy('TOTAL_AMOUNT' in firstRow); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Cascade delete behavior + +test.serial( + `DELETE /table/row/:connectionId - Should cascade delete referenced rows when deleting from main composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesCompositeKeysData; + + // Add a new row to main table + const newMainRow = { + order_id: 7777, + customer_id: 7777, + status: 'Pending', + total_amount: 100.0, + }; + const addMainResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newMainRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addMainResp.status, 201); + + // Add a referenced row + const newReferencedRow = { + order_id: 7777, + customer_id: 7777, + product_name: 'Test Product', + quantity: 5, + price_per_unit: 20.0, + }; + const addRefResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newReferencedRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRefResp.status, 201); + const addedRefRow = JSON.parse(addRefResp.text); + const refRowItemId = addedRefRow.row.ITEM_ID; + + // Delete the main row (should cascade) + const deleteResp = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=7777&CUSTOMER_ID=7777`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteResp.status, 200); + + // Verify referenced row is also deleted (CASCADE) + const getRefResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&ITEM_ID=${refRowItemId}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getRefResp.status, 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Error cases + +test.serial(`GET /table/rows/:connectionId - Should return error when tableName is missing`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return error when primary key is incomplete for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Only provide one part of composite key + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&ORDER_ID=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Should fail because composite key requires both ORDER_ID and CUSTOMER_ID + t.truthy(getRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `POST /table/row/:connectionId - Should return error when adding row with duplicate composite primary key`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Try to add a row with existing composite key (1, 101) + const duplicateRow = { + order_id: 1, + customer_id: 101, + status: 'Duplicate', + total_amount: 0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(duplicateRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.truthy(addRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); 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 375f908cf..38eea6be4 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 @@ -230,3 +230,939 @@ test.serial( } }, ); + +// GET /table/structure/:connectionId + +test.serial( + `GET /table/structure/:connectionId - Should return table structure for composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const structureRO = JSON.parse(getStructureResponse.text); + t.is(getStructureResponse.status, 200); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + t.truthy(structureRO.structure.length > 0); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + for (const expectedCol of main_table.column_names) { + t.truthy(columnNames.includes(expectedCol), `Structure should include column ${expectedCol}`); + } + + const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'order_id'); + const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'customer_id'); + t.truthy(orderIdCol); + t.truthy(customerIdCol); + + // Validate primaryColumns + t.truthy(structureRO.primaryColumns); + const primaryColumnNames = structureRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `GET /table/structure/:connectionId - Should return structure for referenced table with foreign keys`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getStructureResponse.status, 200); + const structureRO = JSON.parse(getStructureResponse.text); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + t.truthy(columnNames.includes('item_id')); + t.truthy(columnNames.includes('order_id')); + t.truthy(columnNames.includes('customer_id')); + t.truthy(columnNames.includes('product_name')); + t.truthy(columnNames.includes('quantity')); + t.truthy(columnNames.includes('price_per_unit')); + + // Validate foreignKeys are present for referenced table + t.truthy(structureRO.foreignKeys); + t.truthy(structureRO.foreignKeys.length > 0); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/rows/:connectionId - Pagination tests + +test.serial(`GET /table/rows/:connectionId - Should return paginated rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const perPage = 10; + const getRowsPage1Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage1Response.status, 200); + const page1RO = JSON.parse(getRowsPage1Response.text); + t.truthy(page1RO.rows); + t.is(page1RO.rows.length, perPage); + t.truthy(page1RO.pagination); + t.is(page1RO.pagination.currentPage, 1); + t.is(page1RO.pagination.perPage, perPage); + t.truthy(page1RO.pagination.total >= 42); + + const getRowsPage2Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage2Response.status, 200); + const page2RO = JSON.parse(getRowsPage2Response.text); + t.truthy(page2RO.rows); + t.is(page2RO.rows.length, perPage); + t.is(page2RO.pagination.currentPage, 2); + + const page1OrderIds = page1RO.rows.map((r: any) => r.order_id); + const page2OrderIds = page2RO.rows.map((r: any) => r.order_id); + for (const id of page2OrderIds) { + t.falsy(page1OrderIds.includes(id), 'Page 2 rows should not overlap with page 1'); + } + } catch (e) { + console.error(e); + throw e; + } +}); + +// POST /table/row/:connectionId - Add row + +test.serial(`POST /table/row/:connectionId - Should add a new row to composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const newRow = { + order_id: 9999, + customer_id: 9999, + order_date: '2025-01-15', + status: 'Pending', + total_amount: 150.5, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.is(addRowRO.row.order_id, 9999); + t.is(addRowRO.row.customer_id, 9999); + t.is(addRowRO.row.status, 'Pending'); + t.truthy(addRowRO.structure); + t.truthy(addRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/row/:connectionId - Should add a new row to simple foreign key referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesSimpleKeysData; + + // First get an existing customer_id + const getMainRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const mainRowsRO = JSON.parse(getMainRowsResponse.text); + const existingCustomerId = mainRowsRO.rows[0].customer_id; + + const newOrder = { + customer_id: existingCustomerId, + order_date: '2025-03-01', + status: 'Shipped', + total_amount: 250.0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newOrder) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.truthy(addRowRO.row.order_id); + t.is(addRowRO.row.status, 'Shipped'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/row/:connectionId - Get single row + +test.serial(`GET /table/row/:connectionId - Should return a single row by composite primary key`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.is(getRowRO.row.order_id, 1); + t.is(getRowRO.row.customer_id, 100); + t.truthy(getRowRO.row.status); + t.truthy(getRowRO.structure); + t.truthy(getRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return a single row by simple primary key from referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesSimpleKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.truthy(getRowRO.row.order_id); + t.truthy(getRowRO.row.customer_id); + t.truthy(getRowRO.structure); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/row/:connectionId - Update row + +test.serial(`PUT /table/row/:connectionId - Should update a row in composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const updatedValues = { + status: 'Delivered', + total_amount: 999.99, + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.status, 'Delivered'); + t.is(parseFloat(updateRowRO.row.total_amount), 999.99); + t.is(updateRowRO.row.order_id, 1); + t.is(updateRowRO.row.customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`PUT /table/row/:connectionId - Should update a row in simple primary key referenced table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesSimpleKeysData; + + const updatedValues = { + name: 'Updated Customer Name', + email: 'updated@test.com', + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&customer_id=1`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.name, 'Updated Customer Name'); + t.is(updateRowRO.row.email, 'updated@test.com'); + } catch (e) { + console.error(e); + throw e; + } +}); + +// DELETE /table/row/:connectionId - Delete row + +test.serial(`DELETE /table/row/:connectionId - Should delete a row from composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Delete the row we added earlier (9999, 9999) + const deleteRowResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowResponse.status, 200); + const deleteRowRO = JSON.parse(deleteRowResponse.text); + t.truthy(deleteRowRO.row); + + // Verify the row is deleted + const getDeletedRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getDeletedRowResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +// PUT /table/rows/delete/:connectionId - Bulk delete + +test.serial( + `PUT /table/rows/delete/:connectionId - Should bulk delete rows from composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // First add rows to delete + const rowsToAdd = [ + { order_id: 8881, customer_id: 8881, status: 'Pending', total_amount: 10.0 }, + { order_id: 8882, customer_id: 8882, status: 'Pending', total_amount: 20.0 }, + { order_id: 8883, customer_id: 8883, status: 'Pending', total_amount: 30.0 }, + ]; + + for (const row of rowsToAdd) { + const addResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(row) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addResp.status, 201); + } + + const primaryKeysToDelete = [ + { order_id: 8881, customer_id: 8881 }, + { order_id: 8882, customer_id: 8882 }, + { order_id: 8883, customer_id: 8883 }, + ]; + + const bulkDeleteResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(primaryKeysToDelete) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkDeleteResponse.status, 200); + + // Verify rows are deleted + for (const pk of primaryKeysToDelete) { + const getResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=${pk.order_id}&customer_id=${pk.customer_id}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getResp.status, 400); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/rows/update/:connectionId - Bulk update + +test.serial( + `PUT /table/rows/update/:connectionId - Should bulk update rows in composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const bulkUpdateBody = { + primaryKeys: [ + { order_id: 2, customer_id: 101 }, + { order_id: 3, customer_id: 102 }, + ], + newValues: { + status: 'Cancelled', + }, + }; + + const bulkUpdateResponse = await request(app.getHttpServer()) + .put(`/table/rows/update/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(bulkUpdateBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkUpdateResponse.status, 200); + + // Verify rows are updated + const getRow1Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=2&customer_id=101`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow1Response.status, 200); + const row1RO = JSON.parse(getRow1Response.text); + t.is(row1RO.row.status, 'Cancelled'); + + const getRow2Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=3&customer_id=102`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow2Response.status, 200); + const row2RO = JSON.parse(getRow2Response.text); + t.is(row2RO.row.status, 'Cancelled'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// POST /table/rows/find/:connectionId - Rows with body filters + +test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + order_id: { eq: 1 }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + t.is(findRowsRO.rows.length, 1); + t.is(findRowsRO.rows[0].order_id, 1); + t.is(findRowsRO.rows[0].customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/rows/find/:connectionId - Should return rows filtered by status for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + status: { eq: 'Cancelled' }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + for (const row of findRowsRO.rows) { + t.is(row.status, 'Cancelled'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /connection/tables/:connectionId - List tables + +test.serial(`GET /connection/tables/:connectionId - Should list all complex test tables in connection`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getTablesResponse.status, 200); + const tablesRO = JSON.parse(getTablesResponse.text); + t.truthy(Array.isArray(tablesRO)); + + const tableNames = tablesRO.map((t: any) => t.table); + + const { main_table: compositeMain } = testTablesCompositeKeysData; + const { main_table: simpleMain } = testTablesSimpleKeysData; + + t.truthy(tableNames.includes(compositeMain.table_name), `Tables should include ${compositeMain.table_name}`); + t.truthy(tableNames.includes(simpleMain.table_name), `Tables should include ${simpleMain.table_name}`); + } catch (e) { + console.error(e); + throw e; + } +}); + +// GET /table/rows/:connectionId - Response structure validation + +test.serial( + `GET /table/rows/:connectionId - Should return correct response structure with primaryColumns and pagination for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 200); + const rowsRO = JSON.parse(getRowsResponse.text); + + // Validate response has required fields + t.truthy(rowsRO.rows); + t.truthy(rowsRO.primaryColumns); + t.truthy(rowsRO.pagination); + + // Validate primaryColumns includes both composite key columns + const primaryColumnNames = rowsRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + t.is(rowsRO.primaryColumns.length, 2); + + // Validate row data + t.truthy(rowsRO.rows.length > 0); + const firstRow = rowsRO.rows[0]; + t.truthy('order_id' in firstRow); + t.truthy('customer_id' in firstRow); + t.truthy('status' in firstRow); + t.truthy('total_amount' in firstRow); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Cascade delete behavior + +test.serial( + `DELETE /table/row/:connectionId - Should cascade delete referenced rows when deleting from main composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesCompositeKeysData; + + // Add a new row to main table + const newMainRow = { + order_id: 7777, + customer_id: 7777, + status: 'Pending', + total_amount: 100.0, + }; + const addMainResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newMainRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addMainResp.status, 201); + + // Add a referenced row + const newReferencedRow = { + order_id: 7777, + customer_id: 7777, + product_name: 'Test Product', + quantity: 5, + price_per_unit: 20.0, + }; + const addRefResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newReferencedRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRefResp.status, 201); + const addedRefRow = JSON.parse(addRefResp.text); + const refRowItemId = addedRefRow.row.item_id; + + // Delete the main row (should cascade) + const deleteResp = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=7777&customer_id=7777`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteResp.status, 200); + + // Verify referenced row is also deleted (CASCADE) + const getRefResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&item_id=${refRowItemId}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getRefResp.status, 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Error cases + +test.serial(`GET /table/rows/:connectionId - Should return error when tableName is missing`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return error when primary key is incomplete for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Only provide one part of composite key + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Should fail because composite key requires both order_id and customer_id + t.truthy(getRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `POST /table/row/:connectionId - Should return error when adding row with duplicate composite primary key`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Try to add a row with existing composite key (1, 100) + const duplicateRow = { + order_id: 1, + customer_id: 100, + status: 'Duplicate', + total_amount: 0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(duplicateRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.truthy(addRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); 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 893354738..e9226ddd0 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 @@ -230,3 +230,939 @@ test.serial( } }, ); + +// GET /table/structure/:connectionId + +test.serial( + `GET /table/structure/:connectionId - Should return table structure for composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const structureRO = JSON.parse(getStructureResponse.text); + t.is(getStructureResponse.status, 200); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + t.truthy(structureRO.structure.length > 0); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + for (const expectedCol of main_table.column_names) { + t.truthy(columnNames.includes(expectedCol), `Structure should include column ${expectedCol}`); + } + + const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'order_id'); + const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'customer_id'); + t.truthy(orderIdCol); + t.truthy(customerIdCol); + + // Validate primaryColumns + t.truthy(structureRO.primaryColumns); + const primaryColumnNames = structureRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `GET /table/structure/:connectionId - Should return structure for referenced table with foreign keys`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getStructureResponse.status, 200); + const structureRO = JSON.parse(getStructureResponse.text); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + t.truthy(columnNames.includes('item_id')); + t.truthy(columnNames.includes('order_id')); + t.truthy(columnNames.includes('customer_id')); + t.truthy(columnNames.includes('product_name')); + t.truthy(columnNames.includes('quantity')); + t.truthy(columnNames.includes('price_per_unit')); + + // Validate foreignKeys are present for referenced table + t.truthy(structureRO.foreignKeys); + t.truthy(structureRO.foreignKeys.length > 0); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/rows/:connectionId - Pagination tests + +test.serial(`GET /table/rows/:connectionId - Should return paginated rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const perPage = 10; + const getRowsPage1Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage1Response.status, 200); + const page1RO = JSON.parse(getRowsPage1Response.text); + t.truthy(page1RO.rows); + t.is(page1RO.rows.length, perPage); + t.truthy(page1RO.pagination); + t.is(page1RO.pagination.currentPage, 1); + t.is(page1RO.pagination.perPage, perPage); + t.truthy(page1RO.pagination.total >= 42); + + const getRowsPage2Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage2Response.status, 200); + const page2RO = JSON.parse(getRowsPage2Response.text); + t.truthy(page2RO.rows); + t.is(page2RO.rows.length, perPage); + t.is(page2RO.pagination.currentPage, 2); + + const page1OrderIds = page1RO.rows.map((r: any) => r.order_id); + const page2OrderIds = page2RO.rows.map((r: any) => r.order_id); + for (const id of page2OrderIds) { + t.falsy(page1OrderIds.includes(id), 'Page 2 rows should not overlap with page 1'); + } + } catch (e) { + console.error(e); + throw e; + } +}); + +// POST /table/row/:connectionId - Add row + +test.serial(`POST /table/row/:connectionId - Should add a new row to composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const newRow = { + order_id: 9999, + customer_id: 9999, + order_date: '2025-01-15', + status: 'Pending', + total_amount: 150.5, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.is(addRowRO.row.order_id, 9999); + t.is(addRowRO.row.customer_id, 9999); + t.is(addRowRO.row.status, 'Pending'); + t.truthy(addRowRO.structure); + t.truthy(addRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/row/:connectionId - Should add a new row to simple foreign key referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesSimpleKeysData; + + // First get an existing customer_id + const getMainRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const mainRowsRO = JSON.parse(getMainRowsResponse.text); + const existingCustomerId = mainRowsRO.rows[0].customer_id; + + const newOrder = { + customer_id: existingCustomerId, + order_date: '2025-03-01', + status: 'Shipped', + total_amount: 250.0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newOrder) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.truthy(addRowRO.row.order_id); + t.is(addRowRO.row.status, 'Shipped'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/row/:connectionId - Get single row + +test.serial(`GET /table/row/:connectionId - Should return a single row by composite primary key`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.is(getRowRO.row.order_id, 1); + t.is(getRowRO.row.customer_id, 100); + t.truthy(getRowRO.row.status); + t.truthy(getRowRO.structure); + t.truthy(getRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return a single row by simple primary key from referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesSimpleKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.truthy(getRowRO.row.order_id); + t.truthy(getRowRO.row.customer_id); + t.truthy(getRowRO.structure); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/row/:connectionId - Update row + +test.serial(`PUT /table/row/:connectionId - Should update a row in composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const updatedValues = { + status: 'Delivered', + total_amount: 999.99, + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.status, 'Delivered'); + t.is(parseFloat(updateRowRO.row.total_amount), 999.99); + t.is(updateRowRO.row.order_id, 1); + t.is(updateRowRO.row.customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`PUT /table/row/:connectionId - Should update a row in simple primary key referenced table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesSimpleKeysData; + + const updatedValues = { + name: 'Updated Customer Name', + email: 'updated@test.com', + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&customer_id=1`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.name, 'Updated Customer Name'); + t.is(updateRowRO.row.email, 'updated@test.com'); + } catch (e) { + console.error(e); + throw e; + } +}); + +// DELETE /table/row/:connectionId - Delete row + +test.serial(`DELETE /table/row/:connectionId - Should delete a row from composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Delete the row we added earlier (9999, 9999) + const deleteRowResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowResponse.status, 200); + const deleteRowRO = JSON.parse(deleteRowResponse.text); + t.truthy(deleteRowRO.row); + + // Verify the row is deleted + const getDeletedRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getDeletedRowResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +// PUT /table/rows/delete/:connectionId - Bulk delete + +test.serial( + `PUT /table/rows/delete/:connectionId - Should bulk delete rows from composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // First add rows to delete + const rowsToAdd = [ + { order_id: 8881, customer_id: 8881, status: 'Pending', total_amount: 10.0 }, + { order_id: 8882, customer_id: 8882, status: 'Pending', total_amount: 20.0 }, + { order_id: 8883, customer_id: 8883, status: 'Pending', total_amount: 30.0 }, + ]; + + for (const row of rowsToAdd) { + const addResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(row) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addResp.status, 201); + } + + const primaryKeysToDelete = [ + { order_id: 8881, customer_id: 8881 }, + { order_id: 8882, customer_id: 8882 }, + { order_id: 8883, customer_id: 8883 }, + ]; + + const bulkDeleteResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(primaryKeysToDelete) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkDeleteResponse.status, 200); + + // Verify rows are deleted + for (const pk of primaryKeysToDelete) { + const getResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=${pk.order_id}&customer_id=${pk.customer_id}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getResp.status, 400); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/rows/update/:connectionId - Bulk update + +test.serial( + `PUT /table/rows/update/:connectionId - Should bulk update rows in composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const bulkUpdateBody = { + primaryKeys: [ + { order_id: 2, customer_id: 101 }, + { order_id: 3, customer_id: 102 }, + ], + newValues: { + status: 'Cancelled', + }, + }; + + const bulkUpdateResponse = await request(app.getHttpServer()) + .put(`/table/rows/update/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(bulkUpdateBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkUpdateResponse.status, 200); + + // Verify rows are updated + const getRow1Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=2&customer_id=101`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow1Response.status, 200); + const row1RO = JSON.parse(getRow1Response.text); + t.is(row1RO.row.status, 'Cancelled'); + + const getRow2Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=3&customer_id=102`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow2Response.status, 200); + const row2RO = JSON.parse(getRow2Response.text); + t.is(row2RO.row.status, 'Cancelled'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// POST /table/rows/find/:connectionId - Rows with body filters + +test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + order_id: { eq: 1 }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + t.is(findRowsRO.rows.length, 1); + t.is(findRowsRO.rows[0].order_id, 1); + t.is(findRowsRO.rows[0].customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/rows/find/:connectionId - Should return rows filtered by status for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + status: { eq: 'Cancelled' }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + for (const row of findRowsRO.rows) { + t.is(row.status, 'Cancelled'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /connection/tables/:connectionId - List tables + +test.serial(`GET /connection/tables/:connectionId - Should list all complex test tables in connection`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getTablesResponse.status, 200); + const tablesRO = JSON.parse(getTablesResponse.text); + t.truthy(Array.isArray(tablesRO)); + + const tableNames = tablesRO.map((t: any) => t.table); + + const { main_table: compositeMain } = testTablesCompositeKeysData; + const { main_table: simpleMain } = testTablesSimpleKeysData; + + t.truthy(tableNames.includes(compositeMain.table_name), `Tables should include ${compositeMain.table_name}`); + t.truthy(tableNames.includes(simpleMain.table_name), `Tables should include ${simpleMain.table_name}`); + } catch (e) { + console.error(e); + throw e; + } +}); + +// GET /table/rows/:connectionId - Response structure validation + +test.serial( + `GET /table/rows/:connectionId - Should return correct response structure with primaryColumns and pagination for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 200); + const rowsRO = JSON.parse(getRowsResponse.text); + + // Validate response has required fields + t.truthy(rowsRO.rows); + t.truthy(rowsRO.primaryColumns); + t.truthy(rowsRO.pagination); + + // Validate primaryColumns includes both composite key columns + const primaryColumnNames = rowsRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + t.is(rowsRO.primaryColumns.length, 2); + + // Validate row data + t.truthy(rowsRO.rows.length > 0); + const firstRow = rowsRO.rows[0]; + t.truthy('order_id' in firstRow); + t.truthy('customer_id' in firstRow); + t.truthy('status' in firstRow); + t.truthy('total_amount' in firstRow); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Cascade delete behavior + +test.serial( + `DELETE /table/row/:connectionId - Should cascade delete referenced rows when deleting from main composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesCompositeKeysData; + + // Add a new row to main table + const newMainRow = { + order_id: 7777, + customer_id: 7777, + status: 'Pending', + total_amount: 100.0, + }; + const addMainResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newMainRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addMainResp.status, 201); + + // Add a referenced row + const newReferencedRow = { + order_id: 7777, + customer_id: 7777, + product_name: 'Test Product', + quantity: 5, + price_per_unit: 20.0, + }; + const addRefResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newReferencedRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRefResp.status, 201); + const addedRefRow = JSON.parse(addRefResp.text); + const refRowItemId = addedRefRow.row.item_id; + + // Delete the main row (should cascade) + const deleteResp = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=7777&customer_id=7777`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteResp.status, 200); + + // Verify referenced row is also deleted (CASCADE) + const getRefResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&item_id=${refRowItemId}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getRefResp.status, 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Error cases + +test.serial(`GET /table/rows/:connectionId - Should return error when tableName is missing`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return error when primary key is incomplete for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Only provide one part of composite key + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Should fail because composite key requires both order_id and customer_id + t.truthy(getRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `POST /table/row/:connectionId - Should return error when adding row with duplicate composite primary key`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Try to add a row with existing composite key (1, 100) + const duplicateRow = { + order_id: 1, + customer_id: 100, + status: 'Duplicate', + total_amount: 0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(duplicateRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.truthy(addRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); 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 518e29d1b..7f2d4a92c 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 @@ -230,3 +230,939 @@ test.serial( } }, ); + +// GET /table/structure/:connectionId + +test.serial( + `GET /table/structure/:connectionId - Should return table structure for composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const structureRO = JSON.parse(getStructureResponse.text); + t.is(getStructureResponse.status, 200); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + t.truthy(structureRO.structure.length > 0); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + for (const expectedCol of main_table.column_names) { + t.truthy(columnNames.includes(expectedCol), `Structure should include column ${expectedCol}`); + } + + const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'order_id'); + const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'customer_id'); + t.truthy(orderIdCol); + t.truthy(customerIdCol); + + // Validate primaryColumns + t.truthy(structureRO.primaryColumns); + const primaryColumnNames = structureRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `GET /table/structure/:connectionId - Should return structure for referenced table with foreign keys`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getStructureResponse.status, 200); + const structureRO = JSON.parse(getStructureResponse.text); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + t.truthy(columnNames.includes('item_id')); + t.truthy(columnNames.includes('order_id')); + t.truthy(columnNames.includes('customer_id')); + t.truthy(columnNames.includes('product_name')); + t.truthy(columnNames.includes('quantity')); + t.truthy(columnNames.includes('price_per_unit')); + + // Validate foreignKeys are present for referenced table + t.truthy(structureRO.foreignKeys); + t.truthy(structureRO.foreignKeys.length > 0); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/rows/:connectionId - Pagination tests + +test.serial(`GET /table/rows/:connectionId - Should return paginated rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const perPage = 10; + const getRowsPage1Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage1Response.status, 200); + const page1RO = JSON.parse(getRowsPage1Response.text); + t.truthy(page1RO.rows); + t.is(page1RO.rows.length, perPage); + t.truthy(page1RO.pagination); + t.is(page1RO.pagination.currentPage, 1); + t.is(page1RO.pagination.perPage, perPage); + t.truthy(page1RO.pagination.total >= 42); + + const getRowsPage2Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage2Response.status, 200); + const page2RO = JSON.parse(getRowsPage2Response.text); + t.truthy(page2RO.rows); + t.is(page2RO.rows.length, perPage); + t.is(page2RO.pagination.currentPage, 2); + + const page1OrderIds = page1RO.rows.map((r: any) => r.order_id); + const page2OrderIds = page2RO.rows.map((r: any) => r.order_id); + for (const id of page2OrderIds) { + t.falsy(page1OrderIds.includes(id), 'Page 2 rows should not overlap with page 1'); + } + } catch (e) { + console.error(e); + throw e; + } +}); + +// POST /table/row/:connectionId - Add row + +test.serial(`POST /table/row/:connectionId - Should add a new row to composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const newRow = { + order_id: 9999, + customer_id: 9999, + order_date: '2025-01-15', + status: 'Pending', + total_amount: 150.5, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.is(addRowRO.row.order_id, 9999); + t.is(addRowRO.row.customer_id, 9999); + t.is(addRowRO.row.status, 'Pending'); + t.truthy(addRowRO.structure); + t.truthy(addRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/row/:connectionId - Should add a new row to simple foreign key referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesSimpleKeysData; + + // First get an existing customer_id + const getMainRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const mainRowsRO = JSON.parse(getMainRowsResponse.text); + const existingCustomerId = mainRowsRO.rows[0].customer_id; + + const newOrder = { + customer_id: existingCustomerId, + order_date: '2025-03-01', + status: 'Shipped', + total_amount: 250.0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newOrder) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.truthy(addRowRO.row.order_id); + t.is(addRowRO.row.status, 'Shipped'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/row/:connectionId - Get single row + +test.serial(`GET /table/row/:connectionId - Should return a single row by composite primary key`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.is(getRowRO.row.order_id, 1); + t.is(getRowRO.row.customer_id, 100); + t.truthy(getRowRO.row.status); + t.truthy(getRowRO.structure); + t.truthy(getRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return a single row by simple primary key from referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesSimpleKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.truthy(getRowRO.row.order_id); + t.truthy(getRowRO.row.customer_id); + t.truthy(getRowRO.structure); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/row/:connectionId - Update row + +test.serial(`PUT /table/row/:connectionId - Should update a row in composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const updatedValues = { + status: 'Delivered', + total_amount: 999.99, + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.status, 'Delivered'); + t.is(parseFloat(updateRowRO.row.total_amount), 999.99); + t.is(updateRowRO.row.order_id, 1); + t.is(updateRowRO.row.customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`PUT /table/row/:connectionId - Should update a row in simple primary key referenced table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesSimpleKeysData; + + const updatedValues = { + name: 'Updated Customer Name', + email: 'updated@test.com', + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&customer_id=1`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.name, 'Updated Customer Name'); + t.is(updateRowRO.row.email, 'updated@test.com'); + } catch (e) { + console.error(e); + throw e; + } +}); + +// DELETE /table/row/:connectionId - Delete row + +test.serial(`DELETE /table/row/:connectionId - Should delete a row from composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Delete the row we added earlier (9999, 9999) + const deleteRowResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowResponse.status, 200); + const deleteRowRO = JSON.parse(deleteRowResponse.text); + t.truthy(deleteRowRO.row); + + // Verify the row is deleted + const getDeletedRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getDeletedRowResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +// PUT /table/rows/delete/:connectionId - Bulk delete + +test.serial( + `PUT /table/rows/delete/:connectionId - Should bulk delete rows from composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // First add rows to delete + const rowsToAdd = [ + { order_id: 8881, customer_id: 8881, status: 'Pending', total_amount: 10.0 }, + { order_id: 8882, customer_id: 8882, status: 'Pending', total_amount: 20.0 }, + { order_id: 8883, customer_id: 8883, status: 'Pending', total_amount: 30.0 }, + ]; + + for (const row of rowsToAdd) { + const addResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(row) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addResp.status, 201); + } + + const primaryKeysToDelete = [ + { order_id: 8881, customer_id: 8881 }, + { order_id: 8882, customer_id: 8882 }, + { order_id: 8883, customer_id: 8883 }, + ]; + + const bulkDeleteResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(primaryKeysToDelete) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkDeleteResponse.status, 200); + + // Verify rows are deleted + for (const pk of primaryKeysToDelete) { + const getResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=${pk.order_id}&customer_id=${pk.customer_id}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getResp.status, 400); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/rows/update/:connectionId - Bulk update + +test.serial( + `PUT /table/rows/update/:connectionId - Should bulk update rows in composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const bulkUpdateBody = { + primaryKeys: [ + { order_id: 2, customer_id: 101 }, + { order_id: 3, customer_id: 102 }, + ], + newValues: { + status: 'Cancelled', + }, + }; + + const bulkUpdateResponse = await request(app.getHttpServer()) + .put(`/table/rows/update/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(bulkUpdateBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkUpdateResponse.status, 200); + + // Verify rows are updated + const getRow1Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=2&customer_id=101`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow1Response.status, 200); + const row1RO = JSON.parse(getRow1Response.text); + t.is(row1RO.row.status, 'Cancelled'); + + const getRow2Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=3&customer_id=102`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow2Response.status, 200); + const row2RO = JSON.parse(getRow2Response.text); + t.is(row2RO.row.status, 'Cancelled'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// POST /table/rows/find/:connectionId - Rows with body filters + +test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + order_id: { eq: 1 }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + t.is(findRowsRO.rows.length, 1); + t.is(findRowsRO.rows[0].order_id, 1); + t.is(findRowsRO.rows[0].customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/rows/find/:connectionId - Should return rows filtered by status for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + status: { eq: 'Cancelled' }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + for (const row of findRowsRO.rows) { + t.is(row.status, 'Cancelled'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /connection/tables/:connectionId - List tables + +test.serial(`GET /connection/tables/:connectionId - Should list all complex test tables in connection`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getTablesResponse.status, 200); + const tablesRO = JSON.parse(getTablesResponse.text); + t.truthy(Array.isArray(tablesRO)); + + const tableNames = tablesRO.map((t: any) => t.table); + + const { main_table: compositeMain } = testTablesCompositeKeysData; + const { main_table: simpleMain } = testTablesSimpleKeysData; + + t.truthy(tableNames.includes(compositeMain.table_name), `Tables should include ${compositeMain.table_name}`); + t.truthy(tableNames.includes(simpleMain.table_name), `Tables should include ${simpleMain.table_name}`); + } catch (e) { + console.error(e); + throw e; + } +}); + +// GET /table/rows/:connectionId - Response structure validation + +test.serial( + `GET /table/rows/:connectionId - Should return correct response structure with primaryColumns and pagination for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 200); + const rowsRO = JSON.parse(getRowsResponse.text); + + // Validate response has required fields + t.truthy(rowsRO.rows); + t.truthy(rowsRO.primaryColumns); + t.truthy(rowsRO.pagination); + + // Validate primaryColumns includes both composite key columns + const primaryColumnNames = rowsRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + t.is(rowsRO.primaryColumns.length, 2); + + // Validate row data + t.truthy(rowsRO.rows.length > 0); + const firstRow = rowsRO.rows[0]; + t.truthy('order_id' in firstRow); + t.truthy('customer_id' in firstRow); + t.truthy('status' in firstRow); + t.truthy('total_amount' in firstRow); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Cascade delete behavior + +test.serial( + `DELETE /table/row/:connectionId - Should cascade delete referenced rows when deleting from main composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesCompositeKeysData; + + // Add a new row to main table + const newMainRow = { + order_id: 7777, + customer_id: 7777, + status: 'Pending', + total_amount: 100.0, + }; + const addMainResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newMainRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addMainResp.status, 201); + + // Add a referenced row + const newReferencedRow = { + order_id: 7777, + customer_id: 7777, + product_name: 'Test Product', + quantity: 5, + price_per_unit: 20.0, + }; + const addRefResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newReferencedRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRefResp.status, 201); + const addedRefRow = JSON.parse(addRefResp.text); + const refRowItemId = addedRefRow.row.item_id; + + // Delete the main row (should cascade) + const deleteResp = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=7777&customer_id=7777`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteResp.status, 200); + + // Verify referenced row is also deleted (CASCADE) + const getRefResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&item_id=${refRowItemId}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getRefResp.status, 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Error cases + +test.serial(`GET /table/rows/:connectionId - Should return error when tableName is missing`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return error when primary key is incomplete for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Only provide one part of composite key + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Should fail because composite key requires both order_id and customer_id + t.truthy(getRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `POST /table/row/:connectionId - Should return error when adding row with duplicate composite primary key`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Try to add a row with existing composite key (1, 100) + const duplicateRow = { + order_id: 1, + customer_id: 100, + status: 'Duplicate', + total_amount: 0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(duplicateRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.truthy(addRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); 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 7272b7adc..72af23295 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 @@ -230,3 +230,939 @@ test.serial( } }, ); + +// GET /table/structure/:connectionId + +test.serial( + `GET /table/structure/:connectionId - Should return table structure for composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const structureRO = JSON.parse(getStructureResponse.text); + t.is(getStructureResponse.status, 200); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + t.truthy(structureRO.structure.length > 0); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + for (const expectedCol of main_table.column_names) { + t.truthy(columnNames.includes(expectedCol), `Structure should include column ${expectedCol}`); + } + + const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'order_id'); + const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'customer_id'); + t.truthy(orderIdCol); + t.truthy(customerIdCol); + + // Validate primaryColumns + t.truthy(structureRO.primaryColumns); + const primaryColumnNames = structureRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `GET /table/structure/:connectionId - Should return structure for referenced table with foreign keys`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesCompositeKeysData; + + const getStructureResponse = await request(app.getHttpServer()) + .get(`/table/structure/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getStructureResponse.status, 200); + const structureRO = JSON.parse(getStructureResponse.text); + t.truthy(structureRO.structure); + t.truthy(Array.isArray(structureRO.structure)); + + const columnNames = structureRO.structure.map((col: any) => col.column_name); + t.truthy(columnNames.includes('item_id')); + t.truthy(columnNames.includes('order_id')); + t.truthy(columnNames.includes('customer_id')); + t.truthy(columnNames.includes('product_name')); + t.truthy(columnNames.includes('quantity')); + t.truthy(columnNames.includes('price_per_unit')); + + // Validate foreignKeys are present for referenced table + t.truthy(structureRO.foreignKeys); + t.truthy(structureRO.foreignKeys.length > 0); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/rows/:connectionId - Pagination tests + +test.serial(`GET /table/rows/:connectionId - Should return paginated rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const perPage = 10; + const getRowsPage1Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage1Response.status, 200); + const page1RO = JSON.parse(getRowsPage1Response.text); + t.truthy(page1RO.rows); + t.is(page1RO.rows.length, perPage); + t.truthy(page1RO.pagination); + t.is(page1RO.pagination.currentPage, 1); + t.is(page1RO.pagination.perPage, perPage); + t.truthy(page1RO.pagination.total >= 42); + + const getRowsPage2Response = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=${perPage}&page=2`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsPage2Response.status, 200); + const page2RO = JSON.parse(getRowsPage2Response.text); + t.truthy(page2RO.rows); + t.is(page2RO.rows.length, perPage); + t.is(page2RO.pagination.currentPage, 2); + + const page1OrderIds = page1RO.rows.map((r: any) => r.order_id); + const page2OrderIds = page2RO.rows.map((r: any) => r.order_id); + for (const id of page2OrderIds) { + t.falsy(page1OrderIds.includes(id), 'Page 2 rows should not overlap with page 1'); + } + } catch (e) { + console.error(e); + throw e; + } +}); + +// POST /table/row/:connectionId - Add row + +test.serial(`POST /table/row/:connectionId - Should add a new row to composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const newRow = { + order_id: 9999, + customer_id: 9999, + order_date: '2025-01-15', + status: 'Pending', + total_amount: 150.5, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.is(addRowRO.row.order_id, 9999); + t.is(addRowRO.row.customer_id, 9999); + t.is(addRowRO.row.status, 'Pending'); + t.truthy(addRowRO.structure); + t.truthy(addRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/row/:connectionId - Should add a new row to simple foreign key referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesSimpleKeysData; + + // First get an existing customer_id + const getMainRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}&perPage=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const mainRowsRO = JSON.parse(getMainRowsResponse.text); + const existingCustomerId = mainRowsRO.rows[0].customer_id; + + const newOrder = { + customer_id: existingCustomerId, + order_date: '2025-03-01', + status: 'Shipped', + total_amount: 250.0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newOrder) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(addRowResponse.status, 201); + const addRowRO = JSON.parse(addRowResponse.text); + t.truthy(addRowRO.row); + t.truthy(addRowRO.row.order_id); + t.is(addRowRO.row.status, 'Shipped'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /table/row/:connectionId - Get single row + +test.serial(`GET /table/row/:connectionId - Should return a single row by composite primary key`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.is(getRowRO.row.order_id, 1); + t.is(getRowRO.row.customer_id, 100); + t.truthy(getRowRO.row.status); + t.truthy(getRowRO.structure); + t.truthy(getRowRO.primaryColumns); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return a single row by simple primary key from referenced table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { first_referenced_table } = testTablesSimpleKeysData; + + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowResponse.status, 200); + const getRowRO = JSON.parse(getRowResponse.text); + t.truthy(getRowRO.row); + t.truthy(getRowRO.row.order_id); + t.truthy(getRowRO.row.customer_id); + t.truthy(getRowRO.structure); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/row/:connectionId - Update row + +test.serial(`PUT /table/row/:connectionId - Should update a row in composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const updatedValues = { + status: 'Delivered', + total_amount: 999.99, + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1&customer_id=100`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.status, 'Delivered'); + t.is(parseFloat(updateRowRO.row.total_amount), 999.99); + t.is(updateRowRO.row.order_id, 1); + t.is(updateRowRO.row.customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial(`PUT /table/row/:connectionId - Should update a row in simple primary key referenced table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesSimpleKeysData; + + const updatedValues = { + name: 'Updated Customer Name', + email: 'updated@test.com', + }; + + const updateRowResponse = await request(app.getHttpServer()) + .put(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&customer_id=1`) + .send(updatedValues) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(updateRowResponse.status, 200); + const updateRowRO = JSON.parse(updateRowResponse.text); + t.truthy(updateRowRO.row); + t.is(updateRowRO.row.name, 'Updated Customer Name'); + t.is(updateRowRO.row.email, 'updated@test.com'); + } catch (e) { + console.error(e); + throw e; + } +}); + +// DELETE /table/row/:connectionId - Delete row + +test.serial(`DELETE /table/row/:connectionId - Should delete a row from composite primary key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Delete the row we added earlier (9999, 9999) + const deleteRowResponse = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(deleteRowResponse.status, 200); + const deleteRowRO = JSON.parse(deleteRowResponse.text); + t.truthy(deleteRowRO.row); + + // Verify the row is deleted + const getDeletedRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=9999&customer_id=9999`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getDeletedRowResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +// PUT /table/rows/delete/:connectionId - Bulk delete + +test.serial( + `PUT /table/rows/delete/:connectionId - Should bulk delete rows from composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // First add rows to delete + const rowsToAdd = [ + { order_id: 8881, customer_id: 8881, status: 'Pending', total_amount: 10.0 }, + { order_id: 8882, customer_id: 8882, status: 'Pending', total_amount: 20.0 }, + { order_id: 8883, customer_id: 8883, status: 'Pending', total_amount: 30.0 }, + ]; + + for (const row of rowsToAdd) { + const addResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(row) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addResp.status, 201); + } + + const primaryKeysToDelete = [ + { order_id: 8881, customer_id: 8881 }, + { order_id: 8882, customer_id: 8882 }, + { order_id: 8883, customer_id: 8883 }, + ]; + + const bulkDeleteResponse = await request(app.getHttpServer()) + .put(`/table/rows/delete/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(primaryKeysToDelete) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkDeleteResponse.status, 200); + + // Verify rows are deleted + for (const pk of primaryKeysToDelete) { + const getResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=${pk.order_id}&customer_id=${pk.customer_id}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getResp.status, 400); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// PUT /table/rows/update/:connectionId - Bulk update + +test.serial( + `PUT /table/rows/update/:connectionId - Should bulk update rows in composite primary key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const bulkUpdateBody = { + primaryKeys: [ + { order_id: 2, customer_id: 101 }, + { order_id: 3, customer_id: 102 }, + ], + newValues: { + status: 'Cancelled', + }, + }; + + const bulkUpdateResponse = await request(app.getHttpServer()) + .put(`/table/rows/update/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(bulkUpdateBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(bulkUpdateResponse.status, 200); + + // Verify rows are updated + const getRow1Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=2&customer_id=101`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow1Response.status, 200); + const row1RO = JSON.parse(getRow1Response.text); + t.is(row1RO.row.status, 'Cancelled'); + + const getRow2Response = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=3&customer_id=102`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRow2Response.status, 200); + const row2RO = JSON.parse(getRow2Response.text); + t.is(row2RO.row.status, 'Cancelled'); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// POST /table/rows/find/:connectionId - Rows with body filters + +test.serial(`POST /table/rows/find/:connectionId - Should return filtered rows for composite key table`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + order_id: { eq: 1 }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + t.is(findRowsRO.rows.length, 1); + t.is(findRowsRO.rows[0].order_id, 1); + t.is(findRowsRO.rows[0].customer_id, 100); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `POST /table/rows/find/:connectionId - Should return rows filtered by status for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const filterBody = { + filters: { + status: { eq: 'Cancelled' }, + }, + }; + + const findRowsResponse = await request(app.getHttpServer()) + .post(`/table/rows/find/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(filterBody) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(findRowsResponse.status, 201); + const findRowsRO = JSON.parse(findRowsResponse.text); + t.truthy(findRowsRO.rows); + for (const row of findRowsRO.rows) { + t.is(row.status, 'Cancelled'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// GET /connection/tables/:connectionId - List tables + +test.serial(`GET /connection/tables/:connectionId - Should list all complex test tables in connection`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getTablesResponse = await request(app.getHttpServer()) + .get(`/connection/tables/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getTablesResponse.status, 200); + const tablesRO = JSON.parse(getTablesResponse.text); + t.truthy(Array.isArray(tablesRO)); + + const tableNames = tablesRO.map((t: any) => t.table); + + const { main_table: compositeMain } = testTablesCompositeKeysData; + const { main_table: simpleMain } = testTablesSimpleKeysData; + + t.truthy(tableNames.includes(compositeMain.table_name), `Tables should include ${compositeMain.table_name}`); + t.truthy(tableNames.includes(simpleMain.table_name), `Tables should include ${simpleMain.table_name}`); + } catch (e) { + console.error(e); + throw e; + } +}); + +// GET /table/rows/:connectionId - Response structure validation + +test.serial( + `GET /table/rows/:connectionId - Should return correct response structure with primaryColumns and pagination for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + const getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 200); + const rowsRO = JSON.parse(getRowsResponse.text); + + // Validate response has required fields + t.truthy(rowsRO.rows); + t.truthy(rowsRO.primaryColumns); + t.truthy(rowsRO.pagination); + + // Validate primaryColumns includes both composite key columns + const primaryColumnNames = rowsRO.primaryColumns.map((col: any) => col.column_name); + t.truthy(primaryColumnNames.includes('order_id')); + t.truthy(primaryColumnNames.includes('customer_id')); + t.is(rowsRO.primaryColumns.length, 2); + + // Validate row data + t.truthy(rowsRO.rows.length > 0); + const firstRow = rowsRO.rows[0]; + t.truthy('order_id' in firstRow); + t.truthy('customer_id' in firstRow); + t.truthy('status' in firstRow); + t.truthy('total_amount' in firstRow); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Cascade delete behavior + +test.serial( + `DELETE /table/row/:connectionId - Should cascade delete referenced rows when deleting from main composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table, first_referenced_table } = testTablesCompositeKeysData; + + // Add a new row to main table + const newMainRow = { + order_id: 7777, + customer_id: 7777, + status: 'Pending', + total_amount: 100.0, + }; + const addMainResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(newMainRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addMainResp.status, 201); + + // Add a referenced row + const newReferencedRow = { + order_id: 7777, + customer_id: 7777, + product_name: 'Test Product', + quantity: 5, + price_per_unit: 20.0, + }; + const addRefResp = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .send(newReferencedRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(addRefResp.status, 201); + const addedRefRow = JSON.parse(addRefResp.text); + const refRowItemId = addedRefRow.row.item_id; + + // Delete the main row (should cascade) + const deleteResp = await request(app.getHttpServer()) + .delete(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=7777&customer_id=7777`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(deleteResp.status, 200); + + // Verify referenced row is also deleted (CASCADE) + const getRefResp = await request(app.getHttpServer()) + .get( + `/table/row/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}&item_id=${refRowItemId}`, + ) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + t.is(getRefResp.status, 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +// Error cases + +test.serial(`GET /table/rows/:connectionId - Should return error when tableName is missing`, async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 getRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.is(getRowsResponse.status, 400); + } catch (e) { + console.error(e); + throw e; + } +}); + +test.serial( + `GET /table/row/:connectionId - Should return error when primary key is incomplete for composite key table`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Only provide one part of composite key + const getRowResponse = await request(app.getHttpServer()) + .get(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}&order_id=1`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + // Should fail because composite key requires both order_id and customer_id + t.truthy(getRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `POST /table/row/:connectionId - Should return error when adding row with duplicate composite primary key`, + async (t) => { + try { + const firstUserToken = (await registerUserAndReturnUserInfo(app)).token; + + 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 { main_table } = testTablesCompositeKeysData; + + // Try to add a row with existing composite key (1, 100) + const duplicateRow = { + order_id: 1, + customer_id: 100, + status: 'Duplicate', + total_amount: 0, + }; + + const addRowResponse = await request(app.getHttpServer()) + .post(`/table/row/${createConnectionRO.id}?tableName=${main_table.table_name}`) + .send(duplicateRow) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + t.truthy(addRowResponse.status >= 400); + } catch (e) { + console.error(e); + throw e; + } + }, +); From 02d040e7ca1fa77737d8a9a0abba5e05f6185cde Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Thu, 12 Mar 2026 14:39:08 +0000 Subject: [PATCH 2/2] Refactor table column names to use uppercase for consistency in test utilities and E2E tests --- .../complex-ibmdb2-table-e2e.test.ts | 24 ++++++++-------- .../create-test-ibmdb2-tables.ts | 28 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) 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 fc325c88c..5ed96eb03 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 @@ -267,16 +267,16 @@ test.serial( t.truthy(columnNames.includes(expectedCol), `Structure should include column ${expectedCol}`); } - const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'order_id'); - const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'customer_id'); + const orderIdCol = structureRO.structure.find((col: any) => col.column_name === 'ORDER_ID'); + const customerIdCol = structureRO.structure.find((col: any) => col.column_name === 'CUSTOMER_ID'); t.truthy(orderIdCol); t.truthy(customerIdCol); // Validate primaryColumns t.truthy(structureRO.primaryColumns); const primaryColumnNames = structureRO.primaryColumns.map((col: any) => col.column_name); - t.truthy(primaryColumnNames.includes('order_id')); - t.truthy(primaryColumnNames.includes('customer_id')); + t.truthy(primaryColumnNames.includes('ORDER_ID')); + t.truthy(primaryColumnNames.includes('CUSTOMER_ID')); } catch (e) { console.error(e); throw e; @@ -313,12 +313,12 @@ test.serial( t.truthy(Array.isArray(structureRO.structure)); const columnNames = structureRO.structure.map((col: any) => col.column_name); - t.truthy(columnNames.includes('item_id')); - t.truthy(columnNames.includes('order_id')); - t.truthy(columnNames.includes('customer_id')); - t.truthy(columnNames.includes('product_name')); - t.truthy(columnNames.includes('quantity')); - t.truthy(columnNames.includes('price_per_unit')); + t.truthy(columnNames.includes('ITEM_ID')); + t.truthy(columnNames.includes('ORDER_ID')); + t.truthy(columnNames.includes('CUSTOMER_ID')); + t.truthy(columnNames.includes('PRODUCT_NAME')); + t.truthy(columnNames.includes('QUANTITY')); + t.truthy(columnNames.includes('PRICE_PER_UNIT')); // Validate foreignKeys are present for referenced table t.truthy(structureRO.foreignKeys); @@ -972,8 +972,8 @@ test.serial( // Validate primaryColumns includes both composite key columns const primaryColumnNames = rowsRO.primaryColumns.map((col: any) => col.column_name); - t.truthy(primaryColumnNames.includes('order_id')); - t.truthy(primaryColumnNames.includes('customer_id')); + t.truthy(primaryColumnNames.includes('ORDER_ID')); + t.truthy(primaryColumnNames.includes('CUSTOMER_ID')); t.is(rowsRO.primaryColumns.length, 2); // Validate row data diff --git a/backend/test/utils/test-utilities/create-test-ibmdb2-tables.ts b/backend/test/utils/test-utilities/create-test-ibmdb2-tables.ts index 3ebdb93e3..3bb9b2de4 100644 --- a/backend/test/utils/test-utilities/create-test-ibmdb2-tables.ts +++ b/backend/test/utils/test-utilities/create-test-ibmdb2-tables.ts @@ -288,21 +288,21 @@ export const createTestIBMDB2TablesWithComplexPFKeys = async (connectionParams: return { first_referenced_table: { table_name: firstReferencedTableName, - column_names: ['order_id', 'customer_id', 'item_id', 'product_name', 'quantity', 'price_per_unit'], - primary_key_column_names: ['item_id'], + column_names: ['ORDER_ID', 'CUSTOMER_ID', 'ITEM_ID', 'PRODUCT_NAME', 'QUANTITY', 'PRICE_PER_UNIT'], + primary_key_column_names: ['ITEM_ID'], }, main_table: { table_name: mainTableName, - column_names: ['order_id', 'customer_id', 'order_date', 'status', 'total_amount'], + column_names: ['ORDER_ID', 'CUSTOMER_ID', 'ORDER_DATE', 'STATUS', 'TOTAL_AMOUNT'], foreign_key_column_names: [], binary_column_names: [], - primary_key_column_names: ['order_id', 'customer_id'], + primary_key_column_names: ['ORDER_ID', 'CUSTOMER_ID'], }, second_referenced_table: { table_name: referencedOnTableName, - column_names: ['shipment_id', 'order_id', 'customer_id', 'shipped_date', 'carrier', 'tracking_number'], - primary_key_column_names: ['shipment_id'], - foreign_key_column_names: ['order_id', 'customer_id'], + column_names: ['SHIPMENT_ID', 'ORDER_ID', 'CUSTOMER_ID', 'SHIPPED_DATE', 'CARRIER', 'TRACKING_NUMBER'], + primary_key_column_names: ['SHIPMENT_ID'], + foreign_key_column_names: ['ORDER_ID', 'CUSTOMER_ID'], }, }; }; @@ -550,21 +550,21 @@ export const createTestIBMDB2TablesWithSimplePFKeys = async (connectionParams: a return { main_table: { table_name: mainTableName, - column_names: ['order_id', 'customer_id', 'order_date', 'status', 'total_amount'], + column_names: ['ORDER_ID', 'CUSTOMER_ID', 'ORDER_DATE', 'STATUS', 'TOTAL_AMOUNT'], foreign_key_column_names: [], binary_column_names: [], - primary_key_column_names: ['order_id'], + primary_key_column_names: ['ORDER_ID'], }, first_referenced_table: { table_name: firstReferencedTableName, - column_names: ['item_id', 'order_id', 'product_name', 'quantity', 'price_per_unit'], - primary_key_column_names: ['item_id'], + column_names: ['ITEM_ID', 'ORDER_ID', 'PRODUCT_NAME', 'QUANTITY', 'PRICE_PER_UNIT'], + primary_key_column_names: ['ITEM_ID'], }, second_referenced_table: { table_name: referencedOnTableName, - column_names: ['shipment_id', 'order_id', 'shipped_date', 'carrier', 'tracking_number'], - primary_key_column_names: ['shipment_id'], - foreign_key_column_names: ['order_id'], + column_names: ['SHIPMENT_ID', 'ORDER_ID', 'SHIPPED_DATE', 'CARRIER', 'TRACKING_NUMBER'], + primary_key_column_names: ['SHIPMENT_ID'], + foreign_key_column_names: ['ORDER_ID'], }, }; };