Fix app:dev CLI by removing deleted createOneApplication mutation (#18460)
## Summary - The `createOneApplication` GraphQL mutation was removed from the server during the application architecture refactor (#18432), but the SDK CLI (`app:dev`, `app:build --sync`) still called it, causing failures. - Simplified the SDK to use `syncApplication` (which now internally creates the `ApplicationEntity` via `ensureApplicationExists`) instead of a separate create step. - On first run (clean install), the orchestrator now runs an initial sync before initializing the file uploader, so file uploads can proceed (they require the `ApplicationEntity` to exist). ## Test plan - [x] Typecheck passes for both `twenty-sdk` and `twenty-server` - [x] `app:dev` tested locally with existing app (finds app, uploads, syncs) - [x] `app:dev` tested locally after `app:uninstall` (creates app via sync, uploads, syncs) - [x] SDK unit tests pass (23/26 files, 3 pre-existing failures unrelated) Made with [Cursor](https://cursor.com)
This commit is contained in:
+40
-32
@@ -24,6 +24,8 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationInput } from 'src/engine/core-modules/application/application-development/dtos/application.input';
|
||||
import { CreateDevelopmentApplicationInput } from 'src/engine/core-modules/application/application-development/dtos/create-development-application.input';
|
||||
import { DevelopmentApplicationDTO } from 'src/engine/core-modules/application/application-development/dtos/development-application.dto';
|
||||
import { GenerateApplicationTokenInput } from 'src/engine/core-modules/application/application-development/dtos/generate-application-token.input';
|
||||
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/application-development/dtos/upload-application-file.input';
|
||||
import { WorkspaceMigrationDTO } from 'src/engine/core-modules/application/application-development/dtos/workspace-migration.dto';
|
||||
@@ -64,6 +66,44 @@ export class ApplicationDevelopmentResolver {
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => DevelopmentApplicationDTO)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
async createDevelopmentApplication(
|
||||
@Args() { universalIdentifier, name }: CreateDevelopmentApplicationInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<DevelopmentApplicationDTO> {
|
||||
const applicationRegistrationId = await this.findApplicationRegistrationId(
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const existing = await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return {
|
||||
id: existing.id,
|
||||
universalIdentifier: existing.universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
const application = await this.applicationService.create({
|
||||
universalIdentifier,
|
||||
name,
|
||||
sourcePath: universalIdentifier,
|
||||
sourceType: ApplicationRegistrationSourceType.LOCAL,
|
||||
applicationRegistrationId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
id: application.id,
|
||||
universalIdentifier: application.universalIdentifier,
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => ApplicationTokenPairDTO)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
async generateApplicationToken(
|
||||
@@ -87,13 +127,6 @@ export class ApplicationDevelopmentResolver {
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.ensureApplicationExists({
|
||||
universalIdentifier: manifest.application.universalIdentifier,
|
||||
name: manifest.application.displayName,
|
||||
workspaceId,
|
||||
applicationRegistrationId,
|
||||
});
|
||||
|
||||
const workspaceMigration =
|
||||
await this.applicationSyncService.synchronizeFromManifest({
|
||||
workspaceId,
|
||||
@@ -223,29 +256,4 @@ export class ApplicationDevelopmentResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureApplicationExists(params: {
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
applicationRegistrationId: string;
|
||||
}): Promise<void> {
|
||||
const existing = await this.applicationService.findByUniversalIdentifier({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applicationService.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
sourcePath: params.universalIdentifier,
|
||||
sourceType: ApplicationRegistrationSourceType.LOCAL,
|
||||
applicationRegistrationId: params.applicationRegistrationId,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
@InputType()
|
||||
export class CreateApplicationInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
version: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
sourcePath: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
applicationRegistrationId?: string;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
@ArgsType()
|
||||
export class CreateDevelopmentApplicationInput {
|
||||
@Field(() => String)
|
||||
universalIdentifier: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType('DevelopmentApplication')
|
||||
export class DevelopmentApplicationDTO {
|
||||
@Field(() => String)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
universalIdentifier: string;
|
||||
}
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import axios from 'axios';
|
||||
import { Repository } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
const npmPackageMetadataSchema = z.object({
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationUpgradeService {
|
||||
private readonly logger = new Logger(ApplicationUpgradeService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async checkForUpdates(
|
||||
appRegistration: ApplicationRegistrationEntity,
|
||||
): Promise<string | null> {
|
||||
if (appRegistration.sourceType !== ApplicationRegistrationSourceType.NPM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registryUrl = this.twentyConfigService.get('APP_REGISTRY_URL');
|
||||
|
||||
if (!appRegistration.sourcePackage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const encodedPackage = encodeURIComponent(appRegistration.sourcePackage);
|
||||
|
||||
const { data } = await axios.get(
|
||||
`${registryUrl}/${encodedPackage}/latest`,
|
||||
{
|
||||
headers: { 'User-Agent': 'Twenty-AppUpgrade' },
|
||||
timeout: 10_000,
|
||||
},
|
||||
);
|
||||
|
||||
const parsed = npmPackageMetadataSchema.safeParse(data);
|
||||
|
||||
if (!parsed.success) {
|
||||
this.logger.warn(
|
||||
`Unexpected response shape from registry for ${appRegistration.sourcePackage}`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.appRegistrationRepository.update(appRegistration.id, {
|
||||
latestAvailableVersion: parsed.data.version,
|
||||
});
|
||||
|
||||
return parsed.data.version;
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to check updates for ${appRegistration.sourcePackage}: ${error}`,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async checkAllForUpdates(): Promise<void> {
|
||||
const npmRegistrations = await this.appRegistrationRepository.find({
|
||||
where: { sourceType: ApplicationRegistrationSourceType.NPM },
|
||||
});
|
||||
|
||||
for (const registration of npmRegistrations) {
|
||||
await this.checkForUpdates(registration);
|
||||
}
|
||||
}
|
||||
|
||||
async upgradeApplication(params: {
|
||||
appRegistrationId: string;
|
||||
targetVersion: string;
|
||||
workspaceId: string;
|
||||
}): Promise<boolean> {
|
||||
const appRegistration = await this.appRegistrationRepository.findOneOrFail({
|
||||
where: { id: params.appRegistrationId },
|
||||
});
|
||||
|
||||
if (
|
||||
appRegistration.sourceType === ApplicationRegistrationSourceType.LOCAL ||
|
||||
appRegistration.sourceType === ApplicationRegistrationSourceType.TARBALL
|
||||
) {
|
||||
throw new ApplicationException(
|
||||
'Cannot upgrade an app installed from a tarball or local source',
|
||||
ApplicationExceptionCode.UPGRADE_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.applicationInstallService.installApplication({
|
||||
appRegistrationId: params.appRegistrationId,
|
||||
version: params.targetVersion,
|
||||
workspaceId: params.workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
const appName =
|
||||
appRegistration.sourcePackage ?? appRegistration.universalIdentifier;
|
||||
|
||||
this.logger.error(`Upgrade failed for ${appName}`, error);
|
||||
|
||||
if (error instanceof ApplicationException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new ApplicationException(
|
||||
`Upgrade failed for ${appName}`,
|
||||
ApplicationExceptionCode.UPGRADE_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { APPLICATION_VERSION_CHECK_CRON_PATTERN } from 'src/engine/core-modules/application/application-upgrade/crons/constants/application-version-check-cron-pattern.constant';
|
||||
import { ApplicationUpgradeService } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.service';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
|
||||
@Injectable()
|
||||
@Processor(MessageQueue.cronQueue)
|
||||
export class ApplicationVersionCheckCronJob {
|
||||
private readonly logger = new Logger(ApplicationVersionCheckCronJob.name);
|
||||
|
||||
constructor(
|
||||
private readonly applicationUpgradeService: ApplicationUpgradeService,
|
||||
) {}
|
||||
|
||||
@Process(ApplicationVersionCheckCronJob.name)
|
||||
@SentryCronMonitor(
|
||||
ApplicationVersionCheckCronJob.name,
|
||||
APPLICATION_VERSION_CHECK_CRON_PATTERN,
|
||||
)
|
||||
async handle(): Promise<void> {
|
||||
this.logger.log('Starting application version check...');
|
||||
|
||||
try {
|
||||
await this.applicationUpgradeService.checkAllForUpdates();
|
||||
this.logger.log('Application version check completed successfully');
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Application version check failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
import { APPLICATION_VERSION_CHECK_CRON_PATTERN } from 'src/engine/core-modules/application/application-upgrade/crons/constants/application-version-check-cron-pattern.constant';
|
||||
import { ApplicationVersionCheckCronJob } from 'src/engine/core-modules/application/application-upgrade/crons/application-version-check.cron.job';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
|
||||
@Command({
|
||||
name: 'cron:app-version-check',
|
||||
description:
|
||||
'Starts a cron job to check for app version updates on npm registries',
|
||||
})
|
||||
export class ApplicationVersionCheckCronCommand extends CommandRunner {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.cronQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.messageQueueService.addCron<undefined>({
|
||||
jobName: ApplicationVersionCheckCronJob.name,
|
||||
data: undefined,
|
||||
options: {
|
||||
repeat: {
|
||||
pattern: APPLICATION_VERSION_CHECK_CRON_PATTERN,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
// Every 6 hours
|
||||
export const APPLICATION_VERSION_CHECK_CRON_PATTERN = '0 */6 * * *';
|
||||
-2
@@ -6,7 +6,6 @@ import { ApplicationRegistrationResolver } from 'src/engine/core-modules/applica
|
||||
import { ApplicationRegistrationService } from 'src/engine/core-modules/application/application-registration/application-registration.service';
|
||||
import { ApplicationRegistrationVariableModule } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.module';
|
||||
import { ApplicationTarballService } from 'src/engine/core-modules/application/application-registration/application-tarball.service';
|
||||
import { ApplicationPackageModule } from 'src/engine/core-modules/application/application-package/application-package.module';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
@@ -24,7 +23,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
|
||||
WorkspaceEntity,
|
||||
]),
|
||||
ApplicationRegistrationVariableModule,
|
||||
ApplicationPackageModule,
|
||||
ApplicationModule,
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
|
||||
-40
@@ -13,7 +13,6 @@ import { ApplicationRegistrationVariableEntity } from 'src/engine/core-modules/a
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
|
||||
import { CreateApplicationRegistrationVariableInput } from 'src/engine/core-modules/application/application-registration-variable/dtos/create-application-registration-variable.input';
|
||||
import { UpdateApplicationRegistrationVariableInput } from 'src/engine/core-modules/application/application-registration-variable/dtos/update-application-registration-variable.input';
|
||||
import { ApplicationPackageFetcherService } from 'src/engine/core-modules/application/application-package/application-package-fetcher.service';
|
||||
import { ApplicationRegistrationExceptionFilter } from 'src/engine/core-modules/application/application-registration/application-registration-exception-filter';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import {
|
||||
@@ -63,7 +62,6 @@ export class ApplicationRegistrationResolver {
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private readonly applicationTarballService: ApplicationTarballService,
|
||||
private readonly applicationPackageFetcherService: ApplicationPackageFetcherService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
) {}
|
||||
|
||||
@@ -262,44 +260,6 @@ export class ApplicationRegistrationResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
FeatureFlagGuard,
|
||||
SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS),
|
||||
)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_APPLICATION_ENABLED)
|
||||
@Mutation(() => ApplicationRegistrationEntity)
|
||||
async registerNpmPackage(
|
||||
@Args('packageName') packageName: string,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
): Promise<ApplicationRegistrationEntity> {
|
||||
const resolvedPackage =
|
||||
await this.applicationPackageFetcherService.resolveNpmPackage(
|
||||
packageName,
|
||||
);
|
||||
|
||||
try {
|
||||
const manifest = resolvedPackage.manifest.application;
|
||||
|
||||
return await this.applicationRegistrationService.createFromNpmPackage({
|
||||
packageName,
|
||||
universalIdentifier: manifest.universalIdentifier,
|
||||
name: manifest.displayName,
|
||||
description: manifest.description,
|
||||
author: manifest.author,
|
||||
logoUrl: manifest.logoUrl,
|
||||
websiteUrl: manifest.websiteUrl,
|
||||
termsUrl: manifest.termsUrl,
|
||||
version: resolvedPackage.packageJson.version as string | undefined,
|
||||
ownerWorkspaceId: workspaceId,
|
||||
});
|
||||
} finally {
|
||||
await this.applicationPackageFetcherService.cleanupExtractedDir(
|
||||
resolvedPackage.cleanupDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
FeatureFlagGuard,
|
||||
|
||||
-56
@@ -327,62 +327,6 @@ export class ApplicationRegistrationService {
|
||||
await this.applicationRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
async createFromNpmPackage(params: {
|
||||
packageName: string;
|
||||
universalIdentifier: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
termsUrl?: string;
|
||||
version?: string;
|
||||
ownerWorkspaceId: string;
|
||||
}): Promise<ApplicationRegistrationEntity> {
|
||||
const existingByUid = await this.findOneByUniversalIdentifier(
|
||||
params.universalIdentifier,
|
||||
);
|
||||
|
||||
if (isDefined(existingByUid)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
`An app with universalIdentifier "${params.universalIdentifier}" is already registered`,
|
||||
ApplicationRegistrationExceptionCode.UNIVERSAL_IDENTIFIER_ALREADY_CLAIMED,
|
||||
);
|
||||
}
|
||||
|
||||
const existingByPackage =
|
||||
await this.applicationRegistrationRepository.findOne({
|
||||
where: { sourcePackage: params.packageName },
|
||||
});
|
||||
|
||||
if (isDefined(existingByPackage)) {
|
||||
throw new ApplicationRegistrationException(
|
||||
`Package "${params.packageName}" is already registered`,
|
||||
ApplicationRegistrationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
description: params.description ?? null,
|
||||
author: params.author ?? null,
|
||||
logoUrl: params.logoUrl ?? null,
|
||||
websiteUrl: params.websiteUrl ?? null,
|
||||
termsUrl: params.termsUrl ?? null,
|
||||
latestAvailableVersion: params.version ?? null,
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
sourcePackage: params.packageName,
|
||||
isListed: true,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
return this.applicationRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
async findManyBySourceType(
|
||||
sourceType: ApplicationRegistrationSourceType,
|
||||
): Promise<ApplicationRegistrationEntity[]> {
|
||||
|
||||
@@ -212,12 +212,6 @@ export class ApplicationService {
|
||||
});
|
||||
}
|
||||
|
||||
async createOneApplication(
|
||||
data: Partial<ApplicationEntity> & { workspaceId: string },
|
||||
): Promise<ApplicationEntity> {
|
||||
return this.create(data);
|
||||
}
|
||||
|
||||
async findTwentyStandardApplicationOrThrow(workspaceId: string): Promise<{
|
||||
application: ApplicationEntity;
|
||||
workspace: WorkspaceEntity;
|
||||
|
||||
Reference in New Issue
Block a user