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,12 +1,16 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { FileFolder } from 'twenty-shared/types';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import type { LogicFunctionDriverFactory } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-driver.factory';
import { getLogicFunctionSubfolderForFromSource } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-subfolder-for-from-source';
import {
FlatDeleteLogicFunctionAction,
UniversalDeleteLogicFunctionAction,
@@ -15,14 +19,17 @@ import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { getLogicFunctionSubfolderForFromSource } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-subfolder-for-from-source';
@Injectable()
export class DeleteLogicFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'logicFunction',
) {
constructor(private readonly fileStorageService: FileStorageService) {
constructor(
private readonly fileStorageService: FileStorageService,
@Inject(LOGIC_FUNCTION_DRIVER_FACTORY_TOKEN)
private readonly logicFunctionDriverFactory: LogicFunctionDriverFactory,
) {
super();
}
@@ -60,19 +67,51 @@ export class DeleteLogicFunctionActionHandlerService extends WorkspaceMigrationR
const applicationUniversalIdentifier = flatApplication.universalIdentifier;
await this.fileStorageService.deleteFolder({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
folderPath: getLogicFunctionSubfolderForFromSource(flatLogicFunction.id),
});
await Promise.all([
this.deleteBestEffort(
`source folder for logic function ${flatLogicFunction.id}`,
() =>
this.fileStorageService.deleteFolder({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
folderPath: getLogicFunctionSubfolderForFromSource(
flatLogicFunction.id,
),
}),
),
this.deleteBestEffort(
`built handler for logic function ${flatLogicFunction.id}`,
() =>
this.fileStorageService.deleteFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
}),
),
this.deleteBestEffort(
`runtime resource for logic function ${flatLogicFunction.id}`,
() =>
this.logicFunctionDriverFactory
.getCurrentDriver()
.delete(flatLogicFunction),
),
]);
}
await this.fileStorageService.deleteFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: flatLogicFunction.builtHandlerPath,
});
private async deleteBestEffort(
description: string,
operation: () => Promise<void>,
): Promise<void> {
try {
await operation();
} catch (error) {
this.logger.warn(
`Failed to delete ${description}: ${error instanceof Error ? error.message : String(error)}`,
DeleteLogicFunctionActionHandlerService.name,
);
}
}
async rollbackForMetadata(): Promise<void> {}