998 workflow restore (#12417)
Add a post hook to restore workflow sub-entities
This commit is contained in:
@ -33,7 +33,7 @@ const documents = {
|
|||||||
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
"\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument,
|
||||||
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
"\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument,
|
||||||
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
"\n query ObjectMetadataItems {\n objects(paging: { first: 1000 }) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shortcut\n isLabelSyncedWithName\n isSearchable\n duplicateCriteria\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fieldsList {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n isLabelSyncedWithName\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument,
|
||||||
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
"\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc,
|
||||||
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
"\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument,
|
||||||
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
"\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument,
|
||||||
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n logs\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
"\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n logs\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument,
|
||||||
@ -142,7 +142,7 @@ export function graphql(source: "\n query ObjectMetadataItems {\n objects(pa
|
|||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n syncStatus\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n timeoutSeconds\n latestVersion\n latestVersionInputSchema\n publishedVersions\n createdAt\n updatedAt\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1882,7 +1882,6 @@ export type ServerlessFunction = {
|
|||||||
name: Scalars['String'];
|
name: Scalars['String'];
|
||||||
publishedVersions: Array<Scalars['String']>;
|
publishedVersions: Array<Scalars['String']>;
|
||||||
runtime: Scalars['String'];
|
runtime: Scalars['String'];
|
||||||
syncStatus: ServerlessFunctionSyncStatus;
|
|
||||||
timeoutSeconds: Scalars['Float'];
|
timeoutSeconds: Scalars['Float'];
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
};
|
};
|
||||||
@ -1913,13 +1912,6 @@ export type ServerlessFunctionIdInput = {
|
|||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SyncStatus of the serverlessFunction */
|
|
||||||
export enum ServerlessFunctionSyncStatus {
|
|
||||||
BUILDING = 'BUILDING',
|
|
||||||
NOT_READY = 'NOT_READY',
|
|
||||||
READY = 'READY'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SettingPermission = {
|
export type SettingPermission = {
|
||||||
__typename?: 'SettingPermission';
|
__typename?: 'SettingPermission';
|
||||||
id: Scalars['String'];
|
id: Scalars['String'];
|
||||||
|
|||||||
@ -193,9 +193,11 @@ export const WORKFLOW_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
|
|||||||
SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
||||||
SingleRecordActionKeys.DELETE,
|
SingleRecordActionKeys.DELETE,
|
||||||
SingleRecordActionKeys.DESTROY,
|
SingleRecordActionKeys.DESTROY,
|
||||||
|
SingleRecordActionKeys.RESTORE,
|
||||||
SingleRecordActionKeys.EXPORT,
|
SingleRecordActionKeys.EXPORT,
|
||||||
MultipleRecordsActionKeys.DELETE,
|
MultipleRecordsActionKeys.DELETE,
|
||||||
MultipleRecordsActionKeys.DESTROY,
|
MultipleRecordsActionKeys.DESTROY,
|
||||||
|
MultipleRecordsActionKeys.RESTORE,
|
||||||
MultipleRecordsActionKeys.EXPORT,
|
MultipleRecordsActionKeys.EXPORT,
|
||||||
NoSelectionRecordActionKeys.SEE_DELETED_RECORDS,
|
NoSelectionRecordActionKeys.SEE_DELETED_RECORDS,
|
||||||
NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS,
|
NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
|||||||
description
|
description
|
||||||
runtime
|
runtime
|
||||||
timeoutSeconds
|
timeoutSeconds
|
||||||
syncStatus
|
|
||||||
latestVersion
|
latestVersion
|
||||||
latestVersionInputSchema
|
latestVersionInputSchema
|
||||||
publishedVersions
|
publishedVersions
|
||||||
|
|||||||
@ -36,7 +36,6 @@ const meta: Meta<PageDecoratorArgs> = {
|
|||||||
id: 'adb4bd21-7670-4c81-9f74-1fc196fe87ea',
|
id: 'adb4bd21-7670-4c81-9f74-1fc196fe87ea',
|
||||||
name: 'Serverless Function Name',
|
name: 'Serverless Function Name',
|
||||||
description: '',
|
description: '',
|
||||||
syncStatus: 'READY',
|
|
||||||
runtime: 'nodejs18.x',
|
runtime: 'nodejs18.x',
|
||||||
updatedAt: '2024-02-24T10:23:10.673Z',
|
updatedAt: '2024-02-24T10:23:10.673Z',
|
||||||
createdAt: '2024-02-24T10:23:10.673Z',
|
createdAt: '2024-02-24T10:23:10.673Z',
|
||||||
|
|||||||
@ -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 {
|
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
||||||
Field,
|
|
||||||
HideField,
|
|
||||||
ObjectType,
|
|
||||||
registerEnumType,
|
|
||||||
} from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Authorize,
|
Authorize,
|
||||||
@ -13,7 +8,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
IsEnum,
|
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsString,
|
IsString,
|
||||||
@ -22,14 +16,8 @@ import {
|
|||||||
import GraphQLJSON from 'graphql-type-json';
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
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';
|
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')
|
@ObjectType('ServerlessFunction')
|
||||||
@Authorize({
|
@Authorize({
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -75,11 +63,6 @@ export class ServerlessFunctionDTO {
|
|||||||
@Field(() => GraphQLJSON, { nullable: true })
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
latestVersionInputSchema: InputSchema;
|
latestVersionInputSchema: InputSchema;
|
||||||
|
|
||||||
@IsEnum(ServerlessFunctionSyncStatus)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@Field(() => ServerlessFunctionSyncStatus)
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus;
|
|
||||||
|
|
||||||
@HideField()
|
@HideField()
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,9 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
@ -11,17 +13,12 @@ import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-sche
|
|||||||
|
|
||||||
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
|
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
|
||||||
|
|
||||||
export enum ServerlessFunctionSyncStatus {
|
|
||||||
NOT_READY = 'NOT_READY',
|
|
||||||
BUILDING = 'BUILDING',
|
|
||||||
READY = 'READY',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ServerlessFunctionRuntime {
|
export enum ServerlessFunctionRuntime {
|
||||||
NODE18 = 'nodejs18.x',
|
NODE18 = 'nodejs18.x',
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity('serverlessFunction')
|
@Entity('serverlessFunction')
|
||||||
|
@Index('IDX_SERVERLESS_FUNCTION_ID_DELETED_AT', ['id', 'deletedAt'])
|
||||||
export class ServerlessFunctionEntity {
|
export class ServerlessFunctionEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id: string;
|
id: string;
|
||||||
@ -51,14 +48,6 @@ export class ServerlessFunctionEntity {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
layerVersion: number;
|
layerVersion: number;
|
||||||
|
|
||||||
@Column({
|
|
||||||
nullable: false,
|
|
||||||
default: ServerlessFunctionSyncStatus.NOT_READY,
|
|
||||||
type: 'enum',
|
|
||||||
enum: ServerlessFunctionSyncStatus,
|
|
||||||
})
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus;
|
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'uuid' })
|
@Column({ nullable: false, type: 'uuid' })
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
@ -67,4 +56,7 @@ export class ServerlessFunctionEntity {
|
|||||||
|
|
||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
|
deletedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import {
|
|||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
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 { 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)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -32,6 +33,8 @@ export class ServerlessFunctionResolver {
|
|||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
@InjectRepository(FeatureFlag, 'core')
|
@InjectRepository(FeatureFlag, 'core')
|
||||||
private readonly featureFlagRepository: Repository<FeatureFlag>,
|
private readonly featureFlagRepository: Repository<FeatureFlag>,
|
||||||
|
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
||||||
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async checkFeatureFlag(workspaceId: string) {
|
async checkFeatureFlag(workspaceId: string) {
|
||||||
@ -57,9 +60,11 @@ export class ServerlessFunctionResolver {
|
|||||||
try {
|
try {
|
||||||
await this.checkFeatureFlag(workspaceId);
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
|
||||||
return await this.serverlessFunctionService.findOneOrFail({
|
return await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
id,
|
where: {
|
||||||
workspaceId,
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
serverlessFunctionGraphQLApiExceptionHandler(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 { 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 { 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 { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||||
import {
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
ServerlessFunctionEntity,
|
|
||||||
ServerlessFunctionSyncStatus,
|
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
|
||||||
import {
|
import {
|
||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
@ -51,29 +48,6 @@ export class ServerlessFunctionService {
|
|||||||
return this.serverlessFunctionRepository.findBy(where);
|
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) {
|
async hasServerlessFunctionPublishedVersion(serverlessFunctionId: string) {
|
||||||
return await this.serverlessFunctionRepository.exists({
|
return await this.serverlessFunctionRepository.exists({
|
||||||
where: {
|
where: {
|
||||||
@ -88,10 +62,13 @@ export class ServerlessFunctionService {
|
|||||||
id: string,
|
id: string,
|
||||||
version: string,
|
version: string,
|
||||||
): Promise<{ [filePath: string]: string } | undefined> {
|
): Promise<{ [filePath: string]: string } | undefined> {
|
||||||
const serverlessFunction = await this.findOneOrFail({
|
const serverlessFunction =
|
||||||
id,
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
workspaceId,
|
where: {
|
||||||
});
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const folderPath = getServerlessFolder({
|
const folderPath = getServerlessFolder({
|
||||||
@ -129,10 +106,13 @@ export class ServerlessFunctionService {
|
|||||||
): Promise<ServerlessExecuteResult> {
|
): Promise<ServerlessExecuteResult> {
|
||||||
await this.throttleExecution(workspaceId);
|
await this.throttleExecution(workspaceId);
|
||||||
|
|
||||||
const functionToExecute = await this.findOneOrFail({
|
const functionToExecute =
|
||||||
id,
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
workspaceId,
|
where: {
|
||||||
});
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const resultServerlessFunction = await this.serverlessService.execute(
|
const resultServerlessFunction = await this.serverlessService.execute(
|
||||||
functionToExecute,
|
functionToExecute,
|
||||||
@ -158,10 +138,13 @@ export class ServerlessFunctionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async publishOneServerlessFunction(id: string, workspaceId: string) {
|
async publishOneServerlessFunction(id: string, workspaceId: string) {
|
||||||
const existingServerlessFunction = await this.findOneOrFail({
|
const existingServerlessFunction =
|
||||||
id,
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
workspaceId,
|
where: {
|
||||||
});
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (isDefined(existingServerlessFunction.latestVersion)) {
|
if (isDefined(existingServerlessFunction.latestVersion)) {
|
||||||
const latestCode = await this.getServerlessFunctionSourceCode(
|
const latestCode = await this.getServerlessFunctionSourceCode(
|
||||||
@ -222,19 +205,25 @@ export class ServerlessFunctionService {
|
|||||||
async deleteOneServerlessFunction({
|
async deleteOneServerlessFunction({
|
||||||
id,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
isHardDeletion = true,
|
softDelete = false,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
isHardDeletion?: boolean;
|
softDelete?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const existingServerlessFunction = await this.findOneOrFail({
|
const existingServerlessFunction =
|
||||||
id,
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
workspaceId,
|
where: {
|
||||||
});
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
withDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
if (isHardDeletion) {
|
if (softDelete) {
|
||||||
await this.serverlessFunctionRepository.delete(id);
|
await this.serverlessFunctionRepository.softDelete({ id });
|
||||||
|
} else {
|
||||||
|
await this.serverlessFunctionRepository.delete({ id });
|
||||||
await this.fileStorageService.delete({
|
await this.fileStorageService.delete({
|
||||||
folderPath: getServerlessFolder({
|
folderPath: getServerlessFolder({
|
||||||
serverlessFunction: existingServerlessFunction,
|
serverlessFunction: existingServerlessFunction,
|
||||||
@ -247,14 +236,21 @@ export class ServerlessFunctionService {
|
|||||||
return existingServerlessFunction;
|
return existingServerlessFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async restoreOneServerlessFunction(id: string) {
|
||||||
|
await this.serverlessFunctionRepository.restore({ id });
|
||||||
|
}
|
||||||
|
|
||||||
async updateOneServerlessFunction(
|
async updateOneServerlessFunction(
|
||||||
serverlessFunctionInput: UpdateServerlessFunctionInput,
|
serverlessFunctionInput: UpdateServerlessFunctionInput,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const existingServerlessFunction = await this.findOneOrFail({
|
const existingServerlessFunction =
|
||||||
id: serverlessFunctionInput.id,
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
workspaceId,
|
where: {
|
||||||
});
|
id: serverlessFunctionInput.id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await this.serverlessFunctionRepository.update(
|
await this.serverlessFunctionRepository.update(
|
||||||
existingServerlessFunction.id,
|
existingServerlessFunction.id,
|
||||||
@ -316,13 +312,13 @@ export class ServerlessFunctionService {
|
|||||||
serverlessFunctionInput: CreateServerlessFunctionInput,
|
serverlessFunctionInput: CreateServerlessFunctionInput,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
const serverlessFunctionToCreate =
|
const serverlessFunctionToCreate = this.serverlessFunctionRepository.create(
|
||||||
await this.serverlessFunctionRepository.create({
|
{
|
||||||
...serverlessFunctionInput,
|
...serverlessFunctionInput,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
layerVersion: LAST_LAYER_VERSION,
|
layerVersion: LAST_LAYER_VERSION,
|
||||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const createdServerlessFunction =
|
const createdServerlessFunction =
|
||||||
await this.serverlessFunctionRepository.save(serverlessFunctionToCreate);
|
await this.serverlessFunctionRepository.save(serverlessFunctionToCreate);
|
||||||
@ -359,10 +355,13 @@ export class ServerlessFunctionService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverlessFunction = await this.findOneOrFail({
|
const serverlessFunction =
|
||||||
id,
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
workspaceId,
|
where: {
|
||||||
});
|
id,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await this.fileStorageService.copy({
|
await this.fileStorageService.copy({
|
||||||
from: {
|
from: {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Milliseconds } from 'cache-manager';
|
import { Milliseconds } from 'cache-manager';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
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';
|
import { CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type';
|
||||||
|
|
||||||
type AsyncFactoryCallback<T> = () => Promise<T | null>;
|
type AsyncFactoryCallback<T> = () => Promise<T | null>;
|
||||||
@ -37,10 +39,12 @@ export class PromiseMemoizer<T> {
|
|||||||
return existingPromise;
|
return existingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
if (process.env.NODE_ENV !== NodeEnvironment.TEST) {
|
||||||
console.log(
|
// eslint-disable-next-line no-console
|
||||||
`Computing new Datasource for cacheKey: ${cacheKey} out of ${this.cache.size}`,
|
console.log(
|
||||||
);
|
`Computing new Datasource for cacheKey: ${cacheKey} out of ${this.cache.size}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const newPromise = (async () => {
|
const newPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -22,9 +22,10 @@ export class WorkflowDeleteManyPostQueryHook
|
|||||||
_objectName: string,
|
_objectName: string,
|
||||||
payload: WorkflowWorkspaceEntity[],
|
payload: WorkflowWorkspaceEntity[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.workflowCommonWorkspaceService.cleanWorkflowsSubEntities(
|
this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
|
||||||
payload.map((workflow) => workflow.id),
|
workflowIds: payload.map((workflow) => workflow.id),
|
||||||
authContext.workspace.id,
|
workspaceId: authContext.workspace.id,
|
||||||
);
|
operation: 'delete',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,9 +22,10 @@ export class WorkflowDeleteOnePostQueryHook
|
|||||||
_objectName: string,
|
_objectName: string,
|
||||||
payload: WorkflowWorkspaceEntity[],
|
payload: WorkflowWorkspaceEntity[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.workflowCommonWorkspaceService.cleanWorkflowsSubEntities(
|
this.workflowCommonWorkspaceService.handleWorkflowSubEntities({
|
||||||
payload.map((workflow) => workflow.id),
|
workflowIds: payload.map((workflow) => workflow.id),
|
||||||
authContext.workspace.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 { 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 { 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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -49,6 +53,8 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
|
|||||||
WorkflowRunUpdateManyPreQueryHook,
|
WorkflowRunUpdateManyPreQueryHook,
|
||||||
WorkflowRunDeleteOnePreQueryHook,
|
WorkflowRunDeleteOnePreQueryHook,
|
||||||
WorkflowRunDeleteManyPreQueryHook,
|
WorkflowRunDeleteManyPreQueryHook,
|
||||||
|
WorkflowRestoreOnePostQueryHook,
|
||||||
|
WorkflowRestoreManyPostQueryHook,
|
||||||
WorkflowVersionCreateOnePreQueryHook,
|
WorkflowVersionCreateOnePreQueryHook,
|
||||||
WorkflowVersionCreateManyPreQueryHook,
|
WorkflowVersionCreateManyPreQueryHook,
|
||||||
WorkflowVersionUpdateOnePreQueryHook,
|
WorkflowVersionUpdateOnePreQueryHook,
|
||||||
@ -61,6 +67,8 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
|
|||||||
WorkflowCommonWorkspaceService,
|
WorkflowCommonWorkspaceService,
|
||||||
WorkflowDeleteManyPostQueryHook,
|
WorkflowDeleteManyPostQueryHook,
|
||||||
WorkflowDeleteOnePostQueryHook,
|
WorkflowDeleteOnePostQueryHook,
|
||||||
|
WorkflowDestroyOnePreQueryHook,
|
||||||
|
WorkflowDestroyManyPreQueryHook,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class WorkflowQueryHookModule {}
|
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 { 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 { 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 { 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';
|
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||||
@ -19,6 +18,7 @@ import {
|
|||||||
WorkflowTriggerException,
|
WorkflowTriggerException,
|
||||||
WorkflowTriggerExceptionCode,
|
WorkflowTriggerExceptionCode,
|
||||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
} 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 = {
|
export type ObjectMetadataInfo = {
|
||||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||||
@ -114,10 +114,15 @@ export class WorkflowCommonWorkspaceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanWorkflowsSubEntities(
|
async handleWorkflowSubEntities({
|
||||||
workflowIds: string[],
|
workflowIds,
|
||||||
workspaceId: string,
|
workspaceId,
|
||||||
): Promise<void> {
|
operation,
|
||||||
|
}: {
|
||||||
|
workflowIds: string[];
|
||||||
|
workspaceId: string;
|
||||||
|
operation: 'restore' | 'delete' | 'destroy';
|
||||||
|
}): Promise<void> {
|
||||||
const workflowVersionRepository =
|
const workflowVersionRepository =
|
||||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||||
'workflowVersion',
|
'workflowVersion',
|
||||||
@ -133,46 +138,91 @@ export class WorkflowCommonWorkspaceService {
|
|||||||
'workflowAutomatedTrigger',
|
'workflowAutomatedTrigger',
|
||||||
);
|
);
|
||||||
|
|
||||||
workflowIds.forEach((workflowId) => {
|
for (const workflowId of workflowIds) {
|
||||||
workflowAutomatedTriggerRepository.softDelete({
|
switch (operation) {
|
||||||
workflowId,
|
case 'delete':
|
||||||
});
|
await workflowAutomatedTriggerRepository.softDelete({
|
||||||
|
workflowId,
|
||||||
|
});
|
||||||
|
|
||||||
workflowRunRepository.softDelete({
|
await workflowRunRepository.softDelete({
|
||||||
workflowId,
|
workflowId,
|
||||||
});
|
});
|
||||||
|
|
||||||
workflowVersionRepository.softDelete({
|
await workflowVersionRepository.softDelete({
|
||||||
workflowId,
|
workflowId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.deleteServerlessFunctions(
|
break;
|
||||||
|
case 'restore':
|
||||||
|
await workflowAutomatedTriggerRepository.restore({
|
||||||
|
workflowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await workflowRunRepository.restore({
|
||||||
|
workflowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await workflowVersionRepository.restore({
|
||||||
|
workflowId,
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.handleServerlessFunctionSubEntities({
|
||||||
workflowVersionRepository,
|
workflowVersionRepository,
|
||||||
workflowId,
|
workflowId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
operation,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteServerlessFunctions(
|
async handleServerlessFunctionSubEntities({
|
||||||
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>,
|
workflowVersionRepository,
|
||||||
workflowId: string,
|
workflowId,
|
||||||
workspaceId: string,
|
workspaceId,
|
||||||
) {
|
operation,
|
||||||
|
}: {
|
||||||
|
workflowVersionRepository: WorkspaceRepository<WorkflowVersionWorkspaceEntity>;
|
||||||
|
|
||||||
|
workflowId: string;
|
||||||
|
|
||||||
|
workspaceId: string;
|
||||||
|
operation: 'restore' | 'delete' | 'destroy';
|
||||||
|
}) {
|
||||||
const workflowVersions = await workflowVersionRepository.find({
|
const workflowVersions = await workflowVersionRepository.find({
|
||||||
where: {
|
where: {
|
||||||
workflowId,
|
workflowId,
|
||||||
},
|
},
|
||||||
|
withDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
workflowVersions.forEach((workflowVersion) => {
|
workflowVersions.forEach((workflowVersion) => {
|
||||||
workflowVersion.steps?.forEach(async (step) => {
|
workflowVersion.steps?.forEach(async (step) => {
|
||||||
if (step.type === WorkflowActionType.CODE) {
|
if (step.type === WorkflowActionType.CODE) {
|
||||||
await this.serverlessFunctionService.deleteOneServerlessFunction({
|
switch (operation) {
|
||||||
id: step.settings.input.serverlessFunctionId,
|
case 'delete':
|
||||||
workspaceId,
|
await this.serverlessFunctionService.deleteOneServerlessFunction({
|
||||||
isHardDeletion: false,
|
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({
|
await this.serverlessFunctionService.deleteOneServerlessFunction({
|
||||||
id: step.settings.input.serverlessFunctionId,
|
id: step.settings.input.serverlessFunctionId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
softDelete: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
WorkflowVersionBatchEvent,
|
WorkflowVersionBatchEvent,
|
||||||
WorkflowVersionEventType,
|
WorkflowVersionEventType,
|
||||||
} from 'src/modules/workflow/workflow-status/jobs/workflow-statuses-update.job';
|
} 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', () => {
|
describe('WorkflowStatusesUpdate', () => {
|
||||||
let job: WorkflowStatusesUpdateJob;
|
let job: WorkflowStatusesUpdateJob;
|
||||||
@ -73,6 +74,14 @@ describe('WorkflowStatusesUpdate', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(ServerlessFunctionEntity, 'metadata'),
|
||||||
|
useValue: {
|
||||||
|
findOneOrFail: jest.fn().mockResolvedValue({
|
||||||
|
latestVersion: 'v2',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {
|
|||||||
WorkflowAction,
|
WorkflowAction,
|
||||||
WorkflowActionType,
|
WorkflowActionType,
|
||||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
} 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 {
|
export enum WorkflowVersionEventType {
|
||||||
CREATE = 'CREATE',
|
CREATE = 'CREATE',
|
||||||
@ -75,6 +76,8 @@ export class WorkflowStatusesUpdateJob {
|
|||||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||||
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||||
|
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
||||||
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(WorkflowStatusesUpdateJob.name)
|
@Process(WorkflowStatusesUpdateJob.name)
|
||||||
@ -212,9 +215,11 @@ export class WorkflowStatusesUpdateJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serverlessFunction =
|
const serverlessFunction =
|
||||||
await this.serverlessFunctionService.findOneOrFail({
|
await this.serverlessFunctionRepository.findOneOrFail({
|
||||||
id: step.settings.input.serverlessFunctionId,
|
where: {
|
||||||
workspaceId,
|
id: step.settings.input.serverlessFunctionId,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const newStepSettings = { ...step.settings };
|
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 { 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 { 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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ServerlessFunctionModule,
|
ServerlessFunctionModule,
|
||||||
WorkspaceEventEmitterModule,
|
WorkspaceEventEmitterModule,
|
||||||
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
|
||||||
|
TypeOrmModule.forFeature([ServerlessFunctionEntity], 'metadata'),
|
||||||
],
|
],
|
||||||
providers: [WorkflowStatusesUpdateJob, WorkflowVersionStatusListener],
|
providers: [WorkflowStatusesUpdateJob, WorkflowVersionStatusListener],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -12,7 +12,6 @@ describe('serverlessFunctionsResolver (e2e)', () => {
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
runtime
|
runtime
|
||||||
syncStatus
|
|
||||||
latestVersion
|
latestVersion
|
||||||
publishedVersions
|
publishedVersions
|
||||||
createdAt
|
createdAt
|
||||||
@ -44,7 +43,6 @@ describe('serverlessFunctionsResolver (e2e)', () => {
|
|||||||
expect(serverlessFunction).toHaveProperty('name');
|
expect(serverlessFunction).toHaveProperty('name');
|
||||||
expect(serverlessFunction).toHaveProperty('description');
|
expect(serverlessFunction).toHaveProperty('description');
|
||||||
expect(serverlessFunction).toHaveProperty('runtime');
|
expect(serverlessFunction).toHaveProperty('runtime');
|
||||||
expect(serverlessFunction).toHaveProperty('syncStatus');
|
|
||||||
expect(serverlessFunction).toHaveProperty('latestVersion');
|
expect(serverlessFunction).toHaveProperty('latestVersion');
|
||||||
expect(serverlessFunction).toHaveProperty('publishedVersions');
|
expect(serverlessFunction).toHaveProperty('publishedVersions');
|
||||||
expect(serverlessFunction).toHaveProperty('createdAt');
|
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