ToolJet/server/src/services/data_sources.service.ts
Midhun G S 487252c5ea
Feature - Multi Environment support (#4833)
* initial commit

* multi env changes

* multi-env changes

* entity fixes

* data query changes

* fix

* trying to avoid conflict with EE

* moved version creation to app creation function

* fixing some issues

* execution of data query

* revert options changes

* changed migration

* fixed some migration issues: testing migration

* multi env support

* app import export fix

* fixes

* migration fixes

* removed plugins from data query

* fixing some migration issues

* fixes

* remove console log

* fix

* front end api changes

* backward compatibility for app import

* Fixed a bug

* correcting some mistakes

* Added constraints and fixed some issues

* changes

* fix for data source listing

* fixing version operation issues

* remove kind from data query

* removed kind from data query

* fixes

* fixes

* fix for version creation

* migration fixes

* Fixed preview and run query issues

* Fix: new version and event query id issue

* fixed rest api oauth issue
- next test refresh token

* import export changes

* fixes for app import

* import fix

* added await for for loops

* fix

* fix for migration

* Fixed backend oauth-envId issue

* import export changes

* migration fixes

* fix

* fix

* fix for app import from 0.9.0

* test case fixes

* test case fixes

* making app name mandatory for import

* adding type for options

* fix: imported apps query linking issues

* review changes

* lint issue fixes

* added on delete cascade

Co-authored-by: Muhsin Shah <muhsinshah21@gmail.com>
2022-12-09 20:23:42 +05:30

418 lines
14 KiB
TypeScript

import allPlugins from '@tooljet/plugins/dist/server';
import { Injectable, NotImplementedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { EntityManager, getManager, Repository } from 'typeorm';
import { DataSource } from '../../src/entities/data_source.entity';
import { CredentialsService } from './credentials.service';
import { cleanObject, dbTransactionWrap } from 'src/helpers/utils.helper';
import { PluginsHelper } from '../helpers/plugins.helper';
import { AppEnvironmentService } from './app_environments.service';
import { App } from 'src/entities/app.entity';
import { AppEnvironment } from 'src/entities/app_environments.entity';
@Injectable()
export class DataSourcesService {
constructor(
private readonly pluginsHelper: PluginsHelper,
private credentialsService: CredentialsService,
private appEnvironmentService: AppEnvironmentService,
@InjectRepository(DataSource)
private dataSourcesRepository: Repository<DataSource>
) {}
async all(query: object): Promise<DataSource[]> {
const { app_version_id: appVersionId, environmentId }: any = query;
let selectedEnvironmentId = environmentId;
return await dbTransactionWrap(async (manager: EntityManager) => {
if (!environmentId) {
selectedEnvironmentId = (await this.appEnvironmentService.get(appVersionId, null, manager))?.id;
}
const result = await manager
.createQueryBuilder(DataSource, 'data_source')
.innerJoinAndSelect('data_source.dataSourceOptions', 'data_source_options')
.leftJoinAndSelect('data_source.plugin', 'plugin')
.leftJoinAndSelect('plugin.iconFile', 'iconFile')
.leftJoinAndSelect('plugin.manifestFile', 'manifestFile')
.leftJoinAndSelect('plugin.operationsFile', 'operationsFile')
.where('data_source_options.environmentId = :selectedEnvironmentId', { selectedEnvironmentId })
.andWhere('data_source.appVersionId = :appVersionId', { appVersionId })
.getMany();
//remove tokenData from restapi datasources
const dataSources = result
?.map((ds) => {
if (ds.kind === 'restapi') {
const options = {};
Object.keys(ds.dataSourceOptions?.[0]?.options || {}).filter((key) => {
if (key !== 'tokenData') {
return (options[key] = ds.dataSourceOptions[0].options[key]);
}
});
ds.options = options;
} else {
ds.options = { ...(ds.dataSourceOptions?.[0]?.options || {}) };
}
delete ds['dataSourceOptions'];
return ds;
})
?.filter((ds) => ds.kind !== 'restapidefault' && ds.kind !== 'runjsdefault');
return dataSources;
});
}
async findOne(dataSourceId: string): Promise<DataSource> {
return await this.dataSourcesRepository.findOneOrFail({
where: { id: dataSourceId },
relations: ['plugin', 'apps', 'dataSourceOptions'],
});
}
async findOneByEnvironment(dataSourceId: string, environmentId?: string): Promise<DataSource> {
const dataSource = await this.dataSourcesRepository.findOneOrFail({
where: { id: dataSourceId },
relations: ['plugin', 'apps', 'dataSourceOptions'],
});
if (environmentId) {
dataSource.options = await this.appEnvironmentService.getOptions(dataSourceId, null, environmentId);
} else {
dataSource.options = dataSource.dataSourceOptions?.[0]?.options || {};
}
return dataSource;
}
async findApp(dataSourceId: string, manager: EntityManager): Promise<App> {
return (
await manager
.createQueryBuilder(DataSource, 'data_source')
.innerJoinAndSelect('data_source.apps', 'apps')
.where('data_source.id = :dataSourceId', { dataSourceId })
.getOneOrFail()
).app;
}
async findDatasourceByKind(kind: string, environmentId: string) {
return await dbTransactionWrap(async (manager: EntityManager) => {
const currentEnv = environmentId
? await manager.findOneOrFail(AppEnvironment, { where: { id: environmentId } })
: await manager.findOneOrFail(AppEnvironment, { where: { isDefault: true } });
return await this.dataSourcesRepository.findOneOrFail({
where: { kind, appVersionId: currentEnv.appVersionId },
relations: ['plugin', 'apps'],
});
});
}
async findDefaultDataSource(
kind: string,
appVersionId: string,
pluginId: string,
manager: EntityManager
): Promise<DataSource> {
const defaultDataSource = await manager.findOne(DataSource, {
where: { kind: `${kind}default`, appVersionId },
});
if (defaultDataSource) {
return defaultDataSource;
}
const dataSource = await this.createDefaultDataSource(kind, appVersionId, pluginId, manager);
await this.appEnvironmentService.createDataSourceInAllEnvironments(appVersionId, dataSource.id, manager);
return dataSource;
}
async createDefaultDataSource(
kind: string,
appVersionId: string,
pluginId: string,
manager?: EntityManager
): Promise<DataSource> {
const newDataSource = manager.create(DataSource, {
name: `${kind}default`,
kind: `${kind}default`,
appVersionId,
pluginId,
createdAt: new Date(),
updatedAt: new Date(),
});
return await manager.save(newDataSource);
}
async create(
name: string,
kind: string,
options: Array<object>,
appVersionId: string,
pluginId?: string,
environmentId?: string
): Promise<DataSource> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const newDataSource = manager.create(DataSource, {
name,
kind,
appVersionId,
pluginId,
createdAt: new Date(),
updatedAt: new Date(),
});
const dataSource = await manager.save(newDataSource);
// Creating empty options mapping
await this.appEnvironmentService.createDataSourceInAllEnvironments(appVersionId, dataSource.id, manager);
// Find the environment to be updated
const envToUpdate = await this.appEnvironmentService.get(appVersionId, environmentId, manager);
await this.appEnvironmentService.updateOptions(
await this.parseOptionsForCreate(options, false, manager),
envToUpdate.id,
dataSource.id,
manager
);
// Find other environments to be updated
const allEnvs = await this.appEnvironmentService.getAll(appVersionId, manager);
if (allEnvs?.length) {
const envsToUpdate = allEnvs.filter((env) => env.id !== envToUpdate.id);
await Promise.all(
envsToUpdate?.map(async (env) => {
await this.appEnvironmentService.updateOptions(
await this.parseOptionsForCreate(options, true, manager),
env.id,
dataSource.id,
manager
);
})
);
}
return dataSource;
});
}
async update(dataSourceId: string, name: string, options: Array<object>, environmentId?: string): Promise<void> {
const dataSource = await this.findOne(dataSourceId);
await dbTransactionWrap(async (manager: EntityManager) => {
const envToUpdate = await this.appEnvironmentService.get(dataSource.appVersionId, environmentId, manager);
// if datasource is restapi then reset the token data
if (dataSource.kind === 'restapi')
options.push({
key: 'tokenData',
value: undefined,
encrypted: false,
});
dataSource.options = await this.appEnvironmentService.getOptions(dataSourceId, null, envToUpdate.id);
await this.appEnvironmentService.updateOptions(
await this.parseOptionsForUpdate(dataSource, options),
envToUpdate.id,
dataSource.id,
manager
);
const updatableParams = {
id: dataSourceId,
name,
updatedAt: new Date(),
};
// Remove keys with undefined values
cleanObject(updatableParams);
await manager.save(DataSource, updatableParams);
});
}
async delete(dataSourceId: string) {
return await this.dataSourcesRepository.delete(dataSourceId);
}
/* This function merges new options with the existing options */
async updateOptions(dataSourceId: string, optionsToMerge: any, environmentId?: string): Promise<void> {
await dbTransactionWrap(async (manager: EntityManager) => {
const dataSource = await manager.findOneOrFail(DataSource, dataSourceId, { relations: ['dataSourceOptions'] });
const parsedOptions = await this.parseOptionsForUpdate(dataSource, optionsToMerge);
const envToUpdate = await this.appEnvironmentService.get(dataSource.appVersionId, environmentId, manager);
const oldOptions = dataSource.dataSourceOptions?.[0]?.options || {};
const updatedOptions = { ...oldOptions, ...parsedOptions };
await this.appEnvironmentService.updateOptions(updatedOptions, envToUpdate.id, dataSourceId, manager);
});
}
async testConnection(kind: string, options: object, plugin_id: string): Promise<object> {
let result = {};
try {
const sourceOptions = {};
for (const key of Object.keys(options)) {
sourceOptions[key] = options[key]['value'];
}
const service = await this.pluginsHelper.getService(plugin_id, kind);
if (!service?.testConnection) {
throw new NotImplementedException('testConnection method not implemented');
}
result = await service.testConnection(sourceOptions);
} catch (error) {
result = {
status: 'failed',
message: error.message,
};
}
return result;
}
async parseOptionsForOauthDataSource(options: Array<object>) {
const findOption = (opts: any[], key: string) => opts.find((opt) => opt['key'] === key);
if (findOption(options, 'oauth2') && findOption(options, 'code')) {
const provider = findOption(options, 'provider')['value'];
const authCode = findOption(options, 'code')['value'];
const queryService = new allPlugins[provider]();
const accessDetails = await queryService.accessDetailsFrom(authCode, options);
for (const row of accessDetails) {
const option = {};
option['key'] = row[0];
option['value'] = row[1];
option['encrypted'] = true;
options.push(option);
}
options = options.filter((option) => !['provider', 'code', 'oauth2'].includes(option['key']));
}
return options;
}
async parseOptionsForCreate(options: Array<object>, resetSecureData = false, entityManager = getManager()) {
if (!options) return {};
const optionsWithOauth = await this.parseOptionsForOauthDataSource(options);
const parsedOptions = {};
for (const option of optionsWithOauth) {
if (option['encrypted']) {
const credential = await this.credentialsService.create(
resetSecureData ? '' : option['value'] || '',
entityManager
);
parsedOptions[option['key']] = {
credential_id: credential.id,
encrypted: option['encrypted'],
};
} else {
parsedOptions[option['key']] = {
value: option['value'],
encrypted: false,
};
}
}
return parsedOptions;
}
async parseOptionsForUpdate(dataSource: DataSource, options: Array<object>, entityManager = getManager()) {
if (!options) return {};
const optionsWithOauth = await this.parseOptionsForOauthDataSource(options);
const parsedOptions = {};
for (const option of optionsWithOauth) {
if (option['encrypted']) {
const existingCredentialId =
dataSource.options[option['key']] && dataSource.options[option['key']]['credential_id'];
if (existingCredentialId) {
await this.credentialsService.update(existingCredentialId, option['value'] || '');
parsedOptions[option['key']] = {
credential_id: existingCredentialId,
encrypted: option['encrypted'],
};
} else {
const credential = await this.credentialsService.create(option['value'] || '', entityManager);
parsedOptions[option['key']] = {
credential_id: credential.id,
encrypted: option['encrypted'],
};
}
} else {
parsedOptions[option['key']] = {
value: option['value'],
encrypted: false,
};
}
}
return parsedOptions;
}
private changeCurrentToken = (
tokenData: any,
userId: string,
accessTokenDetails: any,
isMultiAuthEnabled: boolean
) => {
if (isMultiAuthEnabled) {
return tokenData?.value.map((token: any) => {
if (token.user_id === userId) {
return { ...token, ...accessTokenDetails };
}
return token;
});
} else {
return accessTokenDetails;
}
};
async updateOAuthAccessToken(
accessTokenDetails: object,
dataSourceOptions: object,
dataSourceId: string,
userId: string,
environmentId?: string
) {
const existingAccessTokenCredentialId =
dataSourceOptions['access_token'] && dataSourceOptions['access_token']['credential_id'];
const existingRefreshTokenCredentialId =
dataSourceOptions['refresh_token'] && dataSourceOptions['refresh_token']['credential_id'];
if (existingAccessTokenCredentialId) {
await this.credentialsService.update(existingAccessTokenCredentialId, accessTokenDetails['access_token']);
existingRefreshTokenCredentialId &&
accessTokenDetails['refresh_token'] &&
(await this.credentialsService.update(existingRefreshTokenCredentialId, accessTokenDetails['refresh_token']));
} else if (dataSourceId) {
const isMultiAuthEnabled = dataSourceOptions['multiple_auth_enabled']?.value;
const updatedTokenData = this.changeCurrentToken(
dataSourceOptions['tokenData'],
userId,
accessTokenDetails,
isMultiAuthEnabled
);
const tokenOptions = [
{
key: 'tokenData',
value: updatedTokenData,
encrypted: false,
},
];
await this.updateOptions(dataSourceId, tokenOptions, environmentId);
}
}
getAuthUrl(provider: string, sourceOptions?: any): { url: string } {
const service = new allPlugins[provider]();
return { url: service.authUrl(sourceOptions) };
}
}