UNPKG

@tmlmobilidade/connectors

Version:

This package provides pre-made database connectors to streamline development and reduce boilerplate. By using these connectors, you can avoid re-implementing controller classes every time, ensuring consistency and saving development time.

215 lines (214 loc) • 9.11 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /* * */ import { BasicAuth, Trino } from 'trino-client'; /* * */ export class TrinoConnector { catalog; client; schema; constructor(options) { this.catalog = options.catalog; this.schema = options.schema; this.client = Trino.create({ auth: new BasicAuth(options.user, options.password), server: options.host, }); } /** * Constructs an ORDER BY clause from the given options. */ buildOrderByClause(orderBy) { if (!orderBy) return ''; return `ORDER BY ${orderBy.field} ${orderBy.direction}`; } /** * Constructs a WHERE clause from the given filter object, escaping values to prevent SQL injection. */ buildWhereClause(where) { if (!where) return ''; const conditions = Object.entries(where).map(([key, value]) => { if (key === '$and' && Array.isArray(value)) { const andConditions = value.map((condition) => { // Handle each condition without adding 'WHERE' const conditionKey = Object.keys(condition)[0]; const conditionValue = condition[conditionKey]; return `${conditionKey} = ${this.formatValue(conditionValue)}`; }).join(' AND '); return `(${andConditions})`; } if (key === '$or' && Array.isArray(value)) { const orConditions = value.map((condition) => { // Handle each condition without adding 'WHERE' const conditionKey = Object.keys(condition)[0]; const conditionValue = condition[conditionKey]; return `${conditionKey} = ${this.formatValue(conditionValue)}`; }).join(' OR '); return `(${orConditions})`; } if (key === '$not' && Array.isArray(value)) { const notConditions = value.map((condition) => { // Handle each condition without adding 'WHERE' const conditionKey = Object.keys(condition)[0]; const conditionValue = condition[conditionKey]; return `${conditionKey} = ${this.formatValue(conditionValue)}`; }).join(' AND '); return `NOT (${notConditions})`; } if (key === '$nor' && Array.isArray(value)) { const norConditions = value.map((condition) => { // Handle each condition without adding 'WHERE' const conditionKey = Object.keys(condition)[0]; const conditionValue = condition[conditionKey]; return `${conditionKey} = ${this.formatValue(conditionValue)}`; }).join(' OR '); return `NOT (${norConditions})`; } // Handle field-specific conditions if (typeof value === 'object' && value !== null && !Array.isArray(value)) { // Handle special field operators const subConditions = Object.entries(value).map(([operator, val]) => { switch (operator) { case '$eq': return `${key} = ${this.formatValue(val)}`; case '$gt': return `${key} > ${this.formatValue(val)}`; case '$gte': return `${key} >= ${this.formatValue(val)}`; case '$in': { const inValues = Array.isArray(val) ? val.map(v => this.formatValue(v)).join(', ') : this.formatValue(val); return `${key} IN (${inValues})`; } case '$lt': return `${key} < ${this.formatValue(val)}`; case '$lte': return `${key} <= ${this.formatValue(val)}`; case '$ne': return `${key} != ${this.formatValue(val)}`; case '$nin': { const ninValues = Array.isArray(val) ? val.map(v => this.formatValue(v)).join(', ') : this.formatValue(val); return `${key} NOT IN (${ninValues})`; } default: throw new Error(`Unsupported query operator: ${operator}`); } }); return subConditions.join(' AND '); } else { // Handle standard equality return `${key} = ${this.formatValue(value)}`; } }); return `WHERE ${conditions.join(' AND ')}`; } /** * Converts query result iterators to an array of objects using column headers. */ async convertIteratorToObject(headers, resultsIterator) { const results = []; for (let result = await resultsIterator.next(); !result.done; result = await resultsIterator.next()) { result.value?.data?.forEach((row) => { const rowObj = headers.reduce((acc, header, index) => { acc[header] = row[index]; return acc; }, {}); results.push(rowObj); }); } return results; } /** * Counts the number of rows matching the conditions. */ async count(table, options) { const whereClause = this.buildWhereClause(options?.where); const sql = `SELECT COUNT(*) FROM ${table} ${whereClause}`; const results = await this.executeQuery(sql); return (await results.next()).value?.data[0][0] || undefined; } /** * Executes the SQL query and returns an AsyncIterator. */ async executeQuery(sql) { try { return await this.client.query({ catalog: this.catalog, query: sql, schema: this.schema, }); } catch (error) { console.error(`Error executing query: ${sql}`, error); throw new Error(`Failed to execute query: ${error.message}`); } } /** * Finds the first row matching the conditions. */ async findFirst(table, options) { const whereClause = this.buildWhereClause(options.where); const orderByClause = this.buildOrderByClause(options.orderBy); const sql = `SELECT * FROM ${table} ${whereClause} ${orderByClause} LIMIT 1`; const results = await this.fetchResults(sql, table); return results[0] || null; } /** * Finds multiple rows matching the conditions. */ async findMany(table, options) { const whereClause = this.buildWhereClause(options?.where); const orderByClause = this.buildOrderByClause(options?.orderBy); const sql = `SELECT ${options?.unique ? 'DISTINCT' : 'ALL'} * FROM ${table} ${whereClause} ${orderByClause} ${options?.limit ? `LIMIT ${options.limit}` : ''}`; return await this.fetchResults(sql, table); } /** * Finds a single unique row based on the given conditions. */ async findUnique(table, options) { const whereClause = this.buildWhereClause(options.where); const orderByClause = this.buildOrderByClause(options.orderBy); const sql = `SELECT * FROM ${table} ${whereClause} ${orderByClause} LIMIT 1`; const results = await this.fetchResults(sql, table); if (results.length > 1) throw new Error(`Expected 1 result, got ${results.length}`); return results[0] || null; } /** * Generic method for fetching query results based on options. */ async fetchResults(sql, table) { const headers = await this.getColumnHeaders(table); const resultsIterator = await this.executeQuery(sql); return await this.convertIteratorToObject(headers, resultsIterator); } /** * Utility method to format values for SQL. */ formatValue(value) { if (typeof value === 'string') { return `'${value.replace(/'/g, '\'\'')}'`; // Escape single quotes in strings } else if (value instanceof Date) { return `'${value.toISOString()}'`; // Format date as ISO string } else { return value; // Assume number or boolean } } /** * Fetches column headers for the given table. */ async getColumnHeaders(table) { const sqlHeaders = `DESCRIBE ${table}`; const headersIterator = await this.executeQuery(sqlHeaders); const columnHeaders = []; for (let result = await headersIterator.next(); !result.done; result = await headersIterator.next()) { result.value?.data?.forEach(value => columnHeaders.push(value[0])); } return columnHeaders; } }