diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction.ts index 662edbf06..766a68d27 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction.ts @@ -14,10 +14,11 @@ export const useTestWorkflowSingleRecordAction: ActionHookWithoutObjectMetadataI const shouldBeRegistered = isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) && - workflowWithCurrentVersion.currentVersion.trigger.type === 'MANUAL' && - !isDefined( - workflowWithCurrentVersion.currentVersion.trigger.settings.objectType, - ); + ((workflowWithCurrentVersion.currentVersion.trigger.type === 'MANUAL' && + !isDefined( + workflowWithCurrentVersion.currentVersion.trigger.settings.objectType, + )) || + workflowWithCurrentVersion.currentVersion.trigger.type === 'WEBHOOK'); const onClick = () => { if (!shouldBeRegistered) { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts index b116a6fdb..73a6ebf0b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts @@ -7,6 +7,7 @@ import { IconRobot, IconSettingsAutomation, IconUserCircle, + IconWebhook, } from 'twenty-ui'; export const getActorSourceMultiSelectOptions = ( @@ -53,6 +54,13 @@ export const getActorSourceMultiSelectOptions = ( AvatarIcon: IconSettingsAutomation, isIconInverted: true, }, + { + id: 'WEBHOOK', + name: 'Webhook', + isSelected: selectedSourceNames.includes('WEBHOOK'), + AvatarIcon: IconWebhook, + isIconInverted: true, + }, { id: 'SYSTEM', name: 'System', diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 7aca41823..75a139933 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -286,15 +286,18 @@ const FieldActorSourceSchema = z.union([ z.literal('MANUAL'), z.literal('SYSTEM'), z.literal('WORKFLOW'), + z.literal('WEBHOOK'), ]); export const FieldActorValueSchema = z.object({ source: FieldActorSourceSchema, workspaceMemberId: z.string().nullable(), name: z.string(), - context: z.object({ - provider: z.nativeEnum(ConnectedAccountProvider).optional(), - }), + context: z + .object({ + provider: z.nativeEnum(ConnectedAccountProvider).optional(), + }) + .nullable(), }); export type FieldActorValue = z.infer; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx index 88cbe2fc1..52b556908 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx @@ -14,6 +14,7 @@ import { IconMicrosoftOutlook, IconRobot, IconSettingsAutomation, + IconWebhook, } from 'twenty-ui'; type ActorDisplayProps = Partial & { @@ -54,6 +55,8 @@ export const ActorDisplay = ({ return IconRobot; case 'WORKFLOW': return IconSettingsAutomation; + case 'WEBHOOK': + return IconWebhook; default: return undefined; } diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx index c1e74c582..66c08b313 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextInputV2.tsx @@ -65,6 +65,7 @@ const StyledInput = styled.input< Pick< TextInputV2ComponentProps, | 'LeftIcon' + | 'RightIcon' | 'error' | 'sizeVariant' | 'width' @@ -112,9 +113,16 @@ const StyledInput = styled.input< : LeftIcon ? `calc(${theme.spacing(3)} + 16px)` : theme.spacing(2)}; + padding-right: ${({ theme, RightIcon, autoGrow }) => + autoGrow + ? theme.spacing(1) + : RightIcon + ? `calc(${theme.spacing(3)} + 16px)` + : theme.spacing(2)}; width: ${({ theme, width }) => width ? `calc(${width}px + ${theme.spacing(0.5)})` : '100%'}; max-width: ${({ autoGrow }) => (autoGrow ? '100%' : 'none')}; + text-overflow: ellipsis; &::placeholder, &::-webkit-input-placeholder { color: ${({ theme }) => theme.font.color.light}; @@ -126,6 +134,10 @@ const StyledInput = styled.input< color: ${({ theme }) => theme.font.color.tertiary}; } + &[readonly] { + pointer-events: none; + } + &:focus { ${({ theme, error }) => { return ` @@ -165,7 +177,10 @@ const StyledTrailingIconContainer = styled.div< margin: auto 0; `; -const StyledTrailingIcon = styled.div<{ isFocused?: boolean }>` +const StyledTrailingIcon = styled.div<{ + isFocused?: boolean; + onClick?: () => void; +}>` align-items: center; color: ${({ theme, isFocused }) => isFocused ? theme.font.color.secondary : theme.font.color.light}; @@ -189,6 +204,7 @@ export type TextInputV2ComponentProps = Omit< error?: string; noErrorHelper?: boolean; RightIcon?: IconComponent; + onRightIconClick?: () => void; LeftIcon?: IconComponent; autoGrow?: boolean; onKeyDown?: (event: React.KeyboardEvent) => void; @@ -224,8 +240,10 @@ const TextInputV2Component = forwardRef< autoFocus, placeholder, disabled, + readOnly, tabIndex, RightIcon, + onRightIconClick, LeftIcon, autoComplete, maxLength, @@ -302,10 +320,12 @@ const TextInputV2Component = forwardRef< {...{ autoFocus, disabled, + readOnly, placeholder, required, value, LeftIcon, + RightIcon, maxLength, error, sizeVariant, @@ -337,7 +357,9 @@ const TextInputV2Component = forwardRef< )} {!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && ( - + )} diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index f708ddfed..f8d81b898 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -23,6 +23,7 @@ import { workflowTriggerSchema, workflowUpdateRecordActionSchema, workflowUpdateRecordActionSettingsSchema, + workflowWebhookTriggerSchema, } from '@/workflow/validation-schemas/workflowSchema'; import { z } from 'zod'; @@ -76,6 +77,9 @@ export type WorkflowDatabaseEventTrigger = z.infer< >; export type WorkflowManualTrigger = z.infer; export type WorkflowCronTrigger = z.infer; +export type WorkflowWebhookTrigger = z.infer< + typeof workflowWebhookTriggerSchema +>; export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings']; export type WorkflowManualTriggerAvailability = diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts index 6824d9718..fac611791 100644 --- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts +++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts @@ -203,11 +203,19 @@ export const workflowCronTriggerSchema = baseTriggerSchema.extend({ ]), }); +export const workflowWebhookTriggerSchema = baseTriggerSchema.extend({ + type: z.literal('WEBHOOK'), + settings: z.object({ + outputSchema: z.object({}).passthrough(), + }), +}); + // Combined trigger schema export const workflowTriggerSchema = z.discriminatedUnion('type', [ workflowDatabaseEventTriggerSchema, workflowManualTriggerSchema, workflowCronTriggerSchema, + workflowWebhookTriggerSchema, ]); // Step output schemas diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx index bbdd9313b..913d781b0 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowDiagramStepNodeIcon.tsx @@ -27,27 +27,10 @@ export const WorkflowDiagramStepNodeIcon = ({ switch (data.nodeType) { case 'trigger': { switch (data.triggerType) { - case 'DATABASE_EVENT': { - return ( - - - - ); - } - case 'MANUAL': { - return ( - - - - ); - } - case 'CRON': { + case 'DATABASE_EVENT': + case 'MANUAL': + case 'CRON': + case 'WEBHOOK': { return ( import( @@ -85,6 +86,15 @@ export const WorkflowStepDetail = ({ /> ); } + case 'WEBHOOK': { + return ( + + ); + } case 'CRON': { return ( void; + }; +}; +export const WorkflowEditTriggerWebhookForm = ({ + trigger, + triggerOptions, +}: WorkflowEditTriggerWebhookFormProps) => { + const { enqueueSnackBar } = useSnackBar(); + const theme = useTheme(); + const { t } = useLingui(); + const { getIcon } = useIcons(); + const workflowId = useRecoilValue(workflowIdState); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const headerTitle = isDefined(trigger.name) ? trigger.name : 'Webhook'; + + const headerIcon = getTriggerIcon({ + type: 'WEBHOOK', + }); + + const webhookUrl = `${REACT_APP_SERVER_BASE_URL}/webhooks/workflows/${currentWorkspace?.id}/${workflowId}`; + const displayWebhookUrl = webhookUrl.replace(/^(https?:\/\/)?(www\.)?/, ''); + + const copyToClipboard = async () => { + await navigator.clipboard.writeText(webhookUrl); + enqueueSnackBar(t`Copied to clipboard!`, { + variant: SnackBarVariant.Success, + icon: , + }); + }; + + const copyToClipboardDebounced = useDebouncedCallback(copyToClipboard, 200); + + if (!isDefined(currentWorkspace)) { + return <>; + } + + return ( + <> + { + if (triggerOptions.readonly === true) { + return; + } + + triggerOptions.onTriggerUpdate({ + ...trigger, + name: newName, + }); + }} + Icon={getIcon(headerIcon)} + iconColor={theme.font.color.tertiary} + initialTitle={headerTitle} + headerType="Trigger ยท Webhook" + disabled={triggerOptions.readonly} + /> + + ( + + )} + onRightIconClick={copyToClipboardDebounced} + readOnly + /> + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts index 87786e2e1..21d3518ef 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/constants/OtherTriggerTypes.ts @@ -15,4 +15,9 @@ export const OTHER_TRIGGER_TYPES: Array<{ type: 'CRON', icon: 'IconClock', }, + { + defaultLabel: 'Webhook', + type: 'WEBHOOK', + icon: 'IconWebhook', + }, ]; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts index be2987fb7..128281612 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerDefaultDefinition.ts @@ -58,6 +58,15 @@ export const getTriggerDefaultDefinition = ({ }, }; } + case 'WEBHOOK': { + return { + type, + name: defaultLabel, + settings: { + outputSchema: {}, + }, + }; + } default: { return assertUnreachable(type, `Unknown type: ${type}`); } diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts index d508b600e..1e0d1496c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerIcon.ts @@ -9,6 +9,9 @@ export const getTriggerIcon = ( | { type: 'CRON'; } + | { + type: 'WEBHOOK'; + } | { type: 'DATABASE_EVENT'; eventName: string; diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts index 51eae2fab..2f0f08ea9 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/utils/getTriggerLabel.ts @@ -9,6 +9,9 @@ export const getTriggerDefaultLabel = ( | { type: 'CRON'; } + | { + type: 'WEBHOOK'; + } | { type: 'DATABASE_EVENT'; eventName: string; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts index e1769b370..1a5ad73f4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/getTriggerStepName.ts @@ -12,6 +12,8 @@ export const getTriggerStepName = (trigger: WorkflowTrigger): string => { return getDatabaseEventTriggerStepName(trigger); case 'CRON': return 'On a Schedule'; + case 'WEBHOOK': + return 'Webhook'; case 'MANUAL': if (!isDefined(trigger.settings.objectType)) { return 'Manual trigger'; diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index 75ac244b8..e98e33c68 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -190,6 +190,7 @@ describe('computeSchemaComponents', () => { 'IMPORT', 'MANUAL', 'SYSTEM', + 'WEBHOOK', ], }, }, @@ -378,6 +379,7 @@ describe('computeSchemaComponents', () => { 'IMPORT', 'MANUAL', 'SYSTEM', + 'WEBHOOK', ], }, }, @@ -565,6 +567,7 @@ describe('computeSchemaComponents', () => { 'IMPORT', 'MANUAL', 'SYSTEM', + 'WEBHOOK', ], }, workspaceMemberId: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index f32903d72..a465de850 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -211,6 +211,7 @@ const getSchemaComponentsProperties = ({ 'IMPORT', 'MANUAL', 'SYSTEM', + 'WEBHOOK', ], }, ...(forResponse diff --git a/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts new file mode 100644 index 000000000..522b37c35 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/controllers/workflow-trigger.controller.ts @@ -0,0 +1,96 @@ +import { Controller, Get, Param, UseFilters } from '@nestjs/common'; + +import { isDefined } from 'twenty-shared'; + +import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; +import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter'; +import { + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; +import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; +import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; + +@Controller('webhooks') +@UseFilters(WorkflowTriggerRestApiExceptionFilter) +export class WorkflowTriggerController { + constructor( + private readonly twentyORMManager: TwentyORMManager, + private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService, + ) {} + + @Get('workflows/:workspaceId/:workflowId') + async runWorkflow( + @Param('workspaceId') workspaceId: string, + @Param('workflowId') workflowId: string, + ) { + const workflowRepository = + await this.twentyORMManager.getRepository( + 'workflow', + ); + + const workflow = await workflowRepository.findOne({ + where: { id: workflowId }, + }); + + if (!isDefined(workflow)) { + throw new WorkflowTriggerException( + 'Workflow not found', + WorkflowTriggerExceptionCode.NOT_FOUND, + ); + } + + if ( + !isDefined(workflow.lastPublishedVersionId) || + workflow.lastPublishedVersionId === '' + ) { + throw new WorkflowTriggerException( + 'Workflow not activated', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS, + ); + } + + const workflowVersionRepository = + await this.twentyORMManager.getRepository( + 'workflowVersion', + ); + const workflowVersion = await workflowVersionRepository.findOne({ + where: { id: workflow.lastPublishedVersionId }, + }); + + if (!isDefined(workflowVersion)) { + throw new WorkflowTriggerException( + 'Workflow version not found', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION, + ); + } + + if (workflowVersion.trigger?.type !== WorkflowTriggerType.WEBHOOK) { + throw new WorkflowTriggerException( + 'Workflow does not have a Webhook trigger', + WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER, + ); + } + + const { workflowRunId } = + await this.workflowTriggerWorkspaceService.runWorkflowVersion({ + workflowVersionId: workflow.lastPublishedVersionId, + payload: {}, + createdBy: { + source: FieldActorSource.WEBHOOK, + workspaceMemberId: null, + name: 'Webhook', + context: {}, + }, + }); + + return { + workflowName: workflow.name, + success: true, + workflowRunId, + }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts index a274ccd43..df46ad401 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter.ts @@ -2,6 +2,7 @@ import { Catch, ExceptionFilter } from '@nestjs/common'; import { InternalServerError, + NotFoundError, UserInputError, } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { @@ -19,8 +20,11 @@ export class WorkflowTriggerGraphqlApiExceptionFilter case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE: case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS: case WorkflowTriggerExceptionCode.FORBIDDEN: throw new UserInputError(exception.message); + case WorkflowTriggerExceptionCode.NOT_FOUND: + throw new NotFoundError(exception.message); default: throw new InternalServerError(exception.message); } diff --git a/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter.ts new file mode 100644 index 000000000..f7bf82832 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter.ts @@ -0,0 +1,54 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; + +import { Response } from 'express'; + +import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; +import { CustomException } from 'src/utils/custom-exception'; +import { + WorkflowTriggerException, + WorkflowTriggerExceptionCode, +} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception'; + +@Catch() +export class WorkflowTriggerRestApiExceptionFilter implements ExceptionFilter { + constructor( + private readonly httpExceptionHandlerService: HttpExceptionHandlerService, + ) {} + + catch(exception: WorkflowTriggerException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + switch (exception.code) { + case WorkflowTriggerExceptionCode.INVALID_INPUT: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_TRIGGER: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_VERSION: + case WorkflowTriggerExceptionCode.INVALID_ACTION_TYPE: + case WorkflowTriggerExceptionCode.INVALID_WORKFLOW_STATUS: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 400, + ); + case WorkflowTriggerExceptionCode.FORBIDDEN: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 403, + ); + case WorkflowTriggerExceptionCode.NOT_FOUND: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 404, + ); + case WorkflowTriggerExceptionCode.INTERNAL_ERROR: + default: + return this.httpExceptionHandlerService.handleError( + exception as CustomException, + response, + 500, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts index 70cedd3f5..50bf9010d 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts @@ -10,6 +10,7 @@ import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service'; +import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; @Resolver() @UseGuards(WorkspaceAuthGuard, UserAuthGuard) @@ -43,11 +44,16 @@ export class WorkflowTriggerResolver { @AuthUser() user: User, @Args('input') { workflowVersionId, payload }: RunWorkflowVersionInput, ) { - return await this.workflowTriggerWorkspaceService.runWorkflowVersion( + return await this.workflowTriggerWorkspaceService.runWorkflowVersion({ workflowVersionId, - payload ?? {}, - workspaceMemberId, - user, - ); + payload: payload ?? {}, + createdBy: buildCreatedByFromFullNameMetadata({ + fullNameMetadata: { + firstName: user.firstName, + lastName: user.lastName, + }, + workspaceMemberId: workspaceMemberId, + }), + }); } } diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts index 26c35e74a..0c4d57021 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts @@ -8,6 +8,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module'; import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module'; import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module'; +import { WorkflowTriggerController } from 'src/engine/core-modules/workflow/controllers/workflow-trigger.controller'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/wor WorkflowCommonModule, WorkflowVersionModule, ], + controllers: [WorkflowTriggerController], providers: [ WorkflowTriggerResolver, WorkflowBuilderResolver, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts index e69e81ad9..0944c7db6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts @@ -13,6 +13,7 @@ export enum FieldActorSource { IMPORT = 'IMPORT', MANUAL = 'MANUAL', SYSTEM = 'SYSTEM', + WEBHOOK = 'WEBHOOK', } export const actorCompositeType: CompositeType = { diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts index 44b6cd1ed..3a638665f 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/scoped-workspace-context.factory.ts @@ -14,7 +14,8 @@ export class ScopedWorkspaceContextFactory { workspaceMetadataVersion: number | null; } { const workspaceId: string | undefined = - this.request?.['req']?.['workspaceId']; + this.request?.['req']?.['workspaceId'] || + this.request?.['params']?.['workspaceId']; const workspaceMetadataVersion: number | undefined = this.request?.['req']?.['workspaceMetadataVersion']; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 00ae360f5..d77a4951f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -59,6 +59,7 @@ export class WorkflowSchemaWorkspaceService { objectMetadataRepository: this.objectMetadataRepository, }); } + case WorkflowTriggerType.WEBHOOK: case WorkflowTriggerType.CRON: { return {}; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts index 044a0c561..0d9f2dc21 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception.ts @@ -10,7 +10,9 @@ export enum WorkflowTriggerExceptionCode { INVALID_INPUT = 'INVALID_INPUT', INVALID_WORKFLOW_TRIGGER = 'INVALID_WORKFLOW_TRIGGER', INVALID_WORKFLOW_VERSION = 'INVALID_WORKFLOW_VERSION', + INVALID_WORKFLOW_STATUS = 'INVALID_WORKFLOW_STATUS', INVALID_ACTION_TYPE = 'INVALID_ACTION_TYPE', + NOT_FOUND = 'NOT_FOUND', FORBIDDEN = 'FORBIDDEN', INTERNAL_ERROR = 'INTERNAL_ERROR', } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts index 10764bdce..3a7a2dd9d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/types/workflow-trigger.type.ts @@ -4,6 +4,7 @@ export enum WorkflowTriggerType { DATABASE_EVENT = 'DATABASE_EVENT', MANUAL = 'MANUAL', CRON = 'CRON', + WEBHOOK = 'WEBHOOK', } type BaseWorkflowTriggerSettings = { @@ -58,9 +59,14 @@ export type WorkflowCronTrigger = BaseTrigger & { ) & { outputSchema: object }; }; +export type WorkflowWebhookTrigger = BaseTrigger & { + type: WorkflowTriggerType.WEBHOOK; +}; + export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings']; export type WorkflowTrigger = | WorkflowDatabaseEventTrigger | WorkflowManualTrigger - | WorkflowCronTrigger; + | WorkflowCronTrigger + | WorkflowWebhookTrigger; diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts index 1c51b7a4d..2b85f87a6 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util.ts @@ -69,6 +69,7 @@ function assertTriggerSettingsAreValid( assertDatabaseEventTriggerSettingsAreValid(settings); break; case WorkflowTriggerType.MANUAL: + case WorkflowTriggerType.WEBHOOK: break; case WorkflowTriggerType.CRON: assertCronTriggerSettingsAreValid(settings); diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts index 1298958aa..99e2cb4da 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service.ts @@ -4,11 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import { EntityManager, Repository } from 'typeorm'; import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { User } from 'src/engine/core-modules/user/user.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -37,6 +35,7 @@ import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types import { assertVersionCanBeActivated } from 'src/modules/workflow/workflow-trigger/utils/assert-version-can-be-activated.util'; import { computeCronPatternFromSchedule } from 'src/modules/workflow/workflow-trigger/utils/compute-cron-pattern-from-schedule'; import { assertNever } from 'src/utils/assert'; +import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; @Injectable() export class WorkflowTriggerWorkspaceService { @@ -66,12 +65,15 @@ export class WorkflowTriggerWorkspaceService { return workspaceId; } - async runWorkflowVersion( - workflowVersionId: string, - payload: object, - workspaceMemberId: string, - { firstName, lastName }: User, - ) { + async runWorkflowVersion({ + workflowVersionId, + payload, + createdBy, + }: { + workflowVersionId: string; + payload: object; + createdBy: ActorMetadata; + }) { await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail( workflowVersionId, ); @@ -80,10 +82,7 @@ export class WorkflowTriggerWorkspaceService { this.getWorkspaceId(), workflowVersionId, payload, - buildCreatedByFromFullNameMetadata({ - fullNameMetadata: { firstName, lastName }, - workspaceMemberId, - }), + createdBy, ); } @@ -342,6 +341,7 @@ export class WorkflowTriggerWorkspaceService { return; case WorkflowTriggerType.MANUAL: + case WorkflowTriggerType.WEBHOOK: return; case WorkflowTriggerType.CRON: { const pattern = computeCronPatternFromSchedule(workflowVersion.trigger); @@ -384,6 +384,7 @@ export class WorkflowTriggerWorkspaceService { return; case WorkflowTriggerType.MANUAL: + case WorkflowTriggerType.WEBHOOK: return; case WorkflowTriggerType.CRON: await this.messageQueueService.removeCron({