Rename serverlessFunction to logicFunction (#17494)

## Summary

Rename "Serverless Function" to "Logic Function" across the codebase for
clearer naming.

### Environment Variable Changes

| Old | New |
|-----|-----|
| `SERVERLESS_TYPE` | `LOGIC_FUNCTION_TYPE` |
| `SERVERLESS_LAMBDA_REGION` | `LOGIC_FUNCTION_LAMBDA_REGION` |
| `SERVERLESS_LAMBDA_ROLE` | `LOGIC_FUNCTION_LAMBDA_ROLE` |
| `SERVERLESS_LAMBDA_SUBHOSTING_URL` |
`LOGIC_FUNCTION_LAMBDA_SUBHOSTING_URL` |
| `SERVERLESS_LAMBDA_ACCESS_KEY_ID` |
`LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID` |
| `SERVERLESS_LAMBDA_SECRET_ACCESS_KEY` |
`LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY` |

### Breaking Changes

- Environment variables must be updated in production deployments
- Database migration renames `serverlessFunction` → `logicFunction`
tables
This commit is contained in:
Charles Bochet
2026-01-28 01:42:19 +01:00
committed by GitHub
parent 59d123d2b1
commit da6f1bbef3
351 changed files with 5054 additions and 5139 deletions
@@ -19,7 +19,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
case ApplicationExceptionCode.FIELD_NOT_FOUND:
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
case ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
throw new NotFoundError(exception);
case ApplicationExceptionCode.FORBIDDEN:
case ApplicationExceptionCode.INVALID_INPUT:
@@ -15,8 +15,8 @@ import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permi
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
@@ -32,8 +32,8 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ObjectMetadataModule,
FieldMetadataModule,
DataSourceModule,
ServerlessFunctionLayerModule,
ServerlessFunctionModule,
LogicFunctionLayerModule,
LogicFunctionModule,
WorkspaceMigrationModule,
PermissionsModule,
RoleModule,
@@ -9,8 +9,8 @@ import {
ObjectManifest,
RelationFieldManifest,
RoleManifest,
ServerlessFunctionManifest,
ServerlessFunctionTriggerManifest,
LogicFunctionManifest,
LogicFunctionTriggerManifest,
} from 'twenty-shared/application';
import { FieldMetadataType, HTTPMethod, Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -35,16 +35,16 @@ import { FieldPermissionService } from 'src/engine/metadata-modules/object-permi
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service';
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { LogicFunctionLayerService } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.service';
import { LogicFunctionV2Service } from 'src/engine/metadata-modules/logic-function/services/logic-function-v2.service';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import {
CronTriggerSettings,
DatabaseEventTriggerSettings,
HttpRouteTriggerSettings,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
@Injectable()
export class ApplicationSyncService {
@@ -53,10 +53,10 @@ export class ApplicationSyncService {
constructor(
private readonly applicationService: ApplicationService,
private readonly applicationVariableService: ApplicationVariableEntityService,
private readonly serverlessFunctionLayerService: ServerlessFunctionLayerService,
private readonly logicFunctionLayerService: LogicFunctionLayerService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly serverlessFunctionV2Service: ServerlessFunctionV2Service,
private readonly logicFunctionV2Service: LogicFunctionV2Service,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
@@ -102,19 +102,19 @@ export class ApplicationSyncService {
}
if (manifest.functions.length > 0) {
if (!isDefined(application.serverlessFunctionLayerId)) {
if (!isDefined(application.logicFunctionLayerId)) {
throw new ApplicationException(
`Failed to sync serverless function, could not find a serverless function layer.`,
`Failed to sync logic function, could not find a logic function layer.`,
ApplicationExceptionCode.FIELD_NOT_FOUND,
);
}
await this.syncServerlessFunctions({
serverlessFunctionsToSync: manifest.functions,
await this.syncLogicFunctions({
logicFunctionsToSync: manifest.functions,
code: manifest.sources,
workspaceId,
applicationId: application.id,
serverlessFunctionLayerId: application.serverlessFunctionLayerId,
logicFunctionLayerId: application.logicFunctionLayerId,
});
}
@@ -147,17 +147,17 @@ export class ApplicationSyncService {
description: manifest.application.description,
version: packageJson.version,
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
serverlessFunctionLayerId: null,
defaultServerlessFunctionRoleId: null,
logicFunctionLayerId: null,
defaultLogicFunctionRoleId: null,
workspaceId,
}));
let serverlessFunctionLayerId = application.serverlessFunctionLayerId;
let logicFunctionLayerId = application.logicFunctionLayerId;
if (manifest.functions.length > 0) {
if (!isDefined(serverlessFunctionLayerId)) {
serverlessFunctionLayerId = (
await this.serverlessFunctionLayerService.create(
if (!isDefined(logicFunctionLayerId)) {
logicFunctionLayerId = (
await this.logicFunctionLayerService.create(
{
packageJson,
yarnLock,
@@ -167,8 +167,8 @@ export class ApplicationSyncService {
).id;
}
await this.serverlessFunctionLayerService.update(
serverlessFunctionLayerId,
await this.logicFunctionLayerService.update(
logicFunctionLayerId,
{
packageJson,
yarnLock,
@@ -189,8 +189,8 @@ export class ApplicationSyncService {
name,
description: manifest.application.description,
version: packageJson.version,
serverlessFunctionLayerId,
defaultServerlessFunctionRoleId: null,
logicFunctionLayerId,
defaultLogicFunctionRoleId: null,
});
}
@@ -203,7 +203,7 @@ export class ApplicationSyncService {
workspaceId: string;
applicationId: string;
}) {
let defaultServerlessFunctionRoleId: string | null = null;
let defaultLogicFunctionRoleId: string | null = null;
for (const role of manifest.roles ?? []) {
let existingRole = await this.roleService.getRoleByUniversalIdentifier({
@@ -237,13 +237,13 @@ export class ApplicationSyncService {
existingRole.universalIdentifier ===
manifest.application.functionRoleUniversalIdentifier
) {
defaultServerlessFunctionRoleId = existingRole.id;
defaultLogicFunctionRoleId = existingRole.id;
}
}
if (isDefined(defaultServerlessFunctionRoleId)) {
if (isDefined(defaultLogicFunctionRoleId)) {
await this.applicationService.update(applicationId, {
defaultServerlessFunctionRoleId: defaultServerlessFunctionRoleId,
defaultLogicFunctionRoleId: defaultLogicFunctionRoleId,
});
}
}
@@ -849,149 +849,147 @@ export class ApplicationSyncService {
}
}
private async syncServerlessFunctions({
serverlessFunctionsToSync,
private async syncLogicFunctions({
logicFunctionsToSync,
code,
workspaceId,
applicationId,
serverlessFunctionLayerId,
logicFunctionLayerId,
}: {
serverlessFunctionsToSync: ServerlessFunctionManifest[];
logicFunctionsToSync: LogicFunctionManifest[];
workspaceId: string;
code: Sources;
applicationId: string;
serverlessFunctionLayerId: string;
logicFunctionLayerId: string;
}) {
const { flatServerlessFunctionMaps } =
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatServerlessFunctionMaps'],
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const applicationServerlessFunctions = Object.values(
flatServerlessFunctionMaps.byId,
const applicationLogicFunctions = Object.values(
flatLogicFunctionMaps.byId,
).filter(
(serverlessFunction) =>
isDefined(serverlessFunction) &&
serverlessFunction.applicationId === applicationId,
) as FlatServerlessFunction[];
(logicFunction) =>
isDefined(logicFunction) &&
logicFunction.applicationId === applicationId,
) as FlatLogicFunction[];
const serverlessFunctionsToSyncUniversalIdentifiers =
serverlessFunctionsToSync.map(
(serverlessFunction) => serverlessFunction.universalIdentifier,
const logicFunctionsToSyncUniversalIdentifiers = logicFunctionsToSync.map(
(logicFunction) => logicFunction.universalIdentifier,
);
const applicationLogicFunctionsUniversalIdentifiers =
applicationLogicFunctions.map(
(logicFunction) => logicFunction.universalIdentifier,
);
const applicationServerlessFunctionsUniversalIdentifiers =
applicationServerlessFunctions.map(
(serverlessFunction) => serverlessFunction.universalIdentifier,
);
const serverlessFunctionsToDelete = applicationServerlessFunctions.filter(
(serverlessFunction) =>
isDefined(serverlessFunction.universalIdentifier) &&
!serverlessFunctionsToSyncUniversalIdentifiers.includes(
serverlessFunction.universalIdentifier,
const logicFunctionsToDelete = applicationLogicFunctions.filter(
(logicFunction) =>
isDefined(logicFunction.universalIdentifier) &&
!logicFunctionsToSyncUniversalIdentifiers.includes(
logicFunction.universalIdentifier,
),
);
const serverlessFunctionsToUpdate = applicationServerlessFunctions.filter(
(serverlessFunction) =>
isDefined(serverlessFunction.universalIdentifier) &&
serverlessFunctionsToSyncUniversalIdentifiers.includes(
serverlessFunction.universalIdentifier,
const logicFunctionsToUpdate = applicationLogicFunctions.filter(
(logicFunction) =>
isDefined(logicFunction.universalIdentifier) &&
logicFunctionsToSyncUniversalIdentifiers.includes(
logicFunction.universalIdentifier,
),
);
const serverlessFunctionsToCreate = serverlessFunctionsToSync.filter(
(serverlessFunctionToSync) =>
!applicationServerlessFunctionsUniversalIdentifiers.includes(
serverlessFunctionToSync.universalIdentifier,
const logicFunctionsToCreate = logicFunctionsToSync.filter(
(logicFunctionToSync) =>
!applicationLogicFunctionsUniversalIdentifiers.includes(
logicFunctionToSync.universalIdentifier,
),
);
for (const serverlessFunctionToDelete of serverlessFunctionsToDelete) {
await this.serverlessFunctionV2Service.destroyOne({
destroyServerlessFunctionInput: { id: serverlessFunctionToDelete.id },
for (const logicFunctionToDelete of logicFunctionsToDelete) {
await this.logicFunctionV2Service.destroyOne({
destroyLogicFunctionInput: { id: logicFunctionToDelete.id },
workspaceId,
isSystemBuild: true,
});
}
for (const serverlessFunctionToUpdate of serverlessFunctionsToUpdate) {
const serverlessFunctionToSync = serverlessFunctionsToSync.find(
(serverlessFunction) =>
serverlessFunction.universalIdentifier ===
serverlessFunctionToUpdate.universalIdentifier,
for (const logicFunctionToUpdate of logicFunctionsToUpdate) {
const logicFunctionToSync = logicFunctionsToSync.find(
(logicFunction) =>
logicFunction.universalIdentifier ===
logicFunctionToUpdate.universalIdentifier,
);
if (!serverlessFunctionToSync) {
if (!logicFunctionToSync) {
throw new ApplicationException(
`Failed to find serverlessFunction to sync with universalIdentifier ${serverlessFunctionToUpdate.universalIdentifier}`,
ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
`Failed to find logicFunction to sync with universalIdentifier ${logicFunctionToUpdate.universalIdentifier}`,
ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const name =
serverlessFunctionToSync.name ??
parse(serverlessFunctionToSync.handlerName).name;
logicFunctionToSync.name ?? parse(logicFunctionToSync.handlerName).name;
const updateServerlessFunctionInput = {
id: serverlessFunctionToUpdate.id,
const updateLogicFunctionInput = {
id: logicFunctionToUpdate.id,
update: {
name,
code,
timeoutSeconds: serverlessFunctionToSync.timeoutSeconds,
sourceHandlerPath: serverlessFunctionToSync.sourceHandlerPath,
builtHandlerPath: serverlessFunctionToSync.builtHandlerPath,
handlerName: serverlessFunctionToSync.handlerName,
toolInputSchema: serverlessFunctionToSync.toolInputSchema,
isTool: serverlessFunctionToSync.isTool,
timeoutSeconds: logicFunctionToSync.timeoutSeconds,
sourceHandlerPath: logicFunctionToSync.sourceHandlerPath,
builtHandlerPath: logicFunctionToSync.builtHandlerPath,
handlerName: logicFunctionToSync.handlerName,
toolInputSchema: logicFunctionToSync.toolInputSchema,
isTool: logicFunctionToSync.isTool,
},
};
await this.serverlessFunctionV2Service.updateOne(
updateServerlessFunctionInput,
await this.logicFunctionV2Service.updateOne(
updateLogicFunctionInput,
workspaceId,
);
// Trigger settings are now embedded in the serverless function entity
// Trigger settings are now embedded in the logic function entity
// They are handled through the update input
}
for (const serverlessFunctionToCreate of serverlessFunctionsToCreate) {
for (const logicFunctionToCreate of logicFunctionsToCreate) {
const name =
serverlessFunctionToCreate.name ??
parse(serverlessFunctionToCreate.handlerName).name;
logicFunctionToCreate.name ??
parse(logicFunctionToCreate.handlerName).name;
const createServerlessFunctionInput = {
const createLogicFunctionInput = {
name,
code,
universalIdentifier: serverlessFunctionToCreate.universalIdentifier,
timeoutSeconds: serverlessFunctionToCreate.timeoutSeconds,
sourceHandlerPath: serverlessFunctionToCreate.sourceHandlerPath,
handlerName: serverlessFunctionToCreate.handlerName,
builtHandlerPath: serverlessFunctionToCreate.builtHandlerPath,
universalIdentifier: logicFunctionToCreate.universalIdentifier,
timeoutSeconds: logicFunctionToCreate.timeoutSeconds,
sourceHandlerPath: logicFunctionToCreate.sourceHandlerPath,
handlerName: logicFunctionToCreate.handlerName,
builtHandlerPath: logicFunctionToCreate.builtHandlerPath,
applicationId,
serverlessFunctionLayerId,
toolInputSchema: serverlessFunctionToCreate.toolInputSchema,
isTool: serverlessFunctionToCreate.isTool,
logicFunctionLayerId,
toolInputSchema: logicFunctionToCreate.toolInputSchema,
isTool: logicFunctionToCreate.isTool,
};
await this.serverlessFunctionV2Service.createOne({
createServerlessFunctionInput,
await this.logicFunctionV2Service.createOne({
createLogicFunctionInput,
workspaceId,
applicationId,
});
// Trigger settings are now embedded in the serverless function entity
// Trigger settings are now embedded in the logic function entity
// They are handled through the create input
}
}
private extractTriggerSettingsFromManifest(
triggers: ServerlessFunctionTriggerManifest[] = [],
triggers: LogicFunctionTriggerManifest[] = [],
): {
cronTriggerSettings: CronTriggerSettings | null;
databaseEventTriggerSettings: DatabaseEventTriggerSettings | null;
@@ -16,7 +16,7 @@ import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVa
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity';
@Entity({ name: 'application', schema: 'core' })
@@ -54,13 +54,13 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
sourcePath: string;
@Column({ nullable: true, type: 'uuid' })
serverlessFunctionLayerId: string | null;
logicFunctionLayerId: string | null;
@Column({ nullable: true, type: 'uuid' })
defaultServerlessFunctionRoleId: string | null;
defaultLogicFunctionRoleId: string | null;
@Field(() => RoleDTO, { nullable: true })
defaultServerlessFunctionRole: RoleDTO | null;
defaultLogicFunctionRole: RoleDTO | null;
@Column({ nullable: false, type: 'boolean', default: true })
canBeUninstalled: boolean;
@@ -71,13 +71,13 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
agents: Relation<AgentEntity[]>;
@OneToMany(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.application,
() => LogicFunctionEntity,
(logicFunction) => logicFunction.application,
{
onDelete: 'CASCADE',
},
)
serverlessFunctions: Relation<ServerlessFunctionEntity[]>;
logicFunctions: Relation<LogicFunctionEntity[]>;
@OneToMany(() => ObjectMetadataEntity, (object) => object.application, {
onDelete: 'CASCADE',
@@ -7,7 +7,7 @@ import { CustomException } from 'src/utils/custom-exception';
export enum ApplicationExceptionCode {
OBJECT_NOT_FOUND = 'OBJECT_NOT_FOUND',
FIELD_NOT_FOUND = 'FIELD_NOT_FOUND',
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
LOGIC_FUNCTION_NOT_FOUND = 'LOGIC_FUNCTION_NOT_FOUND',
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
FORBIDDEN = 'FORBIDDEN',
@@ -22,8 +22,8 @@ const getApplicationExceptionUserFriendlyMessage = (
return msg`Object not found.`;
case ApplicationExceptionCode.FIELD_NOT_FOUND:
return msg`Field not found.`;
case ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
return msg`Serverless function not found.`;
case ApplicationExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
return msg`Logic function not found.`;
case ApplicationExceptionCode.ENTITY_NOT_FOUND:
return msg`Entity not found.`;
case ApplicationExceptionCode.APPLICATION_NOT_FOUND:
@@ -35,7 +35,7 @@ export class ApplicationService {
if (
!isDefined(application) ||
!isDefined(application.defaultServerlessFunctionRoleId)
!isDefined(application.defaultLogicFunctionRoleId)
) {
throw new ApplicationException(
`Could not find application ${applicationId}`,
@@ -43,7 +43,7 @@ export class ApplicationService {
);
}
return application.defaultServerlessFunctionRoleId;
return application.defaultLogicFunctionRoleId;
}
async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({
@@ -118,7 +118,7 @@ export class ApplicationService {
return this.applicationRepository.find({
where: { workspaceId },
relations: [
'serverlessFunctions',
'logicFunctions',
'agents',
'objects',
'applicationVariables',
@@ -133,7 +133,7 @@ export class ApplicationService {
const application = await this.applicationRepository.findOne({
where: { workspaceId, id: applicationId },
relations: [
'serverlessFunctions',
'logicFunctions',
'agents',
'objects',
'applicationVariables',
@@ -210,7 +210,7 @@ export class ApplicationService {
const twentyStandardApplication = await this.create(
{
...TWENTY_STANDARD_APPLICATION,
serverlessFunctionLayerId: null,
logicFunctionLayerId: null,
workspaceId,
canBeUninstalled: false,
},
@@ -246,7 +246,7 @@ export class ApplicationService {
universalIdentifier: applicationId,
workspaceId: workspaceId,
id: applicationId,
serverlessFunctionLayerId: null,
logicFunctionLayerId: null,
canBeUninstalled: false,
},
queryRunner,
@@ -3,7 +3,7 @@ import { type ApplicationEntity } from 'src/engine/core-modules/application/appl
export const APPLICATION_ENTITY_RELATION_PROPERTIES = [
'workspace',
'agents',
'serverlessFunctions',
'logicFunctions',
'objects',
'applicationVariables',
] as const satisfies (keyof ApplicationEntity)[];
@@ -12,7 +12,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicationVariable/dtos/application-variable.dto';
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
@ObjectType('Application')
@@ -47,17 +47,17 @@ export class ApplicationDTO {
@IsOptional()
@IsString()
@Field({ nullable: true })
defaultServerlessFunctionRoleId?: string;
defaultLogicFunctionRoleId?: string;
@IsOptional()
@Field(() => RoleDTO, { nullable: true })
defaultServerlessFunctionRole?: RoleDTO;
defaultLogicFunctionRole?: RoleDTO;
@Field(() => [AgentDTO])
agents?: AgentDTO[];
@Field(() => [ServerlessFunctionDTO])
serverlessFunctions?: ServerlessFunctionDTO[];
@Field(() => [LogicFunctionDTO])
logicFunctions?: LogicFunctionDTO[];
@Field(() => [ObjectMetadataDTO])
objects?: ObjectMetadataDTO[];
@@ -27,9 +27,9 @@ import {
type MonitoringTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/workspace-event/monitoring/monitoring';
import {
type SERVERLESS_FUNCTION_EXECUTED_EVENT,
type ServerlessFunctionExecutedTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed';
type LOGIC_FUNCTION_EXECUTED_EVENT,
type LogicFunctionExecutedTrackEvent,
} from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
import {
type USER_SIGNUP_EVENT,
type UserSignupTrackEvent,
@@ -47,7 +47,7 @@ import {
export type TrackEventName =
| typeof CUSTOM_DOMAIN_ACTIVATED_EVENT
| typeof CUSTOM_DOMAIN_DEACTIVATED_EVENT
| typeof SERVERLESS_FUNCTION_EXECUTED_EVENT
| typeof LOGIC_FUNCTION_EXECUTED_EVENT
| typeof WEBHOOK_RESPONSE_EVENT
| typeof WORKSPACE_ENTITY_CREATED_EVENT
| typeof MONITORING_EVENT
@@ -61,7 +61,7 @@ export type TrackEventName =
export interface TrackEvents {
[CUSTOM_DOMAIN_ACTIVATED_EVENT]: CustomDomainActivatedTrackEvent;
[CUSTOM_DOMAIN_DEACTIVATED_EVENT]: CustomDomainDeactivatedTrackEvent;
[SERVERLESS_FUNCTION_EXECUTED_EVENT]: ServerlessFunctionExecutedTrackEvent;
[LOGIC_FUNCTION_EXECUTED_EVENT]: LogicFunctionExecutedTrackEvent;
[WEBHOOK_RESPONSE_EVENT]: WebhookResponseTrackEvent;
[WORKSPACE_ENTITY_CREATED_EVENT]: WorkspaceEntityCreatedTrackEvent;
[USER_SIGNUP_EVENT]: UserSignupTrackEvent;
@@ -0,0 +1,21 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
export const LOGIC_FUNCTION_EXECUTED_EVENT = 'Logic Function Executed' as const;
export const logicFunctionExecutedSchema = z.strictObject({
event: z.literal(LOGIC_FUNCTION_EXECUTED_EVENT),
properties: z.strictObject({
duration: z.number(),
status: z.enum(['IDLE', 'SUCCESS', 'ERROR']),
errorType: z.string().optional(),
functionId: z.string(),
functionName: z.string(),
}),
});
export type LogicFunctionExecutedTrackEvent = z.infer<
typeof logicFunctionExecutedSchema
>;
registerEvent(LOGIC_FUNCTION_EXECUTED_EVENT, logicFunctionExecutedSchema);
@@ -1,25 +0,0 @@
import { z } from 'zod';
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
'Serverless Function Executed' as const;
export const serverlessFunctionExecutedSchema = z.strictObject({
event: z.literal(SERVERLESS_FUNCTION_EXECUTED_EVENT),
properties: z.strictObject({
duration: z.number(),
status: z.enum(['IDLE', 'SUCCESS', 'ERROR']),
errorType: z.string().optional(),
functionId: z.string(),
functionName: z.string(),
}),
});
export type ServerlessFunctionExecutedTrackEvent = z.infer<
typeof serverlessFunctionExecutedSchema
>;
registerEvent(
SERVERLESS_FUNCTION_EXECUTED_EVENT,
serverlessFunctionExecutedSchema,
);
@@ -47,8 +47,8 @@ import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service';
import { SearchModule } from 'src/engine/core-modules/search/search.module';
import { serverlessModuleFactory } from 'src/engine/core-modules/serverless/serverless-module.factory';
import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module';
import { logicFunctionExecutorModuleFactory } from 'src/engine/core-modules/logic-function-executor/logic-function-executor-module.factory';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.module';
import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module';
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
@@ -141,8 +141,8 @@ import { FileModule } from './file/file.module';
CacheStorageModule,
AiModelsModule,
AiBillingModule,
ServerlessModule.forRootAsync({
useFactory: serverlessModuleFactory,
LogicFunctionExecutorModule.forRootAsync({
useFactory: logicFunctionExecutorModuleFactory,
inject: [TwentyConfigService, FileStorageService],
}),
CodeInterpreterModule.forRootAsync({
@@ -24,10 +24,10 @@ export const fileFolderConfigs: Record<FileFolder, FileFolderConfig> = {
[FileFolder.PersonPicture]: {
ignoreExpirationToken: false,
},
[FileFolder.ServerlessFunction]: {
[FileFolder.LogicFunction]: {
ignoreExpirationToken: false,
},
[FileFolder.ServerlessFunctionToDelete]: {
[FileFolder.LogicFunctionToDelete]: {
ignoreExpirationToken: false,
},
[FileFolder.File]: {
@@ -20,7 +20,7 @@ export const checkFilePath = (filePath: string): string => {
}
if (
folder !== kebabCase(FileFolder.ServerlessFunction) &&
folder !== kebabCase(FileFolder.LogicFunction) &&
size &&
// @ts-expect-error legacy noImplicitAny
!settings.storage.imageCropSizes[folder]?.includes(size)
@@ -10,9 +10,9 @@ import { Command, CommandRunner, Option } from 'nest-commander';
const execFilePromise = promisify(execFile);
@Command({
name: 'serverless:add-packages',
name: 'logic-function-executor:add-packages',
description:
'Create a new serverless layer version and install packages in it',
'Create a new logic function executor layer version and install packages in it',
})
export class AddPackagesCommand extends CommandRunner {
private readonly logger = new Logger(AddPackagesCommand.name);
@@ -36,7 +36,7 @@ export class AddPackagesCommand extends CommandRunner {
this.logger.log('');
const layersFolder = this.getAbsoluteFilePath(
`src/engine/core-modules/serverless/drivers/layers`,
`src/engine/core-modules/logic-function-executor/drivers/layers`,
);
const currentVersion = await this.getLastLayerVersion();
@@ -108,7 +108,7 @@ export class AddPackagesCommand extends CommandRunner {
private async getLastLayerVersion() {
const filePath = this.getAbsoluteFilePath(
'src/engine/core-modules/serverless/drivers/layers/last-layer-version.ts',
'src/engine/core-modules/logic-function-executor/drivers/layers/last-layer-version.ts',
);
const content = await fs.readFile(filePath, 'utf8');
@@ -123,7 +123,7 @@ export class AddPackagesCommand extends CommandRunner {
private async updateLastLayerVersion(newVersion: number) {
const filePath = this.getAbsoluteFilePath(
'src/engine/core-modules/serverless/drivers/layers/last-layer-version.ts',
'src/engine/core-modules/logic-function-executor/drivers/layers/last-layer-version.ts',
);
await fs.writeFile(
@@ -0,0 +1,7 @@
import { join } from 'path';
import { tmpdir } from 'os';
export const LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER = join(
tmpdir(),
'logic-function-executor-tmpdir',
);
@@ -0,0 +1,22 @@
import {
type LogicFunctionExecutorDriver,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function-executor/drivers/interfaces/logic-function-executor-driver.interface';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
export class DisabledDriver implements LogicFunctionExecutorDriver {
async delete(): Promise<void> {
// No-op when disabled
}
async execute(): Promise<LogicFunctionExecuteResult> {
throw new LogicFunctionException(
'Logic function execution is disabled. Set LOGIC_FUNCTION_TYPE to LOCAL or LAMBDA to enable.',
LogicFunctionExceptionCode.LOGIC_FUNCTION_DISABLED,
);
}
}
@@ -0,0 +1,34 @@
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
import { type LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
export type LogicFunctionExecuteError = {
errorType: string;
errorMessage: string;
stackTrace: string | string[];
};
export type LogicFunctionExecuteResult = {
data: object | null;
duration: number;
logs: string;
status: LogicFunctionExecutionStatus;
error?: LogicFunctionExecuteError;
};
export interface LogicFunctionExecutorDriver {
delete(flatLogicFunction: FlatLogicFunction): Promise<void>;
execute({
flatLogicFunction,
flatLogicFunctionLayer,
payload,
version,
env,
}: {
flatLogicFunction: FlatLogicFunction;
flatLogicFunctionLayer: FlatLogicFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<LogicFunctionExecuteResult>;
}
@@ -23,27 +23,27 @@ import { isDefined } from 'twenty-shared/utils';
import { FileFolder } from 'twenty-shared/types';
import {
type ServerlessDriver,
type ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
type LogicFunctionExecutorDriver,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function-executor/drivers/interfaces/logic-function-executor-driver.interface';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
import { copyExecutor } from 'src/engine/core-modules/serverless/drivers/utils/copy-executor';
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
import { copyAndBuildDependencies } from 'src/engine/core-modules/logic-function-executor/drivers/utils/copy-and-build-dependencies';
import { copyExecutor } from 'src/engine/core-modules/logic-function-executor/drivers/utils/copy-executor';
import { createZipFile } from 'src/engine/core-modules/logic-function-executor/drivers/utils/create-zip-file';
import {
LambdaBuildDirectoryManager,
NODE_LAYER_SUBFOLDER,
} 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 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 { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
} from 'src/engine/core-modules/logic-function-executor/drivers/utils/lambda-build-directory-manager';
import { getLogicFunctionFolderOrThrow } from 'src/engine/core-modules/logic-function-executor/utils/get-logic-function-folder-or-throw.utils';
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-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';
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
@@ -63,7 +63,7 @@ export interface LambdaDriverOptions extends LambdaClientConfig {
subhostingRole?: string;
}
export class LambdaDriver implements ServerlessDriver {
export class LambdaDriver implements LogicFunctionExecutorDriver {
private lambdaClient: Lambda | undefined;
private credentialsExpiry: Date | null = null;
private readonly options: LambdaDriverOptions;
@@ -125,11 +125,11 @@ export class LambdaDriver implements ServerlessDriver {
}
private async waitFunctionUpdates(
flatServerlessFunction: FlatServerlessFunction,
flatLogicFunction: FlatLogicFunction,
maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS,
) {
const waitParams = {
FunctionName: flatServerlessFunction.id,
FunctionName: flatLogicFunction.id,
};
await waitUntilFunctionUpdatedV2(
@@ -138,16 +138,14 @@ export class LambdaDriver implements ServerlessDriver {
);
}
private getLayerName(
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
return flatServerlessFunctionLayer.checksum;
private getLayerName(flatLogicFunctionLayer: FlatLogicFunctionLayer) {
return flatLogicFunctionLayer.checksum;
}
private async createLayerIfNotExists(
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
): Promise<string> {
const layerName = this.getLayerName(flatServerlessFunctionLayer);
const layerName = this.getLayerName(flatLogicFunctionLayer);
const listLayerParams: ListLayerVersionsCommandInput = {
LayerName: layerName,
@@ -175,7 +173,7 @@ export class LambdaDriver implements ServerlessDriver {
await copyAndBuildDependencies(
nodeDependenciesFolder,
flatServerlessFunctionLayer,
flatLogicFunctionLayer,
);
await createZipFile(sourceTemporaryDir, lambdaZipPath);
@@ -186,8 +184,8 @@ export class LambdaDriver implements ServerlessDriver {
ZipFile: await fs.readFile(lambdaZipPath),
},
CompatibleRuntimes: [
ServerlessFunctionRuntime.NODE18,
ServerlessFunctionRuntime.NODE22,
LogicFunctionRuntime.NODE18,
LogicFunctionRuntime.NODE22,
],
};
@@ -204,12 +202,10 @@ export class LambdaDriver implements ServerlessDriver {
return result.LayerVersionArn;
}
private async getLambdaExecutor(
flatServerlessFunction: FlatServerlessFunction,
) {
private async getLambdaExecutor(flatLogicFunction: FlatLogicFunction) {
try {
const getFunctionCommand: GetFunctionCommand = new GetFunctionCommand({
FunctionName: flatServerlessFunction.id,
FunctionName: flatLogicFunction.id,
});
return await (await this.getLambdaClient()).send(getFunctionCommand);
@@ -220,12 +216,12 @@ export class LambdaDriver implements ServerlessDriver {
}
}
async delete(flatServerlessFunction: FlatServerlessFunction) {
const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction);
async delete(flatLogicFunction: FlatLogicFunction) {
const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction);
if (isDefined(lambdaExecutor)) {
const deleteFunctionCommand = new DeleteFunctionCommand({
FunctionName: flatServerlessFunction.id,
FunctionName: flatLogicFunction.id,
});
await (await this.getLambdaClient()).send(deleteFunctionCommand);
@@ -233,10 +229,10 @@ export class LambdaDriver implements ServerlessDriver {
}
private async isAlreadyBuilt(
flatServerlessFunction: FlatServerlessFunction,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
flatLogicFunction: FlatLogicFunction,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) {
const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction);
const lambdaExecutor = await this.getLambdaExecutor(flatLogicFunction);
if (!isDefined(lambdaExecutor)) {
return false;
@@ -245,38 +241,31 @@ export class LambdaDriver implements ServerlessDriver {
const layers = lambdaExecutor.Configuration?.Layers;
if (!isDefined(layers) || layers.length !== 1) {
await this.delete(flatServerlessFunction);
await this.delete(flatLogicFunction);
return false;
}
const layerName = this.getLayerName(flatServerlessFunctionLayer);
const layerName = this.getLayerName(flatLogicFunctionLayer);
if (layers[0].Arn?.includes(layerName)) {
return true;
}
await this.delete(flatServerlessFunction);
await this.delete(flatLogicFunction);
return false;
}
private async build(
flatServerlessFunction: FlatServerlessFunction,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
flatLogicFunction: FlatLogicFunction,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) {
if (
await this.isAlreadyBuilt(
flatServerlessFunction,
flatServerlessFunctionLayer,
)
) {
if (await this.isAlreadyBuilt(flatLogicFunction, flatLogicFunctionLayer)) {
return;
}
const layerArn = await this.createLayerIfNotExists(
flatServerlessFunctionLayer,
);
const layerArn = await this.createLayerIfNotExists(flatLogicFunctionLayer);
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
@@ -291,12 +280,12 @@ export class LambdaDriver implements ServerlessDriver {
Code: {
ZipFile: await fs.readFile(lambdaZipPath),
},
FunctionName: flatServerlessFunction.id,
FunctionName: flatLogicFunction.id,
Layers: [layerArn],
Handler: 'index.handler',
Role: this.options.lambdaRole,
Runtime: flatServerlessFunction.runtime,
Timeout: 900, // timeout is handled by the serverless function service
Runtime: flatLogicFunction.runtime,
Timeout: 900, // timeout is handled by the logic function service
};
const command = new CreateFunctionCommand(params);
@@ -322,26 +311,26 @@ export class LambdaDriver implements ServerlessDriver {
}
async execute({
flatServerlessFunction,
flatServerlessFunctionLayer,
flatLogicFunction,
flatLogicFunctionLayer,
payload,
version,
env,
}: {
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
flatLogicFunction: FlatLogicFunction;
flatLogicFunctionLayer: FlatLogicFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult> {
await this.build(flatServerlessFunction, flatServerlessFunctionLayer);
}): Promise<LogicFunctionExecuteResult> {
await this.build(flatLogicFunction, flatLogicFunctionLayer);
await this.waitFunctionUpdates(flatServerlessFunction);
await this.waitFunctionUpdates(flatLogicFunction);
const startTime = Date.now();
const builtHandlerFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
const builtHandlerFolderPath = getLogicFunctionFolderOrThrow({
flatLogicFunction,
version,
fileFolder: FileFolder.BuiltFunction,
});
@@ -350,7 +339,7 @@ export class LambdaDriver implements ServerlessDriver {
await streamToBuffer(
await this.fileStorageService.read({
folderPath: builtHandlerFolderPath,
filename: flatServerlessFunction.builtHandlerPath,
filename: flatLogicFunction.builtHandlerPath,
}),
)
).toString('utf-8');
@@ -359,11 +348,11 @@ export class LambdaDriver implements ServerlessDriver {
params: payload,
code: compiledCode,
env: env ?? {},
handlerName: flatServerlessFunction.handlerName,
handlerName: flatLogicFunction.handlerName,
};
const params: InvokeCommandInput = {
FunctionName: flatServerlessFunction.id,
FunctionName: flatLogicFunction.id,
Payload: JSON.stringify(executorPayload),
LogType: LogType.Tail,
};
@@ -385,7 +374,7 @@ export class LambdaDriver implements ServerlessDriver {
return {
data: null,
duration,
status: ServerlessFunctionExecutionStatus.ERROR,
status: LogicFunctionExecutionStatus.ERROR,
error: parsedResult,
logs,
};
@@ -395,13 +384,13 @@ export class LambdaDriver implements ServerlessDriver {
data: parsedResult,
logs,
duration,
status: ServerlessFunctionExecutionStatus.SUCCESS,
status: LogicFunctionExecutionStatus.SUCCESS,
};
} catch (error) {
if (error instanceof ResourceNotFoundException) {
throw new ServerlessFunctionException(
throw new LogicFunctionException(
`Function Version '${version}' does not exist`,
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
throw error;
@@ -5,25 +5,25 @@ import { join } from 'path';
import { FileFolder } from 'twenty-shared/types';
import {
type ServerlessDriver,
type ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
type LogicFunctionExecutorDriver,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function-executor/drivers/interfaces/logic-function-executor-driver.interface';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
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 { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.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 FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function-executor/drivers/constants/logic-function-executor-tmpdir-folder';
import { copyAndBuildDependencies } from 'src/engine/core-modules/logic-function-executor/drivers/utils/copy-and-build-dependencies';
import { ConsoleListener } from 'src/engine/core-modules/logic-function-executor/drivers/utils/intercept-console';
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/logic-function-executor/drivers/utils/lambda-build-directory-manager';
import { getLogicFunctionFolderOrThrow } from 'src/engine/core-modules/logic-function-executor/utils/get-logic-function-folder-or-throw.utils';
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
import { LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
export interface LocalDriverOptions {
fileStorageService: FileStorageService;
}
export class LocalDriver implements ServerlessDriver {
export class LocalDriver implements LogicFunctionExecutorDriver {
private readonly fileStorageService: FileStorageService;
constructor(options: LocalDriverOptions) {
@@ -31,16 +31,19 @@ export class LocalDriver implements ServerlessDriver {
}
private getInMemoryLayerFolderPath = (
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) => {
return join(SERVERLESS_TMPDIR_FOLDER, flatServerlessFunctionLayer.checksum);
return join(
LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER,
flatLogicFunctionLayer.checksum,
);
};
private async createLayerIfNotExists(
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) {
const inMemoryLayerFolderPath = this.getInMemoryLayerFolderPath(
flatServerlessFunctionLayer,
flatLogicFunctionLayer,
);
try {
@@ -48,38 +51,36 @@ export class LocalDriver implements ServerlessDriver {
} catch {
await copyAndBuildDependencies(
inMemoryLayerFolderPath,
flatServerlessFunctionLayer,
flatLogicFunctionLayer,
);
}
}
async delete() {}
private async build(
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
) {
await this.createLayerIfNotExists(flatServerlessFunctionLayer);
private async build(flatLogicFunctionLayer: FlatLogicFunctionLayer) {
await this.createLayerIfNotExists(flatLogicFunctionLayer);
}
async execute({
flatServerlessFunction,
flatServerlessFunctionLayer,
flatLogicFunction,
flatLogicFunctionLayer,
payload,
version,
env,
}: {
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
flatLogicFunction: FlatLogicFunction;
flatLogicFunctionLayer: FlatLogicFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult> {
await this.build(flatServerlessFunctionLayer);
}): Promise<LogicFunctionExecuteResult> {
await this.build(flatLogicFunctionLayer);
const startTime = Date.now();
const builtHandlerFolderPath = getServerlessFolderOrThrow({
flatServerlessFunction,
const builtHandlerFolderPath = getLogicFunctionFolderOrThrow({
flatLogicFunction,
version,
fileFolder: FileFolder.BuiltFunction,
});
@@ -92,18 +93,18 @@ export class LocalDriver implements ServerlessDriver {
await this.fileStorageService.download({
from: {
folderPath: builtHandlerFolderPath,
filename: flatServerlessFunction.builtHandlerPath,
filename: flatLogicFunction.builtHandlerPath,
},
to: {
folderPath: sourceTemporaryDir,
filename: flatServerlessFunction.builtHandlerPath,
filename: flatLogicFunction.builtHandlerPath,
},
});
try {
await fs.symlink(
join(
this.getInMemoryLayerFolderPath(flatServerlessFunctionLayer),
this.getInMemoryLayerFolderPath(flatLogicFunctionLayer),
'node_modules',
),
join(sourceTemporaryDir, 'node_modules'),
@@ -151,13 +152,13 @@ export class LocalDriver implements ServerlessDriver {
try {
const builtBundleFilePath = join(
sourceTemporaryDir,
flatServerlessFunction.builtHandlerPath,
flatLogicFunction.builtHandlerPath,
);
const runnerPath = await this.writeBootstrapRunner({
dir: sourceTemporaryDir,
builtFileAbsPath: builtBundleFilePath,
handlerName: flatServerlessFunction.handlerName,
handlerName: flatLogicFunction.handlerName,
});
const { ok, result, error, stack, stdout, stderr } =
@@ -165,7 +166,7 @@ export class LocalDriver implements ServerlessDriver {
runnerPath,
env: env ?? {},
payload,
timeoutMs: 900_000, // timeout is handled by the serverless function service
timeoutMs: 900_000, // timeout is handled by the logic function service
});
if (stdout)
@@ -190,7 +191,7 @@ export class LocalDriver implements ServerlessDriver {
data: (result ?? null) as object | null,
logs,
duration,
status: ServerlessFunctionExecutionStatus.SUCCESS,
status: LogicFunctionExecutionStatus.SUCCESS,
};
}
@@ -203,7 +204,7 @@ export class LocalDriver implements ServerlessDriver {
errorMessage: error || 'Unknown error',
stackTrace: stack ? String(stack).split('\n') : [],
},
status: ServerlessFunctionExecutionStatus.ERROR,
status: LogicFunctionExecutionStatus.ERROR,
};
} finally {
consoleListener.release();
@@ -232,7 +233,7 @@ export class LocalDriver implements ServerlessDriver {
const builtUrl = pathToFileURL(${JSON.stringify(builtFileAbsPath)});
const mod = await import(builtUrl.href);
if (typeof mod.${handlerName} !== 'function') {
throw new Error('Export "${handlerName}" not found in serverless bundle');
throw new Error('Export "${handlerName}" not found in function bundle');
}
let payload = undefined;
@@ -333,7 +334,7 @@ export class LocalDriver implements ServerlessDriver {
if (settled) return;
settled = true;
if (code === 0) {
// Fallback path if no IPC (shouldnt happen with our stdio)
// Fallback path if no IPC (shouldn't happen with our stdio)
resolve({ ok: true, stdout, stderr });
} else {
resolve({
@@ -1,6 +1,6 @@
import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type';
import { type SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var';
import { buildEnvVar } from 'src/engine/core-modules/logic-function-executor/drivers/utils/build-env-var';
describe('buildEnvVar', () => {
const mockSecretEncryptionService = {
@@ -3,22 +3,22 @@ import { promises as fs, statSync } from 'fs';
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 FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/logic-function-executor/drivers/utils/get-layer-dependencies-dir-name';
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
const execFilePromise = promisify(execFile);
export const copyAndBuildDependencies = async (
buildDirectory: string,
flatServerlessFunctionLayer: FlatServerlessFunctionLayer,
flatLogicFunctionLayer: FlatLogicFunctionLayer,
) => {
await fs.mkdir(buildDirectory, {
recursive: true,
});
const packageJson = flatServerlessFunctionLayer.packageJson;
const packageJson = flatLogicFunctionLayer.packageJson;
const yarnLock = flatServerlessFunctionLayer.yarnLock;
const yarnLock = flatLogicFunctionLayer.yarnLock;
await fs.writeFile(
join(buildDirectory, 'package.json'),
@@ -46,7 +46,7 @@ export const copyAndBuildDependencies = async (
} catch (error: any) {
const errorMessage =
[error?.stdout, error?.stderr].filter(Boolean).join('\n') ||
'Failed to install serverless dependencies';
'Failed to install logic function executor dependencies';
throw new Error(errorMessage);
}
@@ -1,6 +1,6 @@
import { promises as fs } from 'fs';
import { getExecutorFilePath } from 'src/engine/core-modules/serverless/drivers/utils/get-executor-file-path';
import { getExecutorFilePath } from 'src/engine/core-modules/logic-function-executor/drivers/utils/get-executor-file-path';
export const copyExecutor = async (buildDirectory: string) => {
await fs.mkdir(buildDirectory, {
@@ -5,7 +5,7 @@ import { ASSET_PATH } from 'src/constants/assets-path';
export const getExecutorFilePath = (): string => {
const baseTypescriptProjectPath = path.join(
ASSET_PATH,
`engine/core-modules/serverless/drivers/constants/executor`,
`engine/core-modules/logic-function-executor/drivers/constants/executor`,
);
return path.resolve(__dirname, baseTypescriptProjectPath);
@@ -3,8 +3,8 @@ import { join } from 'path';
import { type PackageJson } from 'twenty-shared/application';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/logic-function-executor/drivers/utils/get-layer-dependencies-dir-name';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/logic-function-executor/drivers/layers/last-layer-version';
export type LayerDependencies = {
packageJson: PackageJson;
@@ -7,7 +7,7 @@ export const getLayerDependenciesDirName = (
): string => {
const baseTypescriptProjectPath = path.join(
ASSET_PATH,
`engine/core-modules/serverless/drivers/layers/${version}`,
`engine/core-modules/logic-function-executor/drivers/layers/${version}`,
);
return path.resolve(__dirname, baseTypescriptProjectPath);
@@ -32,7 +32,7 @@ const getAllFiles = async (
export const getSeedProjectFiles = (async () => {
const seedProjectPath = join(
ASSET_PATH,
`engine/core-modules/serverless/drivers/constants/seed-project`,
`engine/core-modules/logic-function-executor/drivers/constants/seed-project`,
);
return await getAllFiles(seedProjectPath);
@@ -3,7 +3,7 @@ import * as fs from 'fs/promises';
import { v4 } from 'uuid';
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
import { LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER } from 'src/engine/core-modules/logic-function-executor/drivers/constants/logic-function-executor-tmpdir-folder';
export const NODE_LAYER_SUBFOLDER = 'nodejs';
@@ -12,7 +12,7 @@ const LAMBDA_ZIP_FILE_NAME = 'lambda.zip';
export class LambdaBuildDirectoryManager {
private temporaryDir = join(
SERVERLESS_TMPDIR_FOLDER,
LOGIC_FUNCTION_EXECUTOR_TMPDIR_FOLDER,
`${TEMPORARY_LAMBDA_FOLDER}-${v4()}`,
);
@@ -2,46 +2,46 @@ import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
ServerlessDriverType,
type ServerlessModuleOptions,
} from 'src/engine/core-modules/serverless/serverless.interface';
LogicFunctionExecutorDriverType,
type LogicFunctionExecutorModuleOptions,
} from 'src/engine/core-modules/logic-function-executor/logic-function-executor.interface';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
export const serverlessModuleFactory = async (
export const logicFunctionExecutorModuleFactory = async (
twentyConfigService: TwentyConfigService,
fileStorageService: FileStorageService,
): Promise<ServerlessModuleOptions> => {
const driverType = twentyConfigService.get('SERVERLESS_TYPE');
): Promise<LogicFunctionExecutorModuleOptions> => {
const driverType = twentyConfigService.get('LOGIC_FUNCTION_TYPE');
const options = { fileStorageService };
switch (driverType) {
case ServerlessDriverType.DISABLED: {
case LogicFunctionExecutorDriverType.DISABLED: {
return {
type: ServerlessDriverType.DISABLED,
type: LogicFunctionExecutorDriverType.DISABLED,
};
}
case ServerlessDriverType.LOCAL: {
case LogicFunctionExecutorDriverType.LOCAL: {
return {
type: ServerlessDriverType.LOCAL,
type: LogicFunctionExecutorDriverType.LOCAL,
options,
};
}
case ServerlessDriverType.LAMBDA: {
const region = twentyConfigService.get('SERVERLESS_LAMBDA_REGION');
case LogicFunctionExecutorDriverType.LAMBDA: {
const region = twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_REGION');
const accessKeyId = twentyConfigService.get(
'SERVERLESS_LAMBDA_ACCESS_KEY_ID',
'LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID',
);
const secretAccessKey = twentyConfigService.get(
'SERVERLESS_LAMBDA_SECRET_ACCESS_KEY',
'LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY',
);
const lambdaRole = twentyConfigService.get('SERVERLESS_LAMBDA_ROLE');
const lambdaRole = twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_ROLE');
const subhostingRole = twentyConfigService.get(
'SERVERLESS_LAMBDA_SUBHOSTING_ROLE',
'LOGIC_FUNCTION_LAMBDA_SUBHOSTING_ROLE',
);
return {
type: ServerlessDriverType.LAMBDA,
type: LogicFunctionExecutorDriverType.LAMBDA,
options: {
...options,
credentials: accessKeyId
@@ -60,7 +60,7 @@ export const serverlessModuleFactory = async (
}
default:
throw new Error(
`Invalid serverless driver type (${driverType}), check your .env file`,
`Invalid logic function executor driver type (${driverType}), check your .env file`,
);
}
};
@@ -0,0 +1,3 @@
export const LOGIC_FUNCTION_EXECUTOR_DRIVER = Symbol(
'LOGIC_FUNCTION_EXECUTOR_DRIVER',
);
@@ -1,37 +1,39 @@
import { type FactoryProvider, type ModuleMetadata } from '@nestjs/common';
import { type LambdaDriverOptions } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { type LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { type LambdaDriverOptions } from 'src/engine/core-modules/logic-function-executor/drivers/lambda.driver';
import { type LocalDriverOptions } from 'src/engine/core-modules/logic-function-executor/drivers/local.driver';
export enum ServerlessDriverType {
export enum LogicFunctionExecutorDriverType {
DISABLED = 'DISABLED',
LAMBDA = 'LAMBDA',
LOCAL = 'LOCAL',
}
export interface DisabledDriverFactoryOptions {
type: ServerlessDriverType.DISABLED;
type: LogicFunctionExecutorDriverType.DISABLED;
}
export interface LocalDriverFactoryOptions {
type: ServerlessDriverType.LOCAL;
type: LogicFunctionExecutorDriverType.LOCAL;
options: LocalDriverOptions;
}
export interface LambdaDriverFactoryOptions {
type: ServerlessDriverType.LAMBDA;
type: LogicFunctionExecutorDriverType.LAMBDA;
options: LambdaDriverOptions;
}
export type ServerlessModuleOptions =
export type LogicFunctionExecutorModuleOptions =
| DisabledDriverFactoryOptions
| LocalDriverFactoryOptions
| LambdaDriverFactoryOptions;
export type ServerlessModuleAsyncOptions = {
export type LogicFunctionExecutorModuleAsyncOptions = {
useFactory: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[]
) => ServerlessModuleOptions | Promise<ServerlessModuleOptions>;
) =>
| LogicFunctionExecutorModuleOptions
| Promise<LogicFunctionExecutorModuleOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;
@@ -0,0 +1,51 @@
import { type DynamicModule, Global } from '@nestjs/common';
import { AddPackagesCommand } from 'src/engine/core-modules/logic-function-executor/commands/add-packages.command';
import { DisabledDriver } from 'src/engine/core-modules/logic-function-executor/drivers/disabled.driver';
import { LambdaDriver } from 'src/engine/core-modules/logic-function-executor/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/logic-function-executor/drivers/local.driver';
import { LOGIC_FUNCTION_EXECUTOR_DRIVER } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.constants';
import {
LogicFunctionExecutorDriverType,
type LogicFunctionExecutorModuleAsyncOptions,
} from 'src/engine/core-modules/logic-function-executor/logic-function-executor.interface';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.service';
@Global()
export class LogicFunctionExecutorModule {
static forRootAsync(
options: LogicFunctionExecutorModuleAsyncOptions,
): DynamicModule {
const provider = {
provide: LOGIC_FUNCTION_EXECUTOR_DRIVER,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
switch (config?.type) {
case LogicFunctionExecutorDriverType.DISABLED:
return new DisabledDriver();
case LogicFunctionExecutorDriverType.LOCAL:
return new LocalDriver(config.options);
case LogicFunctionExecutorDriverType.LAMBDA:
return new LambdaDriver(config.options);
default: {
const unknownConfig = config as { type?: string };
throw new Error(
`Unknown logic function executor driver type: ${unknownConfig?.type}`,
);
}
}
},
inject: options.inject || [],
};
return {
module: LogicFunctionExecutorModule,
imports: options.imports || [],
providers: [LogicFunctionExecutorService, provider, AddPackagesCommand],
exports: [LogicFunctionExecutorService],
};
}
}
@@ -0,0 +1,46 @@
import { Inject, Injectable } from '@nestjs/common';
import {
LogicFunctionExecutorDriver,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function-executor/drivers/interfaces/logic-function-executor-driver.interface';
import { LOGIC_FUNCTION_EXECUTOR_DRIVER } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.constants';
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
@Injectable()
export class LogicFunctionExecutorService
implements LogicFunctionExecutorDriver
{
constructor(
@Inject(LOGIC_FUNCTION_EXECUTOR_DRIVER)
private driver: LogicFunctionExecutorDriver,
) {}
async delete(flatLogicFunction: FlatLogicFunction): Promise<void> {
return this.driver.delete(flatLogicFunction);
}
async execute({
flatLogicFunction,
flatLogicFunctionLayer,
payload,
version,
env,
}: {
flatLogicFunction: FlatLogicFunction;
flatLogicFunctionLayer: FlatLogicFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<LogicFunctionExecuteResult> {
return this.driver.execute({
flatLogicFunction,
flatLogicFunctionLayer,
payload,
version,
env,
});
}
}
@@ -0,0 +1,40 @@
import { join } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { FileFolder } from 'twenty-shared/types';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
export const getLogicFunctionFolderOrThrow = ({
flatLogicFunction,
version,
fileFolder = FileFolder.LogicFunction,
}: {
flatLogicFunction: FlatLogicFunction;
version?: 'draft' | 'latest' | (string & NonNullable<unknown>);
fileFolder?:
| FileFolder.LogicFunction
| FileFolder.LogicFunctionToDelete
| FileFolder.BuiltFunction;
}) => {
if (version === 'latest' && !isDefined(flatLogicFunction.latestVersion)) {
throw new LogicFunctionException(
"Can't get 'latest' version when logicFunction 'latestVersion' is undefined",
LogicFunctionExceptionCode.LOGIC_FUNCTION_VERSION_NOT_FOUND,
);
}
const computedVersion =
version === 'latest' ? flatLogicFunction.latestVersion : version;
return join(
'workspace-' + flatLogicFunction.workspaceId,
fileFolder,
flatLogicFunction.id,
computedVersion || '',
);
};
@@ -24,7 +24,7 @@ import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { CleanOnboardingWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-onboarding-workspaces.job';
import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job';
@@ -70,7 +70,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module';
AiAgentMonitorModule,
CronTriggerModule,
DatabaseEventTriggerModule,
ServerlessFunctionModule,
LogicFunctionModule,
],
providers: [
CleanSuspendedWorkspacesJob,
@@ -11,7 +11,7 @@ export const MESSAGE_QUEUE_PRIORITY = {
[MessageQueue.calendarQueue]: 4,
[MessageQueue.contactCreationQueue]: 4,
[MessageQueue.taskAssignedQueue]: 4,
[MessageQueue.serverlessFunctionQueue]: 4,
[MessageQueue.logicFunctionQueue]: 4,
[MessageQueue.workspaceQueue]: 5,
[MessageQueue.triggerQueue]: 5,
[MessageQueue.deleteCascadeQueue]: 6,
@@ -16,7 +16,7 @@ export enum MessageQueue {
workflowQueue = 'workflow-queue',
delayedJobsQueue = 'delayed-jobs-queue',
deleteCascadeQueue = 'delete-cascade-queue',
serverlessFunctionQueue = 'serverless-function-queue',
logicFunctionQueue = 'logic-function-queue',
triggerQueue = 'trigger-queue',
aiQueue = 'ai-queue',
}
@@ -122,10 +122,8 @@ export class CommonApiContextBuilderService {
authContext.apiKey.id,
workspaceId,
);
} else if (
isDefined(authContext.application?.defaultServerlessFunctionRoleId)
) {
roleId = authContext.application.defaultServerlessFunctionRoleId;
} else if (isDefined(authContext.application?.defaultLogicFunctionRoleId)) {
roleId = authContext.application.defaultLogicFunctionRoleId;
} else if (isDefined(authContext.userWorkspaceId)) {
const userWorkspaceRoleId =
await this.userRoleService.getRoleIdForUserWorkspace({
@@ -1,4 +0,0 @@
import { join } from 'path';
import { tmpdir } from 'os';
export const SERVERLESS_TMPDIR_FOLDER = join(tmpdir(), 'serverless-tmpdir');
@@ -1,22 +0,0 @@
import {
type ServerlessDriver,
type ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
export class DisabledDriver implements ServerlessDriver {
async delete(): Promise<void> {
// No-op when disabled
}
async execute(): Promise<ServerlessExecuteResult> {
throw new ServerlessFunctionException(
'Serverless function execution is disabled. Set SERVERLESS_TYPE to LOCAL or LAMBDA to enable.',
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_DISABLED,
);
}
}
@@ -1,34 +0,0 @@
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 FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
export type ServerlessExecuteError = {
errorType: string;
errorMessage: string;
stackTrace: string | string[];
};
export type ServerlessExecuteResult = {
data: object | null;
duration: number;
logs: string;
status: ServerlessFunctionExecutionStatus;
error?: ServerlessExecuteError;
};
export interface ServerlessDriver {
delete(flatServerlessFunction: FlatServerlessFunction): Promise<void>;
execute({
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
}: {
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult>;
}
@@ -1 +0,0 @@
export const SERVERLESS_DRIVER = Symbol('SERVERLESS_DRIVER');
@@ -1,49 +0,0 @@
import { type DynamicModule, Global } from '@nestjs/common';
import { AddPackagesCommand } from 'src/engine/core-modules/serverless/commands/add-packages.command';
import { DisabledDriver } from 'src/engine/core-modules/serverless/drivers/disabled.driver';
import { LambdaDriver } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
import {
ServerlessDriverType,
type ServerlessModuleAsyncOptions,
} from 'src/engine/core-modules/serverless/serverless.interface';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
@Global()
export class ServerlessModule {
static forRootAsync(options: ServerlessModuleAsyncOptions): DynamicModule {
const provider = {
provide: SERVERLESS_DRIVER,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
switch (config?.type) {
case ServerlessDriverType.DISABLED:
return new DisabledDriver();
case ServerlessDriverType.LOCAL:
return new LocalDriver(config.options);
case ServerlessDriverType.LAMBDA:
return new LambdaDriver(config.options);
default: {
const unknownConfig = config as { type?: string };
throw new Error(
`Unknown serverless driver type: ${unknownConfig?.type}`,
);
}
}
},
inject: options.inject || [],
};
return {
module: ServerlessModule,
imports: options.imports || [],
providers: [ServerlessService, provider, AddPackagesCommand],
exports: [ServerlessService],
};
}
}
@@ -1,41 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import {
ServerlessDriver,
type ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
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(flatServerlessFunction: FlatServerlessFunction): Promise<void> {
return this.driver.delete(flatServerlessFunction);
}
async execute({
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
}: {
flatServerlessFunction: FlatServerlessFunction;
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
payload: object;
version: string;
env?: Record<string, string>;
}): Promise<ServerlessExecuteResult> {
return this.driver.execute({
flatServerlessFunction,
flatServerlessFunctionLayer,
payload,
version,
env,
});
}
}
@@ -1,43 +0,0 @@
import { join } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { FileFolder } from 'twenty-shared/types';
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 getServerlessFolderOrThrow = ({
flatServerlessFunction,
version,
fileFolder = FileFolder.ServerlessFunction,
}: {
flatServerlessFunction: FlatServerlessFunction;
version?: 'draft' | 'latest' | (string & NonNullable<unknown>);
fileFolder?:
| FileFolder.ServerlessFunction
| FileFolder.ServerlessFunctionToDelete
| FileFolder.BuiltFunction;
}) => {
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,
);
}
const computedVersion =
version === 'latest' ? flatServerlessFunction.latestVersion : version;
return join(
'workspace-' + flatServerlessFunction.workspaceId,
fileFolder,
flatServerlessFunction.id,
computedVersion || '',
);
};
@@ -6,5 +6,5 @@ export enum ToolCategory {
NATIVE_MODEL = 'NATIVE_MODEL',
VIEW = 'VIEW',
DASHBOARD = 'DASHBOARD',
SERVERLESS_FUNCTION = 'SERVERLESS_FUNCTION',
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
}
@@ -11,65 +11,63 @@ import {
import { ToolCategory } from 'src/engine/core-modules/tool-provider/enums/tool-category.enum';
import { wrapJsonSchemaForExecution } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
@Injectable()
export class ServerlessFunctionToolProvider implements ToolProvider {
readonly category = ToolCategory.SERVERLESS_FUNCTION;
export class LogicFunctionToolProvider implements ToolProvider {
readonly category = ToolCategory.LOGIC_FUNCTION;
constructor(
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly logicFunctionService: LogicFunctionService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
async isAvailable(_context: ToolProviderContext): Promise<boolean> {
// Serverless function tools are available if there are any functions marked as tools
// Logic function tools are available if there are any functions marked as tools
return true;
}
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
const { flatServerlessFunctionMaps } =
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId: context.workspaceId,
flatMapsKeys: ['flatServerlessFunctionMaps'],
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
// Filter serverless functions that are marked as tools
const serverlessFunctionsWithSchema = Object.values(
flatServerlessFunctionMaps.byId,
// Filter logic functions that are marked as tools
const logicFunctionsWithSchema = Object.values(
flatLogicFunctionMaps.byId,
).filter(
(fn): fn is FlatServerlessFunction =>
(fn): fn is FlatLogicFunction =>
isDefined(fn) && fn.isTool === true && fn.deletedAt === null,
);
const tools: ToolSet = {};
for (const serverlessFunction of serverlessFunctionsWithSchema) {
const toolName = this.buildServerlessFunctionToolName(
serverlessFunction.name,
);
for (const logicFunction of logicFunctionsWithSchema) {
const toolName = this.buildLogicFunctionToolName(logicFunction.name);
const wrappedSchema = wrapJsonSchemaForExecution(
serverlessFunction.toolInputSchema as Record<string, unknown>,
logicFunction.toolInputSchema as Record<string, unknown>,
);
tools[toolName] = {
description:
serverlessFunction.description ||
`Execute the ${serverlessFunction.name} serverless function`,
logicFunction.description ||
`Execute the ${logicFunction.name} logic function`,
inputSchema: jsonSchema(wrappedSchema),
execute: async (parameters: Record<string, unknown>) => {
const { loadingMessage: _, ...actualParams } = parameters;
const result =
await this.serverlessFunctionService.executeOneServerlessFunction({
id: serverlessFunction.id,
await this.logicFunctionService.executeOneLogicFunction({
id: logicFunction.id,
workspaceId: context.workspaceId,
payload: actualParams,
version: serverlessFunction.latestVersion ?? 'draft',
version: logicFunction.latestVersion ?? 'draft',
});
if (result.error) {
@@ -90,9 +88,9 @@ export class ServerlessFunctionToolProvider implements ToolProvider {
return tools;
}
private buildServerlessFunctionToolName(functionName: string): string {
private buildLogicFunctionToolName(functionName: string): string {
// Convert function name to a valid tool name (lowercase, underscores)
return `serverless_${functionName
return `logic_function_${functionName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '')}`;
@@ -25,7 +25,7 @@ export type ToolIndexEntry = {
| 'METADATA'
| 'VIEW'
| 'DASHBOARD'
| 'SERVERLESS_FUNCTION';
| 'LOGIC_FUNCTION';
objectName?: string;
operation?: string;
inputSchema?: object;
@@ -40,7 +40,7 @@ export type ToolSearchOptions = {
| 'METADATA'
| 'VIEW'
| 'DASHBOARD'
| 'SERVERLESS_FUNCTION';
| 'LOGIC_FUNCTION';
};
export type ToolContext = {
@@ -259,7 +259,7 @@ export class ToolRegistryService {
NATIVE_MODEL: 'ACTION',
VIEW: 'VIEW',
DASHBOARD: 'DASHBOARD',
SERVERLESS_FUNCTION: 'SERVERLESS_FUNCTION',
LOGIC_FUNCTION: 'LOGIC_FUNCTION',
};
return Object.entries(tools).map(([name, tool]) => {
@@ -8,7 +8,7 @@ import { DashboardToolProvider } from 'src/engine/core-modules/tool-provider/pro
import { DatabaseToolProvider } from 'src/engine/core-modules/tool-provider/providers/database-tool.provider';
import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider';
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
import { ServerlessFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/serverless-function-tool.provider';
import { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/logic-function-tool.provider';
import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider';
import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider';
import { ToolModule } from 'src/engine/core-modules/tool/tool.module';
@@ -18,7 +18,7 @@ import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { ViewModule } from 'src/engine/metadata-modules/view/view.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -44,7 +44,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
ViewModule,
WorkspaceCacheModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
ServerlessFunctionModule,
LogicFunctionModule,
UserRoleModule,
],
providers: [
@@ -54,7 +54,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
DatabaseToolProvider,
MetadataToolProvider,
NativeModelToolProvider,
ServerlessFunctionToolProvider,
LogicFunctionToolProvider,
ViewToolProvider,
WorkflowToolProvider,
{
@@ -65,7 +65,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
databaseProvider: DatabaseToolProvider,
metadataProvider: MetadataToolProvider,
nativeModelProvider: NativeModelToolProvider,
serverlessFunctionProvider: ServerlessFunctionToolProvider,
logicFunctionProvider: LogicFunctionToolProvider,
viewProvider: ViewToolProvider,
workflowProvider: WorkflowToolProvider,
) => [
@@ -74,7 +74,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
databaseProvider,
metadataProvider,
nativeModelProvider,
serverlessFunctionProvider,
logicFunctionProvider,
viewProvider,
workflowProvider,
],
@@ -84,7 +84,7 @@ import { ToolRegistryService } from './services/tool-registry.service';
DatabaseToolProvider,
MetadataToolProvider,
NativeModelToolProvider,
ServerlessFunctionToolProvider,
LogicFunctionToolProvider,
ViewToolProvider,
WorkflowToolProvider,
],
@@ -18,7 +18,7 @@ export const wrapSchemaForExecution = <T extends z.ZodRawShape>(
}) as z.ZodObject<T & { loadingMessage: z.ZodString }>;
};
// For non-Zod schemas (serverless functions with JSON Schema)
// For non-Zod schemas (logic functions with JSON Schema)
export const wrapJsonSchemaForExecution = (
schema: Record<string, unknown>,
): Record<string, unknown> => {
@@ -23,7 +23,7 @@ import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handle
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
import { type MeterDriver } from 'src/engine/core-modules/metrics/types/meter-driver.type';
import { ServerlessDriverType } from 'src/engine/core-modules/serverless/serverless.interface';
import { LogicFunctionExecutorDriverType } from 'src/engine/core-modules/logic-function-executor/logic-function-executor.interface';
import { CastToLogLevelArray } from 'src/engine/core-modules/twenty-config/decorators/cast-to-log-level-array.decorator';
import { CastToTypeORMLogLevelArray } from 'src/engine/core-modules/twenty-config/decorators/cast-to-typeorm-log-level-array.decorator';
import { CastToMeterDriverArray } from 'src/engine/core-modules/twenty-config/decorators/cast-to-meter-driver.decorator';
@@ -446,87 +446,98 @@ export class ConfigVariables {
STORAGE_S3_SECRET_ACCESS_KEY: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
description: 'Type of serverless execution (local or Lambda)',
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description: 'Type of function execution (local or Lambda)',
type: ConfigVariableType.ENUM,
options: Object.values(ServerlessDriverType),
options: Object.values(LogicFunctionExecutorDriverType),
isEnvOnly: true,
})
@IsOptional()
@CastToUpperSnakeCase()
SERVERLESS_TYPE: ServerlessDriverType = ServerlessDriverType.LOCAL;
LOGIC_FUNCTION_TYPE: LogicFunctionExecutorDriverType =
LogicFunctionExecutorDriverType.LOCAL;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description:
'Configure whether console logs from serverless functions are displayed in the terminal',
'Configure whether console logs from logic functions are displayed in the terminal',
type: ConfigVariableType.BOOLEAN,
})
@IsOptional()
SERVERLESS_LOGS_ENABLED: false;
LOGIC_FUNCTION_LOGS_ENABLED: false;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
description: 'Throttle limit for serverless function execution',
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description: 'Throttle limit for logic function execution',
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
SERVERLESS_FUNCTION_EXEC_THROTTLE_LIMIT = 1000;
LOGIC_FUNCTION_EXEC_THROTTLE_LIMIT = 1000;
// milliseconds
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
description: 'Time-to-live for serverless function execution throttle',
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description: 'Time-to-live for logic function execution throttle',
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL = 60_000;
LOGIC_FUNCTION_EXEC_THROTTLE_TTL = 60_000;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description: 'Region for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@ValidateIf(
(env) => env.LOGIC_FUNCTION_TYPE === LogicFunctionExecutorDriverType.LAMBDA,
)
@IsAWSRegion()
SERVERLESS_LAMBDA_REGION: AwsRegion;
LOGIC_FUNCTION_LAMBDA_REGION: AwsRegion;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description: 'IAM role for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
SERVERLESS_LAMBDA_ROLE: string;
@ValidateIf(
(env) => env.LOGIC_FUNCTION_TYPE === LogicFunctionExecutorDriverType.LAMBDA,
)
LOGIC_FUNCTION_LAMBDA_ROLE: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
description: 'Role to assume when hosting lambdas in dedicated AWS account',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@ValidateIf(
(env) => env.LOGIC_FUNCTION_TYPE === LogicFunctionExecutorDriverType.LAMBDA,
)
@IsOptional()
SERVERLESS_LAMBDA_SUBHOSTING_ROLE?: string;
LOGIC_FUNCTION_LAMBDA_SUBHOSTING_ROLE?: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
isSensitive: true,
description: 'Access key ID for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@ValidateIf(
(env) => env.LOGIC_FUNCTION_TYPE === LogicFunctionExecutorDriverType.LAMBDA,
)
@IsOptional()
SERVERLESS_LAMBDA_ACCESS_KEY_ID: string;
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.SERVERLESS_CONFIG,
group: ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG,
isSensitive: true,
description: 'Secret access key for AWS Lambda functions',
type: ConfigVariableType.STRING,
})
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.LAMBDA)
@ValidateIf(
(env) => env.LOGIC_FUNCTION_TYPE === LogicFunctionExecutorDriverType.LAMBDA,
)
@IsOptional()
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY: string;
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY: string;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.CODE_INTERPRETER_CONFIG,
@@ -89,10 +89,10 @@ export const CONFIG_VARIABLES_GROUP_METADATA: Record<
'Configure the LLM provider and model to use for the app. This is experimental and not linked to any public feature.',
isHiddenOnLoad: true,
},
[ConfigVariablesGroup.SERVERLESS_CONFIG]: {
[ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG]: {
position: 1500,
description:
'In our multi-tenant cloud app, we offload untrusted custom code from workflows to a serverless system (Lambda) for enhanced security and scalability. Self-hosters with a single tenant can typically ignore this configuration.',
'In our multi-tenant cloud app, we offload untrusted custom code from workflows to a function execution system (Lambda) for enhanced security and scalability. Self-hosters with a single tenant can typically ignore this configuration.',
isHiddenOnLoad: true,
},
[ConfigVariablesGroup.CODE_INTERPRETER_CONFIG]: {
@@ -13,7 +13,7 @@ export enum ConfigVariablesGroup {
CAPTCHA_CONFIG = 'CAPTCHA_CONFIG',
CLOUDFLARE_CONFIG = 'CLOUDFLARE_CONFIG',
LLM = 'LLM',
SERVERLESS_CONFIG = 'SERVERLESS_CONFIG',
LOGIC_FUNCTION_CONFIG = 'LOGIC_FUNCTION_CONFIG',
CODE_INTERPRETER_CONFIG = 'CODE_INTERPRETER_CONFIG',
SSL = 'SSL',
SUPPORT_CHAT_CONFIG = 'SUPPORT_CHAT_CONFIG',