998 workflow restore (#12417)

Add a post hook to restore workflow sub-entities
This commit is contained in:
martmull
2025-06-03 15:28:43 +02:00
committed by GitHub
parent a943f9cf36
commit cb010d90fe
27 changed files with 600 additions and 173 deletions

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddDeletedAtToServerlessFunction1748875812894
implements MigrationInterface
{
name = 'AddDeletedAtToServerlessFunction1748875812894';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" ADD "deletedAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`CREATE INDEX "IDX_SERVERLESS_FUNCTION_ID_DELETED_AT" ON "core"."serverlessFunction" ("id", "deletedAt") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "core"."IDX_SERVERLESS_FUNCTION_ID_DELETED_AT"`,
);
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" DROP COLUMN "deletedAt"`,
);
}
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveUselessServerlessFunctionColumn1748942397538
implements MigrationInterface
{
name = 'RemoveUselessServerlessFunctionColumn1748942397538';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" DROP COLUMN "syncStatus"`,
);
await queryRunner.query(
`DROP TYPE "core"."serverlessFunction_syncstatus_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."serverlessFunction_syncstatus_enum" AS ENUM('BUILDING', 'NOT_READY', 'READY')`,
);
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" ADD "syncStatus" "core"."serverlessFunction_syncstatus_enum" NOT NULL DEFAULT 'NOT_READY'`,
);
}
}

View File

@ -1,9 +1,4 @@
import {
Field,
HideField,
ObjectType,
registerEnumType,
} from '@nestjs/graphql';
import { Field, HideField, ObjectType } from '@nestjs/graphql';
import {
Authorize,
@ -13,7 +8,6 @@ import {
import {
IsArray,
IsDateString,
IsEnum,
IsNotEmpty,
IsNumber,
IsString,
@ -22,14 +16,8 @@ import {
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ServerlessFunctionSyncStatus } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
registerEnumType(ServerlessFunctionSyncStatus, {
name: 'ServerlessFunctionSyncStatus',
description: 'SyncStatus of the serverlessFunction',
});
@ObjectType('ServerlessFunction')
@Authorize({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -75,11 +63,6 @@ export class ServerlessFunctionDTO {
@Field(() => GraphQLJSON, { nullable: true })
latestVersionInputSchema: InputSchema;
@IsEnum(ServerlessFunctionSyncStatus)
@IsNotEmpty()
@Field(() => ServerlessFunctionSyncStatus)
syncStatus: ServerlessFunctionSyncStatus;
@HideField()
workspaceId: string;

View File

@ -2,7 +2,9 @@ import {
Check,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@ -11,17 +13,12 @@ import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-sche
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
export enum ServerlessFunctionSyncStatus {
NOT_READY = 'NOT_READY',
BUILDING = 'BUILDING',
READY = 'READY',
}
export enum ServerlessFunctionRuntime {
NODE18 = 'nodejs18.x',
}
@Entity('serverlessFunction')
@Index('IDX_SERVERLESS_FUNCTION_ID_DELETED_AT', ['id', 'deletedAt'])
export class ServerlessFunctionEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@ -51,14 +48,6 @@ export class ServerlessFunctionEntity {
@Column({ nullable: true })
layerVersion: number;
@Column({
nullable: false,
default: ServerlessFunctionSyncStatus.NOT_READY,
type: 'enum',
enum: ServerlessFunctionSyncStatus,
})
syncStatus: ServerlessFunctionSyncStatus;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@ -67,4 +56,7 @@ export class ServerlessFunctionEntity {
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ type: 'timestamptz' })
deletedAt?: Date;
}

View File

@ -24,6 +24,7 @@ import {
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
@ -32,6 +33,8 @@ export class ServerlessFunctionResolver {
private readonly serverlessFunctionService: ServerlessFunctionService,
@InjectRepository(FeatureFlag, 'core')
private readonly featureFlagRepository: Repository<FeatureFlag>,
@InjectRepository(ServerlessFunctionEntity, 'metadata')
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
async checkFeatureFlag(workspaceId: string) {
@ -57,9 +60,11 @@ export class ServerlessFunctionResolver {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.findOneOrFail({
id,
workspaceId,
return await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
} catch (error) {
serverlessFunctionGraphQLApiExceptionHandler(error);

View File

@ -25,10 +25,7 @@ import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.se
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
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 {
ServerlessFunctionEntity,
ServerlessFunctionSyncStatus,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
@ -51,29 +48,6 @@ export class ServerlessFunctionService {
return this.serverlessFunctionRepository.findBy(where);
}
async findOneOrFail({
workspaceId,
id,
}: {
workspaceId: string;
id: string;
}) {
const serverlessFunction =
await this.serverlessFunctionRepository.findOneBy({
id,
workspaceId,
});
if (!serverlessFunction) {
throw new ServerlessFunctionException(
`Function does not exist`,
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
);
}
return serverlessFunction;
}
async hasServerlessFunctionPublishedVersion(serverlessFunctionId: string) {
return await this.serverlessFunctionRepository.exists({
where: {
@ -88,10 +62,13 @@ export class ServerlessFunctionService {
id: string,
version: string,
): Promise<{ [filePath: string]: string } | undefined> {
const serverlessFunction = await this.findOneOrFail({
id,
workspaceId,
});
const serverlessFunction =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
try {
const folderPath = getServerlessFolder({
@ -129,10 +106,13 @@ export class ServerlessFunctionService {
): Promise<ServerlessExecuteResult> {
await this.throttleExecution(workspaceId);
const functionToExecute = await this.findOneOrFail({
id,
workspaceId,
});
const functionToExecute =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
const resultServerlessFunction = await this.serverlessService.execute(
functionToExecute,
@ -158,10 +138,13 @@ export class ServerlessFunctionService {
}
async publishOneServerlessFunction(id: string, workspaceId: string) {
const existingServerlessFunction = await this.findOneOrFail({
id,
workspaceId,
});
const existingServerlessFunction =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
if (isDefined(existingServerlessFunction.latestVersion)) {
const latestCode = await this.getServerlessFunctionSourceCode(
@ -222,19 +205,25 @@ export class ServerlessFunctionService {
async deleteOneServerlessFunction({
id,
workspaceId,
isHardDeletion = true,
softDelete = false,
}: {
id: string;
workspaceId: string;
isHardDeletion?: boolean;
softDelete?: boolean;
}) {
const existingServerlessFunction = await this.findOneOrFail({
id,
workspaceId,
});
const existingServerlessFunction =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
workspaceId,
},
withDeleted: true,
});
if (isHardDeletion) {
await this.serverlessFunctionRepository.delete(id);
if (softDelete) {
await this.serverlessFunctionRepository.softDelete({ id });
} else {
await this.serverlessFunctionRepository.delete({ id });
await this.fileStorageService.delete({
folderPath: getServerlessFolder({
serverlessFunction: existingServerlessFunction,
@ -247,14 +236,21 @@ export class ServerlessFunctionService {
return existingServerlessFunction;
}
async restoreOneServerlessFunction(id: string) {
await this.serverlessFunctionRepository.restore({ id });
}
async updateOneServerlessFunction(
serverlessFunctionInput: UpdateServerlessFunctionInput,
workspaceId: string,
) {
const existingServerlessFunction = await this.findOneOrFail({
id: serverlessFunctionInput.id,
workspaceId,
});
const existingServerlessFunction =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id: serverlessFunctionInput.id,
workspaceId,
},
});
await this.serverlessFunctionRepository.update(
existingServerlessFunction.id,
@ -316,13 +312,13 @@ export class ServerlessFunctionService {
serverlessFunctionInput: CreateServerlessFunctionInput,
workspaceId: string,
) {
const serverlessFunctionToCreate =
await this.serverlessFunctionRepository.create({
const serverlessFunctionToCreate = this.serverlessFunctionRepository.create(
{
...serverlessFunctionInput,
workspaceId,
layerVersion: LAST_LAYER_VERSION,
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
});
},
);
const createdServerlessFunction =
await this.serverlessFunctionRepository.save(serverlessFunctionToCreate);
@ -359,10 +355,13 @@ export class ServerlessFunctionService {
return;
}
const serverlessFunction = await this.findOneOrFail({
id,
workspaceId,
});
const serverlessFunction =
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
workspaceId,
},
});
await this.fileStorageService.copy({
from: {

View File

@ -1,6 +1,8 @@
import { Milliseconds } from 'cache-manager';
import { isDefined } from 'twenty-shared/utils';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type';
type AsyncFactoryCallback<T> = () => Promise<T | null>;
@ -37,10 +39,12 @@ export class PromiseMemoizer<T> {
return existingPromise;
}
// eslint-disable-next-line no-console
console.log(
`Computing new Datasource for cacheKey: ${cacheKey} out of ${this.cache.size}`,
);
if (process.env.NODE_ENV !== NodeEnvironment.TEST) {
// eslint-disable-next-line no-console
console.log(
`Computing new Datasource for cacheKey: ${cacheKey} out of ${this.cache.size}`,
);
}
const newPromise = (async () => {
try {

View File

@ -22,9 +22,10 @@ export class WorkflowDeleteManyPostQueryHook
_objectName: string,
payload: WorkflowWorkspaceEntity[],
): Promise<void> {
this.workflowCommonWorkspaceService.cleanWorkflowsSubEntities(
payload.map((workflow) => workflow.id),
authContext.workspace.id,
);
this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
workflowIds: payload.map((workflow) => workflow.id),
workspaceId: authContext.workspace.id,
operation: 'delete',
});
}
}

View File

@ -22,9 +22,10 @@ export class WorkflowDeleteOnePostQueryHook
_objectName: string,
payload: WorkflowWorkspaceEntity[],
): Promise<void> {
this.workflowCommonWorkspaceService.cleanWorkflowsSubEntities(
payload.map((workflow) => workflow.id),
authContext.workspace.id,
);
this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
workflowIds: payload.map((workflow) => workflow.id),
workspaceId: authContext.workspace.id,
operation: 'delete',
});
}
}

View File

@ -0,0 +1,29 @@
import { WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { DestroyManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@WorkspaceQueryHook('workflow.destroyMany')
export class WorkflowDestroyManyPreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: DestroyManyResolverArgs<{ id: { in: string[] } }>,
): Promise<DestroyManyResolverArgs<{ id: { in: string[] } }>> {
await this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
workflowIds: payload.filter.id.in,
workspaceId: authContext.workspace.id,
operation: 'destroy',
});
return payload;
}
}

View File

@ -0,0 +1,29 @@
import { WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { DestroyOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@WorkspaceQueryHook('workflow.destroyOne')
export class WorkflowDestroyOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: DestroyOneResolverArgs,
): Promise<DestroyOneResolverArgs> {
await this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
workflowIds: [payload.id],
workspaceId: authContext.workspace.id,
operation: 'destroy',
});
return payload;
}
}

View File

@ -29,6 +29,10 @@ import { WorkflowVersionUpdateManyPreQueryHook } from 'src/modules/workflow/comm
import { WorkflowVersionUpdateOnePreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-version-update-one.pre-query.hook';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service';
import { WorkflowRestoreOnePostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-restore-one.post-query.hook';
import { WorkflowRestoreManyPostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-restore-many.post-query.hook';
import { WorkflowDestroyOnePreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-destroy-one.pre-query.hook';
import { WorkflowDestroyManyPreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-destroy-many.pre-query.hook';
@Module({
imports: [
@ -49,6 +53,8 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
WorkflowRunUpdateManyPreQueryHook,
WorkflowRunDeleteOnePreQueryHook,
WorkflowRunDeleteManyPreQueryHook,
WorkflowRestoreOnePostQueryHook,
WorkflowRestoreManyPostQueryHook,
WorkflowVersionCreateOnePreQueryHook,
WorkflowVersionCreateManyPreQueryHook,
WorkflowVersionUpdateOnePreQueryHook,
@ -61,6 +67,8 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
WorkflowCommonWorkspaceService,
WorkflowDeleteManyPostQueryHook,
WorkflowDeleteOnePostQueryHook,
WorkflowDestroyOnePreQueryHook,
WorkflowDestroyManyPreQueryHook,
],
})
export class WorkflowQueryHookModule {}

View File

@ -0,0 +1,31 @@
import { WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@WorkspaceQueryHook({
key: 'workflow.restoreMany',
type: WorkspaceQueryHookType.POST_HOOK,
})
export class WorkflowRestoreManyPostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: WorkflowWorkspaceEntity[],
): Promise<void> {
this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
workflowIds: payload.map((workflow) => workflow.id),
workspaceId: authContext.workspace.id,
operation: 'restore',
});
}
}

View File

@ -0,0 +1,31 @@
import { WorkspacePostQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
@WorkspaceQueryHook({
key: 'workflow.restoreOne',
type: WorkspaceQueryHookType.POST_HOOK,
})
export class WorkflowRestoreOnePostQueryHook
implements WorkspacePostQueryHookInstance
{
constructor(
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: WorkflowWorkspaceEntity[],
): Promise<void> {
this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
workflowIds: payload.map((workflow) => workflow.id),
workspaceId: authContext.workspace.id,
operation: 'restore',
});
}
}

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
@ -19,6 +18,7 @@ import {
WorkflowTriggerException,
WorkflowTriggerExceptionCode,
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
export type ObjectMetadataInfo = {
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
@ -114,10 +114,15 @@ export class WorkflowCommonWorkspaceService {
};
}
async cleanWorkflowsSubEntities(
workflowIds: string[],
workspaceId: string,
): Promise<void> {
async handleWorkflowSubEntities({
workflowIds,
workspaceId,
operation,
}: {
workflowIds: string[];
workspaceId: string;
operation: 'restore' | 'delete' | 'destroy';
}): Promise<void> {
const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
'workflowVersion',
@ -133,46 +138,91 @@ export class WorkflowCommonWorkspaceService {
'workflowAutomatedTrigger',
);
workflowIds.forEach((workflowId) => {
workflowAutomatedTriggerRepository.softDelete({
workflowId,
});
for (const workflowId of workflowIds) {
switch (operation) {
case 'delete':
await workflowAutomatedTriggerRepository.softDelete({
workflowId,
});
workflowRunRepository.softDelete({
workflowId,
});
await workflowRunRepository.softDelete({
workflowId,
});
workflowVersionRepository.softDelete({
workflowId,
});
await workflowVersionRepository.softDelete({
workflowId,
});
this.deleteServerlessFunctions(
break;
case 'restore':
await workflowAutomatedTriggerRepository.restore({
workflowId,
});
await workflowRunRepository.restore({
workflowId,
});
await workflowVersionRepository.restore({
workflowId,
});
break;
}
await this.handleServerlessFunctionSubEntities({
workflowVersionRepository,
workflowId,
workspaceId,
);
});
operation,
});
}
}
private async deleteServerlessFunctions(
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>,
workflowId: string,
workspaceId: string,
) {
async handleServerlessFunctionSubEntities({
workflowVersionRepository,
workflowId,
workspaceId,
operation,
}: {
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>;
workflowId: string;
workspaceId: string;
operation: 'restore' | 'delete' | 'destroy';
}) {
const workflowVersions = await workflowVersionRepository.find({
where: {
workflowId,
},
withDeleted: true,
});
workflowVersions.forEach((workflowVersion) => {
workflowVersion.steps?.forEach(async (step) => {
if (step.type === WorkflowActionType.CODE) {
await this.serverlessFunctionService.deleteOneServerlessFunction({
id: step.settings.input.serverlessFunctionId,
workspaceId,
isHardDeletion: false,
});
switch (operation) {
case 'delete':
await this.serverlessFunctionService.deleteOneServerlessFunction({
id: step.settings.input.serverlessFunctionId,
workspaceId,
softDelete: true,
});
break;
case 'restore':
await this.serverlessFunctionService.restoreOneServerlessFunction(
step.settings.input.serverlessFunctionId,
);
break;
case 'destroy':
await this.serverlessFunctionService.deleteOneServerlessFunction({
id: step.settings.input.serverlessFunctionId,
workspaceId,
softDelete: false,
});
break;
}
}
});
});

View File

@ -375,6 +375,7 @@ export class WorkflowVersionStepWorkspaceService {
await this.serverlessFunctionService.deleteOneServerlessFunction({
id: step.settings.input.serverlessFunctionId,
workspaceId,
softDelete: false,
});
}
break;

View File

@ -12,6 +12,7 @@ import {
WorkflowVersionBatchEvent,
WorkflowVersionEventType,
} from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
describe('WorkflowStatusesUpdate', () => {
let job: WorkflowStatusesUpdateJob;
@ -73,6 +74,14 @@ describe('WorkflowStatusesUpdate', () => {
}),
},
},
{
provide: getRepositoryToken(ServerlessFunctionEntity, 'metadata'),
useValue: {
findOneOrFail: jest.fn().mockResolvedValue({
latestVersion: 'v2',
}),
},
},
],
}).compile();

View File

@ -27,6 +27,7 @@ import {
WorkflowAction,
WorkflowActionType,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export enum WorkflowVersionEventType {
CREATE = 'CREATE',
@ -75,6 +76,8 @@ export class WorkflowStatusesUpdateJob {
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(ServerlessFunctionEntity, 'metadata')
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
@Process(WorkflowStatusesUpdateJob.name)
@ -212,9 +215,11 @@ export class WorkflowStatusesUpdateJob {
}
const serverlessFunction =
await this.serverlessFunctionService.findOneOrFail({
id: step.settings.input.serverlessFunctionId,
workspaceId,
await this.serverlessFunctionRepository.findOneOrFail({
where: {
id: step.settings.input.serverlessFunctionId,
workspaceId,
},
});
const newStepSettings = { ...step.settings };

View File

@ -6,12 +6,14 @@ import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { WorkflowStatusesUpdateJob } from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
import { WorkflowVersionStatusListener } from 'src/modules/workflow/workflow-status/listeners/workflow-version-status.listener';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@Module({
imports: [
ServerlessFunctionModule,
WorkspaceEventEmitterModule,
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
TypeOrmModule.forFeature([ServerlessFunctionEntity], 'metadata'),
],
providers: [WorkflowStatusesUpdateJob, WorkflowVersionStatusListener],
})

View File

@ -12,7 +12,6 @@ describe('serverlessFunctionsResolver (e2e)', () => {
name
description
runtime
syncStatus
latestVersion
publishedVersions
createdAt
@ -44,7 +43,6 @@ describe('serverlessFunctionsResolver (e2e)', () => {
expect(serverlessFunction).toHaveProperty('name');
expect(serverlessFunction).toHaveProperty('description');
expect(serverlessFunction).toHaveProperty('runtime');
expect(serverlessFunction).toHaveProperty('syncStatus');
expect(serverlessFunction).toHaveProperty('latestVersion');
expect(serverlessFunction).toHaveProperty('publishedVersions');
expect(serverlessFunction).toHaveProperty('createdAt');

View File

@ -0,0 +1,215 @@
import request from 'supertest';
const client = request(`http://localhost:${APP_PORT}`);
const testWorkflowId = 'd6f9be23-c8e6-42b2-93f5-34ee0f97f1c7';
describe('workflowResolver', () => {
beforeAll(async () => {
const queryData = {
query: `
mutation CreateOneWorkflow {
createWorkflow(
data: {
name: "Custom Test Workflow"
id: "${testWorkflowId}"
}
) {
id
}
}
`,
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryData);
});
afterAll(async () => {
const queryData = {
query: `
mutation DestroyOneWorkflow {
destroyWorkflow(id: "${testWorkflowId}") {
id
}
}
`,
};
await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryData);
});
it('should create workflow subEntities', async () => {
const queryData = {
query: `
query FindOneWorkflow {
workflow(filter: {id: {eq: "${testWorkflowId}"}}) {
id
deletedAt
versions {
edges {
node {
id
deletedAt
steps
}
}
}
}
}
`,
};
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryData);
expect(response.status).toBe(200);
expect(response.body.errors).toBeUndefined();
const workflow = response.body.data.workflow;
expect(workflow.id).toBe(testWorkflowId);
expect(workflow.deletedAt).toBeNull();
expect(workflow.versions.edges.length).toBeGreaterThan(0);
expect(workflow.versions.edges[0].node.deletedAt).toBeNull();
});
it('should delete workflow subEntities', async () => {
const deleteQueryData = {
query: `
mutation DeleteOneWorkflow {
deleteWorkflow(id: "${testWorkflowId}") {
id
}
}
`,
};
const deleteResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(deleteQueryData);
expect(deleteResponse.status).toBe(200);
const queryData = {
query: `
query FindWorkflow {
workflow(filter: {
id: { eq: "${testWorkflowId}" },
not: { deletedAt: { is: NULL } }
}) {
id
deletedAt
}
}
`,
};
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryData);
expect(response.status).toBe(200);
expect(response.body.errors).toBeUndefined();
const workflow = response.body.data.workflow;
expect(workflow.id).toBe(testWorkflowId);
expect(workflow.deletedAt).not.toBeNull();
const queryWorkflowVersionsData = {
query: `
query FindManyWorkflowVersions {
workflowVersions(filter: {
workflowId: { eq: "${testWorkflowId}" },
not: { deletedAt: { is: NULL } }
}) {
edges {
node {
id
deletedAt
}
}
}
}
`,
};
const workflowVersionsResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryWorkflowVersionsData);
expect(workflowVersionsResponse.status).toBe(200);
expect(workflowVersionsResponse.body.errors).toBeUndefined();
const workflowVersions =
workflowVersionsResponse.body.data.workflowVersions;
expect(workflowVersions.edges.length).toBeGreaterThan(0);
expect(workflowVersions.edges[0].node.deletedAt).not.toBeNull();
});
it('should restore workflow subEntities', async () => {
const restoreQueryData = {
query: `
mutation RestoreOneWorkflow {
restoreWorkflow(id: "${testWorkflowId}") {
id
}
}
`,
};
const restoreResponse = await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(restoreQueryData);
expect(restoreResponse.status).toBe(200);
const queryData = {
query: `
query FindOneWorkflow {
workflow(filter: {id: {eq: "${testWorkflowId}"}}) {
id
deletedAt
versions {
edges {
node {
id
deletedAt
steps
}
}
}
}
}
`,
};
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(queryData);
expect(response.status).toBe(200);
expect(response.body.errors).toBeUndefined();
const workflow = response.body.data.workflow;
expect(workflow.id).toBe(testWorkflowId);
expect(workflow.deletedAt).toBeNull();
expect(workflow.versions.edges.length).toBeGreaterThan(0);
expect(workflow.versions.edges[0].node.deletedAt).toBeNull();
});
});