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:
+2
-2
@@ -313,8 +313,8 @@ export abstract class CommonBaseQueryRunnerService<
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(authContext.application?.defaultServerlessFunctionRoleId)) {
|
||||
return authContext.application?.defaultServerlessFunctionRoleId;
|
||||
if (isDefined(authContext.application?.defaultLogicFunctionRoleId)) {
|
||||
return authContext.application?.defaultLogicFunctionRoleId;
|
||||
}
|
||||
|
||||
if (!isDefined(authContext.userWorkspaceId)) {
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+100
-102
@@ -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,
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+21
@@ -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);
|
||||
-25
@@ -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({
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+5
-5
@@ -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(
|
||||
+7
@@ -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',
|
||||
);
|
||||
+22
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
@@ -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>;
|
||||
}
|
||||
+59
-70
@@ -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;
|
||||
+40
-39
@@ -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 (shouldn’t happen with our stdio)
|
||||
// Fallback path if no IPC (shouldn't happen with our stdio)
|
||||
resolve({ ok: true, stdout, stderr });
|
||||
} else {
|
||||
resolve({
|
||||
+1
-1
@@ -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 = {
|
||||
+6
-6
@@ -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
-1
@@ -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, {
|
||||
+1
-1
@@ -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);
|
||||
+2
-2
@@ -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;
|
||||
+1
-1
@@ -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);
|
||||
+1
-1
@@ -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);
|
||||
+2
-2
@@ -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()}`,
|
||||
);
|
||||
|
||||
+18
-18
@@ -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`,
|
||||
);
|
||||
}
|
||||
};
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export const LOGIC_FUNCTION_EXECUTOR_DRIVER = Symbol(
|
||||
'LOGIC_FUNCTION_EXECUTOR_DRIVER',
|
||||
);
|
||||
+11
-9
@@ -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'>;
|
||||
+51
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
+46
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
+40
@@ -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,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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',
|
||||
}
|
||||
|
||||
+2
-4
@@ -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({
|
||||
|
||||
-4
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
-34
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
-43
@@ -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 || '',
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -6,5 +6,5 @@ export enum ToolCategory {
|
||||
NATIVE_MODEL = 'NATIVE_MODEL',
|
||||
VIEW = 'VIEW',
|
||||
DASHBOARD = 'DASHBOARD',
|
||||
SERVERLESS_FUNCTION = 'SERVERLESS_FUNCTION',
|
||||
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
|
||||
}
|
||||
|
||||
+22
-24
@@ -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, '')}`;
|
||||
+3
-3
@@ -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]) => {
|
||||
|
||||
+7
-7
@@ -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,
|
||||
],
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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]: {
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+3
-3
@@ -393,7 +393,7 @@ ${preloadedTools.length > 0 ? preloadedTools.map((t) => `- \`${t}\` ✓`).join('
|
||||
'DASHBOARD',
|
||||
'METADATA',
|
||||
'VIEW',
|
||||
'SERVERLESS_FUNCTION',
|
||||
'LOGIC_FUNCTION',
|
||||
];
|
||||
|
||||
for (const category of categoryOrder) {
|
||||
@@ -439,8 +439,8 @@ ${tools
|
||||
return 'View Tools (query views)';
|
||||
case 'DASHBOARD':
|
||||
return 'Dashboard Tools (create/manage dashboards)';
|
||||
case 'SERVERLESS_FUNCTION':
|
||||
return 'Serverless Functions (custom tools)';
|
||||
case 'LOGIC_FUNCTION':
|
||||
return 'Logic Functions (custom tools)';
|
||||
default:
|
||||
return category;
|
||||
}
|
||||
|
||||
+2
-4
@@ -4,12 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { CronTriggerCronCommand } from 'src/engine/metadata-modules/cron-trigger/crons/commands/cron-trigger.cron.command';
|
||||
import { CronTriggerCronJob } from 'src/engine/metadata-modules/cron-trigger/crons/jobs/cron-trigger.cron.job';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity, ServerlessFunctionEntity]),
|
||||
],
|
||||
imports: [TypeOrmModule.forFeature([WorkspaceEntity, LogicFunctionEntity])],
|
||||
providers: [CronTriggerCronJob, CronTriggerCronCommand],
|
||||
exports: [CronTriggerCronCommand],
|
||||
})
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@ import {
|
||||
} from 'src/engine/metadata-modules/cron-trigger/crons/jobs/cron-trigger.cron.job';
|
||||
@Command({
|
||||
name: 'cron:trigger:start-cron-trigger',
|
||||
description:
|
||||
'Starts a cron job to trigger cron triggered serverless functions',
|
||||
description: 'Starts a cron job to trigger cron triggered logic functions',
|
||||
})
|
||||
export class CronTriggerCronCommand extends CommandRunner {
|
||||
constructor(
|
||||
|
||||
+15
-15
@@ -12,10 +12,10 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
ServerlessFunctionTriggerJob,
|
||||
ServerlessFunctionTriggerJobData,
|
||||
} from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
LogicFunctionTriggerJob,
|
||||
LogicFunctionTriggerJobData,
|
||||
} from 'src/engine/metadata-modules/logic-function/jobs/logic-function-trigger.job';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import { shouldRunNow } from 'src/utils/should-run-now.utils';
|
||||
|
||||
export const CRON_TRIGGER_CRON_PATTERN = '* * * * *';
|
||||
@@ -23,12 +23,12 @@ export const CRON_TRIGGER_CRON_PATTERN = '* * * * *';
|
||||
@Processor(MessageQueue.cronQueue)
|
||||
export class CronTriggerCronJob {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
|
||||
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
@InjectRepository(ServerlessFunctionEntity)
|
||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||
@InjectRepository(LogicFunctionEntity)
|
||||
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
|
||||
) {}
|
||||
|
||||
@Process(CronTriggerCronJob.name)
|
||||
@@ -44,8 +44,8 @@ export class CronTriggerCronJob {
|
||||
const now = new Date();
|
||||
|
||||
for (const activeWorkspace of activeWorkspaces) {
|
||||
const serverlessFunctionsWithCronTrigger =
|
||||
await this.serverlessFunctionRepository.find({
|
||||
const logicFunctionsWithCronTrigger =
|
||||
await this.logicFunctionRepository.find({
|
||||
where: {
|
||||
workspaceId: activeWorkspace.id,
|
||||
cronTriggerSettings: Not(IsNull()),
|
||||
@@ -53,8 +53,8 @@ export class CronTriggerCronJob {
|
||||
select: ['id', 'cronTriggerSettings', 'workspaceId'],
|
||||
});
|
||||
|
||||
for (const serverlessFunction of serverlessFunctionsWithCronTrigger) {
|
||||
const cronSettings = serverlessFunction.cronTriggerSettings;
|
||||
for (const logicFunction of logicFunctionsWithCronTrigger) {
|
||||
const cronSettings = logicFunction.cronTriggerSettings;
|
||||
|
||||
if (!isDefined(cronSettings?.pattern)) {
|
||||
continue;
|
||||
@@ -64,12 +64,12 @@ export class CronTriggerCronJob {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.messageQueueService.add<ServerlessFunctionTriggerJobData[]>(
|
||||
ServerlessFunctionTriggerJob.name,
|
||||
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
|
||||
LogicFunctionTriggerJob.name,
|
||||
[
|
||||
{
|
||||
serverlessFunctionId: serverlessFunction.id,
|
||||
workspaceId: serverlessFunction.workspaceId,
|
||||
logicFunctionId: logicFunction.id,
|
||||
workspaceId: logicFunction.workspaceId,
|
||||
payload: {},
|
||||
},
|
||||
],
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { CallDatabaseEventTriggerJobsJob } from 'src/engine/metadata-modules/database-event-trigger/jobs/call-database-event-trigger-jobs.job';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ServerlessFunctionEntity])],
|
||||
imports: [TypeOrmModule.forFeature([LogicFunctionEntity])],
|
||||
providers: [CallDatabaseEventTriggerJobsJob],
|
||||
exports: [],
|
||||
})
|
||||
|
||||
+22
-22
@@ -13,10 +13,10 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { transformEventBatchToEventPayloads } from 'src/engine/metadata-modules/database-event-trigger/utils/transform-event-batch-to-event-payloads';
|
||||
import {
|
||||
ServerlessFunctionTriggerJob,
|
||||
ServerlessFunctionTriggerJobData,
|
||||
} from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
LogicFunctionTriggerJob,
|
||||
LogicFunctionTriggerJobData,
|
||||
} from 'src/engine/metadata-modules/logic-function/jobs/logic-function-trigger.job';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
|
||||
const DATABASE_EVENT_JOBS_CHUNK_SIZE = 20;
|
||||
@@ -24,16 +24,16 @@ const DATABASE_EVENT_JOBS_CHUNK_SIZE = 20;
|
||||
@Processor(MessageQueue.triggerQueue)
|
||||
export class CallDatabaseEventTriggerJobsJob {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
|
||||
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectRepository(ServerlessFunctionEntity)
|
||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||
@InjectRepository(LogicFunctionEntity)
|
||||
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
|
||||
) {}
|
||||
|
||||
@Process(CallDatabaseEventTriggerJobsJob.name)
|
||||
async handle(workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>) {
|
||||
const serverlessFunctionsWithDatabaseEventTrigger =
|
||||
await this.serverlessFunctionRepository.find({
|
||||
const logicFunctionsWithDatabaseEventTrigger =
|
||||
await this.logicFunctionRepository.find({
|
||||
where: {
|
||||
workspaceId: workspaceEventBatch.workspaceId,
|
||||
databaseEventTriggerSettings: Not(IsNull()),
|
||||
@@ -41,34 +41,34 @@ export class CallDatabaseEventTriggerJobsJob {
|
||||
select: ['id', 'databaseEventTriggerSettings', 'workspaceId'],
|
||||
});
|
||||
|
||||
const serverlessFunctionsToTrigger =
|
||||
serverlessFunctionsWithDatabaseEventTrigger.filter((serverlessFunction) =>
|
||||
const logicFunctionsToTrigger =
|
||||
logicFunctionsWithDatabaseEventTrigger.filter((logicFunction) =>
|
||||
this.shouldTriggerJob({
|
||||
workspaceEventBatch,
|
||||
eventName: isDefined(serverlessFunction.databaseEventTriggerSettings)
|
||||
? serverlessFunction.databaseEventTriggerSettings.eventName
|
||||
eventName: isDefined(logicFunction.databaseEventTriggerSettings)
|
||||
? logicFunction.databaseEventTriggerSettings.eventName
|
||||
: '',
|
||||
}),
|
||||
);
|
||||
|
||||
const serverlessFunctionPayloads = transformEventBatchToEventPayloads({
|
||||
serverlessFunctions: serverlessFunctionsToTrigger,
|
||||
const logicFunctionPayloads = transformEventBatchToEventPayloads({
|
||||
logicFunctions: logicFunctionsToTrigger,
|
||||
workspaceEventBatch,
|
||||
});
|
||||
|
||||
if (serverlessFunctionPayloads.length === 0) {
|
||||
if (logicFunctionPayloads.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverlessFunctionPayloadsChunks = chunk(
|
||||
serverlessFunctionPayloads,
|
||||
const logicFunctionPayloadsChunks = chunk(
|
||||
logicFunctionPayloads,
|
||||
DATABASE_EVENT_JOBS_CHUNK_SIZE,
|
||||
);
|
||||
|
||||
for (const serverlessFunctionPayloadsChunk of serverlessFunctionPayloadsChunks) {
|
||||
await this.messageQueueService.add<ServerlessFunctionTriggerJobData[]>(
|
||||
ServerlessFunctionTriggerJob.name,
|
||||
serverlessFunctionPayloadsChunk,
|
||||
for (const logicFunctionPayloadsChunk of logicFunctionPayloadsChunks) {
|
||||
await this.messageQueueService.add<LogicFunctionTriggerJobData[]>(
|
||||
LogicFunctionTriggerJob.name,
|
||||
logicFunctionPayloadsChunk,
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
}
|
||||
|
||||
+43
-43
@@ -2,12 +2,12 @@ import type { ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { transformEventBatchToEventPayloads } from 'src/engine/metadata-modules/database-event-trigger/utils/transform-event-batch-to-event-payloads';
|
||||
import { getFlatObjectMetadataMock } from 'src/engine/metadata-modules/flat-object-metadata/__mocks__/get-flat-object-metadata.mock';
|
||||
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import type { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
|
||||
const createMockServerlessFunction = (
|
||||
overrides: Partial<ServerlessFunctionEntity> = {},
|
||||
): ServerlessFunctionEntity =>
|
||||
const createMockLogicFunction = (
|
||||
overrides: Partial<LogicFunctionEntity> = {},
|
||||
): LogicFunctionEntity =>
|
||||
({
|
||||
id: 'function-1',
|
||||
workspaceId: 'workspace-1',
|
||||
@@ -15,7 +15,7 @@ const createMockServerlessFunction = (
|
||||
eventName: 'company.updated',
|
||||
},
|
||||
...overrides,
|
||||
}) as ServerlessFunctionEntity;
|
||||
}) as LogicFunctionEntity;
|
||||
|
||||
const createMockEvent = (
|
||||
overrides: Partial<ObjectRecordEvent> = {},
|
||||
@@ -43,18 +43,18 @@ const createMockWorkspaceEventBatch = (
|
||||
|
||||
describe('transformEventBatchToEventPayloads', () => {
|
||||
describe('basic transformation', () => {
|
||||
it('should transform a single event batch with a single serverless function', () => {
|
||||
it('should transform a single event batch with a single logic function', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch();
|
||||
const serverlessFunctions = [createMockServerlessFunction()];
|
||||
const logicFunctions = [createMockLogicFunction()];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({
|
||||
serverlessFunctionId: 'function-1',
|
||||
logicFunctionId: 'function-1',
|
||||
workspaceId: 'workspace-1',
|
||||
payload: expect.objectContaining({
|
||||
name: 'company.updated',
|
||||
@@ -72,11 +72,11 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
createMockEvent({ recordId: 'record-3' }),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [createMockServerlessFunction()];
|
||||
const logicFunctions = [createMockLogicFunction()];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
@@ -85,24 +85,24 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
).toEqual(['record-1', 'record-2', 'record-3']);
|
||||
});
|
||||
|
||||
it('should create payloads for each serverless function', () => {
|
||||
it('should create payloads for each logic function', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch();
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
id: 'function-1',
|
||||
}),
|
||||
createMockServerlessFunction({
|
||||
createMockLogicFunction({
|
||||
id: 'function-2',
|
||||
}),
|
||||
];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((r) => r.serverlessFunctionId)).toEqual([
|
||||
expect(result.map((r) => r.logicFunctionId)).toEqual([
|
||||
'function-1',
|
||||
'function-2',
|
||||
]);
|
||||
@@ -124,15 +124,15 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: { eventName: 'company.updated' },
|
||||
}),
|
||||
];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
@@ -152,8 +152,8 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: [],
|
||||
@@ -163,7 +163,7 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
@@ -187,8 +187,8 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: ['name'],
|
||||
@@ -198,7 +198,7 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
@@ -225,8 +225,8 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: ['name', 'address'],
|
||||
@@ -236,7 +236,7 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
@@ -259,8 +259,8 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: ['phone'],
|
||||
@@ -270,13 +270,13 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle different updatedFields filters per serverless function', () => {
|
||||
it('should handle different updatedFields filters per logic function', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch({
|
||||
name: 'company.updated',
|
||||
events: [
|
||||
@@ -290,15 +290,15 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const serverlessFunctions = [
|
||||
createMockServerlessFunction({
|
||||
const logicFunctions = [
|
||||
createMockLogicFunction({
|
||||
id: 'function-1',
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
updatedFields: ['name'],
|
||||
},
|
||||
}),
|
||||
createMockServerlessFunction({
|
||||
createMockLogicFunction({
|
||||
id: 'function-2',
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'company.updated',
|
||||
@@ -309,16 +309,16 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
|
||||
const function1Payloads = result.filter(
|
||||
(r) => r.serverlessFunctionId === 'function-1',
|
||||
(r) => r.logicFunctionId === 'function-1',
|
||||
);
|
||||
const function2Payloads = result.filter(
|
||||
(r) => r.serverlessFunctionId === 'function-2',
|
||||
(r) => r.logicFunctionId === 'function-2',
|
||||
);
|
||||
|
||||
expect(function1Payloads).toHaveLength(1);
|
||||
@@ -334,12 +334,12 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty array when no serverless functions provided', () => {
|
||||
it('should return empty array when no logic functions provided', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch();
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions: [],
|
||||
logicFunctions: [],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
@@ -349,11 +349,11 @@ describe('transformEventBatchToEventPayloads', () => {
|
||||
const workspaceEventBatch = createMockWorkspaceEventBatch({
|
||||
events: [],
|
||||
});
|
||||
const serverlessFunctions = [createMockServerlessFunction()];
|
||||
const logicFunctions = [createMockLogicFunction()];
|
||||
|
||||
const result = transformEventBatchToEventPayloads({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
|
||||
+10
-10
@@ -5,24 +5,24 @@ import type {
|
||||
ObjectRecordEvent,
|
||||
} from 'twenty-shared/database-events';
|
||||
|
||||
import { type ServerlessFunctionTriggerJobData } from 'src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job';
|
||||
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { type LogicFunctionTriggerJobData } from 'src/engine/metadata-modules/logic-function/jobs/logic-function-trigger.job';
|
||||
import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import type { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
|
||||
|
||||
export const transformEventBatchToEventPayloads = ({
|
||||
workspaceEventBatch,
|
||||
serverlessFunctions,
|
||||
logicFunctions,
|
||||
}: {
|
||||
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>;
|
||||
serverlessFunctions: ServerlessFunctionEntity[];
|
||||
}): ServerlessFunctionTriggerJobData[] => {
|
||||
const result: ServerlessFunctionTriggerJobData[] = [];
|
||||
logicFunctions: LogicFunctionEntity[];
|
||||
}): LogicFunctionTriggerJobData[] => {
|
||||
const result: LogicFunctionTriggerJobData[] = [];
|
||||
const { events, ...batchEventInfo } = workspaceEventBatch;
|
||||
const [, operation] = workspaceEventBatch.name.split('.');
|
||||
|
||||
for (const serverlessFunction of serverlessFunctions) {
|
||||
for (const logicFunction of logicFunctions) {
|
||||
const triggerUpdatedFields =
|
||||
serverlessFunction.databaseEventTriggerSettings?.updatedFields;
|
||||
logicFunction.databaseEventTriggerSettings?.updatedFields;
|
||||
|
||||
const filteredEvents = filterEventsByUpdatedFields({
|
||||
events,
|
||||
@@ -34,8 +34,8 @@ export const transformEventBatchToEventPayloads = ({
|
||||
const payload: DatabaseEventPayload = { ...batchEventInfo, ...event };
|
||||
|
||||
result.push({
|
||||
serverlessFunctionId: serverlessFunction.id,
|
||||
workspaceId: serverlessFunction.workspaceId,
|
||||
logicFunctionId: logicFunction.id,
|
||||
workspaceId: logicFunction.workspaceId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
+3
-15
@@ -20,7 +20,7 @@ import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-
|
||||
import { FLAT_VIEW_FILTER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter/constants/flat-view-filter-editable-properties.constant';
|
||||
import { FLAT_VIEW_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-group/constants/flat-view-group-editable-properties.constant';
|
||||
import { FLAT_VIEW_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view/constants/flat-view-editable-properties.constant';
|
||||
import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant';
|
||||
import { FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/logic-function/constants/flat-logic-function-editable-properties.constant';
|
||||
|
||||
type OneFlatEntityConfiguration<T extends AllMetadataName> = {
|
||||
propertiesToCompare: (keyof MetadataFlatEntity<T>)[];
|
||||
@@ -73,9 +73,9 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
],
|
||||
propertiesToStringify: ['flatIndexFieldMetadatas'],
|
||||
},
|
||||
serverlessFunction: {
|
||||
logicFunction: {
|
||||
propertiesToCompare: [
|
||||
...FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES.filter(
|
||||
...FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES.filter(
|
||||
(property) => property !== 'code',
|
||||
),
|
||||
'deletedAt',
|
||||
@@ -84,18 +84,6 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = {
|
||||
],
|
||||
propertiesToStringify: ['toolInputSchema', 'publishedVersions'],
|
||||
},
|
||||
cronTrigger: {
|
||||
propertiesToCompare: [],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
databaseEventTrigger: {
|
||||
propertiesToCompare: [],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
routeTrigger: {
|
||||
propertiesToCompare: [],
|
||||
propertiesToStringify: [],
|
||||
},
|
||||
viewFilter: {
|
||||
propertiesToCompare: [
|
||||
'viewId',
|
||||
|
||||
+2
-5
@@ -16,7 +16,7 @@ import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-t
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RowLevelPermissionPredicateGroupEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate-group.entity';
|
||||
import { RowLevelPermissionPredicateEntity } from 'src/engine/metadata-modules/row-level-permission-predicate/entities/row-level-permission-predicate.entity';
|
||||
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 { SkillEntity } from 'src/engine/metadata-modules/skill/entities/skill.entity';
|
||||
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
@@ -36,14 +36,11 @@ export const ALL_METADATA_ENTITY_BY_METADATA_NAME = {
|
||||
view: ViewEntity,
|
||||
index: IndexMetadataEntity,
|
||||
pageLayoutTab: PageLayoutTabEntity,
|
||||
routeTrigger: ServerlessFunctionEntity,
|
||||
cronTrigger: ServerlessFunctionEntity,
|
||||
databaseEventTrigger: ServerlessFunctionEntity,
|
||||
frontComponent: FrontComponentEntity,
|
||||
fieldMetadata: FieldMetadataEntity,
|
||||
pageLayout: PageLayoutEntity,
|
||||
skill: SkillEntity,
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
logicFunction: LogicFunctionEntity,
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
role: RoleEntity,
|
||||
agent: AgentEntity,
|
||||
|
||||
+2
-26
@@ -261,35 +261,11 @@ export const ALL_METADATA_RELATIONS = {
|
||||
indexFieldMetadatas: null,
|
||||
},
|
||||
},
|
||||
serverlessFunction: {
|
||||
logicFunction: {
|
||||
manyToOne: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
serverlessFunctionLayer: null,
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
cronTrigger: {
|
||||
manyToOne: {
|
||||
application: null,
|
||||
serverlessFunctionLayer: null,
|
||||
workspace: null,
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
databaseEventTrigger: {
|
||||
manyToOne: {
|
||||
application: null,
|
||||
serverlessFunctionLayer: null,
|
||||
workspace: null,
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
routeTrigger: {
|
||||
manyToOne: {
|
||||
application: null,
|
||||
serverlessFunctionLayer: null,
|
||||
workspace: null,
|
||||
logicFunctionLayer: null,
|
||||
},
|
||||
oneToMany: {},
|
||||
},
|
||||
|
||||
+1
-10
@@ -31,16 +31,7 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
|
||||
objectMetadata: true,
|
||||
fieldMetadata: true,
|
||||
},
|
||||
serverlessFunction: {},
|
||||
cronTrigger: {
|
||||
serverlessFunction: true,
|
||||
},
|
||||
databaseEventTrigger: {
|
||||
serverlessFunction: true,
|
||||
},
|
||||
routeTrigger: {
|
||||
serverlessFunction: true,
|
||||
},
|
||||
logicFunction: {},
|
||||
viewFilter: {
|
||||
view: true,
|
||||
fieldMetadata: true,
|
||||
|
||||
+11
-38
@@ -21,7 +21,7 @@ import { type FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-
|
||||
import { type NavigationMenuItemEntity } from 'src/engine/metadata-modules/navigation-menu-item/entities/navigation-menu-item.entity';
|
||||
import { type FlatRowLevelPermissionPredicateGroup } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group.type';
|
||||
import { type FlatRowLevelPermissionPredicate } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate.type';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
import {
|
||||
type CreateAgentAction,
|
||||
type DeleteAgentAction,
|
||||
@@ -93,10 +93,10 @@ import {
|
||||
type UpdateRowLevelPermissionPredicateAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/row-level-permission-predicate/types/workspace-migration-row-level-permission-predicate-action.type';
|
||||
import {
|
||||
type CreateServerlessFunctionAction,
|
||||
type DeleteServerlessFunctionAction,
|
||||
type UpdateServerlessFunctionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type';
|
||||
type CreateLogicFunctionAction,
|
||||
type DeleteLogicFunctionAction,
|
||||
type UpdateLogicFunctionAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/logic-function/types/workspace-migration-logic-function-action.type';
|
||||
import {
|
||||
type CreateSkillAction,
|
||||
type DeleteSkillAction,
|
||||
@@ -215,41 +215,14 @@ export type AllFlatEntityTypesByMetadataName = {
|
||||
flatEntity: FlatIndexMetadata;
|
||||
entity: MetadataEntity<'index'>;
|
||||
};
|
||||
serverlessFunction: {
|
||||
logicFunction: {
|
||||
actions: {
|
||||
create: CreateServerlessFunctionAction;
|
||||
update: UpdateServerlessFunctionAction;
|
||||
delete: DeleteServerlessFunctionAction;
|
||||
create: CreateLogicFunctionAction;
|
||||
update: UpdateLogicFunctionAction;
|
||||
delete: DeleteLogicFunctionAction;
|
||||
};
|
||||
flatEntity: FlatServerlessFunction;
|
||||
entity: MetadataEntity<'serverlessFunction'>;
|
||||
};
|
||||
cronTrigger: {
|
||||
actions: {
|
||||
create: never;
|
||||
update: never;
|
||||
delete: never;
|
||||
};
|
||||
flatEntity: FlatServerlessFunction;
|
||||
entity: MetadataEntity<'serverlessFunction'>;
|
||||
};
|
||||
databaseEventTrigger: {
|
||||
actions: {
|
||||
create: never;
|
||||
update: never;
|
||||
delete: never;
|
||||
};
|
||||
flatEntity: FlatServerlessFunction;
|
||||
entity: MetadataEntity<'serverlessFunction'>;
|
||||
};
|
||||
routeTrigger: {
|
||||
actions: {
|
||||
create: never;
|
||||
update: never;
|
||||
delete: never;
|
||||
};
|
||||
flatEntity: FlatServerlessFunction;
|
||||
entity: MetadataEntity<'serverlessFunction'>;
|
||||
flatEntity: FlatLogicFunction;
|
||||
entity: MetadataEntity<'logicFunction'>;
|
||||
};
|
||||
viewFilter: {
|
||||
actions: {
|
||||
|
||||
+2
-8
@@ -8,10 +8,6 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for cronTrigger 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for databaseEventTrigger 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for fieldMetadata 1`] = `
|
||||
[
|
||||
"objectMetadata",
|
||||
@@ -30,6 +26,8 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for logicFunction 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for navigationMenuItem 1`] = `
|
||||
[
|
||||
"objectMetadata",
|
||||
@@ -81,8 +79,6 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for routeTrigger 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for rowLevelPermissionPredicate 1`] = `
|
||||
[
|
||||
"role",
|
||||
@@ -101,8 +97,6 @@ exports[`getMetadataRelatedMetadataNames should return related metadata names fo
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for serverlessFunction 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for skill 1`] = `[]`;
|
||||
|
||||
exports[`getMetadataRelatedMetadataNames should return related metadata names for view 1`] = `
|
||||
|
||||
+1
-4
@@ -13,13 +13,10 @@ exports[`sortMetadataNamesChildrenFirst should return metadata names sorted with
|
||||
"rowLevelPermissionPredicateGroup",
|
||||
"viewGroup",
|
||||
"agent",
|
||||
"cronTrigger",
|
||||
"databaseEventTrigger",
|
||||
"frontComponent",
|
||||
"logicFunction",
|
||||
"pageLayout",
|
||||
"pageLayoutTab",
|
||||
"routeTrigger",
|
||||
"serverlessFunction",
|
||||
"skill",
|
||||
"view",
|
||||
"viewFilterGroup",
|
||||
|
||||
+18
-18
@@ -7,47 +7,47 @@ import { build } from 'esbuild';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
|
||||
import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils';
|
||||
import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
|
||||
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 FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
|
||||
@Injectable()
|
||||
export class FunctionBuildService {
|
||||
constructor(private readonly fileStorageService: FileStorageService) {}
|
||||
|
||||
async isBuilt({
|
||||
flatServerlessFunction,
|
||||
flatLogicFunction,
|
||||
version,
|
||||
}: {
|
||||
flatServerlessFunction: FlatServerlessFunction;
|
||||
flatLogicFunction: FlatLogicFunction;
|
||||
version: string;
|
||||
}): Promise<boolean> {
|
||||
const folderPath = getServerlessFolderOrThrow({
|
||||
flatServerlessFunction,
|
||||
const folderPath = getLogicFunctionFolderOrThrow({
|
||||
flatLogicFunction,
|
||||
version,
|
||||
});
|
||||
|
||||
return await this.fileStorageService.checkFileExists({
|
||||
folderPath,
|
||||
filename: flatServerlessFunction.builtHandlerPath,
|
||||
filename: flatLogicFunction.builtHandlerPath,
|
||||
});
|
||||
}
|
||||
|
||||
async buildAndUpload({
|
||||
flatServerlessFunction,
|
||||
flatLogicFunction,
|
||||
version,
|
||||
}: {
|
||||
flatServerlessFunction: FlatServerlessFunction;
|
||||
flatLogicFunction: FlatLogicFunction;
|
||||
version: string;
|
||||
}): Promise<void> {
|
||||
const sourceFolderPath = getServerlessFolderOrThrow({
|
||||
flatServerlessFunction,
|
||||
const sourceFolderPath = getLogicFunctionFolderOrThrow({
|
||||
flatLogicFunction,
|
||||
version,
|
||||
fileFolder: FileFolder.ServerlessFunction,
|
||||
fileFolder: FileFolder.LogicFunction,
|
||||
});
|
||||
|
||||
const builtFolderPath = getServerlessFolderOrThrow({
|
||||
flatServerlessFunction,
|
||||
const builtFolderPath = getLogicFunctionFolderOrThrow({
|
||||
flatLogicFunction,
|
||||
version,
|
||||
fileFolder: FileFolder.BuiltFunction,
|
||||
});
|
||||
@@ -64,15 +64,15 @@ export class FunctionBuildService {
|
||||
|
||||
const builtBundleFilePath = await this.buildInMemory({
|
||||
sourceTemporaryDir,
|
||||
sourceHandlerPath: flatServerlessFunction.sourceHandlerPath,
|
||||
builtHandlerPath: flatServerlessFunction.builtHandlerPath,
|
||||
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
|
||||
builtHandlerPath: flatLogicFunction.builtHandlerPath,
|
||||
});
|
||||
|
||||
const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8');
|
||||
|
||||
await this.fileStorageService.write({
|
||||
file: builtFile,
|
||||
name: flatServerlessFunction.builtHandlerPath,
|
||||
name: flatLogicFunction.builtHandlerPath,
|
||||
mimeType: 'application/javascript',
|
||||
folder: builtFolderPath,
|
||||
});
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import GraphQLJSON from 'graphql-type-json';
|
||||
import { PackageJson } from 'twenty-shared/application';
|
||||
|
||||
@ArgsType()
|
||||
export class CreateServerlessFunctionLayerInput {
|
||||
export class CreateLogicFunctionLayerInput {
|
||||
@Field(() => GraphQLJSON, { nullable: false })
|
||||
packageJson: PackageJson;
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('ServerlessFunctionLayer')
|
||||
export class ServerlessFunctionLayerDTO {
|
||||
@ObjectType('LogicFunctionLayer')
|
||||
export class LogicFunctionLayerDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
+6
-6
@@ -9,11 +9,11 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
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('serverlessFunctionLayer')
|
||||
export class ServerlessFunctionLayerEntity extends WorkspaceRelatedEntity {
|
||||
@Entity('logicFunctionLayer')
|
||||
export class LogicFunctionLayerEntity extends WorkspaceRelatedEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@@ -27,13 +27,13 @@ export class ServerlessFunctionLayerEntity extends WorkspaceRelatedEntity {
|
||||
checksum: string;
|
||||
|
||||
@OneToMany(
|
||||
() => ServerlessFunctionEntity,
|
||||
(serverlessFunction) => serverlessFunction.serverlessFunctionLayer,
|
||||
() => LogicFunctionEntity,
|
||||
(logicFunction) => logicFunction.logicFunctionLayer,
|
||||
{
|
||||
onDelete: 'RESTRICT',
|
||||
},
|
||||
)
|
||||
serverlessFunctions: Relation<ServerlessFunctionEntity[]>;
|
||||
logicFunctions: Relation<LogicFunctionEntity[]>;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity';
|
||||
import { LogicFunctionLayerResolver } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.resolver';
|
||||
import { LogicFunctionLayerService } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.service';
|
||||
import { WorkspaceLogicFunctionLayerMapCacheService } from 'src/engine/metadata-modules/logic-function-layer/services/workspace-logic-function-layer-map-cache.service';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PermissionsModule,
|
||||
TypeOrmModule.forFeature([LogicFunctionLayerEntity]),
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [
|
||||
LogicFunctionLayerService,
|
||||
LogicFunctionLayerResolver,
|
||||
WorkspaceLogicFunctionLayerMapCacheService,
|
||||
],
|
||||
exports: [
|
||||
LogicFunctionLayerService,
|
||||
WorkspaceLogicFunctionLayerMapCacheService,
|
||||
],
|
||||
})
|
||||
export class LogicFunctionLayerModule {}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateLogicFunctionLayerInput } from 'src/engine/metadata-modules/logic-function-layer/dtos/create-logic-function-layer.input';
|
||||
import { LogicFunctionLayerDTO } from 'src/engine/metadata-modules/logic-function-layer/dtos/logic-function-layer.dto';
|
||||
import { LogicFunctionLayerService } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@Resolver()
|
||||
export class LogicFunctionLayerResolver {
|
||||
constructor(
|
||||
private readonly logicFunctionLayerService: LogicFunctionLayerService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => LogicFunctionLayerDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
|
||||
async createOneLogicFunctionLayer(
|
||||
@Args()
|
||||
createLogicFunctionLayerInput: CreateLogicFunctionLayerInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
return this.logicFunctionLayerService.create(
|
||||
createLogicFunctionLayerInput,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
|
||||
|
||||
import { LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity';
|
||||
import { CreateLogicFunctionLayerInput } from 'src/engine/metadata-modules/logic-function-layer/dtos/create-logic-function-layer.input';
|
||||
import { getLastCommonLayerDependencies } from 'src/engine/core-modules/logic-function-executor/drivers/utils/get-last-common-layer-dependencies';
|
||||
import { logicFunctionCreateHash } from 'src/engine/metadata-modules/logic-function/utils/logic-function-create-hash.utils';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class LogicFunctionLayerService {
|
||||
constructor(
|
||||
@InjectRepository(LogicFunctionLayerEntity)
|
||||
private readonly logicFunctionLayerRepository: Repository<LogicFunctionLayerEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
{ packageJson, yarnLock }: CreateLogicFunctionLayerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const checksum = logicFunctionCreateHash(yarnLock);
|
||||
|
||||
const logicFunctionLayer = this.logicFunctionLayerRepository.create({
|
||||
packageJson,
|
||||
yarnLock,
|
||||
checksum,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const savedLayer =
|
||||
await this.logicFunctionLayerRepository.save(logicFunctionLayer);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'logicFunctionLayerMaps',
|
||||
]);
|
||||
|
||||
return savedLayer;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: QueryDeepPartialEntity<LogicFunctionLayerEntity>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const checksum = data.yarnLock
|
||||
? logicFunctionCreateHash(data.yarnLock as string)
|
||||
: undefined;
|
||||
|
||||
const updateData = { ...data, ...(checksum && { checksum }) };
|
||||
|
||||
const result = await this.logicFunctionLayerRepository.update(
|
||||
id,
|
||||
updateData,
|
||||
);
|
||||
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'logicFunctionLayerMaps',
|
||||
]);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async createCommonLayerIfNotExist(workspaceId: string) {
|
||||
const { packageJson, yarnLock } = await getLastCommonLayerDependencies();
|
||||
const checksum = logicFunctionCreateHash(yarnLock);
|
||||
const commonLayer = await this.logicFunctionLayerRepository.findOne({
|
||||
where: {
|
||||
checksum,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(commonLayer)) {
|
||||
return commonLayer;
|
||||
}
|
||||
|
||||
return this.create({ packageJson, yarnLock }, workspaceId);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service';
|
||||
|
||||
import { LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity';
|
||||
import { type LogicFunctionLayerCacheMaps } from 'src/engine/metadata-modules/logic-function-layer/types/logic-function-layer-cache-maps.type';
|
||||
import { fromLogicFunctionLayerEntityToFlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/utils/from-logic-function-layer-entity-to-flat-logic-function-layer.util';
|
||||
import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator';
|
||||
|
||||
@Injectable()
|
||||
@WorkspaceCache('logicFunctionLayerMaps')
|
||||
export class WorkspaceLogicFunctionLayerMapCacheService extends WorkspaceCacheProvider<LogicFunctionLayerCacheMaps> {
|
||||
constructor(
|
||||
@InjectRepository(LogicFunctionLayerEntity)
|
||||
private readonly logicFunctionLayerRepository: Repository<LogicFunctionLayerEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<LogicFunctionLayerCacheMaps> {
|
||||
const logicFunctionLayerEntities =
|
||||
await this.logicFunctionLayerRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const logicFunctionLayerMaps: LogicFunctionLayerCacheMaps = {
|
||||
byId: {},
|
||||
};
|
||||
|
||||
for (const entity of logicFunctionLayerEntities) {
|
||||
const flatLogicFunctionLayer =
|
||||
fromLogicFunctionLayerEntityToFlatLogicFunctionLayer(entity);
|
||||
|
||||
logicFunctionLayerMaps.byId[flatLogicFunctionLayer.id] =
|
||||
flatLogicFunctionLayer;
|
||||
}
|
||||
|
||||
return logicFunctionLayerMaps;
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity';
|
||||
|
||||
export type FlatLogicFunctionLayer = FlatEntityFrom<LogicFunctionLayerEntity>;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
|
||||
|
||||
export type LogicFunctionLayerCacheMaps = {
|
||||
byId: Partial<Record<string, FlatLogicFunctionLayer>>;
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { type LogicFunctionLayerEntity } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.entity';
|
||||
import { type FlatLogicFunctionLayer } from 'src/engine/metadata-modules/logic-function-layer/types/flat-logic-function-layer.type';
|
||||
|
||||
export const fromLogicFunctionLayerEntityToFlatLogicFunctionLayer = (
|
||||
entity: LogicFunctionLayerEntity,
|
||||
): FlatLogicFunctionLayer => ({
|
||||
id: entity.id,
|
||||
packageJson: entity.packageJson,
|
||||
yarnLock: entity.yarnLock,
|
||||
checksum: entity.checksum,
|
||||
workspaceId: entity.workspaceId,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
logicFunctionIds: entity.logicFunctions?.map((lf) => lf.id) ?? [],
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
|
||||
export const FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES = [
|
||||
'name',
|
||||
'description',
|
||||
'timeoutSeconds',
|
||||
'checksum',
|
||||
'code',
|
||||
'sourceHandlerPath',
|
||||
'handlerName',
|
||||
'toolInputSchema',
|
||||
'isTool',
|
||||
] as const satisfies (keyof FlatLogicFunction)[];
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const LOGIC_FUNCTION_PUBLISHED = 'logic_function_published';
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { ID, InputType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
@InputType()
|
||||
export class PublishServerlessFunctionInput {
|
||||
export class BuildDraftLogicFunctionInput {
|
||||
@IDField(() => ID, { description: 'The id of the function.' })
|
||||
id!: string;
|
||||
}
|
||||
+2
-2
@@ -14,7 +14,7 @@ import graphqlTypeJson from 'graphql-type-json';
|
||||
import { Sources } from 'twenty-shared/types';
|
||||
|
||||
@InputType()
|
||||
export class CreateServerlessFunctionInput {
|
||||
export class CreateLogicFunctionInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
@@ -39,7 +39,7 @@ export class CreateServerlessFunctionInput {
|
||||
universalIdentifier?: string;
|
||||
|
||||
@HideField()
|
||||
serverlessFunctionLayerId?: string;
|
||||
logicFunctionLayerId?: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, { nullable: true })
|
||||
@IsObject()
|
||||
+3
-3
@@ -6,9 +6,9 @@ import graphqlTypeJson from 'graphql-type-json';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class ExecuteServerlessFunctionInput {
|
||||
export class ExecuteLogicFunctionInput {
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'Id of the serverless function to execute',
|
||||
description: 'Id of the logic function to execute',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsUUID()
|
||||
@@ -22,7 +22,7 @@ export class ExecuteServerlessFunctionInput {
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: false,
|
||||
description: 'Version of the serverless function to execute',
|
||||
description: 'Version of the logic function to execute',
|
||||
defaultValue: 'latest',
|
||||
})
|
||||
version: string;
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
@InputType()
|
||||
export class GetServerlessFunctionSourceCodeInput {
|
||||
export class GetLogicFunctionSourceCodeInput {
|
||||
@IDField(() => ID, { description: 'The id of the function.' })
|
||||
id!: string;
|
||||
|
||||
+8
-8
@@ -3,19 +3,19 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
import { IsObject, IsOptional } from 'class-validator';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
export enum ServerlessFunctionExecutionStatus {
|
||||
export enum LogicFunctionExecutionStatus {
|
||||
IDLE = 'IDLE',
|
||||
SUCCESS = 'SUCCESS',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
registerEnumType(ServerlessFunctionExecutionStatus, {
|
||||
name: 'ServerlessFunctionExecutionStatus',
|
||||
description: 'Status of the serverless function execution',
|
||||
registerEnumType(LogicFunctionExecutionStatus, {
|
||||
name: 'LogicFunctionExecutionStatus',
|
||||
description: 'Status of the logic function execution',
|
||||
});
|
||||
|
||||
@ObjectType('ServerlessFunctionExecutionResult')
|
||||
export class ServerlessFunctionExecutionResultDTO {
|
||||
@ObjectType('LogicFunctionExecutionResult')
|
||||
export class LogicFunctionExecutionResultDTO {
|
||||
@IsObject()
|
||||
@Field(() => graphqlTypeJson, {
|
||||
description: 'Execution result in JSON format',
|
||||
@@ -29,10 +29,10 @@ export class ServerlessFunctionExecutionResultDTO {
|
||||
@Field({ description: 'Execution duration in milliseconds' })
|
||||
duration: number;
|
||||
|
||||
@Field(() => ServerlessFunctionExecutionStatus, {
|
||||
@Field(() => LogicFunctionExecutionStatus, {
|
||||
description: 'Execution status',
|
||||
})
|
||||
status: ServerlessFunctionExecutionStatus;
|
||||
status: LogicFunctionExecutionStatus;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { ID, InputType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
@InputType()
|
||||
export class ServerlessFunctionIdInput {
|
||||
export class LogicFunctionIdInput {
|
||||
@IDField(() => ID, { description: 'The id of the function.' })
|
||||
id!: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user