From ab1aa20108609298af315c51c8ea76086d67bb5d Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 6 Feb 2026 12:00:56 +0000 Subject: [PATCH 1/6] refactor: improve code formatting and add escapeRegex method for safer regex usage --- .../data-access-object-mongodb.ts | 1244 +++++++++-------- 1 file changed, 626 insertions(+), 618 deletions(-) diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts index b94e24f3d..e8388b904 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts @@ -28,625 +28,633 @@ import { IDataAccessObject } from '../../shared/interfaces/data-access-object.in import { BasicDataAccessObject } from './basic-data-access-object.js'; export type MongoClientDB = { - db: Db; - dbClient: MongoClient; + db: Db; + dbClient: MongoClient; }; export class DataAccessObjectMongo extends BasicDataAccessObject implements IDataAccessObject { - - public async addRowInTable( - tableName: string, - row: Record, - ): Promise> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - delete row._id; - const result = await collection.insertOne(row); - return { _id: this.processMongoIdField(result?.insertedId) }; - } - - public async deleteRowInTable( - tableName: string, - primaryKey: Record, - ): Promise> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const objectId = this.createObjectIdFromSting(primaryKey._id as string); - await collection.deleteOne({ _id: objectId }); - return { _id: this.processMongoIdField(objectId) }; - } - - public async getIdentityColumns( - tableName: string, - referencedFieldName: string, - identityColumnName: string, - fieldValues: (string | number)[], - ): Promise[]> { - if (!referencedFieldName || !fieldValues.length) { - return []; - } - - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - - if (referencedFieldName === '_id') { - fieldValues = fieldValues.map((value) => this.createObjectIdFromSting(String(value))) as any; - } - - const query = { - [referencedFieldName]: { $in: fieldValues }, - }; - - const projection = identityColumnName - ? { [identityColumnName]: 1, [referencedFieldName]: 1 } - : { [referencedFieldName]: 1 }; - - const results = await collection.find(query).project(projection).toArray(); - - return results.map((doc) => ({ - ...doc, - _id: this.processMongoIdField(doc._id), - })); - } - - public async getRowByPrimaryKey( - tableName: string, - primaryKey: Record, - settings: TableSettingsDS, - ): Promise> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const objectId = this.createObjectIdFromSting(primaryKey._id as string); - - let availableFields: string[] = []; - if (settings) { - const tableStructure = await this.getTableStructure(tableName); - availableFields = this.findAvailableFields(settings, tableStructure); - } - - const foundRow = await collection.findOne({ _id: objectId }); - if (!foundRow) { - return null; - } - - const rowKeys = Object.keys(foundRow); - if (availableFields.length > 0) { - for (const key of rowKeys) { - if (!availableFields.includes(key)) { - delete foundRow[key]; - } - } - } - return { - ...foundRow, - _id: this.processMongoIdField(objectId), - }; - } - - public async bulkGetRowsFromTableByPrimaryKeys( - tableName: string, - primaryKeys: Array>, - settings: TableSettingsDS, - ): Promise>> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - - const objectIds = primaryKeys.map((primaryKey) => { - if (primaryKey._id) { - return this.createObjectIdFromSting(primaryKey._id as string); - } - throw new Error('Missing _id in primary key'); - }); - - let availableFields: string[] = []; - if (settings) { - const tableStructure = await this.getTableStructure(tableName); - availableFields = this.findAvailableFields(settings, tableStructure); - } - - const query = { _id: { $in: objectIds } }; - - const rows = await collection.find(query).toArray(); - - return rows.map((row) => { - const processedRow = { ...row, _id: this.processMongoIdField(row._id) }; - if (availableFields.length > 0) { - Object.keys(processedRow).forEach((key) => { - if (!availableFields.includes(key)) { - delete processedRow[key]; - } - }); - } - return processedRow; - }); - } - - public async getRowsFromTable( - tableName: string, - settings: TableSettingsDS, - page: number, - perPage: number, - searchedFieldValue: string, - filteringFields: FilteringFieldsDS[], - autocompleteFields: AutocompleteFieldsDS, - tableStructure: TableStructureDS[] | null, - ): Promise { - 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; - - const offset = (page - 1) * perPage; - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - - if (!tableStructure) { - tableStructure = await this.getTableStructure(tableName); - } - const availableFields = this.findAvailableFields(settings, tableStructure); - - if (autocompleteFields?.value && autocompleteFields.fields?.length > 0) { - const { fields, value } = autocompleteFields; - const query = fields.reduce((acc, field) => { - acc[field] = new RegExp(String(value), 'i'); - return acc; - }, {}); - const rows = await collection.find(query).limit(DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT).toArray(); - const { large_dataset } = await this.getRowsCount(tableName, query); - return { - data: rows.map((row) => { - Object.keys(row).forEach((key) => { - if (!availableFields.includes(key)) { - delete row[key]; - } - }); - return { - ...row, - _id: this.processMongoIdField(row?._id), - }; - }), - large_dataset, - pagination: {} as any, - }; - } - - const query = {}; - - let { search_fields } = settings; - if ((!search_fields || search_fields?.length === 0) && searchedFieldValue) { - search_fields = availableFields; - } - - if (searchedFieldValue && search_fields?.length > 0) { - const searchQuery = search_fields.reduce((acc, field) => { - let condition; - if (field === '_id') { - const parsedSearchedFieldValue = Buffer.from(searchedFieldValue, 'binary').toString('hex'); - condition = { [field]: this.createObjectIdFromSting(parsedSearchedFieldValue) }; - } else { - condition = { [field]: new RegExp(String(searchedFieldValue), 'i') }; - } - acc.push(condition); - return acc; - }, []); - Object.assign(query, { $or: searchQuery }); - } - - if (filteringFields?.length > 0) { - const groupedFilters = filteringFields.reduce((acc, filterObject) => { - // eslint-disable-next-line prefer-const - let { field, criteria, value } = filterObject; - if (field === '_id') { - value = this.createObjectIdFromSting(value as string); - } - if (!acc[field]) { - acc[field] = {}; - } - switch (criteria) { - case FilterCriteriaEnum.eq: - acc[field] = value; - break; - case FilterCriteriaEnum.contains: - acc[field] = new RegExp(String(value), 'i'); - break; - case FilterCriteriaEnum.gt: - acc[field].$gt = value; - break; - case FilterCriteriaEnum.lt: - acc[field].$lt = value; - break; - case FilterCriteriaEnum.gte: - acc[field].$gte = value; - break; - case FilterCriteriaEnum.lte: - acc[field].$lte = value; - break; - case FilterCriteriaEnum.icontains: - acc[field].$not = new RegExp(String(value), 'i'); - break; - case FilterCriteriaEnum.startswith: - acc[field] = new RegExp(`^${String(value)}`, 'i'); - break; - case FilterCriteriaEnum.endswith: - acc[field] = new RegExp(`${String(value)}$`, 'i'); - break; - case FilterCriteriaEnum.empty: - acc[field].$exists = false; - break; - default: - break; - } - return acc; - }, {}); - - Object.assign(query, groupedFilters); - } - - const { large_dataset, rowsCount } = await this.getRowsCount(tableName, query); - const rows = await collection.find(query).skip(offset).limit(perPage).toArray(); - const pagination = { - total: rowsCount, - lastPage: Math.ceil(rowsCount / perPage), - perPage: perPage, - currentPage: page, - }; - return { - data: rows.map((row) => { - Object.keys(row).forEach((key) => { - if (!availableFields.includes(key)) { - delete row[key]; - } - }); - return { - ...row, - _id: this.processMongoIdField(row?._id), - }; - }), - pagination, - large_dataset, - }; - } - - public async getTableForeignKeys(_tableName: string): Promise { - return []; - } - - public async getTablePrimaryColumns(_tableName: string): Promise { - return [ - { - column_name: '_id', - data_type: 'string', - }, - ]; - } - - public async getTablesFromDB(): Promise { - const db = await this.getConnectionToDatabase(); - const collections = await db.listCollections().toArray(); - return collections.map((collection) => { - return { - tableName: collection.name, - isView: false, - }; - }); - } - - public async getTableStructure(tableName: string): Promise { - return await this.getTableStructureOrReturnPrimaryKeysIfNothingToScan(tableName); - } - - public async testConnect(): Promise { - if (!this.connection.id) { - this.connection.id = nanoid(6); - } - try { - await this.getConnectionToDatabase(); - return { - result: true, - message: 'Successfully connected', - }; - } catch (error) { - return { - result: false, - message: error.message, - }; - } finally { - LRUStorage.delMongoDbCache(this.connection); - } - } - - public async updateRowInTable( - tableName: string, - row: Record, - primaryKey: Record, - ): Promise> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const objectId = this.createObjectIdFromSting(primaryKey._id as string); - delete row._id; - await collection.updateOne({ _id: objectId }, { $set: row }); - return { _id: this.processMongoIdField(objectId) }; - } - - public async bulkUpdateRowsInTable( - tableName: string, - newValues: Record, - primaryKeys: Record[], - ): Promise>> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const objectIds = primaryKeys.map((primaryKey) => this.createObjectIdFromSting(primaryKey._id as string)); - await collection.updateMany({ _id: { $in: objectIds } }, { $set: newValues }); - return primaryKeys; - } - - public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const objectIds = primaryKeys.map((primaryKey) => this.createObjectIdFromSting(primaryKey._id as string)); - await collection.deleteMany({ _id: { $in: objectIds } }); - 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 { - return []; - } - - public async isView(_tableName: string): Promise { - return false; - } - - public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - 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 collection.insertMany(results); - } - - public async getTableRowsStream( - tableName: string, - settings: TableSettingsDS, - page: number, - perPage: number, - searchedFieldValue: string, - filteringFields: FilteringFieldsDS[], - ): Promise> { - const result = await this.getRowsFromTable( - tableName, - settings, - page, - perPage, - searchedFieldValue, - filteringFields, - null, - null, - ); - return result.data as any; - } - - public async executeRawQuery(query: string, tableName: string): Promise[]> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const aggregationPipeline = JSON.parse(query); - const result = await collection.aggregate(aggregationPipeline).toArray(); - return result; - } - - private async getConnectionToDatabase(): Promise { - if (this.connection.ssh) { - const { db } = await this.createTunneledConnection(this.connection); - return db; - } - const { db } = await this.getUsualConnection(); - return db; - } - - private async getUsualConnection(): Promise { - const cachedDatabase = LRUStorage.getMongoDbCache(this.connection); - if (cachedDatabase) { - return cachedDatabase; - } - - let mongoConnectionString = ''; - if (this.connection.host.includes('mongodb+srv')) { - const hostNameParts = this.connection.host.split('//'); - mongoConnectionString = `${hostNameParts[0]}//${encodeURIComponent(this.connection.username)}:${encodeURIComponent(this.connection.password)}@${hostNameParts[1]}/${this.connection.database}`; - } else { - mongoConnectionString = `mongodb://${encodeURIComponent(this.connection.username)}:${encodeURIComponent(this.connection.password)}@${this.connection.host}:${this.connection.port}/${this.connection.database || ''}`; - } - - let options: MongoClientOptions = {}; - if (this.connection.ssl) { - options = { - ssl: true, - ca: this.connection.cert ? [this.connection.cert] : undefined, - }; - } - - if (this.connection.authSource) { - mongoConnectionString += mongoConnectionString.includes('?') ? '&' : '?'; - mongoConnectionString += `authSource=${this.connection.authSource}`; - } - - const client = new MongoClient(mongoConnectionString, options); - let clientDb: MongoClientDB; - try { - const connectedClient = await client.connect(); - clientDb = { db: connectedClient.db(this.connection.database), dbClient: connectedClient }; - LRUStorage.setMongoDbCache(this.connection, clientDb); - } catch (error) { - console.error('Mongo connection error:', error); - throw error; - } - return clientDb; - } - - private async createTunneledConnection(connection: ConnectionParams): Promise { - const connectionCopy = { ...connection }; - return new Promise(async (resolve, reject): Promise => { - const cachedTnl = LRUStorage.getTunnelCache(connectionCopy); - if (cachedTnl?.mongo && cachedTnl.server && cachedTnl.client) { - resolve(cachedTnl.db); - 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(); - - const tnlCachedObj = { - server: server, - client: client, - mongo: database, - }; - LRUStorage.setTunnelCache(connectionCopy, tnlCachedObj); - resolve(tnlCachedObj.mongo); - 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; - } - }); - } - - private async getRowsCount( - tableName: string, - query: Record, - ): Promise<{ rowsCount: number; large_dataset: boolean }> { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const count = await collection.countDocuments(query); - return { rowsCount: count, large_dataset: count > DAO_CONSTANTS.LARGE_DATASET_ROW_LIMIT }; - } - - private createObjectIdFromSting(id: string): ObjectId { - try { - return new ObjectId(id); - } catch (_error) { - throw new Error(ERROR_MESSAGES.INVALID_OBJECT_ID_FORMAT); - } - } - - private getMongoDataTypeByValue(value: unknown): string { - switch (true) { - case Array.isArray(value): - return 'array'; - case typeof value === 'object' && value !== null: - return 'object'; - case value instanceof BSON.Double: - return 'double'; - case value instanceof BSON.Int32: - return 'int32'; - case value instanceof BSON.Binary: - return 'binary'; - case value instanceof BSON.ObjectId: - return 'objectid'; - case value instanceof BSON.Timestamp: - return 'timestamp'; - case value instanceof BSON.Long: - return 'long'; - case value instanceof BSON.Decimal128: - return 'decimal128'; - case value instanceof BSON.BSONRegExp: - return 'regexp'; - case typeof value === 'string': - return 'string'; - case typeof value === 'number': - return 'number'; - case typeof value === 'boolean': - return 'boolean'; - case value instanceof Date: - return 'date'; - default: - return 'unknown'; - } - } - - private async getTableStructureOrReturnPrimaryKeysIfNothingToScan(tableName: string): Promise { - const db = await this.getConnectionToDatabase(); - const collection = db.collection(tableName); - const document = await collection.findOne({}); - if (!document) { - return []; - } - const structure: TableStructureDS[] = Object.keys(document).map((key) => ({ - allow_null: document[key] === null, - character_maximum_length: null, - column_default: key === '_id' ? 'autoincrement' : null, - column_name: key, - data_type: key === '_id' ? 'string' : this.getMongoDataTypeByValue(document[key]), - data_type_params: null, - udt_name: null, - extra: null, - })); - - if (!structure.length) { - const primaryColumns = await this.getTablePrimaryColumns(tableName); - return primaryColumns.map((column) => ({ - allow_null: false, - character_maximum_length: null, - column_default: null, - column_name: column.column_name, - data_type: column.data_type, - data_type_params: null, - udt_name: null, - extra: null, - })); - } - return structure; - } - - private processMongoIdField(_id: unknown): string | undefined { - if (!_id) { - return; - } - if (typeof _id === 'string') { - return _id; - } - if (_id instanceof ObjectId) { - return (_id as ObjectId).toHexString(); - } - try { - return _id.toString(); - } catch (_error) {} - try { - return new ObjectId(_id as string).toHexString(); - } catch (_error) { - return _id as any; - } - } + public async addRowInTable( + tableName: string, + row: Record, + ): Promise> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + delete row._id; + const result = await collection.insertOne(row); + return { _id: this.processMongoIdField(result?.insertedId) }; + } + + public async deleteRowInTable( + tableName: string, + primaryKey: Record, + ): Promise> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const objectId = this.createObjectIdFromSting(primaryKey._id as string); + await collection.deleteOne({ _id: objectId }); + return { _id: this.processMongoIdField(objectId) }; + } + + public async getIdentityColumns( + tableName: string, + referencedFieldName: string, + identityColumnName: string, + fieldValues: (string | number)[], + ): Promise[]> { + if (!referencedFieldName || !fieldValues.length) { + return []; + } + + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + + if (referencedFieldName === '_id') { + fieldValues = fieldValues.map((value) => this.createObjectIdFromSting(String(value))) as any; + } + + const query = { + [referencedFieldName]: { $in: fieldValues }, + }; + + const projection = identityColumnName + ? { [identityColumnName]: 1, [referencedFieldName]: 1 } + : { [referencedFieldName]: 1 }; + + const results = await collection.find(query).project(projection).toArray(); + + return results.map((doc) => ({ + ...doc, + _id: this.processMongoIdField(doc._id), + })); + } + + public async getRowByPrimaryKey( + tableName: string, + primaryKey: Record, + settings: TableSettingsDS, + ): Promise> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const objectId = this.createObjectIdFromSting(primaryKey._id as string); + + let availableFields: string[] = []; + if (settings) { + const tableStructure = await this.getTableStructure(tableName); + availableFields = this.findAvailableFields(settings, tableStructure); + } + + const foundRow = await collection.findOne({ _id: objectId }); + if (!foundRow) { + return null; + } + + const rowKeys = Object.keys(foundRow); + if (availableFields.length > 0) { + for (const key of rowKeys) { + if (!availableFields.includes(key)) { + delete foundRow[key]; + } + } + } + return { + ...foundRow, + _id: this.processMongoIdField(objectId), + }; + } + + public async bulkGetRowsFromTableByPrimaryKeys( + tableName: string, + primaryKeys: Array>, + settings: TableSettingsDS, + ): Promise>> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + + const objectIds = primaryKeys.map((primaryKey) => { + if (primaryKey._id) { + return this.createObjectIdFromSting(primaryKey._id as string); + } + throw new Error('Missing _id in primary key'); + }); + + let availableFields: string[] = []; + if (settings) { + const tableStructure = await this.getTableStructure(tableName); + availableFields = this.findAvailableFields(settings, tableStructure); + } + + const query = { _id: { $in: objectIds } }; + + const rows = await collection.find(query).toArray(); + + return rows.map((row) => { + const processedRow = { ...row, _id: this.processMongoIdField(row._id) }; + if (availableFields.length > 0) { + Object.keys(processedRow).forEach((key) => { + if (!availableFields.includes(key)) { + delete processedRow[key]; + } + }); + } + return processedRow; + }); + } + + public async getRowsFromTable( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: FilteringFieldsDS[], + autocompleteFields: AutocompleteFieldsDS, + tableStructure: TableStructureDS[] | null, + ): Promise { + 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; + + const offset = (page - 1) * perPage; + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + + if (!tableStructure) { + tableStructure = await this.getTableStructure(tableName); + } + const availableFields = this.findAvailableFields(settings, tableStructure); + + if (autocompleteFields?.value && autocompleteFields.fields?.length > 0) { + const { fields, value } = autocompleteFields; + const query = fields.reduce((acc, field) => { + acc[field] = new RegExp(this.escapeRegex(String(value)), 'i'); + return acc; + }, {}); + const rows = await collection.find(query).limit(DAO_CONSTANTS.AUTOCOMPLETE_ROW_LIMIT).toArray(); + const { large_dataset } = await this.getRowsCount(tableName, query); + return { + data: rows.map((row) => { + Object.keys(row).forEach((key) => { + if (!availableFields.includes(key)) { + delete row[key]; + } + }); + return { + ...row, + _id: this.processMongoIdField(row?._id), + }; + }), + large_dataset, + pagination: {} as any, + }; + } + + const query = {}; + + let { search_fields } = settings; + if ((!search_fields || search_fields?.length === 0) && searchedFieldValue) { + search_fields = availableFields; + } + + if (searchedFieldValue && search_fields?.length > 0) { + const searchQuery = search_fields.reduce((acc, field) => { + let condition; + if (field === '_id') { + const parsedSearchedFieldValue = Buffer.from(searchedFieldValue, 'binary').toString('hex'); + condition = { [field]: this.createObjectIdFromSting(parsedSearchedFieldValue) }; + } else { + condition = { [field]: new RegExp(this.escapeRegex(String(searchedFieldValue)), 'i') }; + } + acc.push(condition); + return acc; + }, []); + Object.assign(query, { $or: searchQuery }); + } + + if (filteringFields?.length > 0) { + const groupedFilters = filteringFields.reduce((acc, filterObject) => { + // eslint-disable-next-line prefer-const + let { field, criteria, value } = filterObject; + if (field === '_id') { + value = this.createObjectIdFromSting(value as string); + } + if (!acc[field]) { + acc[field] = {}; + } + switch (criteria) { + case FilterCriteriaEnum.eq: + acc[field] = value; + break; + case FilterCriteriaEnum.contains: + acc[field] = new RegExp(this.escapeRegex(String(value)), 'i'); + break; + case FilterCriteriaEnum.gt: + acc[field].$gt = value; + break; + case FilterCriteriaEnum.lt: + acc[field].$lt = value; + break; + case FilterCriteriaEnum.gte: + acc[field].$gte = value; + break; + case FilterCriteriaEnum.lte: + acc[field].$lte = value; + break; + case FilterCriteriaEnum.icontains: + acc[field].$not = new RegExp(this.escapeRegex(String(value)), 'i'); + break; + case FilterCriteriaEnum.startswith: + acc[field] = new RegExp(`^${this.escapeRegex(String(value))}`, 'i'); + break; + case FilterCriteriaEnum.endswith: + acc[field] = new RegExp(`${this.escapeRegex(String(value))}$`, 'i'); + break; + case FilterCriteriaEnum.empty: + acc[field].$exists = false; + break; + default: + break; + } + return acc; + }, {}); + + Object.assign(query, groupedFilters); + } + + const { large_dataset, rowsCount } = await this.getRowsCount(tableName, query); + const rows = await collection.find(query).skip(offset).limit(perPage).toArray(); + const pagination = { + total: rowsCount, + lastPage: Math.ceil(rowsCount / perPage), + perPage: perPage, + currentPage: page, + }; + return { + data: rows.map((row) => { + Object.keys(row).forEach((key) => { + if (!availableFields.includes(key)) { + delete row[key]; + } + }); + return { + ...row, + _id: this.processMongoIdField(row?._id), + }; + }), + pagination, + large_dataset, + }; + } + + public async getTableForeignKeys(_tableName: string): Promise { + return []; + } + + public async getTablePrimaryColumns(_tableName: string): Promise { + return [ + { + column_name: '_id', + data_type: 'string', + }, + ]; + } + + public async getTablesFromDB(): Promise { + const db = await this.getConnectionToDatabase(); + const collections = await db.listCollections().toArray(); + return collections.map((collection) => { + return { + tableName: collection.name, + isView: false, + }; + }); + } + + public async getTableStructure(tableName: string): Promise { + return await this.getTableStructureOrReturnPrimaryKeysIfNothingToScan(tableName); + } + + public async testConnect(): Promise { + if (!this.connection.id) { + this.connection.id = nanoid(6); + } + try { + await this.getConnectionToDatabase(); + return { + result: true, + message: 'Successfully connected', + }; + } catch (error) { + return { + result: false, + message: error.message, + }; + } finally { + LRUStorage.delMongoDbCache(this.connection); + } + } + + public async updateRowInTable( + tableName: string, + row: Record, + primaryKey: Record, + ): Promise> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const objectId = this.createObjectIdFromSting(primaryKey._id as string); + delete row._id; + await collection.updateOne({ _id: objectId }, { $set: row }); + return { _id: this.processMongoIdField(objectId) }; + } + + public async bulkUpdateRowsInTable( + tableName: string, + newValues: Record, + primaryKeys: Record[], + ): Promise>> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const objectIds = primaryKeys.map((primaryKey) => this.createObjectIdFromSting(primaryKey._id as string)); + await collection.updateMany({ _id: { $in: objectIds } }, { $set: newValues }); + return primaryKeys; + } + + public async bulkDeleteRowsInTable(tableName: string, primaryKeys: Array>): Promise { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const objectIds = primaryKeys.map((primaryKey) => this.createObjectIdFromSting(primaryKey._id as string)); + await collection.deleteMany({ _id: { $in: objectIds } }); + 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 { + return []; + } + + public async isView(_tableName: string): Promise { + return false; + } + + public async importCSVInTable(file: Express.Multer.File, tableName: string): Promise { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + 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 collection.insertMany(results); + } + + public async getTableRowsStream( + tableName: string, + settings: TableSettingsDS, + page: number, + perPage: number, + searchedFieldValue: string, + filteringFields: FilteringFieldsDS[], + ): Promise> { + const result = await this.getRowsFromTable( + tableName, + settings, + page, + perPage, + searchedFieldValue, + filteringFields, + null, + null, + ); + return result.data as any; + } + + public async executeRawQuery(query: string, tableName: string): Promise[]> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const aggregationPipeline = JSON.parse(query); + const result = await collection.aggregate(aggregationPipeline).toArray(); + return result; + } + + private async getConnectionToDatabase(): Promise { + if (this.connection.ssh) { + const { db } = await this.createTunneledConnection(this.connection); + return db; + } + const { db } = await this.getUsualConnection(); + return db; + } + + private async getUsualConnection(): Promise { + const cachedDatabase = LRUStorage.getMongoDbCache(this.connection); + if (cachedDatabase) { + return cachedDatabase; + } + + let mongoConnectionString = ''; + if (this.connection.host.includes('mongodb+srv')) { + const hostNameParts = this.connection.host.split('//'); + mongoConnectionString = `${hostNameParts[0]}//${encodeURIComponent(this.connection.username)}:${encodeURIComponent(this.connection.password)}@${hostNameParts[1]}/${this.connection.database}`; + } else { + mongoConnectionString = `mongodb://${encodeURIComponent(this.connection.username)}:${encodeURIComponent(this.connection.password)}@${this.connection.host}:${this.connection.port}/${this.connection.database || ''}`; + } + + let options: MongoClientOptions = {}; + if (this.connection.ssl) { + options = { + ssl: true, + ca: this.connection.cert ? [this.connection.cert] : undefined, + }; + } + + if (this.connection.authSource) { + mongoConnectionString += mongoConnectionString.includes('?') ? '&' : '?'; + mongoConnectionString += `authSource=${this.connection.authSource}`; + } + + const client = new MongoClient(mongoConnectionString, options); + let clientDb: MongoClientDB; + try { + const connectedClient = await client.connect(); + clientDb = { db: connectedClient.db(this.connection.database), dbClient: connectedClient }; + LRUStorage.setMongoDbCache(this.connection, clientDb); + } catch (error) { + console.error('Mongo connection error:', error); + throw error; + } + return clientDb; + } + + private async createTunneledConnection(connection: ConnectionParams): Promise { + const connectionCopy = { ...connection }; + return new Promise(async (resolve, reject): Promise => { + const cachedTnl = LRUStorage.getTunnelCache(connectionCopy); + if (cachedTnl?.mongo && cachedTnl.server && cachedTnl.client) { + resolve(cachedTnl.db); + 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(); + + const tnlCachedObj = { + server: server, + client: client, + mongo: database, + }; + LRUStorage.setTunnelCache(connectionCopy, tnlCachedObj); + resolve(tnlCachedObj.mongo); + 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; + } + }); + } + + private async getRowsCount( + tableName: string, + query: Record, + ): Promise<{ rowsCount: number; large_dataset: boolean }> { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const count = await collection.countDocuments(query); + return { rowsCount: count, large_dataset: count > DAO_CONSTANTS.LARGE_DATASET_ROW_LIMIT }; + } + + private createObjectIdFromSting(id: string): ObjectId { + try { + return new ObjectId(id); + } catch (_error) { + throw new Error(ERROR_MESSAGES.INVALID_OBJECT_ID_FORMAT); + } + } + + private getMongoDataTypeByValue(value: unknown): string { + switch (true) { + case Array.isArray(value): + return 'array'; + case typeof value === 'object' && value !== null: + return 'object'; + case value instanceof BSON.Double: + return 'double'; + case value instanceof BSON.Int32: + return 'int32'; + case value instanceof BSON.Binary: + return 'binary'; + case value instanceof BSON.ObjectId: + return 'objectid'; + case value instanceof BSON.Timestamp: + return 'timestamp'; + case value instanceof BSON.Long: + return 'long'; + case value instanceof BSON.Decimal128: + return 'decimal128'; + case value instanceof BSON.BSONRegExp: + return 'regexp'; + case typeof value === 'string': + return 'string'; + case typeof value === 'number': + return 'number'; + case typeof value === 'boolean': + return 'boolean'; + case value instanceof Date: + return 'date'; + default: + return 'unknown'; + } + } + + private async getTableStructureOrReturnPrimaryKeysIfNothingToScan(tableName: string): Promise { + const db = await this.getConnectionToDatabase(); + const collection = db.collection(tableName); + const document = await collection.findOne({}); + if (!document) { + return []; + } + const structure: TableStructureDS[] = Object.keys(document).map((key) => ({ + allow_null: document[key] === null, + character_maximum_length: null, + column_default: key === '_id' ? 'autoincrement' : null, + column_name: key, + data_type: key === '_id' ? 'string' : this.getMongoDataTypeByValue(document[key]), + data_type_params: null, + udt_name: null, + extra: null, + })); + + if (!structure.length) { + const primaryColumns = await this.getTablePrimaryColumns(tableName); + return primaryColumns.map((column) => ({ + allow_null: false, + character_maximum_length: null, + column_default: null, + column_name: column.column_name, + data_type: column.data_type, + data_type_params: null, + udt_name: null, + extra: null, + })); + } + return structure; + } + + private processMongoIdField(_id: unknown): string | undefined { + if (!_id) { + return; + } + if (typeof _id === 'string') { + return _id; + } + if (_id instanceof ObjectId) { + return (_id as ObjectId).toHexString(); + } + try { + return _id.toString(); + } catch (_error) {} + try { + return new ObjectId(_id as string).toHexString(); + } catch (_error) { + return _id as any; + } + } + + /** + * Escapes special regex metacharacters to prevent ReDoS attacks. + * @param str - The string to escape + * @returns The escaped string safe for use in RegExp + */ + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } } From 7ae2c2e4f5f14fe6ae137d35f4d1b27b93097c51 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 6 Feb 2026 12:17:05 +0000 Subject: [PATCH 2/6] refactor: remove outdated escapeRegex method documentation --- .../data-access-objects/data-access-object-mongodb.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts index e8388b904..3adf63c21 100644 --- a/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts +++ b/shared-code/src/data-access-layer/data-access-objects/data-access-object-mongodb.ts @@ -649,11 +649,6 @@ export class DataAccessObjectMongo extends BasicDataAccessObject implements IDat } } - /** - * Escapes special regex metacharacters to prevent ReDoS attacks. - * @param str - The string to escape - * @returns The escaped string safe for use in RegExp - */ private escapeRegex(str: string): string { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } From 1acbbe5bb2c7b1b3963076899b5c2bc7251e9b58 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 6 Feb 2026 12:30:52 +0000 Subject: [PATCH 3/6] feat: enhance query normalization by removing comments and unnecessary whitespace --- .../saved-db-query/utils/check-query-is-safe.util.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/entities/visualizations/saved-db-query/utils/check-query-is-safe.util.ts b/backend/src/entities/visualizations/saved-db-query/utils/check-query-is-safe.util.ts index cf752072d..363ba0cd6 100644 --- a/backend/src/entities/visualizations/saved-db-query/utils/check-query-is-safe.util.ts +++ b/backend/src/entities/visualizations/saved-db-query/utils/check-query-is-safe.util.ts @@ -249,6 +249,8 @@ export function validateQuerySafety(query: string, connectionType: ConnectionTyp function normalizeQuery(query: string): string { return query + .replace(/\/\*!(\d+)?\s*([\s\S]*?)\*\//g, ' $2 ') + .replace(/#.*$/gm, '') .replace(/--.*$/gm, '') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\s+/g, ' ') From 3d4d3e1694df80d409663969cc3d2a17806f96fa Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 6 Feb 2026 13:25:56 +0000 Subject: [PATCH 4/6] turnstile disabled in selfhosted mode --- .../src/shared/services/turnstile.service.ts | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/backend/src/shared/services/turnstile.service.ts b/backend/src/shared/services/turnstile.service.ts index b3d402c35..42aebdc97 100644 --- a/backend/src/shared/services/turnstile.service.ts +++ b/backend/src/shared/services/turnstile.service.ts @@ -1,44 +1,45 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import axios from 'axios'; +import { isTest } from '../../helpers/app/is-test.js'; +import { isSaaS } from '../../helpers/app/is-saas.js'; interface TurnstileVerifyResponse { - success: boolean; - 'error-codes'?: string[]; - challenge_ts?: string; - hostname?: string; + success: boolean; + 'error-codes'?: string[]; + challenge_ts?: string; + hostname?: string; } @Injectable() export class TurnstileService { - private readonly verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; + private readonly verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'; - async verifyToken(token: string): Promise { - const secretKey = process.env.TURNSTILE_SECRET_KEY; + async verifyToken(token: string): Promise { + const secretKey = process.env.TURNSTILE_SECRET_KEY; - if (!secretKey) { - console.warn('Turnstile secret key is not configured. Skipping verification.'); - return true; - } + if (isTest() || !isSaaS()) { + return true; + } - if (!token || typeof token !== 'string') { - throw new BadRequestException('Turnstile token is required.'); - } + if (!token || typeof token !== 'string') { + throw new BadRequestException('Turnstile token is required.'); + } - const formData = new URLSearchParams(); - formData.append('secret', secretKey); - formData.append('response', token); + const formData = new URLSearchParams(); + formData.append('secret', secretKey); + formData.append('response', token); - const response = await axios.post(this.verifyUrl, formData.toString(), { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); + const response = await axios.post(this.verifyUrl, formData.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); - if (!response.data.success) { - const errorCodes = response.data['error-codes']?.join(', ') || 'Unknown error'; - throw new BadRequestException(`Turnstile verification failed: ${errorCodes}`); - } + if (!response.data.success) { + const errorCodes = response.data['error-codes']?.join(', ') || 'Unknown error'; + throw new BadRequestException(`Turnstile verification failed: ${errorCodes}`); + } - return true; - } + return true; + } } From 2d38f74bad99390b904243ab22f39c09befa3b86 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 6 Feb 2026 13:26:55 +0000 Subject: [PATCH 5/6] refactor: remove info log for connection token validation --- autoadmin-ws-server/src/services/token-validator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/autoadmin-ws-server/src/services/token-validator.ts b/autoadmin-ws-server/src/services/token-validator.ts index 5de38c3c7..1b22d34a6 100644 --- a/autoadmin-ws-server/src/services/token-validator.ts +++ b/autoadmin-ws-server/src/services/token-validator.ts @@ -21,7 +21,6 @@ export async function validateConnectionToken(connectionToken: string): Promise< try { const checkUrl = `${config.checkConnectionTokenUrl}?token=${connectionToken}`; - logger.info({ url: checkUrl }, 'Validating connection token'); const response = await fetch(checkUrl); From c15d3c5ed64d40a5d65b6416467feb70725ca693 Mon Sep 17 00:00:00 2001 From: Artem Niehrieiev Date: Fri, 6 Feb 2026 13:28:37 +0000 Subject: [PATCH 6/6] refactor: streamline bootstrap function and improve error handling --- backend/src/main.ts | 156 ++++++++++++++++++++++---------------------- 1 file changed, 77 insertions(+), 79 deletions(-) diff --git a/backend/src/main.ts b/backend/src/main.ts index 7f28022ba..72fc56809 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -15,91 +15,89 @@ import { Constants } from './helpers/constants/constants.js'; import { requiredEnvironmentVariablesValidator } from './helpers/validators/required-environment-variables.validator.js'; async function bootstrap() { - try { - requiredEnvironmentVariablesValidator(); - const appOptions: NestApplicationOptions = { - rawBody: true, - logger: new WinstonLogger(), - }; - - const app = await NestFactory.create(ApplicationModule, appOptions); - app.useLogger(app.get(WinstonLogger)); - app.set('query parser', 'extended'); - - Sentry.init({ - dsn: process.env.SENTRY_DSN, - tracesSampleRate: 1.0, - }); - - const globalPrefix = process.env.GLOBAL_PREFIX || '/'; - app.setGlobalPrefix(globalPrefix); - - app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); - - app.use(helmet()); - - app.use(cookieParser()); - - app.enableCors({ - origin: [ - 'https://app.autoadmin.org', - 'http://localhost:4200', - 'https://app.rocketadmin.org', - 'https://saas.rocketadmin.com', - 'https://app-beta.rocketadmin.com', - Constants.APP_DOMAIN_ADDRESS, - ], - methods: 'GET,PUT,PATCH,POST,DELETE', - credentials: true, - preflightContinue: false, - optionsSuccessStatus: 204, - }); - - app.use(cookieParser()); - - app.use(bodyParser.json({ limit: '10mb' })); - app.use(bodyParser.urlencoded({ limit: '10mb', extended: true })); - - const config = new DocumentBuilder() - .setTitle('Rocketadmin') - .setDescription('The Rocketadmin API description') - .setVersion('1.0') - .addTag('rocketadmin') - .setBasePath(globalPrefix) - .addApiKey({ - type: 'apiKey', - name: 'x-api-key', - in: 'header', - }) - .addCookieAuth(Constants.JWT_COOKIE_KEY_NAME) - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup('docs', app, document); - - app.useGlobalPipes( - new ValidationPipe({ - exceptionFactory(validationErrors: ValidationError[] = []) { - return new ValidationException(validationErrors); - }, - }), - ); - - await app.listen(3000); - } catch (e) { - console.error(`Failed to initialize, due to ${e}`); - process.exit(1); - } + try { + requiredEnvironmentVariablesValidator(); + const appOptions: NestApplicationOptions = { + rawBody: true, + logger: new WinstonLogger(), + }; + + const app = await NestFactory.create(ApplicationModule, appOptions); + app.useLogger(app.get(WinstonLogger)); + app.set('query parser', 'extended'); + + Sentry.init({ + dsn: process.env.SENTRY_DSN, + tracesSampleRate: 1.0, + }); + + const globalPrefix = process.env.GLOBAL_PREFIX || '/'; + app.setGlobalPrefix(globalPrefix); + + app.useGlobalFilters(new AllExceptionsFilter(app.get(WinstonLogger))); + + app.use(helmet()); + + app.enableCors({ + origin: [ + 'https://app.autoadmin.org', + 'http://localhost:4200', + 'https://app.rocketadmin.org', + 'https://saas.rocketadmin.com', + 'https://app-beta.rocketadmin.com', + Constants.APP_DOMAIN_ADDRESS, + ], + methods: 'GET,PUT,PATCH,POST,DELETE', + credentials: true, + preflightContinue: false, + optionsSuccessStatus: 204, + }); + + app.use(cookieParser()); + + app.use(bodyParser.json({ limit: '10mb' })); + app.use(bodyParser.urlencoded({ limit: '10mb', extended: true })); + + const config = new DocumentBuilder() + .setTitle('Rocketadmin') + .setDescription('The Rocketadmin API description') + .setVersion('1.0') + .addTag('rocketadmin') + .setBasePath(globalPrefix) + .addApiKey({ + type: 'apiKey', + name: 'x-api-key', + in: 'header', + }) + .addCookieAuth(Constants.JWT_COOKIE_KEY_NAME) + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); + + app.useGlobalPipes( + new ValidationPipe({ + exceptionFactory(validationErrors: ValidationError[] = []) { + return new ValidationException(validationErrors); + }, + }), + ); + + await app.listen(3000); + } catch (e) { + console.error(`Failed to initialize, due to ${e}`); + process.exit(1); + } } const temp = process.exit; process.exit = () => { - console.trace(); - process.exit = temp; - process.exit(); + console.trace(); + process.exit = temp; + process.exit(); }; bootstrap().catch((e) => { - console.error(`Bootstrap promise failed with error: ${e}`); - process.exit(1); + console.error(`Bootstrap promise failed with error: ${e}`); + process.exit(1); });