mirror of
				https://github.com/lingble/twenty.git
				synced 2025-10-31 12:47:58 +00:00 
			
		
		
		
	101 featch available variables from previous steps (#8062)
- add outputSchema in workflow step settings - use outputSchemas to compute step available variables https://github.com/user-attachments/assets/6b851d8e-625c-49ff-b29c-074cd86cbfee
This commit is contained in:
		| @@ -162,6 +162,11 @@ export type ClientConfig = { | |||||||
|   support: Support; |   support: Support; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export type ComputeStepOutputSchemaInput = { | ||||||
|  |   /** Step JSON format */ | ||||||
|  |   step: Scalars['JSON']['input']; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export type CreateAppTokenInput = { | export type CreateAppTokenInput = { | ||||||
|   expiresAt: Scalars['DateTime']['input']; |   expiresAt: Scalars['DateTime']['input']; | ||||||
| }; | }; | ||||||
| @@ -529,6 +534,7 @@ export type Mutation = { | |||||||
|   authorizeApp: AuthorizeApp; |   authorizeApp: AuthorizeApp; | ||||||
|   challenge: LoginToken; |   challenge: LoginToken; | ||||||
|   checkoutSession: SessionEntity; |   checkoutSession: SessionEntity; | ||||||
|  |   computeStepOutputSchema: Scalars['JSON']['output']; | ||||||
|   createOIDCIdentityProvider: SetupSsoOutput; |   createOIDCIdentityProvider: SetupSsoOutput; | ||||||
|   createOneAppToken: AppToken; |   createOneAppToken: AppToken; | ||||||
|   createOneField: Field; |   createOneField: Field; | ||||||
| @@ -625,6 +631,11 @@ export type MutationCheckoutSessionArgs = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export type MutationComputeStepOutputSchemaArgs = { | ||||||
|  |   input: ComputeStepOutputSchemaInput; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
| export type MutationCreateOidcIdentityProviderArgs = { | export type MutationCreateOidcIdentityProviderArgs = { | ||||||
|   input: SetupOidcSsoInput; |   input: SetupOidcSsoInput; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -155,6 +155,11 @@ export type ClientConfig = { | |||||||
|   support: Support; |   support: Support; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export type ComputeStepOutputSchemaInput = { | ||||||
|  |   /** Step JSON format */ | ||||||
|  |   step: Scalars['JSON']; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export type CreateServerlessFunctionInput = { | export type CreateServerlessFunctionInput = { | ||||||
|   description?: InputMaybe<Scalars['String']>; |   description?: InputMaybe<Scalars['String']>; | ||||||
|   name: Scalars['String']; |   name: Scalars['String']; | ||||||
| @@ -424,6 +429,7 @@ export type Mutation = { | |||||||
|   authorizeApp: AuthorizeApp; |   authorizeApp: AuthorizeApp; | ||||||
|   challenge: LoginToken; |   challenge: LoginToken; | ||||||
|   checkoutSession: SessionEntity; |   checkoutSession: SessionEntity; | ||||||
|  |   computeStepOutputSchema: Scalars['JSON']; | ||||||
|   createOIDCIdentityProvider: SetupSsoOutput; |   createOIDCIdentityProvider: SetupSsoOutput; | ||||||
|   createOneAppToken: AppToken; |   createOneAppToken: AppToken; | ||||||
|   createOneObject: Object; |   createOneObject: Object; | ||||||
| @@ -509,6 +515,11 @@ export type MutationCheckoutSessionArgs = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export type MutationComputeStepOutputSchemaArgs = { | ||||||
|  |   input: ComputeStepOutputSchemaInput; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
| export type MutationCreateOidcIdentityProviderArgs = { | export type MutationCreateOidcIdentityProviderArgs = { | ||||||
|   input: SetupOidcSsoInput; |   input: SetupOidcSsoInput; | ||||||
| }; | }; | ||||||
| @@ -1823,6 +1834,13 @@ export type ActivateWorkflowVersionMutationVariables = Exact<{ | |||||||
|  |  | ||||||
| export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean }; | export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean }; | ||||||
|  |  | ||||||
|  | export type ComputeStepOutputSchemaMutationVariables = Exact<{ | ||||||
|  |   input: ComputeStepOutputSchemaInput; | ||||||
|  | }>; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export type ComputeStepOutputSchemaMutation = { __typename?: 'Mutation', computeStepOutputSchema: any }; | ||||||
|  |  | ||||||
| export type DeactivateWorkflowVersionMutationVariables = Exact<{ | export type DeactivateWorkflowVersionMutationVariables = Exact<{ | ||||||
|   workflowVersionId: Scalars['String']; |   workflowVersionId: Scalars['String']; | ||||||
| }>; | }>; | ||||||
| @@ -3443,6 +3461,37 @@ export function useActivateWorkflowVersionMutation(baseOptions?: Apollo.Mutation | |||||||
| export type ActivateWorkflowVersionMutationHookResult = ReturnType<typeof useActivateWorkflowVersionMutation>; | export type ActivateWorkflowVersionMutationHookResult = ReturnType<typeof useActivateWorkflowVersionMutation>; | ||||||
| export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>; | export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>; | ||||||
| export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>; | export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>; | ||||||
|  | export const ComputeStepOutputSchemaDocument = gql` | ||||||
|  |     mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) { | ||||||
|  |   computeStepOutputSchema(input: $input) | ||||||
|  | } | ||||||
|  |     `; | ||||||
|  | export type ComputeStepOutputSchemaMutationFn = Apollo.MutationFunction<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * __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<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>) { | ||||||
|  |         const options = {...defaultOptions, ...baseOptions} | ||||||
|  |         return Apollo.useMutation<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>(ComputeStepOutputSchemaDocument, options); | ||||||
|  |       } | ||||||
|  | export type ComputeStepOutputSchemaMutationHookResult = ReturnType<typeof useComputeStepOutputSchemaMutation>; | ||||||
|  | export type ComputeStepOutputSchemaMutationResult = Apollo.MutationResult<ComputeStepOutputSchemaMutation>; | ||||||
|  | export type ComputeStepOutputSchemaMutationOptions = Apollo.BaseMutationOptions<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>; | ||||||
| export const DeactivateWorkflowVersionDocument = gql` | export const DeactivateWorkflowVersionDocument = gql` | ||||||
|     mutation DeactivateWorkflowVersion($workflowVersionId: String!) { |     mutation DeactivateWorkflowVersion($workflowVersionId: String!) { | ||||||
|   deactivateWorkflowVersion(workflowVersionId: $workflowVersionId) |   deactivateWorkflowVersion(workflowVersionId: $workflowVersionId) | ||||||
|   | |||||||
| @@ -3,13 +3,18 @@ import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableT | |||||||
| describe('findAvailableTimeZoneOption', () => { | describe('findAvailableTimeZoneOption', () => { | ||||||
|   it('should find the matching available IANA time zone select option from a given IANA time zone', () => { |   it('should find the matching available IANA time zone select option from a given IANA time zone', () => { | ||||||
|     const ianaTimeZone = 'Europe/Paris'; |     const ianaTimeZone = 'Europe/Paris'; | ||||||
|     const expectedOption = { |     const expectedValue = 'Europe/Paris'; | ||||||
|       label: '(GMT+02:00) Central European Summer Time - Paris', |     const expectedLabelWinter = | ||||||
|       value: 'Europe/Paris', |       '(GMT+01:00) Central European Standard Time - Paris'; | ||||||
|     }; |     const expectedLabelSummer = | ||||||
|  |       '(GMT+02:00) Central European Summer Time - Paris'; | ||||||
|  |  | ||||||
|     const option = findAvailableTimeZoneOption(ianaTimeZone); |     const option = findAvailableTimeZoneOption(ianaTimeZone); | ||||||
|  |  | ||||||
|     expect(option).toEqual(expectedOption); |     expect(option.value).toEqual(expectedValue); | ||||||
|  |     expect( | ||||||
|  |       expectedLabelWinter === option.label || | ||||||
|  |         expectedLabelSummer === option.label, | ||||||
|  |     ).toBeTruthy(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -3,11 +3,17 @@ import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel'; | |||||||
| describe('formatTimeZoneLabel', () => { | describe('formatTimeZoneLabel', () => { | ||||||
|   it('should format the time zone label correctly when location is included in the label', () => { |   it('should format the time zone label correctly when location is included in the label', () => { | ||||||
|     const ianaTimeZone = 'Europe/Paris'; |     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); |     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', () => { |   it('should format the time zone label correctly when location is not included in the label', () => { | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ export const WorkflowEditActionFormServerlessFunction = ( | |||||||
|         value={props.action.settings.input.serverlessFunctionId} |         value={props.action.settings.input.serverlessFunctionId} | ||||||
|         options={availableFunctions} |         options={availableFunctions} | ||||||
|         disabled={props.readonly} |         disabled={props.readonly} | ||||||
|         onChange={(updatedFunction) => { |         onChange={(serverlessFunctionId) => { | ||||||
|           if (props.readonly === true) { |           if (props.readonly === true) { | ||||||
|             return; |             return; | ||||||
|           } |           } | ||||||
| @@ -58,7 +58,10 @@ export const WorkflowEditActionFormServerlessFunction = ( | |||||||
|             settings: { |             settings: { | ||||||
|               ...props.action.settings, |               ...props.action.settings, | ||||||
|               input: { |               input: { | ||||||
|                 serverlessFunctionId: updatedFunction, |                 serverlessFunctionId, | ||||||
|  |                 serverlessFunctionVersion: | ||||||
|  |                   serverlessFunctions.find((f) => f.id === serverlessFunctionId) | ||||||
|  |                     ?.latestVersion || 'latest', | ||||||
|               }, |               }, | ||||||
|             }, |             }, | ||||||
|           }); |           }); | ||||||
|   | |||||||
| @@ -134,6 +134,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ | |||||||
|                     type: 'DATABASE_EVENT', |                     type: 'DATABASE_EVENT', | ||||||
|                     settings: { |                     settings: { | ||||||
|                       eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`, |                       eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`, | ||||||
|  |                       outputSchema: {}, | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|             ); |             ); | ||||||
| @@ -165,6 +166,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ | |||||||
|                     type: 'DATABASE_EVENT', |                     type: 'DATABASE_EVENT', | ||||||
|                     settings: { |                     settings: { | ||||||
|                       eventName: `${availableMetadata[0].value}.${updatedEvent}`, |                       eventName: `${availableMetadata[0].value}.${updatedEvent}`, | ||||||
|  |                       outputSchema: {}, | ||||||
|                     }, |                     }, | ||||||
|                   }, |                   }, | ||||||
|             ); |             ); | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | import { gql } from '@apollo/client'; | ||||||
|  |  | ||||||
|  | export const COMPUTE_STEP_OUTPUT_SCHEMA = gql` | ||||||
|  |   mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) { | ||||||
|  |     computeStepOutputSchema(input: $input) | ||||||
|  |   } | ||||||
|  | `; | ||||||
| @@ -4,7 +4,7 @@ import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; | |||||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||||
| import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; | 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 { WorkflowVersion } from '@/workflow/types/Workflow'; | ||||||
| import { | import { | ||||||
|   ActivateWorkflowVersionMutation, |   ActivateWorkflowVersionMutation, | ||||||
|   | |||||||
| @@ -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; | ||||||
|  | }; | ||||||
| @@ -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<any>), | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const computeStepOutputSchema = async ( | ||||||
|  |     input: ComputeStepOutputSchemaInput, | ||||||
|  |   ) => { | ||||||
|  |     return await mutate({ variables: { input } }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return { computeStepOutputSchema }; | ||||||
|  | }; | ||||||
| @@ -16,6 +16,7 @@ import { getStepDefaultDefinition } from '@/workflow/utils/getStepDefaultDefinit | |||||||
| import { insertStep } from '@/workflow/utils/insertStep'; | import { insertStep } from '@/workflow/utils/insertStep'; | ||||||
| import { useRecoilValue, useSetRecoilState } from 'recoil'; | import { useRecoilValue, useSetRecoilState } from 'recoil'; | ||||||
| import { isDefined } from 'twenty-ui'; | import { isDefined } from 'twenty-ui'; | ||||||
|  | import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema'; | ||||||
|  |  | ||||||
| export const useCreateStep = ({ | export const useCreateStep = ({ | ||||||
|   workflow, |   workflow, | ||||||
| @@ -40,6 +41,8 @@ export const useCreateStep = ({ | |||||||
|  |  | ||||||
|   const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); |   const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); | ||||||
|  |  | ||||||
|  |   const { computeStepOutputSchema } = useComputeStepOutputSchema(); | ||||||
|  |  | ||||||
|   const insertNodeAndSave = async ({ |   const insertNodeAndSave = async ({ | ||||||
|     parentNodeId, |     parentNodeId, | ||||||
|     nodeToAdd, |     nodeToAdd, | ||||||
| @@ -85,6 +88,17 @@ export const useCreateStep = ({ | |||||||
|  |  | ||||||
|     const newStep = getStepDefaultDefinition(newStepType); |     const newStep = getStepDefaultDefinition(newStepType); | ||||||
|  |  | ||||||
|  |     const outputSchema = ( | ||||||
|  |       await computeStepOutputSchema({ | ||||||
|  |         step: newStep, | ||||||
|  |       }) | ||||||
|  |     )?.data?.computeStepOutputSchema; | ||||||
|  |  | ||||||
|  |     newStep.settings = { | ||||||
|  |       ...newStep.settings, | ||||||
|  |       outputSchema: outputSchema || {}, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     await insertNodeAndSave({ |     await insertNodeAndSave({ | ||||||
|       parentNodeId: workflowCreateStepFromParentStepId, |       parentNodeId: workflowCreateStepFromParentStepId, | ||||||
|       nodeToAdd: newStep, |       nodeToAdd: newStep, | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; | |||||||
| import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; | ||||||
| import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; | ||||||
| import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; | 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 { | import { | ||||||
|   ActivateWorkflowVersionMutation, |   ActivateWorkflowVersionMutation, | ||||||
|   ActivateWorkflowVersionMutationVariables, |   ActivateWorkflowVersionMutationVariables, | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import { | |||||||
| } from '@/workflow/types/Workflow'; | } from '@/workflow/types/Workflow'; | ||||||
| import { replaceStep } from '@/workflow/utils/replaceStep'; | import { replaceStep } from '@/workflow/utils/replaceStep'; | ||||||
| import { isDefined } from 'twenty-ui'; | import { isDefined } from 'twenty-ui'; | ||||||
|  | import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema'; | ||||||
|  |  | ||||||
| export const useUpdateWorkflowVersionStep = ({ | export const useUpdateWorkflowVersionStep = ({ | ||||||
|   workflow, |   workflow, | ||||||
| @@ -22,12 +23,24 @@ export const useUpdateWorkflowVersionStep = ({ | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); |   const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); | ||||||
|  |   const { computeStepOutputSchema } = useComputeStepOutputSchema(); | ||||||
|  |  | ||||||
|   const updateStep = async <T extends WorkflowStep>(updatedStep: T) => { |   const updateStep = async <T extends WorkflowStep>(updatedStep: T) => { | ||||||
|     if (!isDefined(workflow.currentVersion)) { |     if (!isDefined(workflow.currentVersion)) { | ||||||
|       throw new Error('Can not update an undefined workflow version.'); |       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({ |     const updatedSteps = replaceStep({ | ||||||
|       steps: workflow.currentVersion.steps ?? [], |       steps: workflow.currentVersion.steps ?? [], | ||||||
|       stepId, |       stepId, | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { | |||||||
|   WorkflowWithCurrentVersion, |   WorkflowWithCurrentVersion, | ||||||
| } from '@/workflow/types/Workflow'; | } from '@/workflow/types/Workflow'; | ||||||
| import { isDefined } from 'twenty-ui'; | import { isDefined } from 'twenty-ui'; | ||||||
|  | import { useComputeStepOutputSchema } from '@/workflow/hooks/useComputeStepOutputSchema'; | ||||||
|  |  | ||||||
| export const useUpdateWorkflowVersionTrigger = ({ | export const useUpdateWorkflowVersionTrigger = ({ | ||||||
|   workflow, |   workflow, | ||||||
| @@ -20,12 +21,25 @@ export const useUpdateWorkflowVersionTrigger = ({ | |||||||
|  |  | ||||||
|   const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); |   const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); | ||||||
|  |  | ||||||
|  |   const { computeStepOutputSchema } = useComputeStepOutputSchema(); | ||||||
|  |  | ||||||
|   const updateTrigger = async (updatedTrigger: WorkflowTrigger) => { |   const updateTrigger = async (updatedTrigger: WorkflowTrigger) => { | ||||||
|     if (!isDefined(workflow.currentVersion)) { |     if (!isDefined(workflow.currentVersion)) { | ||||||
|       throw new Error('Can not update an undefined workflow version.'); |       throw new Error('Can not update an undefined workflow version.'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (workflow.currentVersion.status === 'DRAFT') { |     if (workflow.currentVersion.status === 'DRAFT') { | ||||||
|  |       const outputSchema = ( | ||||||
|  |         await computeStepOutputSchema({ | ||||||
|  |           step: updatedTrigger, | ||||||
|  |         }) | ||||||
|  |       )?.data?.computeStepOutputSchema; | ||||||
|  |  | ||||||
|  |       updatedTrigger.settings = { | ||||||
|  |         ...updatedTrigger.settings, | ||||||
|  |         outputSchema: outputSchema || {}, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|       await updateOneWorkflowVersion({ |       await updateOneWorkflowVersion({ | ||||||
|         idToUpdate: workflow.currentVersion.id, |         idToUpdate: workflow.currentVersion.id, | ||||||
|         updateOneRecordInput: { |         updateOneRecordInput: { | ||||||
|   | |||||||
| @@ -5,14 +5,14 @@ import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/S | |||||||
| import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; | ||||||
| import { SearchVariablesDropdownStepItem } from '@/workflow/search-variables/components/SearchVariablesDropdownStepItem'; | import { SearchVariablesDropdownStepItem } from '@/workflow/search-variables/components/SearchVariablesDropdownStepItem'; | ||||||
| import SearchVariablesDropdownStepSubItem from '@/workflow/search-variables/components/SearchVariablesDropdownStepSubItem'; | 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 { 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 { useTheme } from '@emotion/react'; | ||||||
| import styled from '@emotion/styled'; | import styled from '@emotion/styled'; | ||||||
| import { Editor } from '@tiptap/react'; | import { Editor } from '@tiptap/react'; | ||||||
| import { useState } from 'react'; | import { useState } from 'react'; | ||||||
| import { IconVariable } from 'twenty-ui'; | import { IconVariable } from 'twenty-ui'; | ||||||
|  | import { useAvailableVariablesInWorkflowStep } from '@/workflow/hooks/useAvailableVariablesInWorkflowStep'; | ||||||
|  |  | ||||||
| const StyledDropdownVariableButtonContainer = styled( | const StyledDropdownVariableButtonContainer = styled( | ||||||
|   StyledDropdownButtonContainer, |   StyledDropdownButtonContainer, | ||||||
| @@ -34,8 +34,11 @@ const SearchVariablesDropdown = ({ | |||||||
|  |  | ||||||
|   const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`; |   const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`; | ||||||
|   const { isDropdownOpen } = useDropdown(dropdownId); |   const { isDropdownOpen } = useDropdown(dropdownId); | ||||||
|  |   const availableVariablesInWorkflowStep = | ||||||
|  |     useAvailableVariablesInWorkflowStep(); | ||||||
|  |  | ||||||
|   const [selectedStep, setSelectedStep] = useState< |   const [selectedStep, setSelectedStep] = useState< | ||||||
|     WorkflowStepMock | undefined |     StepOutputSchema | undefined | ||||||
|   >(undefined); |   >(undefined); | ||||||
|  |  | ||||||
|   const insertVariableTag = (variable: string) => { |   const insertVariableTag = (variable: string) => { | ||||||
| @@ -44,7 +47,7 @@ const SearchVariablesDropdown = ({ | |||||||
|  |  | ||||||
|   const handleStepSelect = (stepId: string) => { |   const handleStepSelect = (stepId: string) => { | ||||||
|     setSelectedStep( |     setSelectedStep( | ||||||
|       AVAILABLE_VARIABLES_MOCK.find((step) => step.id === stepId), |       availableVariablesInWorkflowStep.find((step) => step.id === stepId), | ||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -78,7 +81,7 @@ const SearchVariablesDropdown = ({ | |||||||
|               /> |               /> | ||||||
|             ) : ( |             ) : ( | ||||||
|               <SearchVariablesDropdownStepItem |               <SearchVariablesDropdownStepItem | ||||||
|                 steps={AVAILABLE_VARIABLES_MOCK} |                 steps={availableVariablesInWorkflowStep} | ||||||
|                 onSelect={handleStepSelect} |                 onSelect={handleStepSelect} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; | 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 = { | type SearchVariablesDropdownStepItemProps = { | ||||||
|   steps: WorkflowStepMock[]; |   steps: StepOutputSchema[]; | ||||||
|   onSelect: (value: string) => void; |   onSelect: (value: string) => void; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; | ||||||
| import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; | 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 { isObject } from '@sniptt/guards'; | ||||||
| import { useState } from 'react'; | import { useState } from 'react'; | ||||||
| import { IconChevronLeft } from 'twenty-ui'; | import { IconChevronLeft } from 'twenty-ui'; | ||||||
|  |  | ||||||
| type SearchVariablesDropdownStepSubItemProps = { | type SearchVariablesDropdownStepSubItemProps = { | ||||||
|   step: WorkflowStepMock; |   step: StepOutputSchema; | ||||||
|   onSelect: (value: string) => void; |   onSelect: (value: string) => void; | ||||||
|   onBack: () => void; |   onBack: () => void; | ||||||
| }; | }; | ||||||
| @@ -19,7 +19,7 @@ const SearchVariablesDropdownStepSubItem = ({ | |||||||
|   const [currentPath, setCurrentPath] = useState<string[]>([]); |   const [currentPath, setCurrentPath] = useState<string[]>([]); | ||||||
|  |  | ||||||
|   const getSelectedObject = () => { |   const getSelectedObject = () => { | ||||||
|     let selected = step.output; |     let selected = step.outputSchema; | ||||||
|     for (const key of currentPath) { |     for (const key of currentPath) { | ||||||
|       selected = selected[key]; |       selected = selected[key]; | ||||||
|     } |     } | ||||||
| @@ -28,6 +28,7 @@ const SearchVariablesDropdownStepSubItem = ({ | |||||||
|  |  | ||||||
|   const handleSelect = (key: string) => { |   const handleSelect = (key: string) => { | ||||||
|     const selectedObject = getSelectedObject(); |     const selectedObject = getSelectedObject(); | ||||||
|  |  | ||||||
|     if (isObject(selectedObject[key])) { |     if (isObject(selectedObject[key])) { | ||||||
|       setCurrentPath([...currentPath, key]); |       setCurrentPath([...currentPath, key]); | ||||||
|     } else { |     } else { | ||||||
|   | |||||||
| @@ -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, |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| ]; |  | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | export type StepOutputSchema = { | ||||||
|  |   id: string; | ||||||
|  |   name: string; | ||||||
|  |   outputSchema: Record<string, any>; | ||||||
|  | }; | ||||||
| @@ -1,5 +0,0 @@ | |||||||
| export type WorkflowStepMock = { |  | ||||||
|   id: string; |  | ||||||
|   name: string; |  | ||||||
|   output: Record<string, any>; |  | ||||||
| }; |  | ||||||
| @@ -1,4 +1,6 @@ | |||||||
| type BaseWorkflowStepSettings = { | type BaseWorkflowStepSettings = { | ||||||
|  |   input: object; | ||||||
|  |   outputSchema: object; | ||||||
|   errorHandlingOptions: { |   errorHandlingOptions: { | ||||||
|     retryOnFailure: { |     retryOnFailure: { | ||||||
|       value: boolean; |       value: boolean; | ||||||
| @@ -12,6 +14,7 @@ type BaseWorkflowStepSettings = { | |||||||
| export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { | export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { | ||||||
|   input: { |   input: { | ||||||
|     serverlessFunctionId: string; |     serverlessFunctionId: string; | ||||||
|  |     serverlessFunctionVersion: string; | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -57,6 +60,8 @@ export type WorkflowDatabaseEventTrigger = BaseTrigger & { | |||||||
|   type: 'DATABASE_EVENT'; |   type: 'DATABASE_EVENT'; | ||||||
|   settings: { |   settings: { | ||||||
|     eventName: string; |     eventName: string; | ||||||
|  |     input?: object; | ||||||
|  |     outputSchema: object; | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ describe('addCreateStepNodes', () => { | |||||||
|       type: 'DATABASE_EVENT', |       type: 'DATABASE_EVENT', | ||||||
|       settings: { |       settings: { | ||||||
|         eventName: 'company.created', |         eventName: 'company.created', | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|     const steps: WorkflowStep[] = [ |     const steps: WorkflowStep[] = [ | ||||||
| @@ -23,7 +24,9 @@ describe('addCreateStepNodes', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -38,7 +41,9 @@ describe('addCreateStepNodes', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ describe('generateWorkflowDiagram', () => { | |||||||
|       type: 'DATABASE_EVENT', |       type: 'DATABASE_EVENT', | ||||||
|       settings: { |       settings: { | ||||||
|         eventName: 'company.created', |         eventName: 'company.created', | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|     const steps: WorkflowStep[] = []; |     const steps: WorkflowStep[] = []; | ||||||
| @@ -29,6 +30,7 @@ describe('generateWorkflowDiagram', () => { | |||||||
|       type: 'DATABASE_EVENT', |       type: 'DATABASE_EVENT', | ||||||
|       settings: { |       settings: { | ||||||
|         eventName: 'company.created', |         eventName: 'company.created', | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|     const steps: WorkflowStep[] = [ |     const steps: WorkflowStep[] = [ | ||||||
| @@ -44,7 +46,9 @@ describe('generateWorkflowDiagram', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -59,7 +63,9 @@ describe('generateWorkflowDiagram', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
| @@ -87,6 +93,7 @@ describe('generateWorkflowDiagram', () => { | |||||||
|       type: 'DATABASE_EVENT', |       type: 'DATABASE_EVENT', | ||||||
|       settings: { |       settings: { | ||||||
|         eventName: 'company.created', |         eventName: 'company.created', | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|     const steps: WorkflowStep[] = [ |     const steps: WorkflowStep[] = [ | ||||||
| @@ -102,7 +109,9 @@ describe('generateWorkflowDiagram', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
| @@ -117,7 +126,9 @@ describe('generateWorkflowDiagram', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -42,7 +42,7 @@ describe('getWorkflowVersionDiagram', () => { | |||||||
|       name: '', |       name: '', | ||||||
|       steps: null, |       steps: null, | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
| @@ -83,14 +83,16 @@ describe('getWorkflowVersionDiagram', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ describe('insertStep', () => { | |||||||
|       name: '', |       name: '', | ||||||
|       steps: [], |       steps: [], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
| @@ -27,7 +27,9 @@ describe('insertStep', () => { | |||||||
|         }, |         }, | ||||||
|         input: { |         input: { | ||||||
|           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |           serverlessFunctionVersion: '1', | ||||||
|         }, |         }, | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|       type: 'CODE', |       type: 'CODE', | ||||||
|       valid: true, |       valid: true, | ||||||
| @@ -51,7 +53,7 @@ describe('insertStep', () => { | |||||||
|       name: '', |       name: '', | ||||||
|       steps: [], |       steps: [], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
| @@ -67,7 +69,9 @@ describe('insertStep', () => { | |||||||
|         }, |         }, | ||||||
|         input: { |         input: { | ||||||
|           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |           serverlessFunctionVersion: '1', | ||||||
|         }, |         }, | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|       type: 'CODE', |       type: 'CODE', | ||||||
|       valid: true, |       valid: true, | ||||||
| @@ -101,7 +105,9 @@ describe('insertStep', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
| @@ -116,14 +122,16 @@ describe('insertStep', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
| @@ -139,7 +147,9 @@ describe('insertStep', () => { | |||||||
|         }, |         }, | ||||||
|         input: { |         input: { | ||||||
|           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |           serverlessFunctionVersion: '1', | ||||||
|         }, |         }, | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|       type: 'CODE', |       type: 'CODE', | ||||||
|       valid: true, |       valid: true, | ||||||
| @@ -177,7 +187,9 @@ describe('insertStep', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
| @@ -192,14 +204,16 @@ describe('insertStep', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
| @@ -215,7 +229,9 @@ describe('insertStep', () => { | |||||||
|         }, |         }, | ||||||
|         input: { |         input: { | ||||||
|           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |           serverlessFunctionVersion: '1', | ||||||
|         }, |         }, | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|       type: 'CODE', |       type: 'CODE', | ||||||
|       valid: true, |       valid: true, | ||||||
|   | |||||||
| @@ -12,7 +12,9 @@ it('returns a deep copy of the provided steps array instead of mutating it', () | |||||||
|       }, |       }, | ||||||
|       input: { |       input: { | ||||||
|         serverlessFunctionId: 'first', |         serverlessFunctionId: 'first', | ||||||
|  |         serverlessFunctionVersion: '1', | ||||||
|       }, |       }, | ||||||
|  |       outputSchema: {}, | ||||||
|     }, |     }, | ||||||
|     type: 'CODE', |     type: 'CODE', | ||||||
|     valid: true, |     valid: true, | ||||||
| @@ -25,7 +27,7 @@ it('returns a deep copy of the provided steps array instead of mutating it', () | |||||||
|     name: '', |     name: '', | ||||||
|     steps: [stepToBeRemoved], |     steps: [stepToBeRemoved], | ||||||
|     trigger: { |     trigger: { | ||||||
|       settings: { eventName: 'company.created' }, |       settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|       type: 'DATABASE_EVENT', |       type: 'DATABASE_EVENT', | ||||||
|     }, |     }, | ||||||
|     updatedAt: '', |     updatedAt: '', | ||||||
| @@ -51,7 +53,9 @@ it('removes a step in a non-empty steps array', () => { | |||||||
|       }, |       }, | ||||||
|       input: { |       input: { | ||||||
|         serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |         serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |         serverlessFunctionVersion: '1', | ||||||
|       }, |       }, | ||||||
|  |       outputSchema: {}, | ||||||
|     }, |     }, | ||||||
|     type: 'CODE', |     type: 'CODE', | ||||||
|     valid: true, |     valid: true, | ||||||
| @@ -73,7 +77,9 @@ it('removes a step in a non-empty steps array', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|         type: 'CODE', |         type: 'CODE', | ||||||
|         valid: true, |         valid: true, | ||||||
| @@ -89,14 +95,16 @@ it('removes a step in a non-empty steps array', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |             serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|         type: 'CODE', |         type: 'CODE', | ||||||
|         valid: true, |         valid: true, | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|     trigger: { |     trigger: { | ||||||
|       settings: { eventName: 'company.created' }, |       settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|       type: 'DATABASE_EVENT', |       type: 'DATABASE_EVENT', | ||||||
|     }, |     }, | ||||||
|     updatedAt: '', |     updatedAt: '', | ||||||
|   | |||||||
| @@ -13,7 +13,9 @@ describe('replaceStep', () => { | |||||||
|         }, |         }, | ||||||
|         input: { |         input: { | ||||||
|           serverlessFunctionId: 'first', |           serverlessFunctionId: 'first', | ||||||
|  |           serverlessFunctionVersion: '1', | ||||||
|         }, |         }, | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|       type: 'CODE', |       type: 'CODE', | ||||||
|       valid: true, |       valid: true, | ||||||
| @@ -26,7 +28,7 @@ describe('replaceStep', () => { | |||||||
|       name: '', |       name: '', | ||||||
|       steps: [stepToBeReplaced], |       steps: [stepToBeReplaced], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { eventName: 'company.created', outputSchema: {} }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
| @@ -43,7 +45,9 @@ describe('replaceStep', () => { | |||||||
|           }, |           }, | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: 'second', |             serverlessFunctionId: 'second', | ||||||
|  |             serverlessFunctionVersion: '1', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       stepId: stepToBeReplaced.id, |       stepId: stepToBeReplaced.id, | ||||||
| @@ -63,7 +67,9 @@ describe('replaceStep', () => { | |||||||
|         }, |         }, | ||||||
|         input: { |         input: { | ||||||
|           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |           serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |           serverlessFunctionVersion: '1', | ||||||
|         }, |         }, | ||||||
|  |         outputSchema: {}, | ||||||
|       }, |       }, | ||||||
|       type: 'CODE', |       type: 'CODE', | ||||||
|       valid: true, |       valid: true, | ||||||
| @@ -85,7 +91,9 @@ describe('replaceStep', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
| @@ -101,14 +109,19 @@ describe('replaceStep', () => { | |||||||
|             }, |             }, | ||||||
|             input: { |             input: { | ||||||
|               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', |               serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', | ||||||
|  |               serverlessFunctionVersion: '1', | ||||||
|             }, |             }, | ||||||
|  |             outputSchema: {}, | ||||||
|           }, |           }, | ||||||
|           type: 'CODE', |           type: 'CODE', | ||||||
|           valid: true, |           valid: true, | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|       trigger: { |       trigger: { | ||||||
|         settings: { eventName: 'company.created' }, |         settings: { | ||||||
|  |           eventName: 'company.created', | ||||||
|  |           outputSchema: {}, | ||||||
|  |         }, | ||||||
|         type: 'DATABASE_EVENT', |         type: 'DATABASE_EVENT', | ||||||
|       }, |       }, | ||||||
|       updatedAt: '', |       updatedAt: '', | ||||||
|   | |||||||
| @@ -17,7 +17,9 @@ export const getStepDefaultDefinition = ( | |||||||
|         settings: { |         settings: { | ||||||
|           input: { |           input: { | ||||||
|             serverlessFunctionId: '', |             serverlessFunctionId: '', | ||||||
|  |             serverlessFunctionVersion: '', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|           errorHandlingOptions: { |           errorHandlingOptions: { | ||||||
|             continueOnFailure: { |             continueOnFailure: { | ||||||
|               value: false, |               value: false, | ||||||
| @@ -42,6 +44,7 @@ export const getStepDefaultDefinition = ( | |||||||
|             subject: '', |             subject: '', | ||||||
|             body: '', |             body: '', | ||||||
|           }, |           }, | ||||||
|  |           outputSchema: {}, | ||||||
|           errorHandlingOptions: { |           errorHandlingOptions: { | ||||||
|             continueOnFailure: { |             continueOnFailure: { | ||||||
|               value: false, |               value: false, | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ export const getTriggerDefaultDefinition = ({ | |||||||
|         type, |         type, | ||||||
|         settings: { |         settings: { | ||||||
|           eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`, |           eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`, | ||||||
|  |           outputSchema: {}, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless. | |||||||
| import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; | import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; | ||||||
| import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; | import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; | ||||||
| import { UserModule } from 'src/engine/core-modules/user/user.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 { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; | ||||||
| import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; | import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; | ||||||
| import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; | import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; | ||||||
| @@ -66,7 +66,7 @@ import { FileModule } from './file/file.module'; | |||||||
|     WorkspaceInvitationModule, |     WorkspaceInvitationModule, | ||||||
|     WorkspaceSSOModule, |     WorkspaceSSOModule, | ||||||
|     PostgresCredentialsModule, |     PostgresCredentialsModule, | ||||||
|     WorkflowTriggerApiModule, |     WorkflowApiModule, | ||||||
|     WorkspaceEventEmitterModule, |     WorkspaceEventEmitterModule, | ||||||
|     ActorModule, |     ActorModule, | ||||||
|     TelemetryModule, |     TelemetryModule, | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/typ | |||||||
|  |  | ||||||
| export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent { | export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent { | ||||||
|   properties: { |   properties: { | ||||||
|     updatedFields: string[]; |     updatedFields?: string[]; | ||||||
|     before: T; |     before: T; | ||||||
|     after: T; |     after: T; | ||||||
|     diff?: Partial<T>; |     diff?: Partial<T>; | ||||||
|   | |||||||
| @@ -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 = <Entity>( | ||||||
|  |   objectMetadataEntity: ObjectMetadataEntity, | ||||||
|  |   action: 'created' | 'updated' | 'deleted' | 'destroyed', | ||||||
|  | ): | ||||||
|  |   | ObjectRecordCreateEvent<Entity> | ||||||
|  |   | ObjectRecordUpdateEvent<Entity> | ||||||
|  |   | ObjectRecordDeleteEvent<Entity> | ||||||
|  |   | ObjectRecordDestroyEvent<Entity> => { | ||||||
|  |   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<Entity>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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<Entity>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (action === 'deleted') { | ||||||
|  |     return { | ||||||
|  |       recordId, | ||||||
|  |       userId, | ||||||
|  |       workspaceMemberId, | ||||||
|  |       objectMetadata: objectMetadataEntity, | ||||||
|  |       properties: { | ||||||
|  |         before, | ||||||
|  |       }, | ||||||
|  |     } satisfies ObjectRecordDeleteEvent<Entity>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (action === 'destroyed') { | ||||||
|  |     return { | ||||||
|  |       recordId, | ||||||
|  |       userId, | ||||||
|  |       workspaceMemberId, | ||||||
|  |       objectMetadata: objectMetadataEntity, | ||||||
|  |       properties: { | ||||||
|  |         before, | ||||||
|  |       }, | ||||||
|  |     } satisfies ObjectRecordDestroyEvent<Entity>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   throw new Error(`Unknown action '${action}'`); | ||||||
|  | }; | ||||||
| @@ -8,7 +8,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi | |||||||
| export const objectRecordChangedValues = ( | export const objectRecordChangedValues = ( | ||||||
|   oldRecord: Partial<IRecord>, |   oldRecord: Partial<IRecord>, | ||||||
|   newRecord: Partial<IRecord>, |   newRecord: Partial<IRecord>, | ||||||
|   updatedKeys: string[], |   updatedKeys: string[] | undefined, | ||||||
|   objectMetadata: ObjectMetadataInterface, |   objectMetadata: ObjectMetadataInterface, | ||||||
| ) => { | ) => { | ||||||
|   const fieldsByKey = new Map( |   const fieldsByKey = new Map( | ||||||
| @@ -23,7 +23,7 @@ export const objectRecordChangedValues = ( | |||||||
|  |  | ||||||
|       if ( |       if ( | ||||||
|         key === 'updatedAt' || |         key === 'updatedAt' || | ||||||
|         !updatedKeys.includes(key) || |         !updatedKeys?.includes(key) || | ||||||
|         field?.type === FieldMetadataType.RELATION || |         field?.type === FieldMetadataType.RELATION || | ||||||
|         deepEqual(oldRecordValue, newRecordValue) |         deepEqual(oldRecordValue, newRecordValue) | ||||||
|       ) { |       ) { | ||||||
|   | |||||||
| @@ -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; | ||||||
|  | } | ||||||
| @@ -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<OutputSchema> { | ||||||
|  |     return this.workflowBuilderService.computeStepOutputSchema({ | ||||||
|  |       step, | ||||||
|  |       workspaceId, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -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 {} | ||||||
| @@ -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 {} |  | ||||||
| @@ -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<string, any> = {}; | ||||||
|  |  | ||||||
|  |     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'; | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -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'); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ import { | |||||||
|   CodeIntrospectionException, |   CodeIntrospectionException, | ||||||
|   CodeIntrospectionExceptionCode, |   CodeIntrospectionExceptionCode, | ||||||
| } from 'src/modules/code-introspection/code-introspection.exception'; | } from 'src/modules/code-introspection/code-introspection.exception'; | ||||||
|  | import { generateFakeValue } from 'src/engine/utils/generate-fake-value'; | ||||||
|  |  | ||||||
| type FunctionParameter = { | type FunctionParameter = { | ||||||
|   name: string; |   name: string; | ||||||
| @@ -89,4 +90,24 @@ export class CodeIntrospectionService { | |||||||
|       type: parameter.getType().getText(), |       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<string, any> { | ||||||
|  |     const data: Record<string, any> = {}; | ||||||
|  |  | ||||||
|  |     params.forEach((param) => { | ||||||
|  |       const type = param.type; | ||||||
|  |  | ||||||
|  |       data[param.name] = generateFakeValue(type); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return data; | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 {} | ||||||
| @@ -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<ObjectMetadataEntity>, | ||||||
|  |   ) {} | ||||||
|  |  | ||||||
|  |   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}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import { | import { | ||||||
|   WorkflowCodeStepSettings, |   WorkflowCodeStepSettings, | ||||||
|   WorkflowSendEmailStepSettings, |   WorkflowSendEmailStepSettings, | ||||||
|  |   WorkflowStepSettings, | ||||||
| } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; | } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; | ||||||
|  |  | ||||||
| export enum WorkflowActionType { | export enum WorkflowActionType { | ||||||
| @@ -11,6 +12,8 @@ export enum WorkflowActionType { | |||||||
| type BaseWorkflowStep = { | type BaseWorkflowStep = { | ||||||
|   id: string; |   id: string; | ||||||
|   name: string; |   name: string; | ||||||
|  |   type: WorkflowActionType; | ||||||
|  |   settings: WorkflowStepSettings; | ||||||
|   valid: boolean; |   valid: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
|  | export type OutputSchema = object; | ||||||
|  |  | ||||||
| type BaseWorkflowStepSettings = { | type BaseWorkflowStepSettings = { | ||||||
|  |   input: object; | ||||||
|  |   outputSchema: OutputSchema; | ||||||
|   errorHandlingOptions: { |   errorHandlingOptions: { | ||||||
|     retryOnFailure: { |     retryOnFailure: { | ||||||
|       value: boolean; |       value: boolean; | ||||||
| @@ -11,6 +15,8 @@ type BaseWorkflowStepSettings = { | |||||||
|  |  | ||||||
| export type WorkflowCodeStepInput = { | export type WorkflowCodeStepInput = { | ||||||
|   serverlessFunctionId: string; |   serverlessFunctionId: string; | ||||||
|  |   serverlessFunctionVersion: string; | ||||||
|  |   payloadInput: object; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { | export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { | ||||||
| @@ -27,3 +33,7 @@ export type WorkflowSendEmailStepInput = { | |||||||
| export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { | export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { | ||||||
|   input: WorkflowSendEmailStepInput; |   input: WorkflowSendEmailStepInput; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export type WorkflowStepSettings = | ||||||
|  |   | WorkflowSendEmailStepSettings | ||||||
|  |   | WorkflowCodeStepSettings; | ||||||
|   | |||||||
| @@ -1,12 +1,19 @@ | |||||||
|  | import { OutputSchema } from 'src/modules/workflow/workflow-executor/types/workflow-step-settings.type'; | ||||||
|  |  | ||||||
| export enum WorkflowTriggerType { | export enum WorkflowTriggerType { | ||||||
|   DATABASE_EVENT = 'DATABASE_EVENT', |   DATABASE_EVENT = 'DATABASE_EVENT', | ||||||
|   MANUAL = 'MANUAL', |   MANUAL = 'MANUAL', | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type BaseWorkflowTriggerSettings = { | ||||||
|  |   input?: object; | ||||||
|  |   outputSchema: OutputSchema; | ||||||
|  | }; | ||||||
|  |  | ||||||
| type BaseTrigger = { | type BaseTrigger = { | ||||||
|   name: string; |   name: string; | ||||||
|   type: WorkflowTriggerType; |   type: WorkflowTriggerType; | ||||||
|   input?: object; |   settings: BaseWorkflowTriggerSettings; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type WorkflowDatabaseEventTrigger = BaseTrigger & { | export type WorkflowDatabaseEventTrigger = BaseTrigger & { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 martmull
					martmull