ToolJet/server/src/modules/data-sources/service.ts
Midhun G S 0c5ab3484c
Platform LTS Final fixes (#13221)
* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* Bugfixes/whitelabelling apis (#13180)

* white-labelling apis

* removed consoles logs

* reverts

* fixes for white-labelling

* fixes

* reverted breadcrumb changes (#13194)

* fixes for getting public sso configurations

* fix for enable signup on cloud

* Cloud Trial and Banners (#13182)

* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* Cloud Trial and Banners

* revert

* initial commit

* Added website onboarding APIs

* moved ai onboarding controller to auth module

* ee banners

* fix

---------

Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>
Co-authored-by: gsmithun4 <gsmithun4@gmail.com>

* Bugfixes/minor UI fixes-CLoud (#13203)

* Bugfixes/UI bugs platform 1 (#13205)

* cleanup

* Audit logs fix

* gitignore changes

* postgrest configs removed

* removed unused import

* improvements

* fix

* improved startup logs

* Platform cypress fix (#13192)

* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* Bugfixes/whitelabelling apis (#13180)

* white-labelling apis

* removed consoles logs

* reverts

* fixes for white-labelling

* fixes

* Cypress fix

* reverted breadcrumb changes (#13194)

* cypress fix

* title fix

* fixes for getting public sso configurations

---------

Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>
Co-authored-by: gsmithun4 <gsmithun4@gmail.com>

* deployment fix

* added interfaces and permissions

* Bugfixes/lts 3.6 branch 1 platform (#13238)

* fix

* Licensing Banners Fixes Cloud and EE (#13241)

* design: Adds license buttons to header

* Refactor header actions

* Cloud Blocker bugfixes (#13160)

* fix

* minor email fixes

* settings menu fix

* fixes

* subscription page

* fix banners

---------

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>

* fix for public apps

* fix

* CE Instance Signup bug (#13254)

* CE Instance Signup bug

* improvement

* fix

* Add WEBSITE_SIGNUP_URL to deployment environment variables

* Add WEBSITE_SIGNUP_URL to environment variables for deployment

* Super admin banner fix (#13262)

* Git Sync Fixes  (#13249)

* git-sync module changes

* git sync fixes

* added app resource guard

* git-sync fixes

* removed require feature

* fix

* review comment changes

* ypress fix

* App logo fix inside app builder

* fix for subpath cache

* fix (#13274)

* platform-cypress-fix (#13271)

* git sync fixes (#13277)

* fix

* Add data-cy for new components (#13289)

---------

Co-authored-by: Rohan Lahori <64496391+rohanlahori@users.noreply.github.com>
Co-authored-by: Rudhra Deep Biswas <98055396+rudeUltra@users.noreply.github.com>
Co-authored-by: Ajith KV <ajith.jaban@gmail.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: rohanlahori <rohanlahori99@gmail.com>
Co-authored-by: Adish M <adish.madhu@gmail.com>
Co-authored-by: Rudra deep Biswas <rudra21ultra@gmail.com>
2025-07-09 22:36:41 +05:30

277 lines
10 KiB
TypeScript

import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common';
import { DataSourcesRepository } from './repository';
import { DataSourcesUtilService } from './util.service';
import { User } from '@entities/user.entity';
import { AbilityService } from '@modules/ability/interfaces/IService';
import { MODULES } from '@modules/app/constants/modules';
import { decode } from 'js-base64';
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
import { decamelizeKeys } from 'humps';
import { DataSourceTypes } from './constants';
import {
AuthorizeDataSourceOauthDto,
CreateDataSourceDto,
GetDataSourceOauthUrlDto,
TestDataSourceDto,
TestSampleDataSourceDto,
UpdateDataSourceDto,
} from './dto';
import { GetQueryVariables, UpdateOptions } from './types';
import { DataSource } from '@entities/data_source.entity';
import { PluginsServiceSelector } from './services/plugin-selector.service';
import { IDataSourcesService } from './interfaces/IService';
import { RequestContext } from '@modules/request-context/service';
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
import * as fs from 'fs';
@Injectable()
export class DataSourcesService implements IDataSourcesService {
constructor(
protected readonly dataSourcesRepository: DataSourcesRepository,
protected readonly dataSourcesUtilService: DataSourcesUtilService,
protected readonly abilityService: AbilityService,
protected readonly appEnvironmentsUtilService: AppEnvironmentUtilService,
protected readonly pluginsServiceSelector: PluginsServiceSelector
) {}
async getForApp(query: GetQueryVariables, user: User): Promise<{ data_sources: object[] }> {
const userPermissions = await this.abilityService.resourceActionsPermission(user, {
resources: [{ resource: MODULES.GLOBAL_DATA_SOURCE }],
organizationId: user.organizationId,
});
const shouldIncludeWorkflows = query.shouldIncludeWorkflows ?? true;
let dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, query ?? {});
if (!shouldIncludeWorkflows) {
// remove workflowsdefault data source from static data sources
dataSources = dataSources.filter((dataSource) => dataSource.kind !== 'workflows');
}
const decamelizedDatasources = decamelizeKeys(dataSources);
return { data_sources: decamelizedDatasources };
}
async getAll(query: GetQueryVariables, user: User): Promise<{ data_sources: object[] }> {
const userPermissions = await this.abilityService.resourceActionsPermission(user, {
resources: [{ resource: MODULES.GLOBAL_DATA_SOURCE }],
organizationId: user.organizationId,
});
const selectedEnvironmentId =
query.environmentId || (await this.appEnvironmentsUtilService.get(user.organizationId, null, true))?.id;
const dataSources = await this.dataSourcesRepository.allGlobalDS(userPermissions, user.organizationId, {
appVersionId: query.appVersionId,
environmentId: selectedEnvironmentId,
types: [DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE],
});
for (const dataSource of dataSources) {
const parseIfNeeded = (data: any) => {
if (typeof data === 'object' && data !== null) return data;
if (Buffer.isBuffer(data) || typeof data === 'string') {
return JSON.parse(decode(data.toString('utf8')));
}
return data;
};
try {
if (dataSource.pluginId) {
if (Buffer.isBuffer(dataSource.plugin.iconFile.data)) {
dataSource.plugin.iconFile.data = dataSource.plugin.iconFile.data.toString('utf8');
}
dataSource.plugin.manifestFile.data = parseIfNeeded(dataSource.plugin.manifestFile.data);
dataSource.plugin.operationsFile.data = parseIfNeeded(dataSource.plugin.operationsFile.data);
}
} catch (error) {
throw new BadRequestException(
`Error parsing plugin data for dataSourceId: ${dataSource.id}. Details: ${error.message}`
);
}
}
const decamelizedDatasources = dataSources.map((dataSource) => {
if (dataSource.pluginId) {
return dataSource;
}
if (dataSource.kind === 'openapi') {
const { options, ...objExceptOptions } = dataSource;
const tempDs = decamelizeKeys(objExceptOptions);
const { spec, ...objExceptSpec } = options;
const decamelizedOptions = decamelizeKeys(objExceptSpec);
decamelizedOptions['spec'] = spec;
tempDs['options'] = decamelizedOptions;
return tempDs;
}
if (dataSource.type === DataSourceTypes.SAMPLE) {
delete dataSource.options;
}
return decamelizeKeys(dataSource);
});
return { data_sources: decamelizedDatasources };
}
async create(createDataSourceDto: CreateDataSourceDto, user: User): Promise<DataSource> {
const { kind, name, options, plugin_id: pluginId, environment_id } = createDataSourceDto;
if (kind === 'grpc') {
const rootDir = process.cwd().split('/').slice(0, -1).join('/');
const protoFilePath = `${rootDir}/protos/service.proto`;
const filecontent = fs.readFileSync(protoFilePath, 'utf8');
const rcps = await this.dataSourcesUtilService.getServiceAndRpcNames(filecontent);
options.find((option) => option['key'] === 'protobuf').value = JSON.stringify(rcps, null, 2);
}
const dataSource = await this.dataSourcesUtilService.create(
{
name,
kind,
options,
pluginId,
environmentId: environment_id,
},
user
);
// Setting data for audit logs
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: dataSource?.id,
resourceName: dataSource?.name,
metadata: dataSource,
});
return dataSource;
}
async update(updateDataSourceDto: UpdateDataSourceDto, user: User, updateOptions: UpdateOptions) {
const { name, options } = updateDataSourceDto;
const { dataSourceId, environmentId } = updateOptions;
await this.dataSourcesUtilService.update(dataSourceId, user.organizationId, name, options, environmentId);
// Setting data for audit logs
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: dataSourceId,
resourceName: name,
metadata: updateDataSourceDto,
});
return;
}
async decryptOptions(options: Record<string, any>) {
return await this.dataSourcesUtilService.decrypt(options);
}
async delete(dataSourceId: string, user: User) {
const dataSource = await this.dataSourcesRepository.findById(dataSourceId);
if (!dataSource) {
return;
}
if (dataSource.type === DataSourceTypes.SAMPLE) {
throw new BadRequestException('Cannot delete sample data source');
}
const result = await this.findQueriesLinkedToDatasource(dataSourceId);
if (result.dependent_queries) {
throw new BadRequestException(`Datasource can't be deleted, queries are in use`);
}
await this.dataSourcesRepository.delete(dataSourceId);
// Setting data for audit logs
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: dataSourceId,
resourceName: dataSource.name,
metadata: dataSource,
});
return;
}
async changeScope(dataSourceId: string, user: User) {
await this.dataSourcesRepository.convertToGlobalSource(dataSourceId, user.organizationId);
}
async findOneByEnvironment(
dataSourceId: string,
organizationId: string,
environmentId?: string
): Promise<DataSource> {
const dataSource = await this.dataSourcesUtilService.findOneByEnvironment(
dataSourceId,
organizationId,
environmentId
);
delete dataSource['dataSourceOptions'];
return dataSource;
}
async testConnection(testDataSourceDto: TestDataSourceDto, organization_id: string): Promise<object> {
return await this.dataSourcesUtilService.testConnection(testDataSourceDto, organization_id);
}
async testSampleDBConnection(testDataSourceDto: TestSampleDataSourceDto, user: User) {
const { environment_id, dataSourceId } = testDataSourceDto;
const dataSource = await this.dataSourcesUtilService.findOneByEnvironment(
dataSourceId,
user.defaultOrganizationId,
environment_id
);
testDataSourceDto.options = dataSource.options;
return await this.dataSourcesUtilService.testConnection(testDataSourceDto, user.organizationId);
}
async getAuthUrl(getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto): Promise<{ url: string }> {
return this.dataSourcesUtilService.getAuthUrl(getDataSourceOauthUrlDto);
}
async authorizeOauth2(
dataSourceId: string,
environmentId: string,
authorizeDataSourceOauthDto: AuthorizeDataSourceOauthDto,
user: User
) {
const { code } = authorizeDataSourceOauthDto;
const dataSource = await this.dataSourcesUtilService.findOneByEnvironment(dataSourceId, environmentId);
if (!dataSource) {
throw new UnauthorizedException();
}
// TODO: add privilege if user has data source privilege or user should have app read privilege of the apps using the data source
await this.dataSourcesUtilService.authorizeOauth2(dataSource, code, user.id, environmentId, user.organizationId);
return;
}
async findQueriesLinkedToDatasource(datasourceId: string) {
const dataSourceDetails = await this.dataSourcesRepository.getQueriesByDatasourceId(datasourceId);
if (dataSourceDetails.length == 0) return { datasources: 0, dependent_queries: 0 };
const queries = [];
dataSourceDetails.forEach((datasourceDetail) => {
const { dataQueries = [] } = datasourceDetail;
if (dataQueries.length) queries.push(...dataQueries);
});
return { datasources: dataSourceDetails.length, dependent_queries: queries.length };
}
async findDatasourcesAndQueriesOfMarketplacePlugin(pluginId: string) {
const dataSourcesByMarketplacePlugin = await this.dataSourcesRepository.getDatasourceByPluginId(pluginId);
if (!dataSourcesByMarketplacePlugin.length) return { dependent_queries: 0 };
const queries = [];
dataSourcesByMarketplacePlugin?.forEach((datasource) => {
if (datasource.dataQueries.length) queries.push(...datasource.dataQueries);
});
return {
dependent_queries: queries.length,
};
}
}