Serverless built updates (#17351)

follow up of https://github.com/twentyhq/twenty/pull/17317
This commit is contained in:
martmull
2026-01-22 16:04:29 +01:00
committed by GitHub
parent 7fcc43f96b
commit a68e05682b
24 changed files with 194 additions and 159 deletions
@@ -947,7 +947,7 @@ export class ApplicationSyncService {
name,
code,
timeoutSeconds: serverlessFunctionToSync.timeoutSeconds,
handlerPath: serverlessFunctionToSync.sourceHandlerPath,
sourceHandlerPath: serverlessFunctionToSync.sourceHandlerPath,
handlerName: serverlessFunctionToSync.handlerName,
toolInputSchema: serverlessFunctionToSync.toolInputSchema,
isTool: serverlessFunctionToSync.isTool,
@@ -991,7 +991,7 @@ export class ApplicationSyncService {
code,
universalIdentifier: serverlessFunctionToCreate.universalIdentifier,
timeoutSeconds: serverlessFunctionToCreate.timeoutSeconds,
handlerPath: serverlessFunctionToCreate.sourceHandlerPath,
sourceHandlerPath: serverlessFunctionToCreate.sourceHandlerPath,
handlerName: serverlessFunctionToCreate.handlerName,
applicationId,
serverlessFunctionLayerId,
@@ -1,86 +0,0 @@
import { join } from 'path';
import fs from 'fs/promises';
import { build } from 'esbuild';
import { FileFolder } from 'twenty-shared/types';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
const buildServerlessFunctionInMemory = async ({
sourceTemporaryDir,
handlerPath,
builtHandlerPath,
}: {
sourceTemporaryDir: string;
handlerPath: string;
builtHandlerPath: string;
}): Promise<string> => {
const entryFilePath = join(sourceTemporaryDir, handlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath);
await build({
entryPoints: [entryFilePath],
outfile: builtBundleFilePath,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
});
return builtBundleFilePath;
};
export const buildAndUploadServerlessFunction = async ({
flatServerlessFunction,
version,
fileStorageService,
}: {
flatServerlessFunction: FlatServerlessFunction;
version: string;
fileStorageService: FileStorageService;
}): Promise<void> => {
const sourceFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
fileFolder: FileFolder.ServerlessFunction,
});
const builtFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
fileFolder: FileFolder.BuiltFunction,
});
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
await fileStorageService.download({
from: { folderPath: sourceFolderPath },
to: { folderPath: sourceTemporaryDir },
});
const builtBundleFilePath = await buildServerlessFunctionInMemory({
sourceTemporaryDir,
handlerPath: flatServerlessFunction.handlerPath,
builtHandlerPath: flatServerlessFunction.builtHandlerPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
await fileStorageService.write({
file: builtFile,
name: flatServerlessFunction.builtHandlerPath,
mimeType: 'application/javascript',
folder: builtFolderPath,
});
} finally {
await lambdaBuildDirectoryManager.clean();
}
};
@@ -1,23 +0,0 @@
import type { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import type { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
export const isServerlessFunctionBuilt = async ({
flatServerlessFunction,
version,
fileStorageService,
}: {
flatServerlessFunction: FlatServerlessFunction;
version: string;
fileStorageService: FileStorageService;
}) => {
const folderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
});
return await fileStorageService.checkFileExists({
folderPath,
filename: flatServerlessFunction.builtHandlerPath,
});
};
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service';
@Module({
providers: [FunctionBuildService],
exports: [FunctionBuildService],
})
export class FunctionBuildModule {}
@@ -0,0 +1,109 @@
import { Injectable } from '@nestjs/common';
import { join } from 'path';
import fs from 'fs/promises';
import { build } from 'esbuild';
import { FileFolder } from 'twenty-shared/types';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
@Injectable()
export class FunctionBuildService {
constructor(private readonly fileStorageService: FileStorageService) {}
async isBuilt({
flatServerlessFunction,
version,
}: {
flatServerlessFunction: FlatServerlessFunction;
version: string;
}): Promise<boolean> {
const folderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
});
return await this.fileStorageService.checkFileExists({
folderPath,
filename: flatServerlessFunction.builtHandlerPath,
});
}
async buildAndUpload({
flatServerlessFunction,
version,
}: {
flatServerlessFunction: FlatServerlessFunction;
version: string;
}): Promise<void> {
const sourceFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
fileFolder: FileFolder.ServerlessFunction,
});
const builtFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
fileFolder: FileFolder.BuiltFunction,
});
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
try {
const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init();
await this.fileStorageService.download({
from: { folderPath: sourceFolderPath },
to: { folderPath: sourceTemporaryDir },
});
const builtBundleFilePath = await this.buildInMemory({
sourceTemporaryDir,
sourceHandlerPath: flatServerlessFunction.sourceHandlerPath,
builtHandlerPath: flatServerlessFunction.builtHandlerPath,
});
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
await this.fileStorageService.write({
file: builtFile,
name: flatServerlessFunction.builtHandlerPath,
mimeType: 'application/javascript',
folder: builtFolderPath,
});
} finally {
await lambdaBuildDirectoryManager.clean();
}
}
private async buildInMemory({
sourceTemporaryDir,
sourceHandlerPath,
builtHandlerPath,
}: {
sourceTemporaryDir: string;
sourceHandlerPath: string;
builtHandlerPath: string;
}): Promise<string> {
const entryFilePath = join(sourceTemporaryDir, sourceHandlerPath);
const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath);
await build({
entryPoints: [entryFilePath],
outfile: builtBundleFilePath,
platform: 'node',
format: 'esm',
target: 'es2017',
bundle: true,
sourcemap: true,
packages: 'external',
});
return builtBundleFilePath;
}
}
@@ -6,7 +6,7 @@ export const FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES = [
'timeoutSeconds',
'checksum',
'code',
'handlerPath',
'sourceHandlerPath',
'handlerName',
'toolInputSchema',
'isTool',
@@ -54,7 +54,7 @@ export class CreateServerlessFunctionInput {
@IsString()
@Field({ nullable: true })
@IsOptional()
handlerPath?: string;
sourceHandlerPath?: string;
@IsString()
@Field({ nullable: true })
@@ -63,7 +63,7 @@ export class ServerlessFunctionDTO {
@IsString()
@Field()
handlerPath: string;
sourceHandlerPath: string;
@IsString()
@Field()
@@ -50,7 +50,7 @@ class UpdateServerlessFunctionInputUpdates {
@IsString()
@Field({ nullable: true })
@IsOptional()
handlerPath?: string;
sourceHandlerPath?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@@ -26,7 +26,7 @@ export enum ServerlessFunctionRuntime {
NODE22 = 'nodejs22.x',
}
export const DEFAULT_HANDLER_PATH = 'src/index.ts';
export const DEFAULT_SOURCE_HANDLER_PATH = 'src/index.ts';
export const DEFAULT_BUILT_HANDLER_PATH = 'index.mjs';
export const DEFAULT_HANDLER_NAME = 'main';
@@ -43,8 +43,8 @@ export class ServerlessFunctionEntity
@Column({ nullable: false })
name: string;
@Column({ nullable: false, default: DEFAULT_HANDLER_PATH })
handlerPath: string;
@Column({ nullable: false, default: DEFAULT_SOURCE_HANDLER_PATH })
sourceHandlerPath: string;
@Column({ nullable: false, default: DEFAULT_BUILT_HANDLER_PATH })
builtHandlerPath: string;
@@ -26,6 +26,7 @@ import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metad
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module';
@Module({
imports: [
@@ -45,6 +46,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
PermissionsModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
FunctionBuildModule,
ServerlessFunctionLayerModule,
SubscriptionsModule,
WorkspaceCacheModule,
@@ -48,8 +48,7 @@ import {
WorkflowVersionStepExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
import { cleanServerUrl } from 'src/utils/clean-server-url';
import { isServerlessFunctionBuilt } from 'src/engine/core-modules/serverless/drivers/utils/is-serverless-function-built';
import { buildAndUploadServerlessFunction } from 'src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function';
import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service';
const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5;
@@ -58,6 +57,7 @@ export class ServerlessFunctionService {
constructor(
private readonly fileStorageService: FileStorageService,
private readonly serverlessService: ServerlessService,
private readonly functionBuildService: FunctionBuildService,
private readonly serverlessFunctionLayerService: ServerlessFunctionLayerService,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@@ -210,16 +210,14 @@ export class ServerlessFunctionService {
// We keep that check to build functions
if (
!(await isServerlessFunctionBuilt({
!(await this.functionBuildService.isBuilt({
flatServerlessFunction,
version,
fileStorageService: this.fileStorageService,
}))
) {
await buildAndUploadServerlessFunction({
await this.functionBuildService.buildAndUpload({
flatServerlessFunction,
version,
fileStorageService: this.fileStorageService,
});
}
@@ -6,7 +6,7 @@ import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_HANDLER_NAME,
DEFAULT_HANDLER_PATH,
DEFAULT_SOURCE_HANDLER_PATH,
ServerlessFunctionRuntime,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
@@ -35,8 +35,9 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
id,
name: rawCreateServerlessFunctionInput.name,
description: rawCreateServerlessFunctionInput.description ?? null,
handlerPath:
rawCreateServerlessFunctionInput.handlerPath ?? DEFAULT_HANDLER_PATH,
sourceHandlerPath:
rawCreateServerlessFunctionInput.sourceHandlerPath ??
DEFAULT_SOURCE_HANDLER_PATH,
handlerName:
rawCreateServerlessFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME,
builtHandlerPath:
@@ -43,7 +43,7 @@ export const fromFlatServerlessFunctionToServerlessFunctionDto = ({
runtime: flatServerlessFunction.runtime,
timeoutSeconds: flatServerlessFunction.timeoutSeconds,
latestVersion: flatServerlessFunction.latestVersion ?? undefined,
handlerPath: flatServerlessFunction.handlerPath,
sourceHandlerPath: flatServerlessFunction.sourceHandlerPath,
builtHandlerPath: flatServerlessFunction.builtHandlerPath,
handlerName: flatServerlessFunction.handlerName,
publishedVersions: flatServerlessFunction.publishedVersions,
@@ -13,14 +13,17 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
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';
import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service';
@Injectable()
export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'serverlessFunction',
) {
constructor(private readonly fileStorageService: FileStorageService) {
constructor(
private readonly fileStorageService: FileStorageService,
private readonly functionBuildService: FunctionBuildService,
) {
super();
}
@@ -66,10 +69,9 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra
});
}
}
await buildAndUploadServerlessFunction({
await this.functionBuildService.buildAndUpload({
flatServerlessFunction: serverlessFunction,
version: 'draft',
fileStorageService: this.fileStorageService,
});
}
@@ -14,7 +14,7 @@ import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-f
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';
import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service';
@Injectable()
export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
@@ -24,6 +24,7 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra
constructor(
private readonly fileStorageService: FileStorageService,
private readonly serverlessService: ServerlessService,
private readonly functionBuildService: FunctionBuildService,
) {
super();
}
@@ -86,10 +87,9 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra
await this.fileStorageService.writeFolder(code, fileFolder);
await buildAndUploadServerlessFunction({
await this.functionBuildService.buildAndUpload({
flatServerlessFunction,
version: 'draft',
fileStorageService: this.fileStorageService,
});
}
}
@@ -73,9 +73,10 @@ import { UpdateViewGroupActionHandlerService } from 'src/engine/workspace-manage
import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service';
import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service';
import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service';
import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module';
@Module({
imports: [WorkspaceSchemaManagerModule],
imports: [WorkspaceSchemaManagerModule, FunctionBuildModule],
providers: [
CreateFieldActionHandlerService,
UpdateFieldActionHandlerService,