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

@ -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],
})