Support define is tool logic function (#17926)
- supports isTool and timeout settings in defineLogicFunction in apps and in setting tabs definition - compute for all toolInputSchema for logic funciton, in settings and in code steps <img width="991" height="802" alt="image" src="https://github.com/user-attachments/assets/05dc1221-cac9-45a3-87b0-3b13161446fd" />
This commit is contained in:
@@ -92,7 +92,6 @@ export type {
|
||||
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,
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { canObjectBeManagedByWorkflow } from '@/workflow/utils/canObjectBeManagedByWorkflow';
|
||||
|
||||
describe('canObjectBeManagedByWorkflow', () => {
|
||||
it('should return true for non-system, non-excluded objects', () => {
|
||||
expect(
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: 'company',
|
||||
isSystem: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for system objects', () => {
|
||||
expect(
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: 'company',
|
||||
isSystem: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for workflow object', () => {
|
||||
expect(
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: 'workflow',
|
||||
isSystem: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for workflowVersion object', () => {
|
||||
expect(
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: 'workflowVersion',
|
||||
isSystem: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for workflowRun object', () => {
|
||||
expect(
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: 'workflowRun',
|
||||
isSystem: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for dashboard object', () => {
|
||||
expect(
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: 'dashboard',
|
||||
isSystem: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { extractRawVariableNamePart } from '@/workflow/utils/extractRawVariableNameParts';
|
||||
|
||||
describe('extractRawVariableNamePart', () => {
|
||||
it('should extract stepId from a variable name', () => {
|
||||
const result = extractRawVariableNamePart({
|
||||
rawVariableName: '{{step1.output.field}}',
|
||||
part: 'stepId',
|
||||
});
|
||||
|
||||
expect(result).toBe('step1');
|
||||
});
|
||||
|
||||
it('should extract selectedField from a variable name', () => {
|
||||
const result = extractRawVariableNamePart({
|
||||
rawVariableName: '{{step1.output.field}}',
|
||||
part: 'selectedField',
|
||||
});
|
||||
|
||||
expect(result).toBe('field');
|
||||
});
|
||||
|
||||
it('should handle deeply nested variable names', () => {
|
||||
const result = extractRawVariableNamePart({
|
||||
rawVariableName: '{{trigger.output.nested.deep}}',
|
||||
part: 'selectedField',
|
||||
});
|
||||
|
||||
expect(result).toBe('deep');
|
||||
});
|
||||
});
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { parseBooleanFromStringValue } from '@/workflow/utils/parseBooleanFromStringValue';
|
||||
|
||||
describe('parseBooleanFromStringValue', () => {
|
||||
it('should return true for "true" string', () => {
|
||||
expect(parseBooleanFromStringValue('true')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for "false" string', () => {
|
||||
expect(parseBooleanFromStringValue('false')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return the original value for non-boolean strings', () => {
|
||||
expect(parseBooleanFromStringValue('hello')).toBe('hello');
|
||||
});
|
||||
|
||||
it('should return the original value for numbers', () => {
|
||||
expect(parseBooleanFromStringValue(42)).toBe(42);
|
||||
});
|
||||
|
||||
it('should return the original value for null', () => {
|
||||
expect(parseBooleanFromStringValue(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import { parseDataFromContentType } from '@/workflow/utils/parseDataFromContentType';
|
||||
|
||||
describe('parseDataFromContentType', () => {
|
||||
describe('application/json', () => {
|
||||
it('should stringify object data', () => {
|
||||
const result = parseDataFromContentType(
|
||||
{ key: 'value' },
|
||||
'application/json',
|
||||
);
|
||||
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
|
||||
it('should return string data as-is', () => {
|
||||
const result = parseDataFromContentType(
|
||||
'{"key":"value"}',
|
||||
'application/json',
|
||||
);
|
||||
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('text/plain', () => {
|
||||
it('should return string data as-is', () => {
|
||||
const result = parseDataFromContentType('hello', 'text/plain');
|
||||
|
||||
expect(result).toBe('hello');
|
||||
});
|
||||
|
||||
it('should convert object to key=val format', () => {
|
||||
const result = parseDataFromContentType(
|
||||
{ name: 'Alice', age: '30' },
|
||||
'text/plain',
|
||||
);
|
||||
|
||||
expect(result).toContain('name=Alice');
|
||||
expect(result).toContain('age=30');
|
||||
});
|
||||
});
|
||||
|
||||
describe('application/x-www-form-urlencoded', () => {
|
||||
it('should convert object to URL-encoded format', () => {
|
||||
const result = parseDataFromContentType(
|
||||
{ key: 'value', foo: 'bar' },
|
||||
'application/x-www-form-urlencoded',
|
||||
);
|
||||
|
||||
expect(result).toContain('key=value');
|
||||
expect(result).toContain('foo=bar');
|
||||
});
|
||||
|
||||
it('should parse JSON string data', () => {
|
||||
const result = parseDataFromContentType(
|
||||
'{"key":"value"}',
|
||||
'application/x-www-form-urlencoded',
|
||||
);
|
||||
|
||||
expect(result).toContain('key=value');
|
||||
});
|
||||
|
||||
it('should handle non-JSON string data', () => {
|
||||
const result = parseDataFromContentType(
|
||||
'raw-string',
|
||||
'application/x-www-form-urlencoded',
|
||||
);
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('multipart/form-data', () => {
|
||||
it('should return FormData for object data', () => {
|
||||
const result = parseDataFromContentType(
|
||||
{ key: 'value' },
|
||||
'multipart/form-data',
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(FormData);
|
||||
});
|
||||
|
||||
it('should parse JSON string into FormData', () => {
|
||||
const result = parseDataFromContentType(
|
||||
'{"key":"value"}',
|
||||
'multipart/form-data',
|
||||
);
|
||||
|
||||
expect(result).toBeInstanceOf(FormData);
|
||||
});
|
||||
|
||||
it('should throw for invalid JSON string', () => {
|
||||
expect(() =>
|
||||
parseDataFromContentType('not-json', 'multipart/form-data'),
|
||||
).toThrow('String data for FormData must be valid JSON');
|
||||
});
|
||||
});
|
||||
|
||||
describe('default (no content type)', () => {
|
||||
it('should default to JSON parsing when content type is undefined', () => {
|
||||
const result = parseDataFromContentType({ key: 'value' });
|
||||
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown content type', () => {
|
||||
it('should default to JSON parsing', () => {
|
||||
const result = parseDataFromContentType(
|
||||
{ key: 'value' },
|
||||
'application/xml',
|
||||
);
|
||||
|
||||
expect(result).toBe('{"key":"value"}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,5 +5,4 @@ export type {
|
||||
Node,
|
||||
NodeType,
|
||||
} from './types/base-output-schema.type';
|
||||
export { buildOutputSchemaFromValue } from './utils/buildOutputSchemaFromValue';
|
||||
export { navigateOutputSchemaProperty } from './utils/navigateOutputSchemaProperty';
|
||||
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
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({});
|
||||
});
|
||||
});
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
import { isDefined } from '@/utils';
|
||||
import {
|
||||
type BaseOutputSchemaV2,
|
||||
type LeafType,
|
||||
} from '@/workflow/workflow-schema/types/base-output-schema.type';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
|
||||
const getValueType = (value: any): LeafType => {
|
||||
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';
|
||||
}
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const buildOutputSchemaFromValue = (
|
||||
testResult: object,
|
||||
): BaseOutputSchemaV2 => {
|
||||
return testResult
|
||||
? Object.entries(testResult).reduce(
|
||||
(acc: BaseOutputSchemaV2, [key, value]) => {
|
||||
if (isObject(value) && !Array.isArray(value)) {
|
||||
acc[key] = {
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
label: key,
|
||||
value: buildOutputSchemaFromValue(value),
|
||||
};
|
||||
} else {
|
||||
acc[key] = {
|
||||
isLeaf: true,
|
||||
value,
|
||||
type: getValueType(value),
|
||||
label: key,
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
)
|
||||
: {};
|
||||
};
|
||||
Reference in New Issue
Block a user