diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts index 8a65d447d..183c58106 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-ibmdb2.ts @@ -25,286 +25,324 @@ import { IDataAccessObject } from '../../shared/interfaces/data-access-object.in import { BasicDataAccessObject } from './basic-data-access-object.js'; export class DataAccessObjectIbmDb2 extends BasicDataAccessObject implements IDataAccessObject { - - public async addRowInTable( - tableName: string, - row: Record, - ): Promise> { - this.validateNamesAndThrowError([tableName, this.connection.schema, ...Object.keys(row)]); - const connectionToDb = await this.getConnectionToDatabase(); - const [tableStructure, primaryColumns] = await Promise.all([ - this.getTableStructure(tableName), - this.getTablePrimaryColumns(tableName), - ]); - - const jsonColumnNames = tableStructure - .filter((structEl) => structEl.data_type.toLowerCase() === 'json') - .map((structEl) => structEl.column_name); - - for (const key in row) { - if (jsonColumnNames.includes(key)) { - // eslint-disable-next-line security/detect-object-injection - row[key] = JSON.stringify(row[key]); - } - } - - const columns = Object.keys(row).join(', '); - const placeholders = Object.keys(row) - .map(() => '?') - .join(', '); - const values = Object.values(row); - const query = ` + public async addRowInTable( + tableName: string, + row: Record, + ): Promise> { + this.validateNamesAndThrowError([tableName, this.connection.schema, ...Object.keys(row)]); + const connectionToDb = await this.getConnectionToDatabase(); + const [tableStructure, primaryColumns] = await Promise.all([ + this.getTableStructure(tableName), + this.getTablePrimaryColumns(tableName), + ]); + + const jsonColumnNames = tableStructure + .filter((structEl) => structEl.data_type.toLowerCase() === 'json') + .map((structEl) => structEl.column_name); + + for (const key in row) { + if (jsonColumnNames.includes(key)) { + // eslint-disable-next-line security/detect-object-injection + row[key] = JSON.stringify(row[key]); + } + } + + const columns = Object.keys(row).join(', '); + const placeholders = Object.keys(row) + .map(() => '?') + .join(', '); + const values = Object.values(row); + const query = ` INSERT INTO ${this.connection.schema.toUpperCase()}.${tableName.toUpperCase()} (${columns}) VALUES (${placeholders}) `; - await connectionToDb.query(query, values); + await connectionToDb.query(query, values); - if (primaryColumns?.length > 0) { - const primaryKey = primaryColumns.map((column) => column.column_name); - const selectQuery = ` + if (primaryColumns?.length > 0) { + const primaryKey = primaryColumns.map((column) => column.column_name); + const selectQuery = ` SELECT ${primaryKey.join(', ')} FROM ${this.connection.schema.toUpperCase()}.${tableName.toUpperCase()} WHERE ${Object.keys(row) - .map((key) => `${key} = ?`) - .join(' AND ')} + .map((key) => `${key} = ?`) + .join(' AND ')} `; - const result = await connectionToDb.query(selectQuery, Object.values(row)); - return result[0]; - } - return row; - } - - public async deleteRowInTable( - tableName: string, - primaryKey: Record, - ): Promise> { - this.validateNamesAndThrowError([tableName, this.connection.schema, ...Object.keys(primaryKey)]); - const connectionToDb = await this.getConnectionToDatabase(); - const whereClause = Object.keys(primaryKey) - .map((key) => `${key} = ?`) - .join(' AND '); - const query = ` + const result = await connectionToDb.query(selectQuery, Object.values(row)); + return result[0]; + } + return row; + } + + public async deleteRowInTable( + tableName: string, + primaryKey: Record, + ): Promise> { + this.validateNamesAndThrowError([tableName, this.connection.schema, ...Object.keys(primaryKey)]); + const connectionToDb = await this.getConnectionToDatabase(); + const whereClause = Object.keys(primaryKey) + .map((key) => `${key} = ?`) + .join(' AND '); + const query = ` DELETE FROM ${this.connection.schema.toUpperCase()}.${tableName.toUpperCase()} WHERE ${whereClause} `; - const params = Object.values(primaryKey); - await connectionToDb.query(query, params); - return primaryKey; - } - - public async getIdentityColumns( - tableName: string, - referencedFieldName: string, - identityColumnName: string, - fieldValues: (string | number)[], - ): Promise>> { - const schemaName = this.connection.schema.toUpperCase(); - const namesToValidate = [tableName, referencedFieldName, schemaName]; - if (identityColumnName) { - namesToValidate.push(identityColumnName); - } - this.validateNamesAndThrowError(namesToValidate); - const connectionToDb = await this.getConnectionToDatabase(); - const columnsToSelect = identityColumnName ? `${referencedFieldName}, ${identityColumnName}` : referencedFieldName; - const placeholders = fieldValues.map(() => '?').join(', '); - const query = ` + const params = Object.values(primaryKey); + await connectionToDb.query(query, params); + return primaryKey; + } + + public async getIdentityColumns( + tableName: string, + referencedFieldName: string, + identityColumnName: string, + fieldValues: (string | number)[], + ): Promise>> { + const schemaName = this.connection.schema.toUpperCase(); + const namesToValidate = [tableName, referencedFieldName, schemaName]; + if (identityColumnName) { + namesToValidate.push(identityColumnName); + } + this.validateNamesAndThrowError(namesToValidate); + const connectionToDb = await this.getConnectionToDatabase(); + const columnsToSelect = identityColumnName ? `${referencedFieldName}, ${identityColumnName}` : referencedFieldName; + const placeholders = fieldValues.map(() => '?').join(', '); + const query = ` SELECT ${columnsToSelect} FROM ${schemaName}.${tableName.toUpperCase()} WHERE ${referencedFieldName} IN (${placeholders}) `; - const result = connectionToDb.query(query, [...fieldValues]); - return result; - } - - public async getRowByPrimaryKey( - tableName: string, - primaryKey: Record, - settings: TableSettingsDS, - ): Promise> { - this.validateNamesAndThrowError([tableName, this.connection.schema, ...Object.keys(primaryKey)]); - const connectionToDb = await this.getConnectionToDatabase(); - const whereClause = Object.keys(primaryKey) - .map((key) => `${key} = ?`) - .join(' AND '); - let selectFields = '*'; - if (settings) { - const tableStructure = await this.getTableStructure(tableName); - const availableFields = this.findAvailableFields(settings, tableStructure); - selectFields = availableFields.join(', '); - } - const query = ` + const result = connectionToDb.query(query, [...fieldValues]); + return result; + } + + public async getRowByPrimaryKey( + tableName: string, + primaryKey: Record, + settings: TableSettingsDS, + ): Promise> { + this.validateNamesAndThrowError([tableName, this.connection.schema, ...Object.keys(primaryKey)]); + const connectionToDb = await this.getConnectionToDatabase(); + const whereClause = Object.keys(primaryKey) + .map((key) => `${key} = ?`) + .join(' AND '); + let selectFields = '*'; + if (settings) { + const tableStructure = await this.getTableStructure(tableName); + const availableFields = this.findAvailableFields(settings, tableStructure); + selectFields = availableFields.join(', '); + } + const query = ` SELECT ${selectFields} FROM ${this.connection.schema.toUpperCase()}.${tableName.toUpperCase()} WHERE ${whereClause} `; - const params = Object.values(primaryKey); - const result = await connectionToDb.query(query, params); - return result[0]; - } - - public async bulkGetRowsFromTableByPrimaryKeys( - tableName: string, - primaryKeys: Array>, - settings: TableSettingsDS, - ): Promise>> { - this.validateNamesAndThrowError([tableName, this.connection.schema, ...primaryKeys.flatMap(Object.keys)]); - - const connectionToDb = await this.getConnectionToDatabase(); - const schemaName = this.connection.schema.toUpperCase(); - tableName = tableName.toUpperCase(); - - let selectFields = '*'; - if (settings) { - const tableStructure = await this.getTableStructure(tableName); - const availableFields = this.findAvailableFields(settings, tableStructure); - selectFields = availableFields.join(', '); - } - - const whereClauses = primaryKeys - .map((primaryKey) => { - const conditions = Object.entries(primaryKey) - .map(([key, _value]) => `${key} = ?`) - .join(' AND '); - return `(${conditions})`; - }) - .join(' OR '); - - const flatPrimaryKeysValues = primaryKeys.flatMap(Object.values); - - const query = ` + const params = Object.values(primaryKey); + const result = await connectionToDb.query(query, params); + return result[0]; + } + + public async bulkGetRowsFromTableByPrimaryKeys( + tableName: string, + primaryKeys: Array>, + settings: TableSettingsDS, + ): Promise>> { + this.validateNamesAndThrowError([tableName, this.connection.schema, ...primaryKeys.flatMap(Object.keys)]); + + const connectionToDb = await this.getConnectionToDatabase(); + const schemaName = this.connection.schema.toUpperCase(); + tableName = tableName.toUpperCase(); + + let selectFields = '*'; + if (settings) { + const tableStructure = await this.getTableStructure(tableName); + const availableFields = this.findAvailableFields(settings, tableStructure); + selectFields = availableFields.join(', '); + } + + const whereClauses = primaryKeys + .map((primaryKey) => { + const conditions = Object.entries(primaryKey) + .map(([key, _value]) => `${key} = ?`) + .join(' AND '); + return `(${conditions})`; + }) + .join(' OR '); + + const flatPrimaryKeysValues = primaryKeys.flatMap(Object.values); + + const query = ` SELECT ${selectFields} FROM ${schemaName}.${tableName} WHERE ${whereClauses} `; - const results = await connectionToDb.query(query, flatPrimaryKeysValues); - return results; - } - - public async getRowsFromTable( - tableName: string, - settings: TableSettingsDS, - page: number, - perPage: number, - searchedFieldValue: string, - filteringFields: FilteringFieldsDS[], - autocompleteFields: AutocompleteFieldsDS, - tableStructure: TableStructureDS[] | null, - ): Promise { - const connectionSchema = this.connection.schema.toUpperCase(); - tableName = tableName.toUpperCase(); - this.validateNamesAndThrowError([tableName, connectionSchema]); - - if (!page || page <= 0) { - page = DAO_CONSTANTS.DEFAULT_PAGINATION.page; - const { list_per_page } = settings; - if (list_per_page && list_per_page > 0 && (!perPage || perPage <= 0)) { - perPage = list_per_page; - } else { - perPage = DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; - } - } - const connectionToDb = await this.getConnectionToDatabase(); - - const { large_dataset, rowsCount } = await this.getRowsCount(tableName, this.connection.schema); - if (!tableStructure) { - tableStructure = await this.getTableStructure(tableName); - } - const availableFields = this.findAvailableFields(settings, tableStructure); - - const lastPage = Math.ceil(rowsCount / perPage); - let rowsRO: FoundRowsDS; - - if (autocompleteFields?.value && autocompleteFields.fields.length > 0) { - const fields = autocompleteFields.fields.join(', '); - const autocompleteQuery = `SELECT ${fields} FROM ${connectionSchema}.${tableName} WHERE ${fields} LIKE '${autocompleteFields.value}%' FETCH FIRST ${DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT} ROWS ONLY`; - const rows = await connectionToDb.query(autocompleteQuery); - - rowsRO = { - data: rows, - pagination: {} as any, - large_dataset: large_dataset, - }; - return rowsRO; - } - - let searchQuery = ''; - if (searchedFieldValue) { - const searchFields = settings.search_fields?.length > 0 ? settings.search_fields : availableFields; - if (rowsCount <= 1000) { - const searchConditions = searchFields - .map((field) => `LOWER(CAST(${field} AS VARCHAR(255))) LIKE '%${searchedFieldValue.toLowerCase()}%'`) - .join(' OR '); - searchQuery = ` WHERE (${searchConditions})`; - } else { - const searchConditions = searchFields - .map((field) => `LOWER(CAST(${field} AS VARCHAR(255))) LIKE '${searchedFieldValue.toLowerCase()}%'`) - .join(' OR '); - searchQuery = ` WHERE (${searchConditions})`; - } - } - - let filterQuery = ''; - if (filteringFields && filteringFields.length > 0) { - const filterConditions = filteringFields - .map((filterObject) => { - switch (filterObject.criteria) { - case FilterCriteriaEnum.eq: - return `${filterObject.field} = '${filterObject.value}'`; - case FilterCriteriaEnum.startswith: - return `${filterObject.field} LIKE '${filterObject.value}%'`; - case FilterCriteriaEnum.endswith: - return `${filterObject.field} LIKE '%${filterObject.value}'`; - case FilterCriteriaEnum.gt: - return `${filterObject.field} > ${filterObject.value}`; - case FilterCriteriaEnum.lt: - return `${filterObject.field} < ${filterObject.value}`; - case FilterCriteriaEnum.lte: - return `${filterObject.field} <= ${filterObject.value}`; - case FilterCriteriaEnum.gte: - return `${filterObject.field} >= ${filterObject.value}`; - case FilterCriteriaEnum.contains: - return `${filterObject.field} LIKE '%${filterObject.value}%'`; - case FilterCriteriaEnum.icontains: - return `${filterObject.field} NOT LIKE '%${filterObject.value}%'`; - case FilterCriteriaEnum.empty: - return `(${filterObject.field} IS NULL)`; - } - }) - .join(' AND '); - filterQuery = ` AND (${filterConditions})`; - } - - const orderQuery = - settings.ordering_field && settings.ordering ? ` ORDER BY ${settings.ordering_field} ${settings.ordering}` : ''; - const paginationQuery = ` OFFSET ${(page - 1) * perPage} ROWS FETCH NEXT ${perPage} ROWS ONLY`; - - const rowsQuery = `SELECT ${availableFields.join( - ',', - )} FROM ${connectionSchema}.${tableName}${searchQuery}${filterQuery}${orderQuery}${paginationQuery}`; - - const rows = await connectionToDb.query(rowsQuery); - - rowsRO = { - data: rows, - pagination: { - total: rowsCount, - lastPage: lastPage, - perPage: perPage, - currentPage: page, - } as any, - large_dataset: large_dataset, - }; - return rowsRO; - } - - public async getTableForeignKeys(tableName: string): Promise { - const cachedForeignKeys = LRUStorage.getTableForeignKeysCache(this.connection, tableName); - if (cachedForeignKeys) { - return cachedForeignKeys; - } - const connectionToDb = await this.getConnectionToDatabase(); - const query = ` + const results = await connectionToDb.query(query, flatPrimaryKeysValues); + return results; + } + + public async getRowsFromTable( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: FilteringFieldsDS[], + autocompleteFields: AutocompleteFieldsDS, + tableStructure: TableStructureDS[] | null, + ): Promise { + const connectionSchema = this.connection.schema.toUpperCase(); + tableName = tableName.toUpperCase(); + this.validateNamesAndThrowError([tableName, connectionSchema]); + + if (!page || page <= 0) { + page = DAO_CONSTANTS.DEFAULT_PAGINATION.page; + const { list_per_page } = settings; + if (list_per_page && list_per_page > 0 && (!perPage || perPage <= 0)) { + perPage = list_per_page; + } else { + perPage = DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; + } + } + const connectionToDb = await this.getConnectionToDatabase(); + + const { large_dataset, rowsCount } = await this.getRowsCount(tableName, this.connection.schema); + if (!tableStructure) { + tableStructure = await this.getTableStructure(tableName); + } + const availableFields = this.findAvailableFields(settings, tableStructure); + + const lastPage = Math.ceil(rowsCount / perPage); + let rowsRO: FoundRowsDS; + + const queryParams: unknown[] = []; + + if (autocompleteFields?.value && autocompleteFields.fields.length > 0) { + const validatedAutocompleteFields = autocompleteFields.fields.filter((field) => availableFields.includes(field)); + if (validatedAutocompleteFields.length === 0) { + throw new Error('Invalid autocomplete fields provided'); + } + this.validateNamesAndThrowError(validatedAutocompleteFields); + const fields = validatedAutocompleteFields.join(', '); + const autocompleteConditions = validatedAutocompleteFields.map((field) => `${field} LIKE ?`).join(' OR '); + const autocompleteQuery = `SELECT ${fields} FROM ${connectionSchema}.${tableName} WHERE ${autocompleteConditions} FETCH FIRST ${DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT} ROWS ONLY`; + const autocompleteParams = validatedAutocompleteFields.map(() => `${autocompleteFields.value}%`); + const rows = await connectionToDb.query(autocompleteQuery, autocompleteParams); + + rowsRO = { + data: rows, + pagination: {} as any, + large_dataset: large_dataset, + }; + return rowsRO; + } + + let searchQuery = ''; + if (searchedFieldValue) { + const searchFields = settings.search_fields?.length > 0 ? settings.search_fields : availableFields; + const validatedSearchFields = searchFields.filter((field) => availableFields.includes(field)); + this.validateNamesAndThrowError(validatedSearchFields); + if (rowsCount <= 1000) { + const searchConditions = validatedSearchFields + .map((field) => { + queryParams.push(`%${searchedFieldValue.toLowerCase()}%`); + return `LOWER(CAST(${field} AS VARCHAR(255))) LIKE ?`; + }) + .join(' OR '); + searchQuery = ` WHERE (${searchConditions})`; + } else { + const searchConditions = validatedSearchFields + .map((field) => { + queryParams.push(`${searchedFieldValue.toLowerCase()}%`); + return `LOWER(CAST(${field} AS VARCHAR(255))) LIKE ?`; + }) + .join(' OR '); + searchQuery = ` WHERE (${searchConditions})`; + } + } + + let filterQuery = ''; + if (filteringFields && filteringFields.length > 0) { + const invalidFields = filteringFields.filter((f) => !availableFields.includes(f.field)); + if (invalidFields.length > 0) { + throw new Error(`Invalid filter fields: ${invalidFields.map((f) => f.field).join(', ')}`); + } + this.validateNamesAndThrowError(filteringFields.map((f) => f.field)); + + const filterConditions = filteringFields + .map((filterObject) => { + switch (filterObject.criteria) { + case FilterCriteriaEnum.eq: + queryParams.push(filterObject.value); + return `${filterObject.field} = ?`; + case FilterCriteriaEnum.startswith: + queryParams.push(`${filterObject.value}%`); + return `${filterObject.field} LIKE ?`; + case FilterCriteriaEnum.endswith: + queryParams.push(`%${filterObject.value}`); + return `${filterObject.field} LIKE ?`; + case FilterCriteriaEnum.gt: + queryParams.push(filterObject.value); + return `${filterObject.field} > ?`; + case FilterCriteriaEnum.lt: + queryParams.push(filterObject.value); + return `${filterObject.field} < ?`; + case FilterCriteriaEnum.lte: + queryParams.push(filterObject.value); + return `${filterObject.field} <= ?`; + case FilterCriteriaEnum.gte: + queryParams.push(filterObject.value); + return `${filterObject.field} >= ?`; + case FilterCriteriaEnum.contains: + queryParams.push(`%${filterObject.value}%`); + return `${filterObject.field} LIKE ?`; + case FilterCriteriaEnum.icontains: + queryParams.push(`%${filterObject.value}%`); + return `${filterObject.field} NOT LIKE ?`; + case FilterCriteriaEnum.empty: + return `(${filterObject.field} IS NULL)`; + } + }) + .join(' AND '); + filterQuery = searchQuery ? ` AND (${filterConditions})` : ` WHERE (${filterConditions})`; + } + + let orderQuery = ''; + if (settings.ordering_field && settings.ordering) { + if (!availableFields.includes(settings.ordering_field)) { + throw new Error(`Invalid ordering field: ${settings.ordering_field}`); + } + this.validateNamesAndThrowError([settings.ordering_field]); + const orderDirection = settings.ordering.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; + orderQuery = ` ORDER BY ${settings.ordering_field} ${orderDirection}`; + } + const paginationQuery = ` OFFSET ${(page - 1) * perPage} ROWS FETCH NEXT ${perPage} ROWS ONLY`; + + const rowsQuery = `SELECT ${availableFields.join( + ',', + )} FROM ${connectionSchema}.${tableName}${searchQuery}${filterQuery}${orderQuery}${paginationQuery}`; + + const rows = await connectionToDb.query(rowsQuery, queryParams); + + rowsRO = { + data: rows, + pagination: { + total: rowsCount, + lastPage: lastPage, + perPage: perPage, + currentPage: page, + } as any, + large_dataset: large_dataset, + }; + return rowsRO; + } + + public async getTableForeignKeys(tableName: string): Promise { + const cachedForeignKeys = LRUStorage.getTableForeignKeysCache(this.connection, tableName); + if (cachedForeignKeys) { + return cachedForeignKeys; + } + const connectionToDb = await this.getConnectionToDatabase(); + const query = ` SELECT ref.constname AS constraint_name, col.colname AS column_name, @@ -329,72 +367,72 @@ WHERE ORDER BY ref.constname, col.colseq `; - const foreignKeys = await connectionToDb.query(query, [ - tableName.toUpperCase(), - this.connection.schema.toUpperCase(), - ]); - - const resultKeys = foreignKeys.map((foreignKey: any) => { - return { - column_name: foreignKey.COLUMN_NAME, - constraint_name: foreignKey.CONSTRAINT_NAME, - referenced_table_name: foreignKey.REFERENCED_TABLE_NAME, - referenced_column_name: foreignKey.REFERENCED_COLUMN_NAME, - }; - }); - LRUStorage.setTableForeignKeysCache(this.connection, tableName, resultKeys); - return resultKeys; - } - - public async getTablePrimaryColumns(tableName: string): Promise { - const cachedPrimaryColumns = LRUStorage.getTablePrimaryKeysCache(this.connection, tableName); - if (cachedPrimaryColumns) { - return cachedPrimaryColumns; - } - const connectionToDb = await this.getConnectionToDatabase(); - const query = ` + const foreignKeys = await connectionToDb.query(query, [ + tableName.toUpperCase(), + this.connection.schema.toUpperCase(), + ]); + + const resultKeys = foreignKeys.map((foreignKey: any) => { + return { + column_name: foreignKey.COLUMN_NAME, + constraint_name: foreignKey.CONSTRAINT_NAME, + referenced_table_name: foreignKey.REFERENCED_TABLE_NAME, + referenced_column_name: foreignKey.REFERENCED_COLUMN_NAME, + }; + }); + LRUStorage.setTableForeignKeysCache(this.connection, tableName, resultKeys); + return resultKeys; + } + + public async getTablePrimaryColumns(tableName: string): Promise { + const cachedPrimaryColumns = LRUStorage.getTablePrimaryKeysCache(this.connection, tableName); + if (cachedPrimaryColumns) { + return cachedPrimaryColumns; + } + const connectionToDb = await this.getConnectionToDatabase(); + const query = ` SELECT colname AS column_name, typename AS data_type FROM syscat.columns WHERE tabname = ? AND tabschema = ? AND keyseq IS NOT NULL `; - const primaryKeys = await connectionToDb.query(query, [ - tableName.toUpperCase(), - this.connection.schema.toUpperCase(), - ]); - - const resultKeys = primaryKeys.map((primaryKey: any) => { - return { - column_name: primaryKey.COLUMN_NAME, - data_type: primaryKey.DATA_TYPE as string, - }; - }); - LRUStorage.setTablePrimaryKeysCache(this.connection, tableName, resultKeys); - return resultKeys; - } - - public async getTablesFromDB(): Promise { - const connectionToDb = await this.getConnectionToDatabase(); - const query = ` + const primaryKeys = await connectionToDb.query(query, [ + tableName.toUpperCase(), + this.connection.schema.toUpperCase(), + ]); + + const resultKeys = primaryKeys.map((primaryKey: any) => { + return { + column_name: primaryKey.COLUMN_NAME, + data_type: primaryKey.DATA_TYPE as string, + }; + }); + LRUStorage.setTablePrimaryKeysCache(this.connection, tableName, resultKeys); + return resultKeys; + } + + public async getTablesFromDB(): Promise { + const connectionToDb = await this.getConnectionToDatabase(); + const query = ` SELECT tabname AS table_name, type AS table_type FROM syscat.tables WHERE tabschema = ? AND type IN ('T', 'V') `; - const tables = await connectionToDb.query(query, [this.connection.schema.toUpperCase()]); - - return tables.map((table: any) => { - return { - tableName: table.TABLE_NAME, - isView: table.TABLE_TYPE === 'V', - }; - }); - } - - public async getTableStructure(tableName: string): Promise { - const connectionToDb = await this.getConnectionToDatabase(); - const query = ` + const tables = await connectionToDb.query(query, [this.connection.schema.toUpperCase()]); + + return tables.map((table: any) => { + return { + tableName: table.TABLE_NAME, + isView: table.TABLE_TYPE === 'V', + }; + }); + } + + public async getTableStructure(tableName: string): Promise { + const connectionToDb = await this.getConnectionToDatabase(); + const query = ` SELECT colname AS column_name, typename AS data_type, length AS character_maximum_length, @@ -406,120 +444,120 @@ ORDER BY WHERE tabname = ? AND tabschema = ? `; - const tableStructure = await connectionToDb.query(query, [ - tableName.toUpperCase(), - this.connection.schema.toUpperCase(), - ]); - - return tableStructure.map((column: any) => { - return { - allow_null: column.IS_NULLABLE === 'Y', - column_default: column.COLUMN_DEFAULT, - column_name: column.COLUMN_NAME, - data_type: column.DATA_TYPE, - character_maximum_length: column.CHARACTER_MAXIMUM_LENGTH, - data_type_params: null, - udt_name: null, - extra: null, - }; - }); - } - - public async testConnect(): Promise { - if (!this.connection.id) { - this.connection.id = nanoid(6); - } - const connectionToDb = await this.getConnectionToDatabase(); - const query = ` + const tableStructure = await connectionToDb.query(query, [ + tableName.toUpperCase(), + this.connection.schema.toUpperCase(), + ]); + + return tableStructure.map((column: any) => { + return { + allow_null: column.IS_NULLABLE === 'Y', + column_default: column.COLUMN_DEFAULT, + column_name: column.COLUMN_NAME, + data_type: column.DATA_TYPE, + character_maximum_length: column.CHARACTER_MAXIMUM_LENGTH, + data_type_params: null, + udt_name: null, + extra: null, + }; + }); + } + + public async testConnect(): Promise { + if (!this.connection.id) { + this.connection.id = nanoid(6); + } + const connectionToDb = await this.getConnectionToDatabase(); + const query = ` SELECT 1 FROM sysibm.sysdummy1 `; - try { - const testResult = await connectionToDb.query(query); - if (testResult?.[0] && testResult[0]['1'] === 1) { - return { - result: true, - message: 'Successfully connected', - }; - } - } catch (error) { - return { - result: false, - message: error.message, - }; - } finally { - LRUStorage.delImdbDb2Cache(this.connection); - } - return { - result: false, - message: 'Unknown error', - }; - } - - public async updateRowInTable( - tableName: string, - row: Record, - primaryKey: Record, - ): Promise> { - this.validateNamesAndThrowError([ - tableName, - this.connection.schema, - ...Object.keys(row), - ...Object.keys(primaryKey), - ]); - const connectionToDb = await this.getConnectionToDatabase(); - - const setClause = Object.keys(row) - .map((key) => `${key} = ?`) - .join(', '); - const whereClause = Object.keys(primaryKey) - .map((key) => `${key} = ?`) - .join(' AND '); - - const query = ` + try { + const testResult = await connectionToDb.query(query); + if (testResult?.[0] && testResult[0]['1'] === 1) { + return { + result: true, + message: 'Successfully connected', + }; + } + } catch (error) { + return { + result: false, + message: error.message, + }; + } finally { + LRUStorage.delImdbDb2Cache(this.connection); + } + return { + result: false, + message: 'Unknown error', + }; + } + + public async updateRowInTable( + tableName: string, + row: Record, + primaryKey: Record, + ): Promise> { + this.validateNamesAndThrowError([ + tableName, + this.connection.schema, + ...Object.keys(row), + ...Object.keys(primaryKey), + ]); + const connectionToDb = await this.getConnectionToDatabase(); + + const setClause = Object.keys(row) + .map((key) => `${key} = ?`) + .join(', '); + const whereClause = Object.keys(primaryKey) + .map((key) => `${key} = ?`) + .join(' AND '); + + const query = ` UPDATE ${this.connection.schema.toUpperCase()}.${tableName.toUpperCase()} SET ${setClause} WHERE ${whereClause} `; - const params = [...Object.values(row), ...Object.values(primaryKey)]; - await connectionToDb.query(query, params); + const params = [...Object.values(row), ...Object.values(primaryKey)]; + await connectionToDb.query(query, params); - const selectQuery = ` + const selectQuery = ` SELECT * FROM ${this.connection.schema.toUpperCase()}.${tableName.toUpperCase()} WHERE ${whereClause} `; - const result = await connectionToDb.query(selectQuery, Object.values(primaryKey)); - return result[0]; - } - - public async bulkUpdateRowsInTable( - tableName: string, - newValues: Record, - primaryKeys: Array>, - ): Promise>> { - await Promise.allSettled(primaryKeys.map((key) => this.updateRowInTable(tableName, newValues, key))); - return primaryKeys; - } - - public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { - await Promise.allSettled(primaryKeys.map((key) => this.deleteRowInTable(tableName, key))); - return primaryKeys.length; - } - - public async validateSettings(settings: ValidateTableSettingsDS, tableName: string): Promise { - const [tableStructure, primaryColumns] = await Promise.all([ - this.getTableStructure(tableName), - this.getTablePrimaryColumns(tableName), - ]); - return tableSettingsFieldValidator(tableStructure, primaryColumns, settings); - } - - public async getReferencedTableNamesAndColumns(tableName: string): Promise { - const primaryColumns = await this.getTablePrimaryColumns(tableName); - const connectionToDb = await this.getConnectionToDatabase(); - const results: Array = []; - for (const primaryColumn of primaryColumns) { - const query = ` + const result = await connectionToDb.query(selectQuery, Object.values(primaryKey)); + return result[0]; + } + + public async bulkUpdateRowsInTable( + tableName: string, + newValues: Record, + primaryKeys: Array>, + ): Promise>> { + await Promise.allSettled(primaryKeys.map((key) => this.updateRowInTable(tableName, newValues, key))); + return primaryKeys; + } + + public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { + await Promise.allSettled(primaryKeys.map((key) => this.deleteRowInTable(tableName, key))); + return primaryKeys.length; + } + + public async validateSettings(settings: ValidateTableSettingsDS, tableName: string): Promise { + const [tableStructure, primaryColumns] = await Promise.all([ + this.getTableStructure(tableName), + this.getTablePrimaryColumns(tableName), + ]); + return tableSettingsFieldValidator(tableStructure, primaryColumns, settings); + } + + public async getReferencedTableNamesAndColumns(tableName: string): Promise { + const primaryColumns = await this.getTablePrimaryColumns(tableName); + const connectionToDb = await this.getConnectionToDatabase(); + const results: Array = []; + for (const primaryColumn of primaryColumns) { + const query = ` SELECT ref.tabname AS referencing_table_name, col.colname AS referencing_column_name @@ -541,184 +579,184 @@ ORDER BY colname = ? ) `; - const foreignKeys = await connectionToDb.query(query, [ - tableName.toUpperCase(), - this.connection.schema.toUpperCase(), - this.connection.schema.toUpperCase(), - tableName.toUpperCase(), - primaryColumn.column_name.toUpperCase(), - ]); - results.push({ - referenced_on_column_name: primaryColumn.column_name, - referenced_by: foreignKeys.map((foreignKey: any) => { - return { - table_name: foreignKey.REFERENCING_TABLE_NAME, - column_name: foreignKey.REFERENCING_COLUMN_NAME, - }; - }), - }); - } - return results; - } - - public async isView(tableName: string): Promise { - const connectionToDb = await this.getConnectionToDatabase(); - const query = ` + const foreignKeys = await connectionToDb.query(query, [ + tableName.toUpperCase(), + this.connection.schema.toUpperCase(), + this.connection.schema.toUpperCase(), + tableName.toUpperCase(), + primaryColumn.column_name.toUpperCase(), + ]); + results.push({ + referenced_on_column_name: primaryColumn.column_name, + referenced_by: foreignKeys.map((foreignKey: any) => { + return { + table_name: foreignKey.REFERENCING_TABLE_NAME, + column_name: foreignKey.REFERENCING_COLUMN_NAME, + }; + }), + }); + } + return results; + } + + public async isView(tableName: string): Promise { + const connectionToDb = await this.getConnectionToDatabase(); + const query = ` SELECT TYPE AS table_type FROM SYSCAT.TABLES WHERE TABSCHEMA = ? AND TABNAME = ? `; - const tableData = await connectionToDb.query(query, [ - this.connection.schema.toUpperCase(), - tableName.toUpperCase(), - ]); - return tableData[0].TABLE_TYPE === 'V'; - } - - public async getTableRowsStream( - tableName: string, - settings: TableSettingsDS, - page: number, - perPage: number, - searchedFieldValue: string, - filteringFields: FilteringFieldsDS[], - ): Promise> { - const { large_dataset } = await this.getRowsCount(tableName, this.connection.schema); - if (large_dataset) { - throw new Error(ERROR_MESSAGES.DATA_IS_TO_LARGE); - } - const rowsResult = (await this.getRowsFromTable( - tableName, - settings, - page, - perPage, - searchedFieldValue, - filteringFields, - null, - null, - )) as any; - return rowsResult.data; - } - - public async getRowsCount( - tableName: string, - tableSchema: string, - ): Promise<{ rowsCount: number; large_dataset: boolean }> { - const connectionToDb = await this.getConnectionToDatabase(); - const fastCount = await this.getFastRowsCount(tableName, tableSchema); - if (fastCount >= DAO_CONSTANTS.LARGE_DATASET_ROW_LIMIT) { - return { rowsCount: fastCount, large_dataset: true }; - } - const countQuery = ` + const tableData = await connectionToDb.query(query, [ + this.connection.schema.toUpperCase(), + tableName.toUpperCase(), + ]); + return tableData[0].TABLE_TYPE === 'V'; + } + + public async getTableRowsStream( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: FilteringFieldsDS[], + ): Promise> { + const { large_dataset } = await this.getRowsCount(tableName, this.connection.schema); + if (large_dataset) { + throw new Error(ERROR_MESSAGES.DATA_IS_TO_LARGE); + } + const rowsResult = (await this.getRowsFromTable( + tableName, + settings, + page, + perPage, + searchedFieldValue, + filteringFields, + null, + null, + )) as any; + return rowsResult.data; + } + + public async getRowsCount( + tableName: string, + tableSchema: string, + ): Promise<{ rowsCount: number; large_dataset: boolean }> { + const connectionToDb = await this.getConnectionToDatabase(); + const fastCount = await this.getFastRowsCount(tableName, tableSchema); + if (fastCount >= DAO_CONSTANTS.LARGE_DATASET_ROW_LIMIT) { + return { rowsCount: fastCount, large_dataset: true }; + } + const countQuery = ` SELECT COUNT(*) FROM ${tableSchema}.${tableName} `; - const countResult = await connectionToDb.query(countQuery); - const rowsCount = parseInt(countResult[0]['1'], 10); - return { rowsCount: rowsCount, large_dataset: false }; - } - - public async getFastRowsCount(tableName: string, tableSchema: string): Promise { - const connectionToDb = await this.getConnectionToDatabase(); - const fastCountQuery = ` + const countResult = await connectionToDb.query(countQuery); + const rowsCount = parseInt(countResult[0]['1'], 10); + return { rowsCount: rowsCount, large_dataset: false }; + } + + public async getFastRowsCount(tableName: string, tableSchema: string): Promise { + const connectionToDb = await this.getConnectionToDatabase(); + const fastCountQuery = ` SELECT CARD FROM SYSIBM.SYSTABLES WHERE NAME = ? AND CREATOR = ? `; - const fastCountParams = [tableName, tableSchema]; - const fastCountQueryResult = await connectionToDb.query(fastCountQuery, fastCountParams); - const fastCount = fastCountQueryResult[0].CARD; - return fastCount; - } - - public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { - const stream = new Readable(); - stream.push(file.buffer); - stream.push(null); - - const parser = stream.pipe(csv.parse({ columns: true })); - const results: any[] = []; - for await (const record of parser) { - results.push(record); - } - await Promise.allSettled( - results.map(async (row) => { - return await this.addRowInTable(tableName, row); - }), - ); - } - - public async executeRawQuery(query: string): Promise>> { - const connectionToDb = await this.getConnectionToDatabase(); - const result = await connectionToDb.query(query); - return result; - } - - private async getConnectionToDatabase(): Promise { - if (this.connection.ssh) { - return this.createTunneledConnection(this.connection); - } - return this.getUsualConnection(); - } - - private async getUsualConnection(withCache = true): Promise { - const cachedDatabase = LRUStorage.getImdbDb2Cache(this.connection); - if (withCache && cachedDatabase && cachedDatabase.connected) { - return cachedDatabase; - } - let connStr = `DATABASE=${this.connection.database};HOSTNAME=${this.connection.host};UID=${this.connection.username};PWD=${this.connection.password};PORT=${this.connection.port};PROTOCOL=TCPIP`; - if (this.connection.ssl) { - connStr += ';SECURITY=SSL'; - if (this.connection.cert) { - connStr += `;SSLServerCertificate=${this.connection.cert}`; - } - } - const connectionPool = new Pool(); - const databaseConnection = await connectionPool.open(connStr); - LRUStorage.setImdbDb2Cache(this.connection, databaseConnection); - return databaseConnection; - } - - private async createTunneledConnection(connection: ConnectionParams): Promise { - const connectionCopy = { ...connection }; - return new Promise(async (resolve, reject): Promise => { - const cachedTnl = LRUStorage.getTunnelCache(connectionCopy); - if (cachedTnl?.database && cachedTnl.server && cachedTnl.client && cachedTnl.database.connected) { - resolve(cachedTnl.database); - return; - } - const freePort = await getPort(); - try { - const [server, client] = await getTunnel(connectionCopy, freePort); - connection.host = '127.0.0.1'; - connection.port = freePort; - const database = await this.getUsualConnection(false); - const tnlCachedObj = { - server: server, - client: client, - database: database, - }; - LRUStorage.setTunnelCache(connectionCopy, tnlCachedObj); - resolve(tnlCachedObj.database); - - client.on('error', (e) => { - LRUStorage.delTunnelCache(connectionCopy); - reject(e); - return; - }); - - server.on('error', (e) => { - LRUStorage.delTunnelCache(connectionCopy); - reject(e); - return; - }); - return; - } catch (error) { - LRUStorage.delTunnelCache(connectionCopy); - reject(error); - return; - } - }); - } + const fastCountParams = [tableName, tableSchema]; + const fastCountQueryResult = await connectionToDb.query(fastCountQuery, fastCountParams); + const fastCount = fastCountQueryResult[0].CARD; + return fastCount; + } + + public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { + const stream = new Readable(); + stream.push(file.buffer); + stream.push(null); + + const parser = stream.pipe(csv.parse({ columns: true })); + const results: any[] = []; + for await (const record of parser) { + results.push(record); + } + await Promise.allSettled( + results.map(async (row) => { + return await this.addRowInTable(tableName, row); + }), + ); + } + + public async executeRawQuery(query: string): Promise>> { + const connectionToDb = await this.getConnectionToDatabase(); + const result = await connectionToDb.query(query); + return result; + } + + private async getConnectionToDatabase(): Promise { + if (this.connection.ssh) { + return this.createTunneledConnection(this.connection); + } + return this.getUsualConnection(); + } + + private async getUsualConnection(withCache = true): Promise { + const cachedDatabase = LRUStorage.getImdbDb2Cache(this.connection); + if (withCache && cachedDatabase && cachedDatabase.connected) { + return cachedDatabase; + } + let connStr = `DATABASE=${this.connection.database};HOSTNAME=${this.connection.host};UID=${this.connection.username};PWD=${this.connection.password};PORT=${this.connection.port};PROTOCOL=TCPIP`; + if (this.connection.ssl) { + connStr += ';SECURITY=SSL'; + if (this.connection.cert) { + connStr += `;SSLServerCertificate=${this.connection.cert}`; + } + } + const connectionPool = new Pool(); + const databaseConnection = await connectionPool.open(connStr); + LRUStorage.setImdbDb2Cache(this.connection, databaseConnection); + return databaseConnection; + } + + private async createTunneledConnection(connection: ConnectionParams): Promise { + const connectionCopy = { ...connection }; + return new Promise(async (resolve, reject): Promise => { + const cachedTnl = LRUStorage.getTunnelCache(connectionCopy); + if (cachedTnl?.database && cachedTnl.server && cachedTnl.client && cachedTnl.database.connected) { + resolve(cachedTnl.database); + return; + } + const freePort = await getPort(); + try { + const [server, client] = await getTunnel(connectionCopy, freePort); + connection.host = '127.0.0.1'; + connection.port = freePort; + const database = await this.getUsualConnection(false); + const tnlCachedObj = { + server: server, + client: client, + database: database, + }; + LRUStorage.setTunnelCache(connectionCopy, tnlCachedObj); + resolve(tnlCachedObj.database); + + client.on('error', (e) => { + LRUStorage.delTunnelCache(connectionCopy); + reject(e); + return; + }); + + server.on('error', (e) => { + LRUStorage.delTunnelCache(connectionCopy); + reject(e); + return; + }); + return; + } catch (error) { + LRUStorage.delTunnelCache(connectionCopy); + reject(error); + return; + } + }); + } } diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts index 9b5b43bde..4cb934713 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-oracle.ts @@ -7,10 +7,10 @@ import { LRUStorage } from '../../caching/lru-storage.js'; import { DAO_CONSTANTS } from '../../helpers/data-access-objects-constants.js'; import { ERROR_MESSAGES } from '../../helpers/errors/error-messages.js'; import { - isOracleDateOrTimeType, - isOracleDateStringByRegexp, - isOracleDateType, - isOracleTimeType, + isOracleDateOrTimeType, + isOracleDateStringByRegexp, + isOracleDateType, + isOracleTimeType, } from '../../helpers/is-database-date.js'; import { objectKeysToLowercase } from '../../helpers/object-kyes-to-lowercase.js'; import { renameObjectKeyName } from '../../helpers/rename-object-keyname.js'; @@ -31,379 +31,389 @@ import { IDataAccessObject } from '../../shared/interfaces/data-access-object.in import { BasicDataAccessObject } from './basic-data-access-object.js'; type RefererencedConstraint = { - TABLE_NAME: string; - CONSTRAINT_NAME: string; - TABLE_NAME_ON: string; - COLUMN_NAME_ON: string; + TABLE_NAME: string; + CONSTRAINT_NAME: string; + TABLE_NAME_ON: string; + COLUMN_NAME_ON: string; }; export class DataAccessObjectOracle extends BasicDataAccessObject implements IDataAccessObject { - - public async addRowInTable( - tableName: string, - row: Record, - ): Promise> { - const knex = await this.configureKnex(); - - const [tableStructure, primaryColumns] = await Promise.all([ - this.getTableStructure(tableName), - this.getTablePrimaryColumns(tableName), - ]); - - const primaryKeys: Array = primaryColumns.map(({ column_name }) => column_name); - - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - - const timestampColumnNames = tableStructure - .filter(({ data_type }) => isOracleTimeType(data_type)) - .map(({ column_name }) => column_name); - - const dateColumnNames = tableStructure - .filter(({ data_type }) => isOracleDateType(data_type)) - .map(({ column_name }) => column_name); - - Object.keys(row).forEach((key) => { - if (timestampColumnNames.includes(key) && row[key]) { - row[key] = this.formatTimestamp(row[key] as string); - } - if (dateColumnNames.includes(key) && row[key]) { - row[key] = this.formatDate(new Date(row[key] as string)); - } - }); - - const insertResult = await knex(tableName).withSchema(schema).returning(primaryKeys).insert(row); - const responseObject = {}; - primaryKeys.forEach((key) => { - responseObject[key] = insertResult[0][key]; - }); - return responseObject; - } - - public async deleteRowInTable( - tableName: string, - primaryKey: Record, - ): Promise> { - try { - const knex = await this.configureKnex(); - return knex(tableName) - .withSchema(this.connection.schema ?? this.connection.username.toUpperCase()) - .where(primaryKey) - .del(); - } catch (error) { - console.error('Error deleting row in table: %s', tableName, error); - throw error; - } - } - - public async getIdentityColumns( - tableName: string, - referencedFieldName: string, - identityColumnName: string, - fieldValues: (string | number)[], - ): Promise>> { - const knex = await this.configureKnex(); - tableName = this.attachSchemaNameToTableName(tableName); - const columnsForSelect = [referencedFieldName]; - if (identityColumnName) { - columnsForSelect.push(identityColumnName); - } - return await knex.transaction((trx) => { - knex - .raw( - `SELECT ${columnsForSelect.map((_) => '??').join(', ')} + public async addRowInTable( + tableName: string, + row: Record, + ): Promise> { + const knex = await this.configureKnex(); + + const [tableStructure, primaryColumns] = await Promise.all([ + this.getTableStructure(tableName), + this.getTablePrimaryColumns(tableName), + ]); + + const primaryKeys: Array = primaryColumns.map(({ column_name }) => column_name); + + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + + const timestampColumnNames = tableStructure + .filter(({ data_type }) => isOracleTimeType(data_type)) + .map(({ column_name }) => column_name); + + const dateColumnNames = tableStructure + .filter(({ data_type }) => isOracleDateType(data_type)) + .map(({ column_name }) => column_name); + + Object.keys(row).forEach((key) => { + if (timestampColumnNames.includes(key) && row[key]) { + row[key] = this.formatTimestamp(row[key] as string); + } + if (dateColumnNames.includes(key) && row[key]) { + row[key] = this.formatDate(new Date(row[key] as string)); + } + }); + + const insertResult = await knex(tableName).withSchema(schema).returning(primaryKeys).insert(row); + const responseObject = {}; + primaryKeys.forEach((key) => { + responseObject[key] = insertResult[0][key]; + }); + return responseObject; + } + + public async deleteRowInTable( + tableName: string, + primaryKey: Record, + ): Promise> { + try { + const knex = await this.configureKnex(); + return knex(tableName) + .withSchema(this.connection.schema ?? this.connection.username.toUpperCase()) + .where(primaryKey) + .del(); + } catch (error) { + console.error('Error deleting row in table: %s', tableName, error); + throw error; + } + } + + public async getIdentityColumns( + tableName: string, + referencedFieldName: string, + identityColumnName: string, + fieldValues: (string | number)[], + ): Promise>> { + const knex = await this.configureKnex(); + tableName = this.attachSchemaNameToTableName(tableName); + const columnsForSelect = [referencedFieldName]; + if (identityColumnName) { + columnsForSelect.push(identityColumnName); + } + return await knex.transaction((trx) => { + knex + .raw( + `SELECT ${columnsForSelect.map((_) => '??').join(', ')} FROM ${tableName} WHERE ?? IN (${fieldValues.map((_) => '?').join(', ')})`, - [...columnsForSelect, referencedFieldName, ...fieldValues], - ) - .transacting(trx) - .then(trx.commit) - .catch(trx.rollback); - }); - } - - public async getRowByPrimaryKey( - tableName: string, - primaryKey: Record, - tableSettings: TableSettingsDS, - ): Promise> { - try { - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - const knex = await this.configureKnex(); - let query = knex(tableName).withSchema(schema).where(primaryKey); - - if (tableSettings) { - const tableStructure = await this.getTableStructure(tableName); - const availableFields = this.findAvailableFields(tableSettings, tableStructure); - query = query.select(availableFields); - } - - const result = await query; - return result[0] as unknown as Record; - } catch (error) { - console.error(`Error getting row by primary key from table ${tableName}:`, error); - throw error; - } - } - - public async bulkGetRowsFromTableByPrimaryKeys( - tableName: string, - primaryKeys: Array>, - settings: TableSettingsDS, - ): Promise>> { - const knex = await this.configureKnex(); - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - let availableFields: string[] = []; - - if (settings) { - const tableStructure = await this.getTableStructure(tableName); - availableFields = this.findAvailableFields(settings, tableStructure); - } - - const query = knex(tableName) - .withSchema(schema) - .select(availableFields.length ? availableFields : '*'); - - primaryKeys.forEach((primaryKey) => { - query.orWhere((builder) => { - Object.entries(primaryKey).forEach(([column, value]) => { - builder.andWhere(column, value); - }); - }); - }); - - const results = await query; - return results as Array>; - } - - public async getRowsFromTable( - tableName: string, - settings: TableSettingsDS, - page: number, - perPage: number, - searchedFieldValue: string, - filteringFields: FilteringFieldsDS[], - autocompleteFields: AutocompleteFieldsDS, - tableStructure: TableStructureDS[] | null, - ): Promise { - const knex = await this.configureKnex(); - - if (autocompleteFields?.value && autocompleteFields?.fields?.length > 0) { - const andWhere = autocompleteFields.fields - .map((_, i) => `${i === 0 ? ' WHERE' : ' OR'} CAST (?? AS VARCHAR (255)) LIKE '${autocompleteFields.value}%'`) - .join(''); - - tableName = this.attachSchemaNameToTableName(tableName); - - const rows = await knex.transaction(async (trx) => { - try { - const result = await knex - .raw( - `SELECT ${autocompleteFields.fields.map((_) => '??').join(', ')} + [...columnsForSelect, referencedFieldName, ...fieldValues], + ) + .transacting(trx) + .then(trx.commit) + .catch(trx.rollback); + }); + } + + public async getRowByPrimaryKey( + tableName: string, + primaryKey: Record, + tableSettings: TableSettingsDS, + ): Promise> { + try { + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + const knex = await this.configureKnex(); + let query = knex(tableName).withSchema(schema).where(primaryKey); + + if (tableSettings) { + const tableStructure = await this.getTableStructure(tableName); + const availableFields = this.findAvailableFields(tableSettings, tableStructure); + query = query.select(availableFields); + } + + const result = await query; + return result[0] as unknown as Record; + } catch (error) { + console.error(`Error getting row by primary key from table ${tableName}:`, error); + throw error; + } + } + + public async bulkGetRowsFromTableByPrimaryKeys( + tableName: string, + primaryKeys: Array>, + settings: TableSettingsDS, + ): Promise>> { + const knex = await this.configureKnex(); + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + let availableFields: string[] = []; + + if (settings) { + const tableStructure = await this.getTableStructure(tableName); + availableFields = this.findAvailableFields(settings, tableStructure); + } + + const query = knex(tableName) + .withSchema(schema) + .select(availableFields.length ? availableFields : '*'); + + primaryKeys.forEach((primaryKey) => { + query.orWhere((builder) => { + Object.entries(primaryKey).forEach(([column, value]) => { + builder.andWhere(column, value); + }); + }); + }); + + const results = await query; + return results as Array>; + } + + public async getRowsFromTable( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: FilteringFieldsDS[], + autocompleteFields: AutocompleteFieldsDS, + tableStructure: TableStructureDS[] | null, + ): Promise { + const knex = await this.configureKnex(); + + if (autocompleteFields?.value && autocompleteFields?.fields?.length > 0) { + if (!tableStructure) { + tableStructure = await this.getTableStructure(tableName); + } + const validFieldNames = tableStructure.map((col) => col.column_name); + const validatedAutocompleteFields = autocompleteFields.fields.filter((field) => validFieldNames.includes(field)); + if (validatedAutocompleteFields.length === 0) { + throw new Error('Invalid autocomplete fields provided'); + } + + const andWhere = validatedAutocompleteFields + .map((_, i) => `${i === 0 ? ' WHERE' : ' OR'} CAST (?? AS VARCHAR (255)) LIKE ?`) + .join(''); + + const likeParams = validatedAutocompleteFields.map(() => `${autocompleteFields.value}%`); + + tableName = this.attachSchemaNameToTableName(tableName); + + const rows = await knex.transaction(async (trx) => { + try { + const result = await knex + .raw( + `SELECT ${validatedAutocompleteFields.map((_) => '??').join(', ')} FROM ${tableName} ${andWhere} FETCH FIRST ${DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT} ROWS ONLY`, - [...autocompleteFields.fields, ...autocompleteFields.fields], - ) - .transacting(trx); - trx.commit(); - return result; - } catch (error) { - trx.rollback(); - throw error; - } - }); - - return { - data: rows as Array>, - pagination: {} as any, - large_dataset: false, - }; - } - - page = page > 0 ? page : DAO_CONSTANTS.DEFAULT_PAGINATION.page; - perPage = - perPage > 0 - ? perPage - : settings.list_per_page > 0 - ? settings.list_per_page - : DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; - - if (!tableStructure) { - tableStructure = await this.getTableStructure(tableName); - } - const availableFields = this.findAvailableFields(settings, tableStructure); - const timestampColumnNames = tableStructure - .filter(({ data_type }) => isOracleTimeType(data_type)) - .map(({ column_name }) => column_name); - - const datesColumnsNames = tableStructure - .filter(({ data_type }) => isOracleDateType(data_type)) - .map(({ column_name }) => column_name); - - const searchedFields = - settings?.search_fields?.length > 0 ? settings.search_fields : searchedFieldValue ? availableFields : []; - - const tableSchema = this.connection.schema || this.connection.username.toUpperCase(); - return await this.getAvailableFieldsWithPagination( - knex, - tableName, - tableSchema, - page, - perPage, - availableFields, - searchedFields, - searchedFieldValue, - filteringFields, - settings, - timestampColumnNames, - datesColumnsNames, - ); - } - - public async getAvailableFieldsWithPagination( - knex: Knex, - tableName: string, - tableSchema: string, - page: number, - perPage: number, - availableFields: Array, - searchedFields: Array, - searchedFieldValue: any, - filteringFields: FilteringFieldsDS[], - settings: TableSettingsDS, - timestampColumnNames: Array, - datesColumnsNames: Array, - ) { - const offset = (page - 1) * perPage; - const { rowsCount: fastCount } = await this.getRowsCount(knex, tableName, tableSchema); - const applySearchFields = (builder: Knex.QueryBuilder) => { - if (searchedFieldValue && searchedFields.length > 0) { - for (const field of searchedFields) { - if (Buffer.isBuffer(searchedFieldValue)) { - builder.orWhere(field, '=', searchedFieldValue); - } else { - if (fastCount <= 1000) { - builder.orWhereRaw(` Lower(??) LIKE ?`, [field, `%${searchedFieldValue.toLowerCase()}%`]); - } else { - builder.orWhereRaw(` Lower(??) LIKE ?`, [field, `${searchedFieldValue.toLowerCase()}%`]); - } - } - } - } - }; - - const applyFilteringFields = ( - builder: Knex.QueryBuilder, - timestampColumnNames: Array, - datesColumnsNames: Array, - ) => { - if (filteringFields && filteringFields.length > 0) { - // eslint-disable-next-line prefer-const - for (let { field, criteria, value } of filteringFields) { - if (datesColumnsNames.includes(field) && value) { - const valueToDate = new Date(String(value)); - value = this.formatDate(valueToDate); - } - if (timestampColumnNames.includes(field) && value) { - value = this.formatTimestamp(String(value)); - } - const operators = { - [FilterCriteriaEnum.eq]: '=', - [FilterCriteriaEnum.startswith]: 'like', - [FilterCriteriaEnum.endswith]: 'like', - [FilterCriteriaEnum.gt]: '>', - [FilterCriteriaEnum.lt]: '<', - [FilterCriteriaEnum.lte]: '<=', - [FilterCriteriaEnum.gte]: '>=', - [FilterCriteriaEnum.contains]: 'like', - [FilterCriteriaEnum.icontains]: 'not like', - [FilterCriteriaEnum.empty]: 'is', - }; - const values = { - [FilterCriteriaEnum.startswith]: `${value}%`, - [FilterCriteriaEnum.endswith]: `%${value}`, - [FilterCriteriaEnum.contains]: `%${value}%`, - [FilterCriteriaEnum.icontains]: `%${value}%`, - [FilterCriteriaEnum.empty]: null, - }; - builder.where(field, operators[criteria], values[criteria] || value); - } - } - }; - - const applyOrdering = (builder: Knex.QueryBuilder) => { - if (settings.ordering_field && settings.ordering) { - builder.orderBy(settings.ordering_field, settings.ordering); - } - }; - - const rows = await knex(tableName) - .withSchema(tableSchema) - .select(availableFields) - .modify(applySearchFields) - .modify((builder) => applyFilteringFields(builder, timestampColumnNames, datesColumnsNames)) - .modify(applyOrdering) - .limit(perPage) - .offset(offset); - - const { rowsCount, large_dataset } = await this.getRowsCount(knex, tableName, tableSchema); - const lastPage = Math.ceil(rowsCount / perPage); - - return { - data: rows.map((row) => { - delete row.ROWNUM_; - return row; - }), - pagination: { - total: rowsCount, - lastPage: lastPage, - perPage: perPage, - currentPage: page, - }, - large_dataset: large_dataset, - }; - } - - public async getRowsCount(knex: Knex, tableName: string, tableSchema: string) { - const fastCount = await this.getFastRowsCount(knex, tableName, tableSchema); - if (fastCount && fastCount >= DAO_CONSTANTS.LARGE_DATASET_ROW_LIMIT) { - return { rowsCount: fastCount, large_dataset: true }; - } - - const rowsCount = await this.slowCountWithTimeOut(knex, tableName, tableSchema); - return { rowsCount: rowsCount, large_dataset: false }; - } - - public async getFastRowsCount( - knex: Knex, - tableName: string, - tableSchema: string, - ): Promise { - const fastCountQueryResult = await knex('ALL_TABLES') - .select('NUM_ROWS') - .where('TABLE_NAME', '=', tableName) - .andWhere('OWNER', '=', tableSchema); - if (!fastCountQueryResult[0]) { - return null; - } - const fastCount = fastCountQueryResult[0].NUM_ROWS; - return fastCount; - } - - public async slowCountWithTimeOut(knex: Knex, tableName: string, tableSchema: string) { - const count = (await knex(tableName).withSchema(tableSchema).count('*')) as any; - const rowsCount = parseInt(count[0]['COUNT(*)'], 10); - return rowsCount; - } - - public async getTableForeignKeys(tableName: string): Promise { - const cachedForeignKeys = LRUStorage.getTableForeignKeysCache(this.connection, tableName); - if (cachedForeignKeys) { - return cachedForeignKeys; - } - - const knex = await this.configureKnex(); - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - - const foreignKeysQuery = ` + [...validatedAutocompleteFields, ...validatedAutocompleteFields, ...likeParams], + ) + .transacting(trx); + trx.commit(); + return result; + } catch (error) { + trx.rollback(); + throw error; + } + }); + + return { + data: rows as Array>, + pagination: {} as any, + large_dataset: false, + }; + } + + page = page > 0 ? page : DAO_CONSTANTS.DEFAULT_PAGINATION.page; + perPage = + perPage > 0 + ? perPage + : settings.list_per_page > 0 + ? settings.list_per_page + : DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; + + if (!tableStructure) { + tableStructure = await this.getTableStructure(tableName); + } + const availableFields = this.findAvailableFields(settings, tableStructure); + const timestampColumnNames = tableStructure + .filter(({ data_type }) => isOracleTimeType(data_type)) + .map(({ column_name }) => column_name); + + const datesColumnsNames = tableStructure + .filter(({ data_type }) => isOracleDateType(data_type)) + .map(({ column_name }) => column_name); + + const searchedFields = + settings?.search_fields?.length > 0 ? settings.search_fields : searchedFieldValue ? availableFields : []; + + const tableSchema = this.connection.schema || this.connection.username.toUpperCase(); + return await this.getAvailableFieldsWithPagination( + knex, + tableName, + tableSchema, + page, + perPage, + availableFields, + searchedFields, + searchedFieldValue, + filteringFields, + settings, + timestampColumnNames, + datesColumnsNames, + ); + } + + public async getAvailableFieldsWithPagination( + knex: Knex, + tableName: string, + tableSchema: string, + page: number, + perPage: number, + availableFields: Array, + searchedFields: Array, + searchedFieldValue: any, + filteringFields: FilteringFieldsDS[], + settings: TableSettingsDS, + timestampColumnNames: Array, + datesColumnsNames: Array, + ) { + const offset = (page - 1) * perPage; + const { rowsCount: fastCount } = await this.getRowsCount(knex, tableName, tableSchema); + const applySearchFields = (builder: Knex.QueryBuilder) => { + if (searchedFieldValue && searchedFields.length > 0) { + for (const field of searchedFields) { + if (Buffer.isBuffer(searchedFieldValue)) { + builder.orWhere(field, '=', searchedFieldValue); + } else { + if (fastCount <= 1000) { + builder.orWhereRaw(` Lower(??) LIKE ?`, [field, `%${searchedFieldValue.toLowerCase()}%`]); + } else { + builder.orWhereRaw(` Lower(??) LIKE ?`, [field, `${searchedFieldValue.toLowerCase()}%`]); + } + } + } + } + }; + + const applyFilteringFields = ( + builder: Knex.QueryBuilder, + timestampColumnNames: Array, + datesColumnsNames: Array, + ) => { + if (filteringFields && filteringFields.length > 0) { + // eslint-disable-next-line prefer-const + for (let { field, criteria, value } of filteringFields) { + if (datesColumnsNames.includes(field) && value) { + const valueToDate = new Date(String(value)); + value = this.formatDate(valueToDate); + } + if (timestampColumnNames.includes(field) && value) { + value = this.formatTimestamp(String(value)); + } + const operators = { + [FilterCriteriaEnum.eq]: '=', + [FilterCriteriaEnum.startswith]: 'like', + [FilterCriteriaEnum.endswith]: 'like', + [FilterCriteriaEnum.gt]: '>', + [FilterCriteriaEnum.lt]: '<', + [FilterCriteriaEnum.lte]: '<=', + [FilterCriteriaEnum.gte]: '>=', + [FilterCriteriaEnum.contains]: 'like', + [FilterCriteriaEnum.icontains]: 'not like', + [FilterCriteriaEnum.empty]: 'is', + }; + const values = { + [FilterCriteriaEnum.startswith]: `${value}%`, + [FilterCriteriaEnum.endswith]: `%${value}`, + [FilterCriteriaEnum.contains]: `%${value}%`, + [FilterCriteriaEnum.icontains]: `%${value}%`, + [FilterCriteriaEnum.empty]: null, + }; + builder.where(field, operators[criteria], values[criteria] || value); + } + } + }; + + const applyOrdering = (builder: Knex.QueryBuilder) => { + if (settings.ordering_field && settings.ordering) { + builder.orderBy(settings.ordering_field, settings.ordering); + } + }; + + const rows = await knex(tableName) + .withSchema(tableSchema) + .select(availableFields) + .modify(applySearchFields) + .modify((builder) => applyFilteringFields(builder, timestampColumnNames, datesColumnsNames)) + .modify(applyOrdering) + .limit(perPage) + .offset(offset); + + const { rowsCount, large_dataset } = await this.getRowsCount(knex, tableName, tableSchema); + const lastPage = Math.ceil(rowsCount / perPage); + + return { + data: rows.map((row) => { + delete row.ROWNUM_; + return row; + }), + pagination: { + total: rowsCount, + lastPage: lastPage, + perPage: perPage, + currentPage: page, + }, + large_dataset: large_dataset, + }; + } + + public async getRowsCount(knex: Knex, tableName: string, tableSchema: string) { + const fastCount = await this.getFastRowsCount(knex, tableName, tableSchema); + if (fastCount && fastCount >= DAO_CONSTANTS.LARGE_DATASET_ROW_LIMIT) { + return { rowsCount: fastCount, large_dataset: true }; + } + + const rowsCount = await this.slowCountWithTimeOut(knex, tableName, tableSchema); + return { rowsCount: rowsCount, large_dataset: false }; + } + + public async getFastRowsCount( + knex: Knex, + tableName: string, + tableSchema: string, + ): Promise { + const fastCountQueryResult = await knex('ALL_TABLES') + .select('NUM_ROWS') + .where('TABLE_NAME', '=', tableName) + .andWhere('OWNER', '=', tableSchema); + if (!fastCountQueryResult[0]) { + return null; + } + const fastCount = fastCountQueryResult[0].NUM_ROWS; + return fastCount; + } + + public async slowCountWithTimeOut(knex: Knex, tableName: string, tableSchema: string) { + const count = (await knex(tableName).withSchema(tableSchema).count('*')) as any; + const rowsCount = parseInt(count[0]['COUNT(*)'], 10); + return rowsCount; + } + + public async getTableForeignKeys(tableName: string): Promise { + const cachedForeignKeys = LRUStorage.getTableForeignKeysCache(this.connection, tableName); + if (cachedForeignKeys) { + return cachedForeignKeys; + } + + const knex = await this.configureKnex(); + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + + const foreignKeysQuery = ` SELECT a.constraint_name, a.table_name, a.column_name, @@ -422,29 +432,29 @@ export class DataAccessObjectOracle extends BasicDataAccessObject implements IDa AND a.OWNER = ? `; - const foreignKeys = await knex.raw(foreignKeysQuery, [tableName, schema]); + const foreignKeys = await knex.raw(foreignKeysQuery, [tableName, schema]); - const resultKeys = foreignKeys.map(({ R_COLUMN_NAME, R_TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME }: any) => ({ - referenced_column_name: R_COLUMN_NAME, - referenced_table_name: R_TABLE_NAME, - constraint_name: CONSTRAINT_NAME, - column_name: COLUMN_NAME, - })) as ForeignKeyDS[]; + const resultKeys = foreignKeys.map(({ R_COLUMN_NAME, R_TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME }: any) => ({ + referenced_column_name: R_COLUMN_NAME, + referenced_table_name: R_TABLE_NAME, + constraint_name: CONSTRAINT_NAME, + column_name: COLUMN_NAME, + })) as ForeignKeyDS[]; - LRUStorage.setTableForeignKeysCache(this.connection, tableName, resultKeys); + LRUStorage.setTableForeignKeysCache(this.connection, tableName, resultKeys); - return resultKeys; - } + return resultKeys; + } - public async getTablePrimaryColumns(tableName: string): Promise { - const cachedPrimaryColumns = LRUStorage.getTablePrimaryKeysCache(this.connection, tableName); - if (cachedPrimaryColumns) { - return cachedPrimaryColumns; - } - const knex = await this.configureKnex(); - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + public async getTablePrimaryColumns(tableName: string): Promise { + const cachedPrimaryColumns = LRUStorage.getTablePrimaryKeysCache(this.connection, tableName); + if (cachedPrimaryColumns) { + return cachedPrimaryColumns; + } + const knex = await this.configureKnex(); + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - const primaryColumnsQuery = ` + const primaryColumnsQuery = ` cols.table_name = ? AND cols.owner = ? AND cons.constraint_type = 'P' @@ -452,202 +462,202 @@ export class DataAccessObjectOracle extends BasicDataAccessObject implements IDa AND cons.owner = cols.owner `; - const primaryColumns = await knex(tableName) - .select(knex.raw('cols.column_name')) - .from(knex.raw('all_constraints cons, all_cons_columns cols')) - .where(knex.raw(primaryColumnsQuery, [tableName, schema])); + const primaryColumns = await knex(tableName) + .select(knex.raw('cols.column_name')) + .from(knex.raw('all_constraints cons, all_cons_columns cols')) + .where(knex.raw(primaryColumnsQuery, [tableName, schema])); - const primaryColumnsInLowercase = primaryColumns.map((column) => { - return objectKeysToLowercase(column); - }); + const primaryColumnsInLowercase = primaryColumns.map((column) => { + return objectKeysToLowercase(column); + }); - LRUStorage.setTablePrimaryKeysCache(this.connection, tableName, primaryColumnsInLowercase as PrimaryKeyDS[]); - return primaryColumnsInLowercase as PrimaryKeyDS[]; - } + LRUStorage.setTablePrimaryKeysCache(this.connection, tableName, primaryColumnsInLowercase as PrimaryKeyDS[]); + return primaryColumnsInLowercase as PrimaryKeyDS[]; + } - public async getTablesFromDB(): Promise { - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - const knex = await this.configureKnex(); - const query = ` + public async getTablesFromDB(): Promise { + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + const knex = await this.configureKnex(); + const query = ` SELECT object_name, object_type FROM all_objects WHERE owner = ? AND object_type IN ('TABLE', 'VIEW') `; - const result = await knex.raw<{ OBJECT_NAME: string; OBJECT_TYPE: string }[]>(query, [schema]); - return result.map((row) => { - return { - tableName: row.OBJECT_NAME, - isView: row.OBJECT_TYPE === 'VIEW', - }; - }); - } - - public async getTableStructure(tableName: string): Promise { - const cachedTableStructure = LRUStorage.getTableStructureCache(this.connection, tableName); - if (cachedTableStructure) { - return cachedTableStructure; - } - const knex = await this.configureKnex(); - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - const structureColumns = await knex - .queryBuilder() - .select('COLUMN_NAME', 'DATA_DEFAULT', 'DATA_TYPE', 'NULLABLE', 'DATA_LENGTH') - .from('ALL_TAB_COLUMNS') - .orderBy('COLUMN_ID') - .where(`TABLE_NAME`, `${tableName}`) - .andWhere(`OWNER`, `${schema}`); - const resultColumns = structureColumns.map((column) => { - renameObjectKeyName(column, 'DATA_DEFAULT', 'column_default'); - column.NULLABLE = column.NULLABLE === 'Y'; - renameObjectKeyName(column, 'NULLABLE', 'allow_null'); - renameObjectKeyName(column, 'DATA_LENGTH', 'character_maximum_length'); - if (typeof column.column_default === 'string' && column.column_default.includes('SYS_GUID()')) { - column.extra = 'auto_increment'; - } - return objectKeysToLowercase(column); - }) as TableStructureDS[]; - LRUStorage.setTableStructureCache(this.connection, tableName, resultColumns); - return resultColumns; - } - - public async testConnect(): Promise { - if (!this.connection.id) { - this.connection.id = nanoid(6); - } - const knex = await this.configureKnex(); - try { - await knex.transaction((trx) => { - return knex.raw(`SELECT 1 FROM DUAL`).transacting(trx); - }); - return { - result: true, - message: 'Successfully connected', - }; - } catch (e) { - return { - result: false, - message: e.message || 'Connection failed', - }; - } finally { - LRUStorage.delKnexCache(this.connection); - } - } - - public async updateRowInTable( - tableName: string, - row: Record, - primaryKey: Record, - ): Promise> { - const knex = await this.configureKnex(); - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - const tableStructure = await this.getTableStructure(tableName); - - const timestampColumnNames = tableStructure - .filter(({ data_type }) => isOracleTimeType(data_type)) - .map(({ column_name }) => column_name); - - const dateColumnNames = tableStructure - .filter(({ data_type }) => isOracleDateType(data_type)) - .map(({ column_name }) => column_name); - - Object.keys(row).forEach((key) => { - if (timestampColumnNames.includes(key) && row[key]) { - row[key] = this.formatTimestamp(row[key] as string); - } - if (dateColumnNames.includes(key) && row[key]) { - row[key] = this.formatDate(new Date(row[key] as string)); - } - }); - - return await knex(tableName).withSchema(schema).returning(Object.keys(primaryKey)).where(primaryKey).update(row); - } - - public async bulkUpdateRowsInTable( - tableName: string, - newValues: Record, - primaryKeys: Array>, - ): Promise>> { - const knex = await this.configureKnex(); - const schema = this.connection.schema ?? this.connection.username.toUpperCase(); - const tableStructure = await this.getTableStructure(tableName); - - const timestampColumnNames = tableStructure - .filter(({ data_type }) => isOracleTimeType(data_type)) - .map(({ column_name }) => column_name); - - const dateColumnNames = tableStructure - .filter(({ data_type }) => isOracleDateType(data_type)) - .map(({ column_name }) => column_name); - - // Format date and timestamp fields in newValues - Object.keys(newValues).forEach((key) => { - if (timestampColumnNames.includes(key) && newValues[key]) { - newValues[key] = this.formatTimestamp(newValues[key] as string); - } - if (dateColumnNames.includes(key) && newValues[key]) { - newValues[key] = this.formatDate(new Date(newValues[key] as string)); - } - }); - - return await knex.transaction(async (trx) => { - const results = []; - for (const primaryKey of primaryKeys) { - const result = await trx(tableName) - .withSchema(schema) - .returning(Object.keys(primaryKey)) - .where(primaryKey) - .update(newValues); - results.push(result[0]); - } - return results; - }); - } - - public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { - const knex = await this.configureKnex(); - - if (primaryKeys.length === 0) { - return 0; - } - - await knex.transaction(async (trx) => { - await trx(tableName) - .withSchema(this.connection.schema ?? this.connection.username.toUpperCase()) - .delete() - .modify((queryBuilder) => { - primaryKeys.forEach((key) => { - queryBuilder.orWhere((builder) => { - Object.entries(key).forEach(([column, value]) => { - builder.andWhere(column, value); - }); - }); - }); - }); - }); - - return primaryKeys.length; - } - - public async validateSettings(settings: ValidateTableSettingsDS, tableName: string): Promise { - const [tableStructure, primaryColumns] = await Promise.all([ - this.getTableStructure(tableName), - this.getTablePrimaryColumns(tableName), - ]); - return tableSettingsFieldValidator(tableStructure, primaryColumns, settings); - } - - public async getReferencedTableNamesAndColumns(tableName: string): Promise { - const primaryColumns = await this.getTablePrimaryColumns(tableName); - const knex = await this.configureKnex(); - const result: Array = []; - - await Promise.all( - primaryColumns.map(async (primaryColumn) => { - const referencedConstraints: Array = (await knex.transaction((trx) => { - return knex - .raw( - ` + const result = await knex.raw<{ OBJECT_NAME: string; OBJECT_TYPE: string }[]>(query, [schema]); + return result.map((row) => { + return { + tableName: row.OBJECT_NAME, + isView: row.OBJECT_TYPE === 'VIEW', + }; + }); + } + + public async getTableStructure(tableName: string): Promise { + const cachedTableStructure = LRUStorage.getTableStructureCache(this.connection, tableName); + if (cachedTableStructure) { + return cachedTableStructure; + } + const knex = await this.configureKnex(); + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + const structureColumns = await knex + .queryBuilder() + .select('COLUMN_NAME', 'DATA_DEFAULT', 'DATA_TYPE', 'NULLABLE', 'DATA_LENGTH') + .from('ALL_TAB_COLUMNS') + .orderBy('COLUMN_ID') + .where(`TABLE_NAME`, `${tableName}`) + .andWhere(`OWNER`, `${schema}`); + const resultColumns = structureColumns.map((column) => { + renameObjectKeyName(column, 'DATA_DEFAULT', 'column_default'); + column.NULLABLE = column.NULLABLE === 'Y'; + renameObjectKeyName(column, 'NULLABLE', 'allow_null'); + renameObjectKeyName(column, 'DATA_LENGTH', 'character_maximum_length'); + if (typeof column.column_default === 'string' && column.column_default.includes('SYS_GUID()')) { + column.extra = 'auto_increment'; + } + return objectKeysToLowercase(column); + }) as TableStructureDS[]; + LRUStorage.setTableStructureCache(this.connection, tableName, resultColumns); + return resultColumns; + } + + public async testConnect(): Promise { + if (!this.connection.id) { + this.connection.id = nanoid(6); + } + const knex = await this.configureKnex(); + try { + await knex.transaction((trx) => { + return knex.raw(`SELECT 1 FROM DUAL`).transacting(trx); + }); + return { + result: true, + message: 'Successfully connected', + }; + } catch (e) { + return { + result: false, + message: e.message || 'Connection failed', + }; + } finally { + LRUStorage.delKnexCache(this.connection); + } + } + + public async updateRowInTable( + tableName: string, + row: Record, + primaryKey: Record, + ): Promise> { + const knex = await this.configureKnex(); + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + const tableStructure = await this.getTableStructure(tableName); + + const timestampColumnNames = tableStructure + .filter(({ data_type }) => isOracleTimeType(data_type)) + .map(({ column_name }) => column_name); + + const dateColumnNames = tableStructure + .filter(({ data_type }) => isOracleDateType(data_type)) + .map(({ column_name }) => column_name); + + Object.keys(row).forEach((key) => { + if (timestampColumnNames.includes(key) && row[key]) { + row[key] = this.formatTimestamp(row[key] as string); + } + if (dateColumnNames.includes(key) && row[key]) { + row[key] = this.formatDate(new Date(row[key] as string)); + } + }); + + return await knex(tableName).withSchema(schema).returning(Object.keys(primaryKey)).where(primaryKey).update(row); + } + + public async bulkUpdateRowsInTable( + tableName: string, + newValues: Record, + primaryKeys: Array>, + ): Promise>> { + const knex = await this.configureKnex(); + const schema = this.connection.schema ?? this.connection.username.toUpperCase(); + const tableStructure = await this.getTableStructure(tableName); + + const timestampColumnNames = tableStructure + .filter(({ data_type }) => isOracleTimeType(data_type)) + .map(({ column_name }) => column_name); + + const dateColumnNames = tableStructure + .filter(({ data_type }) => isOracleDateType(data_type)) + .map(({ column_name }) => column_name); + + // Format date and timestamp fields in newValues + Object.keys(newValues).forEach((key) => { + if (timestampColumnNames.includes(key) && newValues[key]) { + newValues[key] = this.formatTimestamp(newValues[key] as string); + } + if (dateColumnNames.includes(key) && newValues[key]) { + newValues[key] = this.formatDate(new Date(newValues[key] as string)); + } + }); + + return await knex.transaction(async (trx) => { + const results = []; + for (const primaryKey of primaryKeys) { + const result = await trx(tableName) + .withSchema(schema) + .returning(Object.keys(primaryKey)) + .where(primaryKey) + .update(newValues); + results.push(result[0]); + } + return results; + }); + } + + public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { + const knex = await this.configureKnex(); + + if (primaryKeys.length === 0) { + return 0; + } + + await knex.transaction(async (trx) => { + await trx(tableName) + .withSchema(this.connection.schema ?? this.connection.username.toUpperCase()) + .delete() + .modify((queryBuilder) => { + primaryKeys.forEach((key) => { + queryBuilder.orWhere((builder) => { + Object.entries(key).forEach(([column, value]) => { + builder.andWhere(column, value); + }); + }); + }); + }); + }); + + return primaryKeys.length; + } + + public async validateSettings(settings: ValidateTableSettingsDS, tableName: string): Promise { + const [tableStructure, primaryColumns] = await Promise.all([ + this.getTableStructure(tableName), + this.getTablePrimaryColumns(tableName), + ]); + return tableSettingsFieldValidator(tableStructure, primaryColumns, settings); + } + + public async getReferencedTableNamesAndColumns(tableName: string): Promise { + const primaryColumns = await this.getTablePrimaryColumns(tableName); + const knex = await this.configureKnex(); + const result: Array = []; + + await Promise.all( + primaryColumns.map(async (primaryColumn) => { + const referencedConstraints: Array = (await knex.transaction((trx) => { + return knex + .raw( + ` SELECT UC.TABLE_NAME as TABLE_NAME, UC.CONSTRAINT_NAME as CONSTRAINT_NAME, @@ -667,235 +677,235 @@ export class DataAccessObjectOracle extends BasicDataAccessObject implements IDa UCC.TABLE_NAME, UCC.COLUMN_NAME `, - [tableName, primaryColumn.column_name], - ) - .transacting(trx); - })) as Array; - - await Promise.all( - referencedConstraints.map(async (referencedConstraint) => { - const columnName = await knex.transaction((trx) => { - return knex - .raw( - ` + [tableName, primaryColumn.column_name], + ) + .transacting(trx); + })) as Array; + + await Promise.all( + referencedConstraints.map(async (referencedConstraint) => { + const columnName = await knex.transaction((trx) => { + return knex + .raw( + ` SELECT column_name FROM all_cons_columns WHERE constraint_name = ? `, - referencedConstraint.CONSTRAINT_NAME, - ) - .transacting(trx); - }); - referencedConstraint.CONSTRAINT_NAME = columnName[0].COLUMN_NAME; - }), - ); - - result.push({ - referenced_on_column_name: primaryColumn.column_name, - referenced_by: referencedConstraints.map(({ TABLE_NAME, CONSTRAINT_NAME }) => ({ - table_name: TABLE_NAME, - column_name: CONSTRAINT_NAME, - })), - }); - }), - ); - - return result; - } - - public async isView(tableName: string): Promise { - const knex = await this.configureKnex(); - const schemaName = this.connection.schema ?? this.connection.username.toUpperCase(); - - const [result] = await knex.raw( - `SELECT object_type + referencedConstraint.CONSTRAINT_NAME, + ) + .transacting(trx); + }); + referencedConstraint.CONSTRAINT_NAME = columnName[0].COLUMN_NAME; + }), + ); + + result.push({ + referenced_on_column_name: primaryColumn.column_name, + referenced_by: referencedConstraints.map(({ TABLE_NAME, CONSTRAINT_NAME }) => ({ + table_name: TABLE_NAME, + column_name: CONSTRAINT_NAME, + })), + }); + }), + ); + + return result; + } + + public async isView(tableName: string): Promise { + const knex = await this.configureKnex(); + const schemaName = this.connection.schema ?? this.connection.username.toUpperCase(); + + const [result] = await knex.raw( + `SELECT object_type FROM all_objects WHERE owner = :schemaName AND object_name = :tableName`, - { - schemaName, - tableName, - }, - ); - - if (!result) { - const errorMessage = ERROR_MESSAGES.TABLE_NOT_FOUND(tableName); - throw new Error(errorMessage); - } - - const { OBJECT_TYPE } = result; - return OBJECT_TYPE === 'VIEW'; - } - - public async getTableRowsStream( - tableName: string, - settings: TableSettingsDS, - page: number, - perPage: number, - searchedFieldValue: string, - filteringFields: Array, - ): Promise> { - const knex = await this.configureKnex(); - const { page: updatedPage, perPage: updatedPerPage } = this.setupPagination(page, perPage, settings); - - const tableStructure = await this.getTableStructure(tableName); - const availableFields = this.findAvailableFields(settings, tableStructure); - - const searchedFields = - settings.search_fields?.length > 0 ? settings.search_fields : searchedFieldValue ? availableFields : undefined; - - const tableSchema = this.connection.schema ? this.connection.schema : this.connection.username.toUpperCase(); - - const offset = (updatedPage - 1) * updatedPerPage; - - const { large_dataset } = await this.getRowsCount(knex, tableName, tableSchema); - if (large_dataset) { - throw new Error(ERROR_MESSAGES.DATA_IS_TO_LARGE); - } - - const rowsAsStream = await knex(tableName) - .withSchema(tableSchema) - .select(availableFields) - .modify((builder) => { - if (searchedFieldValue && searchedFields?.length > 0) { - for (const field of searchedFields) { - if (Buffer.isBuffer(searchedFieldValue)) { - builder.orWhere(field, '=', searchedFieldValue); - } else { - builder.orWhereRaw(` Lower(??) LIKE ?`, [field, `${searchedFieldValue.toLowerCase()}%`]); - } - } - } - }) - .modify((builder) => { - if (filteringFields && filteringFields.length > 0) { - for (const { field, criteria, value } of filteringFields) { - const operators = { - [FilterCriteriaEnum.eq]: '=', - [FilterCriteriaEnum.startswith]: 'like', - [FilterCriteriaEnum.endswith]: 'like', - [FilterCriteriaEnum.gt]: '>', - [FilterCriteriaEnum.lt]: '<', - [FilterCriteriaEnum.lte]: '<=', - [FilterCriteriaEnum.gte]: '>=', - [FilterCriteriaEnum.contains]: 'like', - [FilterCriteriaEnum.icontains]: 'not like', - [FilterCriteriaEnum.empty]: 'is', - }; - const values = { - [FilterCriteriaEnum.startswith]: `${value}%`, - [FilterCriteriaEnum.endswith]: `%${value}`, - [FilterCriteriaEnum.contains]: `%${value}%`, - [FilterCriteriaEnum.icontains]: `%${value}%`, - [FilterCriteriaEnum.empty]: null, - }; - - builder.where(field, operators[criteria], values[criteria] || value); - } - } - }) - .modify((builder) => { - const { ordering_field, ordering } = settings; - if (ordering_field && ordering) { - builder.orderBy(ordering_field, ordering); - } - }) - .limit(updatedPerPage) - .offset(offset); - - return rowsAsStream; - } - - public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { - const tableSchema = this.connection.schema ? this.connection.schema : this.connection.username.toUpperCase(); - const knex = await this.configureKnex(); - const structure = await this.getTableStructure(tableName); - const timestampColumnNames = structure - .filter(({ data_type }) => isOracleDateOrTimeType(data_type)) - .map(({ column_name }) => column_name); - const stream = new Readable(); - stream.push(file.buffer); - stream.push(null); - - const parser = stream.pipe(csv.parse({ columns: true })); - - const results: any[] = []; - for await (const record of parser) { - results.push(record); - } - await knex.transaction(async (trx) => { - for (const row of results) { - for (const column of timestampColumnNames) { - if (row[column] && !isOracleDateStringByRegexp(row[column])) { - const date = new Date(Number(row[column])); - row[column] = this.formatDate(date); - } - } - - await trx(tableName).withSchema(tableSchema).insert(row); - } - }); - } - - public async executeRawQuery(query: string): Promise>> { - const knex = await this.configureKnex(); - return await knex.raw(query); - } - - private setupPagination(page: number, perPage: number, settings: TableSettingsDS) { - if (!page || page <= 0) { - page = DAO_CONSTANTS.DEFAULT_PAGINATION.page; - } - - const { list_per_page } = settings; - if (list_per_page && list_per_page > 0 && (!perPage || perPage <= 0)) { - perPage = list_per_page; - } else { - perPage = DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; - } - - return { page, perPage }; - } - - private attachSchemaNameToTableName(tableName: string): string { - tableName = this.connection.schema - ? `"${this.connection.schema}"."${tableName}"` - : `"${this.connection.username.toUpperCase()}"."${tableName}"`; - return tableName; - } - - private formatDate(date: Date) { - if (!date) { - return date as any; - } - const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; - const day = date.getDate(); - const monthIndex = date.getMonth(); - const year = date.getFullYear().toString().slice(-2); - const resultString = `${day}-${monthNames[monthIndex]}-${year}`; - return resultString; - } - - private formatTimestamp(timestamp: string | number | Date): string { - if (!timestamp) { - return timestamp as any; - } - const date = new Date(timestamp); - - const day = `0${date.getDate()}`.slice(-2); - const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; - const month = monthNames[date.getMonth()]; - const year = date.getFullYear().toString().slice(-2); - - let hours = date.getHours(); - const period = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; - const hoursStr = `0${hours}`.slice(-2); - - const minutes = `0${date.getMinutes()}`.slice(-2); - const seconds = `0${date.getSeconds()}`.slice(-2); - - return `${day}-${month}-${year} ${hoursStr}:${minutes}:${seconds} ${period}`; - } + { + schemaName, + tableName, + }, + ); + + if (!result) { + const errorMessage = ERROR_MESSAGES.TABLE_NOT_FOUND(tableName); + throw new Error(errorMessage); + } + + const { OBJECT_TYPE } = result; + return OBJECT_TYPE === 'VIEW'; + } + + public async getTableRowsStream( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: Array, + ): Promise> { + const knex = await this.configureKnex(); + const { page: updatedPage, perPage: updatedPerPage } = this.setupPagination(page, perPage, settings); + + const tableStructure = await this.getTableStructure(tableName); + const availableFields = this.findAvailableFields(settings, tableStructure); + + const searchedFields = + settings.search_fields?.length > 0 ? settings.search_fields : searchedFieldValue ? availableFields : undefined; + + const tableSchema = this.connection.schema ? this.connection.schema : this.connection.username.toUpperCase(); + + const offset = (updatedPage - 1) * updatedPerPage; + + const { large_dataset } = await this.getRowsCount(knex, tableName, tableSchema); + if (large_dataset) { + throw new Error(ERROR_MESSAGES.DATA_IS_TO_LARGE); + } + + const rowsAsStream = await knex(tableName) + .withSchema(tableSchema) + .select(availableFields) + .modify((builder) => { + if (searchedFieldValue && searchedFields?.length > 0) { + for (const field of searchedFields) { + if (Buffer.isBuffer(searchedFieldValue)) { + builder.orWhere(field, '=', searchedFieldValue); + } else { + builder.orWhereRaw(` Lower(??) LIKE ?`, [field, `${searchedFieldValue.toLowerCase()}%`]); + } + } + } + }) + .modify((builder) => { + if (filteringFields && filteringFields.length > 0) { + for (const { field, criteria, value } of filteringFields) { + const operators = { + [FilterCriteriaEnum.eq]: '=', + [FilterCriteriaEnum.startswith]: 'like', + [FilterCriteriaEnum.endswith]: 'like', + [FilterCriteriaEnum.gt]: '>', + [FilterCriteriaEnum.lt]: '<', + [FilterCriteriaEnum.lte]: '<=', + [FilterCriteriaEnum.gte]: '>=', + [FilterCriteriaEnum.contains]: 'like', + [FilterCriteriaEnum.icontains]: 'not like', + [FilterCriteriaEnum.empty]: 'is', + }; + const values = { + [FilterCriteriaEnum.startswith]: `${value}%`, + [FilterCriteriaEnum.endswith]: `%${value}`, + [FilterCriteriaEnum.contains]: `%${value}%`, + [FilterCriteriaEnum.icontains]: `%${value}%`, + [FilterCriteriaEnum.empty]: null, + }; + + builder.where(field, operators[criteria], values[criteria] || value); + } + } + }) + .modify((builder) => { + const { ordering_field, ordering } = settings; + if (ordering_field && ordering) { + builder.orderBy(ordering_field, ordering); + } + }) + .limit(updatedPerPage) + .offset(offset); + + return rowsAsStream; + } + + public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { + const tableSchema = this.connection.schema ? this.connection.schema : this.connection.username.toUpperCase(); + const knex = await this.configureKnex(); + const structure = await this.getTableStructure(tableName); + const timestampColumnNames = structure + .filter(({ data_type }) => isOracleDateOrTimeType(data_type)) + .map(({ column_name }) => column_name); + const stream = new Readable(); + stream.push(file.buffer); + stream.push(null); + + const parser = stream.pipe(csv.parse({ columns: true })); + + const results: any[] = []; + for await (const record of parser) { + results.push(record); + } + await knex.transaction(async (trx) => { + for (const row of results) { + for (const column of timestampColumnNames) { + if (row[column] && !isOracleDateStringByRegexp(row[column])) { + const date = new Date(Number(row[column])); + row[column] = this.formatDate(date); + } + } + + await trx(tableName).withSchema(tableSchema).insert(row); + } + }); + } + + public async executeRawQuery(query: string): Promise>> { + const knex = await this.configureKnex(); + return await knex.raw(query); + } + + private setupPagination(page: number, perPage: number, settings: TableSettingsDS) { + if (!page || page <= 0) { + page = DAO_CONSTANTS.DEFAULT_PAGINATION.page; + } + + const { list_per_page } = settings; + if (list_per_page && list_per_page > 0 && (!perPage || perPage <= 0)) { + perPage = list_per_page; + } else { + perPage = DAO_CONSTANTS.DEFAULT_PAGINATION.perPage; + } + + return { page, perPage }; + } + + private attachSchemaNameToTableName(tableName: string): string { + tableName = this.connection.schema + ? `"${this.connection.schema}"."${tableName}"` + : `"${this.connection.username.toUpperCase()}"."${tableName}"`; + return tableName; + } + + private formatDate(date: Date) { + if (!date) { + return date as any; + } + const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; + const day = date.getDate(); + const monthIndex = date.getMonth(); + const year = date.getFullYear().toString().slice(-2); + const resultString = `${day}-${monthNames[monthIndex]}-${year}`; + return resultString; + } + + private formatTimestamp(timestamp: string | number | Date): string { + if (!timestamp) { + return timestamp as any; + } + const date = new Date(timestamp); + + const day = `0${date.getDate()}`.slice(-2); + const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']; + const month = monthNames[date.getMonth()]; + const year = date.getFullYear().toString().slice(-2); + + let hours = date.getHours(); + const period = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; + const hoursStr = `0${hours}`.slice(-2); + + const minutes = `0${date.getMinutes()}`.slice(-2); + const seconds = `0${date.getSeconds()}`.slice(-2); + + return `${day}-${month}-${year} ${hoursStr}:${minutes}:${seconds} ${period}`; + } }