8726 workflow add a test button in workflow code step (#9016)
- add test button to workflow code step - add test tab to workflow code step https://github.com/user-attachments/assets/e180a827-7321-49a2-8026-88490c557da2  
This commit is contained in:
@ -0,0 +1,44 @@
|
||||
import { InputSchema } from '@/workflow/types/InputSchema';
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';
|
||||
|
||||
describe('getDefaultFunctionInputFromInputSchema', () => {
|
||||
it('should init function input properly', () => {
|
||||
const inputSchema = [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
a: {
|
||||
type: 'string',
|
||||
},
|
||||
b: {
|
||||
type: 'number',
|
||||
},
|
||||
c: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
d: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
da: { type: 'string', enum: ['my', 'enum'] },
|
||||
db: { type: 'number' },
|
||||
},
|
||||
},
|
||||
e: { type: 'object' },
|
||||
},
|
||||
},
|
||||
] as InputSchema;
|
||||
const expectedResult = [
|
||||
{
|
||||
a: null,
|
||||
b: null,
|
||||
c: [],
|
||||
d: { da: 'my', db: null },
|
||||
e: {},
|
||||
},
|
||||
];
|
||||
expect(getDefaultFunctionInputFromInputSchema(inputSchema)).toEqual(
|
||||
expectedResult,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,50 @@
|
||||
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
||||
|
||||
describe('getFunctionInputFromSourceCode', () => {
|
||||
it('should return empty input if not parameter', () => {
|
||||
const fileContent = 'function testFunction() { return }';
|
||||
const result = getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
it('should return first input if multiple parameters', () => {
|
||||
const fileContent =
|
||||
'function testFunction(params1: {}, params2: {}) { return }';
|
||||
const result = getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
it('should return empty input if wrong parameter', () => {
|
||||
const fileContent = 'function testFunction(params: string) { return }';
|
||||
const result = getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
it('should return input from source code', () => {
|
||||
const fileContent = `
|
||||
function testFunction(
|
||||
params: {
|
||||
param1: string;
|
||||
param2: number;
|
||||
param3: boolean;
|
||||
param4: object;
|
||||
param5: { subParam1: string };
|
||||
param6: "my" | "enum";
|
||||
param7: string[];
|
||||
}
|
||||
): void {
|
||||
return
|
||||
}
|
||||
`;
|
||||
|
||||
const result = getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({
|
||||
param1: null,
|
||||
param2: null,
|
||||
param3: null,
|
||||
param4: {},
|
||||
param5: {
|
||||
subParam1: null,
|
||||
},
|
||||
param6: 'my',
|
||||
param7: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
import { getFunctionInputSchema } from '@/serverless-functions/utils/getFunctionInputSchema';
|
||||
|
||||
describe('getFunctionInputSchema', () => {
|
||||
it('should analyze a simple function correctly', () => {
|
||||
const fileContent = `
|
||||
function testFunction(param1: string, param2: number): void {
|
||||
return;
|
||||
}
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([{ type: 'string' }, { type: 'number' }]);
|
||||
});
|
||||
|
||||
it('should analyze a arrow function correctly', () => {
|
||||
const fileContent = `
|
||||
export const main = async (
|
||||
param1: string,
|
||||
param2: number,
|
||||
): Promise<object> => {
|
||||
return param1;
|
||||
};
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([{ type: 'string' }, { type: 'number' }]);
|
||||
});
|
||||
|
||||
it('should analyze a complex function correctly', () => {
|
||||
const fileContent = `
|
||||
function testFunction(
|
||||
params: {
|
||||
param1: string;
|
||||
param2: number;
|
||||
param3: boolean;
|
||||
param4: object;
|
||||
param5: { subParam1: string };
|
||||
param6: "my" | "enum";
|
||||
param7: string[];
|
||||
}
|
||||
): void {
|
||||
return
|
||||
}
|
||||
`;
|
||||
const result = getFunctionInputSchema(fileContent);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
param1: { type: 'string' },
|
||||
param2: { type: 'number' },
|
||||
param3: { type: 'boolean' },
|
||||
param4: { type: 'object' },
|
||||
param5: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
subParam1: { type: 'string' },
|
||||
},
|
||||
},
|
||||
param6: { type: 'string', enum: ['my', 'enum'] },
|
||||
param7: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
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 },
|
||||
b: { isLeaf: true, type: 'string', value: 'b' },
|
||||
c: {
|
||||
isLeaf: false,
|
||||
value: { cc: { isLeaf: true, type: 'number', value: 1 } },
|
||||
},
|
||||
d: { isLeaf: true, type: 'boolean', value: true },
|
||||
e: { isLeaf: true, type: 'array', value: [1, 2, 3] },
|
||||
};
|
||||
expect(getFunctionOutputSchema(testResult)).toEqual(expectedOutputSchema);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '../mergeDefaultFunctionInputAndFunctionInput';
|
||||
|
||||
describe('mergeDefaultFunctionInputAndFunctionInput', () => {
|
||||
it('should merge properly', () => {
|
||||
const newInput = {
|
||||
a: null,
|
||||
b: null,
|
||||
c: { cc: null },
|
||||
d: null,
|
||||
e: { ee: null },
|
||||
};
|
||||
const oldInput = { a: 'a', c: 'c', d: { da: null }, e: { ee: 'ee' } };
|
||||
const expectedResult = {
|
||||
a: 'a',
|
||||
b: null,
|
||||
c: { cc: null },
|
||||
d: null,
|
||||
e: { ee: 'ee' },
|
||||
};
|
||||
expect(
|
||||
mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: newInput,
|
||||
oldInput: oldInput,
|
||||
}),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { InputSchema } from '@/workflow/types/InputSchema';
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const getDefaultFunctionInputFromInputSchema = (
|
||||
inputSchema: InputSchema,
|
||||
): FunctionInput => {
|
||||
return inputSchema.map((param) => {
|
||||
if (['string', 'number', 'boolean'].includes(param.type)) {
|
||||
return param.enum && param.enum.length > 0 ? param.enum[0] : null;
|
||||
} else if (param.type === 'object') {
|
||||
const result: FunctionInput = {};
|
||||
if (isDefined(param.properties)) {
|
||||
Object.entries(param.properties).forEach(([key, val]) => {
|
||||
result[key] = getDefaultFunctionInputFromInputSchema([val])[0];
|
||||
});
|
||||
}
|
||||
return result;
|
||||
} else if (param.type === 'array' && isDefined(param.items)) {
|
||||
return [];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';
|
||||
import { getFunctionInputSchema } from '@/serverless-functions/utils/getFunctionInputSchema';
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const getFunctionInputFromSourceCode = (
|
||||
sourceCode?: string,
|
||||
): FunctionInput => {
|
||||
if (!isDefined(sourceCode)) {
|
||||
throw new Error('Source code is not defined');
|
||||
}
|
||||
|
||||
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
||||
|
||||
const result = getDefaultFunctionInputFromInputSchema(functionInputSchema)[0];
|
||||
|
||||
if (!isObject(result)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@ -0,0 +1,137 @@
|
||||
import {
|
||||
ArrayTypeNode,
|
||||
ArrowFunction,
|
||||
createSourceFile,
|
||||
FunctionDeclaration,
|
||||
FunctionLikeDeclaration,
|
||||
LiteralTypeNode,
|
||||
PropertySignature,
|
||||
ScriptTarget,
|
||||
StringLiteral,
|
||||
SyntaxKind,
|
||||
TypeNode,
|
||||
Node,
|
||||
UnionTypeNode,
|
||||
VariableStatement,
|
||||
} from 'typescript';
|
||||
import { InputSchema, InputSchemaProperty } from '@/workflow/types/InputSchema';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const getTypeString = (typeNode: TypeNode): InputSchemaProperty => {
|
||||
switch (typeNode.kind) {
|
||||
case SyntaxKind.NumberKeyword:
|
||||
return { type: 'number' };
|
||||
case SyntaxKind.StringKeyword:
|
||||
return { type: 'string' };
|
||||
case SyntaxKind.BooleanKeyword:
|
||||
return { type: 'boolean' };
|
||||
case SyntaxKind.ArrayType:
|
||||
return {
|
||||
type: 'array',
|
||||
items: getTypeString((typeNode as ArrayTypeNode).elementType),
|
||||
};
|
||||
case SyntaxKind.ObjectKeyword:
|
||||
return { type: 'object' };
|
||||
case SyntaxKind.TypeLiteral: {
|
||||
const properties: InputSchemaProperty['properties'] = {};
|
||||
|
||||
(typeNode as any).members.forEach((member: PropertySignature) => {
|
||||
if (isDefined(member.name) && isDefined(member.type)) {
|
||||
const memberName = (member.name as any).text;
|
||||
|
||||
properties[memberName] = getTypeString(member.type);
|
||||
}
|
||||
});
|
||||
|
||||
return { type: 'object', properties };
|
||||
}
|
||||
case SyntaxKind.UnionType: {
|
||||
const unionNode = typeNode as UnionTypeNode;
|
||||
const enumValues: string[] = [];
|
||||
|
||||
let isEnum = true;
|
||||
|
||||
unionNode.types.forEach((subType) => {
|
||||
if (subType.kind === SyntaxKind.LiteralType) {
|
||||
const literal = (subType as LiteralTypeNode).literal;
|
||||
|
||||
if (literal.kind === SyntaxKind.StringLiteral) {
|
||||
enumValues.push((literal as StringLiteral).text);
|
||||
} else {
|
||||
isEnum = false;
|
||||
}
|
||||
} else {
|
||||
isEnum = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isEnum) {
|
||||
return { type: 'string', enum: enumValues };
|
||||
}
|
||||
|
||||
return { type: 'unknown' };
|
||||
}
|
||||
default:
|
||||
return { type: 'unknown' };
|
||||
}
|
||||
};
|
||||
|
||||
const computeFunctionParameters = (
|
||||
funcNode: FunctionDeclaration | FunctionLikeDeclaration | ArrowFunction,
|
||||
schema: InputSchema,
|
||||
): InputSchema => {
|
||||
const params = funcNode.parameters;
|
||||
|
||||
return params.reduce((updatedSchema, param) => {
|
||||
const typeNode = param.type;
|
||||
|
||||
if (isDefined(typeNode)) {
|
||||
return [...updatedSchema, getTypeString(typeNode)];
|
||||
} else {
|
||||
return [...updatedSchema, { type: 'unknown' }];
|
||||
}
|
||||
}, schema);
|
||||
};
|
||||
|
||||
const extractFunctions = (node: Node): FunctionLikeDeclaration[] => {
|
||||
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
||||
return [node as FunctionDeclaration];
|
||||
}
|
||||
|
||||
if (node.kind === SyntaxKind.VariableStatement) {
|
||||
const varStatement = node as VariableStatement;
|
||||
return varStatement.declarationList.declarations
|
||||
.filter(
|
||||
(declaration) =>
|
||||
isDefined(declaration.initializer) &&
|
||||
declaration.initializer.kind === SyntaxKind.ArrowFunction,
|
||||
)
|
||||
.map((declaration) => declaration.initializer as ArrowFunction);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getFunctionInputSchema = (fileContent: string): InputSchema => {
|
||||
const sourceFile = createSourceFile(
|
||||
'temp.ts',
|
||||
fileContent,
|
||||
ScriptTarget.ESNext,
|
||||
true,
|
||||
);
|
||||
let schema: InputSchema = [];
|
||||
|
||||
sourceFile.forEachChild((node) => {
|
||||
if (
|
||||
node.kind === SyntaxKind.FunctionDeclaration ||
|
||||
node.kind === SyntaxKind.VariableStatement
|
||||
) {
|
||||
const functions = extractFunctions(node);
|
||||
functions.forEach((func) => {
|
||||
schema = computeFunctionParameters(func, schema);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return schema;
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { BaseOutputSchema } from '@/workflow/search-variables/types/StepOutputSchema';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { InputSchemaPropertyType } from '@/workflow/types/InputSchema';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const getValueType = (value: any): InputSchemaPropertyType => {
|
||||
if (!isDefined(value) || value === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return 'string';
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return 'number';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return 'boolean';
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return 'array';
|
||||
}
|
||||
if (isObject(value)) {
|
||||
return 'object';
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const getFunctionOutputSchema = (testResult: object) => {
|
||||
return testResult
|
||||
? Object.entries(testResult).reduce(
|
||||
(acc: BaseOutputSchema, [key, value]) => {
|
||||
if (isObject(value) && !Array.isArray(value)) {
|
||||
acc[key] = {
|
||||
isLeaf: false,
|
||||
value: getFunctionOutputSchema(value),
|
||||
};
|
||||
} else {
|
||||
acc[key] = {
|
||||
isLeaf: true,
|
||||
value,
|
||||
type: getValueType(value),
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: {};
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import { FunctionInput } from '@/workflow/types/FunctionInput';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
|
||||
export const mergeDefaultFunctionInputAndFunctionInput = ({
|
||||
newInput,
|
||||
oldInput,
|
||||
}: {
|
||||
newInput: FunctionInput;
|
||||
oldInput: FunctionInput;
|
||||
}): FunctionInput => {
|
||||
const result: FunctionInput = {};
|
||||
|
||||
for (const key of Object.keys(newInput)) {
|
||||
const newValue = newInput[key];
|
||||
const oldValue = oldInput[key];
|
||||
|
||||
if (!(key in oldInput)) {
|
||||
result[key] = newValue;
|
||||
} else if (newValue === null && isObject(oldValue)) {
|
||||
result[key] = null;
|
||||
} else if (isObject(newValue)) {
|
||||
result[key] = mergeDefaultFunctionInputAndFunctionInput({
|
||||
newInput: newValue,
|
||||
oldInput: isObject(oldValue) ? oldValue : {},
|
||||
});
|
||||
} else {
|
||||
result[key] = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user