Keep simplifying logic functions (#17595)

## Summary

Refactors the `LogicFunctionService` API by consolidating v1 and v2
services:

In metadata-module (presentation layer module)
- **Renamed methods**: `deleteOneLogicFunction` → `destroyOne`,
`updateOneLogicFunction` → `updateOne`, `createOneLogicFunction` →
`createOne`
- **Added duplicate methods**: `duplicateLogicFunction`,
`createLogicFunctionFromExistingLogicFunctionById`
- **Removed soft delete/restore** functionality - only hard delete
(`destroyOne`) is supported

In core-module (lower level module)
- **Moved execution methods** to `LogicFunctionExecutorService` which is
lower level: `executeOneLogicFunction`, `getAvailablePackages`,
`getLogicFunctionSourceCode`
This commit is contained in:
Charles Bochet
2026-01-30 20:30:52 +01:00
committed by GitHub
parent db31b83a86
commit 46d28509b9
18 changed files with 371 additions and 803 deletions
@@ -4617,6 +4617,7 @@ export type UpdateLogicFunctionInput = {
};
export type UpdateLogicFunctionInputUpdates = {
builtHandlerPath?: InputMaybe<Scalars['String']>;
code: Scalars['JSON'];
description?: InputMaybe<Scalars['String']>;
handlerName?: InputMaybe<Scalars['String']>;
@@ -4456,6 +4456,7 @@ export type UpdateLogicFunctionInput = {
};
export type UpdateLogicFunctionInputUpdates = {
builtHandlerPath?: InputMaybe<Scalars['String']>;
code: Scalars['JSON'];
description?: InputMaybe<Scalars['String']>;
handlerName?: InputMaybe<Scalars['String']>;
@@ -3,16 +3,15 @@ import { Injectable, Logger } from '@nestjs/common';
import { parse } from 'path';
import {
Manifest,
FieldManifest,
LogicFunctionManifest,
LogicFunctionTriggerManifest,
ObjectManifest,
RoleManifest,
Manifest,
ObjectFieldManifest,
ObjectManifest,
RelationFieldManifest,
RoleManifest,
} from 'twenty-shared/application';
import { FieldMetadataType, HTTPMethod, Sources } from 'twenty-shared/types';
import { FieldMetadataType, Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@@ -23,25 +22,20 @@ import {
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
import { LogicFunctionLayerService } from 'src/engine/core-modules/logic-function/logic-function-layer/services/logic-function-layer.service';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntitiesByApplicationId } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entities-by-application-id.util';
import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
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 { LogicFunctionLayerService } from 'src/engine/core-modules/logic-function/logic-function-layer/services/logic-function-layer.service';
import {
CronTriggerSettings,
DatabaseEventTriggerSettings,
HttpRouteTriggerSettings,
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
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';
@@ -55,7 +49,7 @@ export class ApplicationSyncService {
private readonly logicFunctionLayerService: LogicFunctionLayerService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly fieldMetadataService: FieldMetadataService,
private readonly logicFunctionV2Service: LogicFunctionV2Service,
private readonly logicFunctionService: LogicFunctionService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly dataSourceService: DataSourceService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
@@ -844,9 +838,10 @@ export class ApplicationSyncService {
);
for (const logicFunctionToDelete of logicFunctionsToDelete) {
await this.logicFunctionV2Service.destroyOne({
destroyLogicFunctionInput: { id: logicFunctionToDelete.id },
await this.logicFunctionService.destroyOne({
id: logicFunctionToDelete.id,
workspaceId,
applicationId,
isSystemBuild: true,
});
}
@@ -868,7 +863,7 @@ export class ApplicationSyncService {
const name =
logicFunctionToSync.name ?? parse(logicFunctionToSync.handlerName).name;
const updateLogicFunctionInput = {
await this.logicFunctionService.updateOne({
id: logicFunctionToUpdate.id,
update: {
name,
@@ -880,12 +875,9 @@ export class ApplicationSyncService {
toolInputSchema: logicFunctionToSync.toolInputSchema,
isTool: logicFunctionToSync.isTool,
},
};
await this.logicFunctionV2Service.updateOne(
updateLogicFunctionInput,
workspaceId,
);
applicationId,
});
// Trigger settings are now embedded in the logic function entity
// They are handled through the update input
@@ -896,68 +888,25 @@ export class ApplicationSyncService {
logicFunctionToCreate.name ??
parse(logicFunctionToCreate.handlerName).name;
const createLogicFunctionInput = {
name,
code,
universalIdentifier: logicFunctionToCreate.universalIdentifier,
timeoutSeconds: logicFunctionToCreate.timeoutSeconds,
sourceHandlerPath: logicFunctionToCreate.sourceHandlerPath,
handlerName: logicFunctionToCreate.handlerName,
builtHandlerPath: logicFunctionToCreate.builtHandlerPath,
applicationId,
logicFunctionLayerId,
toolInputSchema: logicFunctionToCreate.toolInputSchema,
isTool: logicFunctionToCreate.isTool,
};
await this.logicFunctionV2Service.createOne({
createLogicFunctionInput,
await this.logicFunctionService.createOne({
input: {
name,
code,
universalIdentifier: logicFunctionToCreate.universalIdentifier,
timeoutSeconds: logicFunctionToCreate.timeoutSeconds,
sourceHandlerPath: logicFunctionToCreate.sourceHandlerPath,
handlerName: logicFunctionToCreate.handlerName,
builtHandlerPath: logicFunctionToCreate.builtHandlerPath,
logicFunctionLayerId,
toolInputSchema: logicFunctionToCreate.toolInputSchema,
isTool: logicFunctionToCreate.isTool,
},
workspaceId,
applicationId,
});
// Trigger settings are now embedded in the logic function entity
// They are handled through the create input
}
}
private extractTriggerSettingsFromManifest(
triggers: LogicFunctionTriggerManifest[] = [],
): {
cronTriggerSettings: CronTriggerSettings | null;
databaseEventTriggerSettings: DatabaseEventTriggerSettings | null;
httpRouteTriggerSettings: HttpRouteTriggerSettings | null;
} {
let cronTriggerSettings: CronTriggerSettings | null = null;
let databaseEventTriggerSettings: DatabaseEventTriggerSettings | null =
null;
let httpRouteTriggerSettings: HttpRouteTriggerSettings | null = null;
for (const trigger of triggers) {
if (trigger.type === 'cron') {
cronTriggerSettings = { pattern: trigger.pattern };
} else if (trigger.type === 'databaseEvent') {
databaseEventTriggerSettings = {
eventName: trigger.eventName,
updatedFields: trigger.updatedFields,
};
} else if (trigger.type === 'route') {
httpRouteTriggerSettings = {
path: trigger.path,
httpMethod: trigger.httpMethod as HTTPMethod,
isAuthRequired: trigger.isAuthRequired,
forwardedRequestHeaders: trigger.forwardedRequestHeaders,
};
}
}
return {
cronTriggerSettings,
databaseEventTriggerSettings,
httpRouteTriggerSettings,
};
}
public async uninstallApplication({
workspaceId,
applicationUniversalIdentifier,
@@ -1,12 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { LogicFunctionBuildModule } from 'src/engine/core-modules/logic-function/logic-function-build/logic-function-build.module';
import { AddPackagesCommand } from 'src/engine/core-modules/logic-function/logic-function-executor/commands/add-packages.command';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@@ -19,6 +22,8 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
SubscriptionsModule,
WorkspaceCacheModule,
LogicFunctionBuildModule,
FileModule,
TypeOrmModule.forFeature([LogicFunctionEntity]),
],
providers: [LogicFunctionExecutorService, AddPackagesCommand],
exports: [LogicFunctionExecutorService],
@@ -1,28 +1,36 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
DEFAULT_API_KEY_NAME,
DEFAULT_API_URL_NAME,
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import {
LogicFunctionExecutorDriver,
type LogicFunctionExecuteParams,
type LogicFunctionExecuteResult,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-executor-driver.interface';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-drivers/utils/build-env-var';
import { LOGIC_FUNCTION_EXECUTOR_DRIVER } from 'src/engine/core-modules/logic-function/logic-function-executor/constants/logic-function-executor.constants';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
@@ -60,6 +68,9 @@ export class LogicFunctionExecutorService
private readonly functionBuildService: LogicFunctionBuildService,
private readonly subscriptionService: SubscriptionService,
private readonly auditService: AuditService,
private readonly fileStorageService: FileStorageService,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
) {}
async delete(flatLogicFunction: FlatLogicFunction): Promise<void> {
@@ -228,6 +239,84 @@ export class LogicFunctionExecutorService
return resultLogicFunction;
}
async getLogicFunctionSourceCode(workspaceId: string, id: string) {
try {
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
'flatApplicationMaps',
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new LogicFunctionExecutionException(
`Application universal identifier not found for logic function ${id}`,
LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
return await this.fileStorageService.readFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
} catch (error) {
if (
isDefined(error) &&
'code' in error &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
return;
}
throw error;
}
}
async getAvailablePackages(logicFunctionId: string) {
const logicFunction = await this.logicFunctionRepository.findOneOrFail({
where: { id: logicFunctionId },
relations: ['logicFunctionLayer'],
});
const packageJson = logicFunction.logicFunctionLayer.packageJson;
const yarnLock = logicFunction.logicFunctionLayer.yarnLock;
const packageVersionRegex = /^"([^@]+)@.*?":\n\s+version: (.+)$/gm;
const versions: Record<string, string> = {};
let match: RegExpExecArray | null;
while ((match = packageVersionRegex.exec(yarnLock)) !== null) {
const packageName = match[1].split('@', 1)[0];
const version = match[2];
// @ts-expect-error legacy noImplicitAny
if (packageJson.dependencies?.[packageName]) {
versions[packageName] = version;
}
}
return versions;
}
private async throttleExecution(workspaceId: string) {
try {
await this.throttlerService.tokenBucketThrottleOrThrow(
@@ -8,10 +8,10 @@ import {
type ToolProviderContext,
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
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 { 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()
@@ -19,7 +19,7 @@ export class LogicFunctionToolProvider implements ToolProvider {
readonly category = ToolCategory.LOGIC_FUNCTION;
constructor(
private readonly logicFunctionService: LogicFunctionService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -63,7 +63,7 @@ export class LogicFunctionToolProvider implements ToolProvider {
const { loadingMessage: _, ...actualParams } = parameters;
const result =
await this.logicFunctionService.executeOneLogicFunction({
await this.logicFunctionExecutorService.executeOneLogicFunction({
id: logicFunction.id,
workspaceId: context.workspaceId,
payload: actualParams,
@@ -52,6 +52,11 @@ class UpdateLogicFunctionInputUpdates {
@IsOptional()
sourceHandlerPath?: string;
@IsString()
@Field({ nullable: true })
@IsOptional()
builtHandlerPath?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
@@ -11,14 +11,15 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { CoreLogicFunctionLayerModule } from 'src/engine/core-modules/logic-function/logic-function-layer/logic-function-layer.module';
import { SecretEncryptionModule } from 'src/engine/core-modules/secret-encryption/secret-encryption.module';
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionResolver } from 'src/engine/metadata-modules/logic-function/logic-function.resolver';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionV2Service } from 'src/engine/metadata-modules/logic-function/services/logic-function-v2.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { WorkspaceFlatLogicFunctionMapCacheService } from 'src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
@@ -39,6 +40,8 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationModule,
LogicFunctionLayerModule,
CoreLogicFunctionLayerModule,
LogicFunctionExecutorModule,
SubscriptionsModule,
WorkspaceCacheModule,
TokenModule,
@@ -46,10 +49,9 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
],
providers: [
LogicFunctionService,
LogicFunctionV2Service,
LogicFunctionResolver,
WorkspaceFlatLogicFunctionMapCacheService,
],
exports: [LogicFunctionService, LogicFunctionV2Service],
exports: [LogicFunctionService, LogicFunctionExecutorModule],
})
export class LogicFunctionModule {}
@@ -7,6 +7,7 @@ import { isDefined } from 'twenty-shared/utils';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
@@ -22,7 +23,7 @@ import { LogicFunctionLogsDTO } from 'src/engine/metadata-modules/logic-function
import { LogicFunctionLogsInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.input';
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
import { UpdateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromFlatLogicFunctionToLogicFunctionDto } from 'src/engine/metadata-modules/logic-function/utils/from-flat-logic-function-to-logic-function-dto.util';
@@ -41,6 +42,7 @@ import { SubscriptionService } from 'src/engine/subscriptions/subscription.servi
export class LogicFunctionResolver {
constructor(
private readonly logicFunctionService: LogicFunctionService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly subscriptionService: SubscriptionService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -104,7 +106,7 @@ export class LogicFunctionResolver {
@Query(() => graphqlTypeJson)
async getAvailablePackages(@Args('input') { id }: LogicFunctionIdInput) {
try {
return await this.logicFunctionService.getAvailablePackages(id);
return await this.logicFunctionExecutorService.getAvailablePackages(id);
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -116,7 +118,7 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.logicFunctionService.getLogicFunctionSourceCode(
return await this.logicFunctionExecutorService.getLogicFunctionSourceCode(
workspaceId,
input.id,
);
@@ -132,11 +134,10 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
const flatLogicFunction =
await this.logicFunctionService.deleteOneLogicFunction({
id: input.id,
workspaceId,
});
const flatLogicFunction = await this.logicFunctionService.destroyOne({
id: input.id,
workspaceId,
});
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction,
@@ -154,11 +155,11 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
const flatLogicFunction =
await this.logicFunctionService.updateOneLogicFunction(
input,
workspaceId,
);
const flatLogicFunction = await this.logicFunctionService.updateOne({
id: input.id,
update: input.update,
workspaceId,
});
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction,
@@ -176,11 +177,10 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
const flatLogicFunction =
await this.logicFunctionService.createOneLogicFunction(
input,
workspaceId,
);
const flatLogicFunction = await this.logicFunctionService.createOne({
input,
workspaceId,
});
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction,
@@ -199,7 +199,7 @@ export class LogicFunctionResolver {
try {
const { id, payload } = input;
return await this.logicFunctionService.executeOneLogicFunction({
return await this.logicFunctionExecutorService.executeOneLogicFunction({
id,
workspaceId,
payload,
@@ -1,544 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import { type LogicFunctionExecuteResult } from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-executor-driver.interface';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { LogicFunctionBuildService } from 'src/engine/core-modules/logic-function/logic-function-build/services/logic-function-build.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import {
LogicFunctionExecutionException,
LogicFunctionExecutionExceptionCode,
LogicFunctionExecutorService,
} from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { LogicFunctionLayerService } from 'src/engine/core-modules/logic-function/logic-function-layer/services/logic-function-layer.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import { type UpdateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
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';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromCreateLogicFunctionInputToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-input-to-flat-logic-function.util';
import { fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/logic-function/utils/from-update-logic-function-input-to-flat-logic-function-to-update-or-throw.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class LogicFunctionService {
constructor(
private readonly fileStorageService: FileStorageService,
private readonly functionBuildService: LogicFunctionBuildService,
private readonly logicFunctionLayerService: LogicFunctionLayerService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
async getLogicFunctionSourceCode(workspaceId: string, id: string) {
try {
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
'flatApplicationMaps',
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
const applicationUniversalIdentifier = isDefined(
flatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[flatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const baseFolderPath = getLogicFunctionBaseFolderPath(
flatLogicFunction.sourceHandlerPath,
);
return await this.fileStorageService.readFolder_v2({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: baseFolderPath,
});
} catch (error) {
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
return;
}
throw error;
}
}
async executeOneLogicFunction({
id,
workspaceId,
payload,
}: {
id: string;
workspaceId: string;
payload: object;
}): Promise<LogicFunctionExecuteResult> {
try {
return await this.logicFunctionExecutorService.executeOneLogicFunction({
id,
workspaceId,
payload,
});
} catch (error) {
if (error instanceof LogicFunctionExecutionException) {
switch (error.code) {
case LogicFunctionExecutionExceptionCode.LOGIC_FUNCTION_NOT_FOUND:
throw new LogicFunctionException(
error.message,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
case LogicFunctionExecutionExceptionCode.RATE_LIMIT_EXCEEDED:
throw new LogicFunctionException(
error.message,
LogicFunctionExceptionCode.LOGIC_FUNCTION_EXECUTION_LIMIT_REACHED,
);
}
}
throw error;
}
}
async deleteOneLogicFunction({
id,
workspaceId,
softDelete = false,
}: {
id: string;
workspaceId: string;
softDelete?: boolean;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingFlatLogicFunction = flatLogicFunctionMaps.byId[id];
if (!isDefined(existingFlatLogicFunction)) {
throw new LogicFunctionException(
'Logic function to delete not found',
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
if (softDelete) {
const updatedFlatLogicFunctionWithDeletedAt: FlatLogicFunction = {
...existingFlatLogicFunction,
deletedAt: new Date().toISOString(),
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [updatedFlatLogicFunctionWithDeletedAt],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting logic function',
);
}
return updatedFlatLogicFunctionWithDeletedAt;
} else {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatLogicFunction],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying logic function',
);
}
}
return existingFlatLogicFunction;
}
async restoreOneLogicFunction(
id: string,
workspaceId: string,
): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingFlatLogicFunction = flatLogicFunctionMaps.byId[id];
if (!isDefined(existingFlatLogicFunction)) {
throw new LogicFunctionException(
'Logic function to restore not found',
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const restoredFlatLogicFunction: FlatLogicFunction = {
...existingFlatLogicFunction,
deletedAt: null,
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [restoredFlatLogicFunction],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while restoring logic function',
);
}
const { flatLogicFunctionMaps: recomputedFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedFlatLogicFunctionMaps,
});
}
async updateOneLogicFunction(
logicFunctionInput: UpdateLogicFunctionInput,
workspaceId: string,
): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const updatedFlatLogicFunction =
fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow({
flatLogicFunctionMaps,
updateLogicFunctionInput: logicFunctionInput,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [updatedFlatLogicFunction],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating logic function',
);
}
const { flatLogicFunctionMaps: recomputedFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: updatedFlatLogicFunction.id,
flatEntityMaps: recomputedFlatLogicFunctionMaps,
});
}
async getAvailablePackages(logicFunctionId: string) {
const logicFunction = await this.logicFunctionRepository.findOneOrFail({
where: { id: logicFunctionId },
relations: ['logicFunctionLayer'],
});
const packageJson = logicFunction.logicFunctionLayer.packageJson;
const yarnLock = logicFunction.logicFunctionLayer.yarnLock;
const packageVersionRegex = /^"([^@]+)@.*?":\n\s+version: (.+)$/gm;
const versions: Record<string, string> = {};
let match: RegExpExecArray | null;
while ((match = packageVersionRegex.exec(yarnLock)) !== null) {
const packageName = match[1].split('@', 1)[0];
const version = match[2];
// @ts-expect-error legacy noImplicitAny
if (packageJson.dependencies?.[packageName]) {
versions[packageName] = version;
}
}
return versions;
}
async createOneLogicFunction(
logicFunctionInput: CreateLogicFunctionInput & {
logicFunctionLayerId?: string;
},
workspaceId: string,
): Promise<FlatLogicFunction> {
let logicFunctionToCreateLayerId = logicFunctionInput.logicFunctionLayerId;
if (!isDefined(logicFunctionToCreateLayerId)) {
const { id: commonLogicFunctionLayerId } =
await this.logicFunctionLayerService.createCommonLayerIfNotExist(
workspaceId,
);
logicFunctionToCreateLayerId = commonLogicFunctionLayerId;
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
workspaceId,
},
);
const flatLogicFunctionToCreate =
fromCreateLogicFunctionInputToFlatLogicFunction({
createLogicFunctionInput: {
...logicFunctionInput,
logicFunctionLayerId: logicFunctionToCreateLayerId,
},
workspaceId,
workspaceCustomApplicationId:
logicFunctionInput.applicationId ?? workspaceCustomFlatApplication.id,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [flatLogicFunctionToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating logic function',
);
}
const { flatLogicFunctionMaps: recomputedFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatLogicFunctionToCreate.id,
flatEntityMaps: recomputedFlatLogicFunctionMaps,
});
}
async duplicateLogicFunction({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
return this.createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return this.createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
}: {
existingLogicFunction: FlatLogicFunction;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatApplicationMaps',
]);
const existingApplicationUniversalIdentifier = isDefined(
existingLogicFunction.applicationId,
)
? flatApplicationMaps.byId[existingLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(existingApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${existingLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const newFlatLogicFunction = await this.createOneLogicFunction(
{
name: existingLogicFunction.name,
description: existingLogicFunction.description ?? undefined,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
applicationId: existingLogicFunction.applicationId ?? undefined,
logicFunctionLayerId: existingLogicFunction.logicFunctionLayerId,
},
workspaceId,
);
const newApplicationUniversalIdentifier = isDefined(
newFlatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[newFlatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(newApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${newFlatLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const fromBaseFolderPath = getLogicFunctionBaseFolderPath(
existingLogicFunction.sourceHandlerPath,
);
const toBaseFolderPath = getLogicFunctionBaseFolderPath(
newFlatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier: existingApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier: newApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toBaseFolderPath,
},
});
return newFlatLogicFunction;
}
}
@@ -1,46 +1,61 @@
import { Injectable } from '@nestjs/common';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { getLogicFunctionBaseFolderPath } from 'src/engine/core-modules/logic-function/logic-function-build/utils/get-logic-function-base-folder-path.util';
import { LogicFunctionLayerService } from 'src/engine/core-modules/logic-function/logic-function-layer/services/logic-function-layer.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import type { CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import { LogicFunctionIdInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-id.input';
import { UpdateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
import type { UpdateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromCreateLogicFunctionInputToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-input-to-flat-logic-function.util';
import { fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/logic-function/utils/from-update-logic-function-input-to-flat-logic-function-to-update-or-throw.util';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class LogicFunctionV2Service {
export class LogicFunctionService {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
private readonly logicFunctionLayerService: LogicFunctionLayerService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fileStorageService: FileStorageService,
) {}
async createOne({
createLogicFunctionInput,
input,
workspaceId,
applicationId,
}: {
createLogicFunctionInput: CreateLogicFunctionInput & {
logicFunctionLayerId: string;
input: Omit<CreateLogicFunctionInput, 'applicationId'> & {
logicFunctionLayerId?: string;
};
/**
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
* when interacting with another application than workspace custom one
* */
applicationId?: string;
workspaceId: string;
applicationId?: string;
}) {
let logicFunctionToCreateLayerId = input.logicFunctionLayerId;
if (!isDefined(logicFunctionToCreateLayerId)) {
const { id: commonLogicFunctionLayerId } =
await this.logicFunctionLayerService.createCommonLayerIfNotExist(
workspaceId,
);
logicFunctionToCreateLayerId = commonLogicFunctionLayerId;
}
const { workspaceCustomFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{
@@ -50,7 +65,10 @@ export class LogicFunctionV2Service {
const flatLogicFunctionToCreate =
fromCreateLogicFunctionInputToFlatLogicFunction({
createLogicFunctionInput,
createLogicFunctionInput: {
...input,
logicFunctionLayerId: logicFunctionToCreateLayerId,
},
workspaceId,
workspaceCustomApplicationId:
applicationId ?? workspaceCustomFlatApplication.id,
@@ -92,10 +110,17 @@ export class LogicFunctionV2Service {
});
}
async updateOne(
logicFunctionInput: UpdateLogicFunctionInput,
workspaceId: string,
) {
async updateOne({
id,
update,
workspaceId,
applicationId: _applicationId,
}: {
id: string;
update: UpdateLogicFunctionInput['update'];
workspaceId: string;
applicationId?: string;
}) {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
@@ -107,7 +132,7 @@ export class LogicFunctionV2Service {
const optimisticallyUpdatedFlatLogicFunction =
fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow({
flatLogicFunctionMaps,
updateLogicFunctionInput: logicFunctionInput,
updateLogicFunctionInput: { id, update },
});
const validateAndBuildResult =
@@ -146,83 +171,15 @@ export class LogicFunctionV2Service {
});
}
async deleteOne({
deleteLogicFunctionInput,
workspaceId,
isSystemBuild = false,
}: {
deleteLogicFunctionInput: LogicFunctionIdInput;
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps: existingFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingFlatLogicFunction =
existingFlatLogicFunctionMaps.byId[deleteLogicFunctionInput.id];
if (!isDefined(existingFlatLogicFunction)) {
throw new LogicFunctionException(
'Logic function to delete not found',
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const optimisticallyUpdatedFlatLogicFunctionWithDeletedAt = {
...existingFlatLogicFunction,
deletedAt: new Date().toISOString(),
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [
optimisticallyUpdatedFlatLogicFunctionWithDeletedAt,
],
},
},
workspaceId,
isSystemBuild,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while deleting logic function',
);
}
const { flatLogicFunctionMaps: recomputedExistingFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: optimisticallyUpdatedFlatLogicFunctionWithDeletedAt.id,
flatEntityMaps: recomputedExistingFlatLogicFunctionMaps,
});
}
async destroyOne({
destroyLogicFunctionInput,
id,
workspaceId,
applicationId: _applicationId,
isSystemBuild = false,
}: {
destroyLogicFunctionInput: LogicFunctionIdInput;
id: string;
workspaceId: string;
applicationId?: string;
isSystemBuild?: boolean;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps: existingFlatLogicFunctionMaps } =
@@ -233,8 +190,7 @@ export class LogicFunctionV2Service {
},
);
const existingFlatLogicFunction =
existingFlatLogicFunctionMaps.byId[destroyLogicFunctionInput.id];
const existingFlatLogicFunction = existingFlatLogicFunctionMaps.byId[id];
if (!isDefined(existingFlatLogicFunction)) {
throw new LogicFunctionException(
@@ -267,4 +223,119 @@ export class LogicFunctionV2Service {
return existingFlatLogicFunction;
}
async duplicateLogicFunction({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
return this.createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunctionById({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return this.createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
});
}
async createLogicFunctionFromExistingLogicFunction({
existingLogicFunction,
workspaceId,
}: {
existingLogicFunction: FlatLogicFunction;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatApplicationMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatApplicationMaps',
]);
const existingApplicationUniversalIdentifier = isDefined(
existingLogicFunction.applicationId,
)
? flatApplicationMaps.byId[existingLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(existingApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${existingLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const newFlatLogicFunction = await this.createOne({
input: {
name: existingLogicFunction.name,
description: existingLogicFunction.description ?? undefined,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
logicFunctionLayerId: existingLogicFunction.logicFunctionLayerId,
},
workspaceId,
applicationId: existingLogicFunction.applicationId ?? undefined,
});
const newApplicationUniversalIdentifier = isDefined(
newFlatLogicFunction.applicationId,
)
? flatApplicationMaps.byId[newFlatLogicFunction.applicationId]
?.universalIdentifier
: undefined;
if (!isDefined(newApplicationUniversalIdentifier)) {
throw new LogicFunctionException(
`Application universal identifier not found for logic function ${newFlatLogicFunction.id}`,
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const fromBaseFolderPath = getLogicFunctionBaseFolderPath(
existingLogicFunction.sourceHandlerPath,
);
const toBaseFolderPath = getLogicFunctionBaseFolderPath(
newFlatLogicFunction.sourceHandlerPath,
);
await this.fileStorageService.copy_v2({
from: {
workspaceId,
applicationUniversalIdentifier: existingApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: fromBaseFolderPath,
},
to: {
workspaceId,
applicationUniversalIdentifier: newApplicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: toBaseFolderPath,
},
});
return newFlatLogicFunction;
}
}
@@ -7,7 +7,7 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -303,6 +303,11 @@ export class WorkflowCommonWorkspaceService {
workspaceId: string;
operation: 'restore' | 'delete' | 'destroy';
}) {
// Only handle destroy operation - soft delete/restore is no longer supported
if (operation !== 'destroy') {
return;
}
const workflowVersions = await workflowVersionRepository.find({
where: {
workflowId,
@@ -310,33 +315,15 @@ export class WorkflowCommonWorkspaceService {
withDeleted: true,
});
workflowVersions.forEach((workflowVersion) => {
workflowVersion.steps?.forEach(async (step) => {
for (const workflowVersion of workflowVersions) {
for (const step of workflowVersion.steps ?? []) {
if (step.type === WorkflowActionType.CODE) {
switch (operation) {
case 'delete':
await this.logicFunctionService.deleteOneLogicFunction({
id: step.settings.input.logicFunctionId,
workspaceId,
softDelete: true,
});
break;
case 'restore':
await this.logicFunctionService.restoreOneLogicFunction(
step.settings.input.logicFunctionId,
workspaceId,
);
break;
case 'destroy':
await this.logicFunctionService.deleteOneLogicFunction({
id: step.settings.input.logicFunctionId,
workspaceId,
softDelete: false,
});
break;
}
await this.logicFunctionService.destroyOne({
id: step.settings.input.logicFunctionId,
workspaceId,
});
}
});
});
}
}
}
}
@@ -6,7 +6,7 @@ import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/ag
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
@@ -36,8 +36,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
beforeEach(async () => {
logicFunctionService = {
createOneLogicFunction: jest.fn(),
deleteOneLogicFunction: jest.fn(),
createOne: jest.fn(),
destroyOne: jest.fn(),
duplicateLogicFunction: jest.fn(),
} as unknown as jest.Mocked<LogicFunctionService>;
@@ -154,10 +154,9 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
workspaceId: mockWorkspaceId,
});
expect(logicFunctionService.deleteOneLogicFunction).toHaveBeenCalledWith({
expect(logicFunctionService.destroyOne).toHaveBeenCalledWith({
id: 'function-id',
workspaceId: mockWorkspaceId,
softDelete: false,
});
});
@@ -265,9 +264,7 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
httpRouteTriggerSettings: null,
};
logicFunctionService.createOneLogicFunction.mockResolvedValue(
mockFlatLogicFunction,
);
logicFunctionService.createOne.mockResolvedValue(mockFlatLogicFunction);
const result = await service.runStepCreationSideEffectsAndBuildStep({
type: WorkflowActionType.CODE,
@@ -21,7 +21,7 @@ import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
@@ -86,10 +86,9 @@ export class WorkflowVersionStepOperationsWorkspaceService {
}) {
switch (step.type) {
case WorkflowActionType.CODE: {
await this.logicFunctionService.deleteOneLogicFunction({
await this.logicFunctionService.destroyOne({
id: step.settings.input.logicFunctionId,
workspaceId,
softDelete: false,
});
break;
}
@@ -152,14 +151,13 @@ export class WorkflowVersionStepOperationsWorkspaceService {
switch (type) {
case WorkflowActionType.CODE: {
const newLogicFunction =
await this.logicFunctionService.createOneLogicFunction(
{
name: 'A Logic Function Code Workflow Step',
description: '',
},
workspaceId,
);
const newLogicFunction = await this.logicFunctionService.createOne({
input: {
name: 'A Logic Function Code Workflow Step',
description: '',
},
workspaceId,
});
if (!isDefined(newLogicFunction)) {
throw new WorkflowVersionStepException(
@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { CodeWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/code/code.workflow-action';
@Module({
imports: [LogicFunctionModule],
imports: [LogicFunctionExecutorModule],
providers: [CodeWorkflowAction],
exports: [CodeWorkflowAction],
})
@@ -4,7 +4,7 @@ import { resolveInput } from 'twenty-shared/utils';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
@@ -17,7 +17,9 @@ import { type WorkflowCodeActionInput } from 'src/modules/workflow/workflow-exec
@Injectable()
export class CodeWorkflowAction implements WorkflowAction {
constructor(private readonly logicFunctionService: LogicFunctionService) {}
constructor(
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
) {}
async execute({
currentStepId,
@@ -45,11 +47,12 @@ export class CodeWorkflowAction implements WorkflowAction {
try {
const { workspaceId } = runInfo;
const result = await this.logicFunctionService.executeOneLogicFunction({
id: workflowActionInput.logicFunctionId,
workspaceId,
payload: workflowActionInput.logicFunctionInput,
});
const result =
await this.logicFunctionExecutorService.executeOneLogicFunction({
id: workflowActionInput.logicFunctionId,
workspaceId,
payload: workflowActionInput.logicFunctionInput,
});
if (result.error) {
return { error: result.error.errorMessage };
@@ -1,11 +1,14 @@
import { Module } from '@nestjs/common';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { LogicFunctionWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/logic-function/logic-function.workflow-action';
@Module({
imports: [LogicFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule],
imports: [
LogicFunctionExecutorModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
],
providers: [LogicFunctionWorkflowAction],
exports: [LogicFunctionWorkflowAction],
})
@@ -4,8 +4,8 @@ import { resolveInput } from 'twenty-shared/utils';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/services/logic-function-executor.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/logic-function.service';
import {
WorkflowStepExecutorException,
WorkflowStepExecutorExceptionCode,
@@ -19,7 +19,7 @@ import { WorkflowLogicFunctionActionInput } from 'src/modules/workflow/workflow-
@Injectable()
export class LogicFunctionWorkflowAction implements WorkflowAction {
constructor(
private readonly logicFunctionService: LogicFunctionService,
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -67,11 +67,12 @@ export class LogicFunctionWorkflowAction implements WorkflowAction {
);
}
const result = await this.logicFunctionService.executeOneLogicFunction({
id: workflowActionInput.logicFunctionId,
workspaceId,
payload: workflowActionInput.logicFunctionInput,
});
const result =
await this.logicFunctionExecutorService.executeOneLogicFunction({
id: workflowActionInput.logicFunctionId,
workspaceId,
payload: workflowActionInput.logicFunctionInput,
});
if (result.error) {
return { error: result.error.errorMessage };