ToolJet/plugins/packages/postgresql/lib/index.ts
Johnson Cherian 97fb315019
Appbuilder sprint 5 merge main (#11573)
* statement timeout for postgresql mssql mysql can now be configured from env

* Enhance: Add clear all, cancel & apply functionality to TJDB filter-popup (#2344)

* Update filter inputs

* Update filter body

* Style fixes

* Handle invalid filters

* Fix bugs

* Feature: Add list all sheets and create new spreadsheet operation to GoogleSheets (#2370)

* Add create spreadsheet functionality

* Add list all sheets functionality

* Updated delete and batch-update methods

* Change interface to type

* Enhance: Add read documentation link with data source drop down in query builder (#2162)

* Add read-documentation link

* Change copywriting for datasources name

* Update new component for link

* Increase input width

Update redshift link

* Fix the datasource name issue

* Enhance: Improve error handling in Google-Sheets run query (#2286)

* Add error details

* Display error for incorrect JSON

* Throw error for empty spreadsheetid and operator

* Enhance: Error handling for PgSql, MySql and MsSql (#2389)

* Enhance error handling for pgsql

* Enhance error handling for mysql

* Remove console logs

* Enhance error handling for MsSql

* Refactor error handling for consistency

* Enhance: Rest api body to accept raw input instead of raw json. (#2249)

* Enhanced rest api body to accept raw input instead of raw json.

* Changed content type from application/json to text/json and changed copywrite to RAW.

* Changed rest api body toggle label from 'RAW' to 'Raw'.

* Added request label for static REST API data source.

* Fixed issue where GET query failed since body was undefined.

* Integrated json_body to add backward compatibility.

* Removed console logs.

* Added support for 'text/json' type in checkIfContentTypeIsJson function.

* Made changes according to new frontend architecture in v3.

* Fixed request URL field overflow issue.

---------

Co-authored-by: Akshay Sasidharan <akshaysasidharan93@gmail.com>

* Feature: Add SSL support to MongoDB datasource (#2430)

* Add TLS support inputs in frontend

* Add backend logic for TLS support

* Add TLS inputs types

* Update TLS label

* Change ssl_certificate to tls_certificate

* Update the file handling in tls

* Update connection logic

* Fix unlinking file issue

* Remove catch block for unlinking file

* Handle tls certs directly

* Feature: Stripe plugin UI fixes with OpenAPI endpoint as source (#2725)

* Fixed GET and DELETE request input renders and added all UX fixes (#9498)

* fixed get and delete request input renders and added all UX fixes

* Extracted Stripe plugin component as a separate component for Dynamic Form.

* Resolved PR review comments and fixed issue where rendering path input field crashed the app.

* Changed param name underline to dashed for tooltip and revamped input field clear button

* Fixed a few sonarlint issues.

* Removed the duplicate code by creating a separate function named RenderParameterFields.

* Refactored computeOperationSelectionOptions function to not nest functions more than 4 levels deep.

* Refactored RenderParameterFields function to reduce its Cognitive Complexity.

* Made span tag with the 'button' interactive role focusable.

* Inside switch case for codehinter in DynamicForm > getElementProps, Extracted nested ternary operation for theme into an independent statement.

* Added keyboard listener to the clear button.

* Removed opacity from select dropdown and operation from operation select dropdown UI.

* Fixed syntax error in clearButton function.

* Removed the package @nrwl/nx-linux-x64-gnu from marketplace dependencies.

---------

Co-authored-by: Mansukh Kaur <mansukhkaur@Mansukhs-MacBook-Pro.local>
Co-authored-by: Devanshu Rastogi <devanshu.rastogi05@gmail.com>

* Made changes according to new app builder architecture in v3.

* Fixed import issue for codehintor

* Fixed issue where due to incorrect value, stripe queries crashed the entire app.

---------

Co-authored-by: Mansukh Kaur <mansukhkaur@Mansukhs-MacBook-Pro.local>
Co-authored-by: Devanshu Rastogi <devanshu.rastogi05@gmail.com>

* Enhance: Improve error handling in Airtable run query (#2234)

* Refactor error handling to streamline QueryError messages

* Handle 404 errors

* Enhance: Add AI-tag to datasources in marketplace page (#2597)

* Add AI-tag to datasources in marketplace page

* AI BANNER Tag

* margin

* Refactor tag rendering logic for marketplace page

* Refactor tag rendering for datasource page

* Refactor import and props

* Remove ai prefix and fix indentations

* Make custom hook for fetching plugins.json

* Add AI tag on installed page

* Marketplace page UI fix and add a plugin button fixes

* Add AI tag on datasource connection form

Change classname

* Fix svg flickering

* Fix svg chipping issue

* Push AI tag to extreme right

---------

Co-authored-by: Rudra <rudra21ultra@gmail.com>

* Fix: Api call for token generation in client-credentials grant type (#2785)

* fix: query kind for select source in rest api

* Fix: ToolJet database limit check API issue (#11416)

* bump to v3.0.5-ce

* Added data-cy for newly added components (#11435)

* Modified failed Platform cypress test cases for Tooljet V3 (#11486)

* Modify platform cypress test cases

* Added cypress test cases for user onboarding flow (#11499)

* Add data-cy for newly added components

* Add data-cy for onboarding page elements

* Modify failed test cases

* Adding more cases

* Modify onboarding test cases

* Modify user invite flow

* chnages on onboarding test scripts

* revert the changes

* revert the changes

* removed .only form profile file

* resolved review changes

---------

Co-authored-by: ajith-k-v <ajith.jaban@gmail.com>
Co-authored-by: Sri mani Teja s <mani@Sris-MacBook-Pro-4.local>

* fix: Fixes broken loading state for container

* update cypress workflow for subpath

* Add data-cy for workspace constants components (#11530)

* fix table down load event not showing up

* Hotfix: The build failed to include the reference file for custom validation of the ToolJet database schema. (#11490)

* tooljet database schema custom validation reference file was not included in the build

* fix: missed a dependency for copyfiles

* change version to 3.1.0

---------

Co-authored-by: Ganesh Kumar <ganesh8056234@gmail.com>
Co-authored-by: Parth <108089718+parthy007@users.noreply.github.com>
Co-authored-by: Devanshu Rastogi <devanshu.rastogi05@gmail.com>
Co-authored-by: Akshay Sasidharan <akshaysasidharan93@gmail.com>
Co-authored-by: Mansukh Kaur <mansukhkaur@Mansukhs-MacBook-Pro.local>
Co-authored-by: Rudra <rudra21ultra@gmail.com>
Co-authored-by: Vijaykant Yadav <vjy239@gmail.com>
Co-authored-by: Ganesh Kumar <40178541+ganesh8056@users.noreply.github.com>
Co-authored-by: Ajith KV <ajith.jaban@gmail.com>
Co-authored-by: Adish M <44204658+adishM98@users.noreply.github.com>
Co-authored-by: Srimanitejas123 <mani@tooljet.com>
Co-authored-by: Sri mani Teja s <mani@Sris-MacBook-Pro-4.local>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Kartik Gupta <gupta.kartik18kg@gmail.com>
2024-12-10 12:21:34 +05:30

180 lines
6.4 KiB
TypeScript

import {
ConnectionTestResult,
cacheConnection,
getCachedConnection,
QueryService,
QueryResult,
QueryError,
} from '@tooljet-plugins/common';
import { SourceOptions, QueryOptions } from './types';
import knex, { Knex } from 'knex';
import { isEmpty } from '@tooljet-plugins/common';
export default class PostgresqlQueryService implements QueryService {
private static _instance: PostgresqlQueryService;
private STATEMENT_TIMEOUT;
constructor() {
this.STATEMENT_TIMEOUT =
process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT && !isNaN(Number(process.env?.PLUGINS_SQL_DB_STATEMENT_TIMEOUT))
? Number(process.env.PLUGINS_SQL_DB_STATEMENT_TIMEOUT)
: 120000;
if (PostgresqlQueryService._instance) {
return PostgresqlQueryService._instance;
}
PostgresqlQueryService._instance = this;
return PostgresqlQueryService._instance;
}
async run(
sourceOptions: SourceOptions,
queryOptions: QueryOptions,
dataSourceId: string,
dataSourceUpdatedAt: string
): Promise<QueryResult> {
try {
const knexInstance = await this.getConnection(sourceOptions, {}, true, dataSourceId, dataSourceUpdatedAt);
switch (queryOptions.mode) {
case 'sql':
return await this.handleRawQuery(knexInstance, queryOptions);
case 'gui':
return await this.handleGuiQuery(knexInstance, queryOptions);
default:
throw new Error("Invalid query mode. Must be either 'sql' or 'gui'.");
}
} catch (err) {
const errorMessage = err.message || 'An unknown error occurred';
const errorDetails: any = {};
if (err && err instanceof Error) {
const postgresError = err as any;
const { code, detail, hint, routine } = postgresError;
errorDetails.code = code || null;
errorDetails.detail = detail || null;
errorDetails.hint = hint || null;
errorDetails.routine = routine || null;
}
throw new QueryError('Query could not be completed', errorMessage, errorDetails);
}
}
async testConnection(sourceOptions: SourceOptions): Promise<ConnectionTestResult> {
const knexInstance = await this.getConnection(sourceOptions, {}, false);
await knexInstance.raw('SELECT version();').timeout(this.STATEMENT_TIMEOUT);
return { status: 'ok' };
}
private async handleGuiQuery(knexInstance: Knex, queryOptions: QueryOptions): Promise<any> {
if (queryOptions.operation !== 'bulk_update_pkey') {
return { rows: [] };
}
const query = this.buildBulkUpdateQuery(queryOptions);
return await this.executeQuery(knexInstance, query);
}
private async handleRawQuery(knexInstance: Knex, queryOptions: QueryOptions): Promise<QueryResult> {
const { query, query_params } = queryOptions;
const queryParams = query_params || [];
const sanitizedQueryParams: Record<string, any> = Object.fromEntries(queryParams.filter(([key]) => !isEmpty(key)));
const result = await this.executeQuery(knexInstance, query, sanitizedQueryParams);
return { status: 'ok', data: result };
}
private async executeQuery(knexInstance: Knex, query: string, sanitizedQueryParams: Record<string, any> = {}) {
if (isEmpty(query)) throw new Error('Query is empty');
const { rows } = await knexInstance.raw(query, sanitizedQueryParams);
return rows;
}
private connectionOptions(sourceOptions: SourceOptions) {
const _connectionOptions = (sourceOptions.connection_options || []).filter((o) => o.some((e) => !isEmpty(e)));
const connectionOptions = Object.fromEntries(_connectionOptions);
Object.keys(connectionOptions).forEach((key) =>
connectionOptions[key] === '' ? delete connectionOptions[key] : {}
);
return connectionOptions;
}
private async buildConnection(sourceOptions: SourceOptions): Promise<Knex> {
let connectionConfig;
if (sourceOptions.connection_type === 'manual') {
connectionConfig = {
user: sourceOptions.username,
host: sourceOptions.host,
database: sourceOptions.database,
password: sourceOptions.password,
port: sourceOptions.port,
ssl: this.getSslConfig(sourceOptions),
statement_timeout: this.STATEMENT_TIMEOUT,
};
} else if (sourceOptions.connection_type === 'string' && sourceOptions.connection_string) {
connectionConfig = {
connectionString: sourceOptions.connection_string,
ssl: this.getSslConfig(sourceOptions),
};
}
const connectionOptions: Knex.Config = {
client: 'pg',
connection: connectionConfig,
pool: { min: 0, max: 10, acquireTimeoutMillis: 10000 },
acquireConnectionTimeout: 60000,
...this.connectionOptions(sourceOptions),
};
return knex(connectionOptions);
}
private getSslConfig(sourceOptions: SourceOptions) {
if (!sourceOptions.ssl_enabled) return undefined;
return {
rejectUnauthorized: (sourceOptions.ssl_certificate ?? 'none') !== 'none',
ca: sourceOptions.ssl_certificate === 'ca_certificate' ? sourceOptions.ca_cert : undefined,
key: sourceOptions.ssl_certificate === 'self_signed' ? sourceOptions.client_key : undefined,
cert: sourceOptions.ssl_certificate === 'self_signed' ? sourceOptions.client_cert : undefined,
};
}
async getConnection(
sourceOptions: SourceOptions,
options: any,
checkCache: boolean,
dataSourceId?: string,
dataSourceUpdatedAt?: string
): Promise<Knex> {
if (checkCache) {
const cachedConnection = await getCachedConnection(dataSourceId, dataSourceUpdatedAt);
if (cachedConnection) return cachedConnection;
}
const connection = await this.buildConnection(sourceOptions);
if (checkCache && dataSourceId) cacheConnection(dataSourceId, connection);
return connection;
}
buildBulkUpdateQuery(queryOptions: QueryOptions): string {
let queryText = '';
const { table: tableName, primary_key_column: primaryKey, records } = queryOptions;
for (const record of records) {
const primaryKeyValue = typeof record[primaryKey] === 'string' ? `'${record[primaryKey]}'` : record[primaryKey];
queryText = `${queryText} UPDATE ${tableName} SET`;
for (const key of Object.keys(record)) {
if (key !== primaryKey) {
queryText = ` ${queryText} ${key} = ${record[key] === null ? null : `'${record[key]}'`},`;
}
}
queryText = queryText.slice(0, -1);
queryText = `${queryText} WHERE ${primaryKey} = ${primaryKeyValue};`;
}
return queryText.trim();
}
}