diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index c13fca662..80e56c4f5 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -223,4 +223,4 @@ jobs: uses: ./.github/workflows/actions/nx-affected with: tag: scope:frontend - tasks: ${{ matrix.task }} \ No newline at end of file + tasks: ${{ matrix.task }} diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 829147a72..5db253223 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -162,6 +162,11 @@ export type ClientConfig = { support: Support; }; +export type ComputeStepOutputSchemaInput = { + /** Step JSON format */ + step: Scalars['JSON']['input']; +}; + export type CreateAppTokenInput = { expiresAt: Scalars['DateTime']['input']; }; @@ -529,6 +534,7 @@ export type Mutation = { authorizeApp: AuthorizeApp; challenge: LoginToken; checkoutSession: SessionEntity; + computeStepOutputSchema: Scalars['JSON']['output']; createOIDCIdentityProvider: SetupSsoOutput; createOneAppToken: AppToken; createOneField: Field; @@ -625,6 +631,11 @@ export type MutationCheckoutSessionArgs = { }; +export type MutationComputeStepOutputSchemaArgs = { + input: ComputeStepOutputSchemaInput; +}; + + export type MutationCreateOidcIdentityProviderArgs = { input: SetupOidcSsoInput; }; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 13ab80f01..d18e39ba1 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -155,6 +155,11 @@ export type ClientConfig = { support: Support; }; +export type ComputeStepOutputSchemaInput = { + /** Step JSON format */ + step: Scalars['JSON']; +}; + export type CreateServerlessFunctionInput = { description?: InputMaybe; name: Scalars['String']; @@ -424,6 +429,7 @@ export type Mutation = { authorizeApp: AuthorizeApp; challenge: LoginToken; checkoutSession: SessionEntity; + computeStepOutputSchema: Scalars['JSON']; createOIDCIdentityProvider: SetupSsoOutput; createOneAppToken: AppToken; createOneObject: Object; @@ -509,6 +515,11 @@ export type MutationCheckoutSessionArgs = { }; +export type MutationComputeStepOutputSchemaArgs = { + input: ComputeStepOutputSchemaInput; +}; + + export type MutationCreateOidcIdentityProviderArgs = { input: SetupOidcSsoInput; }; @@ -1823,6 +1834,13 @@ export type ActivateWorkflowVersionMutationVariables = Exact<{ export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean }; +export type ComputeStepOutputSchemaMutationVariables = Exact<{ + input: ComputeStepOutputSchemaInput; +}>; + + +export type ComputeStepOutputSchemaMutation = { __typename?: 'Mutation', computeStepOutputSchema: any }; + export type DeactivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; }>; @@ -3443,6 +3461,37 @@ export function useActivateWorkflowVersionMutation(baseOptions?: Apollo.Mutation export type ActivateWorkflowVersionMutationHookResult = ReturnType; export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult; export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions; +export const ComputeStepOutputSchemaDocument = gql` + mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) { + computeStepOutputSchema(input: $input) +} + `; +export type ComputeStepOutputSchemaMutationFn = Apollo.MutationFunction; + +/** + * __useComputeStepOutputSchemaMutation__ + * + * To run a mutation, you first call `useComputeStepOutputSchemaMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useComputeStepOutputSchemaMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [computeStepOutputSchemaMutation, { data, loading, error }] = useComputeStepOutputSchemaMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useComputeStepOutputSchemaMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ComputeStepOutputSchemaDocument, options); + } +export type ComputeStepOutputSchemaMutationHookResult = ReturnType; +export type ComputeStepOutputSchemaMutationResult = Apollo.MutationResult; +export type ComputeStepOutputSchemaMutationOptions = Apollo.BaseMutationOptions; export const DeactivateWorkflowVersionDocument = gql` mutation DeactivateWorkflowVersion($workflowVersionId: String!) { deactivateWorkflowVersion(workflowVersionId: $workflowVersionId) diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts index fa2bc2507..2199b3a10 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/findAvailableTimeZoneOption.test.ts @@ -3,13 +3,18 @@ import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableT describe('findAvailableTimeZoneOption', () => { it('should find the matching available IANA time zone select option from a given IANA time zone', () => { const ianaTimeZone = 'Europe/Paris'; - const expectedOption = { - label: '(GMT+02:00) Central European Summer Time - Paris', - value: 'Europe/Paris', - }; + const expectedValue = 'Europe/Paris'; + const expectedLabelWinter = + '(GMT+01:00) Central European Standard Time - Paris'; + const expectedLabelSummer = + '(GMT+02:00) Central European Summer Time - Paris'; const option = findAvailableTimeZoneOption(ianaTimeZone); - expect(option).toEqual(expectedOption); + expect(option.value).toEqual(expectedValue); + expect( + expectedLabelWinter === option.label || + expectedLabelSummer === option.label, + ).toBeTruthy(); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts index bec60cc4e..64d53d898 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatTimeZoneLabel.test.ts @@ -3,11 +3,17 @@ import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel'; describe('formatTimeZoneLabel', () => { it('should format the time zone label correctly when location is included in the label', () => { const ianaTimeZone = 'Europe/Paris'; - const expectedLabel = '(GMT+02:00) Central European Summer Time - Paris'; + const expectedLabelSummer = + '(GMT+02:00) Central European Summer Time - Paris'; + const expectedLabelWinter = + '(GMT+01:00) Central European Standard Time - Paris'; const formattedLabel = formatTimeZoneLabel(ianaTimeZone); - expect(formattedLabel).toEqual(expectedLabel); + expect( + expectedLabelSummer === formattedLabel || + expectedLabelWinter === formattedLabel, + ).toBeTruthy(); }); it('should format the time zone label correctly when location is not included in the label', () => { diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx index 0a45bd313..09cdb5458 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx @@ -48,7 +48,7 @@ export const WorkflowEditActionFormServerlessFunction = ( value={props.action.settings.input.serverlessFunctionId} options={availableFunctions} disabled={props.readonly} - onChange={(updatedFunction) => { + onChange={(serverlessFunctionId) => { if (props.readonly === true) { return; } @@ -58,7 +58,10 @@ export const WorkflowEditActionFormServerlessFunction = ( settings: { ...props.action.settings, input: { - serverlessFunctionId: updatedFunction, + serverlessFunctionId, + serverlessFunctionVersion: + serverlessFunctions.find((f) => f.id === serverlessFunctionId) + ?.latestVersion || 'latest', }, }, }); diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx index db45bae2f..7475810ce 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerDatabaseEventForm.tsx @@ -134,6 +134,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ type: 'DATABASE_EVENT', settings: { eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`, + outputSchema: {}, }, }, ); @@ -165,6 +166,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ type: 'DATABASE_EVENT', settings: { eventName: `${availableMetadata[0].value}.${updatedEvent}`, + outputSchema: {}, }, }, ); diff --git a/packages/twenty-front/src/modules/workflow/graphql/activateWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/activateWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/graphql/activateWorkflowVersion.ts rename to packages/twenty-front/src/modules/workflow/graphql/mutations/activateWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/graphql/mutations/computeStepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/computeStepOutputSchema.ts new file mode 100644 index 000000000..0b185c730 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/graphql/mutations/computeStepOutputSchema.ts @@ -0,0 +1,7 @@ +import { gql } from '@apollo/client'; + +export const COMPUTE_STEP_OUTPUT_SCHEMA = gql` + mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) { + computeStepOutputSchema(input: $input) + } +`; diff --git a/packages/twenty-front/src/modules/workflow/graphql/deactivateWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/graphql/mutations/deactivateWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/graphql/deactivateWorkflowVersion.ts rename to packages/twenty-front/src/modules/workflow/graphql/mutations/deactivateWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts index 23bdf15a4..4f6395f32 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts @@ -4,7 +4,7 @@ import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; -import { ACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/activateWorkflowVersion'; +import { ACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/activateWorkflowVersion'; import { WorkflowVersion } from '@/workflow/types/Workflow'; import { ActivateWorkflowVersionMutation, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useAvailableVariablesInWorkflowStep.ts b/packages/twenty-front/src/modules/workflow/hooks/useAvailableVariablesInWorkflowStep.ts new file mode 100644 index 000000000..88e0a05c0 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useAvailableVariablesInWorkflowStep.ts @@ -0,0 +1,67 @@ +import { capitalize } from '~/utils/string/capitalize'; +import { useRecoilValue } from 'recoil'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; +import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; +import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow'; +import { isDefined } from 'twenty-ui'; +import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema'; + +export const useAvailableVariablesInWorkflowStep = (): StepOutputSchema[] => { + const workflowId = useRecoilValue(workflowIdState); + const workflow = useWorkflowWithCurrentVersion(workflowId); + const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState); + + if (!isDefined(workflowSelectedNode) || !isDefined(workflow)) { + return []; + } + + const stepDefinition = getStepDefinitionOrThrow({ + stepId: workflowSelectedNode, + workflowVersion: workflow.currentVersion, + }); + + if ( + !isDefined(stepDefinition) || + stepDefinition.type === 'trigger' || + !isDefined(workflow.currentVersion.steps) + ) { + return []; + } + + const previousSteps = []; + + for (const step of workflow.currentVersion.steps) { + if (step.id === workflowSelectedNode) { + break; + } + previousSteps.push(step); + } + + const result = []; + + if ( + workflow.currentVersion.trigger?.type === 'DATABASE_EVENT' && + isDefined(workflow.currentVersion.trigger?.settings?.outputSchema) + ) { + const [object, action] = + workflow.currentVersion.trigger.settings.eventName.split('.'); + result.push({ + id: 'trigger', + name: `${capitalize(object)} is ${capitalize(action)}`, + outputSchema: workflow.currentVersion.trigger.settings.outputSchema, + }); + } + + previousSteps.forEach((previousStep) => { + if (isDefined(previousStep.settings.outputSchema)) { + result.push({ + id: previousStep.id, + name: previousStep.name, + outputSchema: previousStep.settings.outputSchema, + }); + } + }); + + return result; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts new file mode 100644 index 000000000..3b84d3907 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useComputeStepOutputSchema.ts @@ -0,0 +1,26 @@ +import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; +import { ApolloClient, useMutation } from '@apollo/client'; +import { + ComputeStepOutputSchemaInput, + ComputeStepOutputSchemaMutation, + ComputeStepOutputSchemaMutationVariables, +} from '~/generated/graphql'; +import { COMPUTE_STEP_OUTPUT_SCHEMA } from '@/workflow/graphql/mutations/computeStepOutputSchema'; + +export const useComputeStepOutputSchema = () => { + const apolloMetadataClient = useApolloMetadataClient(); + const [mutate] = useMutation< + ComputeStepOutputSchemaMutation, + ComputeStepOutputSchemaMutationVariables + >(COMPUTE_STEP_OUTPUT_SCHEMA, { + client: apolloMetadataClient ?? ({} as ApolloClient), + }); + + const computeStepOutputSchema = async ( + input: ComputeStepOutputSchemaInput, + ) => { + return await mutate({ variables: { input } }); + }; + + return { computeStepOutputSchema }; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts index a9327a6e8..b5d793258 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts @@ -16,6 +16,7 @@ import { getStepDefaultDefinition } from '@/workflow/utils/getStepDefaultDefinit import { insertStep } from '@/workflow/utils/insertStep'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-ui'; +import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema'; export const useCreateStep = ({ workflow, @@ -40,6 +41,8 @@ export const useCreateStep = ({ const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + const { computeStepOutputSchema } = useComputeStepOutputSchema(); + const insertNodeAndSave = async ({ parentNodeId, nodeToAdd, @@ -85,6 +88,17 @@ export const useCreateStep = ({ const newStep = getStepDefaultDefinition(newStepType); + const outputSchema = ( + await computeStepOutputSchema({ + step: newStep, + }) + )?.data?.computeStepOutputSchema; + + newStep.settings = { + ...newStep.settings, + outputSchema: outputSchema || {}, + }; + await insertNodeAndSave({ parentNodeId: workflowCreateStepFromParentStepId, nodeToAdd: newStep, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts b/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts index ec71956e6..2059e1339 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts @@ -4,7 +4,7 @@ import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; -import { DEACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/deactivateWorkflowVersion'; +import { DEACTIVATE_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/deactivateWorkflowVersion'; import { ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.ts index a3696eb41..53d5a6a12 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.ts @@ -8,6 +8,7 @@ import { } from '@/workflow/types/Workflow'; import { replaceStep } from '@/workflow/utils/replaceStep'; import { isDefined } from 'twenty-ui'; +import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema'; export const useUpdateWorkflowVersionStep = ({ workflow, @@ -22,12 +23,24 @@ export const useUpdateWorkflowVersionStep = ({ }); const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + const { computeStepOutputSchema } = useComputeStepOutputSchema(); const updateStep = async (updatedStep: T) => { if (!isDefined(workflow.currentVersion)) { throw new Error('Can not update an undefined workflow version.'); } + const outputSchema = ( + await computeStepOutputSchema({ + step: updatedStep, + }) + )?.data?.computeStepOutputSchema; + + updatedStep.settings = { + ...updatedStep.settings, + outputSchema: outputSchema || {}, + }; + const updatedSteps = replaceStep({ steps: workflow.currentVersion.steps ?? [], stepId, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.ts b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.ts index e6703dae8..0eff99a81 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.ts +++ b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.ts @@ -7,6 +7,7 @@ import { WorkflowWithCurrentVersion, } from '@/workflow/types/Workflow'; import { isDefined } from 'twenty-ui'; +import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema'; export const useUpdateWorkflowVersionTrigger = ({ workflow, @@ -20,12 +21,25 @@ export const useUpdateWorkflowVersionTrigger = ({ const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + const { computeStepOutputSchema } = useComputeStepOutputSchema(); + const updateTrigger = async (updatedTrigger: WorkflowTrigger) => { if (!isDefined(workflow.currentVersion)) { throw new Error('Can not update an undefined workflow version.'); } if (workflow.currentVersion.status === 'DRAFT') { + const outputSchema = ( + await computeStepOutputSchema({ + step: updatedTrigger, + }) + )?.data?.computeStepOutputSchema; + + updatedTrigger.settings = { + ...updatedTrigger.settings, + outputSchema: outputSchema || {}, + }; + await updateOneWorkflowVersion({ idToUpdate: workflow.currentVersion.id, updateOneRecordInput: { diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx index 9a56cf8a6..15dc75dd4 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx @@ -5,14 +5,14 @@ import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/S import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { SearchVariablesDropdownStepItem } from '@/workflow/search-variables/components/SearchVariablesDropdownStepItem'; import SearchVariablesDropdownStepSubItem from '@/workflow/search-variables/components/SearchVariablesDropdownStepSubItem'; -import { AVAILABLE_VARIABLES_MOCK } from '@/workflow/search-variables/constants/AvailableVariablesMock'; import { SEARCH_VARIABLES_DROPDOWN_ID } from '@/workflow/search-variables/constants/SearchVariablesDropdownId'; -import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; +import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Editor } from '@tiptap/react'; import { useState } from 'react'; import { IconVariable } from 'twenty-ui'; +import { useAvailableVariablesInWorkflowStep } from '@/workflow/hooks/useAvailableVariablesInWorkflowStep'; const StyledDropdownVariableButtonContainer = styled( StyledDropdownButtonContainer, @@ -34,8 +34,11 @@ const SearchVariablesDropdown = ({ const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`; const { isDropdownOpen } = useDropdown(dropdownId); + const availableVariablesInWorkflowStep = + useAvailableVariablesInWorkflowStep(); + const [selectedStep, setSelectedStep] = useState< - WorkflowStepMock | undefined + StepOutputSchema | undefined >(undefined); const insertVariableTag = (variable: string) => { @@ -44,7 +47,7 @@ const SearchVariablesDropdown = ({ const handleStepSelect = (stepId: string) => { setSelectedStep( - AVAILABLE_VARIABLES_MOCK.find((step) => step.id === stepId), + availableVariablesInWorkflowStep.find((step) => step.id === stepId), ); }; @@ -78,7 +81,7 @@ const SearchVariablesDropdown = ({ /> ) : ( )} diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx index ea0d7cbe5..c4887931b 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx @@ -1,8 +1,8 @@ import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; -import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; +import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema'; type SearchVariablesDropdownStepItemProps = { - steps: WorkflowStepMock[]; + steps: StepOutputSchema[]; onSelect: (value: string) => void; }; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx index 205591289..89b726b10 100644 --- a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx @@ -1,12 +1,12 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; -import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; +import { StepOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema'; import { isObject } from '@sniptt/guards'; import { useState } from 'react'; import { IconChevronLeft } from 'twenty-ui'; type SearchVariablesDropdownStepSubItemProps = { - step: WorkflowStepMock; + step: StepOutputSchema; onSelect: (value: string) => void; onBack: () => void; }; @@ -19,7 +19,7 @@ const SearchVariablesDropdownStepSubItem = ({ const [currentPath, setCurrentPath] = useState([]); const getSelectedObject = () => { - let selected = step.output; + let selected = step.outputSchema; for (const key of currentPath) { selected = selected[key]; } @@ -28,6 +28,7 @@ const SearchVariablesDropdownStepSubItem = ({ const handleSelect = (key: string) => { const selectedObject = getSelectedObject(); + if (isObject(selectedObject[key])) { setCurrentPath([...currentPath, key]); } else { diff --git a/packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts b/packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts deleted file mode 100644 index bb9836360..000000000 --- a/packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; - -export const AVAILABLE_VARIABLES_MOCK: WorkflowStepMock[] = [ - { - id: '1', - name: 'Person is Created', - output: { - userId: '1', - recordId: '123', - objectMetadataItem: { - id: '1234', - nameSingular: 'person', - namePlural: 'people', - }, - properties: { - after: { - name: 'John Doe', - email: 'john.doe@email.com', - }, - }, - }, - }, - { - id: '2', - name: 'Send Email', - output: { - success: true, - }, - }, -]; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts new file mode 100644 index 000000000..acc688e90 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/types/StepOutputSchema.ts @@ -0,0 +1,5 @@ +export type StepOutputSchema = { + id: string; + name: string; + outputSchema: Record; +}; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts b/packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts deleted file mode 100644 index 7f6d1813a..000000000 --- a/packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type WorkflowStepMock = { - id: string; - name: string; - output: Record; -}; diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 660e86961..565c38558 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -1,4 +1,6 @@ type BaseWorkflowStepSettings = { + input: object; + outputSchema: object; errorHandlingOptions: { retryOnFailure: { value: boolean; @@ -12,6 +14,7 @@ type BaseWorkflowStepSettings = { export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { input: { serverlessFunctionId: string; + serverlessFunctionVersion: string; }; }; @@ -57,6 +60,8 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & { type: 'DATABASE_EVENT'; settings: { eventName: string; + input?: object; + outputSchema: object; }; }; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts index c2c9d760f..415a327c7 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts @@ -8,6 +8,7 @@ describe('addCreateStepNodes', () => { type: 'DATABASE_EVENT', settings: { eventName: 'company.created', + outputSchema: {}, }, }; const steps: WorkflowStep[] = [ @@ -23,7 +24,9 @@ describe('addCreateStepNodes', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, { @@ -38,7 +41,9 @@ describe('addCreateStepNodes', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, ]; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts index 663e1ef56..93681887b 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts @@ -7,6 +7,7 @@ describe('generateWorkflowDiagram', () => { type: 'DATABASE_EVENT', settings: { eventName: 'company.created', + outputSchema: {}, }, }; const steps: WorkflowStep[] = []; @@ -29,6 +30,7 @@ describe('generateWorkflowDiagram', () => { type: 'DATABASE_EVENT', settings: { eventName: 'company.created', + outputSchema: {}, }, }; const steps: WorkflowStep[] = [ @@ -44,7 +46,9 @@ describe('generateWorkflowDiagram', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, { @@ -59,7 +63,9 @@ describe('generateWorkflowDiagram', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, ]; @@ -87,6 +93,7 @@ describe('generateWorkflowDiagram', () => { type: 'DATABASE_EVENT', settings: { eventName: 'company.created', + outputSchema: {}, }, }; const steps: WorkflowStep[] = [ @@ -102,7 +109,9 @@ describe('generateWorkflowDiagram', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, { @@ -117,7 +126,9 @@ describe('generateWorkflowDiagram', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, ]; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts index 30ff17e74..c77ee527f 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts @@ -42,7 +42,7 @@ describe('getWorkflowVersionDiagram', () => { name: '', steps: null, trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -83,14 +83,16 @@ describe('getWorkflowVersionDiagram', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, }, ], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts index b264d5eda..6311192cb 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts @@ -11,7 +11,7 @@ describe('insertStep', () => { name: '', steps: [], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -27,7 +27,9 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -51,7 +53,7 @@ describe('insertStep', () => { name: '', steps: [], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -67,7 +69,9 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -101,7 +105,9 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -116,14 +122,16 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, }, ], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -139,7 +147,9 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -177,7 +187,9 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -192,14 +204,16 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, }, ], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -215,7 +229,9 @@ describe('insertStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts index 349d2f742..cfcdabdcc 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts @@ -12,7 +12,9 @@ it('returns a deep copy of the provided steps array instead of mutating it', () }, input: { serverlessFunctionId: 'first', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -25,7 +27,7 @@ it('returns a deep copy of the provided steps array instead of mutating it', () name: '', steps: [stepToBeRemoved], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -51,7 +53,9 @@ it('removes a step in a non-empty steps array', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -73,7 +77,9 @@ it('removes a step in a non-empty steps array', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -89,14 +95,16 @@ it('removes a step in a non-empty steps array', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, }, ], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts index 41e4f8cba..efc4cf8c2 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts @@ -13,7 +13,9 @@ describe('replaceStep', () => { }, input: { serverlessFunctionId: 'first', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -26,7 +28,7 @@ describe('replaceStep', () => { name: '', steps: [stepToBeReplaced], trigger: { - settings: { eventName: 'company.created' }, + settings: { eventName: 'company.created', outputSchema: {} }, type: 'DATABASE_EVENT', }, updatedAt: '', @@ -43,7 +45,9 @@ describe('replaceStep', () => { }, input: { serverlessFunctionId: 'second', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, }, stepId: stepToBeReplaced.id, @@ -63,7 +67,9 @@ describe('replaceStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -85,7 +91,9 @@ describe('replaceStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, @@ -101,14 +109,19 @@ describe('replaceStep', () => { }, input: { serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + serverlessFunctionVersion: '1', }, + outputSchema: {}, }, type: 'CODE', valid: true, }, ], trigger: { - settings: { eventName: 'company.created' }, + settings: { + eventName: 'company.created', + outputSchema: {}, + }, type: 'DATABASE_EVENT', }, updatedAt: '', diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts index d59a03486..b88acaea1 100644 --- a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts @@ -17,7 +17,9 @@ export const getStepDefaultDefinition = ( settings: { input: { serverlessFunctionId: '', + serverlessFunctionVersion: '', }, + outputSchema: {}, errorHandlingOptions: { continueOnFailure: { value: false, @@ -42,6 +44,7 @@ export const getStepDefaultDefinition = ( subject: '', body: '', }, + outputSchema: {}, errorHandlingOptions: { continueOnFailure: { value: false, diff --git a/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts index dc0a76244..73b600549 100644 --- a/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/utils/getTriggerDefaultDefinition.ts @@ -26,6 +26,7 @@ export const getTriggerDefaultDefinition = ({ type, settings: { eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`, + outputSchema: {}, }, }; } diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index 48d1819e1..a04e6cc67 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -39,7 +39,7 @@ import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless. import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; -import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module'; +import { WorkflowApiModule } from 'src/engine/core-modules/workflow/workflow-api.module'; import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; @@ -66,7 +66,7 @@ import { FileModule } from './file/file.module'; WorkspaceInvitationModule, WorkspaceSSOModule, PostgresCredentialsModule, - WorkflowTriggerApiModule, + WorkflowApiModule, WorkspaceEventEmitterModule, ActorModule, TelemetryModule, diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts index 66d9f2645..7f2df5a1e 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-update.event.ts @@ -2,7 +2,7 @@ import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/typ export class ObjectRecordUpdateEvent extends ObjectRecordBaseEvent { properties: { - updatedFields: string[]; + updatedFields?: string[]; before: T; after: T; diff?: Partial; diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event.ts new file mode 100644 index 000000000..66e8026c5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event.ts @@ -0,0 +1,85 @@ +import { v4 } from 'uuid'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; +import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; +import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; +import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; +import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; + +export const generateFakeObjectRecordEvent = ( + objectMetadataEntity: ObjectMetadataEntity, + action: 'created' | 'updated' | 'deleted' | 'destroyed', +): + | ObjectRecordCreateEvent + | ObjectRecordUpdateEvent + | ObjectRecordDeleteEvent + | ObjectRecordDestroyEvent => { + const recordId = v4(); + const userId = v4(); + const workspaceMemberId = v4(); + + const after = objectMetadataEntity.fields.reduce((acc, field) => { + acc[field.name] = generateFakeValue(field.type); + + return acc; + }, {} as Entity); + + if (action === 'created') { + return { + recordId, + userId, + workspaceMemberId, + objectMetadata: objectMetadataEntity, + properties: { + after, + }, + } satisfies ObjectRecordCreateEvent; + } + + const before = objectMetadataEntity.fields.reduce((acc, field) => { + acc[field.name] = generateFakeValue(field.type); + + return acc; + }, {} as Entity); + + if (action === 'updated') { + return { + recordId, + userId, + workspaceMemberId, + objectMetadata: objectMetadataEntity, + properties: { + before, + after, + diff: after, + }, + } satisfies ObjectRecordUpdateEvent; + } + + if (action === 'deleted') { + return { + recordId, + userId, + workspaceMemberId, + objectMetadata: objectMetadataEntity, + properties: { + before, + }, + } satisfies ObjectRecordDeleteEvent; + } + + if (action === 'destroyed') { + return { + recordId, + userId, + workspaceMemberId, + objectMetadata: objectMetadataEntity, + properties: { + before, + }, + } satisfies ObjectRecordDestroyEvent; + } + + throw new Error(`Unknown action '${action}'`); +}; diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts index eaf3c4ebf..392fe732c 100644 --- a/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/utils/object-record-changed-values.ts @@ -8,7 +8,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi export const objectRecordChangedValues = ( oldRecord: Partial, newRecord: Partial, - updatedKeys: string[], + updatedKeys: string[] | undefined, objectMetadata: ObjectMetadataInterface, ) => { const fieldsByKey = new Map( @@ -23,7 +23,7 @@ export const objectRecordChangedValues = ( if ( key === 'updatedAt' || - !updatedKeys.includes(key) || + !updatedKeys?.includes(key) || field?.type === FieldMetadataType.RELATION || deepEqual(oldRecordValue, newRecordValue) ) { diff --git a/packages/twenty-server/src/engine/core-modules/workflow/dtos/compute-step-output-schema-input.dto.ts b/packages/twenty-server/src/engine/core-modules/workflow/dtos/compute-step-output-schema-input.dto.ts new file mode 100644 index 000000000..6b23c95bb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/dtos/compute-step-output-schema-input.dto.ts @@ -0,0 +1,15 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import graphqlTypeJson from 'graphql-type-json'; + +import { WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; +import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; + +@InputType() +export class ComputeStepOutputSchemaInput { + @Field(() => graphqlTypeJson, { + description: 'Step JSON format', + nullable: false, + }) + step: WorkflowTrigger | WorkflowStep; +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts new file mode 100644 index 000000000..f35e3f23e --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-builder.resolver.ts @@ -0,0 +1,33 @@ +import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { UseFilters, UseGuards } from '@nestjs/common'; + +import graphqlTypeJson from 'graphql-type-json'; + +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; +import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter'; +import { OutputSchema } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { ComputeStepOutputSchemaInput } from 'src/engine/core-modules/workflow/dtos/compute-step-output-schema-input.dto'; +import { WorkflowBuilderService } from 'src/modules/workflow/workflow-builder/workflow-builder.service'; + +@Resolver() +@UseGuards(WorkspaceAuthGuard, UserAuthGuard) +@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter) +export class WorkflowBuilderResolver { + constructor( + private readonly workflowBuilderService: WorkflowBuilderService, + ) {} + + @Mutation(() => graphqlTypeJson) + async computeStepOutputSchema( + @AuthWorkspace() { id: workspaceId }: Workspace, + @Args('input') { step }: ComputeStepOutputSchemaInput, + ): Promise { + return this.workflowBuilderService.computeStepOutputSchema({ + step, + workspaceId, + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts b/packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger.resolver.ts rename to packages/twenty-server/src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver.ts 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 new file mode 100644 index 000000000..a311b102b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver'; +import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver'; +import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module'; +import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module'; + +@Module({ + imports: [WorkflowTriggerModule, WorkflowBuilderModule], + providers: [WorkflowTriggerResolver, WorkflowBuilderResolver], +}) +export class WorkflowApiModule {} diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger-api.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger-api.module.ts deleted file mode 100644 index 8c469833c..000000000 --- a/packages/twenty-server/src/engine/core-modules/workflow/workflow-trigger-api.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/workflow-trigger.resolver'; -import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module'; - -@Module({ - imports: [WorkflowTriggerModule], - providers: [WorkflowTriggerResolver], -}) -export class WorkflowTriggerApiModule {} diff --git a/packages/twenty-server/src/engine/utils/generate-fake-value.ts b/packages/twenty-server/src/engine/utils/generate-fake-value.ts new file mode 100644 index 000000000..3cc669d83 --- /dev/null +++ b/packages/twenty-server/src/engine/utils/generate-fake-value.ts @@ -0,0 +1,33 @@ +export const generateFakeValue = (valueType: string): any => { + if (valueType === 'string') { + return 'generated-string-value'; + } else if (valueType === 'number') { + return 1; + } else if (valueType === 'boolean') { + return true; + } else if (valueType === 'Date') { + return new Date(); + } else if (valueType.endsWith('[]')) { + const elementType = valueType.replace('[]', ''); + + return Array.from({ length: 3 }, () => generateFakeValue(elementType)); + } else if (valueType.startsWith('{') && valueType.endsWith('}')) { + const objData: Record = {}; + + const properties = valueType + .slice(1, -1) + .split(';') + .map((p) => p.trim()) + .filter((p) => p); + + properties.forEach((property) => { + const [key, valueType] = property.split(':').map((s) => s.trim()); + + objData[key] = generateFakeValue(valueType); + }); + + return objData; + } else { + return 'generated-string-value'; + } +}; diff --git a/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts index 882969915..88913841c 100644 --- a/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts +++ b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts @@ -103,4 +103,34 @@ describe('CodeIntrospectionService', () => { ]); }); }); + + describe('generateFakeDataForFunction', () => { + it('should generate fake data for function', () => { + const fileContent = ` + const testArrowFunction = (param1: string, param2: number): void => { + console.log(param1, param2); + }; + `; + + const result = service.generateInputData(fileContent); + + expect(typeof result['param1']).toEqual('string'); + expect(typeof result['param2']).toEqual('number'); + }); + + it('should generate fake data for complex function', () => { + const fileContent = ` + const testArrowFunction = (param1: string[], param2: { key: number }): void => { + console.log(param1, param2); + }; + `; + + const result = service.generateInputData(fileContent); + + expect(Array.isArray(result['param1'])).toBeTruthy(); + expect(typeof result['param1'][0]).toEqual('string'); + expect(typeof result['param2']).toEqual('object'); + expect(typeof result['param2']['key']).toEqual('number'); + }); + }); }); diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts index 31e18b25f..db94f4c9b 100644 --- a/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts @@ -12,6 +12,7 @@ import { CodeIntrospectionException, CodeIntrospectionExceptionCode, } from 'src/modules/code-introspection/code-introspection.exception'; +import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; type FunctionParameter = { name: string; @@ -89,4 +90,24 @@ export class CodeIntrospectionService { type: parameter.getType().getText(), }; } + + public generateInputData(fileContent: string, fileName = 'temp.ts') { + const parameters = this.analyze(fileContent, fileName); + + return this.generateFakeDataFromParams(parameters); + } + + private generateFakeDataFromParams( + params: FunctionParameter[], + ): Record { + const data: Record = {}; + + params.forEach((param) => { + const type = param.type; + + data[param.name] = generateFakeValue(type); + }); + + return data; + } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.module.ts new file mode 100644 index 000000000..adf9a0dc1 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { WorkflowBuilderService } from 'src/modules/workflow/workflow-builder/workflow-builder.service'; +import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; +import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), + ServerlessFunctionModule, + CodeIntrospectionModule, + ], + providers: [WorkflowBuilderService], + exports: [WorkflowBuilderService], +}) +export class WorkflowBuilderModule {} diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts new file mode 100644 index 000000000..743197a90 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-builder.service.ts @@ -0,0 +1,111 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; + +import { join } from 'path'; + +import { Repository } from 'typeorm'; + +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + WorkflowTrigger, + WorkflowTriggerType, +} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; +import { + WorkflowActionType, + WorkflowStep, +} from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; +import { isDefined } from 'src/utils/is-defined'; +import { generateFakeObjectRecordEvent } from 'src/engine/core-modules/event-emitter/utils/generate-fake-object-record-event'; +import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; + +@Injectable() +export class WorkflowBuilderService { + constructor( + private readonly serverlessFunctionService: ServerlessFunctionService, + private readonly codeIntrospectionService: CodeIntrospectionService, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository, + ) {} + + async computeStepOutputSchema({ + step, + workspaceId, + }: { + step: WorkflowTrigger | WorkflowStep; + workspaceId: string; + }) { + const stepType = step.type; + + switch (stepType) { + case WorkflowTriggerType.DATABASE_EVENT: { + const [nameSingular, action] = step.settings.eventName.split('.'); + + if (!['created', 'updated', 'deleted', 'destroyed'].includes(action)) { + return {}; + } + + const objectMetadata = + await this.objectMetadataRepository.findOneOrFail({ + where: { + nameSingular, + workspaceId, + }, + relations: ['fields'], + }); + + if (!isDefined(objectMetadata)) { + return {}; + } + + return generateFakeObjectRecordEvent( + objectMetadata, + action as 'created' | 'updated' | 'deleted' | 'destroyed', + ); + } + case WorkflowActionType.SEND_EMAIL: { + return { success: true }; + } + case WorkflowActionType.CODE: { + const { serverlessFunctionId, serverlessFunctionVersion } = + step.settings.input; + + if (serverlessFunctionId === '') { + return {}; + } + + const sourceCode = ( + await this.serverlessFunctionService.getServerlessFunctionSourceCode( + workspaceId, + serverlessFunctionId, + serverlessFunctionVersion, + ) + )?.[join('src', INDEX_FILE_NAME)]; + + if (!isDefined(sourceCode)) { + return {}; + } + + const fakeFunctionInput = + this.codeIntrospectionService.generateInputData(sourceCode); + + // handle the case when event parameter is destructured: + // (event: {param1: string; param2: number}) VS ({param1, param2}: {param1: string; param2: number}) + const formattedInput = Object.values(fakeFunctionInput)[0]; + + const resultFromFakeInput = + await this.serverlessFunctionService.executeOneServerlessFunction( + serverlessFunctionId, + workspaceId, + formattedInput, + serverlessFunctionVersion, + ); + + return resultFromFakeInput.data ?? {}; + } + default: + throw new Error(`Unknown type ${stepType}`); + } + } +} diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-action.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-action.type.ts index aeacb3377..435901dc7 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-action.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-action.type.ts @@ -1,6 +1,7 @@ import { WorkflowCodeStepSettings, WorkflowSendEmailStepSettings, + WorkflowStepSettings, } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; export enum WorkflowActionType { @@ -11,6 +12,8 @@ export enum WorkflowActionType { type BaseWorkflowStep = { id: string; name: string; + type: WorkflowActionType; + settings: WorkflowStepSettings; valid: boolean; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts index 1cc9b20c9..bd8f3da0d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts @@ -1,4 +1,8 @@ +export type OutputSchema = object; + type BaseWorkflowStepSettings = { + input: object; + outputSchema: OutputSchema; errorHandlingOptions: { retryOnFailure: { value: boolean; @@ -11,6 +15,8 @@ type BaseWorkflowStepSettings = { export type WorkflowCodeStepInput = { serverlessFunctionId: string; + serverlessFunctionVersion: string; + payloadInput: object; }; export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { @@ -27,3 +33,7 @@ export type WorkflowSendEmailStepInput = { export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { input: WorkflowSendEmailStepInput; }; + +export type WorkflowStepSettings = + | WorkflowSendEmailStepSettings + | WorkflowCodeStepSettings; 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 7897463c1..164d47b74 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 @@ -1,12 +1,19 @@ +import { OutputSchema } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; + export enum WorkflowTriggerType { DATABASE_EVENT = 'DATABASE_EVENT', MANUAL = 'MANUAL', } +type BaseWorkflowTriggerSettings = { + input?: object; + outputSchema: OutputSchema; +}; + type BaseTrigger = { name: string; type: WorkflowTriggerType; - input?: object; + settings: BaseWorkflowTriggerSettings; }; export type WorkflowDatabaseEventTrigger = BaseTrigger & {