@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
JavaScript
/* 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;
}
}