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:
martmull
2024-10-28 12:25:29 +01:00
committed by GitHub
parent 3ae987be92
commit 1aa961dedf
49 changed files with 706 additions and 83 deletions

View File

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

View File

@@ -155,6 +155,11 @@ export type ClientConfig = {
support: Support;
};
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON'];
};
export type CreateServerlessFunctionInput = {
description?: InputMaybe<Scalars['String']>;
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<typeof useActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>;
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`
mutation DeactivateWorkflowVersion($workflowVersionId: String!) {
deactivateWorkflowVersion(workflowVersionId: $workflowVersionId)

View File

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

View File

@@ -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', () => {

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const COMPUTE_STEP_OUTPUT_SCHEMA = gql`
mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) {
computeStepOutputSchema(input: $input)
}
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <T extends WorkflowStep>(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,

View File

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

View File

@@ -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 = ({
/>
) : (
<SearchVariablesDropdownStepItem
steps={AVAILABLE_VARIABLES_MOCK}
steps={availableVariablesInWorkflowStep}
onSelect={handleStepSelect}
/>
)}

View File

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

View File

@@ -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<string[]>([]);
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 {

View File

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

View File

@@ -0,0 +1,5 @@
export type StepOutputSchema = {
id: string;
name: string;
outputSchema: Record<string, any>;
};

View File

@@ -1,5 +0,0 @@
export type WorkflowStepMock = {
id: string;
name: string;
output: Record<string, any>;
};

View File

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

View File

@@ -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: {},
},
},
];

View File

@@ -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: {},
},
},
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,7 @@ export const getTriggerDefaultDefinition = ({
type,
settings: {
eventName: `${activeObjectMetadataItems[0].nameSingular}.${OBJECT_EVENT_TRIGGERS[0].value}`,
outputSchema: {},
},
};
}

View File

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

View File

@@ -2,7 +2,7 @@ import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/typ
export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent {
properties: {
updatedFields: string[];
updatedFields?: string[];
before: T;
after: T;
diff?: Partial<T>;

View File

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

View File

@@ -8,7 +8,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi
export const objectRecordChangedValues = (
oldRecord: Partial<IRecord>,
newRecord: Partial<IRecord>,
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)
) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<string, any> {
const data: Record<string, any> = {};
params.forEach((param) => {
const type = param.type;
data[param.name] = generateFakeValue(type);
});
return data;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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