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

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

View File

@ -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'];

View File

@ -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,

View File

@ -7,7 +7,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql`
description description
runtime runtime
timeoutSeconds timeoutSeconds
syncStatus
latestVersion latestVersion
latestVersionInputSchema latestVersionInputSchema
publishedVersions publishedVersions

View File

@ -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',

View File

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

View File

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

View File

@ -1,9 +1,4 @@
import { 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;

View File

@ -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;
} }

View File

@ -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);

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,6 +29,10 @@ import { WorkflowVersionUpdateManyPreQueryHook } from 'src/modules/workflow/comm
import { WorkflowVersionUpdateOnePreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-version-update-one.pre-query.hook'; import { 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 {}

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { 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;
}
} }
}); });
}); });

View File

@ -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;

View File

@ -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();

View File

@ -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 };

View File

@ -6,12 +6,14 @@ import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; import { 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],
}) })

View File

@ -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');

View File

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