2114 extensibility manage serverless execution from built code (#17317)

Summary

- Serverless functions are now built when created/updated instead of at
execution time
  - Added builtHandlerPath field to track pre-built function artifacts
- Renamed base-typescript-project to seed-project with pre-built ESM
output included
- Renamed file folder enums for consistency (Functions → BuiltFunction,
SourceCode → Source)
  - supports backwards compatibility with existing serverless functions

Tested with all combinations
- storage : local driver and S3 driver
- serverless : local driver and lambda driver
This commit is contained in:
martmull
2026-01-22 12:56:44 +01:00
committed by GitHub
parent 9ee2cd0fe3
commit f92c8a98a4
34 changed files with 348 additions and 193 deletions
@@ -7,11 +7,13 @@ import { isDefined } from 'twenty-shared/utils';
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 { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { getSeedProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-seed-project-files';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { CreateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { buildAndUploadServerlessFunction } from 'src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function';
@Injectable()
export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
@@ -28,6 +30,8 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra
const { action, queryRunner, workspaceId } = context;
const { flatEntity: serverlessFunction } = action;
await this.buildAndSaveServerlessFunction(serverlessFunction);
const serverlessFunctionRepository =
queryRunner.manager.getRepository<ServerlessFunctionEntity>(
ServerlessFunctionEntity,
@@ -37,7 +41,11 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra
...serverlessFunction,
workspaceId,
});
}
private async buildAndSaveServerlessFunction(
serverlessFunction: FlatServerlessFunction,
) {
const draftFileFolder = getServerlessFolderOrThrow({
flatServerlessFunction: serverlessFunction,
version: 'draft',
@@ -49,15 +57,20 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra
draftFileFolder,
);
} else {
for (const file of await getBaseTypescriptProjectFiles) {
for (const file of await getSeedProjectFiles) {
await this.fileStorageService.write({
file: file.content,
name: file.name,
mimeType: undefined,
mimeType: 'application/typescript',
folder: join(draftFileFolder, file.path),
});
}
}
await buildAndUploadServerlessFunction({
flatServerlessFunction: serverlessFunction,
version: 'draft',
fileStorageService: this.fileStorageService,
});
}
async rollbackForMetadata(
@@ -1,9 +1,11 @@
import { 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 { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { DeleteServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type';
@@ -44,15 +46,24 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra
from: {
folderPath: getServerlessFolderOrThrow({
flatServerlessFunction,
fileFolder: FileFolder.ServerlessFunction,
}),
},
to: {
folderPath: getServerlessFolderOrThrow({
flatServerlessFunction,
toDelete: true,
fileFolder: FileFolder.ServerlessFunctionToDelete,
}),
},
});
// We can delete built code as it can be computed from source code if rollback occurs
await this.fileStorageService.delete({
folderPath: getServerlessFolderOrThrow({
flatServerlessFunction,
fileFolder: FileFolder.BuiltFunction,
}),
});
}
async rollbackForMetadata(
@@ -70,13 +81,13 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra
from: {
folderPath: getServerlessFolderOrThrow({
flatServerlessFunction,
toDelete: true,
fileFolder: FileFolder.ServerlessFunctionToDelete,
}),
},
to: {
folderPath: getServerlessFolderOrThrow({
flatServerlessFunction,
toDelete: false,
fileFolder: FileFolder.ServerlessFunction,
}),
},
});
@@ -7,13 +7,14 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { UpdateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
import { buildAndUploadServerlessFunction } from 'src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function';
@Injectable()
export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
@@ -84,5 +85,11 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra
});
await this.fileStorageService.writeFolder(code, fileFolder);
await buildAndUploadServerlessFunction({
flatServerlessFunction,
version: 'draft',
fileStorageService: this.fileStorageService,
});
}
}