998 workflow restore (#12417)
Add a post hook to restore workflow sub-entities
This commit is contained in:
@ -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"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -375,6 +375,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
await this.serverlessFunctionService.deleteOneServerlessFunction({
|
||||
id: step.settings.input.serverlessFunctionId,
|
||||
workspaceId,
|
||||
softDelete: false,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user