Migrate serverless function service to v2 (#17285)

# Introduction

In this PR we're migrating the serverless function service that was
using the SF repo directly to the v2 build and runner.
The whole serverless engine now deals with flat entities only

## Resolvers
Refactored the resolvers ( serverlessFunction, route, database and cron
trigger) :
- return types to `dto`
- Standardized the flat to dto transpilation within the resolvers
- Find and findMany passing by the cached data

## Services
Refactored the services ( serverlessFunction, route, database and cron
trigger) :
- return type to be `flat`
- always calling v2 and computing cache

## New additional caches
- application variables ( cf
https://github.com/twentyhq/core-team-issues/issues/2116 )
- serverless function layer

## What to test:
- CRUD ( database trigger  , route trigger, cron trigger, serverless
function through workflows  )
- Duplicating a workflow with a serverless function code node  

## Concerns
We need to implement the cron that will hard delete soft deleted s3
serverless functions, not in this PR though ( cf
https://github.com/twentyhq/twenty/pull/17285#discussion_r2709168570 and
https://github.com/twentyhq/core-team-issues/issues/2118 )
This commit is contained in:
Paul Rastoin
2026-01-20 19:32:22 +01:00
committed by GitHub
parent ccf2eae684
commit e35b4b86cb
50 changed files with 1415 additions and 505 deletions
@@ -177,6 +177,7 @@ export class ApplicationSyncService {
packageJson,
yarnLock,
},
workspaceId,
);
}
@@ -184,6 +185,7 @@ export class ApplicationSyncService {
{
applicationVariables: manifest.application.applicationVariables,
applicationId: application.id,
workspaceId,
},
);
@@ -1,21 +1,30 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
import { ApplicationVariableEntityResolver } from 'src/engine/core-modules/applicationVariable/application-variable.resolver';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
import { WorkspaceApplicationVariableMapCacheService } from 'src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature([ApplicationVariableEntity]),
TypeOrmModule.forFeature([ApplicationVariableEntity]),
PermissionsModule,
WorkspaceCacheModule,
],
providers: [
ApplicationVariableEntityService,
ApplicationVariableEntityResolver,
WorkspaceApplicationVariableMapCacheService,
],
exports: [
ApplicationVariableEntityService,
WorkspaceApplicationVariableMapCacheService,
],
exports: [ApplicationVariableEntityService],
})
export class ApplicationVariableEntityModule {}
@@ -6,6 +6,8 @@ import { PermissionFlagType } from 'twenty-shared/constants';
import { ApplicationVariableEntityExceptionFilter } from 'src/engine/core-modules/applicationVariable/application-variable-exception-filter';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
import { UpdateApplicationVariableEntityInput } from 'src/engine/core-modules/applicationVariable/dtos/update-application-variable.input';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@@ -23,8 +25,14 @@ export class ApplicationVariableEntityResolver {
@Mutation(() => Boolean)
async updateOneApplicationVariable(
@Args() { key, value, applicationId }: UpdateApplicationVariableEntityInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
await this.applicationVariableService.update({ key, value, applicationId });
await this.applicationVariableService.update({
key,
value,
applicationId,
workspaceId,
});
return true;
}
@@ -1,3 +1,4 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
@@ -5,19 +6,24 @@ import { In, Not, Repository } from 'typeorm';
import { ApplicationVariables } from 'twenty-shared/application';
import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@Injectable()
export class ApplicationVariableEntityService {
constructor(
@InjectRepository(ApplicationVariableEntity)
private readonly applicationVariableRepository: Repository<ApplicationVariableEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
async update({
key,
value,
applicationId,
workspaceId,
}: Pick<ApplicationVariableEntity, 'key' | 'value'> & {
applicationId: string;
workspaceId: string;
}) {
await this.applicationVariableRepository.update(
{ key, applicationId },
@@ -25,14 +31,20 @@ export class ApplicationVariableEntityService {
value,
},
);
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'applicationVariableMaps',
]);
}
async upsertManyApplicationVariableEntities({
applicationVariables,
applicationId,
workspaceId,
}: {
applicationVariables?: ApplicationVariables;
applicationId: string;
workspaceId: string;
}) {
if (!isDefined(applicationVariables)) {
return;
@@ -74,5 +86,9 @@ export class ApplicationVariableEntityService {
applicationId,
key: Not(In(Object.keys(applicationVariables))),
});
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'applicationVariableMaps',
]);
}
}
@@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type';
import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util';
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
@Injectable()
@WorkspaceCache('applicationVariableMaps')
export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheProvider<ApplicationVariableCacheMaps> {
constructor(
@InjectRepository(ApplicationVariableEntity)
private readonly applicationVariableRepository: Repository<ApplicationVariableEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<ApplicationVariableCacheMaps> {
const applicationVariableEntities = await this.applicationVariableRepository
.createQueryBuilder('applicationVariable')
.innerJoin('applicationVariable.application', 'application')
.where('application.workspaceId = :workspaceId', { workspaceId })
.getMany();
const applicationVariableMaps: ApplicationVariableCacheMaps = {
byId: {},
byApplicationId: {},
};
for (const entity of applicationVariableEntities) {
const flatApplicationVariable =
fromApplicationVariableEntityToFlatApplicationVariable(entity);
applicationVariableMaps.byId[flatApplicationVariable.id] =
flatApplicationVariable;
if (!isDefined(flatApplicationVariable.applicationId)) {
continue;
}
if (
!isDefined(
applicationVariableMaps.byApplicationId[
flatApplicationVariable.applicationId
],
)
) {
applicationVariableMaps.byApplicationId[
flatApplicationVariable.applicationId
] = [flatApplicationVariable];
continue;
}
applicationVariableMaps.byApplicationId[
flatApplicationVariable.applicationId
]?.push(flatApplicationVariable);
}
return applicationVariableMaps;
}
}
@@ -0,0 +1,6 @@
import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type';
export type ApplicationVariableCacheMaps = {
byId: Partial<Record<string, FlatApplicationVariable>>;
byApplicationId: Partial<Record<string, FlatApplicationVariable[]>>;
};
@@ -0,0 +1,4 @@
import { type ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type';
export type FlatApplicationVariable = FlatEntityFrom<ApplicationVariableEntity>;
@@ -0,0 +1,15 @@
import { type ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type';
export const fromApplicationVariableEntityToFlatApplicationVariable = (
entity: ApplicationVariableEntity,
): FlatApplicationVariable => ({
id: entity.id,
key: entity.key,
value: entity.value,
description: entity.description,
isSecret: entity.isSecret,
applicationId: entity.applicationId,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
});
@@ -1,5 +1,6 @@
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
import { type ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import { type ServerlessFunctionEntity } 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';
export type ServerlessExecuteError = {
errorType: string;
@@ -15,16 +16,17 @@ export type ServerlessExecuteResult = {
error?: ServerlessExecuteError;
};
// TODO refactor to be using FlatServerlessFunction
export interface ServerlessDriver {
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
delete(flatServerlessFunction: FlatServerlessFunction): Promise<void>;
execute({
serverlessFunction,
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
}: {
serverlessFunction: ServerlessFunctionEntity;
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
@@ -36,16 +36,15 @@ import {
LambdaBuildDirectoryManager,
NODE_LAYER_SUBFOLDER,
} from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import {
type ServerlessFunctionEntity,
ServerlessFunctionRuntime,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h
@@ -126,11 +125,11 @@ export class LambdaDriver implements ServerlessDriver {
}
private async waitFunctionUpdates(
serverlessFunction: ServerlessFunctionEntity,
flatServerlessFunction: FlatServerlessFunction,
maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS,
) {
const waitParams = {
FunctionName: serverlessFunction.id,
FunctionName: flatServerlessFunction.id,
};
await waitUntilFunctionUpdatedV2(
@@ -139,14 +138,16 @@ export class LambdaDriver implements ServerlessDriver {
);
}
private getLayerName(serverlessFunction: ServerlessFunctionEntity) {
return serverlessFunction.serverlessFunctionLayer.checksum;
private getLayerName(
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
return flatServerlessFunctionLayer.checksum;
}
private async createLayerIfNotExists(
serverlessFunction: ServerlessFunctionEntity,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
): Promise<string> {
const layerName = this.getLayerName(serverlessFunction);
const layerName = this.getLayerName(flatServerlessFunctionLayer);
const listLayerParams: ListLayerVersionsCommandInput = {
LayerName: layerName,
@@ -172,7 +173,10 @@ export class LambdaDriver implements ServerlessDriver {
NODE_LAYER_SUBFOLDER,
);
await copyAndBuildDependencies(nodeDependenciesFolder, serverlessFunction);
await copyAndBuildDependencies(
nodeDependenciesFolder,
flatServerlessFunctionLayer,
);
await createZipFile(sourceTemporaryDir, lambdaZipPath);
@@ -201,11 +205,11 @@ export class LambdaDriver implements ServerlessDriver {
}
private async getLambdaExecutor(
serverlessFunction: ServerlessFunctionEntity,
flatServerlessFunction: FlatServerlessFunction,
) {
try {
const getFunctionCommand: GetFunctionCommand = new GetFunctionCommand({
FunctionName: serverlessFunction.id,
FunctionName: flatServerlessFunction.id,
});
return await (await this.getLambdaClient()).send(getFunctionCommand);
@@ -216,20 +220,23 @@ export class LambdaDriver implements ServerlessDriver {
}
}
async delete(serverlessFunction: ServerlessFunctionEntity) {
const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction);
async delete(flatServerlessFunction: FlatServerlessFunction) {
const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction);
if (isDefined(lambdaExecutor)) {
const deleteFunctionCommand = new DeleteFunctionCommand({
FunctionName: serverlessFunction.id,
FunctionName: flatServerlessFunction.id,
});
await (await this.getLambdaClient()).send(deleteFunctionCommand);
}
}
private async isAlreadyBuilt(serverlessFunction: ServerlessFunctionEntity) {
const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction);
private async isAlreadyBuilt(
flatServerlessFunction: FlatServerlessFunction,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction);
if (!isDefined(lambdaExecutor)) {
return false;
@@ -238,28 +245,38 @@ export class LambdaDriver implements ServerlessDriver {
const layers = lambdaExecutor.Configuration?.Layers;
if (!isDefined(layers) || layers.length !== 1) {
await this.delete(serverlessFunction);
await this.delete(flatServerlessFunction);
return false;
}
const layerName = this.getLayerName(serverlessFunction);
const layerName = this.getLayerName(flatServerlessFunctionLayer);
if (layers[0].Arn?.includes(layerName)) {
return true;
}
await this.delete(serverlessFunction);
await this.delete(flatServerlessFunction);
return false;
}
private async build(serverlessFunction: ServerlessFunctionEntity) {
if (await this.isAlreadyBuilt(serverlessFunction)) {
private async build(
flatServerlessFunction: FlatServerlessFunction,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
if (
await this.isAlreadyBuilt(
flatServerlessFunction,
flatServerlessFunctionLayer,
)
) {
return;
}
const layerArn = await this.createLayerIfNotExists(serverlessFunction);
const layerArn = await this.createLayerIfNotExists(
flatServerlessFunctionLayer,
);
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
@@ -274,11 +291,11 @@ export class LambdaDriver implements ServerlessDriver {
Code: {
ZipFile: await fs.readFile(lambdaZipPath),
},
FunctionName: serverlessFunction.id,
FunctionName: flatServerlessFunction.id,
Layers: [layerArn],
Handler: 'index.handler',
Role: this.options.lambdaRole,
Runtime: serverlessFunction.runtime,
Runtime: flatServerlessFunction.runtime,
Timeout: 900, // timeout is handled by the serverless function service
};
@@ -305,23 +322,25 @@ export class LambdaDriver implements ServerlessDriver {
}
async execute({
serverlessFunction,
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
}: {
serverlessFunction: ServerlessFunctionEntity;
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult> {
await this.build(serverlessFunction);
await this.waitFunctionUpdates(serverlessFunction);
await this.build(flatServerlessFunction, flatServerlessFunctionLayer);
await this.waitFunctionUpdates(flatServerlessFunction);
const startTime = Date.now();
const folderPath = getServerlessFolder({
serverlessFunction,
const folderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
});
@@ -340,7 +359,7 @@ export class LambdaDriver implements ServerlessDriver {
try {
builtBundleFilePath = await buildServerlessFunctionInMemory({
sourceTemporaryDir,
handlerPath: serverlessFunction.handlerPath,
handlerPath: flatServerlessFunction.handlerPath,
});
} catch (error) {
return formatBuildError(error, startTime);
@@ -354,11 +373,11 @@ export class LambdaDriver implements ServerlessDriver {
params: payload,
code: compiledCode,
env: env ?? {},
handlerName: serverlessFunction.handlerName,
handlerName: flatServerlessFunction.handlerName,
};
const params: InvokeCommandInput = {
FunctionName: serverlessFunction.id,
FunctionName: flatServerlessFunction.id,
Payload: JSON.stringify(executorPayload),
LogType: LogType.Tail,
};
@@ -14,9 +14,10 @@ import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/dri
import { formatBuildError } from 'src/engine/core-modules/serverless/drivers/utils/format-build-error';
import { ConsoleListener } from 'src/engine/core-modules/serverless/drivers/utils/intercept-console';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import { type ServerlessFunctionEntity } 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';
export interface LocalDriverOptions {
fileStorageService: FileStorageService;
@@ -30,53 +31,55 @@ export class LocalDriver implements ServerlessDriver {
}
private getInMemoryLayerFolderPath = (
serverlessFunction: ServerlessFunctionEntity,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) => {
return join(
SERVERLESS_TMPDIR_FOLDER,
serverlessFunction.serverlessFunctionLayer.checksum,
);
return join(SERVERLESS_TMPDIR_FOLDER, flatServerlessFunctionLayer.checksum);
};
private async createLayerIfNotExists(
serverlessFunction: ServerlessFunctionEntity,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
const inMemoryLayerFolderPath =
this.getInMemoryLayerFolderPath(serverlessFunction);
const inMemoryLayerFolderPath = this.getInMemoryLayerFolderPath(
flatServerlessFunctionLayer,
);
try {
await fs.access(inMemoryLayerFolderPath);
} catch {
await copyAndBuildDependencies(
inMemoryLayerFolderPath,
serverlessFunction,
flatServerlessFunctionLayer,
);
}
}
async delete() {}
private async build(serverlessFunction: ServerlessFunctionEntity) {
await this.createLayerIfNotExists(serverlessFunction);
private async build(
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
await this.createLayerIfNotExists(flatServerlessFunctionLayer);
}
async execute({
serverlessFunction,
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
}: {
serverlessFunction: ServerlessFunctionEntity;
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult> {
await this.build(serverlessFunction);
await this.build(flatServerlessFunctionLayer);
const startTime = Date.now();
const folderPath = getServerlessFolder({
serverlessFunction,
const folderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
version,
});
@@ -95,7 +98,7 @@ export class LocalDriver implements ServerlessDriver {
try {
builtBundleFilePath = await buildServerlessFunctionInMemory({
sourceTemporaryDir,
handlerPath: serverlessFunction.handlerPath,
handlerPath: flatServerlessFunction.handlerPath,
});
} catch (error) {
return formatBuildError(error, startTime);
@@ -104,7 +107,7 @@ export class LocalDriver implements ServerlessDriver {
try {
await fs.symlink(
join(
this.getInMemoryLayerFolderPath(serverlessFunction),
this.getInMemoryLayerFolderPath(flatServerlessFunctionLayer),
'node_modules',
),
join(sourceTemporaryDir, 'node_modules'),
@@ -153,7 +156,7 @@ export class LocalDriver implements ServerlessDriver {
const runnerPath = await this.writeBootstrapRunner({
dir: sourceTemporaryDir,
builtFileAbsPath: builtBundleFilePath,
handlerName: serverlessFunction.handlerName,
handlerName: flatServerlessFunction.handlerName,
});
const { ok, result, error, stack, stdout, stderr } =
@@ -1,12 +1,14 @@
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type';
export const buildEnvVar = (serverlessFunction: ServerlessFunctionEntity) => {
return (serverlessFunction.application?.applicationVariables ?? []).reduce(
(acc, v) => {
acc[v.key] = String(v.value ?? '');
export const buildEnvVar = (
flatApplicationVariables: FlatApplicationVariable[],
): Record<string, string> => {
return flatApplicationVariables.reduce<Record<string, string>>(
(acc, flatApplicationVariable) => {
acc[flatApplicationVariable.key] = flatApplicationVariable.value;
return acc;
},
{} as Record<string, string>,
{},
);
};
@@ -4,21 +4,21 @@ import { join } from 'path';
import { promisify } from 'util';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
import type { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
const execFilePromise = promisify(execFile);
export const copyAndBuildDependencies = async (
buildDirectory: string,
serverlessFunction: ServerlessFunctionEntity,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) => {
await fs.mkdir(buildDirectory, {
recursive: true,
});
const packageJson = serverlessFunction.serverlessFunctionLayer.packageJson;
const packageJson = flatServerlessFunctionLayer.packageJson;
const yarnLock = serverlessFunction.serverlessFunctionLayer.yarnLock;
const yarnLock = flatServerlessFunctionLayer.yarnLock;
await fs.writeFile(
join(buildDirectory, 'package.json'),
@@ -6,27 +6,36 @@ import {
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
@Injectable()
export class ServerlessService implements ServerlessDriver {
constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {}
async delete(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
return this.driver.delete(serverlessFunction);
async delete(flatServerlessFunction: FlatServerlessFunction): Promise<void> {
return this.driver.delete(flatServerlessFunction);
}
async execute({
serverlessFunction,
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
}: {
serverlessFunction: ServerlessFunctionEntity;
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult> {
return this.driver.execute({ serverlessFunction, payload, version, env });
return this.driver.execute({
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
});
}
}
@@ -3,23 +3,25 @@ import { join } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { FileFolder } from 'twenty-shared/types';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
export const getServerlessFolder = ({
serverlessFunction,
export const getServerlessFolderOrThrow = ({
flatServerlessFunction,
version,
toDelete = false,
}: {
serverlessFunction: ServerlessFunctionEntity | FlatServerlessFunction;
flatServerlessFunction: FlatServerlessFunction;
version?: 'draft' | 'latest' | (string & NonNullable<unknown>);
toDelete?: boolean;
}) => {
if (version === 'latest' && !isDefined(serverlessFunction.latestVersion)) {
if (
version === 'latest' &&
!isDefined(flatServerlessFunction.latestVersion)
) {
throw new ServerlessFunctionException(
"Can't get 'latest' version when serverlessFunction 'latestVersion' is undefined",
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND,
@@ -27,14 +29,14 @@ export const getServerlessFolder = ({
}
const computedVersion =
version === 'latest' ? serverlessFunction.latestVersion : version;
version === 'latest' ? flatServerlessFunction.latestVersion : version;
return join(
'workspace-' + serverlessFunction.workspaceId,
'workspace-' + flatServerlessFunction.workspaceId,
toDelete
? FileFolder.ServerlessFunctionToDelete
: FileFolder.ServerlessFunction,
serverlessFunction.id,
flatServerlessFunction.id,
computedVersion || '',
);
};