Merge pull request #12673 from ToolJet/fix/external-epi

Fix External API Modularisation
This commit is contained in:
Adish M 2025-05-15 10:37:07 +05:30 committed by GitHub
commit b94934279e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 336 additions and 119 deletions

@ -1 +1 @@
Subproject commit dd5796edccc8d5eda524d804a222845791d733fb
Subproject commit 280578f99c45224428f78ee16285b62f4c3631fd

@ -1 +1 @@
Subproject commit 7848948f90077fa3fa02e43fc577a62d00f9a4da
Subproject commit 69bdefb1f3f1d35bd6e7231e50799ff10a77a60f

View file

@ -10,6 +10,7 @@ import Ajv from 'ajv';
import * as path from 'path';
import * as fs from 'fs';
import { ImportResourcesDto } from '@dto/import-resources.dto';
import { AppImportRequestDto } from '@modules/external-apis/dto';
const ajv = new Ajv({ allErrors: true, coerceTypes: true });
const logger = new Logger('TooljetDatabaseSchemaValidator');
@ -109,3 +110,15 @@ export function ValidateTooljetDatabaseSchema(validationOptions?: ValidationOpti
});
};
}
export function ValidateTooljetDatabaseImportSchema(validationOptions?: ValidationOptions) {
return function (object: AppImportRequestDto, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: ValidateTooljetDatabaseConstraint,
});
};
}

View file

@ -21,6 +21,7 @@ import { AppsSubscriber } from './subscribers/apps.subscriber';
import { AiModule } from '@modules/ai/module';
import { AppPermissionsModule } from '@modules/app-permissions/module';
import { RolesRepository } from '@modules/roles/repository';
import { UsersModule } from '@modules/users/module';
@Module({})
export class AppsModule {
static async register(configs: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -55,6 +56,7 @@ export class AppsModule {
await DataSourcesModule.register(configs),
await AiModule.register(configs),
await AppPermissionsModule.register(configs),
await UsersModule.register(configs),
],
controllers: [AppsController],
providers: [
@ -74,7 +76,7 @@ export class AppsModule {
AppImportExportService,
RolesRepository,
],
exports: [AppsUtilService],
exports: [AppsUtilService, AppImportExportService],
};
}
}

View file

@ -2,6 +2,7 @@ import { App } from '@entities/app.entity';
import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { SessionAppData } from './types';
import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto';
@Injectable()
export class AppsRepository extends Repository<App> {
@ -63,4 +64,23 @@ export class AppsRepository extends Repository<App> {
},
});
}
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
return await this.createQueryBuilder('app')
.select([
'app.id AS id',
'app.name AS name',
'app.slug AS slug',
'app.created_at AS createdAt',
'app.organization_id AS organizationId',
'version.id AS versionId',
'version.name AS versionName',
'version.created_at AS versionCreatedAt',
])
.leftJoin('app_versions', 'version', 'version.app_id = app.id')
.where('app.organizationId = :organizationId', { organizationId })
.orderBy('app.created_At', 'ASC')
.orderBy('version.created_at', 'ASC')
.getRawMany();
}
}

View file

@ -29,9 +29,6 @@ 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 { DataQuery } from '@entities/data_query.entity';
import { DataSource } from '@entities/data_source.entity';
import { AppVersion } from '@entities/app_version.entity';
import { PageService } from './services/page.service';
import { EventsService } from './services/event.service';
import { LICENSE_FIELD } from '@modules/licensing/constants';
@ -224,40 +221,7 @@ export class AppsService implements IAppsService {
}
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const tooljetDbDataQueries = await manager
.createQueryBuilder(DataQuery, 'data_queries')
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
.where('app_versions.app_id = :appId', { appId })
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
.getMany();
const uniqTableIds = new Set();
tooljetDbDataQueries.forEach((dq) => {
if (dq.options?.operation === 'join_tables') {
const joinOptions = dq.options?.join_table?.joins ?? [];
(joinOptions || []).forEach((join) => {
const { table, conditions } = join;
if (table) uniqTableIds.add(table);
conditions?.conditionsList?.forEach((condition) => {
const { leftField, rightField } = condition;
if (leftField?.table) {
uniqTableIds.add(leftField?.table);
}
if (rightField?.table) {
uniqTableIds.add(rightField?.table);
}
});
});
}
if (dq.options.table_id) uniqTableIds.add(dq.options.table_id);
});
return [...uniqTableIds].map((table_id) => {
return { table_id };
});
});
return await this.appsUtilService.findTooljetDbTables(appId); //moved to util
}
async getOne(app: App, user: User): Promise<any> {

View file

@ -33,6 +33,7 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { DataSourcesRepository } from '@modules/data-sources/repository';
import { AppEnvironmentUtilService } from '@modules/app-environments/util.service';
import { ComponentsService } from './component.service';
import { UsersUtilService } from '@modules/users/util.service';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record<string, string>;
dataQueryMapping: Record<string, string>;
@ -51,7 +52,17 @@ type DefaultDataSourceName =
| 'tooljetdbdefault'
| 'workflowsdefault';
type NewRevampedComponent = 'Text' | 'TextInput' | 'PasswordInput' | 'NumberInput' | 'Table' | 'Button' | 'Checkbox' | 'Divider' | 'VerticalDivider' | 'Link';
type NewRevampedComponent =
| 'Text'
| 'TextInput'
| 'PasswordInput'
| 'NumberInput'
| 'Table'
| 'Button'
| 'Checkbox'
| 'Divider'
| 'VerticalDivider'
| 'Link';
const DefaultDataSourceNames: DefaultDataSourceName[] = [
'restapidefault',
@ -80,9 +91,10 @@ export class AppImportExportService {
protected dataSourcesUtilService: DataSourcesUtilService,
protected dataSourcesRepository: DataSourcesRepository,
protected appEnvironmentUtilService: AppEnvironmentUtilService,
protected usersUtilService: UsersUtilService,
protected readonly entityManager: EntityManager,
protected componentsService: ComponentsService
) { }
) {}
async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> {
// https://github.com/typeorm/typeorm/issues/3857
@ -94,7 +106,7 @@ export class AppImportExportService {
.createQueryBuilder(App, 'apps')
.where('apps.id = :id AND apps.organization_id = :organizationId', {
id,
organizationId: user.organizationId,
organizationId: user?.organizationId,
});
const appToExport = await queryForAppToExport.getOne();
@ -123,7 +135,7 @@ export class AppImportExportService {
const appEnvironments = await manager
.createQueryBuilder(AppEnvironment, 'app_environments')
.where('app_environments.organizationId = :organizationId', {
organizationId: user.organizationId,
organizationId: user?.organizationId,
})
.orderBy('app_environments.createdAt', 'ASC')
.getMany();
@ -184,13 +196,13 @@ export class AppImportExportService {
const components =
pages.length > 0
? await manager
.createQueryBuilder(Component, 'components')
.leftJoinAndSelect('components.layouts', 'layouts')
.where('components.pageId IN(:...pageId)', {
pageId: pages.map((v) => v.id),
})
.orderBy('components.created_at', 'ASC')
.getMany()
.createQueryBuilder(Component, 'components')
.leftJoinAndSelect('components.layouts', 'layouts')
.where('components.pageId IN(:...pageId)', {
pageId: pages.map((v) => v.id),
})
.orderBy('components.created_at', 'ASC')
.getMany()
: [];
const events = await manager
@ -340,8 +352,8 @@ export class AppImportExportService {
return await catchDbException(async () => {
const importedApp = manager.create(App, {
name: appParams.name,
organizationId: user.organizationId,
userId: user.id,
organizationId: user?.organizationId,
userId: user.id, //fetch super admin user id for EE
slug: null,
icon: appParams.icon,
creationMode: `${isGitApp ? 'GIT' : 'DEFAULT'}`,
@ -762,7 +774,7 @@ export class AppImportExportService {
const { dataQueryMapping } = await this.createDataQueriesForAppVersion(
manager,
user.organizationId,
user?.organizationId,
importingDataQueriesForAppVersion,
importingDataSource,
dataSourceForAppVersion,
@ -1059,10 +1071,10 @@ export class AppImportExportService {
const options =
importingDataSource.kind === 'tooljetdb'
? this.replaceTooljetDbTableIds(
importingQuery.options,
externalResourceMappings['tooljet_database'],
organizationId
)
importingQuery.options,
externalResourceMappings['tooljet_database'],
organizationId
)
: importingQuery.options;
const newQuery = manager.create(DataQuery, {
@ -1153,7 +1165,7 @@ export class AppImportExportService {
appResourceMappings: AppResourceMappings
) {
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
user.organizationId,
user?.organizationId,
appResourceMappings.appVersionMapping[appVersion.id],
DefaultDataSourceKinds,
manager
@ -1192,7 +1204,7 @@ export class AppImportExportService {
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
scope: 'global',
organizationId: user.organizationId,
organizationId: user?.organizationId,
},
});
};
@ -1203,7 +1215,7 @@ export class AppImportExportService {
kind: dataSource.kind,
type: In([DataSourceTypes.DEFAULT, DataSourceTypes.SAMPLE]),
scope: 'global',
organizationId: user.organizationId,
organizationId: user?.organizationId,
},
});
};
@ -1221,7 +1233,7 @@ export class AppImportExportService {
if (plugin) {
const newDataSource = manager.create(DataSource, {
organizationId: user.organizationId,
organizationId: user?.organizationId,
name: dataSource.name,
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
@ -1236,7 +1248,7 @@ export class AppImportExportService {
const createNewGlobalDs = async (ds: DataSource): Promise<DataSource> => {
const newDataSource = manager.create(DataSource, {
organizationId: user.organizationId,
organizationId: user?.organizationId,
name: dataSource.name,
kind: dataSource.kind,
type: DataSourceTypes.DEFAULT,
@ -1264,7 +1276,7 @@ export class AppImportExportService {
) {
appResourceMappings = { ...appResourceMappings };
const currentOrgEnvironments = await this.appEnvironmentUtilService.getAll(
user.organizationId,
user?.organizationId,
appVersion.appId,
manager
);
@ -1326,7 +1338,7 @@ export class AppImportExportService {
appResourceMappings = { ...appResourceMappings };
const { appVersionMapping, appDefaultEnvironmentMapping } = appResourceMappings;
const organization: Organization = await manager.findOne(Organization, {
where: { id: user.organizationId },
where: { id: user?.organizationId },
relations: ['appEnvironments'],
});
let currentEnvironmentId: string;
@ -1545,7 +1557,7 @@ export class AppImportExportService {
// Create default data sources
const defaultDataSourceIds = await this.createDefaultDataSourceForVersion(
user.organizationId,
user?.organizationId,
version.id,
DefaultDataSourceKinds,
manager
@ -1553,7 +1565,7 @@ export class AppImportExportService {
let envIdArray: string[] = [];
const organization: Organization = await manager.findOne(Organization, {
where: { id: user.organizationId },
where: { id: user?.organizationId },
relations: ['appEnvironments'],
});
envIdArray = [...organization.appEnvironments.map((env) => env.id)];
@ -1562,7 +1574,7 @@ export class AppImportExportService {
await Promise.all(
defaultAppEnvironments.map(async (en) => {
const env = manager.create(AppEnvironment, {
organizationId: user.organizationId,
organizationId: user?.organizationId,
name: en.name,
isDefault: en.isDefault,
priority: en.priority,
@ -1627,10 +1639,10 @@ export class AppImportExportService {
options:
dataSourceId == defaultDataSourceIds['tooljetdb']
? this.replaceTooljetDbTableIds(
query.options,
externalResourceMappings['tooljet_database'],
user.organizationId
)
query.options,
externalResourceMappings['tooljet_database'],
user?.organizationId
)
: query.options,
});
await manager.save(newQuery);

View file

@ -37,6 +37,9 @@ import { DataSourcesRepository } from '@modules/data-sources/repository';
import { IAppsUtilService } from './interfaces/IUtilService';
import { DataSourcesUtilService } from '@modules/data-sources/util.service';
import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
import { WorkspaceAppsResponseDto } from '@modules/external-apis/dto';
import { DataQuery } from '@entities/data_query.entity';
import { DataSource } from '@entities/data_source.entity';
@Injectable()
export class AppsUtilService implements IAppsUtilService {
@ -522,4 +525,45 @@ export class AppsUtilService implements IAppsUtilService {
return components;
}
async findAllOrganizationApps(organizationId: string): Promise<WorkspaceAppsResponseDto[]> {
return await this.appRepository.findAllOrganizationApps(organizationId);
}
async findTooljetDbTables(appId: string): Promise<{ table_id: string }[]> {
return await dbTransactionWrap(async (manager: EntityManager) => {
const tooljetDbDataQueries = await manager
.createQueryBuilder(DataQuery, 'data_queries')
.innerJoin(DataSource, 'data_sources', 'data_queries.data_source_id = data_sources.id')
.innerJoin(AppVersion, 'app_versions', 'app_versions.id = data_sources.app_version_id')
.where('app_versions.app_id = :appId', { appId })
.andWhere('data_sources.kind = :kind', { kind: 'tooljetdb' })
.getMany();
const uniqTableIds = new Set();
tooljetDbDataQueries.forEach((dq) => {
if (dq.options?.operation === 'join_tables') {
const joinOptions = dq.options?.join_table?.joins ?? [];
(joinOptions || []).forEach((join) => {
const { table, conditions } = join;
if (table) uniqTableIds.add(table);
conditions?.conditionsList?.forEach((condition) => {
const { leftField, rightField } = condition;
if (leftField?.table) {
uniqTableIds.add(leftField?.table);
}
if (rightField?.table) {
uniqTableIds.add(rightField?.table);
}
});
});
}
if (dq.options.table_id) uniqTableIds.add(dq.options.table_id);
});
return [...uniqTableIds].map((table_id) => {
return { table_id };
});
});
}
}

View file

@ -14,7 +14,7 @@ export class ExternalApiSecurityGuard implements CanActivate {
throw new ForbiddenException('External API is disabled');
}
// Check the authorization header
// // Check the authorization header
const authHeader = request.headers['authorization'];
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');

View file

@ -37,5 +37,17 @@ export const FEATURES: FeaturesConfig = {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
[FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
[FEATURE_KEY.IMPORT_APP]: {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
[FEATURE_KEY.EXPORT_APP]: {
license: LICENSE_FIELD.EXTERNAL_API,
isPublic: true,
},
},
};

View file

@ -7,4 +7,41 @@ export enum FEATURE_KEY {
UPDATE_USER_WORKSPACE = 'UPDATE_USER_WORKSPACE',
GET_ALL_WORKSPACES = 'GET_ALL_WORKSPACES',
UPDATE_USER_ROLE = 'UPDATE_USER_ROLE',
GET_ALL_WORKSPACE_APPS = 'GET_ALL_WORKSPACE_APPS',
IMPORT_APP = 'IMPORT_APP',
EXPORT_APP = 'EXPORT_APP',
}
export type DefaultDataSourceKind = 'restapi' | 'runjs' | 'runpy' | 'tooljetdb' | 'workflows';
export type NewRevampedComponent =
| 'Text'
| 'TextInput'
| 'PasswordInput'
| 'NumberInput'
| 'Table'
| 'Button'
| 'Checkbox';
export type DefaultDataSourceName =
| 'restapidefault'
| 'runjsdefault'
| 'runpydefault'
| 'tooljetdbdefault'
| 'workflowsdefault';
export const DefaultDataSourceKinds: DefaultDataSourceKind[] = ['restapi', 'runjs', 'runpy', 'tooljetdb', 'workflows'];
export const DefaultDataSourceNames: DefaultDataSourceName[] = [
'restapidefault',
'runjsdefault',
'runpydefault',
'tooljetdbdefault',
'workflowsdefault',
];
export const NewRevampedComponents: NewRevampedComponent[] = [
'Text',
'TextInput',
'PasswordInput',
'NumberInput',
'Table',
'Checkbox',
'Button',
];

View file

@ -1,8 +1,8 @@
import { Controller, Get, Param, UseGuards, Body, Patch, Post, Put, NotFoundException } from '@nestjs/common';
import { ExternalApiSecurityGuard } from './guards/external-api-security.guard';
import { UpdateUserDto, WorkspaceDto, UpdateGivenWorkspaceDto, CreateUserDto } from './dto';
import { IExternalApisController } from './Interfaces/IController';
import { EditUserRoleDto } from '@modules/roles/dto';
import { ExternalApiSecurityGuard } from '@modules/auth/guards/external-api-security.guard';
@Controller('ext')
export class ExternalApisController implements IExternalApisController {

View file

@ -10,10 +10,13 @@ import {
MaxLength,
ValidateIf,
IsNotEmpty,
IsDefined,
IsObject,
} from 'class-validator';
import { Type } from 'class-transformer';
import { Transform, Type } from 'class-transformer';
import { USER_ROLE } from '@modules/group-permissions/constants';
import { TjdbSchemaToLatestVersion } from '@dto/transformers/resource-transformer';
import { ValidateTooljetDatabaseImportSchema } from '@dto/validators/tooljet-database.validator';
export enum Status {
ACTIVE = 'active',
ARCHIVED = 'archived',
@ -131,3 +134,73 @@ export class UpdateUserWorkspaceDto {
@IsOptional()
groups?: GroupDto[];
}
export class VersionDto {
id: string;
name: string;
createdAt?: Date;
}
export class AppWithVersionsDto {
id: string;
name: string;
slug: string;
createdAt: Date;
organizationId: string;
versions: VersionDto[];
versionCount: number;
}
export class WorkspaceAppsResponseDto {
apps: AppWithVersionsDto[];
total: number;
}
export class AppImportRequestDto {
@IsString()
tooljet_version: string;
// TODO: Add transformation and validation for app similar to tooljet_database
@IsOptional()
app: AppImportDto[];
// Optional parameter -> To be provided in import request to import app with custom name.
@IsOptional()
@IsString()
appName: string;
// TJ-DB field
@IsOptional()
// Transform the input data to the latest schema version
// This should be applied first to ensure the data is in
// the correct format before validation
@Transform(TjdbSchemaToLatestVersion)
@ValidateNested({ each: true })
// Ensure each item is properly instantiated as ImportTooljetDatabaseDto
// This is crucial for nested validation to work correctly
@Type(() => ImportTooljetDatabaseDto)
// Custom validator to check against the tooljet database schema
// This should be applied last to validate the transformed
// and instantiated data
@ValidateTooljetDatabaseImportSchema({ each: true })
tooljet_database: ImportTooljetDatabaseDto[];
}
export class AppImportDto {
@IsDefined()
@IsObject()
definition: any;
}
export class ImportTooljetDatabaseDto {
@IsUUID()
id: string;
@IsString()
table_name: string;
@IsDefined()
schema: any;
// @IsOptional()
// data: boolean;
}

View file

@ -1,27 +0,0 @@
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class ExternalApiSecurityGuard implements CanActivate {
constructor(protected configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// Check if external API is enabled
const isExternalApiEnabled = this.configService.get<string>('ENABLE_EXTERNAL_API') === 'true';
if (!isExternalApiEnabled) {
throw new ForbiddenException('External API is disabled');
}
// Check the authorization header
const authHeader = request.headers['authorization'];
const externalApiAccessToken = this.configService.get<string>('EXTERNAL_API_ACCESS_TOKEN');
if (!authHeader || authHeader !== `Basic ${externalApiAccessToken}`) {
throw new ForbiddenException('Unauthorized');
}
return true;
}
}

View file

@ -3,8 +3,12 @@ import { GroupPermissionsModule } from '@modules/group-permissions/module';
import { RolesModule } from '@modules/roles/module';
import { DynamicModule } from '@nestjs/common';
import { getImportPath } from '@modules/app/constants';
import { ExternalApiSecurityGuard } from './guards/external-api-security.guard';
import { RolesRepository } from '@modules/roles/repository';
import { TooljetDbModule } from '@modules/tooljet-db/module';
import { AppsModule } from '@modules/apps/module';
import { OrganizationsModule } from '@modules/organizations/module';
import { VersionModule } from '@modules/versions/module';
import { UsersModule } from '@modules/users/module';
export class ExternalApiModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
const importPath = await getImportPath(configs?.IS_GET_CONTEXT);
@ -14,14 +18,16 @@ export class ExternalApiModule {
return {
module: ExternalApiModule,
imports: [await RolesModule.register(configs), await GroupPermissionsModule.register(configs)],
providers: [
ExternalApiUtilService,
ExternalApisService,
ExternalApiSecurityGuard,
FeatureAbilityFactory,
RolesRepository,
imports: [
await UsersModule.register(configs),
await RolesModule.register(configs),
await GroupPermissionsModule.register(configs),
await TooljetDbModule.register(configs),
await AppsModule.register(configs),
await OrganizationsModule.register(configs),
await VersionModule.register(configs),
],
providers: [ExternalApiUtilService, ExternalApisService, FeatureAbilityFactory, RolesRepository],
controllers: [ExternalApisController],
exports: [ExternalApiUtilService],
};

View file

@ -11,6 +11,9 @@ interface Features {
[FEATURE_KEY.UPDATE_USER_WORKSPACE]: FeatureConfig;
[FEATURE_KEY.GET_ALL_WORKSPACES]: FeatureConfig;
[FEATURE_KEY.UPDATE_USER_ROLE]: FeatureConfig;
[FEATURE_KEY.GET_ALL_WORKSPACE_APPS]: FeatureConfig;
[FEATURE_KEY.IMPORT_APP]: FeatureConfig;
[FEATURE_KEY.EXPORT_APP]: FeatureConfig;
}
export interface FeaturesConfig {
@ -22,3 +25,13 @@ export interface ValidateEditUserGroupAdditionObject {
groupsToAddIds: string[];
organizationId: string;
}
export interface AppResourceMappings {
defaultDataSourceIdMapping: Record<string, string>;
dataQueryMapping: Record<string, string>;
appVersionMapping: Record<string, string>;
appEnvironmentMapping: Record<string, string>;
appDefaultEnvironmentMapping: Record<string, string[]>;
pagesMapping: Record<string, string>;
componentsMapping: Record<string, string>;
}

View file

@ -0,0 +1,3 @@
export interface IOrganizationUtilService {
validateWorkspaceExists(workspaceId: string): Promise<void>;
}

View file

@ -2,6 +2,7 @@ import { DynamicModule } from '@nestjs/common';
import { getImportPath } from '@modules/app/constants';
import { InstanceSettingsModule } from '@modules/instance-settings/module';
import { OrganizationRepository } from './repository';
import { AppEnvironmentsModule } from '@modules/app-environments/module';
export class OrganizationsModule {
static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise<DynamicModule> {
@ -9,13 +10,14 @@ export class OrganizationsModule {
const { OrganizationsService } = await import(`${importPath}/organizations/service`);
const { OrganizationsController } = await import(`${importPath}/organizations/controller`);
const { FeatureAbilityFactory } = await import(`${importPath}/organizations/ability`);
const { AppEnvironmentUtilService } = await import(`${importPath}/app-environments/util.service`);
const { OrganizationsUtilService } = await import(`${importPath}/organizations/util.service`);
return {
module: OrganizationsModule,
imports: [await InstanceSettingsModule.register(configs)],
imports: [await InstanceSettingsModule.register(configs), await AppEnvironmentsModule.register(configs)],
controllers: [OrganizationsController],
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, AppEnvironmentUtilService],
providers: [OrganizationsService, OrganizationRepository, FeatureAbilityFactory, OrganizationsUtilService],
exports: [OrganizationsUtilService],
};
}
}

View file

@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { OrganizationRepository } from './repository';
import { BadRequestException } from '@nestjs/common';
import { IOrganizationUtilService } from './interfaces/IUtilService';
@Injectable()
export class OrganizationsUtilService implements IOrganizationUtilService {
constructor(protected readonly organizationRepository: OrganizationRepository) {}
async validateWorkspaceExists(workspaceId: string) {
const existingWorkspace = await this.organizationRepository.findOne({
where: { id: workspaceId },
});
if (!existingWorkspace) {
throw new BadRequestException(`Invalid workspaceId: ${workspaceId}`);
}
}
}

View file

@ -1,13 +1,13 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { EditUserRoleDto } from './dto';
import { RolesUtilService } from './util.service';
import { ERROR_HANDLER } from '../group-permissions/constants/error';
import { _ } from 'lodash';
import { LicenseUserService } from '@modules/licensing/services/user.service';
import { dbTransactionWrap } from '@helpers/database.helper';
import { EntityManager } from 'typeorm';
import { RolesRepository } from './repository';
import { IRolesService } from './interfaces/IService';
import { EntityManager } from 'typeorm';
import { dbTransactionWrap } from '@helpers/database.helper';
import { LicenseUserService } from '@modules/licensing/services/user.service';
import { ERROR_HANDLER } from '@modules/group-permissions/constants/error';
import { _ } from 'lodash';
@Injectable()
export class RolesService implements IRolesService {

View file

@ -16,6 +16,7 @@ export class UsersModule {
imports: [await SessionModule.register(configs)],
controllers: [UsersController],
providers: [UsersService, UserRepository, UsersUtilService, FeatureAbilityFactory],
exports: [UsersUtilService],
};
}
}

View file

@ -18,6 +18,7 @@ import { Organization } from '@entities/organization.entity';
import { OrganizationUser } from '@entities/organization_user.entity';
import { isSuperAdmin } from '@helpers/utils.helper';
import * as uuid from 'uuid';
import { USER_ROLE } from '@modules/group-permissions/constants';
type UserFilterOptions = { searchText?: string; status?: string; page?: number };
@ -168,6 +169,18 @@ export class UserRepository extends Repository<User> {
await manager.upsert(UserDetails, updatableParams, conflictsPaths);
}
async getUserWithAdminRole(organizationId: string, manager?: EntityManager): Promise<User | null> {
return dbTransactionWrap((manager: EntityManager) => {
return manager
.createQueryBuilder(User, 'user')
.innerJoin('user.userGroups', 'groupUsers')
.innerJoin('groupUsers.group', 'group')
.where('group.name = :groupName', { groupName: USER_ROLE.ADMIN })
.andWhere('group.organizationId = :organizationId', { organizationId })
.getOne();
}, manager || this.manager);
}
async findByEmail(
email: string,
organizationId?: string,

View file

@ -3,4 +3,5 @@ import { AppVersionUpdateDto } from '@dto/app-version-update.dto';
export interface IVersionUtilService {
updateVersion(appVersion: AppVersion, appVersionUpdateDto: AppVersionUpdateDto): Promise<void>;
fetchVersions(appId: string): Promise<AppVersion[]>;
}

View file

@ -51,6 +51,7 @@ export class VersionModule {
VersionUtilService,
FeatureAbilityFactory,
],
exports: [VersionUtilService],
};
}
}

View file

@ -72,4 +72,13 @@ export class VersionUtilService implements IVersionUtilService {
await this.versionRepository.update(appVersion.id, editableParams);
return;
}
async fetchVersions(appId: string): Promise<AppVersion[]> {
return await this.versionRepository.find({
where: { appId },
order: {
createdAt: 'DESC',
},
});
}
}