diff --git a/backend/src/entities/table/use-cases/get-table-rows.use.case.ts b/backend/src/entities/table/use-cases/get-table-rows.use.case.ts index 723c3d06c..07bcf6675 100644 --- a/backend/src/entities/table/use-cases/get-table-rows.use.case.ts +++ b/backend/src/entities/table/use-cases/get-table-rows.use.case.ts @@ -115,11 +115,11 @@ export class GetTableRowsUseCase extends AbstractUseCase = tableWidgets .filter((widget) => widget.widget_type === WidgetTypeEnum.Foreign_key) - .map((widget) => { + .reduce>((acc, widget) => { if (widget.widget_params) { try { const widgetParams = JSON5.parse(widget.widget_params) as ForeignKeyDSInfo; - return widgetParams; + acc.push(widgetParams); } catch (_e) { - return null; + // } } - }) - .filter((el) => el !== null); + return acc; + }, []); tableForeignKeys = [...tableForeignKeys, ...foreignKeysFromWidgets]; @@ -205,23 +205,17 @@ export class GetTableRowsUseCase extends AbstractUseCase 0) { tableForeignKeys = await Promise.all( - tableForeignKeys.map((el) => { - try { - return this.attachForeignColumnNames(el, userId, connectionId, dao); - } catch (_e) { - return el; - } - }), + tableForeignKeys.map((el) => this.attachForeignColumnNames(el, userId, connectionId, dao).catch(() => el)), ); } const formedTableStructure = formFullTableStructure(tableStructure, tableSettings); - const largeDataset = rows.large_dataset - ? true - : rows.pagination.total > Constants.LARGE_DATASET_ROW_LIMIT - ? true - : false; + const largeDataset = rows.large_dataset || rows.pagination.total > Constants.LARGE_DATASET_ROW_LIMIT; + + const listFields = findAvailableFields(tableSettings, tableStructure); + const actionEventsDtos = customActionEvents.map((el) => buildActionEventDto(el)); + const savedFiltersRO = savedTableFilters.map((el) => buildCreatedTableFilterRO(el)); const rowsRO = { rows: rows.data, @@ -237,13 +231,13 @@ export class GetTableRowsUseCase extends AbstractUseCase buildActionEventDto(el)), - table_actions: customActionEvents.map((el) => buildActionEventDto(el)), + list_fields: listFields, + action_events: actionEventsDtos, + table_actions: actionEventsDtos, large_dataset: largeDataset, allow_csv_export: allowCsvExport, allow_csv_import: allowCsvImport, - saved_filters: savedTableFilters.map((el) => buildCreatedTableFilterRO(el)), + saved_filters: savedFiltersRO, can_delete: can_delete, can_update: can_update, can_add: can_add, @@ -263,24 +257,53 @@ export class GetTableRowsUseCase extends AbstractUseCase(); if (tableForeignKeys?.length > 0) { + const uniqueReferencedTables = [...new Set(tableForeignKeys.map((fk) => fk.referenced_table_name))]; + const foreignTableSettingsPromises = uniqueReferencedTables.map((tableName) => + this._dbContext.tableSettingsRepository.findTableSettings(connectionId, tableName).then((settings) => ({ + tableName, + settings, + })), + ); + const foreignTableSettingsResults = await Promise.all(foreignTableSettingsPromises); + const foreignTableSettingsMap = new Map( + foreignTableSettingsResults.map((result) => [result.tableName, result.settings]), + ); + + const foreignKeyDataMap = new Map }>(); + for (const foreignKey of tableForeignKeys) { - const foreignKeysValuesCollection = rowsRO.rows - .map((row) => row[foreignKey.column_name]) - .filter((value) => value !== undefined) as (string | number)[]; + const valuesSet = new Set(); + for (const row of rowsRO.rows) { + const value = row[foreignKey.column_name]; + if (value !== undefined && value !== null) { + valuesSet.add(value as string | number); + } + } - const foreignTableSettings = await this._dbContext.tableSettingsRepository.findTableSettings( - connectionId, - foreignKey.referenced_table_name, - ); + const key = `${foreignKey.referenced_table_name}:${foreignKey.referenced_column_name}`; + if (foreignKeyDataMap.has(key)) { + const existing = foreignKeyDataMap.get(key); + valuesSet.forEach((v) => existing!.values.add(v)); + } else { + foreignKeyDataMap.set(key, { foreignKey, values: valuesSet }); + } + } + const identityPromises = Array.from(foreignKeyDataMap.values()).map(async ({ foreignKey, values }) => { + const foreignTableSettings = foreignTableSettingsMap.get(foreignKey.referenced_table_name); const identityColumns = await this.getBatchedIdentityColumns( - foreignKeysValuesCollection, + Array.from(values), foreignKey, dao, foreignTableSettings, userEmail, ); + return { foreignKey, identityColumns }; + }); + const identityResults = await Promise.all(identityPromises); + + for (const { foreignKey, identityColumns } of identityResults) { if (!identitiesMap.has(foreignKey.referenced_table_name)) { identitiesMap.set(foreignKey.referenced_table_name, []); } @@ -299,6 +322,7 @@ export class GetTableRowsUseCase extends AbstractUseCase>(); for (const element of foreignKeysConformity) { const identityForCurrentTable = identities.find( (el) => el.referenced_table_name === element.referencedTableName, @@ -306,15 +330,26 @@ export class GetTableRowsUseCase extends AbstractUseCase [col[element.realFKeyName], col]), - ); + const lookupKey = `${element.referencedTableName}:${element.realFKeyName}`; + if (!identityLookupMaps.has(lookupKey)) { + const identityColumnsMap = new Map( + identityForCurrentTable.identity_columns.map((col) => [col[element.realFKeyName], col]), + ); + identityLookupMaps.set(lookupKey, identityColumnsMap); + } + } + + for (const row of rowsRO.rows) { + for (const element of foreignKeysConformity) { + const lookupKey = `${element.referencedTableName}:${element.realFKeyName}`; + const identityColumnsMap = identityLookupMaps.get(lookupKey); + + if (!identityColumnsMap) continue; - for (const row of rowsRO.rows) { const identityForCurrentValue = identityColumnsMap.get(row[element.currentFKeyName]); row[element.currentFKeyName] = typeof identityForCurrentValue === 'object' && identityForCurrentValue !== null - ? { ...identityForCurrentValue } + ? identityForCurrentValue : {}; } } @@ -395,22 +430,37 @@ export class GetTableRowsUseCase extends AbstractUseCase>> { - foreignKeysValuesCollection = [...new Set(foreignKeysValuesCollection)]; + const uniqueValues = [...new Set(foreignKeysValuesCollection)]; + + if (uniqueValues.length === 0) { + return []; + } + const batchSize = 50; - const chunkedValues = this.chunkArray(foreignKeysValuesCollection, batchSize); - let identityColumns = []; - for (const chunk of chunkedValues) { - const result = await dao.getIdentityColumns( + if (uniqueValues.length <= batchSize) { + return await dao.getIdentityColumns( foreignKey.referenced_table_name, foreignKey.referenced_column_name, foreignTableSettings?.identity_column, - chunk, + uniqueValues, userEmail, ); - identityColumns = identityColumns.concat(result); } - return identityColumns; + const chunkedValues = this.chunkArray(uniqueValues, batchSize); + const results = await Promise.all( + chunkedValues.map((chunk) => + dao.getIdentityColumns( + foreignKey.referenced_table_name, + foreignKey.referenced_column_name, + foreignTableSettings?.identity_column, + chunk, + userEmail, + ), + ), + ); + + return results.flat(); } } 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 new file mode 100644 index 000000000..b284193be --- /dev/null +++ b/backend/test/ava-tests/complex-table-tests/complex-postgres-table-e2e.test.ts @@ -0,0 +1,240 @@ +import { faker } from '@faker-js/faker'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import test from 'ava'; +import { ValidationError } from 'class-validator'; +import cookieParser from 'cookie-parser'; +import fs from 'fs'; +import path, { join } from 'path'; +import request from 'supertest'; +import { fileURLToPath } from 'url'; +import { ApplicationModule } from '../../../src/app.module.js'; +import { LogOperationTypeEnum, QueryOrderingEnum } from '../../../src/enums/index.js'; +import { AllExceptionsFilter } from '../../../src/exceptions/all-exceptions.filter.js'; +import { ValidationException } from '../../../src/exceptions/custom-exceptions/validation-exception.js'; +import { Messages } from '../../../src/exceptions/text/messages.js'; +import { Cacher } from '../../../src/helpers/cache/cacher.js'; +import { Constants } from '../../../src/helpers/constants/constants.js'; +import { DatabaseModule } from '../../../src/shared/database/database.module.js'; +import { DatabaseService } from '../../../src/shared/database/database.service.js'; +import { MockFactory } from '../../mock.factory.js'; +import { createTestTable } from '../../utils/create-test-table.js'; +import { getTestData } from '../../utils/get-test-data.js'; +import { registerUserAndReturnUserInfo } from '../../utils/register-user-and-return-user-info.js'; +import { TestUtils } from '../../utils/test.utils.js'; +import { WinstonLogger } from '../../../src/entities/logging/winston-logger.js'; +import { getTestKnex } from '../../utils/get-test-knex.js'; +import { getRandomTestTableName } from '../../utils/get-random-test-table-name.js'; +import { + createTestTablesWithComplexPFKeys, + createTestTablesWithSimplePFKeys, +} from '../../utils/test-utilities/create-test-postgres-tables.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const mockFactory = new MockFactory(); +let app: INestApplication; +let testUtils: TestUtils; +const testSearchedUserName = 'Vasia'; +const connectionToTestDB = getTestData(mockFactory).connectionToPostgres; + +let testTablesCompositeKeysData: TableCreationResult; +let testTablesSimpleKeysData: TableCreationResult; + +test.before(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ApplicationModule, DatabaseModule], + providers: [DatabaseService, TestUtils], + }).compile(); + app = moduleFixture.createNestApplication() as any; + testUtils = moduleFixture.get(TestUtils); + + app.use(cookieParser()); + app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); + app.useGlobalPipes( + new ValidationPipe({ + exceptionFactory(validationErrors: ValidationError[] = []) { + return new ValidationException(validationErrors); + }, + }), + ); + await app.init(); + app.getHttpServer().listen(0); + + testTablesCompositeKeysData = await createTestTablesWithComplexPFKeys(connectionToTestDB); + testTablesSimpleKeysData = await createTestTablesWithSimplePFKeys(connectionToTestDB); +}); + +test.after(async () => { + try { + await Cacher.clearAllCache(); + await app.close(); + } catch (e) { + console.error('After tests error ' + e); + } +}); + +export type TableCreationResult = { + main_table: { + table_name: string; + column_names: string[]; + foreign_key_column_names: string[]; + binary_column_names: string[]; + primary_key_column_names: string[]; + }; + first_referenced_table: { + table_name: string; + column_names: string[]; + primary_key_column_names: string[]; + }; + second_referenced_table: { + table_name: string; + column_names: string[]; + primary_key_column_names: string[]; + foreign_key_column_names: string[]; + }; +}; + +// GET /connection/tables/:slug + +test.serial( + `GET /connection/tables/:slug - Should return list of table rows with referenced columns, when primary and foreign keys has composite structure`, + 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, second_referenced_table } = testTablesCompositeKeysData; + + const getMainTableRowsResponse = 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'); + + const getMainTableRowsRO = JSON.parse(getMainTableRowsResponse.text); + t.is(getMainTableRowsResponse.status, 200); + + const getFirstReferencedTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getFirstReferencedTableRowsRO = JSON.parse(getFirstReferencedTableRowsResponse.text); + t.is(getFirstReferencedTableRowsResponse.status, 200); + + const firstReturnedRows = getFirstReferencedTableRowsRO.rows; + + for (const row of firstReturnedRows) { + t.is(typeof row.order_id, 'object'); + t.truthy(row.order_id.hasOwnProperty('order_id')); + t.truthy(row.order_id.order_id); + t.truthy(typeof row.order_id.order_id === 'number'); + t.is(typeof row.customer_id, 'object'); + t.truthy(row.customer_id.hasOwnProperty('customer_id')); + t.truthy(row.customer_id.customer_id); + t.truthy(typeof row.customer_id.customer_id === 'number'); + } + + const getSecondReferencedTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${second_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getSecondReferencedTableRowsRO = JSON.parse(getSecondReferencedTableRowsResponse.text); + t.is(getSecondReferencedTableRowsResponse.status, 200); + const secondReturnedRows = getSecondReferencedTableRowsRO.rows; + for (const row of secondReturnedRows) { + t.is(typeof row.order_id, 'object'); + t.truthy(row.order_id.hasOwnProperty('order_id')); + t.truthy(row.order_id.order_id); + t.truthy(typeof row.order_id.order_id === 'number'); + t.is(typeof row.customer_id, 'object'); + t.truthy(row.customer_id.hasOwnProperty('customer_id')); + t.truthy(row.customer_id.customer_id); + t.truthy(typeof row.customer_id.customer_id === 'number'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); + +test.serial( + `GET /connection/tables/:slug - Should return list of table rows with referenced columns, when primary and foreign keys has simple structure`, + 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, second_referenced_table } = testTablesSimpleKeysData; + + const getMainTableRowsResponse = 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'); + + const getMainTableRowsRO = JSON.parse(getMainTableRowsResponse.text); + t.is(getMainTableRowsResponse.status, 200); + + const getFirstReferencedTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${first_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getFirstReferencedTableRowsRO = JSON.parse(getFirstReferencedTableRowsResponse.text); + t.is(getFirstReferencedTableRowsResponse.status, 200); + + const firstReturnedRows = getFirstReferencedTableRowsRO.rows; + + for (const element of firstReturnedRows) { + t.is(typeof element.customer_id, 'object'); + t.truthy(element.customer_id.hasOwnProperty('customer_id')); + t.truthy(element.customer_id.customer_id); + t.truthy(typeof element.customer_id.customer_id === 'number'); + } + + const getSecondReferencedTableRowsResponse = await request(app.getHttpServer()) + .get(`/table/rows/${createConnectionRO.id}?tableName=${second_referenced_table.table_name}`) + .set('Cookie', firstUserToken) + .set('Content-Type', 'application/json') + .set('Accept', 'application/json'); + + const getSecondReferencedTableRowsRO = JSON.parse(getSecondReferencedTableRowsResponse.text); + t.is(getSecondReferencedTableRowsResponse.status, 200); + + const secondReturnedRows = getSecondReferencedTableRowsRO.rows; + for (const element of secondReturnedRows) { + t.is(typeof element.order_id, 'object'); + t.truthy(element.order_id.hasOwnProperty('order_id')); + t.truthy(element.order_id.order_id); + t.truthy(typeof element.order_id.order_id === 'number'); + } + } catch (e) { + console.error(e); + throw e; + } + }, +); diff --git a/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts b/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts index 084f65987..c5bb6ed0b 100644 --- a/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts +++ b/backend/test/ava-tests/saas-tests/custom-field-e2e.test.ts @@ -146,7 +146,8 @@ test.serial(`${currentTest} should return custom fields array, when custom field .set('Accept', 'application/json'); t.is(getTableRowsResponse.status, 200); const getTableRowsRO = JSON.parse(getTableRowsResponse.text); - t.is(getTableRowsRO.rows.length >= 10, true); + + t.is(getTableRowsRO.rows.length > 0, true); for (const row of getTableRowsRO.rows) { t.is(row.hasOwnProperty('#autoadmin:customFields'), true); t.is(typeof row['#autoadmin:customFields'], 'object'); diff --git a/backend/test/utils/test-utilities/create-test-postgres-tables.ts b/backend/test/utils/test-utilities/create-test-postgres-tables.ts new file mode 100644 index 000000000..a0d5c1ae2 --- /dev/null +++ b/backend/test/utils/test-utilities/create-test-postgres-tables.ts @@ -0,0 +1,300 @@ +import { getTestKnex } from '../get-test-knex.js'; +import { faker } from '@faker-js/faker'; + +export const createTestTablesWithComplexPFKeys = async (connectionParams: any) => { + const connectionParamsCopy = { + ...connectionParams, + }; + if (connectionParams.type === 'mysql') { + connectionParamsCopy.type = 'mysql2'; + } + + const knex = getTestKnex(connectionParamsCopy); + + const mainTableName = 'Orders_Complex'; + const firstReferencedTableName = 'OrderItems_Complex'; + const referencedOnTableName = 'Shipments_Complex'; + + await knex.schema.dropTableIfExists(referencedOnTableName); + await knex.schema.dropTableIfExists(firstReferencedTableName); + await knex.schema.dropTableIfExists(mainTableName); + + // Create main_table + await knex.schema.createTable(mainTableName, (table) => { + table.integer('order_id').notNullable(); + table.integer('customer_id').notNullable(); + table.date('order_date'); + table.string('status', 20); + table.decimal('total_amount', 10, 2); + table.primary(['order_id', 'customer_id']); + }); + + // Create first_referenced_table + const firstReferencedTableQuery = knex.schema.createTable(firstReferencedTableName, (table) => { + table.integer('order_id').notNullable(); + table.integer('customer_id').notNullable(); + table.increments('item_id').primary(); + table.string('product_name', 100); + table.integer('quantity'); + table.decimal('price_per_unit', 10, 2); + table + .foreign(['order_id', 'customer_id']) + .references(['order_id', 'customer_id']) + .inTable(mainTableName) + .onDelete('CASCADE'); + }); + console.log('SQL Query for first_referenced_table:', firstReferencedTableQuery.toString()); + await firstReferencedTableQuery; + + // Create second_referenced_table + await knex.schema.createTable(referencedOnTableName, (table) => { + table.increments('shipment_id').primary(); + table.integer('order_id'); + table.integer('customer_id'); + table.date('shipped_date'); + table.string('carrier', 50); + table.string('tracking_number', 100); + table + .foreign(['order_id', 'customer_id']) + .references(['order_id', 'customer_id']) + .inTable(mainTableName) + .onDelete('SET NULL'); + }); + + // seeding data in test tables + + const testEntitiesCount = 42; + + const mainTableInserts = []; + const firstReferencedTableInserts = []; + const secondReferencedTableInserts = []; + + for (let i = 0; i < testEntitiesCount; i++) { + const orderId = i + 1; + const customerId = 100 + i; + mainTableInserts.push({ + order_id: orderId, + customer_id: customerId, + order_date: faker.date.past({ years: 1 }), + status: faker.helpers.arrayElement(['Pending', 'Shipped', 'Delivered', 'Cancelled']), + total_amount: parseFloat(faker.commerce.price({ min: 20, max: 500, dec: 2 })), + }); + + const itemsCount = faker.number.int({ min: 1, max: 5 }); + for (let j = 0; j < itemsCount; j++) { + firstReferencedTableInserts.push({ + order_id: orderId, + customer_id: customerId, + product_name: faker.commerce.productName(), + quantity: faker.number.int({ min: 1, max: 10 }), + price_per_unit: parseFloat(faker.commerce.price({ min: 5, max: 100, dec: 2 })), + }); + } + + if (faker.datatype.boolean()) { + secondReferencedTableInserts.push({ + order_id: orderId, + customer_id: customerId, + shipped_date: faker.date.recent({ days: 30 }), + carrier: faker.company.name(), + tracking_number: faker.string.uuid(), + }); + } + } + + await knex(mainTableName).insert(mainTableInserts); + await knex(firstReferencedTableName).insert(firstReferencedTableInserts); + await knex(referencedOnTableName).insert(secondReferencedTableInserts); + + // Fetch column info + + const mainTableColumnData = await knex(mainTableName).columnInfo(); + const firstReferencedTableColumnData = await knex(firstReferencedTableName).columnInfo(); + const secondReferencedTableColumnData = await knex(referencedOnTableName).columnInfo(); + + return { + main_table: { + table_name: mainTableName, + column_names: Object.keys(mainTableColumnData), + foreign_key_column_names: [], + binary_column_names: [], + primary_key_column_names: ['order_id', 'customer_id'], + }, + first_referenced_table: { + table_name: firstReferencedTableName, + column_names: Object.keys(firstReferencedTableColumnData), + primary_key_column_names: ['item_id'], + }, + second_referenced_table: { + table_name: referencedOnTableName, + column_names: Object.keys(secondReferencedTableColumnData), + primary_key_column_names: ['shipment_id'], + foreign_key_column_names: [], + }, + }; +}; + +export const createTestTablesWithSimplePFKeys = async (connectionParams: any) => { + const connectionParamsCopy = { + ...connectionParams, + }; + if (connectionParams.type === 'mysql') { + connectionParamsCopy.type = 'mysql2'; + } + + const knex = getTestKnex(connectionParamsCopy); + + // -- Table 1: Customers (simple primary key) + // CREATE TABLE customers ( + // customer_id SERIAL PRIMARY KEY, + // name VARCHAR(100), + // email VARCHAR(100), + // created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + // ); + + // -- Table 2: Orders (foreign key to Customers) + // CREATE TABLE orders ( + // order_id SERIAL PRIMARY KEY, + // customer_id INT, + // order_date DATE, + // status VARCHAR(20), + // total_amount DECIMAL(10, 2), + // FOREIGN KEY (customer_id) + // REFERENCES customers(customer_id) + // ON DELETE CASCADE + // ); + + // -- Table 3: Shipments (foreign key to Orders) + // CREATE TABLE shipments ( + // shipment_id SERIAL PRIMARY KEY, + // order_id INT, + // shipped_date DATE, + // carrier VARCHAR(50), + // tracking_number VARCHAR(100), + // FOREIGN KEY (order_id) + // REFERENCES orders(order_id) + // ON DELETE SET NULL + // ); + + const mainTableName = 'Customers_Simple'; + const firstReferencedTableName = 'Orders_Simple'; + const secondReferencedTableName = 'Shipments_Simple'; + + await knex.schema.dropTableIfExists(secondReferencedTableName); + await knex.schema.dropTableIfExists(firstReferencedTableName); + await knex.schema.dropTableIfExists(mainTableName); + + // Create main_table + await knex.schema.createTable(mainTableName, (table) => { + table.increments('customer_id').primary(); + table.string('name', 100); + table.string('email', 100); + table.timestamp('created_at').defaultTo(knex.fn.now()); + }); + + // Create first_referenced_table + await knex.schema.createTable(firstReferencedTableName, (table) => { + table.increments('order_id').primary(); + table.integer('customer_id').notNullable(); + table.date('order_date'); + table.string('status', 20); + table.decimal('total_amount', 10, 2); + table.foreign('customer_id').references('customer_id').inTable(mainTableName).onDelete('CASCADE'); + }); + + // Create second_referenced_table + await knex.schema.createTable(secondReferencedTableName, (table) => { + table.increments('shipment_id').primary(); + table.integer('order_id'); + table.date('shipped_date'); + table.string('carrier', 50); + table.string('tracking_number', 100); + table.foreign('order_id').references('order_id').inTable(firstReferencedTableName).onDelete('SET NULL'); + }); + + // seeding data in test tables + + const testEntitiesCount = 42; + + const mainTableInserts = []; + const firstReferencedTableInserts = []; + const secondReferencedTableInserts = []; + + for (let i = 0; i < testEntitiesCount; i++) { + mainTableInserts.push({ + name: faker.person.fullName(), + email: faker.internet.email(), + }); + } + + const insertedCustomerIds = await knex(mainTableName).insert(mainTableInserts).returning('customer_id'); + + for (let i = 0; i < testEntitiesCount; i++) { + const customerId = Array.isArray(insertedCustomerIds) + ? typeof insertedCustomerIds[i] === 'object' + ? (insertedCustomerIds[i] as any).customer_id + : insertedCustomerIds[i] + : insertedCustomerIds; + const orderId = i + 1; + firstReferencedTableInserts.push({ + customer_id: customerId, + order_date: faker.date.past({ years: 1 }), + status: faker.helpers.arrayElement(['Pending', 'Shipped', 'Delivered', 'Cancelled']), + total_amount: parseFloat(faker.commerce.price({ min: 20, max: 500, dec: 2 })), + }); + + if (faker.datatype.boolean()) { + secondReferencedTableInserts.push({ + order_id: orderId, + shipped_date: faker.date.recent({ days: 30 }), + carrier: faker.company.name(), + tracking_number: faker.string.uuid(), + }); + } + } + + const insertedOrderIds = await knex(firstReferencedTableName) + .insert(firstReferencedTableInserts) + .returning('order_id'); + + // Adjust order_id in shipments to match inserted orders + for (let i = 0; i < secondReferencedTableInserts.length; i++) { + if (insertedOrderIds[i]) { + secondReferencedTableInserts[i].order_id = Array.isArray(insertedOrderIds) + ? typeof insertedOrderIds[i] === 'object' + ? (insertedOrderIds[i] as any).order_id + : insertedOrderIds[i] + : insertedOrderIds; + } + } + + await knex(secondReferencedTableName).insert(secondReferencedTableInserts); + + // Fetch column info + + const mainTableColumnData = await knex(mainTableName).columnInfo(); + const firstReferencedTableColumnData = await knex(firstReferencedTableName).columnInfo(); + const secondReferencedTableColumnData = await knex(secondReferencedTableName).columnInfo(); + + return { + main_table: { + table_name: mainTableName, + column_names: Object.keys(mainTableColumnData), + foreign_key_column_names: [], + binary_column_names: [], + primary_key_column_names: ['customer_id'], + }, + first_referenced_table: { + table_name: firstReferencedTableName, + column_names: Object.keys(firstReferencedTableColumnData), + primary_key_column_names: ['order_id'], + foreign_key_column_names: ['customer_id'], + }, + second_referenced_table: { + table_name: secondReferencedTableName, + column_names: Object.keys(secondReferencedTableColumnData), + primary_key_column_names: ['shipment_id'], + foreign_key_column_names: ['order_id'], + }, + }; +}; diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts index e02eb4fd7..67cc5bee0 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-postgres.ts @@ -274,10 +274,10 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I column_name: string; }> = await knex(tableName) .select( - knex.raw(`tc.constraint_name, + knex.raw(`kcu.constraint_name, kcu.column_name, - ccu.table_name AS foreign_table_name, - ccu.column_name AS foreign_column_name`), + kcu2.table_name AS foreign_table_name, + kcu2.column_name AS foreign_column_name`), ) .from( knex.raw( @@ -285,11 +285,15 @@ export class DataAccessObjectPostgres extends BasicDataAccessObject implements I JOIN ??.information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema - JOIN ??.information_schema.constraint_column_usage AS ccu - ON ccu.constraint_name = tc.constraint_name - AND ccu.table_schema = tc.table_schema + JOIN ??.information_schema.referential_constraints AS rc + ON tc.constraint_name = rc.constraint_name + AND tc.table_schema = rc.constraint_schema + JOIN ??.information_schema.key_column_usage AS kcu2 + ON rc.unique_constraint_name = kcu2.constraint_name + AND rc.unique_constraint_schema = kcu2.table_schema + AND kcu.ordinal_position = kcu2.ordinal_position WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name=? AND tc.table_schema =?;`, - [database, database, database, tableName, tableSchema], + [database, database, database, database, tableName, tableSchema], ), ); const resultKeys = foreignKeys.map((key) => {