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:
Thomas Trompette
2025-10-15 18:15:08 +02:00
committed by GitHub
parent b16ab1b7c9
commit d3f3f991a5
42 changed files with 774 additions and 170 deletions
@@ -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] = {
@@ -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;
@@ -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,
@@ -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,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,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,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,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) {
@@ -58,7 +58,7 @@ export const WorkflowEditActionIterator = ({
? defaultItems
: isArray(defaultItems)
? stringifyArrayItems(defaultItems)
: stringifyArrayItems(JSON.parse(defaultItems));
: [];
const [formData, setFormData] = useState({
items: parsedItems,
@@ -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(
{
@@ -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';
@@ -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,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,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,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,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,3 +1,3 @@
import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
import { type BaseOutputSchemaV2 } from 'twenty-shared/workflow';
export type OpenStepOutputSchema = BaseOutputSchemaV2;
@@ -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,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,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,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,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({
@@ -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;
@@ -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,
});
};
@@ -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,
});
};
@@ -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',
};
}
@@ -27,6 +27,5 @@ export const searchVariableThroughManualTriggerOutputSchema = ({
stepName,
baseOutputSchema: manualTriggerOutputSchema,
rawVariableName,
isFullRecord,
});
};
@@ -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,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,
@@ -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']);
});
});
@@ -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);
});
});
@@ -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);
};
@@ -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,
};
};
@@ -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';
@@ -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>;
@@ -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({});
});
});
@@ -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();
});
});
@@ -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] = {
@@ -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;
};