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:
@ -17,4 +17,5 @@ export enum MessageQueue {
|
||||
entityEventsToDbQueue = 'entity-events-to-db-queue',
|
||||
testQueue = 'test-queue',
|
||||
workflowQueue = 'workflow-queue',
|
||||
serverlessFunctionQueue = 'serverless-function-queue',
|
||||
}
|
||||
|
||||
@ -8,17 +8,18 @@ export type LayerDependencies = {
|
||||
yarnLock: string;
|
||||
};
|
||||
|
||||
export const getLastLayerDependencies =
|
||||
async (): Promise<LayerDependencies> => {
|
||||
const lastVersionLayerDirName = getLayerDependenciesDirName('latest');
|
||||
const packageJson = await fs.readFile(
|
||||
join(lastVersionLayerDirName, 'package.json'),
|
||||
'utf8',
|
||||
);
|
||||
const yarnLock = await fs.readFile(
|
||||
join(lastVersionLayerDirName, 'yarn.lock'),
|
||||
'utf8',
|
||||
);
|
||||
export const getLayerDependencies = async (
|
||||
layerVersion: number | 'latest',
|
||||
): Promise<LayerDependencies> => {
|
||||
const lastVersionLayerDirName = getLayerDependenciesDirName(layerVersion);
|
||||
const packageJson = await fs.readFile(
|
||||
join(lastVersionLayerDirName, 'package.json'),
|
||||
'utf8',
|
||||
);
|
||||
const yarnLock = await fs.readFile(
|
||||
join(lastVersionLayerDirName, 'yarn.lock'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
return { packageJson: JSON.parse(packageJson), yarnLock };
|
||||
};
|
||||
return { packageJson: JSON.parse(packageJson), yarnLock };
|
||||
};
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@InputType()
|
||||
export class CreateWorkflowVersionStepInput {
|
||||
@Field(() => String, {
|
||||
description: 'Workflow version ID',
|
||||
nullable: false,
|
||||
})
|
||||
workflowVersionId: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'New step type',
|
||||
nullable: false,
|
||||
})
|
||||
stepType: WorkflowActionType;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
@InputType()
|
||||
export class DeleteWorkflowVersionStepInput {
|
||||
@Field(() => String, {
|
||||
description: 'Workflow version ID',
|
||||
nullable: false,
|
||||
})
|
||||
workflowVersionId: string;
|
||||
|
||||
@Field(() => String, {
|
||||
description: 'Step to delete ID',
|
||||
nullable: false,
|
||||
})
|
||||
stepId: string;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@InputType()
|
||||
export class UpdateWorkflowVersionStepInput {
|
||||
@Field(() => String, {
|
||||
description: 'Workflow version ID',
|
||||
nullable: false,
|
||||
})
|
||||
workflowVersionId: string;
|
||||
|
||||
@Field(() => graphqlTypeJson, {
|
||||
description: 'Step to update in JSON format',
|
||||
nullable: false,
|
||||
})
|
||||
step: WorkflowAction;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
|
||||
@ObjectType('WorkflowAction')
|
||||
export class WorkflowActionDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
type: WorkflowActionType;
|
||||
|
||||
@Field(() => graphqlTypeJson)
|
||||
settings: object;
|
||||
|
||||
@Field(() => Boolean)
|
||||
valid: boolean;
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
|
||||
import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-version-step-input.dto';
|
||||
import { DeleteWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/delete-workflow-version-step-input.dto';
|
||||
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||
|
||||
@Resolver()
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
|
||||
export class WorkflowVersionStepResolver {
|
||||
constructor(
|
||||
private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService,
|
||||
) {}
|
||||
|
||||
@Mutation(() => WorkflowActionDTO)
|
||||
async createWorkflowVersionStep(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Args('input')
|
||||
{ stepType, workflowVersionId }: CreateWorkflowVersionStepInput,
|
||||
): Promise<WorkflowActionDTO> {
|
||||
return this.workflowVersionStepWorkspaceService.createWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepType,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => WorkflowActionDTO)
|
||||
async updateWorkflowVersionStep(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Args('input') { step, workflowVersionId }: UpdateWorkflowVersionStepInput,
|
||||
): Promise<WorkflowActionDTO> {
|
||||
return this.workflowVersionStepWorkspaceService.updateWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
step,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => WorkflowActionDTO)
|
||||
async deleteWorkflowVersionStep(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Args('input')
|
||||
{ stepId, workflowVersionId }: DeleteWorkflowVersionStepInput,
|
||||
): Promise<WorkflowActionDTO> {
|
||||
return this.workflowVersionStepWorkspaceService.deleteWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,17 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver';
|
||||
import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-builder.resolver';
|
||||
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
|
||||
import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/workflow-trigger.module';
|
||||
import { WorkflowVersionStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version-step.resolver';
|
||||
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
|
||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
|
||||
@Module({
|
||||
imports: [WorkflowTriggerModule, WorkflowBuilderModule],
|
||||
providers: [WorkflowTriggerResolver, WorkflowBuilderResolver],
|
||||
imports: [WorkflowTriggerModule, WorkflowBuilderModule, WorkflowCommonModule],
|
||||
providers: [
|
||||
WorkflowTriggerResolver,
|
||||
WorkflowBuilderResolver,
|
||||
WorkflowVersionStepResolver,
|
||||
],
|
||||
})
|
||||
export class WorkflowApiModule {}
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
import { Scope } from '@nestjs/common';
|
||||
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
|
||||
export type DeleteServerlessFunctionBatchEvent = {
|
||||
ids: string[];
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
@Processor({
|
||||
queueName: MessageQueue.serverlessFunctionQueue,
|
||||
scope: Scope.REQUEST,
|
||||
})
|
||||
export class DeleteServerlessFunctionJob {
|
||||
constructor(
|
||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||
) {}
|
||||
|
||||
@Process(DeleteServerlessFunctionJob.name)
|
||||
async handle(batchEvent: DeleteServerlessFunctionBatchEvent): Promise<void> {
|
||||
await Promise.all(
|
||||
batchEvent.ids.map((id) =>
|
||||
this.serverlessFunctionService.deleteOneServerlessFunction(
|
||||
id,
|
||||
batchEvent.workspaceId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
import { SERVERLESS_FUNCTION_PUBLISHED } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-published';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionPublicationListener {
|
||||
constructor(
|
||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||
private readonly codeIntrospectionService: CodeIntrospectionService,
|
||||
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||
) {}
|
||||
|
||||
@OnCustomBatchEvent(SERVERLESS_FUNCTION_PUBLISHED)
|
||||
async handle(
|
||||
batchEvent: WorkspaceEventBatch<{
|
||||
serverlessFunctionId: string;
|
||||
serverlessFunctionVersion: string;
|
||||
}>,
|
||||
): Promise<void> {
|
||||
for (const event of batchEvent.events) {
|
||||
const sourceCode =
|
||||
await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
||||
batchEvent.workspaceId,
|
||||
event.serverlessFunctionId,
|
||||
event.serverlessFunctionVersion,
|
||||
);
|
||||
|
||||
if (!sourceCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexCode = sourceCode[join('src', INDEX_FILE_NAME)];
|
||||
|
||||
if (!indexCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestVersionInputSchema =
|
||||
this.codeIntrospectionService.getFunctionInputSchema(indexCode);
|
||||
|
||||
await this.serverlessFunctionRepository.update(
|
||||
{ id: event.serverlessFunctionId },
|
||||
{ latestVersionInputSchema },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,11 +8,9 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { ServerlessFunctionPublicationListener } from 'src/engine/metadata-modules/serverless-function/listeners/serverless-function-publication.listener';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-introspection.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -22,13 +20,8 @@ import { CodeIntrospectionModule } from 'src/modules/code-introspection/code-int
|
||||
FileModule,
|
||||
ThrottlerModule,
|
||||
AnalyticsModule,
|
||||
CodeIntrospectionModule,
|
||||
],
|
||||
providers: [
|
||||
ServerlessFunctionService,
|
||||
ServerlessFunctionResolver,
|
||||
ServerlessFunctionPublicationListener,
|
||||
],
|
||||
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
||||
exports: [ServerlessFunctionService],
|
||||
})
|
||||
export class ServerlessFunctionModule {}
|
||||
|
||||
@ -84,11 +84,14 @@ export class ServerlessFunctionResolver {
|
||||
}
|
||||
|
||||
@Query(() => graphqlTypeJson)
|
||||
async getAvailablePackages(@AuthWorkspace() { id: workspaceId }: Workspace) {
|
||||
async getAvailablePackages(
|
||||
@Args('input') { id }: ServerlessFunctionIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
try {
|
||||
await this.checkFeatureFlag(workspaceId);
|
||||
|
||||
return await this.serverlessFunctionService.getAvailablePackages();
|
||||
return await this.serverlessFunctionService.getAvailablePackages(id);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
|
||||
@ -17,11 +17,9 @@ import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/consta
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
|
||||
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
|
||||
import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
|
||||
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
|
||||
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
|
||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||
import { SERVERLESS_FUNCTION_PUBLISHED } from 'src/engine/metadata-modules/serverless-function/constants/serverless-function-published';
|
||||
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||
import {
|
||||
@ -32,8 +30,8 @@ import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { getLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionService {
|
||||
@ -45,7 +43,6 @@ export class ServerlessFunctionService {
|
||||
private readonly throttlerService: ThrottlerService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
) {}
|
||||
|
||||
async findManyServerlessFunctions(where) {
|
||||
@ -194,17 +191,6 @@ export class ServerlessFunctionService {
|
||||
},
|
||||
);
|
||||
|
||||
this.workspaceEventEmitter.emitCustomBatchEvent(
|
||||
SERVERLESS_FUNCTION_PUBLISHED,
|
||||
[
|
||||
{
|
||||
serverlessFunctionId: existingServerlessFunction.id,
|
||||
serverlessFunctionVersion: newVersion,
|
||||
},
|
||||
],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return this.serverlessFunctionRepository.findOneBy({
|
||||
id: existingServerlessFunction.id,
|
||||
});
|
||||
@ -290,8 +276,14 @@ export class ServerlessFunctionService {
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailablePackages() {
|
||||
const { packageJson, yarnLock } = await getLastLayerDependencies();
|
||||
async getAvailablePackages(serverlessFunctionId: string) {
|
||||
const serverlessFunction =
|
||||
await this.serverlessFunctionRepository.findOneBy({
|
||||
id: serverlessFunctionId,
|
||||
});
|
||||
const { packageJson, yarnLock } = await getLayerDependencies(
|
||||
serverlessFunction?.layerVersion || 'latest',
|
||||
);
|
||||
|
||||
const packageVersionRegex = /^"([^@]+)@.*?":\n\s+version: (.+)$/gm;
|
||||
const versions: Record<string, string> = {};
|
||||
|
||||
@ -24,15 +24,15 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class CodeIntrospectionService {
|
||||
public generateInputData(inputSchema: InputSchema) {
|
||||
public generateInputData(inputSchema: InputSchema, setNullValue = false) {
|
||||
return Object.entries(inputSchema).reduce((acc, [key, value]) => {
|
||||
if (isDefined(value.enum)) {
|
||||
acc[key] = value.enum?.[0];
|
||||
} else if (['string', 'number', 'boolean'].includes(value.type)) {
|
||||
acc[key] = generateFakeValue(value.type);
|
||||
acc[key] = setNullValue ? null : generateFakeValue(value.type);
|
||||
} else if (value.type === 'object') {
|
||||
acc[key] = isDefined(value.properties)
|
||||
? this.generateInputData(value.properties)
|
||||
? this.generateInputData(value.properties, setNullValue)
|
||||
: {};
|
||||
} else if (value.type === 'array' && isDefined(value.items)) {
|
||||
acc[key] = [generateFakeValue(value.items.type)];
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class WorkflowVersionStepException extends CustomException {
|
||||
constructor(message: string, code: WorkflowVersionStepExceptionCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
export enum WorkflowVersionStepExceptionCode {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
UNDEFINED = 'UNDEFINED',
|
||||
FAILURE = 'FAILURE',
|
||||
}
|
||||
@ -1,12 +1,32 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { WorkflowCommandModule } from 'src/modules/workflow/common/commands/workflow-command.module';
|
||||
import { WorkflowQueryHookModule } from 'src/modules/workflow/common/query-hooks/workflow-query-hook.module';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-version-step.workspace-service';
|
||||
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [WorkflowQueryHookModule, WorkflowCommandModule],
|
||||
providers: [WorkflowCommonWorkspaceService],
|
||||
exports: [WorkflowCommonWorkspaceService],
|
||||
imports: [
|
||||
WorkflowQueryHookModule,
|
||||
WorkflowCommandModule,
|
||||
WorkflowBuilderModule,
|
||||
ServerlessFunctionModule,
|
||||
CodeIntrospectionModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||
],
|
||||
providers: [
|
||||
WorkflowCommonWorkspaceService,
|
||||
WorkflowVersionStepWorkspaceService,
|
||||
],
|
||||
exports: [
|
||||
WorkflowCommonWorkspaceService,
|
||||
WorkflowVersionStepWorkspaceService,
|
||||
],
|
||||
})
|
||||
export class WorkflowCommonModule {}
|
||||
|
||||
@ -0,0 +1,363 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkflowBuilderWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-builder.workspace-service';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { WorkflowRecordCRUDType } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||
import {
|
||||
WorkflowVersionStepException,
|
||||
WorkflowVersionStepExceptionCode,
|
||||
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
|
||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||
|
||||
const TRIGGER_STEP_ID = 'trigger';
|
||||
|
||||
const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = {
|
||||
input: {},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
continueOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
retryOnFailure: {
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowVersionStepWorkspaceService {
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly workflowBuilderWorkspaceService: WorkflowBuilderWorkspaceService,
|
||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||
private readonly codeIntrospectionService: CodeIntrospectionService,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
) {}
|
||||
|
||||
private async getStepDefaultDefinition({
|
||||
type,
|
||||
workspaceId,
|
||||
}: {
|
||||
type: WorkflowActionType;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
const newStepId = v4();
|
||||
|
||||
switch (`${type}`) {
|
||||
case WorkflowActionType.CODE: {
|
||||
const newServerlessFunction =
|
||||
await this.serverlessFunctionService.createOneServerlessFunction(
|
||||
{
|
||||
name: 'A Serverless Function Code Workflow Step',
|
||||
description: '',
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isDefined(newServerlessFunction)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'Fail to create Code Step',
|
||||
WorkflowVersionStepExceptionCode.FAILURE,
|
||||
);
|
||||
}
|
||||
|
||||
const sourceCode = (
|
||||
await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
||||
workspaceId,
|
||||
newServerlessFunction.id,
|
||||
'draft',
|
||||
)
|
||||
)?.[join('src', INDEX_FILE_NAME)];
|
||||
|
||||
const inputSchema = isDefined(sourceCode)
|
||||
? this.codeIntrospectionService.getFunctionInputSchema(sourceCode)
|
||||
: {};
|
||||
|
||||
const serverlessFunctionInput =
|
||||
this.codeIntrospectionService.generateInputData(inputSchema, true);
|
||||
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Code - Serverless Function',
|
||||
type: WorkflowActionType.CODE,
|
||||
valid: false,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
serverlessFunctionId: newServerlessFunction.id,
|
||||
serverlessFunctionVersion: 'draft',
|
||||
serverlessFunctionInput,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case WorkflowActionType.SEND_EMAIL: {
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Send Email',
|
||||
type: WorkflowActionType.SEND_EMAIL,
|
||||
valid: false,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
connectedAccountId: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
body: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case `${WorkflowActionType.RECORD_CRUD}.${WorkflowRecordCRUDType.CREATE}`: {
|
||||
const activeObjectMetadataItem =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: { workspaceId, isActive: true, isSystem: false },
|
||||
});
|
||||
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Create Record',
|
||||
type: WorkflowActionType.RECORD_CRUD,
|
||||
valid: false,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
type: WorkflowRecordCRUDType.CREATE,
|
||||
objectName: activeObjectMetadataItem?.nameSingular || '',
|
||||
objectRecord: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case `${WorkflowActionType.RECORD_CRUD}.${WorkflowRecordCRUDType.UPDATE}`: {
|
||||
const activeObjectMetadataItem =
|
||||
await this.objectMetadataRepository.findOne({
|
||||
where: { workspaceId, isActive: true, isSystem: false },
|
||||
});
|
||||
|
||||
return {
|
||||
id: newStepId,
|
||||
name: 'Update Record',
|
||||
type: WorkflowActionType.RECORD_CRUD,
|
||||
valid: false,
|
||||
settings: {
|
||||
...BASE_STEP_DEFINITION,
|
||||
input: {
|
||||
type: WorkflowRecordCRUDType.UPDATE,
|
||||
objectName: activeObjectMetadataItem?.nameSingular || '',
|
||||
objectRecord: {},
|
||||
objectRecordId: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new WorkflowVersionStepException(
|
||||
`WorkflowActionType '${type}' unknown`,
|
||||
WorkflowVersionStepExceptionCode.UNKNOWN,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async enrichOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
const result = { ...step };
|
||||
const outputSchema =
|
||||
await this.workflowBuilderWorkspaceService.computeStepOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
result.settings = {
|
||||
...result.settings,
|
||||
outputSchema: outputSchema || {},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async createWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepType,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
stepType: WorkflowActionType;
|
||||
}): Promise<WorkflowActionDTO> {
|
||||
const newStep = await this.getStepDefaultDefinition({
|
||||
type: stepType,
|
||||
workspaceId,
|
||||
});
|
||||
const enrichedNewStep = await this.enrichOutputSchema({
|
||||
step: newStep,
|
||||
workspaceId,
|
||||
});
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
'workflowVersion',
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: {
|
||||
id: workflowVersionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'WorkflowVersion not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
steps: [...(workflowVersion.steps || []), enrichedNewStep],
|
||||
});
|
||||
|
||||
return enrichedNewStep;
|
||||
}
|
||||
|
||||
async updateWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
step,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
step: WorkflowAction;
|
||||
}): Promise<WorkflowAction> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
'workflowVersion',
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: {
|
||||
id: workflowVersionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'WorkflowVersion not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(workflowVersion.steps)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
"Can't update step from undefined steps",
|
||||
WorkflowVersionStepExceptionCode.UNDEFINED,
|
||||
);
|
||||
}
|
||||
|
||||
const enrichedNewStep = await this.enrichOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const updatedSteps = workflowVersion.steps.map((existingStep) => {
|
||||
if (existingStep.id === step.id) {
|
||||
return enrichedNewStep;
|
||||
} else {
|
||||
return existingStep;
|
||||
}
|
||||
});
|
||||
|
||||
await workflowVersionRepository.update(workflowVersion.id, {
|
||||
steps: updatedSteps,
|
||||
});
|
||||
|
||||
return enrichedNewStep;
|
||||
}
|
||||
|
||||
async deleteWorkflowVersionStep({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
stepId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
stepId: string;
|
||||
}): Promise<WorkflowActionDTO> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
'workflowVersion',
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
where: {
|
||||
id: workflowVersionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(workflowVersion)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
'WorkflowVersion not found',
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(workflowVersion.steps)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
"Can't delete step from undefined steps",
|
||||
WorkflowVersionStepExceptionCode.UNDEFINED,
|
||||
);
|
||||
}
|
||||
|
||||
const stepToDelete = workflowVersion.steps.filter(
|
||||
(step) => step.id === stepId,
|
||||
)?.[0];
|
||||
|
||||
if (!isDefined(stepToDelete)) {
|
||||
throw new WorkflowVersionStepException(
|
||||
"Can't delete not existing step",
|
||||
WorkflowVersionStepExceptionCode.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowVersionUpdates =
|
||||
stepId === TRIGGER_STEP_ID
|
||||
? { trigger: null }
|
||||
: { steps: workflowVersion.steps.filter((step) => step.id !== stepId) };
|
||||
|
||||
await workflowVersionRepository.update(
|
||||
workflowVersion.id,
|
||||
workflowVersionUpdates,
|
||||
);
|
||||
|
||||
switch (stepToDelete.type) {
|
||||
case WorkflowActionType.CODE:
|
||||
await this.serverlessFunctionService.deleteOneServerlessFunction(
|
||||
stepToDelete.settings.input.serverlessFunctionId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
return stepToDelete;
|
||||
}
|
||||
}
|
||||
@ -79,6 +79,14 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.data.steps) {
|
||||
throw new WorkflowQueryValidationException(
|
||||
'Updating workflowVersion steps directly is forbidden. ' +
|
||||
'Use createWorkflowVersionStep, updateWorkflowVersionStep or deleteWorkflowVersionStep endpoint instead.',
|
||||
WorkflowQueryValidationExceptionCode.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async validateWorkflowVersionForDeleteOne(payload: DeleteOneResolverArgs) {
|
||||
|
||||
Reference in New Issue
Block a user