import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; import graphqlTypeJson from 'graphql-type-json'; import { PermissionFlagType } from 'twenty-shared/constants'; import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; import { ApplicationService } from 'src/engine/core-modules/application/services/application.service'; 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 { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.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'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ExecuteOneLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/execute-logic-function.input'; import { LogicFunctionExecutionResultDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto'; import { LogicFunctionIdInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-id.input'; import { LogicFunctionLogsDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.dto'; 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 { UpdateLogicFunctionSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-source.input'; import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.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'; import { logicFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils'; import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service'; import { ApplicationLayerService } from 'src/engine/core-modules/application-layer/application-layer.service'; import { CreateDefaultLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-default-logic-function.input'; @UseGuards( WorkspaceAuthGuard, FeatureFlagGuard, SettingsPermissionGuard(PermissionFlagType.WORKFLOWS), ) @Resolver() @UsePipes(ResolverValidationPipe) @UseFilters(PreventNestToAutoLogGraphqlErrorsFilter) export class LogicFunctionResolver { constructor( private readonly logicFunctionExecutorService: LogicFunctionExecutorService, private readonly logicFunctionMetadataService: LogicFunctionMetadataService, private readonly logicFunctionResourceService: LogicFunctionResourceService, private readonly applicationService: ApplicationService, private readonly applicationLayerService: ApplicationLayerService, private readonly subscriptionService: SubscriptionService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => LogicFunctionDTO) async findOneLogicFunction( @Args('input') { id }: LogicFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { try { const { flatLogicFunctionMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatLogicFunctionMaps'], }, ); const flatLogicFunction = findFlatLogicFunctionOrThrow({ id, flatLogicFunctionMaps, }); return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction, }); } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Query(() => [LogicFunctionDTO]) async findManyLogicFunctions( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { try { const { flatLogicFunctionMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatLogicFunctionMaps'], }, ); return Object.values(flatLogicFunctionMaps.byUniversalIdentifier) .filter( (flatLogicFunction): flatLogicFunction is FlatLogicFunction => isDefined(flatLogicFunction) && !isDefined(flatLogicFunction.deletedAt), ) .map((flatLogicFunction) => fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction, }), ); } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Query(() => graphqlTypeJson) async getAvailablePackages( @Args('input') { id }: LogicFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { try { return await this.applicationLayerService.getAvailablePackages({ logicFunctionId: id, workspaceId, }); } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Mutation(() => LogicFunctionDTO) @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS)) async deleteOneLogicFunction( @Args('input') input: LogicFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { try { const flatLogicFunction = await this.logicFunctionMetadataService.destroyOne({ id: input.id, workspaceId, }); return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction, }); } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Mutation(() => LogicFunctionDTO) @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS)) async createDefaultLogicFunction( @Args('input') input: CreateDefaultLogicFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { try { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId, }, ); const logicFunctionId = input.id ?? v4(); const { sourceHandlerPath, builtHandlerPath, handlerName, checksum } = await this.logicFunctionResourceService.seedSourceFiles({ sourceSubfolder: logicFunctionId, workspaceId, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, }); const flatLogicFunction = await this.logicFunctionMetadataService.createOne({ input: { ...input, id: logicFunctionId, sourceHandlerPath, builtHandlerPath, handlerName, checksum, }, workspaceId, ownerFlatApplication: workspaceCustomFlatApplication, }); return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction, }); } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Mutation(() => LogicFunctionExecutionResultDTO) @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS)) async executeOneLogicFunction( @Args('input') { id, payload, forceRebuild }: ExecuteOneLogicFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { // TODO: remove forceRebuild parameter and add a column called shouldRebuild or isBuiltUpToDate try { if (forceRebuild) { const { flatLogicFunction, applicationUniversalIdentifier } = await this.getLogicFunctionContext({ id, workspaceId }); const { checksum } = await this.logicFunctionResourceService.buildFromSource({ workspaceId, applicationUniversalIdentifier, sourceHandlerPath: flatLogicFunction.sourceHandlerPath, builtHandlerPath: flatLogicFunction.builtHandlerPath, }); await this.logicFunctionMetadataService.updateChecksum({ id, checksum, workspaceId, }); } const result = await this.logicFunctionExecutorService.execute({ logicFunctionId: id, workspaceId, payload, }); 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, }; } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Query(() => graphqlTypeJson, { nullable: true }) async getLogicFunctionSourceCode( @Args('input') { id }: LogicFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { try { const { flatLogicFunction, applicationUniversalIdentifier } = await this.getLogicFunctionContext({ id, workspaceId }); return this.logicFunctionResourceService.getSourceCode({ workspaceId, applicationUniversalIdentifier, sourceHandlerPath: flatLogicFunction.sourceHandlerPath, }); } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Mutation(() => Boolean) @UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS)) async updateLogicFunctionSource( @Args('input') { id, code }: UpdateLogicFunctionSourceInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ): Promise { try { const { flatLogicFunction, applicationUniversalIdentifier } = await this.getLogicFunctionContext({ id, workspaceId }); await this.logicFunctionResourceService.updateSourceFiles({ workspaceId, applicationUniversalIdentifier, sourceHandlerPath: flatLogicFunction.sourceHandlerPath, code, }); return true; } catch (error) { return logicFunctionGraphQLApiExceptionHandler(error); } } @Subscription(() => LogicFunctionLogsDTO, { filter: ( payload: { logicFunctionLogs: LogicFunctionLogsDTO }, variables: { input: LogicFunctionLogsInput }, ) => { const { logicFunctionLogs } = payload; const { id, universalIdentifier, applicationId, applicationUniversalIdentifier, name, } = logicFunctionLogs; const { id: inputId, universalIdentifier: inputUniversalIdentifier, name: inputName, applicationId: inputApplicationId, applicationUniversalIdentifier: inputApplicationUniversalIdentifier, } = variables.input; return ( (!isDefined(inputId) || inputId === id) && (!isDefined(inputUniversalIdentifier) || inputUniversalIdentifier === universalIdentifier) && (!isDefined(inputName) || inputName === name) && (!isDefined(inputApplicationId) || inputApplicationId === applicationId) && (!isDefined(inputApplicationUniversalIdentifier) || inputApplicationUniversalIdentifier === applicationUniversalIdentifier) ); }, }) logicFunctionLogs( @Args('input') _: LogicFunctionLogsInput, @AuthWorkspace() workspace: WorkspaceEntity, ) { return this.subscriptionService.subscribe({ channel: SubscriptionChannel.LOGIC_FUNCTION_LOGS_CHANNEL, workspaceId: workspace.id, }); } private async getLogicFunctionContext({ id, workspaceId, }: { id: string; workspaceId: string; }) { const [{ flatLogicFunctionMaps }, { workspaceCustomFlatApplication }] = await Promise.all([ this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({ workspaceId, flatMapsKeys: ['flatLogicFunctionMaps'], }), this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ), ]); const flatLogicFunction = findFlatLogicFunctionOrThrow({ id, flatLogicFunctionMaps, }); return { flatLogicFunction, applicationUniversalIdentifier: workspaceCustomFlatApplication.universalIdentifier, }; } }