8723 workflow add editor in serverless function code step (#8805)
- create a serverless function when creating a new workflow code step - add code editor in workflow code step - move workflowVersion steps management from frontend to backend - add a custom resolver for workflow-version management - fix optimistic rendering on frontend - fix css - delete serverless function when deleting workflow code step TODO - Don't update serverlessFunction if no code change - Factorize what can be between crud trigger and crud step - Publish serverless version when activating workflow - delete serverless functions when deleting workflow or workflowVersion - fix optimistic rendering for code updates - Unify CRUD types <img width="1279" alt="image" src="https://github.com/user-attachments/assets/3d97ee9f-4b96-4abc-9d36-5c0280058be4">
This commit is contained in:
@ -1,142 +0,0 @@
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { getStepDefaultDefinition } from '../getStepDefaultDefinition';
|
||||
|
||||
it('returns a valid definition for CODE actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'CODE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionVersion: '',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a valid definition for SEND_EMAIL actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'SEND_EMAIL',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a valid definition for RECORD_CRUD.CREATE actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'RECORD_CRUD.CREATE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Create Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'CREATE',
|
||||
objectName: generatedMockObjectMetadataItems[0].nameSingular,
|
||||
objectRecord: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a valid definition for RECORD_CRUD.UPDATE actions', () => {
|
||||
expect(
|
||||
getStepDefaultDefinition({
|
||||
type: 'RECORD_CRUD.UPDATE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
id: expect.any(String),
|
||||
name: 'Update Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'UPDATE',
|
||||
objectName: generatedMockObjectMetadataItems[0].nameSingular,
|
||||
objectRecord: {},
|
||||
objectRecordId: '',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("throws for RECORD_CRUD.DELETE actions as it's not implemented yet", () => {
|
||||
expect(() => {
|
||||
getStepDefaultDefinition({
|
||||
type: 'RECORD_CRUD.DELETE',
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
});
|
||||
}).toThrow('Not implemented yet');
|
||||
});
|
||||
|
||||
it('throws when providing an unknown type', () => {
|
||||
expect(() => {
|
||||
getStepDefaultDefinition({
|
||||
type: 'unknown' as any,
|
||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
});
|
||||
}).toThrow('Unknown type: unknown');
|
||||
});
|
||||
@ -0,0 +1,129 @@
|
||||
import {
|
||||
ArrayTypeNode,
|
||||
ArrowFunction,
|
||||
createSourceFile,
|
||||
FunctionDeclaration,
|
||||
LiteralTypeNode,
|
||||
PropertySignature,
|
||||
ScriptTarget,
|
||||
StringLiteral,
|
||||
SyntaxKind,
|
||||
TypeNode,
|
||||
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: InputSchema = {};
|
||||
|
||||
(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' };
|
||||
}
|
||||
};
|
||||
|
||||
export const getFunctionInputSchema = (fileContent: string): InputSchema => {
|
||||
const sourceFile = createSourceFile(
|
||||
'temp.ts',
|
||||
fileContent,
|
||||
ScriptTarget.ESNext,
|
||||
true,
|
||||
);
|
||||
|
||||
const schema: InputSchema = {};
|
||||
|
||||
sourceFile.forEachChild((node) => {
|
||||
if (node.kind === SyntaxKind.FunctionDeclaration) {
|
||||
const funcNode = node as FunctionDeclaration;
|
||||
const params = funcNode.parameters;
|
||||
|
||||
params.forEach((param) => {
|
||||
const paramName = param.name.getText();
|
||||
const typeNode = param.type;
|
||||
|
||||
if (isDefined(typeNode)) {
|
||||
schema[paramName] = getTypeString(typeNode);
|
||||
} else {
|
||||
schema[paramName] = { type: 'unknown' };
|
||||
}
|
||||
});
|
||||
} else if (node.kind === SyntaxKind.VariableStatement) {
|
||||
const varStatement = node as VariableStatement;
|
||||
|
||||
varStatement.declarationList.declarations.forEach((declaration) => {
|
||||
if (
|
||||
isDefined(declaration.initializer) &&
|
||||
declaration.initializer.kind === SyntaxKind.ArrowFunction
|
||||
) {
|
||||
const arrowFunction = declaration.initializer as ArrowFunction;
|
||||
const params = arrowFunction.parameters;
|
||||
|
||||
params.forEach((param: any) => {
|
||||
const paramName = param.name.text;
|
||||
const typeNode = param.type;
|
||||
|
||||
if (isDefined(typeNode)) {
|
||||
schema[paramName] = getTypeString(typeNode);
|
||||
} else {
|
||||
schema[paramName] = { type: 'unknown' };
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return schema;
|
||||
};
|
||||
@ -1,100 +0,0 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { WorkflowStep, WorkflowStepType } from '@/workflow/types/Workflow';
|
||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const BASE_DEFAULT_STEP_SETTINGS = {
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getStepDefaultDefinition = ({
|
||||
type,
|
||||
activeObjectMetadataItems,
|
||||
}: {
|
||||
type: WorkflowStepType;
|
||||
activeObjectMetadataItems: ObjectMetadataItem[];
|
||||
}): WorkflowStep => {
|
||||
const newStepId = v4();
|
||||
|
||||
switch (type) {
|
||||
case 'CODE': {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Code',
|
||||
type: 'CODE',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
serverlessFunctionId: '',
|
||||
serverlessFunctionVersion: '',
|
||||
serverlessFunctionInput: {},
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SEND_EMAIL': {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Send Email',
|
||||
type: 'SEND_EMAIL',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'RECORD_CRUD.CREATE': {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Create Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'CREATE',
|
||||
objectName: activeObjectMetadataItems[0].nameSingular,
|
||||
objectRecord: {},
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'RECORD_CRUD.UPDATE':
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Update Record',
|
||||
type: 'RECORD_CRUD',
|
||||
valid: false,
|
||||
settings: {
|
||||
input: {
|
||||
type: 'UPDATE',
|
||||
objectName: activeObjectMetadataItems[0].nameSingular,
|
||||
objectRecordId: '',
|
||||
objectRecord: {},
|
||||
},
|
||||
...BASE_DEFAULT_STEP_SETTINGS,
|
||||
},
|
||||
};
|
||||
case 'RECORD_CRUD.DELETE': {
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
default: {
|
||||
return assertUnreachable(type, `Unknown type: ${type}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user