diff --git a/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts b/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts index 4aecfbd6fe..d5035d4621 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAiAgentOutputSchema.ts @@ -1,13 +1,13 @@ import { type OutputSchemaField } from '@/ai/constants/OutputFieldTypeOptions'; import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow'; -import { type BaseOutputSchemaDeprecated } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +import { type AiAgentOutputSchema } from '@/workflow/workflow-variables/types/AiAgentOutputSchema'; import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { useDebouncedCallback } from 'use-debounce'; import { v4 } from 'uuid'; export const useAiAgentOutputSchema = ( - outputSchema?: BaseOutputSchemaDeprecated, + outputSchema?: AiAgentOutputSchema, onActionUpdate?: (action: WorkflowAiAgentAction) => void, action?: WorkflowAiAgentAction, readonly?: boolean, @@ -26,7 +26,7 @@ export const useAiAgentOutputSchema = ( return; } - const newOutputSchema = fields.reduce( + const newOutputSchema = fields.reduce( (schema, field) => { if (isDefined(field.name)) { schema[field.name] = { diff --git a/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts b/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts deleted file mode 100644 index a3a9e13898..0000000000 --- a/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionOutputSchema.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema'; - -describe('getFunctionOutputSchema', () => { - it('should compute outputSchema properly', () => { - const testResult = { - a: null, - b: 'b', - c: { cc: 1 }, - d: true, - e: [1, 2, 3], - }; - const expectedOutputSchema = { - a: { - isLeaf: true, - type: 'unknown', - value: null, - label: 'a', - }, - b: { - isLeaf: true, - type: 'string', - value: 'b', - label: 'b', - }, - c: { - isLeaf: false, - type: 'object', - label: 'c', - value: { - cc: { - isLeaf: true, - type: 'number', - value: 1, - label: 'cc', - }, - }, - }, - d: { - isLeaf: true, - type: 'boolean', - value: true, - label: 'd', - }, - e: { - isLeaf: true, - type: 'array', - value: [1, 2, 3], - label: 'e', - }, - }; - expect(getFunctionOutputSchema(testResult)).toEqual(expectedOutputSchema); - }); -}); diff --git a/packages/twenty-front/src/modules/workflow/types/InputSchema.ts b/packages/twenty-front/src/modules/workflow/types/InputSchema.ts index d7823ff23f..f53d56aded 100644 --- a/packages/twenty-front/src/modules/workflow/types/InputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/types/InputSchema.ts @@ -1,13 +1,7 @@ +import { type LeafType, type NodeType } from 'twenty-shared/workflow'; import { type FieldMetadataType } from '~/generated-metadata/graphql'; -export type InputSchemaPropertyType = - | 'string' - | 'number' - | 'boolean' - | 'object' - | 'array' - | 'unknown' - | FieldMetadataType; +export type InputSchemaPropertyType = LeafType | NodeType | FieldMetadataType; export type InputSchemaProperty = { type: InputSchemaPropertyType; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx index 80ca9d6019..bb3210428f 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/WorkflowEditActionAiAgent.tsx @@ -8,7 +8,7 @@ import { WorkflowActionFooter } from '@/workflow/workflow-steps/components/Workf import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; -import { type BaseOutputSchemaDeprecated } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +import { type AiAgentOutputSchema } from '@/workflow/workflow-variables/types/AiAgentOutputSchema'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { useRecoilValue } from 'recoil'; @@ -48,7 +48,7 @@ export const WorkflowEditActionAiAgent = ({ }); const { handleOutputSchemaChange, outputFields } = useAiAgentOutputSchema( - action.settings.outputSchema as BaseOutputSchemaDeprecated, + action.settings.outputSchema as AiAgentOutputSchema, actionOptions.readonly === true ? undefined : actionOptions.onActionUpdate, action, actionOptions.readonly, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx index 979953264e..e2b8052ec5 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunction.tsx @@ -15,7 +15,6 @@ import { ServerlessFunctionExecutionResult } from '@/serverless-functions/compon import { INDEX_FILE_NAME } from '@/serverless-functions/constants/IndexFileName'; import { useTestServerlessFunction } from '@/serverless-functions/hooks/useTestServerlessFunction'; import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode'; -import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema'; import { mergeDefaultFunctionInputAndFunctionInput } from '@/serverless-functions/utils/mergeDefaultFunctionInputAndFunctionInput'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { TextArea } from '@/ui/input/components/TextArea'; @@ -47,6 +46,7 @@ import { useEffect, useState } from 'react'; import { useRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; import { isDefined } from 'twenty-shared/utils'; +import { buildOutputSchemaFromValue } from 'twenty-shared/workflow'; import { IconCode, IconPlayerPlay, useIcons } from 'twenty-ui/display'; import { CodeEditor } from 'twenty-ui/input'; import { useIsMobile } from 'twenty-ui/utilities'; @@ -135,7 +135,7 @@ export const WorkflowEditActionServerlessFunction = ({ if (actionOptions.readonly === true) { return; } - const newOutputSchema = getFunctionOutputSchema(testResult); + const newOutputSchema = buildOutputSchemaFromValue(testResult); updateAction({ ...action, settings: { ...action.settings, outputSchema: newOutputSchema }, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/hooks/useHttpRequestOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/hooks/useHttpRequestOutputSchema.ts index e3fa51d724..d2d0b22003 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/hooks/useHttpRequestOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/hooks/useHttpRequestOutputSchema.ts @@ -1,8 +1,8 @@ import { type WorkflowHttpRequestAction } from '@/workflow/types/Workflow'; import { parseAndValidateVariableFriendlyStringifiedJson } from '@/workflow/utils/parseAndValidateVariableFriendlyStringifiedJson'; -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; import { convertOutputSchemaToJson } from '../utils/convertOutputSchemaToJson'; import { getHttpRequestOutputSchema } from '../utils/getHttpRequestOutputSchema'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/__tests__/convertOutputSchemaToJson.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/__tests__/convertOutputSchemaToJson.test.ts index 81cae5145d..358b2b41cd 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/__tests__/convertOutputSchemaToJson.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/__tests__/convertOutputSchemaToJson.test.ts @@ -1,4 +1,4 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; import { convertOutputSchemaToJson } from '../convertOutputSchemaToJson'; describe('convertOutputSchemaToJson', () => { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/convertOutputSchemaToJson.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/convertOutputSchemaToJson.ts index 6d8ea293a4..af6c804823 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/convertOutputSchemaToJson.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/convertOutputSchemaToJson.ts @@ -1,4 +1,4 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; export const convertOutputSchemaToJson = ( schema: BaseOutputSchemaV2, diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/getHttpRequestOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/getHttpRequestOutputSchema.ts index d24e4a6fda..b006d58c13 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/getHttpRequestOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/http-request-action/utils/getHttpRequestOutputSchema.ts @@ -1,5 +1,5 @@ import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; const getValueType = (value: unknown): InputSchemaPropertyType => { if (value === null || value === undefined) { diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx index fd66b293e7..b108cadc40 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator.tsx @@ -58,7 +58,7 @@ export const WorkflowEditActionIterator = ({ ? defaultItems : isArray(defaultItems) ? stringifyArrayItems(defaultItems) - : stringifyArrayItems(JSON.parse(defaultItems)); + : []; const [formData, setFormData] = useState({ items: parsedItems, diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx index e93fbbdc8f..3db8fd2d80 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm.tsx @@ -1,7 +1,6 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader'; import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput'; -import { getFunctionOutputSchema } from '@/serverless-functions/utils/getFunctionOutputSchema'; import { Select } from '@/ui/input/components/Select'; import { TextInput } from '@/ui/input/components/TextInput'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; @@ -22,6 +21,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { buildOutputSchemaFromValue } from 'twenty-shared/workflow'; import { IconCopy, useIcons } from 'twenty-ui/display'; import { useDebouncedCallback } from 'use-debounce'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; @@ -175,7 +175,9 @@ export const WorkflowEditTriggerWebhookForm = ({ expectedBody: undefined, })); - const outputSchema = getFunctionOutputSchema(parsingResult.data); + const outputSchema = buildOutputSchemaFromValue( + parsingResult.data, + ); triggerOptions.onTriggerUpdate( { diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts index 82ad1b0e03..28afd1b36b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useVariableDropdown.ts @@ -6,7 +6,6 @@ import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState'; import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState'; import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState'; -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type LinkOutputSchema } from '@/workflow/workflow-variables/types/LinkOutputSchema'; import { type FieldOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2'; import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2'; @@ -14,6 +13,7 @@ import { getVariableTemplateFromPath } from '@/workflow/workflow-variables/utils import { useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; import { useIcons } from 'twenty-ui/display'; import { isBaseOutputSchemaV2 } from '../types/guards/isBaseOutputSchemaV2'; import { isLinkOutputSchema } from '../types/guards/isLinkOutputSchema'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/AiAgentOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/AiAgentOutputSchema.ts new file mode 100644 index 0000000000..f07f5c7575 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/AiAgentOutputSchema.ts @@ -0,0 +1,17 @@ +import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; + +export type AiAgentLeaf = { + isLeaf: true; + type: InputSchemaPropertyType | undefined; + label: string; + value: any; +}; + +export type AiAgentNode = { + isLeaf: false; + type: 'object' | 'unknown'; + label: string; + value: AiAgentOutputSchema; +}; + +export type AiAgentOutputSchema = Record; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/CodeOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/CodeOutputSchema.ts index 349ac579db..f4c81640cf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/CodeOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/CodeOutputSchema.ts @@ -1,4 +1,4 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type LinkOutputSchema } from '@/workflow/workflow-variables/types/LinkOutputSchema'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; export type CodeOutputSchema = LinkOutputSchema | BaseOutputSchemaV2; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts index f2fc9d63db..ba9a3e09e6 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/FindRecordsOutputSchema.ts @@ -1,5 +1,5 @@ -import type { Leaf } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type RecordNode } from '@/workflow/workflow-variables/types/RecordNode'; +import type { Leaf } from 'twenty-shared/workflow'; export type FindRecordsOutputSchema = { first: RecordNode; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/IteratorOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/IteratorOutputSchema.ts index 8263744da4..9e7cfe1669 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/IteratorOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/IteratorOutputSchema.ts @@ -1,5 +1,5 @@ -import { type Leaf } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type RecordNode } from '@/workflow/workflow-variables/types/RecordNode'; +import { type Leaf } from 'twenty-shared/workflow'; export type IteratorOutputSchema = { // TODO(t.trompette): add support for node items that are not records diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/ManualTriggerOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/ManualTriggerOutputSchema.ts index e3146ee154..2d1faed316 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/ManualTriggerOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/ManualTriggerOutputSchema.ts @@ -1,5 +1,5 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; export type ManualTriggerOutputSchema = | BaseOutputSchemaV2 diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/OpenStepOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/OpenStepOutputSchema.ts index 3769545d04..1034fffcf8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/OpenStepOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/OpenStepOutputSchema.ts @@ -1,3 +1,3 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; export type OpenStepOutputSchema = BaseOutputSchemaV2; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchemaV2.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchemaV2.ts index 06c83b0c85..8cdeb33e13 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchemaV2.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/StepOutputSchemaV2.ts @@ -2,13 +2,13 @@ import { type WorkflowActionType, type WorkflowTriggerType, } from '@/workflow/types/Workflow'; -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeOutputSchema'; import { type FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema'; import { type FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema'; import { type IteratorOutputSchema } from '@/workflow/workflow-variables/types/IteratorOutputSchema'; import { type ManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/ManualTriggerOutputSchema'; import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; export type OutputSchemaV2 = | BaseOutputSchemaV2 diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/guards/isBaseOutputSchemaV2.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/types/guards/isBaseOutputSchemaV2.ts index dd5b412875..3706e6a8bb 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/guards/isBaseOutputSchemaV2.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/types/guards/isBaseOutputSchemaV2.ts @@ -1,6 +1,6 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type OutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2'; import { isObject } from '@sniptt/guards'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; export const isBaseOutputSchemaV2 = ( outputSchema: OutputSchemaV2, diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts index 25e7dc70d0..e825476319 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/filterOutputSchema.test.ts @@ -1,6 +1,6 @@ -import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2'; import { FieldMetadataType } from 'twenty-shared/types'; +import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow'; import { filterOutputSchema } from '../filterOutputSchema'; describe('filterOutputSchema', () => { diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts index 5e64c0d2a7..eccf087e1d 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughBaseOutputSchema.test.ts @@ -1,5 +1,5 @@ -import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema'; +import type { BaseOutputSchemaV2 } from 'twenty-shared/workflow'; describe('searchVariableThroughBaseOutputSchema', () => { const mockBaseSchema: BaseOutputSchemaV2 = { @@ -91,7 +91,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.message}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -106,7 +105,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.count}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -121,7 +119,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.isSuccess}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -136,7 +133,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.items}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -151,7 +147,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.user.name}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -166,7 +161,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.user.profile.email}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -182,7 +176,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.user.profile.isActive}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -198,7 +191,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'Code Action', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.config.timeout}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -213,7 +205,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.invalidField}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -227,7 +218,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.user.invalidField}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -241,7 +231,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.user.profile.invalidField}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -255,7 +244,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.message.nestedField}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -269,7 +257,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: undefined as any, rawVariableName: '{{step1.message}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -283,7 +270,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: '{{}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -297,7 +283,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'HTTP Request', baseOutputSchema: mockBaseSchema, rawVariableName: 'step1.message', - isFullRecord: false, }); expect(result).toEqual({ @@ -312,7 +297,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'Code Action', baseOutputSchema: mockBaseSchema, rawVariableName: '{{step1.user}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -336,7 +320,6 @@ describe('searchVariableThroughBaseOutputSchema', () => { stepName: 'AI Agent', baseOutputSchema: schemaWithUnknown, rawVariableName: '{{step1.unknownField}}', - isFullRecord: false, }); expect(result).toEqual({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughCodeOutputSchema.test.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughCodeOutputSchema.test.ts index d12085b71d..57b9318e49 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughCodeOutputSchema.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/__tests__/searchVariableThroughCodeOutputSchema.test.ts @@ -1,6 +1,6 @@ -import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import type { CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeOutputSchema'; import { searchVariableThroughCodeOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema'; +import type { BaseOutputSchemaV2 } from 'twenty-shared/workflow'; describe('searchVariableThroughCodeOutputSchema', () => { describe('LinkOutputSchema tests', () => { @@ -18,7 +18,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Step', codeOutputSchema: mockLinkSchema, rawVariableName: '{{step1.link}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -87,7 +86,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.message}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -102,7 +100,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.count}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -117,7 +114,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.isEnabled}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -132,7 +128,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.data.userId}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -147,7 +142,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.data.profile.name}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -163,7 +157,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.data.profile.email}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -179,7 +172,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.data}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -194,7 +186,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.invalidField}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -208,7 +199,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{step1.data.invalidField}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -233,7 +223,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: undefined as any, rawVariableName: '{{step1.message}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -247,7 +236,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: '{{}}', - isFullRecord: false, }); expect(result).toEqual({ @@ -261,7 +249,6 @@ describe('searchVariableThroughCodeOutputSchema', () => { stepName: 'Code Action', codeOutputSchema: mockBaseSchema, rawVariableName: 'step1.simpleField', - isFullRecord: false, }); expect(result).toEqual({ diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts index 5afdec8974..7032d78390 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/filterOutputSchema.ts @@ -1,8 +1,4 @@ import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; -import { - type BaseOutputSchemaV2, - type Node, -} from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { type FieldOutputSchemaV2, type RecordOutputSchemaV2, @@ -13,6 +9,7 @@ import { isLinkOutputSchema } from '@/workflow/workflow-variables/types/guards/i import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2'; import { isFieldTypeCompatibleWithRecordId } from '@/workflow/workflow-variables/utils/isFieldTypeCompatibleWithRecordId'; import { isDefined } from 'twenty-shared/utils'; +import { type BaseOutputSchemaV2, type Node } from 'twenty-shared/workflow'; const isValidRecordOutputSchema = ({ shouldDisplayRecordFields, @@ -108,7 +105,7 @@ const filterBaseOutputSchema = ({ continue; } - if (field.isLeaf) { + if (field.isLeaf === true) { if (isFieldTypeCompatibleWithRecordId(field.type)) { filteredSchema[key] = field; hasValidFields = true; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts index 6f043d8ec8..dda08f8d49 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema.ts @@ -1,7 +1,9 @@ import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable'; -import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; import { isDefined } from 'twenty-shared/utils'; -import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow'; +import { + CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX, + type BaseOutputSchemaV2, +} from 'twenty-shared/workflow'; const parseVariableName = (rawVariableName: string) => { const variableWithoutBrackets = rawVariableName.replace( @@ -29,7 +31,7 @@ const navigateToTargetField = ( for (const pathSegment of pathSegments) { const field = currentSchema[pathSegment]; - if (!isDefined(field) || field.isLeaf) { + if (!isDefined(field) || field.isLeaf === true) { return null; } @@ -66,6 +68,35 @@ const buildVariableResult = ( }; }; +export const searchBaseOutputSchema = ({ + stepName, + baseOutputSchema, + path, + selectedField, +}: { + stepName: string; + baseOutputSchema: BaseOutputSchemaV2; + path: string[]; + selectedField: string; +}): VariableSearchResult => { + const navigationResult = navigateToTargetField(baseOutputSchema, path); + + if (!navigationResult) { + return { + variableLabel: undefined, + variablePathLabel: undefined, + variableType: undefined, + }; + } + + return buildVariableResult( + stepName, + navigationResult.pathLabels, + navigationResult.schema, + selectedField, + ); +}; + /** * Searches for a variable within a base output schema and returns its metadata * @@ -83,7 +114,6 @@ export const searchVariableThroughBaseOutputSchema = ({ stepName: string; baseOutputSchema: BaseOutputSchemaV2; rawVariableName: string; - isFullRecord?: boolean; }): VariableSearchResult => { if (!isDefined(baseOutputSchema)) { return { @@ -102,22 +132,10 @@ export const searchVariableThroughBaseOutputSchema = ({ }; } - const navigationResult = navigateToTargetField( - baseOutputSchema, - pathSegments, - ); - - if (!navigationResult) { - return { - variableLabel: undefined, - variablePathLabel: undefined, - }; - } - - return buildVariableResult( + return searchBaseOutputSchema({ stepName, - navigationResult.pathLabels, - navigationResult.schema, - targetFieldName, - ); + baseOutputSchema, + path: pathSegments, + selectedField: targetFieldName, + }); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema.ts index 98e22eb78d..0ee9788d9e 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema.ts @@ -24,12 +24,10 @@ export const searchVariableThroughCodeOutputSchema = ({ stepName, codeOutputSchema, rawVariableName, - isFullRecord = false, }: { stepName: string; codeOutputSchema: CodeOutputSchema; rawVariableName: string; - isFullRecord?: boolean; }): VariableSearchResult => { if (!isDefined(codeOutputSchema) || isLinkOutputSchema(codeOutputSchema)) { return { @@ -42,6 +40,5 @@ export const searchVariableThroughCodeOutputSchema = ({ stepName, baseOutputSchema: codeOutputSchema, rawVariableName, - isFullRecord, }); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts index 58635475d5..5fb51fa03b 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema.ts @@ -1,6 +1,8 @@ import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable'; +import { isBaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isBaseOutputSchemaV2'; import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2'; import { type IteratorOutputSchema } from '@/workflow/workflow-variables/types/IteratorOutputSchema'; +import { searchBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema'; import { searchRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema'; import { FieldMetadataType } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; @@ -94,10 +96,21 @@ export const searchVariableThroughIteratorOutputSchema = ({ }); } + if (isBaseOutputSchemaV2(schema) && isDefined(fieldName)) { + return searchBaseOutputSchema({ + stepName, + baseOutputSchema: schema, + path: pathSegments, + selectedField: fieldName, + }); + } + return { - variableLabel: 'Current Item', - variablePathLabel: `${stepName} > Current Item`, - variableType: schema.type, + variableLabel: iteratorOutputSchema.currentItem.label, + variablePathLabel: `${stepName} > ${iteratorOutputSchema.currentItem.label}`, + variableType: iteratorOutputSchema.currentItem.isLeaf + ? iteratorOutputSchema.currentItem.type + : 'unknown', }; } diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughManualTriggerOutputSchema.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughManualTriggerOutputSchema.ts index 1aa62c0489..6e1d1ad406 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughManualTriggerOutputSchema.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughManualTriggerOutputSchema.ts @@ -27,6 +27,5 @@ export const searchVariableThroughManualTriggerOutputSchema = ({ stepName, baseOutputSchema: manualTriggerOutputSchema, rawVariableName, - isFullRecord, }); }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchemaV2.ts b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchemaV2.ts index 1d282812de..41415dcc4a 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchemaV2.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-variables/utils/searchVariableThroughOutputSchemaV2.ts @@ -83,7 +83,6 @@ export const searchVariableThroughOutputSchemaV2 = ({ stepName: stepOutputSchema.name, codeOutputSchema: stepOutputSchema.outputSchema, rawVariableName, - isFullRecord, }); } @@ -100,6 +99,5 @@ export const searchVariableThroughOutputSchemaV2 = ({ stepName: stepOutputSchema.name, baseOutputSchema: stepOutputSchema.outputSchema, rawVariableName, - isFullRecord, }); }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const.ts index cb037ca494..32aa7e289f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const.ts @@ -1,5 +1,4 @@ -import { type Leaf } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; - +import { type Leaf } from 'twenty-shared/workflow'; export const DEFAULT_ITERATOR_CURRENT_ITEM: Leaf = { label: 'Current Item', isLeaf: true, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/extract-property-path-from-variable.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/extract-property-path-from-variable.spec.ts new file mode 100644 index 0000000000..ccb6ee8414 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/extract-property-path-from-variable.spec.ts @@ -0,0 +1,55 @@ +import { extractPropertyPathFromVariable } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/extract-property-path-from-variable'; + +describe('extractPropertyPathFromVariable', () => { + it('should extract single property path', () => { + const result = extractPropertyPathFromVariable('{{step1.result}}'); + + expect(result).toEqual(['result']); + }); + + it('should extract nested property path', () => { + const result = extractPropertyPathFromVariable('{{step1.result.items}}'); + + expect(result).toEqual(['result', 'items']); + }); + + it('should extract deeply nested property path', () => { + const result = extractPropertyPathFromVariable( + '{{step1.data.user.address.street}}', + ); + + expect(result).toEqual(['data', 'user', 'address', 'street']); + }); + + it('should handle variable without brackets', () => { + const result = extractPropertyPathFromVariable('step1.result.items'); + + expect(result).toEqual(['result', 'items']); + }); + + it('should return empty array for variable with only step id', () => { + const result = extractPropertyPathFromVariable('{{step1}}'); + + expect(result).toEqual([]); + }); + + it('should return empty array for variable with only step id without brackets', () => { + const result = extractPropertyPathFromVariable('step1'); + + expect(result).toEqual([]); + }); + + it('should handle complex step ids', () => { + const result = extractPropertyPathFromVariable( + '{{step_with_underscore.result.data}}', + ); + + expect(result).toEqual(['result', 'data']); + }); + + it('should handle numeric step ids', () => { + const result = extractPropertyPathFromVariable('{{step123.output.value}}'); + + expect(result).toEqual(['output', 'value']); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/infer-array-item-schema.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/infer-array-item-schema.spec.ts new file mode 100644 index 0000000000..69b037d0cd --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/infer-array-item-schema.spec.ts @@ -0,0 +1,210 @@ +import { type Leaf, type Node } from 'twenty-shared/workflow'; + +import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const'; +import { inferArrayItemSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/infer-array-item-schema'; + +describe('inferArrayItemSchema', () => { + it('should return DEFAULT_ITERATOR_CURRENT_ITEM for non-leaf node', () => { + const schemaNode: Node = { + isLeaf: false, + type: 'object', + label: 'test', + value: {}, + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM); + }); + + it('should return DEFAULT_ITERATOR_CURRENT_ITEM for non-array leaf', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'string', + label: 'test', + value: 'not an array', + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM); + }); + + it('should return DEFAULT_ITERATOR_CURRENT_ITEM for empty array', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM); + }); + + it('should infer schema for array of strings', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: ['item1', 'item2', 'item3'], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual({ + isLeaf: true, + type: 'string', + label: 'Current Item', + value: 'item1', + }); + }); + + it('should infer schema for array of numbers', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [1, 2, 3], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual({ + isLeaf: true, + type: 'number', + label: 'Current Item', + value: 1, + }); + }); + + it('should infer schema for array of booleans', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [true, false], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual({ + isLeaf: true, + type: 'boolean', + label: 'Current Item', + value: true, + }); + }); + + it('should infer full schema for array of simple objects', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual({ + isLeaf: false, + type: 'object', + label: 'Current Item', + value: { + id: { + isLeaf: true, + type: 'number', + label: 'id', + value: 1, + }, + name: { + isLeaf: true, + type: 'string', + label: 'name', + value: 'Item 1', + }, + }, + }); + }); + + it('should infer full schema for array of nested objects', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [ + { + toto: { + titi: 1, + tata: 'hello', + }, + }, + { + toto: { + titi: 2, + tata: 'world', + }, + }, + ], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual({ + isLeaf: false, + type: 'object', + label: 'Current Item', + value: { + toto: { + isLeaf: false, + type: 'object', + label: 'toto', + value: { + titi: { + isLeaf: true, + type: 'number', + label: 'titi', + value: 1, + }, + tata: { + isLeaf: true, + type: 'string', + label: 'tata', + value: 'hello', + }, + }, + }, + }, + }); + }); + + it('should handle array with null first item', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [null, 'item2'], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM); + }); + + it('should handle array with undefined first item', () => { + const schemaNode: Leaf = { + isLeaf: true, + type: 'array', + label: 'items', + value: [undefined, 'item2'], + }; + + const result = inferArrayItemSchema({ schemaNode }); + + expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/extract-property-path-from-variable.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/extract-property-path-from-variable.ts new file mode 100644 index 0000000000..00e310390a --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/extract-property-path-from-variable.ts @@ -0,0 +1,14 @@ +import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow'; + +export const extractPropertyPathFromVariable = ( + rawVariableName: string, +): string[] => { + const variableWithoutBrackets = rawVariableName.replace( + CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX, + (_, variableName) => variableName, + ); + + const parts = variableWithoutBrackets.split('.'); + + return parts.slice(1); +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/infer-array-item-schema.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/infer-array-item-schema.ts new file mode 100644 index 0000000000..83aaebd59d --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/infer-array-item-schema.ts @@ -0,0 +1,65 @@ +import { + isArray, + isBoolean, + isNumber, + isObject, + isString, +} from '@sniptt/guards'; +import { isDefined } from 'twenty-shared/utils'; +import { + buildOutputSchemaFromValue, + type Leaf, + type LeafType, + type Node, +} from 'twenty-shared/workflow'; + +import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const'; + +export const inferArrayItemSchema = ({ + schemaNode, +}: { + schemaNode: Leaf | Node; +}): Leaf | Node => { + if (!schemaNode.isLeaf || schemaNode.type !== 'array') { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + + const arrayValue = schemaNode.value; + + if (!Array.isArray(arrayValue) || arrayValue.length === 0) { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + + const firstItem = arrayValue[0]; + + if (!isDefined(firstItem)) { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + + if (isObject(firstItem)) { + const itemSchema = buildOutputSchemaFromValue(firstItem); + + return { + isLeaf: false, + type: 'object', + label: 'Current Item', + value: itemSchema, + }; + } + + const getValueType = (value: unknown): LeafType => { + if (isString(value)) return 'string'; + if (isNumber(value)) return 'number'; + if (isBoolean(value)) return 'boolean'; + if (isArray(value)) return 'array'; + + return 'unknown'; + }; + + return { + isLeaf: true, + type: getValueType(firstItem), + label: 'Current Item', + value: firstItem, + }; +}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 4ab13b2f69..dc4acc34b0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -3,9 +3,11 @@ import { Injectable } from '@nestjs/common'; import { isString } from '@sniptt/guards'; import { isDefined, isValidVariable } from 'twenty-shared/utils'; import { + BaseOutputSchemaV2, BulkRecordsAvailability, extractRawVariableNamePart, GlobalAvailability, + navigateOutputSchemaProperty, SingleRecordAvailability, TRIGGER_STEP_ID, } from 'twenty-shared/workflow'; @@ -22,10 +24,12 @@ import { Node, type OutputSchema, } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type'; +import { extractPropertyPathFromVariable } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/extract-property-path-from-variable'; import { generateFakeArrayItem } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-array-item'; import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response'; import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record'; import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event'; +import { inferArrayItemSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/infer-array-item-schema'; import { type FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type'; import { type WorkflowAction, @@ -413,7 +417,19 @@ export class WorkflowSchemaWorkspaceService { return DEFAULT_ITERATOR_CURRENT_ITEM; } - // TODO(t.trompette): handle other trigger types + case WorkflowTriggerType.WEBHOOK: { + const propertyPath = extractPropertyPathFromVariable(items); + const schemaNode = navigateOutputSchemaProperty({ + schema: trigger.settings.outputSchema as BaseOutputSchemaV2, + propertyPath, + }); + + if (!isDefined(schemaNode)) { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + + return inferArrayItemSchema({ schemaNode }); + } default: { return DEFAULT_ITERATOR_CURRENT_ITEM; } @@ -447,6 +463,20 @@ export class WorkflowSchemaWorkspaceService { }), }; } + case WorkflowActionType.CODE: + case WorkflowActionType.HTTP_REQUEST: { + const propertyPath = extractPropertyPathFromVariable(items); + const schemaNode = navigateOutputSchemaProperty({ + schema: step.settings.outputSchema as BaseOutputSchemaV2, + propertyPath, + }); + + if (!isDefined(schemaNode)) { + return DEFAULT_ITERATOR_CURRENT_ITEM; + } + + return inferArrayItemSchema({ schemaNode }); + } default: { return DEFAULT_ITERATOR_CURRENT_ITEM; } diff --git a/packages/twenty-shared/src/workflow/index.ts b/packages/twenty-shared/src/workflow/index.ts index bb6a19bba2..deae73e6e3 100644 --- a/packages/twenty-shared/src/workflow/index.ts +++ b/packages/twenty-shared/src/workflow/index.ts @@ -60,6 +60,15 @@ export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkfl export { extractRawVariableNamePart } from './utils/extractRawVariableNameParts'; export { getWorkflowRunContext } from './utils/getWorkflowRunContext'; export { parseDataFromContentType } from './utils/parseDataFromContentType'; +export type { + LeafType, + NodeType, + Leaf, + Node, + BaseOutputSchemaV2, +} from './workflow-schema/types/base-output-schema.type'; +export { buildOutputSchemaFromValue } from './workflow-schema/utils/buildOutputSchemaFromValue'; +export { navigateOutputSchemaProperty } from './workflow-schema/utils/navigateOutputSchemaProperty'; export type { GlobalAvailability, SingleRecordAvailability, diff --git a/packages/twenty-shared/src/workflow/workflow-schema/index.ts b/packages/twenty-shared/src/workflow/workflow-schema/index.ts new file mode 100644 index 0000000000..45ea747da7 --- /dev/null +++ b/packages/twenty-shared/src/workflow/workflow-schema/index.ts @@ -0,0 +1,9 @@ +export type { + BaseOutputSchemaV2, + Leaf, + LeafType, + Node, + NodeType, +} from './types/base-output-schema.type'; +export { buildOutputSchemaFromValue } from './utils/buildOutputSchemaFromValue'; +export { navigateOutputSchemaProperty } from './utils/navigateOutputSchemaProperty'; diff --git a/packages/twenty-front/src/modules/workflow/workflow-variables/types/BaseOutputSchemaV2.ts b/packages/twenty-shared/src/workflow/workflow-schema/types/base-output-schema.type.ts similarity index 50% rename from packages/twenty-front/src/modules/workflow/workflow-variables/types/BaseOutputSchemaV2.ts rename to packages/twenty-shared/src/workflow/workflow-schema/types/base-output-schema.type.ts index b62fa8f0ae..27de92dc63 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-variables/types/BaseOutputSchemaV2.ts +++ b/packages/twenty-shared/src/workflow/workflow-schema/types/base-output-schema.type.ts @@ -1,7 +1,7 @@ -import { type InputSchemaPropertyType } from '@/workflow/types/InputSchema'; - export type LeafType = 'string' | 'number' | 'boolean' | 'array' | 'unknown'; +export type NodeType = 'object' | 'unknown'; + export type Leaf = { isLeaf: true; type: LeafType; @@ -11,18 +11,9 @@ export type Leaf = { export type Node = { isLeaf: false; - type: 'object' | 'unknown'; + type: NodeType; label: string; value: BaseOutputSchemaV2; }; export type BaseOutputSchemaV2 = Record; - -export type LeafDeprecated = { - isLeaf: true; - type: InputSchemaPropertyType | undefined; - label: string; - value: any; -}; - -export type BaseOutputSchemaDeprecated = Record; diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/buildOutputSchemaFromValue.test.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/buildOutputSchemaFromValue.test.ts new file mode 100644 index 0000000000..631e477538 --- /dev/null +++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/buildOutputSchemaFromValue.test.ts @@ -0,0 +1,106 @@ +import { buildOutputSchemaFromValue } from '../buildOutputSchemaFromValue'; + +describe('buildOutputSchemaFromValue', () => { + it('should compute outputSchema properly for mixed types', () => { + const testResult = { + a: null, + b: 'b', + c: { cc: 1 }, + d: true, + e: [1, 2, 3], + }; + const expectedOutputSchema = { + a: { + isLeaf: true, + type: 'unknown', + value: null, + label: 'a', + }, + b: { + isLeaf: true, + type: 'string', + value: 'b', + label: 'b', + }, + c: { + isLeaf: false, + type: 'object', + label: 'c', + value: { + cc: { + isLeaf: true, + type: 'number', + value: 1, + label: 'cc', + }, + }, + }, + d: { + isLeaf: true, + type: 'boolean', + value: true, + label: 'd', + }, + e: { + isLeaf: true, + type: 'array', + value: [1, 2, 3], + label: 'e', + }, + }; + expect(buildOutputSchemaFromValue(testResult)).toEqual( + expectedOutputSchema, + ); + }); + + it('should handle nested objects', () => { + const testResult = { + user: { + name: 'John', + address: { + street: '123 Main St', + city: 'New York', + }, + }, + }; + + const result = buildOutputSchemaFromValue(testResult); + + expect(result.user).toEqual({ + isLeaf: false, + type: 'object', + label: 'user', + value: { + name: { + isLeaf: true, + type: 'string', + value: 'John', + label: 'name', + }, + address: { + isLeaf: false, + type: 'object', + label: 'address', + value: { + street: { + isLeaf: true, + type: 'string', + value: '123 Main St', + label: 'street', + }, + city: { + isLeaf: true, + type: 'string', + value: 'New York', + label: 'city', + }, + }, + }, + }, + }); + }); + + it('should return empty object for empty input', () => { + expect(buildOutputSchemaFromValue({})).toEqual({}); + }); +}); diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/navigateOutputSchemaProperty.test.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/navigateOutputSchemaProperty.test.ts new file mode 100644 index 0000000000..690a57dfcb --- /dev/null +++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/__tests__/navigateOutputSchemaProperty.test.ts @@ -0,0 +1,123 @@ +import { type BaseOutputSchemaV2 } from '../../types/base-output-schema.type'; +import { navigateOutputSchemaProperty } from '../navigateOutputSchemaProperty'; + +describe('navigateOutputSchemaProperty', () => { + const testSchema: BaseOutputSchemaV2 = { + result: { + isLeaf: false, + type: 'object', + label: 'result', + value: { + items: { + isLeaf: true, + type: 'array', + label: 'items', + value: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ], + }, + metadata: { + isLeaf: false, + type: 'object', + label: 'metadata', + value: { + count: { + isLeaf: true, + type: 'number', + label: 'count', + value: 2, + }, + }, + }, + }, + }, + name: { + isLeaf: true, + type: 'string', + label: 'name', + value: 'Test', + }, + }; + + it('should navigate to direct property', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: ['name'], + }); + + expect(result).toEqual({ + isLeaf: true, + type: 'string', + label: 'name', + value: 'Test', + }); + }); + + it('should navigate to nested property', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: ['result', 'items'], + }); + + expect(result).toEqual({ + isLeaf: true, + type: 'array', + label: 'items', + value: [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, + ], + }); + }); + + it('should navigate to deeply nested property', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: ['result', 'metadata', 'count'], + }); + + expect(result).toEqual({ + isLeaf: true, + type: 'number', + label: 'count', + value: 2, + }); + }); + + it('should return undefined for missing property', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: ['nonexistent'], + }); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for missing nested property', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: ['result', 'nonexistent'], + }); + + expect(result).toBeUndefined(); + }); + + it('should return undefined for empty property path', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: [], + }); + + expect(result).toBeUndefined(); + }); + + it('should return undefined when trying to navigate through a leaf', () => { + const result = navigateOutputSchemaProperty({ + schema: testSchema, + propertyPath: ['name', 'invalid'], + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/buildOutputSchemaFromValue.ts similarity index 80% rename from packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts rename to packages/twenty-shared/src/workflow/workflow-schema/utils/buildOutputSchemaFromValue.ts index 37a726e5fc..132cf8cbc6 100644 --- a/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionOutputSchema.ts +++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/buildOutputSchemaFromValue.ts @@ -1,9 +1,9 @@ +import { isDefined } from '@/utils'; import { type BaseOutputSchemaV2, type LeafType, -} from '@/workflow/workflow-variables/types/BaseOutputSchemaV2'; +} from '@/workflow/workflow-schema/types/base-output-schema.type'; import { isObject } from '@sniptt/guards'; -import { isDefined } from 'twenty-shared/utils'; const getValueType = (value: any): LeafType => { if (!isDefined(value) || value === null) { @@ -24,7 +24,9 @@ const getValueType = (value: any): LeafType => { return 'unknown'; }; -export const getFunctionOutputSchema = (testResult: object) => { +export const buildOutputSchemaFromValue = ( + testResult: object, +): BaseOutputSchemaV2 => { return testResult ? Object.entries(testResult).reduce( (acc: BaseOutputSchemaV2, [key, value]) => { @@ -33,7 +35,7 @@ export const getFunctionOutputSchema = (testResult: object) => { isLeaf: false, type: 'object', label: key, - value: getFunctionOutputSchema(value), + value: buildOutputSchemaFromValue(value), }; } else { acc[key] = { diff --git a/packages/twenty-shared/src/workflow/workflow-schema/utils/navigateOutputSchemaProperty.ts b/packages/twenty-shared/src/workflow/workflow-schema/utils/navigateOutputSchemaProperty.ts new file mode 100644 index 0000000000..bab0b0a251 --- /dev/null +++ b/packages/twenty-shared/src/workflow/workflow-schema/utils/navigateOutputSchemaProperty.ts @@ -0,0 +1,39 @@ +import { isDefined } from '@/utils'; +import { + type BaseOutputSchemaV2, + type Leaf, + type Node, +} from '@/workflow/workflow-schema/types/base-output-schema.type'; + +export const navigateOutputSchemaProperty = ({ + schema, + propertyPath, +}: { + schema: BaseOutputSchemaV2; + propertyPath: string[]; +}): Leaf | Node | undefined => { + if (propertyPath.length === 0) { + return undefined; + } + + let currentSchema: BaseOutputSchemaV2 = schema; + let currentField: Leaf | Node | undefined; + + for (const pathSegment of propertyPath) { + currentField = currentSchema[pathSegment]; + + if (!isDefined(currentField)) { + return undefined; + } + + if (currentField.isLeaf) { + const isLastSegment = + pathSegment === propertyPath[propertyPath.length - 1]; + return isLastSegment ? currentField : undefined; + } + + currentSchema = currentField.value; + } + + return currentField; +};