Infer function input in workflow step (#8308)

- add `inputSchema` column in serverless function. This is an array of
parameters, with their name and type
- on serverless function id update, get the `inputSchema` + store empty
settings in step
- from step settings, build the form 

TODO in next PR:
- use field type to decide what kind of form should be printed
- have a strategy to handle object as input



https://github.com/user-attachments/assets/ed96f919-24b5-4baf-a051-31f76f45e575
This commit is contained in:
Thomas Trompette
2024-11-05 14:57:06 +01:00
committed by GitHub
parent d1531aa1b6
commit be8141ce5e
29 changed files with 334 additions and 90 deletions

View File

@ -18,7 +18,7 @@ describe('CodeIntrospectionService', () => {
expect(service).toBeDefined();
});
describe('analyze', () => {
describe('getFunctionInputSchema', () => {
it('should analyze a function declaration correctly', () => {
const fileContent = `
function testFunction(param1: string, param2: number): void {
@ -26,7 +26,7 @@ describe('CodeIntrospectionService', () => {
}
`;
const result = service.analyze(fileContent);
const result = service.getFunctionInputSchema(fileContent);
expect(result).toEqual([
{ name: 'param1', type: 'string' },
@ -41,7 +41,7 @@ describe('CodeIntrospectionService', () => {
};
`;
const result = service.analyze(fileContent);
const result = service.getFunctionInputSchema(fileContent);
expect(result).toEqual([
{ name: 'param1', type: 'string' },
@ -55,7 +55,7 @@ describe('CodeIntrospectionService', () => {
console.log(x);
`;
const result = service.analyze(fileContent);
const result = service.getFunctionInputSchema(fileContent);
expect(result).toEqual([]);
});
@ -66,10 +66,10 @@ describe('CodeIntrospectionService', () => {
function func2(param2: number) {}
`;
expect(() => service.analyze(fileContent)).toThrow(
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
CodeIntrospectionException,
);
expect(() => service.analyze(fileContent)).toThrow(
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
'Only one function is allowed',
);
});
@ -80,10 +80,10 @@ describe('CodeIntrospectionService', () => {
const func2 = (param2: number) => {};
`;
expect(() => service.analyze(fileContent)).toThrow(
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
CodeIntrospectionException,
);
expect(() => service.analyze(fileContent)).toThrow(
expect(() => service.getFunctionInputSchema(fileContent)).toThrow(
'Only one arrow function is allowed',
);
});
@ -95,7 +95,7 @@ describe('CodeIntrospectionService', () => {
}
`;
const result = service.analyze(fileContent);
const result = service.getFunctionInputSchema(fileContent);
expect(result).toEqual([
{ name: 'param1', type: 'string[]' },

View File

@ -8,16 +8,12 @@ import {
SyntaxKind,
} from 'ts-morph';
import { FunctionParameter } from 'src/engine/metadata-modules/serverless-function/dtos/function-parameter.dto';
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
import {
CodeIntrospectionException,
CodeIntrospectionExceptionCode,
} from 'src/modules/code-introspection/code-introspection.exception';
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
type FunctionParameter = {
name: string;
type: string;
};
@Injectable()
export class CodeIntrospectionService {
@ -27,7 +23,13 @@ export class CodeIntrospectionService {
this.project = new Project();
}
public analyze(
public generateInputData(fileContent: string, fileName = 'temp.ts') {
const parameters = this.getFunctionInputSchema(fileContent, fileName);
return this.generateFakeDataFromParams(parameters);
}
public getFunctionInputSchema(
fileContent: string,
fileName = 'temp.ts',
): FunctionParameter[] {
@ -38,7 +40,7 @@ export class CodeIntrospectionService {
const functionDeclarations = sourceFile.getFunctions();
if (functionDeclarations.length > 0) {
return this.analyzeFunctions(functionDeclarations);
return this.getFunctionParameters(functionDeclarations);
}
const arrowFunctions = sourceFile.getDescendantsOfKind(
@ -46,13 +48,13 @@ export class CodeIntrospectionService {
);
if (arrowFunctions.length > 0) {
return this.analyzeArrowFunctions(arrowFunctions);
return this.getArrowFunctionParameters(arrowFunctions);
}
return [];
}
private analyzeFunctions(
private getFunctionParameters(
functionDeclarations: FunctionDeclaration[],
): FunctionParameter[] {
if (functionDeclarations.length > 1) {
@ -67,7 +69,7 @@ export class CodeIntrospectionService {
return functionDeclaration.getParameters().map(this.buildFunctionParameter);
}
private analyzeArrowFunctions(
private getArrowFunctionParameters(
arrowFunctions: ArrowFunction[],
): FunctionParameter[] {
if (arrowFunctions.length > 1) {
@ -91,23 +93,13 @@ export class CodeIntrospectionService {
};
}
public generateInputData(fileContent: string, fileName = 'temp.ts') {
const parameters = this.analyze(fileContent, fileName);
return this.generateFakeDataFromParams(parameters);
}
private generateFakeDataFromParams(
params: FunctionParameter[],
): Record<string, any> {
const data: Record<string, any> = {};
return params.reduce((acc, param) => {
acc[param.name] = generateFakeValue(param.type);
params.forEach((param) => {
const type = param.type;
data[param.name] = generateFakeValue(type);
});
return data;
return acc;
}, {});
}
}

View File

@ -30,17 +30,21 @@ export class CodeWorkflowAction implements WorkflowAction {
);
}
const result =
await this.serverlessFunctionService.executeOneServerlessFunction(
workflowStepInput.serverlessFunctionId,
workspaceId,
{}, // TODO: input will be dynamically calculated from function input
);
try {
const result =
await this.serverlessFunctionService.executeOneServerlessFunction(
workflowStepInput.serverlessFunctionId,
workspaceId,
workflowStepInput.serverlessFunctionInput,
);
if (result.error) {
return { error: result.error };
if (result.error) {
return { error: result.error };
}
return { result: result.data || {} };
} catch (error) {
return { error: error.message };
}
return { result: result.data || {} };
}
}

View File

@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { WorkflowBuilderService } from 'src/modules/workflow/workflow-builder/workflow-builder.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
@Module({
imports: [
@ -12,7 +12,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
ServerlessFunctionModule,
CodeIntrospectionModule,
],
providers: [WorkflowBuilderService],
exports: [WorkflowBuilderService],
providers: [WorkflowBuilderWorkspaceService],
exports: [WorkflowBuilderWorkspaceService],
})
export class WorkflowBuilderModule {}

View File

@ -25,7 +25,7 @@ import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-bui
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
@Injectable()
export class WorkflowBuilderService {
export class WorkflowBuilderWorkspaceService {
constructor(
private readonly serverlessFunctionService: ServerlessFunctionService,
private readonly codeIntrospectionService: CodeIntrospectionService,

View File

@ -1,4 +1,5 @@
export type OutputSchema = object;
export type InputSchema = object;
type BaseWorkflowStepSettings = {
input: object;
@ -16,7 +17,9 @@ type BaseWorkflowStepSettings = {
export type WorkflowCodeStepInput = {
serverlessFunctionId: string;
serverlessFunctionVersion: string;
payloadInput: object;
serverlessFunctionInput: {
[key: string]: any;
};
};
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {

View File

@ -33,6 +33,7 @@ export class WorkflowRunWorkspaceService {
return (
await workflowRunRepository.save({
name: `Execution of ${workflowVersion.name}`,
workflowVersionId,
createdBy,
workflowId: workflowVersion.workflowId,