Add defineUninstallLogicFunction hook for applications (#23227)
This commit is contained in:
+3
@@ -400,9 +400,12 @@ export class ApplicationInstallService {
|
||||
);
|
||||
|
||||
if (!isVersionUpgrade) {
|
||||
// Rollback of a failed fresh install: the app never finished
|
||||
// installing, so the uninstall hook must not run.
|
||||
await this.applicationSyncService.uninstallApplication({
|
||||
applicationUniversalIdentifier: universalIdentifier,
|
||||
workspaceId: params.workspaceId,
|
||||
shouldRunUninstallHook: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -1,15 +1,18 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { ApplicationManifestApplyService } from 'src/engine/core-modules/application/application-manifest/application-manifest-apply.service';
|
||||
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
|
||||
import { ComputeApplicationManifestAllUniversalFlatEntityMapsService } from 'src/engine/core-modules/application/application-manifest/services/compute-application-manifest-all-universal-flat-entity-maps.service';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-manifest/application-sync.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module';
|
||||
import { ApplicationTranslationModule } from 'src/engine/core-modules/application/application-translation/application-translation.module';
|
||||
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/application/application-variable/application-variable.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
|
||||
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
|
||||
import { SdkClientModule } from 'src/engine/core-modules/sdk-client/sdk-client.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
@@ -18,12 +21,14 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ApplicationRegistrationEntity]),
|
||||
ApplicationModule,
|
||||
ApplicationRegistrationModule,
|
||||
ApplicationTranslationModule,
|
||||
ApplicationVariableEntityModule,
|
||||
FeatureFlagModule,
|
||||
FileStorageModule,
|
||||
LogicFunctionExecutorModule,
|
||||
PermissionsModule,
|
||||
SecretEncryptionModule,
|
||||
SdkClientModule,
|
||||
|
||||
+81
@@ -1,12 +1,15 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PackageJson } from 'type-fest';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
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 { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
|
||||
import { enrichApplicationManifestSyncError } from 'src/engine/core-modules/application/application-manifest/utils/enrich-application-manifest-sync-error.util';
|
||||
@@ -23,6 +26,7 @@ import { type FlatApplication } from 'src/engine/core-modules/application/types/
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/services/file-storage.service';
|
||||
import { LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-driver-factory.token';
|
||||
import { type LogicFunctionDriverFactory } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-driver.factory';
|
||||
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
|
||||
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
@@ -44,6 +48,9 @@ export class ApplicationSyncService {
|
||||
private readonly applicationTranslationSyncService: ApplicationTranslationSyncService,
|
||||
@Inject(LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN)
|
||||
private readonly logicFunctionDriverFactory: LogicFunctionDriverFactory,
|
||||
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
) {}
|
||||
|
||||
public async synchronizeFromManifest({
|
||||
@@ -257,9 +264,11 @@ export class ApplicationSyncService {
|
||||
public async uninstallApplication({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
shouldRunUninstallHook = true,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
applicationUniversalIdentifier: string;
|
||||
shouldRunUninstallHook?: boolean;
|
||||
}): Promise<WorkspaceMigration> {
|
||||
const application = await this.applicationService.findOneApplicationOrThrow(
|
||||
{ universalIdentifier: applicationUniversalIdentifier, workspaceId },
|
||||
@@ -272,6 +281,10 @@ export class ApplicationSyncService {
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldRunUninstallHook) {
|
||||
await this.runUninstallHook({ application, workspaceId });
|
||||
}
|
||||
|
||||
const flatEntityMapsCacheKeys = Object.values(ALL_METADATA_NAME).map(
|
||||
getMetadataFlatEntityMapsKey,
|
||||
);
|
||||
@@ -329,6 +342,74 @@ export class ApplicationSyncService {
|
||||
return validateAndBuildResult.workspaceMigration;
|
||||
}
|
||||
|
||||
// The uninstall hook must run before the deletion migration: once the
|
||||
// migration is applied, the hook's logic function metadata, code, and the
|
||||
// application's data are gone, so nothing can be executed anymore. It is
|
||||
// best-effort cleanup: a failure must never prevent the application from
|
||||
// being removed.
|
||||
private async runUninstallHook({
|
||||
application,
|
||||
workspaceId,
|
||||
}: {
|
||||
application: ApplicationEntity;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
if (!isDefined(application.applicationRegistrationId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const appRegistration = await this.appRegistrationRepository.findOne({
|
||||
where: { id: application.applicationRegistrationId },
|
||||
});
|
||||
|
||||
const uninstallLogicFunction =
|
||||
appRegistration?.manifest?.application.uninstallLogicFunction;
|
||||
|
||||
if (!isDefined(uninstallLogicFunction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { flatLogicFunctionMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatLogicFunctionMaps',
|
||||
]);
|
||||
|
||||
const flatLogicFunction =
|
||||
flatLogicFunctionMaps.byUniversalIdentifier[
|
||||
uninstallLogicFunction.universalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(flatLogicFunction)) {
|
||||
this.logger.warn(
|
||||
`Uninstall logic function "${uninstallLogicFunction.universalIdentifier}" not found for application "${application.universalIdentifier}"; skipping hook`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Executing uninstall hook for app ${application.universalIdentifier}`,
|
||||
);
|
||||
|
||||
const result = await this.logicFunctionExecutorService.execute({
|
||||
logicFunctionId: flatLogicFunction.id,
|
||||
workspaceId,
|
||||
payload: { version: application.version ?? undefined },
|
||||
});
|
||||
|
||||
if (isDefined(result.error)) {
|
||||
this.logger.warn(
|
||||
`Uninstall hook failed for application ${application.universalIdentifier}: ${result.error.errorMessage}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Uninstall hook failed for application ${application.universalIdentifier}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupApplicationRuntimeResources({
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier,
|
||||
|
||||
Reference in New Issue
Block a user