Fix variable search for iterators (#14742)
Add specific search util for iterators. Also making workflow version id optional in compute step output schema endpoint to avoid breaking changes on next release. Before https://github.com/user-attachments/assets/2ceb5b38-4c0a-4d38-8f57-0e4467dcfd93 After https://github.com/user-attachments/assets/3ce503ac-5299-4bca-906f-e0fef379df7c
This commit is contained in:
@@ -445,7 +445,7 @@ export type ComputeStepOutputSchemaInput = {
|
||||
/** Step JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
workflowVersionId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export enum ConfigSource {
|
||||
|
||||
@@ -445,7 +445,7 @@ export type ComputeStepOutputSchemaInput = {
|
||||
/** Step JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
workflowVersionId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export enum ConfigSource {
|
||||
|
||||
+1
-7
@@ -1,10 +1,4 @@
|
||||
import { type RecordOutputSchemaV2 } from './RecordOutputSchemaV2';
|
||||
|
||||
type RecordNode = {
|
||||
isLeaf: false;
|
||||
label: string;
|
||||
value: RecordOutputSchemaV2;
|
||||
};
|
||||
import { type RecordNode } from '@/workflow/workflow-variables/types/RecordNode';
|
||||
|
||||
export type FindRecordsOutputSchema = {
|
||||
first: RecordNode;
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { type Leaf } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import { type RecordNode } from '@/workflow/workflow-variables/types/RecordNode';
|
||||
|
||||
export type IteratorOutputSchema = {
|
||||
// TODO(t.trompette): add support for node items that are not records
|
||||
currentItem: RecordNode | Leaf;
|
||||
currentItemIndex: number;
|
||||
hasProcessedAllItems: boolean;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
|
||||
export type RecordNode = {
|
||||
isLeaf: false;
|
||||
label: string;
|
||||
value: RecordOutputSchemaV2;
|
||||
};
|
||||
+3
-1
@@ -6,6 +6,7 @@ import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/Bas
|
||||
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';
|
||||
|
||||
@@ -15,7 +16,8 @@ export type OutputSchemaV2 =
|
||||
| FindRecordsOutputSchema
|
||||
| FormOutputSchema
|
||||
| RecordOutputSchemaV2
|
||||
| ManualTriggerOutputSchema;
|
||||
| ManualTriggerOutputSchema
|
||||
| IteratorOutputSchema;
|
||||
|
||||
export type StepOutputSchemaV2 = {
|
||||
id: string;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
type WorkflowActionType,
|
||||
type WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type OutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
import { type IteratorOutputSchema } from '../IteratorOutputSchema';
|
||||
|
||||
export const isIteratorOutputSchema = (
|
||||
stepType: WorkflowActionType | WorkflowTriggerType,
|
||||
schema: OutputSchemaV2,
|
||||
): schema is IteratorOutputSchema => {
|
||||
return stepType === 'ITERATOR';
|
||||
};
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { type IteratorOutputSchema } from '@/workflow/workflow-variables/types/IteratorOutputSchema';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { searchVariableThroughIteratorOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('searchVariableThroughIteratorOutputSchema', () => {
|
||||
const mockRecordSchema: RecordOutputSchemaV2 = {
|
||||
object: {
|
||||
objectMetadataId: 'company-metadata-id',
|
||||
label: 'Company',
|
||||
},
|
||||
fields: {
|
||||
name: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Company Name',
|
||||
value: 'Acme Corp',
|
||||
fieldMetadataId: 'company-name-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
revenue: {
|
||||
isLeaf: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
label: 'Revenue',
|
||||
value: 1000000,
|
||||
fieldMetadataId: 'company-revenue-metadata-id',
|
||||
isCompositeSubField: false,
|
||||
},
|
||||
},
|
||||
_outputSchemaType: 'RECORD',
|
||||
};
|
||||
|
||||
const mockIteratorSchema: IteratorOutputSchema = {
|
||||
currentItem: {
|
||||
isLeaf: false,
|
||||
label: 'Current Item',
|
||||
value: mockRecordSchema,
|
||||
},
|
||||
currentItemIndex: 0,
|
||||
hasProcessedAllItems: false,
|
||||
};
|
||||
|
||||
it('should handle currentItemIndex variable correctly', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: mockIteratorSchema,
|
||||
rawVariableName: '{{step1.currentItemIndex}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Current Item Index',
|
||||
variablePathLabel: 'Iterate Companies > Current Item Index',
|
||||
variableType: FieldMetadataType.NUMBER,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle hasProcessedAllItems variable correctly', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: mockIteratorSchema,
|
||||
rawVariableName: '{{step1.hasProcessedAllItems}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Has Processed All Items',
|
||||
variablePathLabel: 'Iterate Companies > Has Processed All Items',
|
||||
variableType: FieldMetadataType.BOOLEAN,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle currentItem field access correctly', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: mockIteratorSchema,
|
||||
rawVariableName: '{{step1.currentItem.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: 'Company Name',
|
||||
variablePathLabel: 'Iterate Companies > Current Item > Company Name',
|
||||
variableType: FieldMetadataType.TEXT,
|
||||
fieldMetadataId: 'company-name-metadata-id',
|
||||
compositeFieldSubFieldName: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid field name', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: mockIteratorSchema,
|
||||
rawVariableName: '{{step1.currentItem.invalidField}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for invalid iterator result key', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: mockIteratorSchema,
|
||||
rawVariableName: '{{step1.invalid.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when iteratorOutputSchema is undefined', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: undefined as any,
|
||||
rawVariableName: '{{step1.currentItem.name}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when stepId or iteratorResultKey is undefined', () => {
|
||||
const result = searchVariableThroughIteratorOutputSchema({
|
||||
stepName: 'Iterate Companies',
|
||||
iteratorOutputSchema: mockIteratorSchema,
|
||||
rawVariableName: '{{}}',
|
||||
isFullRecord: false,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2';
|
||||
import { type IteratorOutputSchema } from '@/workflow/workflow-variables/types/IteratorOutputSchema';
|
||||
import { searchRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
type IteratorResultKey =
|
||||
| 'currentItem'
|
||||
| 'currentItemIndex'
|
||||
| 'hasProcessedAllItems';
|
||||
|
||||
const parseVariableName = (rawVariableName: string) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||
(_, variableName) => variableName,
|
||||
);
|
||||
|
||||
const parts = variableWithoutBrackets.split('.');
|
||||
const stepId = parts.at(0);
|
||||
const iteratorResultKey = parts.at(1) as IteratorResultKey;
|
||||
const remainingParts = parts.slice(2);
|
||||
|
||||
return {
|
||||
stepId,
|
||||
iteratorResultKey,
|
||||
fieldName: remainingParts.at(-1),
|
||||
pathSegments: remainingParts.slice(0, -1),
|
||||
};
|
||||
};
|
||||
|
||||
export const searchVariableThroughIteratorOutputSchema = ({
|
||||
stepName,
|
||||
iteratorOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord = false,
|
||||
}: {
|
||||
stepName: string;
|
||||
iteratorOutputSchema: IteratorOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord?: boolean;
|
||||
}): VariableSearchResult => {
|
||||
if (!isDefined(iteratorOutputSchema)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const { stepId, iteratorResultKey, fieldName, pathSegments } =
|
||||
parseVariableName(rawVariableName);
|
||||
|
||||
if (!isDefined(stepId) || !isDefined(iteratorResultKey)) {
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (iteratorResultKey === 'currentItemIndex') {
|
||||
return {
|
||||
variableLabel: 'Current Item Index',
|
||||
variablePathLabel: `${stepName} > Current Item Index`,
|
||||
variableType: FieldMetadataType.NUMBER,
|
||||
};
|
||||
}
|
||||
|
||||
if (iteratorResultKey === 'hasProcessedAllItems') {
|
||||
return {
|
||||
variableLabel: 'Has Processed All Items',
|
||||
variablePathLabel: `${stepName} > Has Processed All Items`,
|
||||
variableType: FieldMetadataType.BOOLEAN,
|
||||
};
|
||||
}
|
||||
|
||||
if (iteratorResultKey === 'currentItem') {
|
||||
const schema = iteratorOutputSchema.currentItem.value;
|
||||
if (isRecordOutputSchemaV2(schema) && isDefined(fieldName)) {
|
||||
return searchRecordOutputSchema({
|
||||
stepName: `${stepName} > Current Item`,
|
||||
recordOutputSchema: schema,
|
||||
path: pathSegments,
|
||||
selectedField: fieldName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
variableLabel: 'Current Item',
|
||||
variablePathLabel: `${stepName} > Current Item`,
|
||||
variableType: schema.type,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
variableLabel: undefined,
|
||||
variablePathLabel: undefined,
|
||||
};
|
||||
};
|
||||
+11
@@ -6,6 +6,7 @@ import { isCodeOutputSchema } from '@/workflow/workflow-variables/types/guards/i
|
||||
import { isDatabaseEventTriggerOutputSchema } from '@/workflow/workflow-variables/types/guards/isDatabaseEventTriggerOutputSchema';
|
||||
import { isFindRecordsOutputSchema } from '@/workflow/workflow-variables/types/guards/isFindRecordsOutputSchema';
|
||||
import { isFormOutputSchema } from '@/workflow/workflow-variables/types/guards/isFormOutputSchema';
|
||||
import { isIteratorOutputSchema } from '@/workflow/workflow-variables/types/guards/isIteratorOutputSchema';
|
||||
import { isManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/guards/isManualTriggerOutputSchema';
|
||||
import { isRecordStepOutputSchema } from '@/workflow/workflow-variables/types/guards/isRecordStepOutputSchema';
|
||||
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
@@ -14,6 +15,7 @@ import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-varia
|
||||
import { searchVariableThroughCodeOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema';
|
||||
import { searchVariableThroughFindRecordsOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema';
|
||||
import { searchVariableThroughFormOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFormOutputSchema';
|
||||
import { searchVariableThroughIteratorOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughIteratorOutputSchema';
|
||||
import { searchVariableThroughManualTriggerOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughManualTriggerOutputSchema';
|
||||
import { searchVariableThroughRecordEventOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordEventOutputSchema';
|
||||
import { searchVariableThroughRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
@@ -85,6 +87,15 @@ export const searchVariableThroughOutputSchemaV2 = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (isIteratorOutputSchema(stepType, stepOutputSchema.outputSchema)) {
|
||||
return searchVariableThroughIteratorOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
iteratorOutputSchema: stepOutputSchema.outputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return searchVariableThroughBaseOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
baseOutputSchema: stepOutputSchema.outputSchema,
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ export class ComputeStepOutputSchemaInput {
|
||||
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'Workflow version ID',
|
||||
nullable: false,
|
||||
nullable: true,
|
||||
})
|
||||
workflowVersionId: string;
|
||||
workflowVersionId?: string;
|
||||
}
|
||||
|
||||
+11
-7
@@ -47,7 +47,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
}: {
|
||||
step: WorkflowTrigger | WorkflowAction;
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
workflowVersionId?: string;
|
||||
}): Promise<OutputSchema> {
|
||||
const stepType = step.type;
|
||||
|
||||
@@ -108,8 +108,8 @@ export class WorkflowSchemaWorkspaceService {
|
||||
return {
|
||||
currentItem: await this.computeLoopCurrentItemOutputSchema({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}),
|
||||
currentItemIndex: {
|
||||
label: 'Current Item Index',
|
||||
@@ -308,12 +308,12 @@ export class WorkflowSchemaWorkspaceService {
|
||||
|
||||
private async computeLoopCurrentItemOutputSchema({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
items: string | undefined | unknown[];
|
||||
workflowVersionId: string;
|
||||
workspaceId: string;
|
||||
workflowVersionId?: string;
|
||||
}): Promise<Leaf | Node> {
|
||||
if (!isDefined(items)) {
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
@@ -322,8 +322,8 @@ export class WorkflowSchemaWorkspaceService {
|
||||
if (isString(items) && isValidVariable(items)) {
|
||||
return this.computeIteratorCurrentItemFromVariable({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -332,13 +332,17 @@ export class WorkflowSchemaWorkspaceService {
|
||||
|
||||
private async computeIteratorCurrentItemFromVariable({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
items: string;
|
||||
workflowVersionId: string;
|
||||
workspaceId: string;
|
||||
workflowVersionId?: string;
|
||||
}): Promise<Leaf | Node> {
|
||||
if (!isDefined(workflowVersionId)) {
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
|
||||
const workflowVersion =
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
|
||||
workflowVersionId,
|
||||
|
||||
Reference in New Issue
Block a user