ToolJet/server/src/modules/app-environments/util.service.ts
vjaris42 58ba6b8563
Enable Git Sync for Datasources, constants and dashboard (#15434)
* feat: Folder permission system

* fix(group-permissions): resolve custom group validation, folder edit check, and UI inconsistencie

* edit folder container && no folder in custom resource

* fix the ui for custom in empty state

* fix: coercion logic for folder permissions

* feat: enhance folder permissions handling in app components

* feat: add folder granular permissions handling in user apps permissions

* feat: implement granular folder permissions in ability guard and service

* feat: improve error handling for folder permissions with specific messages

* feat: enhance EnvironmentSelect component to handle disabled state and improve display logic

* chore: bump ee submodules

* add basic framework to support platform git

* feat: Update permission prop to isEditable in BaseManageGranularAccess component

* chore: bump ee server submodule

* fix: refine folder visibility logic based on user permissions

* feat: enhance MultiValue rendering and styling for "All environments" option

* fix:Uniqueness-of-data-source

* revert folder changes

* fix folder imports

* feat: allow app lazy loading

feat: import all apps of branches

* feat: implement folder ownership checks and enhance app permissions handling

* fix:ui changes

* feat: update WorkspaceGitSyncModal UI

* feat: enhance folder permissions handling for app ownership and actions

* chore: clarify folder creation and deletion permissions in workspace context

* fix: pull commit button & swtich branch visibility

* feat: import app from git repo

* fix: freezed state

* remove reference of activebranchId

* fix linting

* fix: update folder permission labels

* fixed folder permission cases

* fixed css class issue

* fix: datasource UI

* minor fix

* feat: streamline folder permissions handling by removing redundant checks and simplifying access logic

* refactor: made error message consistent

* fix:ui changes and PR fetching on master

* fix: datasource and snapshot creation

* fix: app rendering and stub loading

* fix: add missing permission message for folder deletion action

* refactor: consolidate forbidden messages for folder actions and maintain consistency

* fix: allow pull into current branch

* fix renaming of tags and reload on branch switch

* fix: allow branches import from git

* fix:push or tab removed

* feat: streamline permission handling and improve app visibility logic

* fix: remove default access denial message in AbilityGuard

* fixed all user page functionality falky case

* feat: add workspace-level PR fetch endpoint (returns all repo PRs without app filtering)

* fix: remove app_branch_table

* Fixed profile flaky case

* fixed granular access flaky case

* fix: allow branch creation from tags

* fix: update default branch creation logic to use provider config

* fix: dso and dsv operations on codebase

* fix: constants reloading and refetch org git details on data

* uniquness per branch

* removed comment

* fix: update app version handling and add is_stub column for branch-level tracking

* fix workspace branch backfilling for scoped branches

* added unique constraint - migration

* fix: update app version unique constraint to include branchId for branch-aware handling

* fix: update subproject commit reference in server/ee

* chore: revert package-lock.json

* chore: revert frontend/package-lock.json to main

* removed banner and changed migration

* minor fix

* fix: remove unused import and handle UUID parse error gracefully in AppsUtilService

* fix: update app stub checks to safely access app_versions

* refactor: revert folder operations

* fix: removed branch id logic

* fix: ds migration

* fix encrypted diff logic

* fix: update openCreateAppModal to handle workspace branch lock

* fix: subscriber filtering, freeze priority, meta hash optimization, and co_relation_id backfill

* feat: add script to generate app metadata from app.json files

* fix: meta script

fix: backfilling of co-realtion-ids

* refactor: streamline parameter formatting in workspace git sync adapter methods

* Improves data source handling for workspace git sync

Fixes workspace git sync to properly recognize data sources across branches by improving correlation ID handling and branch-aware data source version creation.

Uses strict equality comparison in deep equal utility to prevent type coercion issues.

Excludes credential_id from data source comparison to prevent unnecessary save button states.

Removes is_active filter from branch data source queries to include all versions for proper synchronization.

* refactor: update branch switching logic and improve error handling for data source creation

* fix: migration order

* 🚀 chore: update submodules to latest main after auto-merge (#15628)

Co-authored-by: gsmithun4 <3417097+gsmithun4@users.noreply.github.com>

* chore: update version to 3.21.8-beta across all components

* fix:import app from device

* fix:ui Edit&launch,folderCopy,branching dropdown in apps and ds

* fix:encrypted helper text on master

* fix: import from git flow

* logs cleanup

* fix:migration-datasource-uniqueness

* fix: app on pull

* chore: update server submodule hash

* fix: corelation-id generation and version naming

* fix: last versions deletion error

fix: no multiple version creation

* fix:ui and toast

* chore: update server submodule hash

* feat: add branch handling for app availability and improve error handling

* fix: update encrypted value handling in DynamicForm and improve workspace constant validation logic

* fix: improve formatting of help text in DynamicForm and enhance error message for adding constants on master branch

* fix: correct version creation and pull in default branch

* chore: update server submodule hash

fix: remove logs from other PR

* fix:data source uniquness at workspace level

* fix: update header component logic for path validation and improve version import handling

* chore: update server submodule to latest commit

* fixed folder modal changes

* fix:failed to create a query error inside apps

* feat: add branchId support for data source versioning in app import/export service

* fix: push & pull of tags and versions

* fix: update subproject commit reference in server/ee

* fix:removed gitSync logic from module rename

* fix:removed switchbranch modal & allowed renaming from masted module&workflow creation

* chore: Update server submodule hash

* fix: change stub button to edit

* refactor/git-sync-remove-modules-workflows

* fix:version name for module and workflo
w

* fix:templet app creation

* fix: add author details for branch

---------

Co-authored-by: gsmithun4 <gsmithun4@gmail.com>
Co-authored-by: Pratush <pratush@Pratushs-MBP.lan>
Co-authored-by: Shantanu Mane <maneshantanu.20@gmail.com>
Co-authored-by: parthy007 <parthadhikari1812@gmail.com>
Co-authored-by: Yukti Goyal <yuktigoyal02@gmail.com>
Co-authored-by: Muhsin Shah <muhsinshah21@gmail.com>
Co-authored-by: Adish M <44204658+adishM98@users.noreply.github.com>
Co-authored-by: gsmithun4 <3417097+gsmithun4@users.noreply.github.com>
Co-authored-by: Parth <108089718+parthy007@users.noreply.github.com>
2026-03-27 23:23:23 +05:30

366 lines
13 KiB
TypeScript

import { ForbiddenException, Injectable } from '@nestjs/common';
import { EntityManager, FindOptionsOrderValue } from 'typeorm';
import { AppEnvironment } from 'src/entities/app_environments.entity';
import { dbTransactionWrap } from 'src/helpers/database.helper';
import { IAppEnvironmentUtilService } from './interfaces/IUtilService';
import { AppVersion } from '@entities/app_version.entity';
import { App } from '@entities/app.entity';
import { FindOneOptions } from 'typeorm';
import { defaultAppEnvironments } from '@helpers/utils.helper';
import { LICENSE_FIELD } from '@modules/licensing/constants';
import { LicenseTermsService } from '@modules/licensing/interfaces/IService';
import { IAppEnvironmentResponse } from './interfaces/IAppEnvironmentResponse';
import { DataSourceVersion } from '@entities/data_source_version.entity';
import { DataSourceVersionOptions } from '@entities/data_source_version_options.entity';
@Injectable()
export class AppEnvironmentUtilService implements IAppEnvironmentUtilService {
constructor(protected readonly licenseTermsService: LicenseTermsService) {}
async updateOptions(options: object, environmentId: string, dataSourceId: string, manager?: EntityManager) {
await dbTransactionWrap(async (manager: EntityManager) => {
const defaultDsv = await manager.findOne(DataSourceVersion, {
where: { dataSourceId, isDefault: true },
});
if (defaultDsv) {
const dsvo = await manager.findOne(DataSourceVersionOptions, {
where: { dataSourceVersionId: defaultDsv.id, environmentId },
});
if (dsvo) {
await manager.update(DataSourceVersionOptions, { id: dsvo.id }, { options, updatedAt: new Date() });
} else {
await manager.save(
manager.create(DataSourceVersionOptions, {
dataSourceVersionId: defaultDsv.id,
environmentId,
options,
})
);
}
}
}, manager);
}
async updateVersionOptions(
options: object,
dataSourceVersionId: string,
environmentId: string,
manager?: EntityManager
) {
await dbTransactionWrap(async (manager: EntityManager) => {
await manager.update(
DataSourceVersionOptions,
{
dataSourceVersionId,
environmentId,
},
{ options, updatedAt: new Date() }
);
}, manager);
}
async createDefaultEnvironments(organizationId: string, manager?: EntityManager): Promise<void> {
await dbTransactionWrap(async (manager: EntityManager) => {
await Promise.all(
defaultAppEnvironments.map(async (env) => {
const environment = manager.create(AppEnvironment, {
organizationId,
name: env.name,
isDefault: env.isDefault,
priority: env.priority,
createdAt: new Date(),
updatedAt: new Date(),
});
await manager.save(environment);
})
);
}, manager);
}
async getByPriority(organizationId: string, ASC = true, manager?: EntityManager): Promise<AppEnvironment> {
return dbTransactionWrap(async (manager: EntityManager) => {
const condition = {
where: { organizationId },
order: { priority: ASC ? 'ASC' : ('DESC' as FindOptionsOrderValue) },
};
return manager.findOneOrFail(AppEnvironment, condition);
}, manager);
}
async getEnvironmentByName(name: string, organizationId: string, manager?: EntityManager): Promise<AppEnvironment> {
return dbTransactionWrap(async (manager: EntityManager) => {
return manager.findOne(AppEnvironment, {
where: { name, organizationId },
});
}, manager);
}
async getAllEnvironments(organizationId: string, manager?: EntityManager): Promise<AppEnvironment[]> {
return dbTransactionWrap(async (manager: EntityManager) => {
return manager.find(AppEnvironment, { where: { organizationId } });
}, manager);
}
async calculateButtonVisibility(
isMultiEnvironmentEnabled: boolean,
appVersionEnvironment?: AppEnvironment,
appId?: string,
versionId?: string,
manager?: EntityManager
) {
/* Further conditions can handle from here */
if (!isMultiEnvironmentEnabled) {
return {
shouldRenderPromoteButton: false,
shouldRenderReleaseButton: true,
};
}
const appDetails = await manager.findOneOrFail(App, {
select: ['id', 'currentVersionId'],
where: { id: appId },
});
const isVersionReleased = appDetails.currentVersionId && appDetails.currentVersionId === versionId;
const isCurrentVersionInProduction = appVersionEnvironment?.isDefault;
const shouldRenderPromoteButton = !isCurrentVersionInProduction && !isVersionReleased;
const shouldRenderReleaseButton = isCurrentVersionInProduction || isVersionReleased;
return { shouldRenderPromoteButton, shouldRenderReleaseButton };
}
async getSelectedVersion(selectedEnvironmentId: string, appId: string, manager?: EntityManager): Promise<any> {
const subquery = manager
.createQueryBuilder(AppEnvironment, 'innerEnv')
.select('innerEnv.priority')
.where('innerEnv.id = :selectedEnvironmentId', { selectedEnvironmentId });
const result = await manager
.createQueryBuilder(AppVersion, 'appVersion')
.select(['appVersion.name', 'appVersion.id', 'appVersion.currentEnvironmentId'])
.innerJoin(AppEnvironment, 'env', 'appVersion.currentEnvironmentId = env.id')
.where(`env.priority >= (${subquery.getQuery()})`)
.setParameters(subquery.getParameters())
.andWhere('appVersion.appId = :appId', { appId })
.orderBy('appVersion.updatedAt', 'DESC')
.limit(1)
.getRawOne();
if (!result) {
return null;
}
return {
name: result.appVersion_name,
id: result.appVersion_id,
currentEnvironmentId: result.appVersion_current_environment_id,
};
}
/**
* Resolves the effective environment ID, respecting license restrictions.
* Throws ForbiddenException if a non-dev environment is requested without multi-environment license.
* If no environment is requested, defaults to the development environment.
*/
async resolveEnvironmentId(
organizationId: string,
requestedEnvironmentId?: string,
manager?: EntityManager
): Promise<string> {
const isMultiEnvEnabled = await this.licenseTermsService.getLicenseTerms(
LICENSE_FIELD.MULTI_ENVIRONMENT,
organizationId
);
const devEnv = await this.getByPriority(organizationId, true, manager);
if (!isMultiEnvEnabled && requestedEnvironmentId && requestedEnvironmentId !== devEnv.id) {
throw new ForbiddenException(
'Multi-environment is not enabled for this organization. Please contact the super admin.'
);
}
return requestedEnvironmentId || devEnv.id;
}
async get(
organizationId: string,
id?: string,
priorityCheck = false,
manager?: EntityManager
): Promise<AppEnvironment> {
const isMultiEnvironmentEnabled = await this.licenseTermsService.getLicenseTerms(
LICENSE_FIELD.MULTI_ENVIRONMENT,
organizationId
);
return await dbTransactionWrap(async (manager: EntityManager) => {
const condition: FindOneOptions<AppEnvironment> = {
where: {
organizationId,
...(id ? { id } : !isMultiEnvironmentEnabled ? { priority: 1 } : !priorityCheck ? { isDefault: true } : {}),
},
...(priorityCheck && { order: { priority: 'ASC' } }),
};
return await manager.findOneOrFail(AppEnvironment, condition);
}, manager);
}
async getAll(organizationId: string, appId?: string, manager?: EntityManager): Promise<AppEnvironment[]> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const appEnvironments = await manager.find(AppEnvironment, {
where: {
organizationId,
enabled: true,
},
order: {
priority: 'ASC',
},
});
if (appId) {
for (const appEnvironment of appEnvironments) {
const count = await manager.count(AppVersion, {
where: {
...(appEnvironment.priority !== 1 && {
currentEnvironmentId: appEnvironment.id,
}),
appId,
},
});
appEnvironment.appVersionsCount = count;
}
}
return appEnvironments;
}, manager);
}
async getOptions(
dataSourceId: string,
organizationId: string,
environmentId?: string,
branchId?: string,
appVersionId?: string
): Promise<DataSourceVersionOptions> {
return await dbTransactionWrap(async (manager: EntityManager) => {
let envId: string = environmentId;
let envName: string;
if (!environmentId) {
const environment = await this.get(organizationId, null, false, manager);
envId = environment.id;
envName = environment.name;
} else {
// Fetch environment name for the given environment ID
const environment = await manager.findOne(AppEnvironment, {
where: { id: envId, organizationId },
select: ['name'],
});
envName = environment?.name || 'unknown';
}
// Branch-aware path: read from data_source_version_options for a specific branch
if (branchId) {
const dsv = await manager.findOne(DataSourceVersion, {
where: { dataSourceId, branchId, isActive: true },
});
if (dsv) {
const dsvo = await manager.findOne(DataSourceVersionOptions, {
where: { dataSourceVersionId: dsv.id, environmentId: envId },
});
if (dsvo) {
const result = {
id: dsvo.id,
options: dsvo.options,
environmentId: envId,
dataSourceId,
createdAt: dsvo.createdAt,
updatedAt: dsvo.updatedAt,
environmentName: envName,
} as any;
return result;
}
}
}
// Saved/tagged version path: read from data_source_version_options via appVersionId
if (appVersionId) {
const dsv = await manager.findOne(DataSourceVersion, {
where: { dataSourceId, appVersionId, isActive: true },
});
if (dsv) {
const dsvo = await manager.findOne(DataSourceVersionOptions, {
where: { dataSourceVersionId: dsv.id, environmentId: envId },
});
if (dsvo) {
const result = {
id: dsvo.id,
options: dsvo.options,
environmentId: envId,
dataSourceId,
createdAt: dsvo.createdAt,
updatedAt: dsvo.updatedAt,
environmentName: envName,
} as any;
return result;
}
}
}
// Default version path: read from data_source_version_options via default DSV
const defaultDsv = await manager.findOne(DataSourceVersion, {
where: { dataSourceId, isDefault: true },
});
if (defaultDsv) {
const dsvo = await manager.findOne(DataSourceVersionOptions, {
where: { dataSourceVersionId: defaultDsv.id, environmentId: envId },
});
if (dsvo) {
const result = {
id: dsvo.id,
options: dsvo.options,
environmentId: envId,
dataSourceId,
createdAt: dsvo.createdAt,
updatedAt: dsvo.updatedAt,
environmentName: envName,
} as any;
return result;
}
}
throw new ForbiddenException(
`No data source version options found for dataSourceId=${dataSourceId}, environmentId=${envId}`
);
});
}
async init(
editorVersion: Partial<AppVersion>,
organizationId: string,
isMultiEnvironmentEnabled = false,
manager?: EntityManager
): Promise<IAppEnvironmentResponse> {
const environments: AppEnvironment[] = await this.getAll(organizationId, editorVersion.appId, manager);
let editorEnvironment: AppEnvironment;
if (!isMultiEnvironmentEnabled) {
editorEnvironment = environments.find((env) => env.priority === 1);
} else {
editorEnvironment = environments.find((env) => env.id === editorVersion.currentEnvironmentId);
}
const { shouldRenderPromoteButton, shouldRenderReleaseButton } = await this.calculateButtonVisibility(
isMultiEnvironmentEnabled,
editorEnvironment,
editorVersion.appId,
editorVersion.id,
manager
);
const response: IAppEnvironmentResponse = {
editorVersion,
editorEnvironment,
appVersionEnvironment: editorEnvironment,
shouldRenderPromoteButton,
shouldRenderReleaseButton,
environments,
};
return response;
}
}