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

@ -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: {