400 workflows webhooks trigger (#11041)

https://github.com/user-attachments/assets/dc0ece22-4d87-417f-b9e1-a11c3fd52ce8
This commit is contained in:
martmull
2025-03-20 11:12:52 +01:00
committed by GitHub
parent bc94891a27
commit d99f027e8d
30 changed files with 399 additions and 49 deletions

View File

@ -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) {

View File

@ -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',

View File

@ -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<typeof FieldActorValueSchema>;

View File

@ -14,6 +14,7 @@ import {
IconMicrosoftOutlook,
IconRobot,
IconSettingsAutomation,
IconWebhook,
} from 'twenty-ui';
type ActorDisplayProps = Partial<FieldActorValue> & {
@ -54,6 +55,8 @@ export const ActorDisplay = ({
return IconRobot;
case 'WORKFLOW':
return IconSettingsAutomation;
case 'WEBHOOK':
return IconWebhook;
default:
return undefined;
}

View File

@ -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<HTMLInputElement>) => 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<
</StyledTrailingIcon>
)}
{!error && type !== INPUT_TYPE_PASSWORD && !!RightIcon && (
<StyledTrailingIcon>
<StyledTrailingIcon
onClick={onRightIconClick ? onRightIconClick : undefined}
>
<RightIcon size={theme.icon.size.md} />
</StyledTrailingIcon>
)}

View File

@ -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<typeof workflowManualTriggerSchema>;
export type WorkflowCronTrigger = z.infer<typeof workflowCronTriggerSchema>;
export type WorkflowWebhookTrigger = z.infer<
typeof workflowWebhookTriggerSchema
>;
export type WorkflowManualTriggerSettings = WorkflowManualTrigger['settings'];
export type WorkflowManualTriggerAvailability =

View File

@ -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

View File

@ -27,27 +27,10 @@ export const WorkflowDiagramStepNodeIcon = ({
switch (data.nodeType) {
case 'trigger': {
switch (data.triggerType) {
case 'DATABASE_EVENT': {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'MANUAL': {
return (
<StyledStepNodeLabelIconContainer>
<Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</StyledStepNodeLabelIconContainer>
);
}
case 'CRON': {
case 'DATABASE_EVENT':
case 'MANUAL':
case 'CRON':
case 'WEBHOOK': {
return (
<StyledStepNodeLabelIconContainer>
<Icon

View File

@ -33,6 +33,14 @@ export const getWorkflowDiagramTriggerNode = ({
break;
}
case 'WEBHOOK': {
triggerDefaultLabel = 'Webhook';
triggerIcon = getTriggerIcon({
type: 'WEBHOOK',
});
break;
}
case 'DATABASE_EVENT': {
const triggerEvent = splitWorkflowTriggerEventName(
trigger.settings.eventName,

View File

@ -13,6 +13,7 @@ import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/compo
import { Suspense, lazy } from 'react';
import { isDefined } from 'twenty-shared';
import { RightDrawerSkeletonLoader } from '~/loading/components/RightDrawerSkeletonLoader';
import { WorkflowEditTriggerWebhookForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm';
const WorkflowEditActionServerlessFunction = lazy(() =>
import(
@ -85,6 +86,15 @@ export const WorkflowStepDetail = ({
/>
);
}
case 'WEBHOOK': {
return (
<WorkflowEditTriggerWebhookForm
key={stepId}
trigger={stepDefinition.definition}
triggerOptions={props}
/>
);
}
case 'CRON': {
return (
<WorkflowEditTriggerCronForm

View File

@ -0,0 +1,99 @@
import { WorkflowWebhookTrigger } from '@/workflow/types/Workflow';
import { useTheme } from '@emotion/react';
import { useIcons, IconCopy } from 'twenty-ui';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { isDefined } from 'twenty-shared';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useDebouncedCallback } from 'use-debounce';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { workflowIdState } from '@/workflow/states/workflowIdState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
type WorkflowEditTriggerWebhookFormProps = {
trigger: WorkflowWebhookTrigger;
triggerOptions:
| {
readonly: true;
onTriggerUpdate?: undefined;
}
| {
readonly?: false;
onTriggerUpdate: (trigger: WorkflowWebhookTrigger) => 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: <IconCopy size={theme.icon.size.md} />,
});
};
const copyToClipboardDebounced = useDebouncedCallback(copyToClipboard, 200);
if (!isDefined(currentWorkspace)) {
return <></>;
}
return (
<>
<WorkflowStepHeader
onTitleChange={(newName: string) => {
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}
/>
<WorkflowStepBody>
<TextInputV2
label="Live URL"
value={displayWebhookUrl}
RightIcon={() => (
<IconCopy
size={theme.icon.size.md}
color={theme.font.color.secondary}
/>
)}
onRightIconClick={copyToClipboardDebounced}
readOnly
/>
</WorkflowStepBody>
</>
);
};

View File

@ -15,4 +15,9 @@ export const OTHER_TRIGGER_TYPES: Array<{
type: 'CRON',
icon: 'IconClock',
},
{
defaultLabel: 'Webhook',
type: 'WEBHOOK',
icon: 'IconWebhook',
},
];

View File

@ -58,6 +58,15 @@ export const getTriggerDefaultDefinition = ({
},
};
}
case 'WEBHOOK': {
return {
type,
name: defaultLabel,
settings: {
outputSchema: {},
},
};
}
default: {
return assertUnreachable(type, `Unknown type: ${type}`);
}

View File

@ -9,6 +9,9 @@ export const getTriggerIcon = (
| {
type: 'CRON';
}
| {
type: 'WEBHOOK';
}
| {
type: 'DATABASE_EVENT';
eventName: string;

View File

@ -9,6 +9,9 @@ export const getTriggerDefaultLabel = (
| {
type: 'CRON';
}
| {
type: 'WEBHOOK';
}
| {
type: 'DATABASE_EVENT';
eventName: string;

View File

@ -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';

View File

@ -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: {

View File

@ -211,6 +211,7 @@ const getSchemaComponentsProperties = ({
'IMPORT',
'MANUAL',
'SYSTEM',
'WEBHOOK',
],
},
...(forResponse

View File

@ -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<WorkflowWorkspaceEntity>(
'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<WorkflowVersionWorkspaceEntity>(
'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,
};
}
}

View File

@ -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);
}

View File

@ -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<Response>();
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,
);
}
}
}

View File

@ -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,
}),
});
}
}

View File

@ -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,

View File

@ -13,6 +13,7 @@ export enum FieldActorSource {
IMPORT = 'IMPORT',
MANUAL = 'MANUAL',
SYSTEM = 'SYSTEM',
WEBHOOK = 'WEBHOOK',
}
export const actorCompositeType: CompositeType = {

View File

@ -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'];

View File

@ -59,6 +59,7 @@ export class WorkflowSchemaWorkspaceService {
objectMetadataRepository: this.objectMetadataRepository,
});
}
case WorkflowTriggerType.WEBHOOK:
case WorkflowTriggerType.CRON: {
return {};
}

View File

@ -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',
}

View File

@ -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;

View File

@ -69,6 +69,7 @@ function assertTriggerSettingsAreValid(
assertDatabaseEventTriggerSettingsAreValid(settings);
break;
case WorkflowTriggerType.MANUAL:
case WorkflowTriggerType.WEBHOOK:
break;
case WorkflowTriggerType.CRON:
assertCronTriggerSettingsAreValid(settings);

View File

@ -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({