App uninstall lambda, layers cleanup (#21749)

# Introduction
On a logic function deletion also remove the driver entry
On a app uninstall also remove the sdk layer ( keeps the dep one as it
can be shared across several lambda )

| Resource | Scope | Before this PR | After |
|---|---|---|---|
| DB metadata (functions, objects, fields…) | per-app | deleted |
deleted |
| Source folder (`FileFolder.Source`) | per-function | deleted | deleted
|
| Built handler file (`FileFolder.BuiltLogicFunction`) | per-function |
deleted | deleted |
| **Lambda function** | per-function | **leaked** | **deleted** (driver
`delete`) |
| **SDK layer** `sdk-<wsId>-<appId>` (all versions) | per-app |
**leaked** | **deleted** (driver `deleteApplicationResources` →
`deleteSdkLayer`) |
| Deps layer `deps-<checksum>` | shared across apps/workspaces | not
deleted | **intentionally not deleted** (content-addressed, GC'd) |

## What I don't like about all that
Right now there's some non reversible side effect inside the workspace
migration transaction
- If the transaction fails we're facing data loss
- It also slows down everything

I'm about to create a new PR that allow population post transaction
commit side effect / cleanup to be run later

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21749?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-06-19 15:10:39 +02:00
committed by GitHub
parent a59ed80422
commit 98c35ea804
7 changed files with 170 additions and 35 deletions
@@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { type Manifest } from 'twenty-shared/application';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
@@ -6,17 +6,19 @@ import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { PackageJson } from 'type-fest';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationManifestMigrationService } from 'src/engine/core-modules/application/application-manifest/application-manifest-migration.service';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { buildFromToAllUniversalFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/build-from-to-all-universal-flat-entity-maps.util';
import { getApplicationSubAllFlatEntityMaps } from 'src/engine/core-modules/application/application-manifest/utils/get-application-sub-all-flat-entity-maps.util';
import { FileStorageService } from 'src/engine/core-modules/file-storage/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 { 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';
@@ -35,6 +37,8 @@ export class ApplicationSyncService {
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fileStorageService: FileStorageService,
@Inject(LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN)
private readonly logicFunctionDriverFactory: LogicFunctionDriverFactory,
) {}
public async synchronizeFromManifest({
@@ -247,6 +251,32 @@ export class ApplicationSyncService {
workspaceId,
);
await this.cleanupApplicationRuntimeResources({
workspaceId,
applicationUniversalIdentifier,
});
return validateAndBuildResult.workspaceMigration;
}
private async cleanupApplicationRuntimeResources({
workspaceId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
}): Promise<void> {
try {
const driver = this.logicFunctionDriverFactory.getCurrentDriver();
await driver.deleteApplicationResources({
workspaceId,
applicationUniversalIdentifier,
});
} catch (error) {
this.logger.warn(
`Failed to clean up runtime resources for application ${applicationUniversalIdentifier} in workspace ${workspaceId}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}