Rename serverlessFunction to logicFunction (#17494)
## Summary Rename "Serverless Function" to "Logic Function" across the codebase for clearer naming. ### Environment Variable Changes | Old | New | |-----|-----| | `SERVERLESS_TYPE` | `LOGIC_FUNCTION_TYPE` | | `SERVERLESS_LAMBDA_REGION` | `LOGIC_FUNCTION_LAMBDA_REGION` | | `SERVERLESS_LAMBDA_ROLE` | `LOGIC_FUNCTION_LAMBDA_ROLE` | | `SERVERLESS_LAMBDA_SUBHOSTING_URL` | `LOGIC_FUNCTION_LAMBDA_SUBHOSTING_URL` | | `SERVERLESS_LAMBDA_ACCESS_KEY_ID` | `LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID` | | `SERVERLESS_LAMBDA_SECRET_ACCESS_KEY` | `LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY` | ### Breaking Changes - Environment variables must be updated in production deployments - Database migration renames `serverlessFunction` → `logicFunction` tables
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
type ExecutionStatus,
|
||||
WorkflowStepExecutionResult,
|
||||
} from '@/workflow/components/WorkflowStepExecutionResult';
|
||||
import { type LogicFunctionTestData } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
|
||||
import { LogicFunctionExecutionStatus } from '~/generated-metadata/graphql';
|
||||
|
||||
export const LogicFunctionExecutionResult = ({
|
||||
logicFunctionTestData,
|
||||
maxHeight,
|
||||
isTesting = false,
|
||||
}: {
|
||||
logicFunctionTestData: LogicFunctionTestData;
|
||||
maxHeight?: number;
|
||||
isTesting?: boolean;
|
||||
}) => {
|
||||
const result =
|
||||
logicFunctionTestData.output.data ||
|
||||
logicFunctionTestData.output.error ||
|
||||
'';
|
||||
|
||||
const isSuccess =
|
||||
logicFunctionTestData.output.status ===
|
||||
LogicFunctionExecutionStatus.SUCCESS;
|
||||
|
||||
const isError =
|
||||
logicFunctionTestData.output.status === LogicFunctionExecutionStatus.ERROR;
|
||||
|
||||
const duration = logicFunctionTestData.output.duration;
|
||||
const status: ExecutionStatus = {
|
||||
isSuccess,
|
||||
isError,
|
||||
successMessage: isSuccess ? t`200 OK - ${duration}ms` : undefined,
|
||||
errorMessage: isError ? t`500 Error - ${duration}ms` : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<WorkflowStepExecutionResult
|
||||
result={result}
|
||||
language={logicFunctionTestData.language}
|
||||
height={Math.min(
|
||||
logicFunctionTestData.height,
|
||||
maxHeight ?? logicFunctionTestData.height,
|
||||
)}
|
||||
status={status}
|
||||
isTesting={isTesting}
|
||||
loadingMessage={t`Running function`}
|
||||
idleMessage={t`Output`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const INDEX_FILE_NAME = 'index.ts';
|
||||
@@ -0,0 +1 @@
|
||||
export const SOURCE_FOLDER_NAME = 'src';
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useExecuteOneLogicFunction } from '@/settings/logic-functions/hooks/useExecuteOneLogicFunction';
|
||||
import { logicFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/logicFunctionTestDataFamilyState';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { sleep } from '~/utils/sleep';
|
||||
|
||||
export const useTestLogicFunction = ({
|
||||
logicFunctionId,
|
||||
callback,
|
||||
}: {
|
||||
logicFunctionId: string;
|
||||
callback?: (testResult: object) => void;
|
||||
}) => {
|
||||
const [isTesting, setIsTesting] = useState(false);
|
||||
const { executeOneLogicFunction } = useExecuteOneLogicFunction();
|
||||
const [logicFunctionTestData, setLogicFunctionTestData] = useRecoilState(
|
||||
logicFunctionTestDataFamilyState(logicFunctionId),
|
||||
);
|
||||
|
||||
const testLogicFunction = async () => {
|
||||
try {
|
||||
setIsTesting(true);
|
||||
await sleep(200); // Delay artificially to avoid flashing the UI
|
||||
const result = await executeOneLogicFunction({
|
||||
id: logicFunctionId,
|
||||
payload: logicFunctionTestData.input,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
setIsTesting(false);
|
||||
|
||||
if (isDefined(result?.data?.executeOneLogicFunction?.data)) {
|
||||
callback?.(result?.data?.executeOneLogicFunction?.data);
|
||||
}
|
||||
|
||||
setLogicFunctionTestData((prev) => ({
|
||||
...prev,
|
||||
language: 'json',
|
||||
height: 300,
|
||||
output: {
|
||||
data: result?.data?.executeOneLogicFunction?.data
|
||||
? JSON.stringify(
|
||||
result?.data?.executeOneLogicFunction?.data,
|
||||
null,
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
logs: result?.data?.executeOneLogicFunction?.logs || '',
|
||||
duration: result?.data?.executeOneLogicFunction?.duration,
|
||||
status: result?.data?.executeOneLogicFunction?.status,
|
||||
error: result?.data?.executeOneLogicFunction?.error
|
||||
? JSON.stringify(
|
||||
result?.data?.executeOneLogicFunction?.error,
|
||||
null,
|
||||
4,
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
setIsTesting(false);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return { testLogicFunction, isTesting };
|
||||
};
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { computeNewSources } from '@/logic-functions/utils/computeNewSources';
|
||||
|
||||
describe('computeNewSources', () => {
|
||||
it('should compute new code input root 0', () => {
|
||||
const previousCodeInput = {
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
};
|
||||
|
||||
const filePath = 'index.ts';
|
||||
|
||||
const value = 'export const totoUpdated = () => {}';
|
||||
|
||||
const expectedResult = {
|
||||
'index.ts': 'export const totoUpdated = () => {}',
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should compute new code input root 0 file changed', () => {
|
||||
const previousCodeInput = {
|
||||
'.env': 'ENV=env',
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
};
|
||||
|
||||
const filePath = '.env';
|
||||
|
||||
const value = 'ENV=env\nENV2=env2';
|
||||
|
||||
const expectedResult = {
|
||||
'.env': 'ENV=env\nENV2=env2',
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should compute new code input root 0 with multiple files', () => {
|
||||
const previousCodeInput = {
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
'.env': 'ENV',
|
||||
};
|
||||
|
||||
const filePath = 'index.ts';
|
||||
|
||||
const value = 'export const totoUpdated = () => {}';
|
||||
|
||||
const expectedResult = {
|
||||
'index.ts': 'export const totoUpdated = () => {}',
|
||||
'.env': 'ENV',
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should compute new code input root 1', () => {
|
||||
const previousCodeInput = {
|
||||
src: { 'index.ts': 'export const toto = () => {}' },
|
||||
};
|
||||
|
||||
const filePath = 'src/index.ts';
|
||||
|
||||
const value = 'export const totoUpdated = () => {}';
|
||||
|
||||
const expectedResult = {
|
||||
src: { 'index.ts': 'export const totoUpdated = () => {}' },
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should compute new code input root 1 with multiple files', () => {
|
||||
const previousCodeInput = {
|
||||
src: {
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
'index2.ts': 'export const toto2 = () => {}',
|
||||
},
|
||||
};
|
||||
|
||||
const filePath = 'src/index.ts';
|
||||
|
||||
const value = 'export const totoUpdated = () => {}';
|
||||
|
||||
const expectedResult = {
|
||||
src: {
|
||||
'index.ts': 'export const totoUpdated = () => {}',
|
||||
'index2.ts': 'export const toto2 = () => {}',
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should compute new code input root 1 with added files', () => {
|
||||
const previousCodeInput = {
|
||||
src: {
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
},
|
||||
};
|
||||
|
||||
const filePath = 'src/index2.ts';
|
||||
|
||||
const value = 'export const toto2 = () => {}';
|
||||
|
||||
const expectedResult = {
|
||||
src: {
|
||||
'index.ts': 'export const toto = () => {}',
|
||||
'index2.ts': 'export const toto2 = () => {}',
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should compute new code input multiple roots', () => {
|
||||
const previousCodeInput = {
|
||||
'.env': 'ENV=env',
|
||||
src: { 'index.ts': 'export const toto = () => {}' },
|
||||
};
|
||||
|
||||
const filePath = 'src/index.ts';
|
||||
|
||||
const value = 'export const totoUpdated = () => {}';
|
||||
|
||||
const expectedResult = {
|
||||
src: { 'index.ts': 'export const totoUpdated = () => {}' },
|
||||
'.env': 'ENV=env',
|
||||
};
|
||||
|
||||
expect(
|
||||
computeNewSources({ previousCode: previousCodeInput, filePath, value }),
|
||||
).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// IA Generated
|
||||
|
||||
import { flattenSources } from '@/logic-functions/utils/flattenSources';
|
||||
import { type Sources } from 'twenty-shared/types';
|
||||
|
||||
describe('flattenSources', () => {
|
||||
it('flattens nested sources with root files', () => {
|
||||
const input: Sources = {
|
||||
'.env': 'KEY=VALUE',
|
||||
src: {
|
||||
'index.ts': 'export const a = 1',
|
||||
lib: {
|
||||
'util.ts': 'export const util = () => {}',
|
||||
},
|
||||
},
|
||||
docs: {
|
||||
'README.md': '# Hello',
|
||||
},
|
||||
};
|
||||
|
||||
const result = flattenSources(input);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ path: '.env', content: 'KEY=VALUE' },
|
||||
{ path: 'docs/README.md', content: '# Hello' },
|
||||
{ path: 'src/index.ts', content: 'export const a = 1' },
|
||||
{ path: 'src/lib/util.ts', content: 'export const util = () => {}' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles deep nesting and preserves file contents', () => {
|
||||
const input: Sources = {
|
||||
a: { b: { c: { d: { 'file.ts': 'content' } } } },
|
||||
};
|
||||
|
||||
expect(flattenSources(input)).toEqual([
|
||||
{ path: 'a/b/c/d/file.ts', content: 'content' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores empty folders and non-string leaves', () => {
|
||||
const input: Sources = {
|
||||
empty: {},
|
||||
weird: {
|
||||
oops: 42,
|
||||
} as unknown as Sources,
|
||||
file: 'ok',
|
||||
};
|
||||
|
||||
const res = flattenSources(input);
|
||||
expect(res).toEqual([{ path: 'file', content: 'ok' }]);
|
||||
});
|
||||
|
||||
it('accepts a custom basePath prefix', () => {
|
||||
const input: Sources = { src: { 'index.ts': 'x' } };
|
||||
const res = flattenSources(input, 'pkg');
|
||||
expect(res).toEqual([{ path: 'pkg/src/index.ts', content: 'x' }]);
|
||||
});
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { type InputSchema } from '@/workflow/types/InputSchema';
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/logic-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,
|
||||
);
|
||||
});
|
||||
});
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { getFunctionInputFromSourceCode } from '@/logic-functions/utils/getFunctionInputFromSourceCode';
|
||||
|
||||
describe('getFunctionInputFromSourceCode', () => {
|
||||
it('should return empty input if not parameter', async () => {
|
||||
const fileContent = 'function testFunction() { return }';
|
||||
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
it('should return first input if multiple parameters', async () => {
|
||||
const fileContent =
|
||||
'function testFunction(params1: {}, params2: {}) { return }';
|
||||
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
it('should return empty input if wrong parameter', async () => {
|
||||
const fileContent = 'function testFunction(params: string) { return }';
|
||||
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
it('should return input from source code', async () => {
|
||||
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 = await getFunctionInputFromSourceCode(fileContent);
|
||||
expect(result).toEqual({
|
||||
param1: null,
|
||||
param2: null,
|
||||
param3: null,
|
||||
param4: {},
|
||||
param5: {
|
||||
subParam1: null,
|
||||
},
|
||||
param6: 'my',
|
||||
param7: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { getFunctionInputSchema } from '@/logic-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' } },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { mergeDefaultFunctionInputAndFunctionInput } from '@/logic-functions/utils/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,48 @@
|
||||
import { type Sources } from 'twenty-shared/types';
|
||||
|
||||
export const computeNewSources = ({
|
||||
previousCode,
|
||||
filePath,
|
||||
value,
|
||||
}: {
|
||||
previousCode: Sources;
|
||||
filePath: string;
|
||||
value: string;
|
||||
}): Sources => {
|
||||
const result = { ...previousCode };
|
||||
|
||||
const parts = filePath.split('/').filter(Boolean);
|
||||
|
||||
if (parts.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
result[filePath] = value;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const [root, ...rest] = parts;
|
||||
|
||||
const newFilePath = rest.join('/');
|
||||
|
||||
if (
|
||||
typeof result?.[root] === 'string' ||
|
||||
typeof previousCode[root] === 'string'
|
||||
) {
|
||||
throw Error('Cannot compute new code input');
|
||||
}
|
||||
|
||||
return {
|
||||
...previousCode,
|
||||
[root]: {
|
||||
...previousCode[root],
|
||||
...computeNewSources({
|
||||
previousCode: result?.[root] ?? {},
|
||||
filePath: newFilePath,
|
||||
value,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// IA Generated
|
||||
import { type Sources } from 'twenty-shared/types';
|
||||
|
||||
type FlatSource = { path: string; content: string };
|
||||
|
||||
export const flattenSources = (
|
||||
sources: Sources,
|
||||
basePath = '',
|
||||
): FlatSource[] => {
|
||||
const out: FlatSource[] = [];
|
||||
|
||||
const join = (a: string, b: string) => (a ? `${a}/${b}` : b);
|
||||
|
||||
const walk = (node: Sources, prefix: string) => {
|
||||
for (const [name, value] of Object.entries(node)) {
|
||||
if (typeof value === 'string') {
|
||||
out.push({ path: join(prefix, name), content: value });
|
||||
} else if (value && typeof value === 'object') {
|
||||
walk(value as Sources, join(prefix, name));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(sources, basePath);
|
||||
|
||||
out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
||||
return out;
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { type InputSchema } from '@/workflow/types/InputSchema';
|
||||
import { type FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { getDefaultFunctionInputFromInputSchema } from '@/logic-functions/utils/getDefaultFunctionInputFromInputSchema';
|
||||
import { type FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
|
||||
import { isObject } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getFunctionInputFromSourceCode = async (
|
||||
sourceCode?: string,
|
||||
): Promise<FunctionInput> => {
|
||||
if (!isDefined(sourceCode)) {
|
||||
throw new Error('Source code is not defined');
|
||||
}
|
||||
|
||||
const { getFunctionInputSchema } = await import(
|
||||
'@/logic-functions/utils/getFunctionInputSchema'
|
||||
);
|
||||
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
||||
|
||||
const result = getDefaultFunctionInputFromInputSchema(functionInputSchema)[0];
|
||||
|
||||
if (!isObject(result)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
type InputSchema,
|
||||
type InputSchemaProperty,
|
||||
} from '@/workflow/types/InputSchema';
|
||||
import {
|
||||
type ArrayTypeNode,
|
||||
type ArrowFunction,
|
||||
createSourceFile,
|
||||
type FunctionDeclaration,
|
||||
type FunctionLikeDeclaration,
|
||||
type LiteralTypeNode,
|
||||
type Node,
|
||||
type PropertySignature,
|
||||
ScriptTarget,
|
||||
type StringLiteral,
|
||||
SyntaxKind,
|
||||
type TypeNode,
|
||||
type UnionTypeNode,
|
||||
type VariableStatement,
|
||||
} from 'typescript';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
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;
|
||||
};
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const getToolInputSchemaFromSourceCode = async (
|
||||
sourceCode: string,
|
||||
): Promise<object | null> => {
|
||||
const { getFunctionInputSchema } = await import('./getFunctionInputSchema');
|
||||
const inputSchema = getFunctionInputSchema(sourceCode);
|
||||
|
||||
// Logic functions take a single params object
|
||||
const firstParam = inputSchema[0];
|
||||
|
||||
if (firstParam?.type === 'object' && isDefined(firstParam.properties)) {
|
||||
return {
|
||||
type: 'object',
|
||||
properties: firstParam.properties,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/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