Infer array current item schema (#15115)
This PR allows to infer the schema of the current item of an iterator step: - iterator step receive a variable - added an util that navigate to the array in schema - navigateOutputSchemaProperty - use the array value in schema to generate a new schema - used the existing getFunctionOutputSchema that I renamed and moved to twenty-shared Also cleaned a bit the existing schema for AI. Before https://github.com/user-attachments/assets/9767fc89-3524-4bfb-b1ab-8abe92084767 After https://github.com/user-attachments/assets/3650c1d2-14f2-44f9-b10c-e649fe04128d
This commit is contained in:
@@ -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<BaseOutputSchemaDeprecated>(
|
||||
const newOutputSchema = fields.reduce<AiAgentOutputSchema>(
|
||||
(schema, field) => {
|
||||
if (isDefined(field.name)) {
|
||||
schema[field.name] = {
|
||||
|
||||
-53
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 },
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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', () => {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ export const WorkflowEditActionIterator = ({
|
||||
? defaultItems
|
||||
: isArray(defaultItems)
|
||||
? stringifyArrayItems(defaultItems)
|
||||
: stringifyArrayItems(JSON.parse(defaultItems));
|
||||
: [];
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
items: parsedItems,
|
||||
|
||||
+4
-2
@@ -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(
|
||||
{
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+17
@@ -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<string, AiAgentLeaf | AiAgentNode>;
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow';
|
||||
|
||||
export type OpenStepOutputSchema = BaseOutputSchemaV2;
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+1
-1
@@ -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', () => {
|
||||
|
||||
+1
-18
@@ -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({
|
||||
|
||||
+1
-14
@@ -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({
|
||||
|
||||
+2
-5
@@ -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;
|
||||
|
||||
+39
-21
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
-3
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+16
-3
@@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -27,6 +27,5 @@ export const searchVariableThroughManualTriggerOutputSchema = ({
|
||||
stepName,
|
||||
baseOutputSchema: manualTriggerOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
};
|
||||
|
||||
-2
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+55
@@ -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']);
|
||||
});
|
||||
});
|
||||
+210
@@ -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);
|
||||
});
|
||||
});
|
||||
+14
@@ -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);
|
||||
};
|
||||
+65
@@ -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,
|
||||
};
|
||||
};
|
||||
+31
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
+3
-12
@@ -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<string, Leaf | Node>;
|
||||
|
||||
export type LeafDeprecated = {
|
||||
isLeaf: true;
|
||||
type: InputSchemaPropertyType | undefined;
|
||||
label: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export type BaseOutputSchemaDeprecated = Record<string, LeafDeprecated | Node>;
|
||||
+106
@@ -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({});
|
||||
});
|
||||
});
|
||||
+123
@@ -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();
|
||||
});
|
||||
});
|
||||
+6
-4
@@ -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] = {
|
||||
+39
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user