ToolJet/server/src/modules/apps/service.ts

374 lines
14 KiB
TypeScript

import { User } from '@entities/user.entity';
import { dbTransactionWrap } from '@helpers/database.helper';
import {
BadRequestException,
ForbiddenException,
HttpException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { EntityManager } from 'typeorm';
import {
AppCreateDto,
AppListDto,
AppUpdateDto,
ValidateAppAccessDto,
ValidateAppAccessResponseDto,
VersionReleaseDto,
} from './dto';
import { FEATURE_KEY } from './constants';
import { camelizeKeys, decamelizeKeys } from 'humps';
import { App } from '@entities/app.entity';
import { AppsUtilService } from './util.service';
import { LicenseTermsService } from '@modules/licensing/interfaces/IService';
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
import { plainToClass } from 'class-transformer';
import { AppAbility } from '@modules/app/decorators/ability.decorator';
import { VersionRepository } from '@modules/versions/repository';
import { AppsRepository } from './repository';
import { FoldersUtilService } from '@modules/folders/util.service';
import { FolderAppsUtilService } from '@modules/folder-apps/util.service';
import { PageService } from './services/page.service';
import { EventsService } from './services/event.service';
import { LICENSE_FIELD } from '@modules/licensing/constants';
import { AppEnvironment } from '@entities/app_environments.entity';
import { OrganizationThemesUtilService } from '@modules/organization-themes/util.service';
import { IAppsService } from './interfaces/IService';
import { AiUtilService } from '@modules/ai/util.service';
import { RequestContext } from '@modules/request-context/service';
import { AUDIT_LOGS_REQUEST_CONTEXT_KEY } from '@modules/app/constants';
@Injectable()
export class AppsService implements IAppsService {
constructor(
protected readonly appsUtilService: AppsUtilService,
protected readonly licenseTermsService: LicenseTermsService,
protected readonly appEnvironmentUtilService: AppEnvironmentUtilService,
protected readonly versionRepository: VersionRepository,
protected readonly appRepository: AppsRepository,
protected readonly foldersUtilService: FoldersUtilService,
protected readonly folderAppsUtilService: FolderAppsUtilService,
protected readonly pageService: PageService,
protected readonly eventService: EventsService,
protected readonly organizationThemeUtilService: OrganizationThemesUtilService,
protected readonly aiUtilService: AiUtilService
) { }
async create(user: User, appCreateDto: AppCreateDto) {
const { name, icon, type } = appCreateDto;
return await dbTransactionWrap(async (manager: EntityManager) => {
const app = await this.appsUtilService.create(name, user, type, manager);
const appUpdateDto = new AppUpdateDto();
appUpdateDto.name = name;
appUpdateDto.slug = app.id;
appUpdateDto.icon = icon;
await this.appsUtilService.update(app, appUpdateDto, null, manager);
// Setting data for audit logs
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: app.id,
resourceName: app.name,
});
return decamelizeKeys(app);
});
}
async validatePrivateAppAccess(app: App, ability: AppAbility, validateAppAccessDto: ValidateAppAccessDto) {
const { versionName, environmentName, versionId, envId } = validateAppAccessDto;
const response = {
id: app.id,
slug: app.slug,
type: app.type,
};
/* If the request comes from preview which needs version id */
if (versionName || environmentName || (versionId && envId)) {
if (!ability.can(FEATURE_KEY.UPDATE, App, app.id)) {
throw new ForbiddenException(
JSON.stringify({
organizationId: app.organizationId,
})
);
}
/* Adding backward compatibility for old URLs */
const version = versionId
? await this.versionRepository.findById(versionId, app.id)
: versionName
? await this.versionRepository.findByName(versionName, app.id)
: // Handle version retrieval based on env
await this.versionRepository.findLatestVersionForEnvironment(
app.id,
envId,
environmentName,
app.organizationId
);
if (!version) {
throw new NotFoundException("Couldn't found app version. Please check the version name");
}
const environment = await this.appsUtilService.validateVersionEnvironment(
environmentName,
envId,
version.currentEnvironmentId,
app.organizationId
);
if (version) response['versionName'] = version.name;
if (envId) response['environmentName'] = environment.name;
response['versionId'] = version.id;
response['environmentId'] = environment.id;
}
return plainToClass(ValidateAppAccessResponseDto, response);
}
validateReleasedApp(ability: AppAbility, app: App): { id: string; slug: string } {
if (!app.currentVersionId) {
const editPermission = ability.can(FEATURE_KEY.UPDATE, App, app.id);
const errorResponse = {
statusCode: HttpStatus.NOT_IMPLEMENTED,
error: 'App is not released yet',
message: { error: 'App is not released yet', editPermission },
};
throw new HttpException(errorResponse, HttpStatus.NOT_IMPLEMENTED);
}
const { id, slug } = app;
return {
slug: slug,
id: id,
};
}
async update(app: App, appUpdateDto: AppUpdateDto, user: User) {
const { id: userId, organizationId } = user;
// const prevName = app.name;
const { name } = appUpdateDto;
const result = await this.appsUtilService.update(app, appUpdateDto, organizationId);
if (name && app.creationMode != 'GIT' && name != app.name) {
// Can use event emitter
//this.appGitUtilService.renameAppOrVersion(user, app.id, prevName);
}
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId,
organizationId,
resourceId: app.id,
resourceName: app.name,
metadata: { updateParams: { app: appUpdateDto } },
});
const response = decamelizeKeys(result);
return response;
}
async delete(app: App, user: User) {
const { organizationId } = user;
const { id } = app;
await this.appRepository.delete({ id, organizationId });
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: id,
organizationId: user.organizationId,
resourceId: app.id,
resourceName: app.name,
});
}
async getAllApps(user: User, appListDto: AppListDto): Promise<any> {
let apps = [];
let totalFolderCount = 0;
const { folderId, page, searchKey, type } = appListDto;
return dbTransactionWrap(async (manager: EntityManager) => {
if (appListDto.folderId) {
const folder = await this.foldersUtilService.findOne(appListDto.folderId, manager);
const { viewableApps, totalCount } = await this.folderAppsUtilService.getAppsFor(
user,
folder,
parseInt(page || '1'),
searchKey
);
apps = viewableApps;
totalFolderCount = totalCount;
} else {
apps = await this.appsUtilService.all(user, parseInt(page || '1'), searchKey, type);
}
const totalCount = await this.appsUtilService.count(user, searchKey, type);
const totalPageCount = folderId ? totalFolderCount : totalCount;
const meta = {
total_pages: Math.ceil(totalPageCount / 9),
total_count: totalCount,
folder_count: totalFolderCount,
current_page: parseInt(page || '1'),
};
const response = {
meta,
apps,
};
return decamelizeKeys(response);
});
}
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
return await this.appsUtilService.findTooljetDbTables(appId); //moved to util
}
async getOne(app: App, user: User): Promise<any> {
const response = decamelizeKeys(app);
const seralizedQueries = [];
const dataQueriesForVersion = app.editingVersion
? await this.versionRepository.findDataQueriesForVersion(app.editingVersion.id)
: [];
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(app.editingVersion.id) : [];
const eventsForVersion = app.editingVersion
? await this.eventService.findEventsForVersion(app.editingVersion.id)
: [];
// serialize queries
for (const query of dataQueriesForVersion) {
const decamelizedQuery = decamelizeKeys(query);
decamelizedQuery['options'] = query.options;
seralizedQueries.push(decamelizedQuery);
}
response['data_queries'] = seralizedQueries;
response['definition'] = app.editingVersion?.definition;
response['pages'] = this.appsUtilService.mergeDefaultComponentData(pagesForVersion);
response['events'] = eventsForVersion;
//! if editing version exists, camelize the definition
if (app.editingVersion) {
const appTheme = await this.organizationThemeUtilService.getTheme(
user.organizationId,
response['editing_version']['global_settings']?.['theme']?.['id']
);
response['editing_version']['global_settings']['theme'] = appTheme;
if (app.editingVersion.definition) {
response['editing_version'] = {
...response['editing_version'],
definition: camelizeKeys(app.editingVersion.definition),
};
}
}
if (response['editing_version']) {
const hasMultiEnvLicense = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT);
let shouldFreezeEditor = false;
let appVersionEnvironment: AppEnvironment;
if (hasMultiEnvLicense) {
appVersionEnvironment = await this.appEnvironmentUtilService.get(
user.organizationId,
response['editing_version']['current_environment_id']
);
shouldFreezeEditor = appVersionEnvironment.priority > 1;
} else {
appVersionEnvironment = await this.appEnvironmentUtilService.getByPriority(user.organizationId);
response['editing_version']['current_environment_id'] = appVersionEnvironment.id;
}
response['should_freeze_editor'] = app.creationMode === 'GIT' || shouldFreezeEditor;
response['editorEnvironment'] = {
id: appVersionEnvironment.id,
name: appVersionEnvironment.name,
};
// Inject app theme
const appTheme = await this.organizationThemeUtilService.getTheme(
user.organizationId,
response['editing_version']['global_settings']?.['theme']?.['id']
);
response['editing_version']['global_settings']['theme'] = appTheme;
}
return response;
}
async getBySlug(app: App, user: User): Promise<any> {
const versionToLoad = app.currentVersionId
? await this.versionRepository.findVersion(app.currentVersionId)
: await this.versionRepository.findVersion(app.editingVersion?.id);
const pagesForVersion = app.editingVersion ? await this.pageService.findPagesForVersion(versionToLoad.id) : [];
const eventsForVersion = app.editingVersion ? await this.eventService.findEventsForVersion(versionToLoad.id) : [];
const appTheme = await this.organizationThemeUtilService.getTheme(
app.organizationId,
versionToLoad?.globalSettings?.theme?.id
);
if (app?.isPublic && user) {
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: app.id,
resourceName: app.name,
});
}
// serialize
return {
current_version_id: app['currentVersionId'],
data_queries: versionToLoad?.dataQueries,
definition: versionToLoad?.definition,
is_public: app.isPublic,
is_maintenance_on: app.isMaintenanceOn,
name: app.name,
slug: app.slug,
events: eventsForVersion,
pages: this.appsUtilService.mergeDefaultComponentData(pagesForVersion),
homePageId: versionToLoad.homePageId,
globalSettings: { ...versionToLoad.globalSettings, theme: appTheme },
showViewerNavigation: versionToLoad.showViewerNavigation,
pageSettings: versionToLoad?.pageSettings,
};
}
async release(app: App, user: User, versionReleaseDto: VersionReleaseDto) {
await dbTransactionWrap(async (manager: EntityManager) => {
const { versionToBeReleased } = versionReleaseDto;
const { id: appId } = app;
//check if the app version is eligible for release
const currentEnvironment: AppEnvironment = await manager
.createQueryBuilder(AppEnvironment, 'app_environments')
.select(['app_environments.id', 'app_environments.isDefault', 'app_environments.priority'])
.innerJoinAndSelect('app_versions', 'app_versions', 'app_versions.current_environment_id = app_environments.id')
.where('app_versions.id = :versionToBeReleased', {
versionToBeReleased,
})
.getOne();
const isMultiEnvironmentEnabled = await this.licenseTermsService.getLicenseTerms(LICENSE_FIELD.MULTI_ENVIRONMENT);
/*
Allow version release only if the environment is on
production with a valid license or
expired license and development environment (priority no.1) (CE rollback)
*/
if (isMultiEnvironmentEnabled && !currentEnvironment?.isDefault) {
throw new BadRequestException('You can only release when the version is promoted to production');
}
await manager.update(App, appId, { currentVersionId: versionToBeReleased });
RequestContext.setLocals(AUDIT_LOGS_REQUEST_CONTEXT_KEY, {
userId: user.id,
organizationId: user.organizationId,
resourceId: app.id,
resourceName: app.name,
metadata: { data: { name: 'App Released', versionToBeReleased: versionReleaseDto.versionToBeReleased } },
});
return;
});
}
}