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
@@ -1,4 +1,4 @@
// Default tool input schema matching the base-typescript-project template
// Default tool input schema matching the seed-project template
// Template params: { a: string; b: number; }
export const DEFAULT_TOOL_INPUT_SCHEMA = {
type: 'object',
@@ -56,6 +56,11 @@ export class CreateServerlessFunctionInput {
@IsOptional()
handlerPath?: string;
@IsString()
@Field({ nullable: true })
@IsOptional()
builtHandlerPath?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
@@ -65,6 +65,10 @@ export class ServerlessFunctionDTO {
@Field()
handlerPath: string;
@IsString()
@Field()
builtHandlerPath: string;
@IsString()
@Field()
handlerName: string;
@@ -27,6 +27,7 @@ export enum ServerlessFunctionRuntime {
}
export const DEFAULT_HANDLER_PATH = 'src/index.ts';
export const DEFAULT_BUILT_HANDLER_PATH = 'index.mjs';
export const DEFAULT_HANDLER_NAME = 'main';
@Entity('serverlessFunction')
@@ -45,6 +46,9 @@ export class ServerlessFunctionEntity
@Column({ nullable: false, default: DEFAULT_HANDLER_PATH })
handlerPath: string;
@Column({ nullable: false, default: DEFAULT_BUILT_HANDLER_PATH })
builtHandlerPath: string;
@Column({ nullable: false, default: DEFAULT_HANDLER_NAME })
handlerName: string;
@@ -8,6 +8,7 @@ import {
} from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { FileFolder } from 'twenty-shared/types';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
@@ -19,7 +20,7 @@ import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/serv
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var';
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 { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
@@ -47,6 +48,8 @@ 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';
const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5;
@@ -205,6 +208,21 @@ export class ServerlessFunctionService {
...buildEnvVar(flatApplicationVariables),
};
// We keep that check to build functions
if (
!(await isServerlessFunctionBuilt({
flatServerlessFunction,
version,
fileStorageService: this.fileStorageService,
}))
) {
await buildAndUploadServerlessFunction({
flatServerlessFunction,
version,
fileStorageService: this.fileStorageService,
});
}
const resultServerlessFunction = await this.callWithTimeout({
callback: () =>
this.serverlessService.execute({
@@ -299,19 +317,38 @@ export class ServerlessFunctionService {
? `${parseInt(existingFlatServerlessFunction.latestVersion, 10) + 1}`
: '1';
const draftFolderPath = getServerlessFolderOrThrow({
const draftSourceFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction: existingFlatServerlessFunction,
version: 'draft',
fileFolder: FileFolder.ServerlessFunction,
});
const newFolderPath = getServerlessFolderOrThrow({
const newSourceFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction: existingFlatServerlessFunction,
version: newVersion,
fileFolder: FileFolder.ServerlessFunction,
});
await this.fileStorageService.copy({
from: { folderPath: draftFolderPath },
to: { folderPath: newFolderPath },
from: { folderPath: draftSourceFolderPath },
to: { folderPath: newSourceFolderPath },
});
const draftBuiltFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction: existingFlatServerlessFunction,
version: 'draft',
fileFolder: FileFolder.BuiltFunction,
});
const newBuiltFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction: existingFlatServerlessFunction,
version: newVersion,
fileFolder: FileFolder.BuiltFunction,
});
await this.fileStorageService.copy({
from: { folderPath: draftBuiltFolderPath },
to: { folderPath: newBuiltFolderPath },
});
const newPublishedVersions = [
@@ -4,6 +4,7 @@ import { v4 } from 'uuid';
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant';
import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_HANDLER_NAME,
DEFAULT_HANDLER_PATH,
ServerlessFunctionRuntime,
@@ -38,6 +39,9 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
rawCreateServerlessFunctionInput.handlerPath ?? DEFAULT_HANDLER_PATH,
handlerName:
rawCreateServerlessFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME,
builtHandlerPath:
rawCreateServerlessFunctionInput.builtHandlerPath ??
DEFAULT_BUILT_HANDLER_PATH,
universalIdentifier:
rawCreateServerlessFunctionInput.universalIdentifier ?? v4(),
createdAt: currentDate.toISOString(),
@@ -44,6 +44,7 @@ export const fromFlatServerlessFunctionToServerlessFunctionDto = ({
timeoutSeconds: flatServerlessFunction.timeoutSeconds,
latestVersion: flatServerlessFunction.latestVersion ?? undefined,
handlerPath: flatServerlessFunction.handlerPath,
builtHandlerPath: flatServerlessFunction.builtHandlerPath,
handlerName: flatServerlessFunction.handlerName,
publishedVersions: flatServerlessFunction.publishedVersions,
toolInputSchema: flatServerlessFunction.toolInputSchema ?? undefined,