ToolJet/server/test/helpers/seed.ts
Shantanu Mane 89448cf492
Feature: Group Admin (#16210)
* fix: add FK constraints, unique constraint, and down() to group_admins migration

* fix: add explicit FK constraint names to group_admins migration

* feat: add GroupAdmin TypeORM entity

* fix: add Organization relation to GroupAdmin entity

* feat: add group-admin FEATURE_KEY entries and feature configs

* feat: add CE no-op stub for GroupAdminService

* feat: register GroupAdminService/Controller in module; add group-admin controller (CE stub + EE impl)

* feat: pass user to getAllGroup(); update interface and CE service signature

* test: add e2e tests for group-admin assign, revoke, scoped access, and auto-revocation

* fix: correct imports and tsconfig for group-admin e2e test

- fix entity imports: use @entities alias instead of src/entities
- fix module imports: use @modules alias instead of src/modules
- remove unused findEntityOrFail import from test-helper
- add jest to tsconfig types array for test type definitions
- add test-helper path mapping to tsconfig for proper resolution

* fix: register GroupAdmin entity in TypeOrmModule.forFeature for DI

* fix: grant workspace admins all group-admin feature keys in ability factory

* feat: add group-admin service methods

Add getGroupAdmins, getAddableAdmins, assignGroupAdmin, revokeGroupAdmin methods to groupPermissionV2Service following existing fetch pattern.

* feat: allow group-admin builders to access Groups link in workspace settings

Add is_group_admin and canManageGroups to conditionObj (both useState initializer
and subscribe handler). Change Groups link condition from ['admin'] to
['canManageGroups'] to allow admin OR group-admin builders.

* feat: add Group Admins tab to group permission resources

* fix: handle missing default groups for group-admin builders; hide admin-only controls

* feat: make permissions and granular access tabs read-only for group-admin builders; hide role-change in users tab

* feat: implement GroupAdminOrAdminRoute for session validation; enhance group admin management features

* feat: add GET_USER_ADMIN_GROUPS feature and update related permissions; enhance user group management

* feat: enhance group admin functionality; add builder role and improve admin assignment tests

* Fix: Add compiler options to tsconfig.build.json

* refactor: revert configs for test suite

* feat: enhance group admin functionality with feature access checks and user removal permissions

* feat: add option to duplicate group admins in duplicate group

* feat: enhance group permissions management

- Updated FeatureAbilityFactory to include additional checks for builder permissions and group-specific access.
- Modified GroupPermissionsController to apply FeatureAbilityGuard for various endpoints, ensuring proper permission checks.
- Enhanced GranularPermissionsController with appropriate guards for better access control.
- Refactored GroupExistenceGuard to improve group validation logic and error handling.
- Updated GroupPermissionsModule to streamline service and utility registrations.
- Added unit tests for FeatureAbilityFactory and GroupExistenceGuard to ensure robust permission handling.

* feat: implement afterUpdateUserRole method in RolesService

* feat: refactor GroupPermissionsUtilService usage in GroupPermissionsModule

* feat: enhance group permissions copyright and icons, minor bug fixes:

* Bug fixes

Co-authored-by: Copilot <copilot@github.com>

* bugz

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Rudhra Deep Biswas <rudra21ultra@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 19:29:03 +05:30

991 lines
32 KiB
TypeScript

/**
* Entity factories -- creates users, apps, data sources, permissions, and other domain objects for tests.
*/
import { INestApplication } from '@nestjs/common';
import { DataSource as TypeOrmDataSource, Repository } from 'typeorm';
import { getDataSourceToken } from '@nestjs/typeorm';
import { OrganizationUser } from '@entities/organization_user.entity';
import { Organization } from '@entities/organization.entity';
import { User } from '@entities/user.entity';
import { App } from '@entities/app.entity';
import { File } from '@entities/file.entity';
import { AppVersion } from '@entities/app_version.entity';
import { DataQuery } from '@entities/data_query.entity';
import { DataSource } from '@entities/data_source.entity';
import { GroupPermissions } from '@entities/group_permissions.entity';
import { GroupUsers } from '@entities/group_users.entity';
import { GROUP_PERMISSIONS_TYPE, ResourceType } from '@modules/group-permissions/constants';
import { GranularPermissions } from '@entities/granular_permissions.entity';
import { AppsGroupPermissions } from '@entities/apps_group_permissions.entity';
import { DataSourcesGroupPermissions } from '@entities/data_sources_group_permissions.entity';
import { GroupApps } from '@entities/group_apps.entity';
import { APP_TYPES } from '@modules/apps/constants';
import { v4 as uuidv4 } from 'uuid';
import { CreateFileDto } from '@modules/files/dto';
import * as request from 'supertest';
import { AppEnvironment } from '@entities/app_environments.entity';
import { defaultAppEnvironments } from '@helpers/utils.helper';
import { DataSourceOptions } from '@entities/data_source_options.entity';
import { Page } from '@entities/page.entity';
import { Credential } from '@entities/credential.entity';
import { SSOConfigs, SSOType, ConfigScope } from '@entities/sso_config.entity';
import { Folder } from '@entities/folder.entity';
import { FolderApp } from '@entities/folder_app.entity';
import { getDefaultDataSource } from './setup';
import { login } from './api';
export interface CreateUserOptions {
firstName?: string;
lastName?: string;
email?: string;
groups?: Array<string>;
organization?: Organization;
status?: string;
userType?: string;
invitationToken?: string;
formLoginStatus?: boolean;
organizationName?: string;
ssoConfigs?: Array<SSOConfigInput>;
enableSignUp?: boolean;
userStatus?: string;
}
export interface SSOConfigInput {
sso: string;
enabled: boolean;
configScope?: string;
configs?: Record<string, string>;
}
export interface CreateAppOptions {
name: string;
user?: User & { organizationId: string };
isPublic?: boolean;
slug?: string;
type?: string;
}
export interface CreateAppVersionOptions {
name?: string;
definition?: Record<string, unknown> | null;
currentEnvironmentId?: string | null;
}
export interface CreateDataSourceOptions {
appVersion: AppVersion;
name: string;
kind: string;
type?: string;
options?: Array<Partial<DataSourceOptionInput>> | Record<string, unknown>;
environmentId?: string | null;
}
/** A single key-value option for a data source, optionally encrypted. */
export interface DataSourceOptionInput {
key: string;
value: string;
encrypted?: boolean | string;
}
export interface CreateDataQueryOptions {
name?: string;
dataSource: DataSource;
appVersion: AppVersion;
options?: Record<string, unknown>;
}
export interface CreateDataSourceOptionParams {
dataSource: DataSource;
environmentId: string;
options?: Array<Partial<DataSourceOptionInput>> | Record<string, unknown>;
}
/** Options for creating a fully wired app with version, data source, and query. */
export interface CreateAppWithDependenciesOptions {
isQueryNeeded?: boolean;
isDataSourceNeeded?: boolean;
isAppPublic?: boolean;
dsKind?: string;
dsOptions?: Array<Partial<DataSourceOptionInput>>;
name?: string;
}
export interface CreateGroupPermissionParams {
name?: string;
group?: string;
type?: string;
organizationId?: string;
organization?: Organization;
appCreate?: boolean;
appDelete?: boolean;
folderCRUD?: boolean;
orgConstantCRUD?: boolean;
dataSourceCreate?: boolean;
dataSourceDelete?: boolean;
workflowCreate?: boolean;
workflowDelete?: boolean;
}
export interface PermissionFlags {
read?: boolean;
update?: boolean;
delete?: boolean;
}
export interface EnsureInstanceSSOConfigsOptions {
enabled?: boolean;
gitConfigs?: Record<string, string>;
googleConfigs?: Record<string, string>;
}
/** Authenticated user with workspace context and session cookie. */
export interface TestUser {
user: User;
workspace: Organization;
orgUser: OrganizationUser;
cookie: string[];
}
/** Returns all app environments for a workspace, ordered by priority. */
export async function getAllEnvironments(_nestApp: INestApplication, organizationId: string): Promise<AppEnvironment[]> {
const appEnvironmentRepository: Repository<AppEnvironment> = getDefaultDataSource().getRepository(AppEnvironment);
return await appEnvironmentRepository.find({
where: {
organizationId,
},
order: {
priority: 'ASC',
},
});
}
/** Creates the default app environments for a workspace. */
export async function ensureAppEnvironments(_nestApp: INestApplication, organizationId: string): Promise<AppEnvironment[]> {
const appEnvironmentRepository: Repository<AppEnvironment> = getDefaultDataSource().getRepository(AppEnvironment);
return await Promise.all(
defaultAppEnvironments.map(async (env) => {
return await appEnvironmentRepository.save(
appEnvironmentRepository.create({
organizationId,
name: env.name,
priority: env.priority,
isDefault: env.isDefault,
})
);
})
);
}
/** Finds an app environment by id or priority. */
export const getAppEnvironment = async (id: string, priority: number) => {
const ds = getDefaultDataSource();
return await ds.manager.findOneOrFail(AppEnvironment, {
where: { ...(id && { id }), ...(priority && { priority }) },
});
};
/**
* Seeds instance-level SSO configs (git, google, openid) in the database.
* Required for OAuth tests because getSSOConfigs() expects these rows to exist.
* @param options.enabled - Whether SSO is enabled (default: true for test convenience)
* @param options.gitConfigs - Override git SSO configs
* @param options.googleConfigs - Override google SSO configs
*/
export async function ensureInstanceSSOConfigs(options?: EnsureInstanceSSOConfigsOptions): Promise<void> {
const ds = getDefaultDataSource();
const ssoRepo = ds.getRepository(SSOConfigs);
const enabled = options?.enabled ?? true;
// Use empty strings for secrets to avoid decryption errors
// (EncryptionService.decryptSecret skips falsy values)
const types: Array<{ sso: SSOType; configs: Record<string, string> }> = [
{ sso: SSOType.GIT, configs: options?.gitConfigs ?? { clientId: 'git-client-id', clientSecret: '' } },
{ sso: SSOType.GOOGLE, configs: options?.googleConfigs ?? { clientId: 'google-client-id' } },
{ sso: SSOType.OPENID, configs: { clientId: '', clientSecret: '', name: '', wellKnownUrl: '' } },
];
for (const { sso, configs } of types) {
// Instance-level SSO configs have no organizationId -- use query builder to bypass
// TypeORM's strict typing on nullable foreign keys
const existing = await ssoRepo
.createQueryBuilder('sso')
.where('sso.sso = :sso', { sso })
.andWhere('sso.organizationId IS NULL')
.andWhere('sso.configScope = :scope', { scope: ConfigScope.INSTANCE })
.getOne();
if (!existing) {
await ssoRepo.save(
ssoRepo.create({
sso,
configs: configs as SSOConfigs['configs'],
enabled,
organizationId: undefined,
configScope: ConfigScope.INSTANCE,
})
);
}
}
await ds.query(`UPDATE "instance_settings" SET value='true' WHERE key='ENABLE_SIGNUP'`);
}
async function maybeCreateDefaultGroupPermissions(nestApp: INestApplication, organizationId: string): Promise<void> {
const ds: TypeOrmDataSource = nestApp.get(getDataSourceToken('default')) as TypeOrmDataSource;
const groupPermissionsRepository = ds.getRepository(GroupPermissions);
const defaultGroups = [
{ name: 'admin', isAdmin: true },
{ name: 'builder', isAdmin: true },
{ name: 'end-user', isAdmin: false },
];
for (let { name, isAdmin } of defaultGroups) {
const existing = await groupPermissionsRepository.find({
where: {
organizationId: organizationId,
name: name,
},
});
if (existing.length == 0) {
const groupPermission = groupPermissionsRepository.create({
organizationId: organizationId,
name: name,
type: GROUP_PERMISSIONS_TYPE.DEFAULT,
appCreate: isAdmin,
appDelete: isAdmin,
folderCRUD: isAdmin,
orgConstantCRUD: isAdmin,
dataSourceCreate: isAdmin,
dataSourceDelete: isAdmin,
workflowCreate: isAdmin,
workflowDelete: isAdmin,
});
const savedGroup = await groupPermissionsRepository.save(groupPermission);
const granularRepo = ds.getRepository(GranularPermissions);
const appsGroupRepo = ds.getRepository(AppsGroupPermissions);
for (const resourceType of [ResourceType.APP, ResourceType.DATA_SOURCE, ResourceType.WORKFLOWS]) {
const granular = granularRepo.create({
groupId: savedGroup.id,
name: resourceType === ResourceType.APP ? 'Apps' : resourceType === ResourceType.DATA_SOURCE ? 'Data Sources' : 'Workflows',
type: resourceType,
isAll: isAdmin,
});
const savedGranular = await granularRepo.save(granular);
if (resourceType === ResourceType.APP) {
const appsPerm = appsGroupRepo.create({
granularPermissionId: savedGranular.id,
appType: APP_TYPES.FRONT_END,
canEdit: isAdmin,
canView: true,
hideFromDashboard: false,
});
await appsGroupRepo.save(appsPerm);
}
}
}
}
}
async function addEndUserGroupToUser(nestApp: INestApplication, user: User & { organizationId: string }): Promise<User> {
const ds: TypeOrmDataSource = nestApp.get(getDataSourceToken('default')) as TypeOrmDataSource;
const groupPermissionsRepository = ds.getRepository(GroupPermissions);
const groupUsersRepository = ds.getRepository(GroupUsers);
const endUserGroup = await groupPermissionsRepository.findOneOrFail({
where: {
organizationId: user.organizationId,
name: 'end-user',
},
});
const groupUser = groupUsersRepository.create({
groupId: endUserGroup.id,
userId: user.id,
});
await groupUsersRepository.save(groupUser);
return user;
}
/** Assigns a user to the specified groups within their workspace, creating custom groups as needed. */
export async function createUserGroupPermissions(nestApp: INestApplication, user: User & { organizationId: string }, groups: string[]): Promise<GroupUsers[]> {
const ds: TypeOrmDataSource = nestApp.get(getDataSourceToken('default')) as TypeOrmDataSource;
const groupPermissionsRepository = ds.getRepository(GroupPermissions);
const groupUsersRepository = ds.getRepository(GroupUsers);
let groupUserEntries = [];
for (const group of groups) {
const groupName = group === 'all_users' ? 'end-user' : group;
let groupPermission: GroupPermissions;
if (groupName === 'admin' || groupName === 'end-user' || groupName === 'builder') {
groupPermission = await groupPermissionsRepository.findOneOrFail({
where: {
organizationId: user.organizationId,
name: groupName,
},
});
} else {
groupPermission =
(await groupPermissionsRepository.findOne({
where: {
organizationId: user.organizationId,
name: groupName,
},
})) ||
groupPermissionsRepository.create({
organizationId: user.organizationId,
name: groupName,
type: GROUP_PERMISSIONS_TYPE.CUSTOM_GROUP,
});
await groupPermissionsRepository.save(groupPermission);
}
const groupUser = groupUsersRepository.create({
groupId: groupPermission.id,
userId: user.id,
});
await groupUsersRepository.save(groupUser);
groupUserEntries.push(groupUser);
}
return groupUserEntries;
}
/** Creates a custom group permission in a workspace with the specified capabilities. */
export async function createGroupPermission(nestApp: INestApplication, params: CreateGroupPermissionParams): Promise<GroupPermissions> {
const ds: TypeOrmDataSource = nestApp.get(getDataSourceToken('default')) as TypeOrmDataSource;
const groupPermissionsRepository = ds.getRepository(GroupPermissions);
const mappedParams = { ...params };
if (mappedParams.group) {
mappedParams.name = mappedParams.group === 'all_users' ? 'end-user' : mappedParams.group;
delete mappedParams.group;
}
if (!mappedParams.type) {
mappedParams.type = GROUP_PERMISSIONS_TYPE.CUSTOM_GROUP;
}
if (mappedParams.organization && !mappedParams.organizationId) {
mappedParams.organizationId = mappedParams.organization.id;
delete mappedParams.organization;
}
let groupPermission = groupPermissionsRepository.create(mappedParams);
await groupPermissionsRepository.save(groupPermission);
return groupPermission;
}
/**
* Grants app-level permissions to a group using the granular permissions system.
* Creates GranularPermission -> AppsGroupPermissions -> GroupApps chain.
*/
export async function grantAppPermission(nestApp: INestApplication, application: App, groupId: string, permissions: PermissionFlags): Promise<void> {
const ds: TypeOrmDataSource = nestApp.get(getDataSourceToken('default')) as TypeOrmDataSource;
const granularRepo = ds.getRepository(GranularPermissions);
const appsGroupRepo = ds.getRepository(AppsGroupPermissions);
const groupAppsRepo = ds.getRepository(GroupApps);
let granular = await granularRepo.findOne({
where: { groupId, type: ResourceType.APP },
});
if (!granular) {
granular = granularRepo.create({
groupId,
name: 'Apps',
type: ResourceType.APP,
isAll: false,
});
granular = await granularRepo.save(granular);
}
let appsPerm = await appsGroupRepo.findOne({
where: { granularPermissionId: granular.id },
});
if (!appsPerm) {
appsPerm = appsGroupRepo.create({
granularPermissionId: granular.id,
appType: APP_TYPES.FRONT_END,
canEdit: permissions.update || false,
canView: permissions.read || false,
hideFromDashboard: false,
});
appsPerm = await appsGroupRepo.save(appsPerm);
} else {
await appsGroupRepo.update(appsPerm.id, {
canEdit: permissions.update || appsPerm.canEdit,
canView: permissions.read || appsPerm.canView,
});
appsPerm = await appsGroupRepo.findOne({ where: { id: appsPerm.id } });
}
const existingGroupApp = await groupAppsRepo.findOne({
where: { appId: application.id, appsGroupPermissionsId: appsPerm.id },
});
if (!existingGroupApp) {
const groupApp = groupAppsRepo.create({
appId: application.id,
appsGroupPermissionsId: appsPerm.id,
});
await groupAppsRepo.save(groupApp);
}
}
/** Grants data source-level permissions to a group using the granular permissions system. */
export async function createDatasourceGroupPermission(nestApp: INestApplication, dataSourceId: string, groupId: string, permissions: PermissionFlags): Promise<void> {
const ds: TypeOrmDataSource = nestApp.get(getDataSourceToken('default')) as TypeOrmDataSource;
const granularRepo = ds.getRepository(GranularPermissions);
const dsGroupRepo = ds.getRepository(DataSourcesGroupPermissions);
let granular = await granularRepo.findOne({
where: { groupId, type: ResourceType.DATA_SOURCE },
});
if (!granular) {
granular = granularRepo.create({
groupId,
name: 'Data Sources',
type: ResourceType.DATA_SOURCE,
isAll: false,
});
granular = await granularRepo.save(granular);
}
let dsPerm = await dsGroupRepo.findOne({
where: { granularPermissionId: granular.id },
});
if (!dsPerm) {
dsPerm = dsGroupRepo.create({
granularPermissionId: granular.id,
canConfigure: permissions.update || false,
canUse: permissions.read || false,
});
await dsGroupRepo.save(dsPerm);
} else {
await dsGroupRepo.update(dsPerm.id, {
canConfigure: permissions.update || dsPerm.canConfigure,
canUse: permissions.read || dsPerm.canUse,
});
}
}
/** Creates a user with workspace membership, group assignments, and default permissions. */
export async function createUser(
nestApp: INestApplication,
{
firstName,
lastName,
email,
groups,
organization,
userType = 'workspace',
status,
invitationToken,
formLoginStatus = true,
organizationName = `${email}'s workspace`,
ssoConfigs = [],
enableSignUp = false,
userStatus = 'active',
}: CreateUserOptions,
existingUser?: User
): Promise<{ organization: Organization; user: User & { organizationId: string }; orgUser: OrganizationUser }> {
const userRepository: Repository<User> = getDefaultDataSource().getRepository(User);
const organizationRepository: Repository<Organization> = getDefaultDataSource().getRepository(Organization);
const organizationUsersRepository: Repository<OrganizationUser> = getDefaultDataSource().getRepository(OrganizationUser);
organization =
organization ||
(await organizationRepository.save(
organizationRepository.create({
name: organizationName,
enableSignUp,
createdAt: new Date(),
updatedAt: new Date(),
ssoConfigs: [
{
sso: 'form',
enabled: formLoginStatus,
configScope: 'organization',
},
...ssoConfigs,
],
})
));
let user: User;
if (!existingUser) {
user = await userRepository.save(
userRepository.create({
firstName: firstName || 'test',
lastName: lastName || 'test',
email: email || 'dev@tooljet.io',
password: 'password',
userType,
status: invitationToken ? 'invited' : userStatus,
invitationToken,
defaultOrganizationId: organization.id,
createdAt: new Date(),
updatedAt: new Date(),
})
);
} else {
user = existingUser;
}
(user as User & { organizationId: string }).organizationId = organization.id;
const orgUser = await organizationUsersRepository.save(
organizationUsersRepository.create({
user: user,
organization,
invitationToken: status === 'invited' ? uuidv4() : null,
status: status || 'active',
role: 'all_users',
createdAt: new Date(),
updatedAt: new Date(),
})
);
const typedUser = user as User & { organizationId: string };
await maybeCreateDefaultGroupPermissions(nestApp, typedUser.organizationId);
await createUserGroupPermissions(
nestApp,
typedUser,
groups || ['end-user', 'admin']
);
return { organization, user: typedUser, orgUser };
}
/** Creates an application in the given user's workspace. */
export async function createApplication(
nestApp: INestApplication,
{ name, user, isPublic, slug, type = 'front-end' }: CreateAppOptions,
shouldCreateEnvs = true
): Promise<App> {
const appRepository: Repository<App> = getDefaultDataSource().getRepository(App);
user = user || (await (await createUser(nestApp, {})).user);
if (shouldCreateEnvs) {
await ensureAppEnvironments(nestApp, user.organizationId);
}
const newApp = await appRepository.save(
appRepository.create({
name,
user,
slug,
type,
isPublic: isPublic || false,
organizationId: user.organizationId,
createdAt: new Date(),
updatedAt: new Date(),
})
);
return newApp;
}
/** Creates an app version with a default page, globalSettings, and environment binding. */
export async function createApplicationVersion(
_nestApp: INestApplication,
application: App & { organizationId: string },
{ name = 'v0', definition = null, currentEnvironmentId = null }: CreateAppVersionOptions = {}
): Promise<AppVersion> {
const ds = getDefaultDataSource();
const appVersionsRepository: Repository<AppVersion> = ds.getRepository(AppVersion);
const appEnvironmentsRepository: Repository<AppEnvironment> = ds.getRepository(AppEnvironment);
const pageRepository: Repository<Page> = ds.getRepository(Page);
const environments = await appEnvironmentsRepository.find({
where: {
organizationId: application.organizationId,
},
});
const envId = currentEnvironmentId
? currentEnvironmentId
: defaultAppEnvironments.length > 1
? environments.find((env) => env.priority === 1)?.id
: environments[0].id;
const version = await appVersionsRepository.save(
appVersionsRepository.create({
appId: application.id,
name: name + Date.now(),
currentEnvironmentId: envId,
definition,
})
);
// Default page required so EE page-level permission checks don't
// treat an empty page list as "no accessible pages" (since [].every() === true).
const defaultPage = await pageRepository.save(
pageRepository.create({
name: 'Home',
handle: 'home',
appVersionId: version.id,
index: 1,
autoComputeLayout: true,
})
);
await appVersionsRepository.update(version.id, {
homePageId: defaultPage.id,
showViewerNavigation: true,
globalSettings: {
appInMaintenance: false,
canvasMaxWidth: 100,
canvasMaxWidthType: '%',
canvasMaxHeight: 2400,
canvasBackgroundColor: 'var(--cc-appBackground-surface)',
backgroundFxQuery: '',
appMode: 'light',
} as AppVersion['globalSettings'],
});
version.homePageId = defaultPage.id;
return version;
}
/** Creates a data source attached to an app version, optionally with environment-specific options. */
export async function createDataSource(
nestApp: INestApplication,
{ appVersion, name, kind, type = 'default', options, environmentId = null }: CreateDataSourceOptions
): Promise<DataSource> {
const dataSourceRepository: Repository<DataSource> = getDefaultDataSource().getRepository(DataSource);
const dataSource = await dataSourceRepository.save(
dataSourceRepository.create({
name,
kind,
appVersion,
type,
scope: type === 'static' ? 'global' : 'local',
createdAt: new Date(),
updatedAt: new Date(),
})
);
environmentId && (await createDataSourceOption(nestApp, { dataSource, environmentId, options }));
return dataSource;
}
/** Creates a data query attached to a data source and app version. */
export async function createDataQuery(_nestApp: INestApplication, { name = 'defaultquery', dataSource, appVersion, options }: CreateDataQueryOptions): Promise<DataQuery> {
const dataQueryRepository: Repository<DataQuery> = getDefaultDataSource().getRepository(DataQuery);
return await dataQueryRepository.save(
dataQueryRepository.create({
name,
options,
dataSource,
appVersion,
createdAt: new Date(),
updatedAt: new Date(),
})
);
}
/** Creates data source options for a specific environment, with Credential records for encrypted values. */
export async function createDataSourceOption(_nestApp: INestApplication, { dataSource, environmentId, options }: CreateDataSourceOptionParams): Promise<DataSourceOptions> {
const ds = getDefaultDataSource();
const dataSourceOptionsRepository = ds.getRepository(DataSourceOptions);
const credentialRepository = ds.getRepository(Credential);
const parsedOptions: Record<string, { credential_id?: string; encrypted: boolean; value?: string }> = {};
if (Array.isArray(options)) {
for (const opt of options) {
if (!opt.key) continue;
if (opt.encrypted === 'true' || opt.encrypted === true) {
const credential = await credentialRepository.save(
credentialRepository.create({ valueCiphertext: opt.value || '' })
);
parsedOptions[opt.key] = {
credential_id: credential.id,
encrypted: true,
};
} else {
parsedOptions[opt.key] = { value: opt.value, encrypted: false };
}
}
} else if (options) {
Object.assign(parsedOptions, options);
}
return await dataSourceOptionsRepository.save(
dataSourceOptionsRepository.create({
options: parsedOptions,
dataSourceId: dataSource.id,
environmentId,
})
);
}
/** Creates a test file entity with dummy binary data. */
export async function createFile(_nestApp: INestApplication): Promise<File> {
const fileRepository: Repository<File> = getDefaultDataSource().getRepository(File);
const createFileDto = new CreateFileDto();
createFileDto.filename = 'testfile';
createFileDto.data = Buffer.from([1, 2, 3, 4]);
return await fileRepository.save(fileRepository.create(createFileDto));
}
export interface CreateFolderOptions {
name: string;
type?: string;
organizationId: string;
}
/** Creates a folder in the given workspace. */
export async function createFolder(
_nestApp: INestApplication,
{ name, type, organizationId }: CreateFolderOptions
): Promise<Folder> {
const folderRepository: Repository<Folder> = getDefaultDataSource().getRepository(Folder);
return await folderRepository.save(
folderRepository.create({ name, ...(type != null && { type }), organizationId })
);
}
/** Links an application to a folder. */
export async function addAppToFolder(
_nestApp: INestApplication,
application: App,
folder: Folder
): Promise<FolderApp> {
const folderAppRepository: Repository<FolderApp> = getDefaultDataSource().getRepository(FolderApp);
return await folderAppRepository.save(
folderAppRepository.create({ app: application, folder })
);
}
/** Creates an app with version, environments, data source, and query in one call. */
export const createAppWithDependencies = async (
app: INestApplication,
user: User & { organizationId: string },
{
isQueryNeeded = true,
isDataSourceNeeded = true,
isAppPublic = false,
dsKind = 'restapi',
dsOptions = [{}],
name = 'name',
}: CreateAppWithDependenciesOptions
) => {
const application = await createApplication(
app,
{
name,
user: user,
isPublic: isAppPublic,
},
false
);
const appEnvironments = await ensureAppEnvironments(app, user.organizationId);
const appVersion = await createApplicationVersion(app, application);
let dataQuery: DataQuery | undefined;
let dataSource: DataSource | undefined;
if (isDataSourceNeeded) {
dataSource = await createDataSource(app, {
name: 'name',
kind: dsKind,
appVersion,
});
await Promise.all(
appEnvironments.map(async (env) => {
await createDataSourceOption(app, { dataSource, environmentId: env.id, options: dsOptions });
})
);
if (isQueryNeeded) {
dataQuery = await createDataQuery(app, {
dataSource,
appVersion,
options: {
method: 'get',
url: 'https://api.github.com/repos/tooljet/tooljet/stargazers',
url_params: [],
headers: [],
body: [],
},
});
}
}
return { application, appVersion, dataSource, dataQuery, appEnvironments };
};
/** Finds an app with all versions, data sources, and data queries eager-loaded. */
export const findAppWithRelations = async (id: string) => {
const ds = getDefaultDataSource();
const app = await ds.manager
.createQueryBuilder(App, 'app')
.where('app.id = :id', { id })
.innerJoinAndSelect('app.appVersions', 'versions')
.leftJoinAndSelect('versions.dataSources', 'dataSources')
.leftJoinAndSelect('versions.dataQueries', 'dataQueries')
.getOneOrFail();
const dataQueries = [];
const dataSources = [];
app.appVersions.map((version) => {
dataSources.push(...version.dataSources);
dataQueries.push(...version.dataQueries);
version.dataSources = undefined;
version.dataQueries = undefined;
});
app['dataQueries'] = dataQueries;
app['dataSources'] = dataSources;
return app;
};
/**
* Sets currentVersionId on the app, simulating a release.
* Required by EE webhook service which looks up workflowApp.currentVersionId.
*/
export const markVersionAsReleased = async (appId: string, versionId: string) => {
const ds = getDefaultDataSource();
await ds.manager
.createQueryBuilder()
.update(App)
.set({ currentVersionId: versionId })
.where('id = :id', { id: appId })
.execute();
};
/** Returns the workflow webhook API token for the given app. */
export const getWorkflowWebhookApiToken = async (appId: string) => {
const ds = getDefaultDataSource();
const app = await ds.manager.createQueryBuilder(App, 'app').where('app.id = :id', { id: appId }).getOneOrFail();
return app?.workflowApiToken ?? '';
};
/** Enables (or disables) the webhook trigger for a workflow and generates an API token. */
export const enableWebhookForWorkflows = async (workflowId: string, status = true) => {
const ds = getDefaultDataSource();
await ds.manager
.createQueryBuilder()
.update(App)
.set({ workflowEnabled: status, workflowApiToken: uuidv4() })
.where('id = :id', { id: workflowId })
.execute();
};
/** Triggers a workflow execution via the webhook endpoint. */
export const triggerWorkflowViaWebhook = async (
app: INestApplication,
apiToken: string,
workflowId: string,
environment = 'development',
bodyJson: Record<string, unknown> = {}
) => {
return await request(app.getHttpServer())
.post(`/api/v2/webhooks/workflows/${workflowId}/trigger?environment=${environment}`)
.set('Authorization', `Bearer ${apiToken}`)
.set('Content-Type', 'application/json')
.send(bodyJson);
};
/** Toggles maintenance mode on a workflow via the app update endpoint. */
export const enableWorkflowStatus = async (
app: INestApplication,
workflowId: string,
orgId: string,
tokenCookie: string[],
isMaintenanceOn = true
) => {
return await request(app.getHttpServer())
.put(`/api/apps/${workflowId}`)
.set('tj-workspace-id', orgId)
.set('Cookie', tokenCookie)
.send({
app: {
is_maintenance_on: isMaintenanceOn,
},
});
};
/** Creates a workspace admin and returns an authenticated session. */
export async function createAdmin(
nestApp: INestApplication,
email: string,
opts?: { workspace?: Organization }
): Promise<TestUser> {
const { organization, user, orgUser } = await createUser(nestApp, {
email,
groups: ['end-user', 'admin'],
organization: opts?.workspace,
});
const { tokenCookie } = await login(nestApp, email, 'password');
return { user, workspace: organization, orgUser, cookie: tokenCookie };
}
/** Creates a workspace builder and returns an authenticated session. */
export async function createBuilder(
nestApp: INestApplication,
email: string,
opts?: { workspace?: Organization }
): Promise<TestUser> {
const { organization, user, orgUser } = await createUser(nestApp, {
email,
groups: ['end-user', 'builder'],
organization: opts?.workspace,
});
const { tokenCookie } = await login(nestApp, email, 'password');
return { user, workspace: organization, orgUser, cookie: tokenCookie };
}
/** Creates a workspace end-user and returns an authenticated session. */
export async function createEndUser(
nestApp: INestApplication,
email: string,
opts?: { workspace?: Organization }
): Promise<TestUser> {
const { organization, user, orgUser } = await createUser(nestApp, {
email,
groups: ['end-user'],
organization: opts?.workspace,
});
const { tokenCookie } = await login(nestApp, email, 'password');
return { user, workspace: organization, orgUser, cookie: tokenCookie };
}
/** Creates a super-admin (instance-level) user and returns an authenticated session. */
export async function createSuperAdmin(
nestApp: INestApplication,
email: string
): Promise<TestUser> {
const { organization, user, orgUser } = await createUser(nestApp, {
email,
groups: ['end-user', 'admin'],
userType: 'instance',
});
const { tokenCookie } = await login(nestApp, email, 'password');
return { user, workspace: organization, orgUser, cookie: tokenCookie };
}