import { Injectable } from '@nestjs/common'; import crypto from 'crypto'; import { v4 } from 'uuid'; import { isDefined } from 'twenty-shared/utils'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service'; import { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.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 { CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input'; import { LogicFunctionExecutionResultDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto'; import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto'; import { LogicFunctionExecutionMode } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; import { LogicFunctionException, LogicFunctionExceptionCode, } from 'src/engine/metadata-modules/logic-function/logic-function.exception'; import { LogicFunctionFromSourceHelperService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source-helper.service'; import { type UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input'; import { buildUniversalFlatLogicFunctionToCreate } from 'src/engine/metadata-modules/logic-function/utils/build-universal-flat-logic-function-to-create.util'; import { fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-from-source-input-to-universal-flat-logic-function-to-create.util'; import { fromFlatLogicFunctionToLogicFunctionDto } from 'src/engine/metadata-modules/logic-function/utils/from-flat-logic-function-to-logic-function-dto.util'; import { fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate } from 'src/engine/metadata-modules/logic-function/utils/from-update-logic-function-from-source-input-to-flat-logic-function-to-update.util'; 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 LogicFunctionFromSourceService { constructor( private readonly logicFunctionExecutorService: LogicFunctionExecutorService, private readonly logicFunctionResourceService: LogicFunctionResourceService, private readonly applicationService: ApplicationService, private readonly helperService: LogicFunctionFromSourceHelperService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, ) {} async createOneFromSource({ input, workspaceId, }: { input: CreateLogicFunctionFromSourceInput; workspaceId: string; }): Promise { const { workspaceCustomFlatApplication: ownerFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ); const logicFunctionId = input.id ?? v4(); const { sourceHandlerPath, builtHandlerPath } = this.helperService.buildHandlerPaths(logicFunctionId); if (input.source) { await this.logicFunctionResourceService.uploadSourceFile({ sourceHandlerPath, sourceHandlerCode: input.source.sourceHandlerCode, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, workspaceId, }); const universalFlatLogicFunctionToCreate = fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate( { createLogicFunctionFromSourceInput: { ...input, id: logicFunctionId, }, sourceHandlerPath, builtHandlerPath, handlerName: input.source.handlerName, checksum: null, isBuildUpToDate: false, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }, ); await this.helperService.createOneFromMetadata({ universalFlatLogicFunctionToCreate, workspaceId, }); return this.findFlatLogicFunctionByIdAndConvertToDto({ id: universalFlatLogicFunctionToCreate.id, workspaceId, }); } const { handlerName, checksum } = await this.logicFunctionResourceService.seedSourceFiles({ sourceHandlerPath, builtHandlerPath, workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }); const universalFlatLogicFunctionToCreate = fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate( { createLogicFunctionFromSourceInput: { ...input, id: logicFunctionId }, sourceHandlerPath, builtHandlerPath, handlerName, checksum, isBuildUpToDate: true, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }, ); await this.helperService.createOneFromMetadata({ universalFlatLogicFunctionToCreate, workspaceId, }); return this.findFlatLogicFunctionByIdAndConvertToDto({ id: universalFlatLogicFunctionToCreate.id, workspaceId, }); } async duplicateOneWithSource({ existingLogicFunctionId, workspaceId, }: { existingLogicFunctionId: string; workspaceId: string; }): Promise<{ id: string }> { const { flatLogicFunction: existingLogicFunction, ownerFlatApplication } = await this.helperService.findLogicFunctionAndApplicationOrThrow({ id: existingLogicFunctionId, workspaceId, }); const newId = v4(); const { sourceHandlerPath, builtHandlerPath } = existingLogicFunction; const toSourceHandlerPath = sourceHandlerPath.replace( existingLogicFunction.id, newId, ); const toBuiltHandlerPath = builtHandlerPath.replace( existingLogicFunction.id, newId, ); await this.logicFunctionResourceService.copyResources({ fromSourceHandlerPath: sourceHandlerPath, toSourceHandlerPath, fromBuiltHandlerPath: builtHandlerPath, toBuiltHandlerPath, workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }); const universalFlatLogicFunctionToCreate = buildUniversalFlatLogicFunctionToCreate({ id: newId, name: existingLogicFunction.name, description: existingLogicFunction.description, timeoutSeconds: existingLogicFunction.timeoutSeconds, isBuildUpToDate: existingLogicFunction.isBuildUpToDate, checksum: existingLogicFunction.checksum, executionMode: LogicFunctionExecutionMode.LIVE, handlerName: existingLogicFunction.handlerName, sourceHandlerPath: toSourceHandlerPath, builtHandlerPath: toBuiltHandlerPath, cronTriggerSettings: existingLogicFunction.cronTriggerSettings, databaseEventTriggerSettings: existingLogicFunction.databaseEventTriggerSettings, httpRouteTriggerSettings: existingLogicFunction.httpRouteTriggerSettings, toolTriggerSettings: existingLogicFunction.toolTriggerSettings, workflowActionTriggerSettings: existingLogicFunction.workflowActionTriggerSettings, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }); const created = await this.helperService.createOneFromMetadata({ universalFlatLogicFunctionToCreate, workspaceId, }); if (!isDefined(created)) { throw new LogicFunctionException( 'Failed to duplicate logic function', LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND, ); } return { id: created.id }; } async updateOneFromSource({ updateLogicFunctionFromSourceInput, workspaceId, }: { updateLogicFunctionFromSourceInput: UpdateLogicFunctionFromSourceInput; workspaceId: string; }): Promise { const { flatLogicFunction, ownerFlatApplication } = await this.helperService.findLogicFunctionAndApplicationOrThrow({ id: updateLogicFunctionFromSourceInput.id, workspaceId, }); const sourceHandlerCode = updateLogicFunctionFromSourceInput.update.sourceHandlerCode; if (sourceHandlerCode) { await this.logicFunctionResourceService.uploadSourceFile({ sourceHandlerPath: flatLogicFunction.sourceHandlerPath, sourceHandlerCode, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, workspaceId, }); } const flatLogicFunctionToUpdate = fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate({ updateLogicFunctionFromSourceInput, existingFlatLogicFunction: flatLogicFunction, }); await this.helperService.updateOneFromMetadata({ flatLogicFunctionToUpdate, workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }); } async deleteOneWithSource({ id, workspaceId, isSystemBuild = false, ownerFlatApplication, }: { id: string; workspaceId: string; isSystemBuild?: boolean; ownerFlatApplication?: FlatApplication; }): Promise { const { flatLogicFunction: existingFlatLogicFunction, ownerFlatApplication: resolvedOwnerFlatApplication, } = await this.helperService.findLogicFunctionAndApplicationOrThrow({ id, workspaceId, }); const effectiveOwnerFlatApplication = ownerFlatApplication ?? resolvedOwnerFlatApplication; const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { logicFunction: { flatEntityToCreate: [], flatEntityToDelete: [existingFlatLogicFunction], flatEntityToUpdate: [], }, }, workspaceId, isSystemBuild, applicationUniversalIdentifier: effectiveOwnerFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying logic function', ); } return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction: existingFlatLogicFunction, }); } async buildOneFromSource({ id, workspaceId, }: { id: string; workspaceId: string; }): Promise { const { flatLogicFunction, ownerFlatApplication } = await this.helperService.findLogicFunctionAndApplicationOrThrow({ id, workspaceId, }); const sourceCode = await this.logicFunctionResourceService.getSourceFile({ workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, sourceHandlerPath: flatLogicFunction.sourceHandlerPath, }); if (!sourceCode) { throw new LogicFunctionException( 'Source file not found', LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND, ); } const { builtCode } = await this.logicFunctionExecutorService.transpile({ sourceCode, sourceFileName: flatLogicFunction.sourceHandlerPath, builtFileName: flatLogicFunction.builtHandlerPath, }); await this.logicFunctionResourceService.uploadBuiltFile({ workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, builtHandlerPath: flatLogicFunction.builtHandlerPath, builtCode, }); const checksum = crypto.createHash('md5').update(builtCode).digest('hex'); await this.helperService.updateOneFromMetadata({ flatLogicFunctionToUpdate: { ...flatLogicFunction, checksum, isBuildUpToDate: true, updatedAt: new Date().toISOString(), }, workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, }); } async executeOneFromSource({ id, payload, workspaceId, }: { id: string; payload: object; workspaceId: string; }): Promise { const { flatLogicFunction } = await this.helperService.findLogicFunctionAndApplicationOrThrow({ id, workspaceId, }); if (!flatLogicFunction.isBuildUpToDate) { await this.buildOneFromSource({ workspaceId, id }); } const result = await this.logicFunctionExecutorService.execute({ logicFunctionId: id, workspaceId, payload, executionMode: LogicFunctionExecutionMode.LIVE, }); return { data: result.data as LogicFunctionExecutionResultDTO['data'], logs: result.logs, duration: result.duration, status: result.status, error: result.error ? { errorType: result.error.errorType, errorMessage: result.error.errorMessage, stackTrace: Array.isArray(result.error.stackTrace) ? result.error.stackTrace.join('\n') : result.error.stackTrace, } : undefined, }; } async getSourceCode({ id, workspaceId, }: { id: string; workspaceId: string; }): Promise { const { flatLogicFunction, ownerFlatApplication } = await this.helperService.findLogicFunctionAndApplicationOrThrow({ id, workspaceId, }); return this.logicFunctionResourceService.getSourceFile({ workspaceId, applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier, sourceHandlerPath: flatLogicFunction.sourceHandlerPath, }); } private async findFlatLogicFunctionByIdAndConvertToDto({ id, workspaceId, }: { id: string; workspaceId: string; }): Promise { const { flatLogicFunctionMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatLogicFunctionMaps'], }, ); return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction: findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId: id, flatEntityMaps: flatLogicFunctionMaps, }), }); } }